Files
linux-scripts/cisa-kev-monitor.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
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.
2026-05-25 03:31:08 +02:00

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 "$@"