refactor(security-threats): KISS rewrite with mesh threat intelligence

Replace overengineered dashboard (2025 lines) with focused security
intelligence view (847 lines). Drop hero banner, risk gauge, device
zoning, nDPId correlation engine. Keep firewall stats, mitmproxy
threats, CrowdSec blocking. Add mesh intelligence section with P2P
threat-intel sharing (IOC counts, peer contributors, publish/apply).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-03 11:30:25 +01:00
parent 1652b39137
commit c4302504df
4 changed files with 476 additions and 1666 deletions

View File

@ -2,10 +2,6 @@
'require baseclass';
'require rpc';
// ==============================================================================
// RPC Method Declarations
// ==============================================================================
var callStatus = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'status',
@ -18,31 +14,18 @@ var callGetActiveThreats = rpc.declare({
expect: { threats: [] }
});
var callGetThreatHistory = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'get_threat_history',
params: ['hours'],
expect: { threats: [] }
});
var callGetStatsByType = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'get_stats_by_type',
expect: { }
});
var callGetStatsByHost = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'get_stats_by_host',
expect: { hosts: [] }
});
var callGetBlockedIPs = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'get_blocked_ips',
expect: { blocked: [] }
});
var callGetSecurityStats = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'get_security_stats',
expect: { }
});
var callBlockThreat = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'block_threat',
@ -64,146 +47,42 @@ var callRemoveWhitelist = rpc.declare({
expect: { }
});
var callGetSecurityStats = rpc.declare({
var callGetThreatIntel = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'get_security_stats',
method: 'get_threat_intel',
expect: { }
});
// ==============================================================================
// nDPId Integration for Device Detection
// ==============================================================================
var callNdpidStatus = rpc.declare({
object: 'luci.ndpid',
method: 'get_service_status',
var callGetMeshIocs = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'get_mesh_iocs',
expect: { }
});
var callNdpidFlows = rpc.declare({
object: 'luci.ndpid',
method: 'get_detailed_flows',
expect: { flows: [] }
var callGetMeshPeers = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'get_mesh_peers',
expect: { }
});
var callNdpidTopApps = rpc.declare({
object: 'luci.ndpid',
method: 'get_top_applications',
expect: { applications: [] }
var callPublishIntel = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'publish_intel',
expect: { }
});
var callNdpidCategories = rpc.declare({
object: 'luci.ndpid',
method: 'get_categories',
expect: { categories: [] }
var callApplyIntel = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'apply_intel',
expect: { }
});
// ==============================================================================
// Utility Functions
// ==============================================================================
/**
* Get color for severity level
* @param {string} severity - Severity level (critical, high, medium, low)
* @returns {string} Hex color code
*/
function getSeverityColor(severity) {
var colors = {
'critical': '#d32f2f', // Red
'high': '#ff5722', // Deep Orange
'medium': '#ff9800', // Orange
'low': '#ffc107' // Amber
};
return colors[severity] || '#666';
}
/**
* Get icon for threat category
* @param {string} category - Threat category
* @returns {string} Unicode emoji icon
*/
function getThreatIcon(category) {
var icons = {
'malware': '🦠',
'web_attack': '⚔️',
'anomaly': '⚠️',
'protocol': '🚫',
'tls_issue': '🔒',
'other': '❓'
};
return icons[category] || '❓';
}
/**
* Format risk flags for display
* @param {Array} risks - Array of risk flag names
* @returns {string} Formatted risk flags
*/
function formatRiskFlags(risks) {
if (!risks || !Array.isArray(risks)) return 'N/A';
return risks.map(function(risk) {
// Convert MALICIOUS_JA3 to "Malicious JA3"
return risk.toString().split('_').map(function(word) {
return word.charAt(0) + word.slice(1).toLowerCase();
}).join(' ');
}).join(', ');
}
/**
* Get human-readable category label
* @param {string} category - Category code
* @returns {string} Display label
*/
function getCategoryLabel(category) {
var labels = {
'malware': 'Malware',
'web_attack': 'Web Attack',
'anomaly': 'Network Anomaly',
'protocol': 'Protocol Threat',
'tls_issue': 'TLS/Certificate',
'other': 'Other'
};
return labels[category] || 'Unknown';
}
/**
* Format duration string (4h, 24h, etc.)
* @param {string} duration - Duration string
* @returns {string} Formatted duration
*/
function formatDuration(duration) {
if (!duration) return 'N/A';
return duration;
}
/**
* Format timestamp to localized string
* @param {string} timestamp - ISO 8601 timestamp
* @returns {string} Formatted timestamp
*/
function formatTimestamp(timestamp) {
if (!timestamp) return 'N/A';
try {
var date = new Date(timestamp);
return date.toLocaleString();
} catch(e) {
return timestamp;
}
}
/**
* Format relative time (e.g., "5 minutes ago")
* @param {string} timestamp - ISO 8601 timestamp
* @returns {string} Relative time string
*/
function formatRelativeTime(timestamp) {
if (!timestamp) return 'N/A';
if (!timestamp) return '-';
try {
var date = new Date(timestamp);
var now = new Date();
var seconds = Math.floor((now - date) / 1000);
if (seconds < 60) return seconds + 's ago';
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago';
@ -213,275 +92,48 @@ function formatRelativeTime(timestamp) {
}
}
/**
* Format bytes to human-readable size
* @param {number} bytes - Byte count
* @returns {string} Formatted size (e.g., "1.5 MB")
*/
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
function formatNumber(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return String(n || 0);
}
/**
* Get badge HTML for severity
* @param {string} severity - Severity level
* @returns {string} HTML string
*/
function getSeverityBadge(severity) {
var color = getSeverityColor(severity);
var label = severity.charAt(0).toUpperCase() + severity.slice(1);
return '<span style="display: inline-block; padding: 2px 8px; border-radius: 3px; background: ' + color + '; color: white; font-size: 0.85em; font-weight: bold;">' + label + '</span>';
}
/**
* Device type classification based on applications/protocols
*/
var deviceTypes = {
'streaming': { icon: '📺', zone: 'media', apps: ['Netflix', 'YouTube', 'Twitch', 'Spotify', 'AppleTV', 'Disney'] },
'gaming': { icon: '🎮', zone: 'gaming', apps: ['Steam', 'PlayStation', 'Xbox', 'Nintendo', 'Discord'] },
'iot': { icon: '🏠', zone: 'iot', apps: ['MQTT', 'CoAP', 'Zigbee', 'HomeKit', 'Alexa', 'GoogleHome'] },
'work': { icon: '💼', zone: 'trusted', apps: ['Teams', 'Zoom', 'Slack', 'Office365', 'Webex'] },
'mobile': { icon: '📱', zone: 'mobile', apps: ['WhatsApp', 'Telegram', 'Instagram', 'TikTok', 'Facebook'] },
'security': { icon: '🔒', zone: 'secure', apps: ['VPN', 'WireGuard', 'OpenVPN', 'SSH', 'HTTPS'] },
'unknown': { icon: '❓', zone: 'guest', apps: [] }
};
/**
* Zone definitions with firewall suggestions
*/
var networkZones = {
'trusted': { icon: '🏠', color: '#2ecc71', access: 'full', desc: 'Full network access' },
'media': { icon: '📺', color: '#9b59b6', access: 'streaming', desc: 'Streaming services only' },
'gaming': { icon: '🎮', color: '#3498db', access: 'gaming', desc: 'Gaming ports & services' },
'iot': { icon: '🤖', color: '#e67e22', access: 'limited', desc: 'Local network only, no WAN' },
'mobile': { icon: '📱', color: '#1abc9c', access: 'filtered', desc: 'Web access with filtering' },
'guest': { icon: '👤', color: '#95a5a6', access: 'isolated', desc: 'Internet only, no LAN' },
'secure': { icon: '🔐', color: '#e74c3c', access: 'vpn', desc: 'VPN/encrypted traffic only' },
'quarantine': { icon: '⛔', color: '#c0392b', access: 'blocked', desc: 'No network access' }
};
/**
* Classify device based on detected applications
* @param {Array} apps - List of detected applications
* @returns {Object} Device classification
*/
function classifyDevice(apps) {
if (!apps || !Array.isArray(apps)) return { type: 'unknown', ...deviceTypes.unknown };
for (var type in deviceTypes) {
var typeApps = deviceTypes[type].apps;
for (var i = 0; i < apps.length; i++) {
for (var j = 0; j < typeApps.length; j++) {
if (apps[i].toLowerCase().indexOf(typeApps[j].toLowerCase()) !== -1) {
return { type: type, ...deviceTypes[type] };
}
}
}
}
return { type: 'unknown', ...deviceTypes.unknown };
}
/**
* Get suggested firewall rules for a device
* @param {Object} device - Device info with classification
* @returns {Array} Suggested firewall rules
*/
function getSuggestedRules(device) {
var zone = device.zone || 'guest';
var rules = [];
switch (zone) {
case 'trusted':
rules.push({ action: 'ACCEPT', desc: 'Allow all traffic' });
break;
case 'media':
rules.push({ action: 'ACCEPT', ports: '443,80,8080', proto: 'tcp', desc: 'HTTPS/HTTP streaming' });
rules.push({ action: 'ACCEPT', ports: '1935', proto: 'tcp', desc: 'RTMP streaming' });
rules.push({ action: 'DROP', desc: 'Block other traffic' });
break;
case 'gaming':
rules.push({ action: 'ACCEPT', ports: '443,80', proto: 'tcp', desc: 'Web services' });
rules.push({ action: 'ACCEPT', ports: '3478-3480,27000-27050', proto: 'udp', desc: 'Gaming ports' });
rules.push({ action: 'DROP', desc: 'Block other traffic' });
break;
case 'iot':
rules.push({ action: 'ACCEPT', dest: 'lan', desc: 'Local network only' });
rules.push({ action: 'DROP', dest: 'wan', desc: 'Block internet access' });
break;
case 'guest':
rules.push({ action: 'ACCEPT', dest: 'wan', ports: '443,80,53', desc: 'Web + DNS only' });
rules.push({ action: 'DROP', dest: 'lan', desc: 'Block local network' });
break;
case 'quarantine':
rules.push({ action: 'DROP', desc: 'Block all traffic' });
break;
default:
rules.push({ action: 'ACCEPT', ports: '443,80,53', desc: 'Basic web access' });
}
return rules;
}
/**
* Get device icon based on MAC vendor or app detection
* @param {Object} device - Device information
* @returns {string} Emoji icon
*/
function getDeviceIcon(device) {
if (device.classification) return device.classification.icon;
if (device.vendor) {
var vendor = device.vendor.toLowerCase();
if (vendor.indexOf('apple') !== -1) return '🍎';
if (vendor.indexOf('samsung') !== -1) return '📱';
if (vendor.indexOf('amazon') !== -1) return '📦';
if (vendor.indexOf('google') !== -1) return '🔍';
if (vendor.indexOf('microsoft') !== -1) return '🪟';
if (vendor.indexOf('sony') !== -1 || vendor.indexOf('playstation') !== -1) return '🎮';
if (vendor.indexOf('intel') !== -1 || vendor.indexOf('dell') !== -1 || vendor.indexOf('hp') !== -1) return '💻';
}
return '📟';
}
/**
* Composite data fetcher for dashboard (with ndpid)
* @returns {Promise} Promise resolving to dashboard data
*/
function getDashboardData() {
return Promise.all([
callStatus(),
callGetActiveThreats(),
callGetStatsByType(),
callGetBlockedIPs(),
callGetSecurityStats(),
callNdpidStatus().catch(function() { return { running: false, dpi_available: false }; }),
callNdpidFlows().catch(function() { return { flows: [] }; }),
callNdpidTopApps().catch(function() { return { applications: [] }; })
callGetThreatIntel().catch(function() { return {}; }),
callGetMeshIocs().catch(function() { return { iocs: [] }; }),
callGetMeshPeers().catch(function() { return { peers: [] }; })
]).then(function(results) {
var ndpidFlows = results[6].flows || [];
var ndpidApps = results[7].applications || [];
var ndpidStatus = results[5] || {};
// Build device list from ndpid flows
var devicesMap = {};
var isLocalIP = function(ip) {
return ip && (ip.indexOf('192.168.') === 0 || ip.indexOf('10.') === 0 || ip.indexOf('172.16.') === 0);
};
ndpidFlows.forEach(function(flow) {
// Check both src_ip and dst_ip for local devices
var localIP = null;
if (isLocalIP(flow.src_ip)) {
localIP = flow.src_ip;
} else if (isLocalIP(flow.dst_ip)) {
localIP = flow.dst_ip;
}
if (!localIP) return; // Skip if no local device involved
if (!devicesMap[localIP]) {
devicesMap[localIP] = {
ip: localIP,
mac: flow.src_mac || flow.local_mac || '',
hostname: flow.hostname || '',
apps: [],
protocols: [],
bytes_rx: 0,
bytes_tx: 0,
flows: 0,
last_seen: flow.timestamp
};
}
var dev = devicesMap[localIP];
// Use 'app' field from ndpid flows (not 'application')
var appName = flow.app || flow.application || '';
if (appName && dev.apps.indexOf(appName) === -1) {
dev.apps.push(appName);
}
// Use 'proto' field from ndpid flows (not 'protocol')
var protoName = flow.proto || flow.protocol || '';
if (protoName && dev.protocols.indexOf(protoName) === -1) {
dev.protocols.push(protoName);
}
dev.bytes_rx += flow.bytes_rx || 0;
dev.bytes_tx += flow.bytes_tx || 0;
dev.flows++;
});
// Classify devices and suggest zones
var devices = Object.values(devicesMap).map(function(dev) {
dev.classification = classifyDevice(dev.apps);
dev.suggestedZone = dev.classification.zone;
dev.suggestedRules = getSuggestedRules(dev.classification);
dev.icon = getDeviceIcon(dev);
return dev;
});
// DPI is available if either ndpid or netifyd is running
var dpiRunning = ndpidStatus.running || ndpidStatus.netifyd_running || ndpidStatus.dpi_available || false;
var dpiUptime = ndpidStatus.uptime || ndpidStatus.netifyd_uptime || 0;
return {
status: results[0] || {},
threats: results[1].threats || [],
stats: results[2] || {},
blocked: results[3].blocked || [],
securityStats: results[4] || {},
ndpid: {
running: dpiRunning,
uptime: dpiUptime,
ndpid_running: ndpidStatus.running || false,
netifyd_running: ndpidStatus.netifyd_running || false,
flow_count: ndpidStatus.flow_count || 0
},
devices: devices,
topApps: ndpidApps,
zones: networkZones
blocked: results[2].blocked || [],
securityStats: results[3] || {},
threatIntel: results[4] || {},
meshIocs: results[5].iocs || [],
meshPeers: results[6].peers || []
};
});
}
// ==============================================================================
// Exports
// ==============================================================================
return baseclass.extend({
// RPC Methods
getStatus: callStatus,
getActiveThreats: callGetActiveThreats,
getThreatHistory: callGetThreatHistory,
getStatsByType: callGetStatsByType,
getStatsByHost: callGetStatsByHost,
getBlockedIPs: callGetBlockedIPs,
getSecurityStats: callGetSecurityStats,
blockThreat: callBlockThreat,
whitelistHost: callWhitelistHost,
removeWhitelist: callRemoveWhitelist,
// nDPId Methods
getNdpidStatus: callNdpidStatus,
getNdpidFlows: callNdpidFlows,
getNdpidTopApps: callNdpidTopApps,
getNdpidCategories: callNdpidCategories,
// Utility Functions
getSeverityColor: getSeverityColor,
getThreatIcon: getThreatIcon,
formatRiskFlags: formatRiskFlags,
getCategoryLabel: getCategoryLabel,
formatDuration: formatDuration,
formatTimestamp: formatTimestamp,
getThreatIntel: callGetThreatIntel,
getMeshIocs: callGetMeshIocs,
getMeshPeers: callGetMeshPeers,
publishIntel: callPublishIntel,
applyIntel: callApplyIntel,
formatRelativeTime: formatRelativeTime,
formatBytes: formatBytes,
getSeverityBadge: getSeverityBadge,
// Device Classification
classifyDevice: classifyDevice,
getSuggestedRules: getSuggestedRules,
getDeviceIcon: getDeviceIcon,
deviceTypes: deviceTypes,
networkZones: networkZones,
// Composite Fetchers
formatNumber: formatNumber,
getDashboardData: getDashboardData
});

View File

@ -1,385 +1,75 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# SecuBox Security Threats Dashboard RPCD backend
# SecuBox Security Intelligence - 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
# Combines mitmproxy threat detection, CrowdSec blocking,
# and P2P mesh threat intelligence sharing
. /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"
}
THREAT_INTEL="/usr/lib/secubox/threat-intel.sh"
# ==============================================================================
# DATA COLLECTION
# HELPERS
# ==============================================================================
# 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 '[]'
}
# Get mitmproxy threats (last 50 from threats.log)
get_mitmproxy_threats() {
local log_file="/srv/mitmproxy/threats.log"
[ ! -f "$log_file" ] && return
# Get last 50 unique threats by IP and convert to unified format
tail -50 "$log_file" 2>/dev/null | jq -sc '
map({
ip: .source_ip,
mac: "N/A",
timestamp: .timestamp,
risk_score: (if .severity == "critical" then 90 elif .severity == "high" then 70 elif .severity == "medium" then 50 else 30 end),
severity: .severity,
category: (if .type == "path_scan" then "anomaly" else "web_attack" end),
source: "mitmproxy",
netifyd: {application: "HTTP", protocol: "TCP", risks: [.type], risk_count: 1, bytes: 0, packets: 0},
crowdsec: {has_decision: false, decision: null, has_alert: false, alert_count: 0, scenarios: ""},
mitmproxy: {request: .request, host: .host, pattern: .pattern, country: .country, is_bot: .is_bot, bot_type: .bot_type, response_code: .response_code}
}) | unique_by(.ip) | .[]
' 2>/dev/null
}
# ==============================================================================
# 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"
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 (Quick Overview)
# SECURITY STATS (firewall counters)
# ==============================================================================
# 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
local wan_drops=0 fw_rejects=0 cs_bans=0 cs_alerts_24h=0 haproxy_conns=0 invalid_conns=0
# Get actual WAN interface from UCI
local wan_iface=$(uci -q get network.wan.device || uci -q get network.wan.ifname)
[ -z "$wan_iface" ] && wan_iface="eth0"
# WAN dropped packets from nftables (use counter on drop rules)
# WAN dropped packets from nftables
if command -v nft >/dev/null 2>&1; then
# Get all drop/reject counters from input_wan zone
# nft format: "counter packets X bytes Y"
wan_drops=$(nft list chain inet fw4 input_wan 2>/dev/null | \
grep -E "counter packets" | \
grep -oE 'packets [0-9]+' | \
awk '{sum+=$2} END {print sum+0}')
[ -z "$wan_drops" ] && wan_drops=0
# Also try reject chain
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 -E "counter packets" | \
grep -oE 'packets [0-9]+' | \
awk '{sum+=$2} END {print sum+0}')
[ -n "$reject_drops" ] && wan_drops=$((wan_drops + reject_drops))
grep -oE 'packets [0-9]+' | awk '{sum+=$2} END {print sum+0}')
[ -n "$reject_drops" ] && wan_drops=$((${wan_drops:-0} + reject_drops))
fi
wan_drops=${wan_drops:-0}
# Firewall rejects - count from reject-specific chains
# Firewall rejects
if command -v nft >/dev/null 2>&1; then
# Count from handle_reject chain which has actual reject rules
fw_rejects=$(nft list chain inet fw4 handle_reject 2>/dev/null | \
grep -E "counter packets" | \
grep -oE 'packets [0-9]+' | \
awk '{sum+=$2} END {print sum+0}')
[ -z "$fw_rejects" ] && fw_rejects=0
# If no handle_reject, try counting reject rules in all chains
if [ "$fw_rejects" = "0" ]; then
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}')
grep -oE 'packets [0-9]+' | awk '{sum+=$2} END {print sum+0}')
fi
else
# Fallback to log parsing
fw_rejects=$(logread 2>/dev/null | grep -c "reject\|DROP\|REJECT" || echo 0)
fi
fw_rejects=${fw_rejects:-0}
# CrowdSec active bans
# CrowdSec bans and alerts
if [ -x "$CSCLI" ]; then
# Use jq for proper JSON parsing if available, fallback to grep
if command -v jq >/dev/null 2>&1; then
cs_bans=$($CSCLI decisions list -o json 2>/dev/null | jq 'length' 2>/dev/null)
[ -z "$cs_bans" ] && cs_bans=0
else
# Count JSON array items
local cs_json=$($CSCLI decisions list -o json 2>/dev/null)
if [ -n "$cs_json" ] && [ "$cs_json" != "null" ] && [ "$cs_json" != "[]" ]; then
cs_bans=$(echo "$cs_json" | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
fi
fi
cs_bans=${cs_bans:-0}
# CrowdSec alerts in last 24h
if command -v jq >/dev/null 2>&1; then
cs_alerts_24h=$($CSCLI alerts list -o json --since 24h 2>/dev/null | jq 'length' 2>/dev/null)
[ -z "$cs_alerts_24h" ] && cs_alerts_24h=0
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)
if [ -n "$alerts_json" ] && [ "$alerts_json" != "null" ] && [ "$alerts_json" != "[]" ]; then
[ -n "$alerts_json" ] && [ "$alerts_json" != "null" ] && [ "$alerts_json" != "[]" ] && \
cs_alerts_24h=$(echo "$alerts_json" | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
fi
fi
cs_alerts_24h=${cs_alerts_24h:-0}
fi
# Invalid connections (conntrack) - only count INVALID, not UNREPLIED
# Invalid connections
if [ -f /proc/net/nf_conntrack ]; then
# grep -c returns 1 exit code when no matches, so we can't use || echo 0
invalid_conns=$(grep -c "\[INVALID\]" /proc/net/nf_conntrack 2>/dev/null)
[ -z "$invalid_conns" ] && invalid_conns=0
fi
invalid_conns=${invalid_conns:-0}
# HAProxy connections
# Try local haproxy first, then LXC
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}')
@ -387,31 +77,20 @@ get_security_stats() {
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 in LXC container
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
haproxy_conns=${haproxy_conns:-0}
# Clean up and ensure numeric values (remove all non-digits)
wan_drops=$(printf '%s' "$wan_drops" | tr -cd '0-9')
fw_rejects=$(printf '%s' "$fw_rejects" | tr -cd '0-9')
cs_bans=$(printf '%s' "$cs_bans" | tr -cd '0-9')
cs_alerts_24h=$(printf '%s' "$cs_alerts_24h" | tr -cd '0-9')
invalid_conns=$(printf '%s' "$invalid_conns" | tr -cd '0-9')
haproxy_conns=$(printf '%s' "$haproxy_conns" | tr -cd '0-9')
# 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}
# Default to 0 if empty
: ${wan_drops:=0}
: ${fw_rejects:=0}
: ${cs_bans:=0}
: ${cs_alerts_24h:=0}
: ${invalid_conns:=0}
: ${haproxy_conns:=0}
# Output JSON
cat << EOF
{
"wan_dropped": $wan_drops,
@ -419,69 +98,22 @@ get_security_stats() {
"crowdsec_bans": $cs_bans,
"crowdsec_alerts_24h": $cs_alerts_24h,
"invalid_connections": $invalid_conns,
"haproxy_connections": $haproxy_conns,
"timestamp": "$(date -Iseconds)"
"haproxy_connections": $haproxy_conns
}
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 "status"; json_close_object
json_add_object "get_security_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"
@ -494,107 +126,62 @@ case "$1" in
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 mitmdump >/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
;;
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)
# Get mitmproxy threats from threats.log (primary source for WAN protection)
_log_file="/srv/mitmproxy/threats.log"
_threats_json="[]"
if [ -f "$_log_file" ]; then
_threats_json=$(tail -50 "$_log_file" 2>/dev/null | jq -sc '
_threats=$(tail -50 "$_log_file" 2>/dev/null | jq -sc '
map({
ip: .source_ip,
mac: "N/A",
timestamp: .timestamp,
risk_score: (if .severity == "critical" then 90 elif .severity == "high" then 70 elif .severity == "medium" then 50 else 30 end),
severity: .severity,
category: (if .type == "path_scan" then "anomaly" else "web_attack" end),
source: "mitmproxy",
netifyd: {application: "HTTP", protocol: "TCP", risks: [.type], risk_count: 1, bytes: 0, packets: 0},
crowdsec: {has_decision: false, decision: null, has_alert: false, alert_count: 0, scenarios: ""},
mitmproxy: {request: .request, host: .host, pattern: .pattern, country: .country, is_bot: .is_bot, bot_type: .bot_type, cve: .cve, response_code: .response_code}
}) | unique_by(.ip) | sort_by(.risk_score) | reverse
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
printf '{"threats":%s}\n' "$_threats_json"
;;
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}"
_decisions=$($CSCLI decisions list -o json 2>/dev/null || echo "[]")
printf '{"blocked":%s}\n' "$_decisions"
else
echo '{"blocked":[]}'
fi
@ -606,19 +193,15 @@ case "$1" in
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
exit 0
fi
duration=${duration:-4h}
reason=${reason:-"Manual block from Security Threats Dashboard"}
if execute_block "$ip" "$duration" "$reason"; then
elif execute_block "$ip" "$duration" "$reason"; then
json_init
json_add_boolean "success" 1
json_add_string "message" "IP $ip blocked for $duration"
@ -626,7 +209,7 @@ case "$1" in
else
json_init
json_add_boolean "success" 0
json_add_string "error" "Failed to block IP (check if CrowdSec is running)"
json_add_string "error" "Failed to block IP"
json_dump
fi
;;
@ -636,28 +219,25 @@ case "$1" in
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
exit 0
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
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)
@ -670,23 +250,67 @@ case "$1" in
json_add_boolean "success" 0
json_add_string "error" "IP address required"
json_dump
exit 0
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
;;
local section="whitelist_${ip//./_}"
uci delete "secubox_security_threats.${section}" 2>/dev/null
uci commit secubox_security_threats
get_threat_intel)
if [ -x "$THREAT_INTEL" ]; then
"$THREAT_INTEL" status 2>/dev/null
else
echo '{"enabled":false,"error":"threat-intel not available"}'
fi
;;
json_init
json_add_boolean "success" 1
json_add_string "message" "IP $ip removed from whitelist"
json_dump
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_boolean "error" 1
json_add_string "message" "Unknown method: $2"
json_add_string "error" "Unknown method: $2"
json_dump
;;
esac

View File

@ -1,40 +1,28 @@
{
"luci-app-secubox-security-threats": {
"description": "Grant access to SecuBox Security Threats Dashboard",
"description": "Grant access to SecuBox Security Intelligence Dashboard",
"read": {
"ubus": {
"luci.secubox-security-threats": [
"status",
"get_active_threats",
"get_threat_history",
"get_stats_by_type",
"get_stats_by_host",
"get_blocked_ips",
"get_security_stats"
],
"luci.crowdsec-dashboard": [
"decisions",
"alerts",
"status"
],
"luci.netifyd-dashboard": [
"status",
"get_flows",
"get_devices"
"get_security_stats",
"get_threat_intel",
"get_mesh_iocs",
"get_mesh_peers"
]
},
"uci": ["secubox_security_threats", "netifyd", "crowdsec"]
"uci": ["secubox_security_threats"]
},
"write": {
"ubus": {
"luci.secubox-security-threats": [
"block_threat",
"whitelist_host",
"remove_whitelist"
],
"luci.crowdsec-dashboard": [
"ban",
"unban"
"remove_whitelist",
"publish_intel",
"apply_intel"
]
},
"uci": ["secubox_security_threats"]