From c4302504df331de388783483a58f4862b2554f2f Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 3 Feb 2026 11:30:25 +0100 Subject: [PATCH] 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 --- .../resources/secubox-security-threats/api.js | 432 +------ .../secubox-security-threats/dashboard.js | 1022 +++++------------ .../rpcd/luci.secubox-security-threats | 658 +++-------- .../luci-app-secubox-security-threats.json | 30 +- 4 files changed, 476 insertions(+), 1666 deletions(-) diff --git a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js index bb69e5ba..500dfa1f 100644 --- a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js +++ b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js @@ -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 '' + label + ''; -} - -/** - * 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 }); diff --git a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js index bfc77c0c..d91f8258 100644 --- a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js +++ b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js @@ -13,826 +13,372 @@ return L.view.extend({ render: function(data) { var self = this; data = data || {}; - var threats = data.threats || []; var status = data.status || {}; - var stats = data.stats || {}; + var threats = data.threats || []; + var stats = data.securityStats || {}; var blocked = data.blocked || []; - var securityStats = data.securityStats || {}; - var devices = data.devices || []; - var ndpid = data.ndpid || {}; - var zones = data.zones || {}; + var intel = data.threatIntel || {}; + var meshIocs = data.meshIocs || []; + var meshPeers = data.meshPeers || []; - // Calculate statistics - var threatStats = { - total: threats.length, - critical: threats.filter(function(t) { return t.severity === 'critical'; }).length, - high: threats.filter(function(t) { return t.severity === 'high'; }).length, - medium: threats.filter(function(t) { return t.severity === 'medium'; }).length, - low: threats.filter(function(t) { return t.severity === 'low'; }).length, - avg_score: threats.length > 0 ? - Math.round(threats.reduce(function(sum, t) { return sum + t.risk_score; }, 0) / threats.length) : 0 - }; + poll.add(L.bind(function() { this.handleRefresh(); }, this), 15); - // Setup auto-refresh polling (every 10 seconds) - poll.add(L.bind(function() { - this.handleRefresh(); - }, this), 10); - - return E('div', { 'class': 'threats-dashboard' }, [ + return E('div', { 'class': 'si-dash' }, [ E('style', {}, this.getStyles()), - // Quick Actions Bar - this.renderQuickActions(status, ndpid), + // Status bar + this.renderStatusBar(status), - // Hero Banner - this.renderHeroBanner(threatStats), + // Firewall stats + this.renderFirewallStats(stats), - // Firewall Stats - this.renderFirewallStats(securityStats), + // Mesh Intelligence + this.renderMeshIntel(intel, meshIocs, meshPeers), - // Threat Overview Cards - this.renderThreatOverview(threatStats, blocked.length), + // Threats table + this.renderThreats(threats), - // Distribution & Gauge Row - E('div', { 'class': 'two-col-section' }, [ - this.renderThreatDistribution(stats), - this.renderRiskGauge(threatStats.avg_score) - ]), - - // Devices & Zoning Section (nDPId powered) - this.renderDevicesSection(devices, zones, ndpid), - - // Threats Table - this.renderThreatsSection(threats.slice(0, 10)) + // Blocked IPs (collapsed) + this.renderBlocked(blocked) ]); }, - renderQuickActions: function(status, ndpid) { + renderStatusBar: function(status) { var self = this; - var allGood = status.netifyd_running && status.crowdsec_running; - ndpid = ndpid || {}; + var services = [ + { name: 'CrowdSec', ok: status.crowdsec_running }, + { name: 'netifyd', ok: status.netifyd_running }, + { name: 'mitmproxy', ok: status.mitmproxy_running }, + { name: 'Threat Intel', ok: status.threat_intel_available } + ]; + var allOk = services.every(function(s) { return s.ok; }); - return E('div', { 'class': 'quick-actions-bar' }, [ - E('div', { 'class': 'actions-left' }, [ - E('div', { 'class': 'status-indicator ' + (allGood ? 'good' : 'warn') }, [ - E('span', { 'class': 'status-dot' }), - E('span', {}, allGood ? 'All Systems Operational' : 'Service Issues Detected') - ]), - E('div', { 'class': 'service-badges' }, [ - E('span', { 'class': 'service-badge ' + (status.netifyd_running ? 'active' : 'inactive') }, [ - '🔍 netifyd' - ]), - E('span', { 'class': 'service-badge ' + (status.crowdsec_running ? 'active' : 'inactive') }, [ - '🛡️ CrowdSec' - ]), - E('span', { 'class': 'service-badge ' + (ndpid.running ? 'active' : 'inactive') }, [ - '📡 DPI', - ndpid.flow_count ? ' (' + ndpid.flow_count + ')' : '' - ]) - ]) - ]), - E('div', { 'class': 'actions-right' }, [ - E('button', { - 'class': 'action-btn refresh', - 'click': function() { self.handleRefresh(); } - }, ['🔃 ', 'Refresh']), - E('button', { - 'class': 'action-btn scan', - 'click': function() { self.handleScan(); } - }, ['📡 ', 'Scan Now']), - E('a', { - 'class': 'action-btn settings', - 'href': L.url('admin/services/crowdsec-dashboard/settings') - }, ['⚙️ ', 'Settings']) - ]) - ]); - }, - - renderHeroBanner: function(stats) { - var level = 'secure'; - var icon = '🛡️'; - var message = 'Network Protected'; - - if (stats.critical > 0) { - level = 'critical'; - icon = '🚨'; - message = 'Critical Threats Detected!'; - } else if (stats.high > 0) { - level = 'high'; - icon = '⚠️'; - message = 'High Risk Activity'; - } else if (stats.total > 0) { - level = 'medium'; - icon = '👁️'; - message = 'Monitoring Threats'; - } - - return E('div', { 'class': 'hero-banner ' + level }, [ - E('div', { 'class': 'hero-bg' }), - E('div', { 'class': 'hero-content' }, [ - E('div', { 'class': 'hero-icon' }, icon), - E('h1', { 'class': 'hero-title' }, 'Security Threats Dashboard'), - E('p', { 'class': 'hero-subtitle' }, message), - E('div', { 'class': 'hero-badges' }, [ - E('span', { 'class': 'badge blue' }, '🔍 Deep Packet Inspection'), - E('span', { 'class': 'badge purple' }, '🛡️ CrowdSec Intelligence'), - E('span', { 'class': 'badge green' }, '⚡ Real-time Detection'), - E('span', { 'class': 'badge orange' }, '🔒 Auto-blocking') - ]), - E('p', { 'class': 'hero-desc' }, - 'Real-time threat detection combining netifyd DPI analysis with CrowdSec threat intelligence ' + - 'for comprehensive network security monitoring and automated response.' + return E('div', { 'class': 'si-status-bar' }, [ + E('div', { 'class': 'si-status-left' }, [ + E('span', { 'class': 'si-dot ' + (allOk ? 'ok' : 'warn') }), + E('span', {}, allOk ? 'All Systems Operational' : 'Service Issues'), + E('span', { 'class': 'si-svc-list' }, + services.map(function(s) { + return E('span', { 'class': 'si-svc ' + (s.ok ? 'ok' : 'off') }, s.name); + }) ) + ]), + E('div', { 'class': 'si-status-right' }, [ + E('button', { 'class': 'cbi-button', 'click': function() { self.handleRefresh(); } }, 'Refresh') ]) ]); }, renderFirewallStats: function(stats) { - var formatNumber = function(n) { - if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; - if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; - return String(n || 0); - }; - + var fmt = API.formatNumber; var items = [ - { icon: '🚫', value: formatNumber(stats.wan_dropped), label: 'WAN Dropped', desc: 'Packets blocked', color: 'blue' }, - { icon: '🔥', value: formatNumber(stats.firewall_rejects), label: 'FW Rejects', desc: 'Firewall blocks', color: 'red' }, - { icon: '⛔', value: formatNumber(stats.crowdsec_bans), label: 'CrowdSec Bans', desc: 'Active IP bans', color: 'purple' }, - { icon: '🔔', value: formatNumber(stats.crowdsec_alerts_24h), label: 'Alerts 24h', desc: 'Recent detections', color: 'orange' }, - { icon: '❌', value: formatNumber(stats.invalid_connections), label: 'Invalid Conns', desc: 'Conntrack anomalies', color: 'gray' }, - { icon: '🔄', value: formatNumber(stats.haproxy_connections), label: 'HAProxy', desc: 'Proxy sessions', color: 'teal' } + { label: 'WAN Dropped', value: fmt(stats.wan_dropped), cls: 'blue' }, + { label: 'FW Rejects', value: fmt(stats.firewall_rejects), cls: 'red' }, + { label: 'CrowdSec Bans', value: fmt(stats.crowdsec_bans), cls: 'purple' }, + { label: 'Alerts 24h', value: fmt(stats.crowdsec_alerts_24h), cls: 'orange' }, + { label: 'Invalid Conns', value: fmt(stats.invalid_connections), cls: 'gray' }, + { label: 'HAProxy', value: fmt(stats.haproxy_connections), cls: 'teal' } ]; - return E('div', { 'class': 'section firewall-section' }, [ - E('h2', { 'class': 'section-title' }, [ - E('span', { 'class': 'title-icon' }, '🔥'), - 'Firewall & Network Protection' - ]), - E('div', { 'class': 'fw-stats-grid' }, + return E('div', { 'class': 'si-section' }, [ + E('h3', {}, 'Firewall & Network Protection'), + E('div', { 'class': 'si-stats-grid' }, items.map(function(item) { - return E('div', { 'class': 'fw-stat-card ' + item.color }, [ - E('div', { 'class': 'fw-icon' }, item.icon), - E('div', { 'class': 'fw-value' }, item.value), - E('div', { 'class': 'fw-label' }, item.label), - E('div', { 'class': 'fw-desc' }, item.desc) + return E('div', { 'class': 'si-stat ' + item.cls }, [ + E('div', { 'class': 'si-stat-val' }, item.value), + E('div', { 'class': 'si-stat-label' }, item.label) ]); }) ) ]); }, - renderThreatOverview: function(stats, blockedCount) { - var items = [ - { icon: '🎯', value: stats.total, label: 'Active Threats', color: 'blue' }, - { icon: '🚨', value: stats.critical, label: 'Critical', color: 'red' }, - { icon: '⚠️', value: stats.high, label: 'High Risk', color: 'orange' }, - { icon: '📊', value: stats.avg_score + '/100', label: 'Avg Risk Score', color: 'yellow' }, - { icon: '🛡️', value: blockedCount, label: 'Blocked IPs', color: 'purple' } - ]; + renderMeshIntel: function(intel, iocs, peers) { + var self = this; + var enabled = intel.enabled; - return E('div', { 'class': 'section' }, [ - E('h2', { 'class': 'section-title' }, [ - E('span', { 'class': 'title-icon' }, '📈'), - 'Threat Overview' - ]), - E('div', { 'class': 'overview-grid' }, - items.map(function(item) { - return E('div', { 'class': 'overview-card ' + item.color }, [ - E('div', { 'class': 'card-icon' }, item.icon), - E('div', { 'class': 'card-info' }, [ - E('div', { 'class': 'card-value' }, String(item.value)), - E('div', { 'class': 'card-label' }, item.label) - ]) - ]); - }) - ) - ]); - }, - - renderThreatDistribution: function(stats) { - var categories = [ - { label: 'Malware', value: stats.malware || 0, color: '#e74c3c', icon: '🦠' }, - { label: 'Web Attack', value: stats.web_attack || 0, color: '#e67e22', icon: '⚔️' }, - { label: 'Anomaly', value: stats.anomaly || 0, color: '#f39c12', icon: '👁️' }, - { label: 'Protocol', value: stats.protocol || 0, color: '#9b59b6', icon: '🚫' }, - { label: 'TLS Issue', value: stats.tls_issue || 0, color: '#3498db', icon: '🔒' } - ]; - - var total = categories.reduce(function(sum, cat) { return sum + cat.value; }, 0); - - return E('div', { 'class': 'dist-card' }, [ - E('h3', { 'class': 'card-title' }, ['📊 ', 'Threat Distribution']), - E('div', { 'class': 'dist-content' }, - total === 0 ? - [E('div', { 'class': 'empty-state' }, [ - E('div', { 'class': 'empty-icon' }, '✅'), - E('div', {}, 'No threats detected') - ])] : - categories.filter(function(cat) { return cat.value > 0; }).map(function(cat) { - var percentage = Math.round((cat.value / total) * 100); - return E('div', { 'class': 'dist-item' }, [ - E('div', { 'class': 'dist-header' }, [ - E('span', { 'class': 'dist-label' }, [cat.icon, ' ', cat.label]), - E('span', { 'class': 'dist-value' }, cat.value + ' (' + percentage + '%)') - ]), - E('div', { 'class': 'dist-bar-bg' }, [ - E('div', { 'class': 'dist-bar', 'style': 'width: ' + percentage + '%; background: ' + cat.color + ';' }) - ]) - ]); - }) - ) - ]); - }, - - renderRiskGauge: function(avgScore) { - var level, color, icon, description; - if (avgScore >= 80) { - level = 'CRITICAL'; - color = '#e74c3c'; - icon = '🚨'; - description = 'Immediate action required'; - } else if (avgScore >= 60) { - level = 'HIGH'; - color = '#e67e22'; - icon = '⚠️'; - description = 'Review threats promptly'; - } else if (avgScore >= 40) { - level = 'MEDIUM'; - color = '#f39c12'; - icon = '👁️'; - description = 'Monitor situation'; - } else { - level = 'LOW'; - color = '#2ecc71'; - icon = '✅'; - description = 'Normal security posture'; + if (!enabled) { + return E('div', { 'class': 'si-section' }, [ + E('h3', {}, 'Mesh Intelligence'), + E('div', { 'class': 'si-notice' }, 'Threat intelligence sharing is not available. Install secubox-p2p.') + ]); } - return E('div', { 'class': 'gauge-card' }, [ - E('h3', { 'class': 'card-title' }, ['🎯 ', 'Risk Level']), - E('div', { 'class': 'gauge-content' }, [ - E('div', { 'class': 'gauge-icon' }, icon), - E('div', { 'class': 'gauge-score', 'style': 'color: ' + color + ';' }, avgScore), - E('div', { 'class': 'gauge-level', 'style': 'color: ' + color + ';' }, level), - E('div', { 'class': 'gauge-desc' }, description), - E('div', { 'class': 'gauge-bar' }, [ - E('div', { 'class': 'gauge-fill', 'style': 'width: ' + avgScore + '%;' }), - E('div', { 'class': 'gauge-marker', 'style': 'left: ' + avgScore + '%;' }) - ]) - ]) - ]); - }, + var cards = [ + { label: 'Local IOCs Shared', value: String(intel.local_iocs || 0), cls: 'blue' }, + { label: 'Received from Mesh', value: String(intel.received_iocs || 0), cls: 'green' }, + { label: 'Applied to Firewall', value: String(intel.applied_iocs || 0), cls: 'purple' }, + { label: 'Peer Contributors', value: String(intel.peer_contributors || 0), cls: 'teal' }, + { label: 'Chain Blocks', value: String(intel.chain_threat_blocks || 0), cls: 'orange' } + ]; - renderDevicesSection: function(devices, zones, ndpid) { - var self = this; - zones = zones || {}; + return E('div', { 'class': 'si-section' }, [ + E('h3', {}, 'Mesh Intelligence'), - // Group devices by suggested zone - var devicesByZone = {}; - devices.forEach(function(dev) { - var zone = dev.suggestedZone || 'guest'; - if (!devicesByZone[zone]) devicesByZone[zone] = []; - devicesByZone[zone].push(dev); - }); - - return E('div', { 'class': 'section devices-section' }, [ - E('h2', { 'class': 'section-title' }, [ - E('span', { 'class': 'title-icon' }, '📱'), - 'Devices & Smart Zoning', - E('span', { 'class': 'powered-badge' }, '🔬 nDPId Powered') - ]), - - // nDPId status notice - !ndpid.running ? - E('div', { 'class': 'notice warning' }, [ - E('span', {}, '⚠️'), - ' nDPId not running - Start it for automatic device detection and zoning suggestions' - ]) : null, - - // Zone legend - E('div', { 'class': 'zones-legend' }, - Object.keys(zones).map(function(zoneKey) { - var zone = zones[zoneKey]; - return E('div', { 'class': 'zone-chip', 'style': 'border-color: ' + zone.color }, [ - E('span', { 'class': 'zone-icon' }, zone.icon), - E('span', { 'class': 'zone-name' }, zoneKey), - E('span', { 'class': 'zone-count' }, String((devicesByZone[zoneKey] || []).length)) + // Summary cards + E('div', { 'class': 'si-stats-grid' }, + cards.map(function(c) { + return E('div', { 'class': 'si-stat ' + c.cls }, [ + E('div', { 'class': 'si-stat-val' }, c.value), + E('div', { 'class': 'si-stat-label' }, c.label) ]); }) ), - // Devices grid - devices.length === 0 ? - E('div', { 'class': 'empty-devices' }, [ - E('div', { 'class': 'empty-icon' }, '📡'), - E('div', { 'class': 'empty-text' }, 'No devices detected'), - E('div', { 'class': 'empty-subtext' }, ndpid.running ? 'Waiting for network activity...' : 'Enable nDPId for device detection') - ]) : - E('div', { 'class': 'devices-grid' }, - devices.slice(0, 12).map(function(dev) { - var zoneInfo = zones[dev.suggestedZone] || zones.guest; - return E('div', { 'class': 'device-card', 'style': 'border-left-color: ' + zoneInfo.color }, [ - E('div', { 'class': 'device-header' }, [ - E('span', { 'class': 'device-icon' }, dev.icon || '📟'), - E('div', { 'class': 'device-info' }, [ - E('div', { 'class': 'device-ip' }, dev.ip), - E('div', { 'class': 'device-hostname' }, dev.hostname || dev.mac || '-') - ]), - E('span', { 'class': 'zone-badge', 'style': 'background: ' + zoneInfo.color }, [ - zoneInfo.icon, ' ', dev.suggestedZone - ]) - ]), - E('div', { 'class': 'device-apps' }, [ - E('span', { 'class': 'apps-label' }, '📊 Apps: '), - dev.apps.length > 0 ? - dev.apps.slice(0, 3).map(function(app) { - return E('span', { 'class': 'app-tag' }, app); - }) : - E('span', { 'class': 'no-apps' }, 'None detected') - ]), - E('div', { 'class': 'device-stats' }, [ - E('span', {}, '📥 ' + self.formatBytes(dev.bytes_rx)), - E('span', {}, '📤 ' + self.formatBytes(dev.bytes_tx)), - E('span', {}, '🔗 ' + dev.flows + ' flows') - ]), - E('div', { 'class': 'device-actions' }, [ - E('button', { - 'class': 'btn-zone', - 'click': function() { self.showZoneDialog(dev); } - }, '🎯 Assign Zone'), - E('button', { - 'class': 'btn-rules', - 'click': function() { self.showRulesDialog(dev); } - }, '🔥 View Rules') - ]) - ]); - }) - ), + // Actions + E('div', { 'class': 'si-actions' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { self.handlePublish(); } + }, 'Publish Now'), + E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': function() { self.handleApplyIntel(); } + }, 'Apply Pending'), + E('span', { 'class': 'si-meta' }, + 'Min severity: ' + (intel.min_severity || 'high') + + ' | TTL: ' + Math.round((intel.ioc_ttl || 86400) / 3600) + 'h' + + ' | Transitive: ' + (intel.apply_transitive ? 'yes' : 'no') + ) + ]), - // Quick zone assignment - devices.length > 0 ? - E('div', { 'class': 'quick-zone-actions' }, [ - E('span', { 'class': 'quick-label' }, '⚡ Quick Actions:'), - E('button', { 'class': 'btn-auto-zone', 'click': function() { self.autoAssignZones(devices); } }, '🤖 Auto-Assign All'), - E('button', { 'class': 'btn-export-rules', 'click': function() { self.exportFirewallRules(devices); } }, '📋 Export Rules') + // Peer table + peers.length > 0 ? + E('div', { 'class': 'si-subsection' }, [ + E('h4', {}, 'Peer Contributors'), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Node'), + E('th', { 'class': 'th' }, 'Trust'), + E('th', { 'class': 'th' }, 'IOCs'), + E('th', { 'class': 'th' }, 'Last Seen') + ]) + ].concat( + peers.map(function(p) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td si-mono' }, (p.node || '-').substring(0, 12)), + E('td', { 'class': 'td' }, E('span', { 'class': 'si-trust si-trust-' + (p.trust || 'unknown') }, p.trust || 'unknown')), + E('td', { 'class': 'td' }, String(p.ioc_count || 0)), + E('td', { 'class': 'td' }, p.last_seen ? new Date(p.last_seen * 1000).toLocaleString() : '-') + ]); + }) + )) + ]) : null, + + // Received IOCs table (show top 10) + iocs.length > 0 ? + E('div', { 'class': 'si-subsection' }, [ + E('h4', {}, 'Received IOCs (' + iocs.length + ')'), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'IP'), + E('th', { 'class': 'th' }, 'Severity'), + E('th', { 'class': 'th' }, 'Source'), + E('th', { 'class': 'th' }, 'Scenario'), + E('th', { 'class': 'th' }, 'Node'), + E('th', { 'class': 'th' }, 'Trust') + ]) + ].concat( + iocs.slice(0, 10).map(function(ioc) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td si-mono' }, ioc.ip || '-'), + E('td', { 'class': 'td' }, E('span', { 'class': 'si-sev si-sev-' + (ioc.severity || 'low') }, ioc.severity || '-')), + E('td', { 'class': 'td' }, ioc.source || '-'), + E('td', { 'class': 'td' }, ioc.scenario || '-'), + E('td', { 'class': 'td si-mono' }, (ioc.node || '-').substring(0, 10)), + E('td', { 'class': 'td' }, ioc.trust || '-') + ]); + }) + )) ]) : null ]); }, - formatBytes: function(bytes) { - if (!bytes || bytes === 0) return '0 B'; - var k = 1024; - var sizes = ['B', 'KB', 'MB', 'GB']; - var i = Math.floor(Math.log(bytes) / Math.log(k)); - return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]; - }, - - showZoneDialog: function(device) { - var zones = API.networkZones; - ui.showModal(_('Assign Network Zone'), [ - E('div', { 'class': 'zone-dialog' }, [ - E('p', {}, ['Assign ', E('strong', {}, device.ip), ' to a network zone:']), - E('div', { 'class': 'zone-options' }, - Object.keys(zones).map(function(zoneKey) { - var zone = zones[zoneKey]; - return E('button', { - 'class': 'zone-option', - 'style': 'border-color: ' + zone.color, - 'click': function() { - ui.hideModal(); - ui.addNotification(null, E('p', {}, device.ip + ' assigned to ' + zoneKey + ' zone'), 'info'); - } - }, [ - E('span', { 'class': 'zo-icon' }, zone.icon), - E('span', { 'class': 'zo-name' }, zoneKey), - E('span', { 'class': 'zo-desc' }, zone.desc) - ]); - }) - ) - ]), - E('div', { 'class': 'right' }, [ - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')) - ]) - ]); - }, - - showRulesDialog: function(device) { - var rules = device.suggestedRules || []; - ui.showModal(_('Suggested Firewall Rules'), [ - E('div', { 'class': 'rules-dialog' }, [ - E('p', {}, ['Suggested rules for ', E('strong', {}, device.ip), ' (', device.suggestedZone, ' zone):']), - E('div', { 'class': 'rules-list' }, - rules.map(function(rule) { - return E('div', { 'class': 'rule-item ' + rule.action.toLowerCase() }, [ - E('span', { 'class': 'rule-action' }, rule.action), - rule.ports ? E('span', { 'class': 'rule-ports' }, 'Ports: ' + rule.ports) : null, - rule.dest ? E('span', { 'class': 'rule-dest' }, 'Dest: ' + rule.dest) : null, - E('span', { 'class': 'rule-desc' }, rule.desc) - ]); - }) - ), - E('div', { 'class': 'rule-actions' }, [ - E('button', { 'class': 'btn-apply', 'click': function() { - ui.hideModal(); - ui.addNotification(null, E('p', {}, 'Rules applied to firewall'), 'success'); - }}, '✓ Apply Rules'), - E('button', { 'class': 'btn-copy', 'click': function() { - var text = rules.map(function(r) { return r.action + ' ' + (r.ports || '') + ' ' + r.desc; }).join('\n'); - navigator.clipboard.writeText(text); - ui.addNotification(null, E('p', {}, 'Rules copied to clipboard'), 'info'); - }}, '📋 Copy') - ]) - ]), - E('div', { 'class': 'right' }, [ - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) - ]) - ]); - }, - - autoAssignZones: function(devices) { - ui.showModal(_('Auto-Assign Zones'), [ - E('p', { 'class': 'spinning' }, _('Analyzing devices and assigning zones...')) - ]); - setTimeout(function() { - ui.hideModal(); - ui.addNotification(null, E('p', {}, devices.length + ' devices assigned to zones based on traffic analysis'), 'success'); - }, 1500); - }, - - exportFirewallRules: function(devices) { - var rules = []; - rules.push('# SecuBox Auto-Generated Firewall Rules'); - rules.push('# Generated: ' + new Date().toISOString()); - rules.push(''); - - devices.forEach(function(dev) { - rules.push('# Device: ' + dev.ip + ' (' + dev.suggestedZone + ')'); - (dev.suggestedRules || []).forEach(function(rule) { - rules.push('# ' + rule.desc); - if (rule.action === 'ACCEPT' && rule.ports) { - rules.push('iptables -A FORWARD -s ' + dev.ip + ' -p tcp --dport ' + rule.ports.split(',')[0] + ' -j ACCEPT'); - } else if (rule.action === 'DROP') { - rules.push('iptables -A FORWARD -s ' + dev.ip + ' -j DROP'); - } - }); - rules.push(''); - }); - - var blob = new Blob([rules.join('\n')], { type: 'text/plain' }); - var url = URL.createObjectURL(blob); - var a = document.createElement('a'); - a.href = url; - a.download = 'secubox-firewall-rules.sh'; - a.click(); - ui.addNotification(null, E('p', {}, 'Firewall rules exported'), 'success'); - }, - - renderThreatsSection: function(threats) { + renderThreats: function(threats) { var self = this; - return E('div', { 'class': 'section threats-section' }, [ - E('h2', { 'class': 'section-title' }, [ - E('span', { 'class': 'title-icon' }, '🎯'), - 'Recent Threats' - ]), + return E('div', { 'class': 'si-section' }, [ + E('h3', {}, 'Active Threats (' + threats.length + ')'), threats.length === 0 ? - E('div', { 'class': 'empty-threats' }, [ - E('div', { 'class': 'empty-icon' }, '🛡️'), - E('div', { 'class': 'empty-text' }, 'No threats detected'), - E('div', { 'class': 'empty-subtext' }, 'Your network is secure') - ]) : - E('div', { 'class': 'threats-table-wrap' }, [ - E('table', { 'class': 'threats-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, 'IP Address'), - E('th', {}, 'MAC'), - E('th', {}, 'Category'), - E('th', {}, 'Severity'), - E('th', {}, 'Risk'), - E('th', {}, 'Flags'), - E('th', {}, 'Status'), - E('th', {}, 'Action') - ]) - ]), - E('tbody', {}, - threats.map(function(threat) { - return E('tr', { 'class': 'threat-row ' + threat.severity }, [ - E('td', { 'class': 'ip-cell' }, [ - E('div', { 'class': 'ip-addr' }, threat.ip), - E('div', { 'class': 'ip-time' }, API.formatRelativeTime(threat.timestamp)) - ]), - E('td', { 'class': 'mac-cell' }, threat.mac || '-'), - E('td', { 'class': 'cat-cell' }, [ - E('span', { 'class': 'cat-icon' }, API.getThreatIcon(threat.category)), - E('span', {}, API.getCategoryLabel(threat.category)) - ]), - E('td', { 'class': 'sev-cell' }, [ - E('span', { 'class': 'severity-badge ' + threat.severity }, threat.severity) - ]), - E('td', { 'class': 'risk-cell' }, [ - E('span', { 'class': 'risk-score' }, threat.risk_score) - ]), - E('td', { 'class': 'flags-cell' }, API.formatRiskFlags(threat.netifyd.risks)), - E('td', { 'class': 'status-cell' }, - threat.crowdsec.has_decision ? - E('span', { 'class': 'blocked-badge' }, '🚫 Blocked') : - E('span', { 'class': 'active-badge' }, '⚡ Active') - ), - E('td', { 'class': 'action-cell' }, - threat.crowdsec.has_decision ? - E('button', { 'class': 'btn-blocked', 'disabled': true }, 'Blocked') : - E('button', { - 'class': 'btn-block', - 'click': function() { self.handleBlock(threat.ip); } - }, '🛡️ Block') - ) - ]); - }) - ) + E('div', { 'class': 'si-empty' }, 'No threats detected') : + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'IP'), + E('th', { 'class': 'th' }, 'Severity'), + E('th', { 'class': 'th' }, 'Type'), + E('th', { 'class': 'th' }, 'Pattern'), + E('th', { 'class': 'th' }, 'Host'), + E('th', { 'class': 'th' }, 'Country'), + E('th', { 'class': 'th' }, 'Time'), + E('th', { 'class': 'th' }, 'Action') ]) - ]) + ].concat( + threats.slice(0, 20).map(function(t) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td si-mono' }, t.ip || '-'), + E('td', { 'class': 'td' }, E('span', { 'class': 'si-sev si-sev-' + (t.severity || 'low') }, t.severity || '-')), + E('td', { 'class': 'td' }, t.type || '-'), + E('td', { 'class': 'td si-pattern' }, t.pattern || '-'), + E('td', { 'class': 'td' }, t.host || '-'), + E('td', { 'class': 'td' }, t.country || '??'), + E('td', { 'class': 'td' }, API.formatRelativeTime(t.timestamp)), + E('td', { 'class': 'td' }, + E('button', { + 'class': 'cbi-button cbi-button-remove', + 'click': function() { self.handleBlock(t.ip); } + }, 'Block') + ) + ]); + }) + )) ]); }, - handleScan: function() { - ui.showModal(_('Scanning Network...'), E('p', { 'class': 'spinning' }, _('Analyzing traffic...'))); - setTimeout(function() { - ui.hideModal(); - ui.addNotification(null, E('p', {}, 'Network scan complete'), 'info'); - }, 2000); + renderBlocked: function(blocked) { + if (!blocked || blocked.length === 0) return null; + + var visible = false; + var tableEl; + + return E('div', { 'class': 'si-section' }, [ + E('h3', { + 'style': 'cursor: pointer;', + 'click': function() { + visible = !visible; + tableEl.style.display = visible ? '' : 'none'; + } + }, 'Blocked IPs (' + blocked.length + ') [click to toggle]'), + tableEl = E('table', { 'class': 'table', 'style': 'display: none;' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'IP'), + E('th', { 'class': 'th' }, 'Reason'), + E('th', { 'class': 'th' }, 'Duration'), + E('th', { 'class': 'th' }, 'Scope') + ]) + ].concat( + blocked.slice(0, 50).map(function(b) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td si-mono' }, b.value || '-'), + E('td', { 'class': 'td' }, b.scenario || b.reason || '-'), + E('td', { 'class': 'td' }, b.duration || '-'), + E('td', { 'class': 'td' }, b.scope || b.origin || '-') + ]); + }) + )) + ]); }, handleBlock: function(ip) { var self = this; - ui.showModal(_('Block IP Address'), [ - E('div', { 'class': 'modal-content' }, [ - E('div', { 'class': 'modal-icon' }, '🛡️'), - E('p', { 'class': 'modal-text' }, _('Block all traffic from %s?').format(ip)), - E('p', { 'class': 'modal-subtext' }, _('This will add a CrowdSec decision for 4 hours.')) - ]), - E('div', { 'class': 'modal-actions' }, [ - E('button', { - 'class': 'btn-cancel', - 'click': ui.hideModal - }, _('Cancel')), - E('button', { - 'class': 'btn-confirm', - 'click': function() { - ui.hideModal(); - ui.showModal(_('Blocking...'), E('p', { 'class': 'spinning' }, _('Please wait...'))); + if (!confirm('Block ' + ip + ' for 4 hours?')) return; - API.blockThreat(ip, '4h', 'Manual block from Security Dashboard').then(function(result) { - ui.hideModal(); - if (result.success) { - ui.addNotification(null, E('p', _('IP %s blocked successfully').format(ip)), 'success'); - self.handleRefresh(); - } else { - ui.addNotification(null, E('p', _('Failed: %s').format(result.error || 'Unknown error')), 'error'); - } - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', _('Error: %s').format(err.message)), 'error'); - }); - } - }, _('Block for 4h')) - ]) - ]); + API.blockThreat(ip, '4h', 'Manual block from Security Dashboard').then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, ip + ' blocked')); + self.handleRefresh(); + } else { + ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'unknown')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'error'); + }); + }, + + handlePublish: function() { + var self = this; + API.publishIntel().then(function(result) { + ui.addNotification(null, E('p', {}, 'Publish started in background. Refresh in a moment to see results.')); + }).catch(function(err) { + ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'error'); + }); + }, + + handleApplyIntel: function() { + var self = this; + ui.showModal('Applying...', [E('p', { 'class': 'spinning' }, 'Applying pending mesh IOCs...')]); + API.applyIntel().then(function(result) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Applied. Total applied: ' + (result.applied_iocs || 0))); + self.handleRefresh(); + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'error'); + }); }, handleRefresh: function() { var self = this; return API.getDashboardData().then(function(data) { - var container = document.querySelector('.threats-dashboard'); + var container = document.querySelector('.si-dash'); if (container) { dom.content(container.parentNode, self.render(data)); } }).catch(function(err) { - console.error('Failed to refresh:', err); + console.error('Refresh failed:', err); }); }, getStyles: function() { return [ - // Base - '.threats-dashboard { font-family: system-ui, -apple-system, sans-serif; color: #e0e0e0; background: linear-gradient(135deg, #0a0a1a 0%, #1a1a2e 50%, #0f0f23 100%); min-height: 100vh; padding: 0; }', + '.si-dash { color: #e0e0e0; background: #0f0f1a; min-height: 100vh; }', - // Quick Actions Bar - '.quick-actions-bar { display: flex; justify-content: space-between; align-items: center; padding: 15px 40px; background: rgba(0,0,0,0.4); border-bottom: 1px solid rgba(255,255,255,0.1); position: sticky; top: 0; z-index: 100; backdrop-filter: blur(10px); flex-wrap: wrap; gap: 15px; }', - '.actions-left, .actions-right { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; }', - '.status-indicator { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 20px; font-size: 13px; }', - '.status-indicator.good { background: rgba(46,204,113,0.2); border: 1px solid rgba(46,204,113,0.4); }', - '.status-indicator.warn { background: rgba(241,196,15,0.2); border: 1px solid rgba(241,196,15,0.4); }', - '.status-indicator .status-dot { width: 8px; height: 8px; border-radius: 50%; }', - '.status-indicator.good .status-dot { background: #2ecc71; box-shadow: 0 0 8px #2ecc71; }', - '.status-indicator.warn .status-dot { background: #f1c40f; box-shadow: 0 0 8px #f1c40f; animation: pulse 2s infinite; }', - '.service-badges { display: flex; gap: 8px; }', - '.service-badge { padding: 6px 12px; border-radius: 15px; font-size: 12px; }', - '.service-badge.active { background: rgba(46,204,113,0.2); border: 1px solid rgba(46,204,113,0.3); color: #2ecc71; }', - '.service-badge.inactive { background: rgba(231,76,60,0.2); border: 1px solid rgba(231,76,60,0.3); color: #e74c3c; }', - '.action-btn { display: inline-flex; align-items: center; gap: 6px; padding: 10px 18px; background: rgba(52,73,94,0.6); border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; color: #e0e0e0; font-size: 13px; cursor: pointer; transition: all 0.2s; text-decoration: none; }', - '.action-btn:hover { transform: translateY(-2px); }', - '.action-btn.refresh { background: rgba(46,204,113,0.3); border-color: rgba(46,204,113,0.4); }', - '.action-btn.scan { background: rgba(52,152,219,0.3); border-color: rgba(52,152,219,0.4); }', - '.action-btn.settings { background: rgba(155,89,182,0.3); border-color: rgba(155,89,182,0.4); }', - - // Hero Banner - '.hero-banner { position: relative; padding: 50px 40px; text-align: center; overflow: hidden; }', - '.hero-banner.secure .hero-bg { background: radial-gradient(ellipse at center, rgba(46,204,113,0.15) 0%, transparent 70%); }', - '.hero-banner.critical .hero-bg { background: radial-gradient(ellipse at center, rgba(231,76,60,0.2) 0%, transparent 70%); }', - '.hero-banner.high .hero-bg { background: radial-gradient(ellipse at center, rgba(230,126,34,0.15) 0%, transparent 70%); }', - '.hero-banner.medium .hero-bg { background: radial-gradient(ellipse at center, rgba(241,196,15,0.15) 0%, transparent 70%); }', - '.hero-bg { position: absolute; inset: 0; }', - '.hero-content { position: relative; z-index: 1; max-width: 800px; margin: 0 auto; }', - '.hero-icon { font-size: 56px; margin-bottom: 15px; animation: pulse 2s infinite; }', - '@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }', - '.hero-title { font-size: 36px; font-weight: 700; margin: 0 0 8px; background: linear-gradient(135deg, #e74c3c, #9b59b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }', - '.hero-subtitle { font-size: 18px; color: #888; margin: 0 0 20px; }', - '.hero-badges { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; margin-bottom: 15px; }', - '.hero-badges .badge { padding: 6px 14px; border-radius: 15px; font-size: 12px; }', - '.badge.blue { background: rgba(52,152,219,0.2); border: 1px solid rgba(52,152,219,0.4); color: #3498db; }', - '.badge.green { background: rgba(46,204,113,0.2); border: 1px solid rgba(46,204,113,0.4); color: #2ecc71; }', - '.badge.purple { background: rgba(155,89,182,0.2); border: 1px solid rgba(155,89,182,0.4); color: #9b59b6; }', - '.badge.orange { background: rgba(230,126,34,0.2); border: 1px solid rgba(230,126,34,0.4); color: #e67e22; }', - '.hero-desc { font-size: 14px; color: #888; line-height: 1.5; max-width: 600px; margin: 0 auto; }', + // Status bar + '.si-status-bar { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; background: rgba(0,0,0,0.4); border-bottom: 1px solid #333; flex-wrap: wrap; gap: 10px; }', + '.si-status-left { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }', + '.si-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }', + '.si-dot.ok { background: #2ecc71; }', + '.si-dot.warn { background: #f1c40f; }', + '.si-svc-list { display: flex; gap: 6px; margin-left: 10px; }', + '.si-svc { padding: 3px 10px; border-radius: 12px; font-size: 12px; }', + '.si-svc.ok { background: rgba(46,204,113,0.2); color: #2ecc71; border: 1px solid rgba(46,204,113,0.3); }', + '.si-svc.off { background: rgba(231,76,60,0.2); color: #e74c3c; border: 1px solid rgba(231,76,60,0.3); }', // Sections - '.section { padding: 30px 40px; }', - '.section-title { display: flex; align-items: center; gap: 12px; font-size: 22px; font-weight: 600; margin: 0 0 20px; color: #fff; }', - '.title-icon { font-size: 24px; }', - '.firewall-section { background: rgba(0,0,0,0.2); }', + '.si-section { padding: 20px; border-bottom: 1px solid #222; }', + '.si-section h3 { margin: 0 0 15px; font-size: 18px; color: #fff; }', + '.si-subsection { margin-top: 20px; }', + '.si-subsection h4 { margin: 0 0 10px; font-size: 15px; color: #ccc; }', - // Firewall Stats Grid - '.fw-stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 15px; }', - '.fw-stat-card { padding: 20px; border-radius: 12px; text-align: center; transition: transform 0.2s; }', - '.fw-stat-card:hover { transform: translateY(-3px); }', - '.fw-stat-card.blue { background: linear-gradient(135deg, rgba(52,152,219,0.3), rgba(52,152,219,0.1)); border: 1px solid rgba(52,152,219,0.3); }', - '.fw-stat-card.red { background: linear-gradient(135deg, rgba(231,76,60,0.3), rgba(231,76,60,0.1)); border: 1px solid rgba(231,76,60,0.3); }', - '.fw-stat-card.purple { background: linear-gradient(135deg, rgba(155,89,182,0.3), rgba(155,89,182,0.1)); border: 1px solid rgba(155,89,182,0.3); }', - '.fw-stat-card.orange { background: linear-gradient(135deg, rgba(230,126,34,0.3), rgba(230,126,34,0.1)); border: 1px solid rgba(230,126,34,0.3); }', - '.fw-stat-card.gray { background: linear-gradient(135deg, rgba(127,140,141,0.3), rgba(127,140,141,0.1)); border: 1px solid rgba(127,140,141,0.3); }', - '.fw-stat-card.teal { background: linear-gradient(135deg, rgba(26,188,156,0.3), rgba(26,188,156,0.1)); border: 1px solid rgba(26,188,156,0.3); }', - '.fw-icon { font-size: 28px; margin-bottom: 8px; }', - '.fw-value { font-size: 28px; font-weight: 700; color: #fff; }', - '.fw-label { font-size: 13px; color: #ccc; margin-top: 5px; }', - '.fw-desc { font-size: 11px; color: #888; margin-top: 3px; }', + // Stats grid + '.si-stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }', + '.si-stat { padding: 16px; border-radius: 10px; text-align: center; }', + '.si-stat.blue { background: rgba(52,152,219,0.15); border: 1px solid rgba(52,152,219,0.3); }', + '.si-stat.red { background: rgba(231,76,60,0.15); border: 1px solid rgba(231,76,60,0.3); }', + '.si-stat.purple { background: rgba(155,89,182,0.15); border: 1px solid rgba(155,89,182,0.3); }', + '.si-stat.orange { background: rgba(230,126,34,0.15); border: 1px solid rgba(230,126,34,0.3); }', + '.si-stat.gray { background: rgba(127,140,141,0.15); border: 1px solid rgba(127,140,141,0.3); }', + '.si-stat.teal { background: rgba(26,188,156,0.15); border: 1px solid rgba(26,188,156,0.3); }', + '.si-stat.green { background: rgba(46,204,113,0.15); border: 1px solid rgba(46,204,113,0.3); }', + '.si-stat-val { font-size: 26px; font-weight: 700; color: #fff; }', + '.si-stat-label { font-size: 12px; color: #999; margin-top: 4px; }', - // Overview Grid - '.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; }', - '.overview-card { display: flex; align-items: center; gap: 15px; padding: 20px; border-radius: 12px; background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); transition: all 0.2s; }', - '.overview-card:hover { transform: translateY(-3px); border-color: rgba(255,255,255,0.2); }', - '.overview-card.blue { border-left: 4px solid #3498db; }', - '.overview-card.red { border-left: 4px solid #e74c3c; }', - '.overview-card.orange { border-left: 4px solid #e67e22; }', - '.overview-card.yellow { border-left: 4px solid #f1c40f; }', - '.overview-card.purple { border-left: 4px solid #9b59b6; }', - '.card-icon { font-size: 32px; }', - '.card-value { font-size: 26px; font-weight: 700; color: #fff; }', - '.card-label { font-size: 12px; color: #888; margin-top: 3px; }', + // Actions + '.si-actions { display: flex; align-items: center; gap: 10px; margin-top: 15px; flex-wrap: wrap; }', + '.si-meta { font-size: 12px; color: #666; margin-left: auto; }', - // Two Column Section - '.two-col-section { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 20px; padding: 0 40px 30px; }', - '.dist-card, .gauge-card { background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; padding: 25px; }', - '.card-title { font-size: 16px; font-weight: 600; color: #fff; margin: 0 0 20px; display: flex; align-items: center; gap: 8px; }', + // Tables + '.si-mono { font-family: monospace; font-size: 13px; }', + '.si-pattern { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', - // Distribution - '.dist-content { display: flex; flex-direction: column; gap: 15px; }', - '.dist-item { }', - '.dist-header { display: flex; justify-content: space-between; margin-bottom: 6px; }', - '.dist-label { font-size: 13px; color: #ccc; }', - '.dist-value { font-size: 13px; font-weight: 600; color: #fff; }', - '.dist-bar-bg { height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; }', - '.dist-bar { height: 100%; border-radius: 4px; transition: width 0.5s; }', - '.empty-state { text-align: center; padding: 30px; color: #888; }', - '.empty-state .empty-icon { font-size: 36px; margin-bottom: 10px; }', + // Severity badges + '.si-sev { padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; text-transform: uppercase; }', + '.si-sev-critical { background: rgba(231,76,60,0.2); color: #e74c3c; }', + '.si-sev-high { background: rgba(230,126,34,0.2); color: #e67e22; }', + '.si-sev-medium { background: rgba(241,196,15,0.2); color: #f1c40f; }', + '.si-sev-low { background: rgba(46,204,113,0.2); color: #2ecc71; }', - // Gauge - '.gauge-content { text-align: center; }', - '.gauge-icon { font-size: 48px; margin-bottom: 10px; }', - '.gauge-score { font-size: 56px; font-weight: 700; }', - '.gauge-level { font-size: 18px; font-weight: 600; margin: 5px 0; }', - '.gauge-desc { font-size: 13px; color: #888; margin-bottom: 20px; }', - '.gauge-bar { height: 8px; background: linear-gradient(to right, #2ecc71, #f1c40f, #e67e22, #e74c3c); border-radius: 4px; position: relative; }', - '.gauge-marker { position: absolute; top: -4px; width: 3px; height: 16px; background: #fff; border-radius: 2px; transform: translateX(-50%); box-shadow: 0 0 8px rgba(255,255,255,0.5); }', + // Trust badges + '.si-trust { padding: 2px 8px; border-radius: 10px; font-size: 11px; }', + '.si-trust-direct { background: rgba(46,204,113,0.2); color: #2ecc71; }', + '.si-trust-transitive { background: rgba(241,196,15,0.2); color: #f1c40f; }', + '.si-trust-unknown { background: rgba(127,140,141,0.2); color: #95a5a6; }', - // Threats Section - '.threats-section { background: rgba(0,0,0,0.2); }', - '.empty-threats { text-align: center; padding: 60px 20px; }', - '.empty-threats .empty-icon { font-size: 64px; margin-bottom: 15px; }', - '.empty-threats .empty-text { font-size: 20px; color: #fff; margin-bottom: 5px; }', - '.empty-threats .empty-subtext { font-size: 14px; color: #888; }', - '.threats-table-wrap { overflow-x: auto; }', - '.threats-table { width: 100%; border-collapse: collapse; }', - '.threats-table th { padding: 12px 15px; text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: #888; border-bottom: 1px solid rgba(255,255,255,0.1); }', - '.threats-table td { padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.05); }', - '.threat-row { transition: background 0.2s; }', - '.threat-row:hover { background: rgba(255,255,255,0.03); }', - '.threat-row.critical { border-left: 3px solid #e74c3c; }', - '.threat-row.high { border-left: 3px solid #e67e22; }', - '.threat-row.medium { border-left: 3px solid #f1c40f; }', - '.threat-row.low { border-left: 3px solid #2ecc71; }', - '.ip-addr { font-family: monospace; font-size: 14px; color: #fff; }', - '.ip-time { font-size: 11px; color: #666; margin-top: 3px; }', - '.mac-cell { font-family: monospace; font-size: 12px; color: #888; }', - '.cat-cell { display: flex; align-items: center; gap: 8px; }', - '.cat-icon { font-size: 18px; }', - '.severity-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; }', - '.severity-badge.critical { background: rgba(231,76,60,0.2); color: #e74c3c; }', - '.severity-badge.high { background: rgba(230,126,34,0.2); color: #e67e22; }', - '.severity-badge.medium { background: rgba(241,196,15,0.2); color: #f1c40f; }', - '.severity-badge.low { background: rgba(46,204,113,0.2); color: #2ecc71; }', - '.risk-score { font-size: 18px; font-weight: 700; color: #fff; }', - '.flags-cell { font-size: 11px; color: #888; max-width: 150px; overflow: hidden; text-overflow: ellipsis; }', - '.blocked-badge { color: #e74c3c; font-weight: 600; }', - '.active-badge { color: #f1c40f; }', - '.btn-block { padding: 6px 14px; background: linear-gradient(135deg, #e74c3c, #c0392b); border: none; border-radius: 6px; color: #fff; font-size: 12px; cursor: pointer; transition: all 0.2s; }', - '.btn-block:hover { transform: scale(1.05); }', - '.btn-blocked { padding: 6px 14px; background: rgba(127,140,141,0.3); border: none; border-radius: 6px; color: #888; font-size: 12px; cursor: not-allowed; }', - - // Modal - '.modal-content { text-align: center; padding: 20px; }', - '.modal-icon { font-size: 48px; margin-bottom: 15px; }', - '.modal-text { font-size: 16px; color: #333; margin: 0 0 5px; }', - '.modal-subtext { font-size: 13px; color: #666; }', - '.modal-actions { display: flex; justify-content: center; gap: 10px; padding: 15px; }', - '.btn-cancel { padding: 10px 20px; background: #eee; border: none; border-radius: 6px; cursor: pointer; }', - '.btn-confirm { padding: 10px 20px; background: #e74c3c; border: none; border-radius: 6px; color: #fff; cursor: pointer; }', - - // Devices Section - '.devices-section { background: rgba(0,0,0,0.15); }', - '.powered-badge { font-size: 11px; padding: 4px 10px; background: rgba(52,152,219,0.2); border: 1px solid rgba(52,152,219,0.3); border-radius: 12px; color: #3498db; margin-left: 15px; }', - '.notice.warning { background: rgba(241,196,15,0.15); border: 1px solid rgba(241,196,15,0.3); padding: 12px 20px; border-radius: 8px; margin-bottom: 20px; color: #f1c40f; }', - '.zones-legend { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; }', - '.zone-chip { display: flex; align-items: center; gap: 8px; padding: 8px 14px; background: rgba(255,255,255,0.05); border: 1px solid; border-radius: 20px; font-size: 12px; }', - '.zone-icon { font-size: 14px; }', - '.zone-name { font-weight: 600; color: #fff; }', - '.zone-count { background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 10px; font-size: 11px; }', - '.empty-devices { text-align: center; padding: 50px 20px; }', - '.empty-devices .empty-icon { font-size: 48px; margin-bottom: 10px; opacity: 0.5; }', - '.empty-devices .empty-text { font-size: 16px; color: #fff; margin-bottom: 5px; }', - '.empty-devices .empty-subtext { font-size: 13px; color: #888; }', - '.devices-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; }', - '.device-card { background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); border-left: 4px solid; border-radius: 12px; padding: 15px; transition: all 0.2s; }', - '.device-card:hover { background: rgba(30,30,50,0.8); transform: translateY(-2px); }', - '.device-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }', - '.device-icon { font-size: 28px; }', - '.device-info { flex: 1; }', - '.device-ip { font-size: 14px; font-weight: 600; color: #fff; font-family: monospace; }', - '.device-hostname { font-size: 11px; color: #888; }', - '.zone-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; color: #fff; }', - '.device-apps { margin-bottom: 10px; }', - '.apps-label { font-size: 11px; color: #888; }', - '.app-tag { display: inline-block; padding: 3px 8px; background: rgba(155,89,182,0.2); border: 1px solid rgba(155,89,182,0.3); border-radius: 10px; font-size: 10px; color: #9b59b6; margin: 2px; }', - '.no-apps { font-size: 11px; color: #666; font-style: italic; }', - '.device-stats { display: flex; gap: 15px; font-size: 11px; color: #888; margin-bottom: 12px; }', - '.device-actions { display: flex; gap: 8px; }', - '.btn-zone, .btn-rules { flex: 1; padding: 8px 12px; border: none; border-radius: 6px; font-size: 11px; cursor: pointer; transition: all 0.2s; }', - '.btn-zone { background: linear-gradient(135deg, rgba(52,152,219,0.3), rgba(52,152,219,0.1)); border: 1px solid rgba(52,152,219,0.3); color: #3498db; }', - '.btn-zone:hover { background: rgba(52,152,219,0.4); }', - '.btn-rules { background: linear-gradient(135deg, rgba(230,126,34,0.3), rgba(230,126,34,0.1)); border: 1px solid rgba(230,126,34,0.3); color: #e67e22; }', - '.btn-rules:hover { background: rgba(230,126,34,0.4); }', - '.quick-zone-actions { display: flex; align-items: center; gap: 15px; margin-top: 20px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); }', - '.quick-label { font-size: 13px; color: #888; }', - '.btn-auto-zone, .btn-export-rules { padding: 10px 18px; border: none; border-radius: 8px; font-size: 12px; cursor: pointer; transition: all 0.2s; }', - '.btn-auto-zone { background: linear-gradient(135deg, #2ecc71, #27ae60); color: #fff; }', - '.btn-auto-zone:hover { opacity: 0.9; transform: translateY(-1px); }', - '.btn-export-rules { background: rgba(155,89,182,0.3); border: 1px solid rgba(155,89,182,0.4); color: #9b59b6; }', - '.btn-export-rules:hover { background: rgba(155,89,182,0.5); }', - - // Zone Dialog - '.zone-dialog { padding: 10px 0; }', - '.zone-options { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px; }', - '.zone-option { display: flex; flex-direction: column; align-items: center; padding: 15px; background: #f5f5f5; border: 2px solid #ddd; border-radius: 10px; cursor: pointer; transition: all 0.2s; }', - '.zone-option:hover { background: #e8e8e8; transform: scale(1.02); }', - '.zo-icon { font-size: 24px; margin-bottom: 5px; }', - '.zo-name { font-weight: 600; font-size: 13px; }', - '.zo-desc { font-size: 10px; color: #666; text-align: center; }', - - // Rules Dialog - '.rules-dialog { padding: 10px 0; }', - '.rules-list { margin: 15px 0; }', - '.rule-item { display: flex; align-items: center; gap: 10px; padding: 10px; background: #f5f5f5; border-radius: 6px; margin-bottom: 8px; }', - '.rule-item.accept { border-left: 3px solid #2ecc71; }', - '.rule-item.drop { border-left: 3px solid #e74c3c; }', - '.rule-action { font-weight: 700; font-size: 11px; padding: 3px 8px; border-radius: 4px; }', - '.rule-item.accept .rule-action { background: rgba(46,204,113,0.2); color: #27ae60; }', - '.rule-item.drop .rule-action { background: rgba(231,76,60,0.2); color: #c0392b; }', - '.rule-ports, .rule-dest { font-size: 11px; color: #666; font-family: monospace; }', - '.rule-desc { flex: 1; font-size: 12px; color: #333; }', - '.rule-actions { display: flex; gap: 10px; margin-top: 15px; }', - '.btn-apply, .btn-copy { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; }', - '.btn-apply { background: #2ecc71; color: #fff; }', - '.btn-copy { background: #3498db; color: #fff; }', + // Notice & empty + '.si-notice { padding: 15px; background: rgba(241,196,15,0.1); border: 1px solid rgba(241,196,15,0.3); border-radius: 8px; color: #f1c40f; }', + '.si-empty { padding: 40px; text-align: center; color: #666; font-size: 14px; }', // Responsive - '@media (max-width: 768px) {', - ' .hero-title { font-size: 24px; }', - ' .section { padding: 20px; }', - ' .two-col-section { padding: 0 20px 20px; }', - ' .quick-actions-bar { padding: 15px 20px; }', - ' .devices-grid { grid-template-columns: 1fr; }', - ' .zone-options { grid-template-columns: 1fr; }', - '}' + '@media (max-width: 768px) { .si-stats-grid { grid-template-columns: repeat(2, 1fr); } .si-section { padding: 15px; } }' ].join('\n'); }, diff --git a/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats b/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats index d23ffb67..6d587ae1 100755 --- a/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats +++ b/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats @@ -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 diff --git a/package/secubox/luci-app-secubox-security-threats/root/usr/share/rpcd/acl.d/luci-app-secubox-security-threats.json b/package/secubox/luci-app-secubox-security-threats/root/usr/share/rpcd/acl.d/luci-app-secubox-security-threats.json index 02ed749e..15978501 100644 --- a/package/secubox/luci-app-secubox-security-threats/root/usr/share/rpcd/acl.d/luci-app-secubox-security-threats.json +++ b/package/secubox/luci-app-secubox-security-threats/root/usr/share/rpcd/acl.d/luci-app-secubox-security-threats.json @@ -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"]