New Packages: - secubox-cve-triage: AI-powered CVE analysis and vulnerability management - NVD API integration for CVE data - CrowdSec CVE alert correlation - LocalAI-powered impact analysis - Approval workflow for patch recommendations - Multi-source monitoring (opkg, LXC, Docker) - luci-app-cve-triage: Dashboard with alerts, pending queue, risk score - secubox-vortex-dns: Meshed multi-dynamic subdomain delegation - Master/slave hierarchical DNS delegation - Wildcard domain management - First Peek auto-registration - Gossip-based exposure config sync - Submastering for nested hierarchies Fixes: - Webmail 401 login: config.docker.inc.php was overriding IMAP host to ssl://mail.secubox.in:993 which Docker couldn't reach - Fixed mailctl webmail configure to use socat proxy (172.17.0.1:10143) Documentation: - Added LXC cgroup:mixed fix to FAQ-TROUBLESHOOTING.md - Updated CLAUDE.md to include FAQ consultation at startup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
470 lines
12 KiB
Bash
470 lines
12 KiB
Bash
#!/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 <command> [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 <cve> Analyze specific CVE with AI
|
|
|
|
Recommendations:
|
|
list-pending List pending recommendations
|
|
approve <id> Approve recommendation
|
|
reject <id> Reject recommendation
|
|
approve-all Approve all pending
|
|
clear-pending Clear all pending
|
|
|
|
Alerts:
|
|
alerts Show active alerts
|
|
ack <id> 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 <CVE-ID>"
|
|
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 <recommendation-id>"
|
|
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 <recommendation-id> [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
|