feat(portal): Add CrowdSec blocked IPs statistics to dashboard

- Add RPC call to fetch CrowdSec nftables statistics
- Replace Security Modules widget with IPs Blocked widget
- Show active/inactive status based on firewall bouncer health
- Add detailed breakdown in System Overview (IPv4/IPv6, CAPI/local)
- Gracefully handle missing CrowdSec dashboard package

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-22 06:00:40 +01:00
parent 7bb437b29f
commit a602462915
2 changed files with 36 additions and 11 deletions

View File

@ -11,7 +11,7 @@ LUCI_DESCRIPTION:=Unified entry point for all SecuBox applications with tabbed n
LUCI_DEPENDS:=+luci-base +luci-theme-secubox LUCI_DEPENDS:=+luci-base +luci-theme-secubox
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_VERSION:=0.6.0 PKG_VERSION:=0.6.0
PKG_RELEASE:=7 PKG_RELEASE:=8
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
PKG_MAINTAINER:=SecuBox Team <secubox@example.com> PKG_MAINTAINER:=SecuBox Team <secubox@example.com>

View File

@ -18,6 +18,11 @@ var callSystemInfo = rpc.declare({
method: 'info' method: 'info'
}); });
var callCrowdSecStats = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'nftables_stats'
});
return view.extend({ return view.extend({
currentSection: 'dashboard', currentSection: 'dashboard',
appStatuses: {}, appStatuses: {},
@ -26,7 +31,8 @@ return view.extend({
return Promise.all([ return Promise.all([
callSystemBoard(), callSystemBoard(),
callSystemInfo(), callSystemInfo(),
this.loadAppStatuses() this.loadAppStatuses(),
callCrowdSecStats().catch(function() { return null; })
]); ]);
}, },
@ -80,6 +86,7 @@ return view.extend({
render: function(data) { render: function(data) {
var boardInfo = data[0] || {}; var boardInfo = data[0] || {};
var sysInfo = data[1] || {}; var sysInfo = data[1] || {};
var crowdSecStats = data[3] || {};
var self = this; var self = this;
// Set portal app context and hide LuCI navigation // Set portal app context and hide LuCI navigation
@ -118,7 +125,7 @@ return view.extend({
this.renderHeader(), this.renderHeader(),
// Content // Content
E('div', { 'class': 'sb-portal-content' }, [ E('div', { 'class': 'sb-portal-content' }, [
this.renderDashboardSection(boardInfo, sysInfo), this.renderDashboardSection(boardInfo, sysInfo, crowdSecStats),
this.renderSecuritySection(), this.renderSecuritySection(),
this.renderNetworkSection(), this.renderNetworkSection(),
this.renderMonitoringSection(), this.renderMonitoringSection(),
@ -204,7 +211,7 @@ return view.extend({
}); });
}, },
renderDashboardSection: function(boardInfo, sysInfo) { renderDashboardSection: function(boardInfo, sysInfo, crowdSecStats) {
var self = this; var self = this;
var securityApps = portal.getAppsBySection('security'); var securityApps = portal.getAppsBySection('security');
var networkApps = portal.getAppsBySection('network'); var networkApps = portal.getAppsBySection('network');
@ -214,6 +221,13 @@ return view.extend({
var runningCount = Object.values(this.appStatuses).filter(function(s) { return s === 'running'; }).length; var runningCount = Object.values(this.appStatuses).filter(function(s) { return s === 'running'; }).length;
var totalServices = Object.keys(this.appStatuses).length; var totalServices = Object.keys(this.appStatuses).length;
// CrowdSec blocked IPs count
var blockedIPv4 = (crowdSecStats.ipv4_total_count || 0);
var blockedIPv6 = (crowdSecStats.ipv6_total_count || 0);
var totalBlocked = blockedIPv4 + blockedIPv6;
var crowdSecHealth = crowdSecStats.firewall_health || {};
var crowdSecActive = crowdSecHealth.bouncer_running && crowdSecHealth.decisions_synced;
return E('div', { 'class': 'sb-portal-section active', 'data-section': 'dashboard' }, [ return E('div', { 'class': 'sb-portal-section active', 'data-section': 'dashboard' }, [
E('div', { 'class': 'sb-section-header' }, [ E('div', { 'class': 'sb-section-header' }, [
E('h2', { 'class': 'sb-section-title' }, 'SecuBox Dashboard'), E('h2', { 'class': 'sb-section-title' }, 'SecuBox Dashboard'),
@ -244,14 +258,15 @@ return view.extend({
E('div', { 'class': 'sb-quick-stat-label' }, 'Services Running') E('div', { 'class': 'sb-quick-stat-label' }, 'Services Running')
]), ]),
// Security Apps // CrowdSec Blocked IPs
E('div', { 'class': 'sb-quick-stat' }, [ E('div', { 'class': 'sb-quick-stat' }, [
E('div', { 'class': 'sb-quick-stat-header' }, [ E('div', { 'class': 'sb-quick-stat-header' }, [
E('div', { 'class': 'sb-quick-stat-icon security' }, '\ud83d\udee1\ufe0f'), E('div', { 'class': 'sb-quick-stat-icon security' }, '\ud83d\udeab'),
E('span', { 'class': 'sb-quick-stat-status running' }, 'Protected') E('span', { 'class': 'sb-quick-stat-status ' + (crowdSecActive ? 'running' : 'warning') },
crowdSecActive ? 'Active' : 'Inactive')
]), ]),
E('div', { 'class': 'sb-quick-stat-value' }, securityApps.length), E('div', { 'class': 'sb-quick-stat-value' }, totalBlocked.toLocaleString()),
E('div', { 'class': 'sb-quick-stat-label' }, 'Security Modules') E('div', { 'class': 'sb-quick-stat-label' }, 'IPs Blocked')
]), ]),
// Network Apps // Network Apps
@ -300,8 +315,18 @@ return view.extend({
' / ' + this.formatBytes(sysInfo.memory ? sysInfo.memory.total : 0) + ' used'), ' / ' + this.formatBytes(sysInfo.memory ? sysInfo.memory.total : 0) + ' used'),
E('span', { 'class': 'sb-events-meta' }, 'Resource Usage') E('span', { 'class': 'sb-events-meta' }, 'Resource Usage')
]) ])
]) ]),
]) totalBlocked > 0 ? E('div', { 'class': 'sb-events-item' }, [
E('div', { 'class': 'sb-events-icon security' }, '\ud83d\udee1\ufe0f'),
E('div', { 'class': 'sb-events-content' }, [
E('p', { 'class': 'sb-events-message' },
'IPv4: ' + blockedIPv4.toLocaleString() + ' (' +
(crowdSecStats.ipv4_capi_count || 0) + ' CAPI + ' +
(crowdSecStats.ipv4_cscli_count || 0) + ' local) | IPv6: ' + blockedIPv6.toLocaleString()),
E('span', { 'class': 'sb-events-meta' }, 'CrowdSec Firewall Protection')
])
]) : null
].filter(Boolean))
]); ]);
}, },