#!/bin/sh # AI Insights Aggregation RPCD Handler # Unified view across all SecuBox AI agents . /usr/share/libubox/jshn.sh log_info() { logger -t ai-insights-rpcd "$*"; } # Check if a command exists cmd_exists() { command -v "$1" >/dev/null 2>&1; } # Get agent status get_agent_status() { local agent="$1" local status="offline" local alerts=0 case "$agent" in threat_analyst) pgrep -f "threat-analyst daemon" >/dev/null 2>&1 && status="online" [ -f /var/lib/threat-analyst/pending_rules.json ] && \ alerts=$(jsonfilter -i /var/lib/threat-analyst/pending_rules.json -e '@[*]' 2>/dev/null | wc -l) ;; dns_guard) pgrep -f "dnsguardctl daemon" >/dev/null 2>&1 && status="online" [ -f /var/lib/dns-guard/alerts.json ] && \ alerts=$(jsonfilter -i /var/lib/dns-guard/alerts.json -e '@[*]' 2>/dev/null | wc -l) ;; network_anomaly) pgrep -f "network-anomalyctl daemon" >/dev/null 2>&1 && status="online" [ -f /var/lib/network-anomaly/alerts.json ] && \ alerts=$(jsonfilter -i /var/lib/network-anomaly/alerts.json -e '@[*]' 2>/dev/null | wc -l) ;; cve_triage) pgrep -f "cve-triagectl daemon" >/dev/null 2>&1 && status="online" [ -f /var/lib/cve-triage/alerts.json ] && \ alerts=$(jsonfilter -i /var/lib/cve-triage/alerts.json -e '@[*]' 2>/dev/null | wc -l) ;; esac printf '{"status":"%s","alerts":%d}' "$status" "$alerts" } # Calculate security posture score (0-100) calculate_posture() { local score=100 local factors="" # Check LocalAI local localai_url=$(uci -q get localrecall.main.localai_url || echo "http://127.0.0.1:8091") if ! wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null; then score=$((score - 10)) factors="${factors}LocalAI offline (-10), " fi # Check agent statuses for agent in threat_analyst dns_guard network_anomaly cve_triage; do local status=$(get_agent_status "$agent" | jsonfilter -e '@.status' 2>/dev/null) if [ "$status" != "online" ]; then score=$((score - 5)) factors="${factors}${agent} offline (-5), " fi done # Check CrowdSec alerts (high = bad) if cmd_exists cscli; then local cs_alerts=$(cscli alerts list -o json --since 1h 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l) if [ "$cs_alerts" -gt 50 ]; then score=$((score - 20)) factors="${factors}High CrowdSec alerts (-20), " elif [ "$cs_alerts" -gt 20 ]; then score=$((score - 10)) factors="${factors}Elevated CrowdSec alerts (-10), " elif [ "$cs_alerts" -gt 5 ]; then score=$((score - 5)) factors="${factors}Some CrowdSec alerts (-5), " fi fi # Check CVE alerts if [ -f /var/lib/cve-triage/vulnerabilities.json ]; then local critical=$(jsonfilter -i /var/lib/cve-triage/vulnerabilities.json -e "@[@.severity='critical']" 2>/dev/null | wc -l) local high=$(jsonfilter -i /var/lib/cve-triage/vulnerabilities.json -e "@[@.severity='high']" 2>/dev/null | wc -l) [ "$critical" -gt 0 ] && score=$((score - critical * 10)) && factors="${factors}Critical CVEs (-$((critical * 10))), " [ "$high" -gt 0 ] && score=$((score - high * 5)) && factors="${factors}High CVEs (-$((high * 5))), " fi # Ensure score is 0-100 [ "$score" -lt 0 ] && score=0 [ "$score" -gt 100 ] && score=100 # Remove trailing comma factors=$(echo "$factors" | sed 's/, $//') [ -z "$factors" ] && factors="All systems nominal" printf '{"score":%d,"factors":"%s"}' "$score" "$factors" } case "$1" in list) cat <<'EOF' { "status": {}, "get_alerts": {"limit": 50}, "get_posture": {}, "get_timeline": {"hours": 24}, "get_cve_feed": {"limit": 10}, "run_all": {}, "analyze": {} } EOF ;; call) case "$2" in status) # Get all agent statuses ta=$(get_agent_status threat_analyst) dg=$(get_agent_status dns_guard) na=$(get_agent_status network_anomaly) ct=$(get_agent_status cve_triage) # Check LocalAI localai_status="offline" localai_url=$(uci -q get localrecall.main.localai_url || echo "http://127.0.0.1:8091") wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null && localai_status="online" # Check LocalRecall memories=0 [ -f /var/lib/localrecall/memories.json ] && \ memories=$(jsonfilter -i /var/lib/localrecall/memories.json -e '@[*]' 2>/dev/null | wc -l) # Get posture posture=$(calculate_posture) posture_score=$(echo "$posture" | jsonfilter -e '@.score' 2>/dev/null) cat </dev/null) [ -z "$limit" ] && limit=50 # Aggregate alerts from all sources alerts='[' first=1 # Threat Analyst pending rules if [ -f /var/lib/threat-analyst/pending_rules.json ]; then jsonfilter -i /var/lib/threat-analyst/pending_rules.json -e '@[*]' 2>/dev/null | head -n 10 | while read -r a; do [ $first -eq 0 ] && printf ',' first=0 printf '{"source":"threat_analyst","type":"rule","data":%s}' "$a" done fi # DNS Guard alerts if [ -f /var/lib/dns-guard/alerts.json ]; then jsonfilter -i /var/lib/dns-guard/alerts.json -e '@[*]' 2>/dev/null | head -n 10 | while read -r a; do [ $first -eq 0 ] && printf ',' first=0 printf '{"source":"dns_guard","type":"alert","data":%s}' "$a" done fi # Network Anomaly alerts if [ -f /var/lib/network-anomaly/alerts.json ]; then jsonfilter -i /var/lib/network-anomaly/alerts.json -e '@[*]' 2>/dev/null | head -n 10 | while read -r a; do [ $first -eq 0 ] && printf ',' first=0 printf '{"source":"network_anomaly","type":"alert","data":%s}' "$a" done fi # CVE Triage alerts if [ -f /var/lib/cve-triage/alerts.json ]; then jsonfilter -i /var/lib/cve-triage/alerts.json -e '@[*]' 2>/dev/null | head -n 10 | while read -r a; do [ $first -eq 0 ] && printf ',' first=0 printf '{"source":"cve_triage","type":"cve","data":%s}' "$a" done fi alerts="${alerts}]" printf '{"alerts":%s}' "$alerts" ;; get_posture) posture=$(calculate_posture) echo "$posture" ;; get_timeline) read -r input hours=$(echo "$input" | jsonfilter -e '@.hours' 2>/dev/null) [ -z "$hours" ] && hours=24 # Build timeline from system log timeline='[' first=1 # Get security-related log entries logread 2>/dev/null | grep -E "(crowdsec|threat-analyst|dns-guard|network-anomaly|cve-triage)" | tail -n 50 | while read -r line; do ts=$(echo "$line" | awk '{print $1" "$2" "$3}') msg=$(echo "$line" | cut -d: -f4-) source=$(echo "$line" | grep -oE "(crowdsec|threat-analyst|dns-guard|network-anomaly|cve-triage)" | head -1) [ $first -eq 0 ] && printf ',' first=0 printf '{"time":"%s","source":"%s","message":"%s"}' "$ts" "$source" "$(echo "$msg" | sed 's/"/\\"/g')" done timeline="${timeline}]" printf '{"timeline":%s}' "$timeline" ;; run_all) # Trigger all agents results='{' # Run Threat Analyst if cmd_exists threat-analystctl; then /usr/bin/threat-analystctl run -q >/dev/null 2>&1 & results="${results}\"threat_analyst\":\"started\"," else results="${results}\"threat_analyst\":\"not_installed\"," fi # Run DNS Guard if cmd_exists dnsguardctl; then /usr/bin/dnsguardctl run -q >/dev/null 2>&1 & results="${results}\"dns_guard\":\"started\"," else results="${results}\"dns_guard\":\"not_installed\"," fi # Run Network Anomaly if cmd_exists network-anomalyctl; then /usr/bin/network-anomalyctl run -q >/dev/null 2>&1 & results="${results}\"network_anomaly\":\"started\"," else results="${results}\"network_anomaly\":\"not_installed\"," fi # Run CVE Triage if cmd_exists cve-triagectl; then /usr/bin/cve-triagectl run -q >/dev/null 2>&1 & results="${results}\"cve_triage\":\"started\"," else results="${results}\"cve_triage\":\"not_installed\"," fi # Remove trailing comma and close results=$(echo "$results" | sed 's/,$//') results="${results}}" echo "$results" ;; get_cve_feed) read -r input limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null) [ -z "$limit" ] && limit=10 # Cache file for CVE feed (refresh every 30 min) cache_file="/tmp/cve_feed_cache.json" cache_age=1800 # Check cache (use ls -l for file age on OpenWrt) if [ -f "$cache_file" ]; then file_time=$(date -r "$cache_file" +%s 2>/dev/null || echo 0) now=$(date +%s) age=$((now - file_time)) if [ "$age" -lt "$cache_age" ] && [ -s "$cache_file" ]; then cat "$cache_file" exit 0 fi fi # Calculate dates (OpenWrt compatible) # 3 days = 259200 seconds now=$(date +%s) start_ts=$((now - 259200)) # Format: YYYY-MM-DDTHH:MM:SS.000 # BusyBox date can format from timestamp with -D start_date=$(date -u -d "@$start_ts" +%Y-%m-%dT00:00:00.000 2>/dev/null) if [ -z "$start_date" ]; then # Fallback: just use current date (NVD will return recent CVEs) start_date=$(date -u +%Y-%m-%dT00:00:00.000) fi end_date=$(date -u +%Y-%m-%dT23:59:59.999) # Fetch from NVD API - use wget if curl not available nvd_url="https://services.nvd.nist.gov/rest/json/cves/2.0?pubStartDate=${start_date}&pubEndDate=${end_date}&resultsPerPage=${limit}" if command -v curl >/dev/null 2>&1; then response=$(curl -s --max-time 20 "$nvd_url" 2>/dev/null) elif command -v wget >/dev/null 2>&1; then response=$(wget -q -O - --timeout=20 "$nvd_url" 2>/dev/null) else echo '{"cves":[],"error":"No HTTP client available"}' exit 0 fi if [ -n "$response" ] && echo "$response" | grep -q "vulnerabilities"; then # Build JSON manually (jshn in while loop doesn't work due to subshell) tmpfile="/tmp/cve_build_$$.json" echo '{"cves":[' > "$tmpfile" first=1 # Parse each vulnerability total=$(echo "$response" | jsonfilter -e '@.totalResults' 2>/dev/null) [ -z "$total" ] && total=0 i=0 while [ "$i" -lt "$limit" ] && [ "$i" -lt "$total" ]; do cve_id=$(echo "$response" | jsonfilter -e "@.vulnerabilities[$i].cve.id" 2>/dev/null) [ -z "$cve_id" ] && { i=$((i + 1)); continue; } desc=$(echo "$response" | jsonfilter -e "@.vulnerabilities[$i].cve.descriptions[0].value" 2>/dev/null | head -c 200 | sed 's/"/\\"/g; s/\\/\\\\/g' | tr '\n' ' ') score=$(echo "$response" | jsonfilter -e "@.vulnerabilities[$i].cve.metrics.cvssMetricV31[0].cvssData.baseScore" 2>/dev/null) [ -z "$score" ] && score=$(echo "$response" | jsonfilter -e "@.vulnerabilities[$i].cve.metrics.cvssMetricV2[0].cvssData.baseScore" 2>/dev/null) [ -z "$score" ] && score="0" published=$(echo "$response" | jsonfilter -e "@.vulnerabilities[$i].cve.published" 2>/dev/null | cut -c1-10) [ "$first" -eq 0 ] && echo ',' >> "$tmpfile" first=0 printf '{"id":"%s","description":"%s","score":%s,"published":"%s"}' \ "$cve_id" "$desc" "$score" "$published" >> "$tmpfile" i=$((i + 1)) done echo ']}' >> "$tmpfile" mv "$tmpfile" "$cache_file" cat "$cache_file" else echo '{"cves":[],"error":"Failed to fetch CVE feed"}' fi ;; analyze) # Get AI security analysis localai_url=$(uci -q get localrecall.main.localai_url || echo "http://127.0.0.1:8091") localai_model=$(uci -q get localrecall.main.localai_model || echo "tinyllama-1.1b-chat-v1.0.Q4_K_M") if ! curl -s --max-time 2 "${localai_url}/v1/models" >/dev/null 2>&1; then echo '{"error":"LocalAI not available"}' exit 0 fi # Collect current state posture=$(calculate_posture) score=$(echo "$posture" | jsonfilter -e '@.score' 2>/dev/null) factors=$(echo "$posture" | jsonfilter -e '@.factors' 2>/dev/null) prompt="You are a security analyst for SecuBox. Current security posture score: $score/100. Factors: $factors. Provide a brief security assessment and top 3 recommendations." prompt=$(printf '%s' "$prompt" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') response=$(curl -s --max-time 60 -X POST "${localai_url}/v1/chat/completions" \ -H "Content-Type: application/json" \ -d "{\"model\":\"$localai_model\",\"messages\":[{\"role\":\"user\",\"content\":\"$prompt\"}],\"max_tokens\":256,\"temperature\":0.3}" 2>/dev/null) if [ -n "$response" ]; then content=$(echo "$response" | jsonfilter -e '@.choices[0].message.content' 2>/dev/null) content=$(printf '%s' "$content" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') printf '{"analysis":"%s"}' "$content" else echo '{"error":"AI analysis failed"}' fi ;; *) echo '{"error":"Unknown method"}' ;; esac ;; esac