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 baseclass';
'require rpc'; 'require rpc';
// ==============================================================================
// RPC Method Declarations
// ==============================================================================
var callStatus = rpc.declare({ var callStatus = rpc.declare({
object: 'luci.secubox-security-threats', object: 'luci.secubox-security-threats',
method: 'status', method: 'status',
@ -18,31 +14,18 @@ var callGetActiveThreats = rpc.declare({
expect: { threats: [] } 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({ var callGetBlockedIPs = rpc.declare({
object: 'luci.secubox-security-threats', object: 'luci.secubox-security-threats',
method: 'get_blocked_ips', method: 'get_blocked_ips',
expect: { blocked: [] } expect: { blocked: [] }
}); });
var callGetSecurityStats = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'get_security_stats',
expect: { }
});
var callBlockThreat = rpc.declare({ var callBlockThreat = rpc.declare({
object: 'luci.secubox-security-threats', object: 'luci.secubox-security-threats',
method: 'block_threat', method: 'block_threat',
@ -64,146 +47,42 @@ var callRemoveWhitelist = rpc.declare({
expect: { } expect: { }
}); });
var callGetSecurityStats = rpc.declare({ var callGetThreatIntel = rpc.declare({
object: 'luci.secubox-security-threats', object: 'luci.secubox-security-threats',
method: 'get_security_stats', method: 'get_threat_intel',
expect: { } expect: { }
}); });
// ============================================================================== var callGetMeshIocs = rpc.declare({
// nDPId Integration for Device Detection object: 'luci.secubox-security-threats',
// ============================================================================== method: 'get_mesh_iocs',
var callNdpidStatus = rpc.declare({
object: 'luci.ndpid',
method: 'get_service_status',
expect: { } expect: { }
}); });
var callNdpidFlows = rpc.declare({ var callGetMeshPeers = rpc.declare({
object: 'luci.ndpid', object: 'luci.secubox-security-threats',
method: 'get_detailed_flows', method: 'get_mesh_peers',
expect: { flows: [] } expect: { }
}); });
var callNdpidTopApps = rpc.declare({ var callPublishIntel = rpc.declare({
object: 'luci.ndpid', object: 'luci.secubox-security-threats',
method: 'get_top_applications', method: 'publish_intel',
expect: { applications: [] } expect: { }
}); });
var callNdpidCategories = rpc.declare({ var callApplyIntel = rpc.declare({
object: 'luci.ndpid', object: 'luci.secubox-security-threats',
method: 'get_categories', method: 'apply_intel',
expect: { categories: [] } 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) { function formatRelativeTime(timestamp) {
if (!timestamp) return 'N/A'; if (!timestamp) return '-';
try { try {
var date = new Date(timestamp); var date = new Date(timestamp);
var now = new Date(); var now = new Date();
var seconds = Math.floor((now - date) / 1000); var seconds = Math.floor((now - date) / 1000);
if (seconds < 60) return seconds + 's ago'; if (seconds < 60) return seconds + 's ago';
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago'; if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago'; if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago';
@ -213,275 +92,48 @@ function formatRelativeTime(timestamp) {
} }
} }
/** function formatNumber(n) {
* Format bytes to human-readable size if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
* @param {number} bytes - Byte count if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
* @returns {string} Formatted size (e.g., "1.5 MB") return String(n || 0);
*/
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];
} }
/**
* 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() { function getDashboardData() {
return Promise.all([ return Promise.all([
callStatus(), callStatus(),
callGetActiveThreats(), callGetActiveThreats(),
callGetStatsByType(),
callGetBlockedIPs(), callGetBlockedIPs(),
callGetSecurityStats(), callGetSecurityStats(),
callNdpidStatus().catch(function() { return { running: false, dpi_available: false }; }), callGetThreatIntel().catch(function() { return {}; }),
callNdpidFlows().catch(function() { return { flows: [] }; }), callGetMeshIocs().catch(function() { return { iocs: [] }; }),
callNdpidTopApps().catch(function() { return { applications: [] }; }) callGetMeshPeers().catch(function() { return { peers: [] }; })
]).then(function(results) { ]).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 { return {
status: results[0] || {}, status: results[0] || {},
threats: results[1].threats || [], threats: results[1].threats || [],
stats: results[2] || {}, blocked: results[2].blocked || [],
blocked: results[3].blocked || [], securityStats: results[3] || {},
securityStats: results[4] || {}, threatIntel: results[4] || {},
ndpid: { meshIocs: results[5].iocs || [],
running: dpiRunning, meshPeers: results[6].peers || []
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
}; };
}); });
} }
// ==============================================================================
// Exports
// ==============================================================================
return baseclass.extend({ return baseclass.extend({
// RPC Methods
getStatus: callStatus, getStatus: callStatus,
getActiveThreats: callGetActiveThreats, getActiveThreats: callGetActiveThreats,
getThreatHistory: callGetThreatHistory,
getStatsByType: callGetStatsByType,
getStatsByHost: callGetStatsByHost,
getBlockedIPs: callGetBlockedIPs, getBlockedIPs: callGetBlockedIPs,
getSecurityStats: callGetSecurityStats, getSecurityStats: callGetSecurityStats,
blockThreat: callBlockThreat, blockThreat: callBlockThreat,
whitelistHost: callWhitelistHost, whitelistHost: callWhitelistHost,
removeWhitelist: callRemoveWhitelist, removeWhitelist: callRemoveWhitelist,
getThreatIntel: callGetThreatIntel,
// nDPId Methods getMeshIocs: callGetMeshIocs,
getNdpidStatus: callNdpidStatus, getMeshPeers: callGetMeshPeers,
getNdpidFlows: callNdpidFlows, publishIntel: callPublishIntel,
getNdpidTopApps: callNdpidTopApps, applyIntel: callApplyIntel,
getNdpidCategories: callNdpidCategories,
// Utility Functions
getSeverityColor: getSeverityColor,
getThreatIcon: getThreatIcon,
formatRiskFlags: formatRiskFlags,
getCategoryLabel: getCategoryLabel,
formatDuration: formatDuration,
formatTimestamp: formatTimestamp,
formatRelativeTime: formatRelativeTime, formatRelativeTime: formatRelativeTime,
formatBytes: formatBytes, formatNumber: formatNumber,
getSeverityBadge: getSeverityBadge,
// Device Classification
classifyDevice: classifyDevice,
getSuggestedRules: getSuggestedRules,
getDeviceIcon: getDeviceIcon,
deviceTypes: deviceTypes,
networkZones: networkZones,
// Composite Fetchers
getDashboardData: getDashboardData getDashboardData: getDashboardData
}); });

View File

@ -1,385 +1,75 @@
#!/bin/sh #!/bin/sh
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SecuBox Security Threats Dashboard RPCD backend # SecuBox Security Intelligence - RPCD backend
# Copyright (C) 2026 CyberMind.fr - Gandalf # Copyright (C) 2026 CyberMind.fr - Gandalf
# #
# Integrates netifyd DPI security risks with CrowdSec threat intelligence # Combines mitmproxy threat detection, CrowdSec blocking,
# for comprehensive network threat monitoring and automated blocking # and P2P mesh threat intelligence sharing
. /lib/functions.sh . /lib/functions.sh
. /usr/share/libubox/jshn.sh . /usr/share/libubox/jshn.sh
HISTORY_FILE="/tmp/secubox-threats-history.json"
CSCLI="/usr/bin/cscli" CSCLI="/usr/bin/cscli"
SECCUBOX_LOG="/usr/sbin/secubox-log" THREAT_INTEL="/usr/lib/secubox/threat-intel.sh"
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 # 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() { execute_block() {
local ip="$1" local ip="$1" duration="$2" reason="$3"
local duration="$2" [ -x "$CSCLI" ] || return 1
local reason="$3" $CSCLI decisions add --ip "$ip" --duration "$duration" --reason "$reason" >/dev/null 2>&1
[ ! -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) # SECURITY STATS (firewall counters)
# ============================================================================== # ==============================================================================
# Get overall security statistics from all sources
get_security_stats() { get_security_stats() {
local wan_drops=0 local wan_drops=0 fw_rejects=0 cs_bans=0 cs_alerts_24h=0 haproxy_conns=0 invalid_conns=0
local fw_rejects=0
local cs_bans=0
local cs_alerts_24h=0
local haproxy_conns=0
local invalid_conns=0
# Get actual WAN interface from UCI # WAN dropped packets from nftables
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)
if command -v nft >/dev/null 2>&1; then 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 | \ 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}')
grep -oE 'packets [0-9]+' | \
awk '{sum+=$2} END {print sum+0}')
[ -z "$wan_drops" ] && wan_drops=0
# Also try reject chain
local reject_drops=$(nft list chain inet fw4 reject_from_wan 2>/dev/null | \ 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}')
grep -oE 'packets [0-9]+' | \ [ -n "$reject_drops" ] && wan_drops=$((${wan_drops:-0} + reject_drops))
awk '{sum+=$2} END {print sum+0}')
[ -n "$reject_drops" ] && wan_drops=$((wan_drops + reject_drops))
fi fi
wan_drops=${wan_drops:-0}
# Firewall rejects - count from reject-specific chains # Firewall rejects
if command -v nft >/dev/null 2>&1; then 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 | \ 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}')
grep -oE 'packets [0-9]+' | \ if [ "${fw_rejects:-0}" = "0" ]; then
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
fw_rejects=$(nft -a list ruleset 2>/dev/null | \ fw_rejects=$(nft -a list ruleset 2>/dev/null | \
grep -E "reject|drop" | grep "counter" | \ grep -E "reject|drop" | grep "counter" | \
grep -oE 'packets [0-9]+' | \ grep -oE 'packets [0-9]+' | awk '{sum+=$2} END {print sum+0}')
awk '{sum+=$2} END {print sum+0}')
fi fi
else
# Fallback to log parsing
fw_rejects=$(logread 2>/dev/null | grep -c "reject\|DROP\|REJECT" || echo 0)
fi fi
fw_rejects=${fw_rejects:-0}
# CrowdSec active bans # CrowdSec bans and alerts
if [ -x "$CSCLI" ]; then if [ -x "$CSCLI" ]; then
# Use jq for proper JSON parsing if available, fallback to grep
if command -v jq >/dev/null 2>&1; 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_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) 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 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) 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) cs_alerts_24h=$(echo "$alerts_json" | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
fi
fi fi
cs_alerts_24h=${cs_alerts_24h:-0}
fi fi
# Invalid connections (conntrack) - only count INVALID, not UNREPLIED # Invalid connections
if [ -f /proc/net/nf_conntrack ]; then 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) invalid_conns=$(grep -c "\[INVALID\]" /proc/net/nf_conntrack 2>/dev/null)
[ -z "$invalid_conns" ] && invalid_conns=0
fi fi
invalid_conns=${invalid_conns:-0}
# HAProxy connections # HAProxy connections
# Try local haproxy first, then LXC
if [ -S /var/run/haproxy/admin.sock ]; then if [ -S /var/run/haproxy/admin.sock ]; then
haproxy_conns=$(echo "show stat" | socat stdio /var/run/haproxy/admin.sock 2>/dev/null | \ 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}') 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 | \ 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}') 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 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 ' 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 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; } [ -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}') done' 2>/dev/null | tail -n+2 | awk -F, '{sum+=$5} END {print sum+0}')
fi fi
haproxy_conns=${haproxy_conns:-0}
# Clean up and ensure numeric values (remove all non-digits) # Sanitize to numeric
wan_drops=$(printf '%s' "$wan_drops" | tr -cd '0-9') wan_drops=$(printf '%s' "${wan_drops:-0}" | tr -cd '0-9'); : ${wan_drops:=0}
fw_rejects=$(printf '%s' "$fw_rejects" | tr -cd '0-9') fw_rejects=$(printf '%s' "${fw_rejects:-0}" | tr -cd '0-9'); : ${fw_rejects:=0}
cs_bans=$(printf '%s' "$cs_bans" | tr -cd '0-9') cs_bans=$(printf '%s' "${cs_bans:-0}" | tr -cd '0-9'); : ${cs_bans:=0}
cs_alerts_24h=$(printf '%s' "$cs_alerts_24h" | tr -cd '0-9') cs_alerts_24h=$(printf '%s' "${cs_alerts_24h:-0}" | tr -cd '0-9'); : ${cs_alerts_24h:=0}
invalid_conns=$(printf '%s' "$invalid_conns" | tr -cd '0-9') invalid_conns=$(printf '%s' "${invalid_conns:-0}" | tr -cd '0-9'); : ${invalid_conns:=0}
haproxy_conns=$(printf '%s' "$haproxy_conns" | tr -cd '0-9') 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 cat << EOF
{ {
"wan_dropped": $wan_drops, "wan_dropped": $wan_drops,
@ -419,69 +98,22 @@ get_security_stats() {
"crowdsec_bans": $cs_bans, "crowdsec_bans": $cs_bans,
"crowdsec_alerts_24h": $cs_alerts_24h, "crowdsec_alerts_24h": $cs_alerts_24h,
"invalid_connections": $invalid_conns, "invalid_connections": $invalid_conns,
"haproxy_connections": $haproxy_conns, "haproxy_connections": $haproxy_conns
"timestamp": "$(date -Iseconds)"
} }
EOF 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 # UBUS INTERFACE
# ============================================================================== # ==============================================================================
case "$1" in case "$1" in
list) list)
# List available methods
json_init json_init
json_add_object "get_security_stats" json_add_object "status"; json_close_object
json_close_object json_add_object "get_security_stats"; json_close_object
json_add_object "status" json_add_object "get_active_threats"; json_close_object
json_close_object json_add_object "get_blocked_ips"; 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_object "block_threat"
json_add_string "ip" "string" json_add_string "ip" "string"
json_add_string "duration" "string" json_add_string "duration" "string"
@ -494,107 +126,62 @@ case "$1" in
json_add_object "remove_whitelist" json_add_object "remove_whitelist"
json_add_string "ip" "string" json_add_string "ip" "string"
json_close_object 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 json_dump
;; ;;
call) call)
case "$2" in 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)
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_active_threats)
# Get mitmproxy threats from threats.log (primary source for WAN protection)
_log_file="/srv/mitmproxy/threats.log" _log_file="/srv/mitmproxy/threats.log"
_threats_json="[]"
if [ -f "$_log_file" ]; then 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({ map({
ip: .source_ip, 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, severity: .severity,
category: (if .type == "path_scan" then "anomaly" else "web_attack" end), score: (if .severity == "critical" then 90
source: "mitmproxy", elif .severity == "high" then 70
netifyd: {application: "HTTP", protocol: "TCP", risks: [.type], risk_count: 1, bytes: 0, packets: 0}, elif .severity == "medium" then 50
crowdsec: {has_decision: false, decision: null, has_alert: false, alert_count: 0, scenarios: ""}, else 30 end),
mitmproxy: {request: .request, host: .host, pattern: .pattern, country: .country, is_bot: .is_bot, bot_type: .bot_type, cve: .cve, response_code: .response_code} type: .type,
}) | unique_by(.ip) | sort_by(.risk_score) | reverse 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 "[]") ' 2>/dev/null || echo "[]")
printf '{"threats":%s}\n' "$_threats"
else
echo '{"threats":[]}'
fi 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) get_blocked_ips)
if [ -x "$CSCLI" ]; then if [ -x "$CSCLI" ]; then
local decisions=$(get_crowdsec_decisions) _decisions=$($CSCLI decisions list -o json 2>/dev/null || echo "[]")
echo "{\"blocked\":$decisions}" printf '{"blocked":%s}\n' "$_decisions"
else else
echo '{"blocked":[]}' echo '{"blocked":[]}'
fi fi
@ -606,19 +193,15 @@ case "$1" in
json_get_var ip ip json_get_var ip ip
json_get_var duration duration json_get_var duration duration
json_get_var reason reason json_get_var reason reason
duration=${duration:-4h}
reason=${reason:-"Manual block from Security Dashboard"}
if [ -z "$ip" ]; then if [ -z "$ip" ]; then
json_init json_init
json_add_boolean "success" 0 json_add_boolean "success" 0
json_add_string "error" "IP address required" json_add_string "error" "IP address required"
json_dump json_dump
exit 0 elif execute_block "$ip" "$duration" "$reason"; then
fi
duration=${duration:-4h}
reason=${reason:-"Manual block from Security Threats Dashboard"}
if execute_block "$ip" "$duration" "$reason"; then
json_init json_init
json_add_boolean "success" 1 json_add_boolean "success" 1
json_add_string "message" "IP $ip blocked for $duration" json_add_string "message" "IP $ip blocked for $duration"
@ -626,7 +209,7 @@ case "$1" in
else else
json_init json_init
json_add_boolean "success" 0 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 json_dump
fi fi
;; ;;
@ -636,28 +219,25 @@ case "$1" in
json_load "$input" json_load "$input"
json_get_var ip ip json_get_var ip ip
json_get_var reason reason json_get_var reason reason
reason=${reason:-"Whitelisted from Security Dashboard"}
if [ -z "$ip" ]; then if [ -z "$ip" ]; then
json_init json_init
json_add_boolean "success" 0 json_add_boolean "success" 0
json_add_string "error" "IP address required" json_add_string "error" "IP address required"
json_dump 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 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) remove_whitelist)
@ -670,23 +250,67 @@ case "$1" in
json_add_boolean "success" 0 json_add_boolean "success" 0
json_add_string "error" "IP address required" json_add_string "error" "IP address required"
json_dump 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 fi
;;
local section="whitelist_${ip//./_}" get_threat_intel)
uci delete "secubox_security_threats.${section}" 2>/dev/null if [ -x "$THREAT_INTEL" ]; then
uci commit secubox_security_threats "$THREAT_INTEL" status 2>/dev/null
else
echo '{"enabled":false,"error":"threat-intel not available"}'
fi
;;
json_init get_mesh_iocs)
json_add_boolean "success" 1 if [ -x "$THREAT_INTEL" ]; then
json_add_string "message" "IP $ip removed from whitelist" _iocs=$("$THREAT_INTEL" list received 2>/dev/null)
json_dump 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_init
json_add_boolean "error" 1 json_add_string "error" "Unknown method: $2"
json_add_string "message" "Unknown method: $2"
json_dump json_dump
;; ;;
esac esac

View File

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