secubox-openwrt/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js
CyberMind-FR c4302504df refactor(security-threats): KISS rewrite with mesh threat intelligence
Replace overengineered dashboard (2025 lines) with focused security
intelligence view (847 lines). Drop hero banner, risk gauge, device
zoning, nDPId correlation engine. Keep firewall stats, mitmproxy
threats, CrowdSec blocking. Add mesh intelligence section with P2P
threat-intel sharing (IOC counts, peer contributors, publish/apply).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 11:30:25 +01:00

389 lines
14 KiB
JavaScript

'use strict';
'require view';
'require poll';
'require ui';
'require dom';
'require secubox-security-threats/api as API';
return L.view.extend({
load: function() {
return API.getDashboardData();
},
render: function(data) {
var self = this;
data = data || {};
var status = data.status || {};
var threats = data.threats || [];
var stats = data.securityStats || {};
var blocked = data.blocked || [];
var intel = data.threatIntel || {};
var meshIocs = data.meshIocs || [];
var meshPeers = data.meshPeers || [];
poll.add(L.bind(function() { this.handleRefresh(); }, this), 15);
return E('div', { 'class': 'si-dash' }, [
E('style', {}, this.getStyles()),
// Status bar
this.renderStatusBar(status),
// Firewall stats
this.renderFirewallStats(stats),
// Mesh Intelligence
this.renderMeshIntel(intel, meshIocs, meshPeers),
// Threats table
this.renderThreats(threats),
// Blocked IPs (collapsed)
this.renderBlocked(blocked)
]);
},
renderStatusBar: function(status) {
var self = this;
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': '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 fmt = API.formatNumber;
var items = [
{ 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': 'si-section' }, [
E('h3', {}, 'Firewall & Network Protection'),
E('div', { 'class': 'si-stats-grid' },
items.map(function(item) {
return E('div', { 'class': 'si-stat ' + item.cls }, [
E('div', { 'class': 'si-stat-val' }, item.value),
E('div', { 'class': 'si-stat-label' }, item.label)
]);
})
)
]);
},
renderMeshIntel: function(intel, iocs, peers) {
var self = this;
var enabled = intel.enabled;
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.')
]);
}
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' }
];
return E('div', { 'class': 'si-section' }, [
E('h3', {}, 'Mesh Intelligence'),
// 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)
]);
})
),
// 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')
)
]),
// 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
]);
},
renderThreats: function(threats) {
var self = this;
return E('div', { 'class': 'si-section' }, [
E('h3', {}, 'Active Threats (' + threats.length + ')'),
threats.length === 0 ?
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')
)
]);
})
))
]);
},
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;
if (!confirm('Block ' + ip + ' for 4 hours?')) return;
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('.si-dash');
if (container) {
dom.content(container.parentNode, self.render(data));
}
}).catch(function(err) {
console.error('Refresh failed:', err);
});
},
getStyles: function() {
return [
'.si-dash { color: #e0e0e0; background: #0f0f1a; min-height: 100vh; }',
// 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
'.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; }',
// 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; }',
// 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; }',
// Tables
'.si-mono { font-family: monospace; font-size: 13px; }',
'.si-pattern { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
// 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; }',
// 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; }',
// 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) { .si-stats-grid { grid-template-columns: repeat(2, 1fr); } .si-section { padding: 15px; } }'
].join('\n');
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});