feat(portal): Add Threat Monitor to security cards and stats

- Add threat-monitor app to security section in portal.js
- Add security stats RPC call (get_security_stats)
- Display packets blocked and alerts on dashboard
- Add Threat Monitor to featured quick access apps
- Show WAN dropped + firewall rejects in events section
- Link to Threat Monitor dashboard from events

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-25 15:24:37 +01:00
parent 283f2567be
commit 37b88e47b9
2 changed files with 52 additions and 13 deletions

View File

@ -61,6 +61,18 @@ return baseclass.extend({
service: 'mitmproxy',
version: '8.1.1'
},
'threat-monitor': {
id: 'threat-monitor',
name: 'Threat Monitor',
desc: 'Real-time threat detection combining netifyd DPI with CrowdSec intelligence',
icon: '\ud83d\udc41\ufe0f',
iconBg: 'rgba(239, 68, 68, 0.15)',
iconColor: '#ef4444',
section: 'security',
path: 'admin/secubox/security/threats/dashboard',
service: null,
version: '1.0.0'
},
// Network Apps
'bandwidth-manager': {

View File

@ -23,6 +23,11 @@ var callCrowdSecStats = rpc.declare({
method: 'nftables_stats'
});
var callSecurityStats = rpc.declare({
object: 'luci.secubox-security-threats',
method: 'get_security_stats'
});
var callGetServices = rpc.declare({
object: 'luci.secubox',
method: 'get_services',
@ -42,13 +47,16 @@ return view.extend({
this.loadAppStatuses(),
callCrowdSecStats().catch(function() { return null; }),
portal.checkInstalledApps(),
callGetServices().catch(function() { return []; })
callGetServices().catch(function() { return []; }),
callSecurityStats().catch(function() { return null; })
]).then(function(results) {
// Store installed apps info from the last promise
self.installedApps = results[4] || {};
// RPC expect unwraps the services array directly
var svcResult = results[5] || [];
self.detectedServices = Array.isArray(svcResult) ? svcResult : (svcResult.services || []);
// Security stats
self.securityStats = results[6] || {};
return results;
});
},
@ -104,6 +112,7 @@ return view.extend({
var boardInfo = data[0] || {};
var sysInfo = data[1] || {};
var crowdSecStats = data[3] || {};
var securityStats = this.securityStats || {};
var self = this;
// Set portal app context and hide LuCI navigation
@ -142,7 +151,7 @@ return view.extend({
this.renderHeader(),
// Content
E('div', { 'class': 'sb-portal-content' }, [
this.renderDashboardSection(boardInfo, sysInfo, crowdSecStats),
this.renderDashboardSection(boardInfo, sysInfo, crowdSecStats, securityStats),
this.renderSecuritySection(),
this.renderNetworkSection(),
this.renderMonitoringSection(),
@ -228,7 +237,7 @@ return view.extend({
});
},
renderDashboardSection: function(boardInfo, sysInfo, crowdSecStats) {
renderDashboardSection: function(boardInfo, sysInfo, crowdSecStats, securityStats) {
var self = this;
var securityApps = portal.getAppsBySection('security');
var networkApps = portal.getAppsBySection('network');
@ -245,6 +254,12 @@ return view.extend({
var crowdSecHealth = crowdSecStats.firewall_health || {};
var crowdSecActive = crowdSecHealth.bouncer_running && crowdSecHealth.decisions_synced;
// Security stats
var wanDropped = securityStats.wan_dropped || 0;
var fwRejects = securityStats.firewall_rejects || 0;
var csBans = securityStats.crowdsec_bans || 0;
var csAlerts = securityStats.crowdsec_alerts_24h || 0;
return E('div', { 'class': 'sb-portal-section active', 'data-section': 'dashboard' }, [
E('div', { 'class': 'sb-section-header' }, [
E('h2', { 'class': 'sb-section-title' }, 'SecuBox Dashboard'),
@ -275,32 +290,33 @@ return view.extend({
E('div', { 'class': 'sb-quick-stat-label' }, 'Services Running')
]),
// CrowdSec Blocked IPs
// Firewall Blocked
E('div', { 'class': 'sb-quick-stat' }, [
E('div', { 'class': 'sb-quick-stat-header' }, [
E('div', { 'class': 'sb-quick-stat-icon security' }, '\ud83d\udeab'),
E('span', { 'class': 'sb-quick-stat-status ' + (crowdSecActive ? 'running' : 'warning') },
crowdSecActive ? 'Active' : 'Inactive')
crowdSecActive ? 'Protected' : 'Monitoring')
]),
E('div', { 'class': 'sb-quick-stat-value' }, totalBlocked.toLocaleString()),
E('div', { 'class': 'sb-quick-stat-label' }, 'IPs Blocked')
E('div', { 'class': 'sb-quick-stat-value' }, (wanDropped + fwRejects).toLocaleString()),
E('div', { 'class': 'sb-quick-stat-label' }, 'Packets Blocked')
]),
// Network Apps
// Threat Alerts
E('div', { 'class': 'sb-quick-stat' }, [
E('div', { 'class': 'sb-quick-stat-header' }, [
E('div', { 'class': 'sb-quick-stat-icon network' }, '\ud83c\udf10'),
E('span', { 'class': 'sb-quick-stat-status running' }, 'Configured')
E('div', { 'class': 'sb-quick-stat-icon security' }, '\ud83d\udc41\ufe0f'),
E('span', { 'class': 'sb-quick-stat-status ' + (csAlerts > 0 ? 'warning' : 'running') },
csAlerts > 0 ? 'Alerts' : 'Clear')
]),
E('div', { 'class': 'sb-quick-stat-value' }, networkApps.length),
E('div', { 'class': 'sb-quick-stat-label' }, 'Network Tools')
E('div', { 'class': 'sb-quick-stat-value' }, csBans + '/' + csAlerts),
E('div', { 'class': 'sb-quick-stat-label' }, 'Bans / Alerts 24h')
])
]),
// Featured Apps
E('h3', { 'style': 'margin: 1.5rem 0 1rem; color: var(--cyber-text-primary);' }, 'Quick Access'),
E('div', { 'class': 'sb-app-grid' },
this.renderFeaturedApps(['crowdsec', 'bandwidth-manager', 'media-flow', 'ndpid'])
this.renderFeaturedApps(['crowdsec', 'threat-monitor', 'bandwidth-manager', 'media-flow'])
),
// Recent Events placeholder
@ -342,6 +358,17 @@ return view.extend({
(crowdSecStats.ipv4_cscli_count || 0) + ' local) | IPv6: ' + blockedIPv6.toLocaleString()),
E('span', { 'class': 'sb-events-meta' }, 'CrowdSec Firewall Protection')
])
]) : null,
wanDropped > 0 ? E('div', { 'class': 'sb-events-item' }, [
E('div', { 'class': 'sb-events-icon warning' }, '\ud83d\udc41\ufe0f'),
E('div', { 'class': 'sb-events-content' }, [
E('p', { 'class': 'sb-events-message' },
'WAN Dropped: ' + wanDropped.toLocaleString() + ' | Firewall Rejects: ' + fwRejects),
E('span', { 'class': 'sb-events-meta' }, [
'Threat Monitor - ',
E('a', { 'href': L.url('admin/secubox/security/threats/dashboard') }, 'View Details')
])
])
]) : null
].filter(Boolean))
]);