a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
576 lines
22 KiB
Bash
576 lines
22 KiB
Bash
#!/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 "$@"
|