secubox-openwrt/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats
CyberMind-FR 283f2567be feat(security): Add security stats and Gitea mirror commands
Security Stats:
- Add get_security_stats RPCD method for quick overview
- Track WAN drops, firewall rejects, CrowdSec bans
- Add secubox-stats CLI tool for quick stats check

Gitea Mirror Commands:
- Add mirror-sync to trigger mirror repository sync
- Add mirror-list to show all mirrored repos
- Add mirror-create to create new mirrors from GitHub URLs
- Add repo-list to list all repositories
- Requires API token: uci set gitea.main.api_token=<token>

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 15:15:50 +01:00

609 lines
18 KiB
Bash
Executable File

#!/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