feat(crowdsec-dashboard): Add Firewall Blocks section with nftables visualization

- Scan ALL nftables sets (CAPI, cscli, etc.) instead of just base set
- Display blocked IPs count by origin (Community vs Local)
- Show sample of blocked IPs with Unban button
- Add ipv4_capi_count, ipv4_cscli_count, ipv4_total_count to API response
- Support for 14,000+ community blocklist IPs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-12 17:35:37 +01:00
parent d1bc9a9b63
commit ddae65d0fc
2 changed files with 176 additions and 20 deletions

View File

@ -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' }, [

View File

@ -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
}