diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js index d161ca98..778da489 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js @@ -37,7 +37,8 @@ return view.extend({ this.csApi.getSecuboxLogs(), this.csApi.getHealthCheck().catch(function() { return {}; }), this.csApi.getCapiMetrics().catch(function() { return {}; }), - this.csApi.getCollections().catch(function() { return { collections: [] }; }) + this.csApi.getCollections().catch(function() { return { collections: [] }; }), + this.csApi.getNftablesStats().catch(function() { return {}; }) ]); }, @@ -451,6 +452,10 @@ return view.extend({ this.renderCapiBlocklist(), this.renderCollectionsCard() ]), + + E('div', { 'class': 'cs-charts-row' }, [ + this.renderFirewallBlocks() + ]), E('div', { 'class': 'cs-charts-row' }, [ E('div', { 'class': 'cs-card', 'style': 'flex: 2' }, [ @@ -514,6 +519,7 @@ return view.extend({ this.healthCheck = payload[2] || {}; this.capiMetrics = payload[3] || {}; this.collections = (payload[4] && payload[4].collections) || []; + this.nftablesStats = payload[5] || {}; // Main wrapper with SecuBox header var wrapper = E('div', { 'class': 'secubox-page-wrapper' }); @@ -541,13 +547,15 @@ refreshDashboard: function() { self.csApi.getSecuboxLogs(), self.csApi.getHealthCheck().catch(function() { return {}; }), self.csApi.getCapiMetrics().catch(function() { return {}; }), - self.csApi.getCollections().catch(function() { return { collections: [] }; }) + self.csApi.getCollections().catch(function() { return { collections: [] }; }), + self.csApi.getNftablesStats().catch(function() { return {}; }) ]).then(function(results) { self.data = results[0]; self.logs = (results[1] && results[1].entries) || []; self.healthCheck = results[2] || {}; self.capiMetrics = results[3] || {}; self.collections = (results[4] && results[4].collections) || []; + self.nftablesStats = results[5] || {}; self.updateView(); }); }, @@ -731,6 +739,112 @@ refreshDashboard: function() { }); }, + // Firewall Blocks - Shows IPs blocked in nftables + renderFirewallBlocks: function() { + var self = this; + var stats = this.nftablesStats || {}; + + // Check if nftables available + if (stats.error) { + return E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, [ + E('div', { 'class': 'cs-card-title' }, _('Firewall Blocks')) + ]), + E('div', { 'class': 'cs-card-body' }, [ + E('div', { 'class': 'cs-empty' }, [ + E('div', { 'class': 'cs-empty-icon' }, '⚠️'), + E('p', {}, stats.error) + ]) + ]) + ]); + } + + var ipv4Active = stats.ipv4_table_exists; + var ipv6Active = stats.ipv6_table_exists; + var ipv4List = stats.ipv4_blocked_ips || []; + var ipv6List = stats.ipv6_blocked_ips || []; + var ipv4Rules = stats.ipv4_rules_count || 0; + var ipv6Rules = stats.ipv6_rules_count || 0; + // Use total counts from API (includes all IPs, not just sample) + var ipv4Total = stats.ipv4_total_count || ipv4List.length; + var ipv6Total = stats.ipv6_total_count || ipv6List.length; + var ipv4Capi = stats.ipv4_capi_count || 0; + var ipv4Cscli = stats.ipv4_cscli_count || 0; + var ipv6Capi = stats.ipv6_capi_count || 0; + var ipv6Cscli = stats.ipv6_cscli_count || 0; + var totalBlocked = ipv4Total + ipv6Total; + + // Build IP list (combine IPv4 and IPv6, limit to 20) + var allIps = []; + ipv4List.forEach(function(ip) { allIps.push({ ip: ip, type: 'IPv4' }); }); + ipv6List.forEach(function(ip) { allIps.push({ ip: ip, type: 'IPv6' }); }); + var displayIps = allIps.slice(0, 20); + + var ipRows = displayIps.map(function(item) { + return E('div', { + 'style': 'display: flex; align-items: center; justify-content: space-between; padding: 0.5em 0; border-bottom: 1px solid rgba(255,255,255,0.1);' + }, [ + E('div', { 'style': 'display: flex; align-items: center; gap: 0.75em;' }, [ + E('span', { 'style': 'font-size: 1.1em;' }, '🚫'), + E('code', { 'style': 'font-size: 0.85em; background: rgba(0,0,0,0.2); padding: 0.2em 0.5em; border-radius: 4px;' }, item.ip), + E('span', { + 'style': 'font-size: 0.7em; padding: 0.15em 0.4em; background: ' + (item.type === 'IPv4' ? '#667eea' : '#764ba2') + '; border-radius: 3px;' + }, item.type) + ]), + E('button', { + 'class': 'cs-btn cs-btn-danger cs-btn-sm', + 'style': 'font-size: 0.75em; padding: 0.25em 0.5em;', + 'click': ui.createHandlerFn(self, 'handleUnban', item.ip) + }, _('Unban')) + ]); + }); + + // Status indicators with breakdown by origin + var statusRow = E('div', { 'style': 'display: flex; gap: 1.5em; margin-bottom: 1em; flex-wrap: wrap;' }, [ + E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ + E('span', { 'style': 'font-size: 1.2em;' }, ipv4Active ? '✅' : '❌'), + E('span', { 'style': 'font-size: 0.85em;' }, 'IPv4'), + E('span', { 'style': 'font-size: 0.75em; color: #888;' }, ipv4Total.toLocaleString() + ' IPs') + ]), + E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ + E('span', { 'style': 'font-size: 1.2em;' }, ipv6Active ? '✅' : '❌'), + E('span', { 'style': 'font-size: 0.85em;' }, 'IPv6'), + E('span', { 'style': 'font-size: 0.75em; color: #888;' }, ipv6Total.toLocaleString() + ' IPs') + ]), + E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding-left: 1em; border-left: 1px solid rgba(255,255,255,0.2);' }, [ + E('span', { 'style': 'font-size: 0.8em; padding: 0.2em 0.5em; background: #667eea; border-radius: 4px;' }, 'CAPI'), + E('span', { 'style': 'font-size: 0.85em; color: #667eea;' }, (ipv4Capi + ipv6Capi).toLocaleString()) + ]), + E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ + E('span', { 'style': 'font-size: 0.8em; padding: 0.2em 0.5em; background: #00d4aa; border-radius: 4px;' }, 'Local'), + E('span', { 'style': 'font-size: 0.85em; color: #00d4aa;' }, (ipv4Cscli + ipv6Cscli).toLocaleString()) + ]) + ]); + + return E('div', { 'class': 'cs-card', 'style': 'flex: 2;' }, [ + E('div', { 'class': 'cs-card-header' }, [ + E('div', { 'class': 'cs-card-title' }, [ + _('Firewall Blocks'), + E('span', { + 'style': 'margin-left: 0.75em; font-size: 0.8em; padding: 0.2em 0.6em; background: linear-gradient(90deg, #ff4757, #ff6b81); border-radius: 12px;' + }, totalBlocked + ' blocked') + ]) + ]), + E('div', { 'class': 'cs-card-body' }, [ + statusRow, + ipRows.length > 0 ? + E('div', { 'style': 'max-height: 300px; overflow-y: auto;' }, ipRows) : + E('div', { 'class': 'cs-empty', 'style': 'padding: 1em;' }, [ + E('div', { 'class': 'cs-empty-icon' }, '✅'), + E('p', {}, _('No IPs currently blocked in firewall')) + ]), + allIps.length > 20 ? E('div', { 'style': 'text-align: center; padding: 0.5em; font-size: 0.8em; color: #888;' }, + _('Showing 20 of ') + allIps.length + _(' blocked IPs') + ) : E('span') + ]) + ]); + }, + renderLogCard: function(entries) { return E('div', { 'class': 'cs-card cs-log-card' }, [ E('div', { 'class': 'cs-card-header' }, [ diff --git a/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard b/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard index 9a63dfdc..b84bdbfa 100755 --- a/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard +++ b/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard @@ -845,31 +845,65 @@ get_nftables_stats() { fi json_add_boolean "ipv6_table_exists" "$ipv6_exists" - # Get blocked IPs from IPv4 set + # Get blocked IPs from ALL IPv4 sets (CAPI, cscli, etc.) + local ipv4_total=0 + local ipv4_capi=0 + local ipv4_cscli=0 + local ipv4_other=0 + json_add_array "ipv4_blocked_ips" if [ "$ipv4_exists" = "1" ]; then - local ips - ips=$(nft list set ip crowdsec crowdsec-blacklists 2>/dev/null | sed -n '/elements = {/,/}/p' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' || echo "") - if [ -n "$ips" ]; then - local ip - for ip in $ips; do - json_add_string "" "$ip" - done - fi + # Get all set names + local sets + sets=$(nft list sets ip 2>/dev/null | grep "set crowdsec-blacklists" | sed 's/.*set //' | sed 's/ {//') + + local setname ips ip + for setname in $sets; do + ips=$(nft list set ip crowdsec "$setname" 2>/dev/null | sed -n '/elements = {/,/}/p' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?' | head -50) + if [ -n "$ips" ]; then + for ip in $ips; do + json_add_string "" "$ip" + ipv4_total=$((ipv4_total + 1)) + done + fi + # Count by origin + local count + count=$(nft list set ip crowdsec "$setname" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | wc -l) + case "$setname" in + *-CAPI) ipv4_capi=$((ipv4_capi + count)) ;; + *-cscli) ipv4_cscli=$((ipv4_cscli + count)) ;; + *) ipv4_other=$((ipv4_other + count)) ;; + esac + done fi json_close_array - # Get blocked IPs from IPv6 set + # Get blocked IPs from ALL IPv6 sets + local ipv6_total=0 + local ipv6_capi=0 + local ipv6_cscli=0 + json_add_array "ipv6_blocked_ips" if [ "$ipv6_exists" = "1" ]; then - local ips - ips=$(nft list set ip6 crowdsec6 crowdsec6-blacklists 2>/dev/null | sed -n '/elements = {/,/}/p' | grep -oE '([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}' || echo "") - if [ -n "$ips" ]; then - local ip - for ip in $ips; do - json_add_string "" "$ip" - done - fi + local sets + sets=$(nft list sets ip6 2>/dev/null | grep "set crowdsec6-blacklists" | sed 's/.*set //' | sed 's/ {//') + + local setname ips ip + for setname in $sets; do + ips=$(nft list set ip6 crowdsec6 "$setname" 2>/dev/null | sed -n '/elements = {/,/}/p' | grep -oE '([0-9a-fA-F:]+:+)+[0-9a-fA-F]+' | head -50) + if [ -n "$ips" ]; then + for ip in $ips; do + json_add_string "" "$ip" + ipv6_total=$((ipv6_total + 1)) + done + fi + local count + count=$(nft list set ip6 crowdsec6 "$setname" 2>/dev/null | grep -oE '([0-9a-fA-F:]+:+)+[0-9a-fA-F]+' | wc -l) + case "$setname" in + *-CAPI) ipv6_capi=$((ipv6_capi + count)) ;; + *-cscli) ipv6_cscli=$((ipv6_cscli + count)) ;; + esac + done fi json_close_array @@ -885,6 +919,14 @@ get_nftables_stats() { json_add_int "ipv4_rules_count" "$ipv4_rules" json_add_int "ipv6_rules_count" "$ipv6_rules" + # Add counts by origin + json_add_int "ipv4_capi_count" "$ipv4_capi" + json_add_int "ipv4_cscli_count" "$ipv4_cscli" + json_add_int "ipv4_total_count" "$((ipv4_capi + ipv4_cscli + ipv4_other))" + json_add_int "ipv6_capi_count" "$ipv6_capi" + json_add_int "ipv6_cscli_count" "$ipv6_cscli" + json_add_int "ipv6_total_count" "$((ipv6_capi + ipv6_cscli))" + json_dump }