#!/usr/bin/env bash

##########################################################################################
#### cisa-kev-monitor.sh — Monitor CISA Known Exploited Vulnerabilities catalog       ####
#### Polls the KEV JSON feed, detects new entries, alerts via email/Slack/Telegram    ####
#### Requires: bash 4+, curl, jq                                                      ####
####                                                                                  ####
#### Author: Phil Connor                                                              ####
#### Contact: contact@mylinux.work                                                    ####
#### License: MIT                                                                     ####
#### Version 1.00                                                                     ####
####                                                                                  ####
#### Usage:                                                                           ####
####   ./cisa-kev-monitor.sh                                                          ####
####   ./cisa-kev-monitor.sh --filter linux,kernel                                    ####
####   ./cisa-kev-monitor.sh --telegram --filter linux                                ####
####                                                                                  ####
#### See --help for all options.                                                      ####
##########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
KEV_URL="https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
STATE_DIR="${KEV_STATE_DIR:-${HOME:-/tmp}/.cisa-kev-monitor}"
STATE_FILE="$STATE_DIR/known-cves.txt"
FILTER_KEYWORDS="${KEV_FILTER:-}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"

# Notification channels
SMTP_TO="${KEV_SMTP_TO:-}"
SMTP_FROM="${KEV_SMTP_FROM:-cisa-kev-monitor@$(hostname -f 2>/dev/null || echo localhost)}"
SLACK_WEBHOOK="${KEV_SLACK_WEBHOOK:-}"
TELEGRAM_BOT_TOKEN="${KEV_TELEGRAM_BOT_TOKEN:-}"
TELEGRAM_CHAT_ID="${KEV_TELEGRAM_CHAT_ID:-}"

# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
NEW_CVES=()
NEW_COUNT=0
TOTAL_COUNT=0
START_TIME=""

# ── Colors ────────────────────────────────────────────────────────────
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" DIM="" RESET=""
setup_colors() {
    if [[ "$COLOR" == "never" ]]; then return; fi
    if [[ "$COLOR" == "auto" && ! -t 1 ]]; then return; fi
    RED="\033[0;31m"
    GREEN="\033[0;32m"
    YELLOW="\033[0;33m"
    BLUE="\033[0;34m"
    BOLD="\033[1m"
    DIM="\033[2m"
    RESET="\033[0m"
}

# ── Helpers ───────────────────────────────────────────────────────────
die()         { printf "%b\n" "${RED}[ERROR]${RESET} $*" >&2; exit 1; }
log_info()    { printf "%b\n" "${GREEN}[INFO]${RESET} $*"; }
log_warn()    { printf "%b\n" "${YELLOW}[WARN]${RESET} $*"; }
log_verbose() { [[ "$VERBOSE" == "true" ]] && printf "%b\n" "${DIM}[DEBUG]${RESET} $*" || true; }

usage() {
    cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS]

Monitor the CISA Known Exploited Vulnerabilities (KEV) catalog for new entries.
On first run, initializes the state file. Subsequent runs detect and alert on new CVEs.

Options:
  --filter KEYWORDS      Comma-separated keywords to match (e.g., linux,kernel,apache)
                         Matches against vendor, product, and description fields
                         Without --filter, all new KEV entries trigger alerts
  --email ADDRESS        Send alerts via email (requires sendmail/msmtp)
  --slack WEBHOOK_URL    Send alerts to Slack webhook
  --telegram             Send alerts via Telegram (requires KEV_TELEGRAM_BOT_TOKEN
                         and KEV_TELEGRAM_CHAT_ID env vars)
  --list                 List all current KEV entries matching the filter and exit
  --list-new DAYS        List entries added in the last N days
  --stats                Show KEV catalog statistics and exit
  --state-dir DIR        State directory (default: ~/.cisa-kev-monitor)
  --reset                Delete state file and re-initialize
  --dry-run              Show what would be alerted without sending notifications
  --verbose              Debug output
  --no-color             Disable colored output
  --help                 Show this help

Environment Variables:
  KEV_STATE_DIR          State directory (default: ~/.cisa-kev-monitor)
  KEV_FILTER             Default filter keywords (comma-separated)
  KEV_SMTP_TO            Default email recipient
  KEV_SMTP_FROM          Email sender address
  KEV_SLACK_WEBHOOK      Default Slack webhook URL
  KEV_TELEGRAM_BOT_TOKEN Telegram bot token
  KEV_TELEGRAM_CHAT_ID   Telegram chat ID

Examples:
  $SCRIPT_NAME                                    # Check for any new KEV entries
  $SCRIPT_NAME --filter linux,kernel              # Only alert on Linux/kernel CVEs
  $SCRIPT_NAME --filter linux --telegram          # Alert via Telegram for Linux CVEs
  $SCRIPT_NAME --filter windows,microsoft --email ops@example.com
  $SCRIPT_NAME --list-new 7                       # Show entries from last 7 days
  $SCRIPT_NAME --stats                            # Show catalog statistics

Cron example (check every 6 hours):
  0 */6 * * * /opt/scripts/cisa-kev-monitor.sh --filter linux,kernel --telegram --no-color 2>&1 | logger -t kev-monitor
EOF
    exit 0
}

# ── Dependency Check ──────────────────────────────────────────────────
check_deps() {
    local missing=()
    for cmd in curl jq; do
        if ! command -v "$cmd" &>/dev/null; then
            missing+=("$cmd")
        fi
    done
    if [[ ${#missing[@]} -gt 0 ]]; then
        die "Missing required commands: ${missing[*]}"
    fi
}

# ── Argument Parsing ──────────────────────────────────────────────────
DRY_RUN="false"
LIST_MODE="false"
LIST_NEW_DAYS=""
STATS_MODE="false"
RESET_MODE="false"
NOTIFY_EMAIL="false"
NOTIFY_SLACK="false"
NOTIFY_TELEGRAM="false"

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --filter)       FILTER_KEYWORDS="${2:?--filter requires keywords}"; shift 2 ;;
            --email)        SMTP_TO="${2:?--email requires an address}"; NOTIFY_EMAIL="true"; shift 2 ;;
            --slack)        SLACK_WEBHOOK="${2:?--slack requires a webhook URL}"; NOTIFY_SLACK="true"; shift 2 ;;
            --telegram)     NOTIFY_TELEGRAM="true"; shift ;;
            --list)         LIST_MODE="true"; shift ;;
            --list-new)     LIST_NEW_DAYS="${2:?--list-new requires days}"; shift 2 ;;
            --stats)        STATS_MODE="true"; shift ;;
            --state-dir)    STATE_DIR="${2:?--state-dir requires a path}"; STATE_FILE="$STATE_DIR/known-cves.txt"; shift 2 ;;
            --reset)        RESET_MODE="true"; shift ;;
            --dry-run)      DRY_RUN="true"; shift ;;
            --verbose)      VERBOSE="true"; shift ;;
            --no-color)     COLOR="never"; shift ;;
            --help)         usage ;;
            *)              die "Unknown option: $1" ;;
        esac
    done

    if [[ "$NOTIFY_TELEGRAM" == "true" ]]; then
        [[ -z "$TELEGRAM_BOT_TOKEN" ]] && die "KEV_TELEGRAM_BOT_TOKEN not set"
        [[ -z "$TELEGRAM_CHAT_ID" ]] && die "KEV_TELEGRAM_CHAT_ID not set"
    fi
}

# ── Fetch KEV Feed ────────────────────────────────────────────────────
fetch_kev() {
    log_verbose "Fetching KEV catalog from CISA..."
    local tmpfile
    tmpfile=$(mktemp)

    if ! curl -sS --max-time 30 --retry 2 -o "$tmpfile" "$KEV_URL" 2>/dev/null; then
        rm -f "$tmpfile"
        die "Failed to fetch KEV catalog from $KEV_URL"
    fi

    # Validate JSON
    if ! jq empty "$tmpfile" 2>/dev/null; then
        rm -f "$tmpfile"
        die "Invalid JSON received from KEV feed"
    fi

    echo "$tmpfile"
}

# ── Filter Entries ────────────────────────────────────────────────────
filter_entries() {
    local json_file="$1"

    if [[ -z "$FILTER_KEYWORDS" ]]; then
        jq -r '.vulnerabilities[]' "$json_file"
        return
    fi

    # Build jq filter from comma-separated keywords
    local jq_filter=""
    IFS=',' read -ra keywords <<< "$FILTER_KEYWORDS"
    for kw in "${keywords[@]}"; do
        kw=$(echo "$kw" | xargs)  # trim whitespace
        kw_lower=$(echo "$kw" | tr '[:upper:]' '[:lower:]')
        if [[ -n "$jq_filter" ]]; then
            jq_filter="$jq_filter or"
        fi
        jq_filter="$jq_filter ((.vendorProject // \"\" | ascii_downcase | contains(\"$kw_lower\")) or (.product // \"\" | ascii_downcase | contains(\"$kw_lower\")) or (.shortDescription // \"\" | ascii_downcase | contains(\"$kw_lower\")) or (.vulnerabilityName // \"\" | ascii_downcase | contains(\"$kw_lower\")))"
    done

    jq -r ".vulnerabilities[] | select($jq_filter)" "$json_file"
}

# ── Initialize State ─────────────────────────────────────────────────
init_state() {
    mkdir -p "$STATE_DIR"

    if [[ "$RESET_MODE" == "true" && -f "$STATE_FILE" ]]; then
        rm -f "$STATE_FILE"
        log_info "State file reset"
    fi

    if [[ ! -f "$STATE_FILE" ]]; then
        log_info "First run — initializing state file"
        return 1
    fi
    return 0
}

# ── Format CVE for Display ────────────────────────────────────────────
format_cve_text() {
    local cve="$1"
    local cve_id vendor product name date_added desc due_date ransomware

    cve_id=$(echo "$cve" | jq -r '.cveID')
    vendor=$(echo "$cve" | jq -r '.vendorProject')
    product=$(echo "$cve" | jq -r '.product')
    name=$(echo "$cve" | jq -r '.vulnerabilityName')
    date_added=$(echo "$cve" | jq -r '.dateAdded')
    desc=$(echo "$cve" | jq -r '.shortDescription')
    due_date=$(echo "$cve" | jq -r '.dueDate')
    ransomware=$(echo "$cve" | jq -r '.knownRansomwareCampaignUse')

    printf "%b%s%b — %s\n" "$BOLD" "$cve_id" "$RESET" "$name"
    printf "  Vendor:     %s / %s\n" "$vendor" "$product"
    printf "  Added:      %s\n" "$date_added"
    printf "  Due:        %s\n" "$due_date"
    printf "  Ransomware: %s\n" "$ransomware"
    printf "  %s\n" "$desc"
    printf "  NVD:        https://nvd.nist.gov/vuln/detail/%s\n" "$cve_id"
    echo ""
}

# ── Format CVE for Notifications ──────────────────────────────────────
format_cve_plain() {
    local cve="$1"
    local cve_id vendor product name date_added desc

    cve_id=$(echo "$cve" | jq -r '.cveID')
    vendor=$(echo "$cve" | jq -r '.vendorProject')
    product=$(echo "$cve" | jq -r '.product')
    name=$(echo "$cve" | jq -r '.vulnerabilityName')
    date_added=$(echo "$cve" | jq -r '.dateAdded')
    desc=$(echo "$cve" | jq -r '.shortDescription')

    echo "$cve_id — $name"
    echo "Vendor: $vendor / $product"
    echo "Added: $date_added"
    echo "$desc"
    echo "https://nvd.nist.gov/vuln/detail/$cve_id"
    echo ""
}

format_cve_telegram() {
    local cve="$1"
    local cve_id vendor product name date_added desc ransomware

    cve_id=$(echo "$cve" | jq -r '.cveID')
    vendor=$(echo "$cve" | jq -r '.vendorProject')
    product=$(echo "$cve" | jq -r '.product')
    name=$(echo "$cve" | jq -r '.vulnerabilityName')
    date_added=$(echo "$cve" | jq -r '.dateAdded')
    desc=$(echo "$cve" | jq -r '.shortDescription' | head -c 200)
    ransomware=$(echo "$cve" | jq -r '.knownRansomwareCampaignUse')

    local emoji="🔴"
    [[ "$ransomware" == "Known" ]] && emoji="🔴🛑"

    echo "${emoji} <b>${cve_id}</b> — ${name}"
    echo "📦 ${vendor} / ${product}"
    echo "📅 Added: ${date_added}"
    [[ "$ransomware" == "Known" ]] && echo "💀 Known ransomware use"
    echo ""
    echo "${desc}..."
    echo ""
    echo "🔗 <a href=\"https://nvd.nist.gov/vuln/detail/${cve_id}\">NVD</a>"
}

# ── Notification: Email ───────────────────────────────────────────────
send_email() {
    local subject="$1"
    local body="$2"

    if ! command -v sendmail &>/dev/null && ! command -v msmtp &>/dev/null; then
        log_warn "No sendmail or msmtp found — skipping email"
        return
    fi

    local mailer="sendmail"
    command -v msmtp &>/dev/null && mailer="msmtp"

    {
        echo "From: $SMTP_FROM"
        echo "To: $SMTP_TO"
        echo "Subject: $subject"
        echo "Content-Type: text/plain; charset=utf-8"
        echo ""
        echo "$body"
    } | "$mailer" -t "$SMTP_TO"

    log_verbose "Email sent to $SMTP_TO"
}

# ── Notification: Slack ───────────────────────────────────────────────
send_slack() {
    local text="$1"

    # Truncate for Slack's 3000 char limit
    text=$(echo "$text" | head -c 2900)

    local payload
    payload=$(jq -n --arg text "$text" '{text: $text}')

    curl -sS --max-time 10 -X POST \
        -H "Content-Type: application/json" \
        -d "$payload" \
        "$SLACK_WEBHOOK" >/dev/null 2>&1

    log_verbose "Slack notification sent"
}

# ── Notification: Telegram ────────────────────────────────────────────
send_telegram() {
    local text="$1"

    # Telegram message limit is 4096 chars
    text=$(echo "$text" | head -c 4000)

    curl -sS --max-time 10 -X POST \
        "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
        -d "chat_id=${TELEGRAM_CHAT_ID}" \
        -d "parse_mode=HTML" \
        -d "disable_web_page_preview=true" \
        --data-urlencode "text=$text" >/dev/null 2>&1

    log_verbose "Telegram notification sent"
}

# ── Notify All Channels ──────────────────────────────────────────────
notify() {
    local count=${#NEW_CVES[@]}
    local filter_label=""
    [[ -n "$FILTER_KEYWORDS" ]] && filter_label=" (filter: $FILTER_KEYWORDS)"

    # Build plain text body
    local plain_body=""
    plain_body+="CISA KEV Monitor — $count new CVE(s) detected${filter_label}"
    plain_body+=$'\n\n'
    for cve_json in "${NEW_CVES[@]}"; do
        plain_body+=$(format_cve_plain "$cve_json")
        plain_body+=$'\n'
    done

    # Build Telegram body
    local tg_body=""
    tg_body+="🚨 <b>CISA KEV — ${count} new CVE(s)</b>${filter_label}"
    tg_body+=$'\n\n'
    for cve_json in "${NEW_CVES[@]}"; do
        tg_body+=$(format_cve_telegram "$cve_json")
        tg_body+=$'\n'
    done

    if [[ "$DRY_RUN" == "true" ]]; then
        log_warn "DRY-RUN — would send notifications to:"
        [[ "$NOTIFY_EMAIL" == "true" ]] && echo "  Email: $SMTP_TO"
        [[ "$NOTIFY_SLACK" == "true" ]] && echo "  Slack: (webhook configured)"
        [[ "$NOTIFY_TELEGRAM" == "true" ]] && echo "  Telegram: chat $TELEGRAM_CHAT_ID"
        return
    fi

    [[ "$NOTIFY_EMAIL" == "true" ]] && send_email "CISA KEV: $count new CVE(s)${filter_label}" "$plain_body"
    [[ "$NOTIFY_SLACK" == "true" ]] && send_slack "$plain_body"
    [[ "$NOTIFY_TELEGRAM" == "true" ]] && send_telegram "$tg_body"
}

# ── Mode: Stats ───────────────────────────────────────────────────────
run_stats() {
    local json_file
    json_file=$(fetch_kev)

    local total last_updated
    total=$(jq '.vulnerabilities | length' "$json_file")
    last_updated=$(jq -r '.catalogVersion' "$json_file")

    local last_7d last_30d
    local cutoff_7d cutoff_30d
    cutoff_7d=$(date -u -d "7 days ago" '+%Y-%m-%d' 2>/dev/null || date -u -v-7d '+%Y-%m-%d' 2>/dev/null)
    cutoff_30d=$(date -u -d "30 days ago" '+%Y-%m-%d' 2>/dev/null || date -u -v-30d '+%Y-%m-%d' 2>/dev/null)

    last_7d=$(jq --arg d "$cutoff_7d" '[.vulnerabilities[] | select(.dateAdded >= $d)] | length' "$json_file")
    last_30d=$(jq --arg d "$cutoff_30d" '[.vulnerabilities[] | select(.dateAdded >= $d)] | length' "$json_file")

    local ransomware_known
    ransomware_known=$(jq '[.vulnerabilities[] | select(.knownRansomwareCampaignUse == "Known")] | length' "$json_file")

    echo ""
    printf "%bCISA KEV Catalog Statistics%b\n" "$BOLD" "$RESET"
    echo "Catalog version: $last_updated"
    echo "Total CVEs:      $total"
    echo "Last 7 days:     $last_7d"
    echo "Last 30 days:    $last_30d"
    echo "Ransomware use:  $ransomware_known"

    if [[ -n "$FILTER_KEYWORDS" ]]; then
        local filtered
        filtered=$(filter_entries "$json_file" | jq -s 'length')
        echo "Matching filter: $filtered (keywords: $FILTER_KEYWORDS)"
    fi

    rm -f "$json_file"
}

# ── Mode: List New ────────────────────────────────────────────────────
run_list_new() {
    local days="$1"
    local json_file
    json_file=$(fetch_kev)

    local cutoff
    cutoff=$(date -u -d "$days days ago" '+%Y-%m-%d' 2>/dev/null || date -u -v-"${days}d" '+%Y-%m-%d' 2>/dev/null)

    echo ""
    printf "%bCISA KEV — entries added in the last %s days%b\n\n" "$BOLD" "$days" "$RESET"

    local count=0
    while IFS= read -r entry; do
        [[ -z "$entry" ]] && continue
        local date_added
        date_added=$(echo "$entry" | jq -r '.dateAdded')
        if [[ "$date_added" > "$cutoff" || "$date_added" == "$cutoff" ]]; then
            format_cve_text "$entry"
            count=$((count + 1))
        fi
    done < <(filter_entries "$json_file" | jq -c '.')

    log_info "$count entries found"
    rm -f "$json_file"
}

# ── Mode: List ────────────────────────────────────────────────────────
run_list() {
    local json_file
    json_file=$(fetch_kev)

    echo ""
    printf "%bCISA KEV — all matching entries%b\n" "$BOLD" "$RESET"
    [[ -n "$FILTER_KEYWORDS" ]] && echo "Filter: $FILTER_KEYWORDS"
    echo ""

    local count=0
    while IFS= read -r entry; do
        [[ -z "$entry" ]] && continue
        format_cve_text "$entry"
        count=$((count + 1))
    done < <(filter_entries "$json_file" | jq -c '.')

    log_info "$count entries"
    rm -f "$json_file"
}

# ── Mode: Monitor ─────────────────────────────────────────────────────
run_monitor() {
    local json_file
    json_file=$(fetch_kev)

    TOTAL_COUNT=$(jq '.vulnerabilities | length' "$json_file")

    # Initialize state on first run
    if ! init_state; then
        jq -r '.vulnerabilities[].cveID' "$json_file" | sort > "$STATE_FILE"
        local init_count
        init_count=$(wc -l < "$STATE_FILE")
        log_info "Initialized with $init_count CVEs. Future runs will detect new entries."
        rm -f "$json_file"
        return
    fi

    # Extract current CVE IDs
    local current_cves
    current_cves=$(mktemp)
    jq -r '.vulnerabilities[].cveID' "$json_file" | sort > "$current_cves"

    # Find new CVEs not in state file
    local new_ids
    new_ids=$(comm -13 "$STATE_FILE" "$current_cves")

    if [[ -z "$new_ids" ]]; then
        log_info "No new KEV entries (catalog: $TOTAL_COUNT CVEs)"
        rm -f "$current_cves" "$json_file"
        return
    fi

    # Collect new CVE details, applying filter
    while IFS= read -r cve_id; do
        [[ -z "$cve_id" ]] && continue

        local cve_json
        cve_json=$(jq -c --arg id "$cve_id" '.vulnerabilities[] | select(.cveID == $id)' "$json_file")

        # Apply filter if set
        if [[ -n "$FILTER_KEYWORDS" ]]; then
            local matches="false"
            IFS=',' read -ra keywords <<< "$FILTER_KEYWORDS"
            for kw in "${keywords[@]}"; do
                kw=$(echo "$kw" | xargs | tr '[:upper:]' '[:lower:]')
                if echo "$cve_json" | tr '[:upper:]' '[:lower:]' | grep -q "$kw"; then
                    matches="true"
                    break
                fi
            done
            [[ "$matches" == "false" ]] && continue
        fi

        NEW_CVES+=("$cve_json")
        format_cve_text "$cve_json"
    done <<< "$new_ids"

    NEW_COUNT=${#NEW_CVES[@]}

    # Update state file with all current CVEs
    mv "$current_cves" "$STATE_FILE"

    if [[ $NEW_COUNT -eq 0 ]]; then
        local total_new
        total_new=$(echo "$new_ids" | wc -w)
        log_info "No new entries matching filter (${total_new} new total, $TOTAL_COUNT in catalog)"
        rm -f "$json_file"
        return
    fi

    log_info "$NEW_COUNT new KEV entry/entries matching filter"

    # Send notifications
    if [[ "$NOTIFY_EMAIL" == "true" || "$NOTIFY_SLACK" == "true" || "$NOTIFY_TELEGRAM" == "true" ]]; then
        notify
    fi

    rm -f "$json_file"
}

# ── Entry Point ───────────────────────────────────────────────────────
main() {
    START_TIME=$(date +%s)
    setup_colors
    parse_args "$@"
    check_deps

    if [[ "$STATS_MODE" == "true" ]]; then
        run_stats
    elif [[ -n "$LIST_NEW_DAYS" ]]; then
        run_list_new "$LIST_NEW_DAYS"
    elif [[ "$LIST_MODE" == "true" ]]; then
        run_list
    else
        run_monitor
    fi

    local elapsed=$(( $(date +%s) - START_TIME ))
    log_verbose "Completed in ${elapsed}s"
}

main "$@"
