#!/bin/sh # SecuBox CVE Triage Agent # Copyright (C) 2026 CyberMind.fr # # AI-powered CVE analysis and vulnerability management CONFIG="cve-triage" LIB_DIR="/usr/lib/cve-triage" STATE_DIR="/var/lib/cve-triage" CACHE_DIR="/var/cache/cve-triage" LOG_TAG="cve-triage" # Source libraries . "$LIB_DIR/collector.sh" . "$LIB_DIR/analyzer.sh" . "$LIB_DIR/recommender.sh" . "$LIB_DIR/applier.sh" usage() { cat <<'EOF' Usage: cve-triage [options] Commands: run Run single triage cycle daemon Run as background daemon status Show agent status scan Scan installed packages only fetch Fetch latest CVE data analyze Analyze specific CVE with AI Recommendations: list-pending List pending recommendations approve Approve recommendation reject Reject recommendation approve-all Approve all pending clear-pending Clear all pending Alerts: alerts Show active alerts ack Acknowledge alert Reports: summary Generate security summary export Export CVE report (JSON) Configuration: /etc/config/cve-triage EOF } log_info() { logger -t "$LOG_TAG" "$*"; echo "[INFO] $*"; } log_warn() { logger -t "$LOG_TAG" -p warning "$*"; echo "[WARN] $*" >&2; } log_error() { logger -t "$LOG_TAG" -p err "$*"; echo "[ERROR] $*" >&2; } uci_get() { uci -q get "${CONFIG}.$1"; } load_config() { enabled=$(uci_get main.enabled) interval=$(uci_get main.interval) localai_url=$(uci_get main.localai_url) localai_model=$(uci_get main.localai_model) min_severity=$(uci_get main.min_severity) affected_only=$(uci_get main.affected_only) auto_apply_patches=$(uci_get main.auto_apply_patches) min_confidence=$(uci_get main.min_confidence) max_recommendations=$(uci_get main.max_recommendations) # Defaults [ -z "$interval" ] && interval=3600 [ -z "$min_severity" ] && min_severity="high" [ -z "$affected_only" ] && affected_only=1 [ -z "$min_confidence" ] && min_confidence=80 [ -z "$max_recommendations" ] && max_recommendations=10 mkdir -p "$STATE_DIR" "$CACHE_DIR" } # ============================================================================= # COMMANDS # ============================================================================= cmd_status() { load_config echo "=== CVE Triage Agent Status ===" echo "" echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")" echo "Interval: ${interval}s ($(($interval / 60)) minutes)" echo "Min Severity: $min_severity" echo "Affected Only: $([ "$affected_only" = "1" ] && echo "Yes" || echo "No")" echo "" # Check LocalAI availability if check_localai; then echo "LocalAI: ONLINE ($localai_url)" else echo "LocalAI: OFFLINE (basic analysis mode)" fi echo "" # Package counts local opkg_count=$(opkg list-installed 2>/dev/null | wc -l) local lxc_count=$(ls -d /srv/lxc/*/ 2>/dev/null | wc -l) local docker_count=$(docker ps -q 2>/dev/null | wc -l) echo "Monitored Packages:" echo " opkg: $opkg_count packages" echo " LXC containers: $lxc_count" echo " Docker containers: $docker_count" echo "" # Pending recommendations local pending=$(get_pending_count) echo "Pending Recommendations: $pending" # Active alerts local alerts=$(get_active_alerts | jsonfilter -e '@[*]' 2>/dev/null | wc -l) echo "Active Alerts: $alerts" # Last run if [ -f "$STATE_DIR/last_run" ]; then echo "" echo "Last Run: $(cat "$STATE_DIR/last_run")" fi } cmd_scan() { load_config log_info "Scanning installed packages..." local packages=$(collect_all_packages) local pkg_count=$(echo "$packages" | jsonfilter -e '@.packages[*]' 2>/dev/null | wc -l) echo "Found $pkg_count packages" echo "$packages" > "$STATE_DIR/packages.json" # Show summary echo "" echo "Package sources:" echo " opkg: $(echo "$packages" | jsonfilter -e '@.packages[*].source' 2>/dev/null | grep -c '^opkg$')" echo " LXC: $(echo "$packages" | jsonfilter -e '@.packages[*].source' 2>/dev/null | grep -c '^lxc:')" echo " Docker: $(echo "$packages" | jsonfilter -e '@.packages[*].source' 2>/dev/null | grep -c '^docker$')" } cmd_fetch() { load_config log_info "Fetching CVE data..." # Fetch NVD CVEs if [ "$(uci_get source.nvd.enabled)" = "1" ]; then local nvd_data=$(fetch_nvd_cves) local cve_count=$(echo "$nvd_data" | jsonfilter -e '@.totalResults' 2>/dev/null) echo "NVD: Found ${cve_count:-0} recent CVEs" fi # Fetch CrowdSec CVE alerts if [ "$(uci_get source.crowdsec_cve.enabled)" = "1" ]; then local cs_cves=$(collect_crowdsec_cves) local cs_count=$(echo "$cs_cves" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) echo "CrowdSec: Found $cs_count CVE-related alerts" fi } cmd_run() { load_config [ "$enabled" = "1" ] || { log_warn "CVE Triage agent is disabled" return 1 } log_info "Starting CVE triage cycle..." # 1. Collect installed packages log_info "Collecting installed packages..." local packages=$(collect_all_packages) # 2. Fetch CVE data log_info "Fetching CVE data..." local nvd_data=$(fetch_nvd_cves) local nvd_cves=$(parse_nvd_cves "$nvd_data") # 3. Also get CrowdSec CVE alerts local cs_cves=$(collect_crowdsec_cves) # 4. Match CVEs to installed packages (if affected_only=1) local matched_cves="$nvd_cves" if [ "$affected_only" = "1" ]; then log_info "Matching CVEs to installed packages..." matched_cves=$(match_cves_to_packages "$nvd_cves" "$packages") fi # 5. Analyze CVEs local cve_count=$(echo "$matched_cves" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) log_info "Analyzing $cve_count CVEs..." local analyzed=$(analyze_cves_batch "$matched_cves") # 6. Generate recommendations log_info "Generating recommendations..." local recommendations=$(create_recommendations "$analyzed") save_recommendations "$recommendations" # 7. Process recommendations (queue/auto-apply) process_recommendations "$recommendations" auto_apply_recommendations "$recommendations" # 8. Generate summary local summary=$(generate_summary "$analyzed") echo "$summary" > "$STATE_DIR/last_summary.json" # Save last run timestamp date -Iseconds > "$STATE_DIR/last_run" # Report results local rec_count=$(echo "$recommendations" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) local pending=$(get_pending_count) echo "" echo "=== Triage Complete ===" echo "CVEs analyzed: $cve_count" echo "Recommendations: $rec_count" echo "Pending approval: $pending" echo "" # Show summary local risk_score=$(echo "$summary" | jsonfilter -e '@.risk_score' 2>/dev/null) local summary_text=$(echo "$summary" | jsonfilter -e '@.summary' 2>/dev/null) echo "Risk Score: ${risk_score:-N/A}/100" echo "Summary: ${summary_text:-Analysis complete}" } cmd_daemon() { load_config [ "$enabled" = "1" ] || { log_error "CVE Triage agent is disabled" exit 1 } log_info "Starting CVE Triage daemon (interval: ${interval}s)..." while true; do cmd_run 2>&1 | while read -r line; do logger -t "$LOG_TAG" "$line" done sleep "$interval" done } cmd_analyze_cve() { local cve_id="$1" [ -z "$cve_id" ] && { echo "Usage: cve-triage analyze " exit 1 } load_config log_info "Analyzing $cve_id..." # Try to fetch CVE details from NVD local nvd_url="https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=$cve_id" local nvd_data=$(wget -q -O - --timeout=30 "$nvd_url" 2>/dev/null) if [ -z "$nvd_data" ]; then log_warn "Could not fetch CVE details from NVD" return 1 fi local cve_info=$(parse_nvd_cves "$nvd_data" | jsonfilter -e '@[0]' 2>/dev/null) if [ -z "$cve_info" ]; then log_error "CVE $cve_id not found" return 1 fi local description=$(echo "$cve_info" | jsonfilter -e '@.description' 2>/dev/null) local cvss=$(echo "$cve_info" | jsonfilter -e '@.cvss' 2>/dev/null) local severity=$(echo "$cve_info" | jsonfilter -e '@.severity' 2>/dev/null) echo "=== $cve_id ===" echo "CVSS: $cvss ($severity)" echo "Description: $description" echo "" # Analyze with AI if check_localai; then log_info "Running AI analysis..." local analysis=$(analyze_cve "$cve_id" "$description" "$cvss" "") echo "=== AI Analysis ===" echo "$analysis" | jsonfilter -e '@' 2>/dev/null || echo "$analysis" else echo "LocalAI not available for detailed analysis" fi } cmd_list_pending() { load_config local pending=$(list_pending) local count=$(echo "$pending" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) echo "=== Pending Recommendations ($count) ===" echo "" echo "$pending" | jsonfilter -e '@[*]' 2>/dev/null | while read -r rec; do local id=$(echo "$rec" | jsonfilter -e '@.id' 2>/dev/null) local cve=$(echo "$rec" | jsonfilter -e '@.cve' 2>/dev/null) local severity=$(echo "$rec" | jsonfilter -e '@.severity' 2>/dev/null) local action=$(echo "$rec" | jsonfilter -e '@.action' 2>/dev/null) local pkg=$(echo "$rec" | jsonfilter -e '@.affected_package' 2>/dev/null) echo "[$severity] $cve" echo " ID: $id" echo " Action: $action" echo " Package: ${pkg:-unknown}" echo "" done } cmd_approve() { local rec_id="$1" [ -z "$rec_id" ] && { echo "Usage: cve-triage approve " exit 1 } load_config approve_recommendation "$rec_id" } cmd_reject() { local rec_id="$1" local reason="$2" [ -z "$rec_id" ] && { echo "Usage: cve-triage reject [reason]" exit 1 } load_config reject_recommendation "$rec_id" "$reason" } cmd_alerts() { load_config local alerts=$(get_active_alerts) local count=$(echo "$alerts" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) echo "=== Active Alerts ($count) ===" echo "" echo "$alerts" | jsonfilter -e '@[*]' 2>/dev/null | while read -r alert; do local id=$(echo "$alert" | jsonfilter -e '@.id' 2>/dev/null) local cve=$(echo "$alert" | jsonfilter -e '@.cve' 2>/dev/null) local severity=$(echo "$alert" | jsonfilter -e '@.severity' 2>/dev/null) local message=$(echo "$alert" | jsonfilter -e '@.message' 2>/dev/null) local created=$(echo "$alert" | jsonfilter -e '@.created' 2>/dev/null) echo "[$severity] $cve - $created" echo " ID: $id" echo " $message" echo "" done } cmd_summary() { load_config if [ -f "$STATE_DIR/last_summary.json" ]; then local summary=$(cat "$STATE_DIR/last_summary.json") local risk=$(echo "$summary" | jsonfilter -e '@.risk_score' 2>/dev/null) local text=$(echo "$summary" | jsonfilter -e '@.summary' 2>/dev/null) echo "=== Security Summary ===" echo "" echo "Risk Score: ${risk:-N/A}/100" echo "" echo "$text" else echo "No summary available. Run 'cve-triage run' first." fi } cmd_export() { load_config local export_file="/tmp/cve-report-$(date +%Y%m%d).json" { echo '{' echo '"generated":"'"$(date -Iseconds)"'",' echo '"packages":' [ -f "$STATE_DIR/packages.json" ] && cat "$STATE_DIR/packages.json" || echo '[]' echo ',' echo '"recommendations":' [ -f "$STATE_DIR/recommendations.json" ] && cat "$STATE_DIR/recommendations.json" || echo '[]' echo ',' echo '"alerts":' [ -f "$STATE_DIR/alerts.json" ] && cat "$STATE_DIR/alerts.json" || echo '[]' echo ',' echo '"summary":' [ -f "$STATE_DIR/last_summary.json" ] && cat "$STATE_DIR/last_summary.json" || echo '{}' echo '}' } > "$export_file" echo "Report exported to: $export_file" } # ============================================================================= # MAIN # ============================================================================= case "$1" in run) cmd_run ;; daemon) cmd_daemon ;; status) cmd_status ;; scan) cmd_scan ;; fetch) cmd_fetch ;; analyze) cmd_analyze_cve "$2" ;; list-pending) cmd_list_pending ;; approve) cmd_approve "$2" ;; reject) cmd_reject "$2" "$3" ;; approve-all) load_config approve_all ;; clear-pending) load_config clear_pending ;; alerts) cmd_alerts ;; ack) # TODO: Acknowledge alert echo "Not implemented yet" ;; summary) cmd_summary ;; export) cmd_export ;; -h|--help|help) usage ;; *) usage exit 1 ;; esac