#!/bin/sh # SPDX-License-Identifier: Apache-2.0 # SecuBox Security Intelligence - RPCD backend # Copyright (C) 2026 CyberMind.fr - Gandalf # # Combines mitmproxy threat detection, CrowdSec blocking, # and P2P mesh threat intelligence sharing . /lib/functions.sh . /usr/share/libubox/jshn.sh CSCLI="/usr/bin/cscli" THREAT_INTEL="/usr/lib/secubox/threat-intel.sh" # ============================================================================== # HELPERS # ============================================================================== execute_block() { local ip="$1" duration="$2" reason="$3" [ -x "$CSCLI" ] || return 1 $CSCLI decisions add --ip "$ip" --duration "$duration" --reason "$reason" >/dev/null 2>&1 } # ============================================================================== # SECURITY STATS (firewall counters) # ============================================================================== get_security_stats() { local wan_drops=0 fw_rejects=0 cs_bans=0 cs_alerts_24h=0 haproxy_conns=0 invalid_conns=0 # WAN dropped packets from nftables if command -v nft >/dev/null 2>&1; then wan_drops=$(nft list chain inet fw4 input_wan 2>/dev/null | \ grep -oE 'packets [0-9]+' | awk '{sum+=$2} END {print sum+0}') local reject_drops=$(nft list chain inet fw4 reject_from_wan 2>/dev/null | \ grep -oE 'packets [0-9]+' | awk '{sum+=$2} END {print sum+0}') [ -n "$reject_drops" ] && wan_drops=$((${wan_drops:-0} + reject_drops)) fi # Firewall rejects if command -v nft >/dev/null 2>&1; then fw_rejects=$(nft list chain inet fw4 handle_reject 2>/dev/null | \ grep -oE 'packets [0-9]+' | awk '{sum+=$2} END {print sum+0}') if [ "${fw_rejects:-0}" = "0" ]; then fw_rejects=$(nft -a list ruleset 2>/dev/null | \ grep -E "reject|drop" | grep "counter" | \ grep -oE 'packets [0-9]+' | awk '{sum+=$2} END {print sum+0}') fi fi # CrowdSec bans and alerts if [ -x "$CSCLI" ]; then if command -v jq >/dev/null 2>&1; then cs_bans=$($CSCLI decisions list -o json 2>/dev/null | jq 'length' 2>/dev/null) cs_alerts_24h=$($CSCLI alerts list -o json --since 24h 2>/dev/null | jq 'length' 2>/dev/null) else local cs_json=$($CSCLI decisions list -o json 2>/dev/null) [ -n "$cs_json" ] && [ "$cs_json" != "null" ] && [ "$cs_json" != "[]" ] && \ cs_bans=$(echo "$cs_json" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) local alerts_json=$($CSCLI alerts list -o json --since 24h 2>/dev/null) [ -n "$alerts_json" ] && [ "$alerts_json" != "null" ] && [ "$alerts_json" != "[]" ] && \ cs_alerts_24h=$(echo "$alerts_json" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) fi fi # Invalid connections if [ -f /proc/net/nf_conntrack ]; then invalid_conns=$(grep -c "\[INVALID\]" /proc/net/nf_conntrack 2>/dev/null) fi # HAProxy connections if [ -S /var/run/haproxy/admin.sock ]; then haproxy_conns=$(echo "show stat" | socat stdio /var/run/haproxy/admin.sock 2>/dev/null | \ tail -n+2 | awk -F, '{sum+=$5} END {print sum+0}') elif [ -S /var/lib/haproxy/stats ]; then haproxy_conns=$(echo "show stat" | socat stdio /var/lib/haproxy/stats 2>/dev/null | \ tail -n+2 | awk -F, '{sum+=$5} END {print sum+0}') elif command -v lxc-info >/dev/null 2>&1 && lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then haproxy_conns=$(lxc-attach -n haproxy -- sh -c ' for sock in /stats /run/haproxy.sock /var/lib/haproxy/stats /var/run/haproxy/admin.sock; do [ -S "$sock" ] && { echo "show stat" | socat stdio "$sock" 2>/dev/null; break; } done' 2>/dev/null | tail -n+2 | awk -F, '{sum+=$5} END {print sum+0}') fi # Sanitize to numeric wan_drops=$(printf '%s' "${wan_drops:-0}" | tr -cd '0-9'); : ${wan_drops:=0} fw_rejects=$(printf '%s' "${fw_rejects:-0}" | tr -cd '0-9'); : ${fw_rejects:=0} cs_bans=$(printf '%s' "${cs_bans:-0}" | tr -cd '0-9'); : ${cs_bans:=0} cs_alerts_24h=$(printf '%s' "${cs_alerts_24h:-0}" | tr -cd '0-9'); : ${cs_alerts_24h:=0} invalid_conns=$(printf '%s' "${invalid_conns:-0}" | tr -cd '0-9'); : ${invalid_conns:=0} haproxy_conns=$(printf '%s' "${haproxy_conns:-0}" | tr -cd '0-9'); : ${haproxy_conns:=0} 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 } EOF } # ============================================================================== # UBUS INTERFACE # ============================================================================== case "$1" in list) json_init json_add_object "status"; json_close_object json_add_object "get_security_stats"; json_close_object json_add_object "get_visit_stats"; json_close_object json_add_object "get_active_threats"; 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_add_object "get_threat_intel"; json_close_object json_add_object "get_mesh_iocs"; json_close_object json_add_object "get_mesh_peers"; json_close_object json_add_object "publish_intel"; json_close_object json_add_object "apply_intel"; json_close_object json_dump ;; call) case "$2" in status) json_init json_add_boolean "netifyd_running" $(pgrep netifyd >/dev/null 2>&1 && echo 1 || echo 0) json_add_boolean "crowdsec_running" $(pgrep crowdsec >/dev/null 2>&1 && echo 1 || echo 0) json_add_boolean "mitmproxy_running" $(pgrep -f mitmweb >/dev/null 2>&1 && echo 1 || echo 0) json_add_boolean "cscli_available" $([ -x "$CSCLI" ] && echo 1 || echo 0) json_add_boolean "threat_intel_available" $([ -x "$THREAT_INTEL" ] && echo 1 || echo 0) json_dump ;; get_security_stats) get_security_stats ;; get_visit_stats) # Parse threats.log for visit statistics _log_file="/srv/mitmproxy/threats.log" if [ -f "$_log_file" ] && command -v jq >/dev/null 2>&1; then # Get stats from last 1000 entries tail -1000 "$_log_file" 2>/dev/null | jq -sc ' { total_requests: length, by_country: (group_by(.country) | map({country: .[0].country, count: length}) | sort_by(.count) | reverse | .[0:10]), by_host: (group_by(.host) | map({host: .[0].host, count: length}) | sort_by(.count) | reverse | .[0:10]), by_type: (group_by(.type) | map({type: .[0].type, count: length}) | sort_by(.count) | reverse), by_severity: (group_by(.severity) | map({severity: .[0].severity, count: length})), bots_vs_humans: { bots: (map(select(.is_bot == true)) | length), humans: (map(select(.is_bot != true)) | length) }, top_urls: (group_by(.request) | map({url: .[0].request, count: length}) | sort_by(.count) | reverse | .[0:10]) } ' 2>/dev/null || echo '{"total_requests":0,"by_country":[],"by_host":[],"by_type":[],"by_severity":[],"bots_vs_humans":{"bots":0,"humans":0},"top_urls":[]}' else echo '{"total_requests":0,"by_country":[],"by_host":[],"by_type":[],"by_severity":[],"bots_vs_humans":{"bots":0,"humans":0},"top_urls":[]}' fi ;; get_active_threats) _log_file="/srv/mitmproxy/threats.log" if [ -f "$_log_file" ]; then _threats=$(tail -50 "$_log_file" 2>/dev/null | jq -sc ' map({ ip: .source_ip, severity: .severity, score: (if .severity == "critical" then 90 elif .severity == "high" then 70 elif .severity == "medium" then 50 else 30 end), type: .type, pattern: .pattern, host: .host, country: (.country // "??"), timestamp: .timestamp, request: (.request // "-"), is_bot: (.is_bot // false), bot_type: (.bot_type // null), cve: (.cve // null) }) | unique_by(.ip) | sort_by(.score) | reverse ' 2>/dev/null || echo "[]") printf '{"threats":%s}\n' "$_threats" else echo '{"threats":[]}' fi ;; get_blocked_ips) if [ -x "$CSCLI" ]; then _decisions=$($CSCLI decisions list -o json 2>/dev/null || echo "[]") printf '{"blocked":%s}\n' "$_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 duration=${duration:-4h} reason=${reason:-"Manual block from Security Dashboard"} if [ -z "$ip" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "IP address required" json_dump elif 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" json_dump fi ;; whitelist_host) read -r input json_load "$input" json_get_var ip ip json_get_var reason reason reason=${reason:-"Whitelisted from Security Dashboard"} if [ -z "$ip" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "IP address required" json_dump else 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 whitelisted" json_dump fi ;; 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 else 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 fi ;; get_threat_intel) if [ -x "$THREAT_INTEL" ]; then "$THREAT_INTEL" status 2>/dev/null else echo '{"enabled":false,"error":"threat-intel not available"}' fi ;; get_mesh_iocs) if [ -x "$THREAT_INTEL" ]; then _iocs=$("$THREAT_INTEL" list received 2>/dev/null) printf '{"iocs":%s}\n' "${_iocs:-[]}" else echo '{"iocs":[]}' fi ;; get_mesh_peers) if [ -x "$THREAT_INTEL" ]; then _peers=$("$THREAT_INTEL" peers 2>/dev/null) printf '{"peers":%s}\n' "${_peers:-[]}" else echo '{"peers":[]}' fi ;; publish_intel) if [ -x "$THREAT_INTEL" ]; then "$THREAT_INTEL" collect-and-publish >/dev/null 2>&1 & json_init json_add_boolean "started" 1 json_add_string "message" "Publish started in background" json_dump else echo '{"error":"threat-intel not available"}' fi ;; apply_intel) if [ -x "$THREAT_INTEL" ]; then "$THREAT_INTEL" apply-pending 2>/dev/null "$THREAT_INTEL" status 2>/dev/null else echo '{"error":"threat-intel not available"}' fi ;; *) json_init json_add_string "error" "Unknown method: $2" json_dump ;; esac ;; esac