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:
parent
d1bc9a9b63
commit
ddae65d0fc
@ -37,7 +37,8 @@ return view.extend({
|
|||||||
this.csApi.getSecuboxLogs(),
|
this.csApi.getSecuboxLogs(),
|
||||||
this.csApi.getHealthCheck().catch(function() { return {}; }),
|
this.csApi.getHealthCheck().catch(function() { return {}; }),
|
||||||
this.csApi.getCapiMetrics().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 {}; })
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -452,6 +453,10 @@ return view.extend({
|
|||||||
this.renderCollectionsCard()
|
this.renderCollectionsCard()
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cs-charts-row' }, [
|
||||||
|
this.renderFirewallBlocks()
|
||||||
|
]),
|
||||||
|
|
||||||
E('div', { 'class': 'cs-charts-row' }, [
|
E('div', { 'class': 'cs-charts-row' }, [
|
||||||
E('div', { 'class': 'cs-card', 'style': 'flex: 2' }, [
|
E('div', { 'class': 'cs-card', 'style': 'flex: 2' }, [
|
||||||
E('div', { 'class': 'cs-card-header' }, [
|
E('div', { 'class': 'cs-card-header' }, [
|
||||||
@ -514,6 +519,7 @@ return view.extend({
|
|||||||
this.healthCheck = payload[2] || {};
|
this.healthCheck = payload[2] || {};
|
||||||
this.capiMetrics = payload[3] || {};
|
this.capiMetrics = payload[3] || {};
|
||||||
this.collections = (payload[4] && payload[4].collections) || [];
|
this.collections = (payload[4] && payload[4].collections) || [];
|
||||||
|
this.nftablesStats = payload[5] || {};
|
||||||
|
|
||||||
// Main wrapper with SecuBox header
|
// Main wrapper with SecuBox header
|
||||||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||||
@ -541,13 +547,15 @@ refreshDashboard: function() {
|
|||||||
self.csApi.getSecuboxLogs(),
|
self.csApi.getSecuboxLogs(),
|
||||||
self.csApi.getHealthCheck().catch(function() { return {}; }),
|
self.csApi.getHealthCheck().catch(function() { return {}; }),
|
||||||
self.csApi.getCapiMetrics().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) {
|
]).then(function(results) {
|
||||||
self.data = results[0];
|
self.data = results[0];
|
||||||
self.logs = (results[1] && results[1].entries) || [];
|
self.logs = (results[1] && results[1].entries) || [];
|
||||||
self.healthCheck = results[2] || {};
|
self.healthCheck = results[2] || {};
|
||||||
self.capiMetrics = results[3] || {};
|
self.capiMetrics = results[3] || {};
|
||||||
self.collections = (results[4] && results[4].collections) || [];
|
self.collections = (results[4] && results[4].collections) || [];
|
||||||
|
self.nftablesStats = results[5] || {};
|
||||||
self.updateView();
|
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) {
|
renderLogCard: function(entries) {
|
||||||
return E('div', { 'class': 'cs-card cs-log-card' }, [
|
return E('div', { 'class': 'cs-card cs-log-card' }, [
|
||||||
E('div', { 'class': 'cs-card-header' }, [
|
E('div', { 'class': 'cs-card-header' }, [
|
||||||
|
|||||||
@ -845,31 +845,65 @@ get_nftables_stats() {
|
|||||||
fi
|
fi
|
||||||
json_add_boolean "ipv6_table_exists" "$ipv6_exists"
|
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"
|
json_add_array "ipv4_blocked_ips"
|
||||||
if [ "$ipv4_exists" = "1" ]; then
|
if [ "$ipv4_exists" = "1" ]; then
|
||||||
local ips
|
# Get all set names
|
||||||
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 "")
|
local sets
|
||||||
if [ -n "$ips" ]; then
|
sets=$(nft list sets ip 2>/dev/null | grep "set crowdsec-blacklists" | sed 's/.*set //' | sed 's/ {//')
|
||||||
local ip
|
|
||||||
for ip in $ips; do
|
local setname ips ip
|
||||||
json_add_string "" "$ip"
|
for setname in $sets; do
|
||||||
done
|
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)
|
||||||
fi
|
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
|
fi
|
||||||
json_close_array
|
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"
|
json_add_array "ipv6_blocked_ips"
|
||||||
if [ "$ipv6_exists" = "1" ]; then
|
if [ "$ipv6_exists" = "1" ]; then
|
||||||
local ips
|
local sets
|
||||||
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 "")
|
sets=$(nft list sets ip6 2>/dev/null | grep "set crowdsec6-blacklists" | sed 's/.*set //' | sed 's/ {//')
|
||||||
if [ -n "$ips" ]; then
|
|
||||||
local ip
|
local setname ips ip
|
||||||
for ip in $ips; do
|
for setname in $sets; do
|
||||||
json_add_string "" "$ip"
|
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)
|
||||||
done
|
if [ -n "$ips" ]; then
|
||||||
fi
|
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
|
fi
|
||||||
json_close_array
|
json_close_array
|
||||||
|
|
||||||
@ -885,6 +919,14 @@ get_nftables_stats() {
|
|||||||
json_add_int "ipv4_rules_count" "$ipv4_rules"
|
json_add_int "ipv4_rules_count" "$ipv4_rules"
|
||||||
json_add_int "ipv6_rules_count" "$ipv6_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
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user