#!/bin/sh # SPDX-License-Identifier: Apache-2.0 # SecuBox Security Threats Dashboard RPCD backend # Copyright (C) 2026 CyberMind.fr - Gandalf # # Integrates netifyd DPI security risks with CrowdSec threat intelligence # for comprehensive network threat monitoring and automated blocking . /lib/functions.sh . /usr/share/libubox/jshn.sh HISTORY_FILE="/tmp/secubox-threats-history.json" CSCLI="/usr/bin/cscli" SECCUBOX_LOG="/usr/sbin/secubox-log" secubox_log() { [ -x "$SECCUBOX_LOG" ] || return "$SECCUBOX_LOG" --tag "security-threats" --message "$1" >/dev/null 2>&1 } # Initialize storage init_storage() { [ ! -f "$HISTORY_FILE" ] && echo '[]' > "$HISTORY_FILE" } # ============================================================================== # DATA COLLECTION # ============================================================================== # Get netifyd flows (socket first, fallback to file) get_netifyd_flows() { if [ -S /var/run/netifyd/netifyd.sock ]; then echo "status" | nc -U /var/run/netifyd/netifyd.sock 2>/dev/null elif [ -f /var/run/netifyd/status.json ]; then cat /var/run/netifyd/status.json else echo '{}' fi } # Filter flows with security risks filter_risky_flows() { local flows="$1" # Extract flows with risks[] array (length > 0) echo "$flows" | jq -c '.flows[]? | select(.risks != null and (.risks | length) > 0)' 2>/dev/null } # Get CrowdSec decisions (active bans) get_crowdsec_decisions() { [ ! -x "$CSCLI" ] && echo '[]' && return $CSCLI decisions list -o json 2>/dev/null || echo '[]' } # Get CrowdSec alerts (recent detections) get_crowdsec_alerts() { [ ! -x "$CSCLI" ] && echo '[]' && return $CSCLI alerts list -o json --limit 100 2>/dev/null || echo '[]' } # ============================================================================== # CLASSIFICATION # ============================================================================== # Classify netifyd risk by category classify_netifyd_risk() { local risk_name="$1" # Map ND_RISK_* to categories case "$risk_name" in *MALICIOUS_JA3*|*SUSPICIOUS_DGA*|*SUSPICIOUS_ENTROPY*|*POSSIBLE_EXPLOIT*|*PERIODIC_FLOW*) echo "malware";; *SQL_INJECTION*|*XSS*|*RCE_INJECTION*|*HTTP_SUSPICIOUS*) echo "web_attack";; *DNS_FRAGMENTED*|*DNS_LARGE_PACKET*|*DNS_SUSPICIOUS*|*RISKY_ASN*|*RISKY_DOMAIN*|*UNIDIRECTIONAL*|*MALFORMED_PACKET*) echo "anomaly";; *BitTorrent*|*Mining*|*Tor*|*PROXY*|*SOCKS*) echo "protocol";; *TLS_*|*CERTIFICATE_*) echo "tls_issue";; *) echo "other";; esac } # Calculate risk score (0-100) calculate_risk_score() { local risk_count="$1" local has_crowdsec="$2" local risk_types="$3" # comma-separated local score=$((risk_count * 10)) # Base: 10 per risk [ "$score" -gt 50 ] && score=50 # Cap base at 50 # Severity weights echo "$risk_types" | grep -q "MALICIOUS_JA3\|SUSPICIOUS_DGA\|POSSIBLE_EXPLOIT" && score=$((score + 20)) echo "$risk_types" | grep -q "SQL_INJECTION\|XSS\|RCE" && score=$((score + 15)) echo "$risk_types" | grep -q "RISKY_ASN\|RISKY_DOMAIN" && score=$((score + 10)) echo "$risk_types" | grep -q "BitTorrent\|Mining\|Tor" && score=$((score + 5)) # CrowdSec correlation bonus [ "$has_crowdsec" = "1" ] && score=$((score + 30)) # Cap at 100 [ "$score" -gt 100 ] && score=100 echo "$score" } # Determine severity level get_threat_severity() { local score="$1" if [ "$score" -ge 80 ]; then echo "critical" elif [ "$score" -ge 60 ]; then echo "high" elif [ "$score" -ge 40 ]; then echo "medium" else echo "low" fi } # ============================================================================== # CORRELATION ENGINE # ============================================================================== # Correlate netifyd risks with CrowdSec data correlate_threats() { local netifyd_flows="$1" local crowdsec_decisions="$2" local crowdsec_alerts="$3" # Create lookup tables with jq local decisions_by_ip=$(echo "$crowdsec_decisions" | jq -c 'INDEX(.value)' 2>/dev/null || echo '{}') local alerts_by_ip=$(echo "$crowdsec_alerts" | jq -c 'group_by(.source.ip) | map({(.[0].source.ip): .}) | add // {}' 2>/dev/null || echo '{}') # Process each risky flow echo "$netifyd_flows" | while read -r flow; do [ -z "$flow" ] && continue local ip=$(echo "$flow" | jq -r '.src_ip // "unknown"') [ "$ip" = "unknown" ] && continue local mac=$(echo "$flow" | jq -r '.src_mac // "N/A"') local risks=$(echo "$flow" | jq -r '.risks | map(tostring) | join(",")' 2>/dev/null || echo "") local risk_count=$(echo "$flow" | jq '.risks | length' 2>/dev/null || echo 0) # Lookup CrowdSec data local decision=$(echo "$decisions_by_ip" | jq -c ".[\"$ip\"] // null") local has_decision=$([[ "$decision" != "null" ]] && echo 1 || echo 0) local alert=$(echo "$alerts_by_ip" | jq -c ".[\"$ip\"] // null") # Calculate metrics local risk_score=$(calculate_risk_score "$risk_count" "$has_decision" "$risks") local severity=$(get_threat_severity "$risk_score") local first_risk=$(echo "$risks" | cut -d, -f1) local category=$(classify_netifyd_risk "$first_risk") # Build unified threat JSON jq -n \ --arg ip "$ip" \ --arg mac "$mac" \ --arg timestamp "$(date -Iseconds)" \ --argjson score "$risk_score" \ --arg severity "$severity" \ --arg category "$category" \ --argjson netifyd "$(echo "$flow" | jq '{ application: .detected_application // "unknown", protocol: .detected_protocol // "unknown", risks: [.risks[] | tostring], risk_count: (.risks | length), bytes: .total_bytes // 0, packets: .total_packets // 0 }')" \ --argjson crowdsec "$(jq -n \ --argjson decision "$decision" \ --argjson alert "$alert" \ '{ has_decision: ($decision != null), decision: $decision, has_alert: ($alert != null), alert_count: (if $alert != null then ($alert | length) else 0 end), scenarios: (if $alert != null then ($alert | map(.scenario) | join(",")) else "" end) }')" \ '{ ip: $ip, mac: $mac, timestamp: $timestamp, risk_score: $score, severity: $severity, category: $category, netifyd: $netifyd, crowdsec: $crowdsec }' 2>/dev/null done } # ============================================================================== # AUTO-BLOCKING # ============================================================================== # Execute block via CrowdSec execute_block() { local ip="$1" local duration="$2" local reason="$3" [ ! -x "$CSCLI" ] && return 1 # Call cscli to add decision if $CSCLI decisions add --ip "$ip" --duration "$duration" --reason "$reason" >/dev/null 2>&1; then secubox_log "Auto-blocked $ip for $duration ($reason)" return 0 else return 1 fi } # Check single rule match check_rule_match() { local section="$1" local threat_category="$2" local threat_risks="$3" local threat_score="$4" local threat_ip="$5" local enabled=$(uci -q get "secubox_security_threats.${section}.enabled") [ "$enabled" != "1" ] && return 1 local rule_types=$(uci -q get "secubox_security_threats.${section}.threat_types") echo "$rule_types" | grep -qw "$threat_category" || return 1 local threshold=$(uci -q get "secubox_security_threats.${section}.threshold" 2>/dev/null || echo 1) [ "$threat_score" -lt "$threshold" ] && return 1 # Rule matched - execute block local duration=$(uci -q get "secubox_security_threats.${section}.duration" || echo "4h") local name=$(uci -q get "secubox_security_threats.${section}.name" || echo "Security threat") execute_block "$threat_ip" "$duration" "Auto-blocked: $name" return $? } # Check if threat should be auto-blocked check_block_rules() { local threat="$1" local ip=$(echo "$threat" | jq -r '.ip') local category=$(echo "$threat" | jq -r '.category') local score=$(echo "$threat" | jq -r '.risk_score') local risks=$(echo "$threat" | jq -r '.netifyd.risks | join(",")') # Check whitelist first local whitelist_section="whitelist_${ip//./_}" uci -q get "secubox_security_threats.${whitelist_section}" >/dev/null 2>&1 && return 1 # Check if auto-blocking is enabled globally local auto_block_enabled=$(uci -q get secubox_security_threats.global.auto_block_enabled 2>/dev/null || echo 1) [ "$auto_block_enabled" != "1" ] && return 1 # Iterate block rules from UCI config_load secubox_security_threats config_foreach check_rule_match block_rule "$category" "$risks" "$score" "$ip" } # ============================================================================== # SECURITY STATS (Quick Overview) # ============================================================================== # Get overall security statistics from all sources get_security_stats() { local wan_drops=0 local fw_rejects=0 local cs_bans=0 local cs_alerts_24h=0 local haproxy_conns=0 local invalid_conns=0 # WAN dropped packets (from kernel stats) if [ -f /sys/class/net/br-wan/statistics/rx_dropped ]; then wan_drops=$(cat /sys/class/net/br-wan/statistics/rx_dropped 2>/dev/null) elif [ -f /sys/class/net/eth1/statistics/rx_dropped ]; then wan_drops=$(cat /sys/class/net/eth1/statistics/rx_dropped 2>/dev/null) fi wan_drops=${wan_drops:-0} # Firewall rejects from logs (last 24h) fw_rejects=$(logread 2>/dev/null | grep -c "reject\|drop" || echo 0) fw_rejects=$(echo "$fw_rejects" | tr -d '\n') fw_rejects=${fw_rejects:-0} # CrowdSec active bans if [ -x "$CSCLI" ]; then cs_bans=$($CSCLI decisions list -o json 2>/dev/null | grep -c '"id":' || echo 0) cs_bans=$(echo "$cs_bans" | tr -d '\n') cs_bans=${cs_bans:-0} # CrowdSec alerts in last 24h cs_alerts_24h=$($CSCLI alerts list -o json --since 24h 2>/dev/null | grep -c '"id":' || echo 0) cs_alerts_24h=$(echo "$cs_alerts_24h" | tr -d '\n') cs_alerts_24h=${cs_alerts_24h:-0} fi # Invalid connections (conntrack) if [ -f /proc/net/nf_conntrack ]; then invalid_conns=$(grep -c "INVALID\|UNREPLIED" /proc/net/nf_conntrack 2>/dev/null || echo 0) fi invalid_conns=$(echo "$invalid_conns" | tr -d '\n') invalid_conns=${invalid_conns:-0} # HAProxy connections (if running in LXC) if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then haproxy_conns=$(lxc-attach -n haproxy -- sh -c 'echo "show stat" | socat stdio /var/run/haproxy/admin.sock 2>/dev/null | tail -n+2 | awk -F, "{sum+=\$8} END {print sum}"' 2>/dev/null || echo 0) fi haproxy_conns=$(echo "$haproxy_conns" | tr -d '\n') haproxy_conns=${haproxy_conns:-0} # Output JSON cat << EOF { "wan_dropped": $wan_drops, "firewall_rejects": $fw_rejects, "crowdsec_bans": $cs_bans, "crowdsec_alerts_24h": $cs_alerts_24h, "invalid_connections": $invalid_conns, "haproxy_connections": $haproxy_conns, "timestamp": "$(date -Iseconds)" } EOF } # ============================================================================== # STATISTICS # ============================================================================== # Get stats by type (category) get_stats_by_type() { local threats="$1" echo "$threats" | jq -s '{ malware: [.[] | select(.category == "malware")] | length, web_attack: [.[] | select(.category == "web_attack")] | length, anomaly: [.[] | select(.category == "anomaly")] | length, protocol: [.[] | select(.category == "protocol")] | length, tls_issue: [.[] | select(.category == "tls_issue")] | length, other: [.[] | select(.category == "other")] | length }' 2>/dev/null } # Get stats by host (IP) get_stats_by_host() { local threats="$1" echo "$threats" | jq -s 'group_by(.ip) | map({ ip: .[0].ip, mac: .[0].mac, threat_count: length, avg_risk_score: (map(.risk_score) | add / length | floor), highest_severity: (map(.severity) | sort | reverse | .[0]), first_seen: (map(.timestamp) | sort | .[0]), last_seen: (map(.timestamp) | sort | reverse | .[0]), categories: (map(.category) | unique | join(",")) })' 2>/dev/null } # ============================================================================== # UBUS INTERFACE # ============================================================================== case "$1" in list) # List available methods json_init json_add_object "get_security_stats" json_close_object json_add_object "status" json_close_object json_add_object "get_active_threats" json_close_object json_add_object "get_threat_history" json_add_string "hours" "int" json_close_object json_add_object "get_stats_by_type" json_close_object json_add_object "get_stats_by_host" json_close_object json_add_object "get_blocked_ips" json_close_object json_add_object "block_threat" json_add_string "ip" "string" json_add_string "duration" "string" json_add_string "reason" "string" json_close_object json_add_object "whitelist_host" json_add_string "ip" "string" json_add_string "reason" "string" json_close_object json_add_object "remove_whitelist" json_add_string "ip" "string" json_close_object json_dump ;; call) case "$2" in get_security_stats) get_security_stats ;; status) json_init json_add_boolean "enabled" 1 json_add_string "module" "secubox-security-threats" json_add_string "version" "1.0.0" json_add_boolean "netifyd_running" $(pgrep netifyd >/dev/null && echo 1 || echo 0) json_add_boolean "crowdsec_running" $(pgrep crowdsec >/dev/null && echo 1 || echo 0) json_add_boolean "cscli_available" $([ -x "$CSCLI" ] && echo 1 || echo 0) json_dump ;; get_active_threats) # Main correlation workflow local netifyd_data=$(get_netifyd_flows) local risky_flows=$(filter_risky_flows "$netifyd_data") # Only fetch CrowdSec data if available local decisions='[]' local alerts='[]' if [ -x "$CSCLI" ]; then decisions=$(get_crowdsec_decisions) alerts=$(get_crowdsec_alerts) fi # Correlate threats local threats=$(correlate_threats "$risky_flows" "$decisions" "$alerts") # Check auto-block rules for each threat if [ -n "$threats" ]; then echo "$threats" | while read -r threat; do [ -z "$threat" ] && continue check_block_rules "$threat" >/dev/null 2>&1 || true done fi # Output as JSON array json_init json_add_array "threats" if [ -n "$threats" ]; then echo "$threats" | jq -s 'sort_by(.risk_score) | reverse' | jq -c '.[]' | while read -r threat; do echo "$threat" done fi json_close_array json_dump ;; get_threat_history) read -r input json_load "$input" json_get_var hours hours hours=${hours:-24} init_storage # Filter history by time local cutoff_time=$(date -d "$hours hours ago" -Iseconds 2>/dev/null || date -Iseconds) json_init json_add_array "threats" if [ -f "$HISTORY_FILE" ]; then jq -c --arg cutoff "$cutoff_time" '.[] | select(.timestamp >= $cutoff)' "$HISTORY_FILE" 2>/dev/null | while read -r threat; do echo "$threat" done fi json_close_array json_dump ;; get_stats_by_type) local netifyd_data=$(get_netifyd_flows) local risky_flows=$(filter_risky_flows "$netifyd_data") local decisions=$(get_crowdsec_decisions) local alerts=$(get_crowdsec_alerts) local threats=$(correlate_threats "$risky_flows" "$decisions" "$alerts") local stats=$(get_stats_by_type "$threats") echo "$stats" ;; get_stats_by_host) local netifyd_data=$(get_netifyd_flows) local risky_flows=$(filter_risky_flows "$netifyd_data") local decisions=$(get_crowdsec_decisions) local alerts=$(get_crowdsec_alerts) local threats=$(correlate_threats "$risky_flows" "$decisions" "$alerts") json_init json_add_array "hosts" if [ -n "$threats" ]; then get_stats_by_host "$threats" | jq -c '.[]' | while read -r host; do echo "$host" done fi json_close_array json_dump ;; get_blocked_ips) if [ -x "$CSCLI" ]; then local decisions=$(get_crowdsec_decisions) echo "{\"blocked\":$decisions}" else echo '{"blocked":[]}' fi ;; block_threat) read -r input json_load "$input" json_get_var ip ip json_get_var duration duration json_get_var reason reason if [ -z "$ip" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "IP address required" json_dump exit 0 fi duration=${duration:-4h} reason=${reason:-"Manual block from Security Threats Dashboard"} if execute_block "$ip" "$duration" "$reason"; then json_init json_add_boolean "success" 1 json_add_string "message" "IP $ip blocked for $duration" json_dump else json_init json_add_boolean "success" 0 json_add_string "error" "Failed to block IP (check if CrowdSec is running)" json_dump fi ;; whitelist_host) read -r input json_load "$input" json_get_var ip ip json_get_var reason reason if [ -z "$ip" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "IP address required" json_dump exit 0 fi reason=${reason:-"Whitelisted from Security Threats Dashboard"} local section="whitelist_${ip//./_}" uci set "secubox_security_threats.${section}=whitelist" uci set "secubox_security_threats.${section}.ip=$ip" uci set "secubox_security_threats.${section}.reason=$reason" uci set "secubox_security_threats.${section}.added_at=$(date -Iseconds)" uci commit secubox_security_threats json_init json_add_boolean "success" 1 json_add_string "message" "IP $ip added to whitelist" json_dump ;; remove_whitelist) read -r input json_load "$input" json_get_var ip ip if [ -z "$ip" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "IP address required" json_dump exit 0 fi local section="whitelist_${ip//./_}" uci delete "secubox_security_threats.${section}" 2>/dev/null uci commit secubox_security_threats json_init json_add_boolean "success" 1 json_add_string "message" "IP $ip removed from whitelist" json_dump ;; *) json_init json_add_boolean "error" 1 json_add_string "message" "Unknown method: $2" json_dump ;; esac ;; esac