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"]