feat(crowdsec-dashboard): Add system health check and CAPI metrics

- Add health_check API with LAPI/CAPI/Console status verification
- Add capi_metrics API for community blocklist statistics
- Add hub_available, install_hub_item, remove_hub_item APIs
- Add System Health panel to overview with visual status indicators
- Add CAPI Blocklist section showing community vs local decisions
- Add Installed Collections card with version display
- Fix settings.js syntax error (missing comma)
- Fix metrics.js null display in acquisition statistics
- Update ACL file with new RPC method permissions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-12 17:08:29 +01:00
parent 290eed2ba1
commit d1bc9a9b63
6 changed files with 632 additions and 12 deletions

View File

@ -250,6 +250,39 @@ var callAcquisitionMetrics = rpc.declare({
expect: { }
});
// Health Check & CAPI Methods
var callHealthCheck = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'health_check',
expect: { }
});
var callCapiMetrics = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'capi_metrics',
expect: { }
});
var callHubAvailable = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'hub_available',
expect: { }
});
var callInstallHubItem = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'install_hub_item',
params: ['item_type', 'item_name'],
expect: { }
});
var callRemoveHubItem = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'remove_hub_item',
params: ['item_type', 'item_name'],
expect: { }
});
function formatDuration(seconds) {
if (!seconds) return 'N/A';
if (seconds < 60) return seconds + 's';
@ -385,6 +418,13 @@ return baseclass.extend({
getAcquisitionConfig: callAcquisitionConfig,
getAcquisitionMetrics: callAcquisitionMetrics,
// Health Check & CAPI Methods
getHealthCheck: callHealthCheck,
getCapiMetrics: callCapiMetrics,
getHubAvailable: callHubAvailable,
installHubItem: callInstallHubItem,
removeHubItem: callRemoveHubItem,
formatDuration: formatDuration,
formatDate: formatDate,
formatRelativeTime: formatRelativeTime,

View File

@ -288,13 +288,13 @@ return view.extend({
E('div', { 'class': 'cyber-stat-card', 'style': 'background: var(--cyber-card-bg, rgba(30,30,40,0.8)); border: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); border-radius: 8px; padding: 1rem; text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-muted, #666); margin-bottom: 0.25rem; text-transform: uppercase;' }, _('Lines Read')),
E('div', { 'class': 'cyber-stat-value', 'style': 'font-size: 1.5rem; font-weight: 700; color: var(--cyber-accent-primary, #667eea);' }, this.formatNumber(totalRead)),
readRate > 0 ? E('div', { 'style': 'font-size: 0.7rem; color: var(--cyber-success, #00d4aa); margin-top: 0.25rem;' }, '+' + readRate + '/s') : null
readRate > 0 ? E('div', { 'style': 'font-size: 0.7rem; color: var(--cyber-success, #00d4aa); margin-top: 0.25rem;' }, '+' + readRate + '/s') : E('span')
]),
// Lines Parsed Card
E('div', { 'class': 'cyber-stat-card', 'style': 'background: var(--cyber-card-bg, rgba(30,30,40,0.8)); border: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); border-radius: 8px; padding: 1rem; text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-muted, #666); margin-bottom: 0.25rem; text-transform: uppercase;' }, _('Parsed')),
E('div', { 'class': 'cyber-stat-value', 'style': 'font-size: 1.5rem; font-weight: 700; color: var(--cyber-success, #00d4aa);' }, this.formatNumber(totalParsed)),
parsedRate > 0 ? E('div', { 'style': 'font-size: 0.7rem; color: var(--cyber-success, #00d4aa); margin-top: 0.25rem;' }, '+' + parsedRate + '/s') : null
parsedRate > 0 ? E('div', { 'style': 'font-size: 0.7rem; color: var(--cyber-success, #00d4aa); margin-top: 0.25rem;' }, '+' + parsedRate + '/s') : E('span')
]),
// Parse Rate Card with progress bar
E('div', { 'class': 'cyber-stat-card', 'style': 'background: var(--cyber-card-bg, rgba(30,30,40,0.8)); border: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); border-radius: 8px; padding: 1rem; text-align: center;' }, [

View File

@ -28,13 +28,16 @@ return view.extend({
cssLink.rel = 'stylesheet';
cssLink.href = L.resource('crowdsec-dashboard/dashboard.css');
document.head.appendChild(cssLink);
// Load API
this.csApi = api;
return Promise.all([
this.csApi.getDashboardData(),
this.csApi.getSecuboxLogs()
this.csApi.getSecuboxLogs(),
this.csApi.getHealthCheck().catch(function() { return {}; }),
this.csApi.getCapiMetrics().catch(function() { return {}; }),
this.csApi.getCollections().catch(function() { return { collections: [] }; })
]);
},
@ -426,8 +429,9 @@ return view.extend({
return E('div', {}, [
this.renderHeader(status),
serviceWarning,
this.renderHealthCheck(),
this.renderStatsGrid(stats, decisions),
E('div', { 'class': 'cs-charts-row' }, [
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
@ -442,6 +446,11 @@ return view.extend({
E('div', { 'class': 'cs-card-body' }, this.renderTopCountries(stats))
])
]),
E('div', { 'class': 'cs-charts-row' }, [
this.renderCapiBlocklist(),
this.renderCollectionsCard()
]),
E('div', { 'class': 'cs-charts-row' }, [
E('div', { 'class': 'cs-card', 'style': 'flex: 2' }, [
@ -502,6 +511,9 @@ return view.extend({
var self = this;
this.data = payload[0] || {};
this.logs = (payload[1] && payload[1].entries) || [];
this.healthCheck = payload[2] || {};
this.capiMetrics = payload[3] || {};
this.collections = (payload[4] && payload[4].collections) || [];
// Main wrapper with SecuBox header
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
@ -526,14 +538,199 @@ refreshDashboard: function() {
var self = this;
return Promise.all([
self.csApi.getDashboardData(),
self.csApi.getSecuboxLogs()
self.csApi.getSecuboxLogs(),
self.csApi.getHealthCheck().catch(function() { return {}; }),
self.csApi.getCapiMetrics().catch(function() { return {}; }),
self.csApi.getCollections().catch(function() { return { collections: [] }; })
]).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.updateView();
});
},
// Health Check Section - Shows LAPI/CAPI/Console status
renderHealthCheck: function() {
var health = this.healthCheck || {};
var csRunning = health.crowdsec_running;
var lapiStatus = health.lapi_status || 'unavailable';
var capiStatus = health.capi_status || 'disconnected';
var capiEnrolled = health.capi_enrolled;
var capiSubscription = health.capi_subscription || '-';
var sharingSignals = health.sharing_signals;
var pullingBlocklist = health.pulling_blocklist;
var version = health.version || 'N/A';
var decisionsCount = health.decisions_count || 0;
return E('div', { 'class': 'cs-health-check', 'style': 'margin-bottom: 1.5em;' }, [
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, _('System Health'))
]),
E('div', { 'class': 'cs-card-body' }, [
E('div', { 'class': 'cs-health-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1em;' }, [
// CrowdSec Status
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, csRunning ? '✅' : '❌'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'CrowdSec'),
E('div', { 'style': 'font-size: 0.85em; color: ' + (csRunning ? '#00d4aa' : '#ff4757') + ';' }, csRunning ? 'Running' : 'Stopped'),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, version ? (version.charAt(0) === 'v' ? version : 'v' + version) : '')
]),
// LAPI Status
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, lapiStatus === 'available' ? '✅' : '❌'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'LAPI'),
E('div', { 'style': 'font-size: 0.85em; color: ' + (lapiStatus === 'available' ? '#00d4aa' : '#ff4757') + ';' }, lapiStatus === 'available' ? 'Available' : 'Unavailable'),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, ':8080')
]),
// CAPI Status
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, capiStatus === 'connected' ? '✅' : '⚠️'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'CAPI'),
E('div', { 'style': 'font-size: 0.85em; color: ' + (capiStatus === 'connected' ? '#00d4aa' : '#ffa500') + ';' }, capiStatus === 'connected' ? 'Connected' : 'Disconnected'),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, capiSubscription)
]),
// Console Status
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, capiEnrolled ? '✅' : '⚪'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'Console'),
E('div', { 'style': 'font-size: 0.85em; color: ' + (capiEnrolled ? '#00d4aa' : '#888') + ';' }, capiEnrolled ? 'Enrolled' : 'Not Enrolled'),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, sharingSignals ? 'Sharing: ON' : 'Sharing: OFF')
]),
// Blocklist Status
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, pullingBlocklist ? '🛡️' : '⚪'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'Blocklist'),
E('div', { 'style': 'font-size: 0.85em; color: ' + (pullingBlocklist ? '#00d4aa' : '#888') + ';' }, pullingBlocklist ? 'Active' : 'Inactive'),
E('div', { 'style': 'font-size: 0.75em; color: #667eea; font-weight: 600;' }, decisionsCount.toLocaleString() + ' IPs')
])
])
])
])
]);
},
// CAPI Blocklist Metrics - Shows blocked IPs by category
renderCapiBlocklist: function() {
var metrics = this.capiMetrics || {};
var totalCapi = metrics.total_capi || 0;
var totalLocal = metrics.total_local || 0;
var breakdown = metrics.breakdown || [];
if (totalCapi === 0 && totalLocal === 0) {
return E('span'); // Empty if no data
}
// Build breakdown bars
var maxCount = Math.max.apply(null, breakdown.map(function(b) { return b.count || 0; }).concat([1]));
var breakdownBars = breakdown.slice(0, 5).map(function(item) {
var scenario = item.scenario || 'unknown';
var count = item.count || 0;
var pct = Math.round((count / maxCount) * 100);
var displayName = scenario.split('/').pop().replace(/-/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
return E('div', { 'style': 'margin-bottom: 0.75em;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.25em;' }, [
E('span', { 'style': 'font-size: 0.85em;' }, displayName),
E('span', { 'style': 'font-size: 0.85em; font-weight: 600; color: #667eea;' }, count.toLocaleString())
]),
E('div', { 'style': 'height: 8px; background: rgba(102,126,234,0.2); border-radius: 4px; overflow: hidden;' }, [
E('div', { 'style': 'height: 100%; width: ' + pct + '%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px;' })
])
]);
});
return E('div', { 'class': 'cs-capi-blocklist', 'style': 'margin-bottom: 1.5em;' }, [
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, _('Community Blocklist (CAPI)'))
]),
E('div', { 'class': 'cs-card-body' }, [
E('div', { 'style': 'display: flex; gap: 2em; margin-bottom: 1em;' }, [
E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #667eea;' }, totalCapi.toLocaleString()),
E('div', { 'style': 'font-size: 0.8em; color: #888;' }, 'CAPI Blocked')
]),
E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #00d4aa;' }, totalLocal.toLocaleString()),
E('div', { 'style': 'font-size: 0.8em; color: #888;' }, 'Local Blocked')
])
]),
breakdownBars.length > 0 ? E('div', { 'style': 'margin-top: 1em;' }, [
E('div', { 'style': 'font-size: 0.85em; font-weight: 600; margin-bottom: 0.75em; color: #888;' }, _('Top Blocked Categories')),
E('div', {}, breakdownBars)
]) : E('span')
])
])
]);
},
// Collections Card - Shows installed collections with quick actions
renderCollectionsCard: function() {
var self = this;
var collections = this.collections || [];
if (!collections.length) {
return E('span'); // Empty if no collections
}
var collectionItems = collections.slice(0, 6).map(function(col) {
var name = col.name || col.Name || 'unknown';
var status = col.status || col.Status || '';
var version = col.version || col.Version || '';
var isInstalled = status.toLowerCase().indexOf('enabled') >= 0 || status.toLowerCase().indexOf('installed') >= 0;
var hasUpdate = status.toLowerCase().indexOf('update') >= 0;
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.5em;' }, [
E('span', { 'style': 'font-size: 1.2em;' }, isInstalled ? '✅' : '⬜'),
E('span', { 'style': 'font-size: 0.9em;' }, name)
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('span', { 'style': 'font-size: 0.75em; color: #888;' }, 'v' + version),
hasUpdate ? E('span', { 'style': 'font-size: 0.7em; padding: 0.15em 0.4em; background: #ffa500; color: #000; border-radius: 3px;' }, 'UPDATE') : E('span')
])
]);
});
return E('div', { 'class': 'cs-collections-card' }, [
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, _('Installed Collections')),
E('button', {
'class': 'cs-btn cs-btn-secondary cs-btn-sm',
'click': ui.createHandlerFn(this, 'handleUpdateHub')
}, _('Update Hub'))
]),
E('div', { 'class': 'cs-card-body' }, collectionItems)
])
]);
},
handleUpdateHub: function() {
var self = this;
ui.showModal(_('Updating Hub'), [
E('p', {}, _('Downloading latest hub index...')),
E('div', { 'class': 'spinning' })
]);
this.csApi.updateHub().then(function(result) {
ui.hideModal();
if (result && result.success) {
self.showToast(_('Hub updated successfully'), 'success');
self.refreshDashboard();
} else {
self.showToast((result && result.error) || _('Hub update failed'), 'error');
}
}).catch(function(err) {
ui.hideModal();
self.showToast(err.message || _('Hub update failed'), 'error');
});
},
renderLogCard: function(entries) {
return E('div', { 'class': 'cs-card cs-log-card' }, [
E('div', { 'class': 'cs-card-header' }, [

View File

@ -616,7 +616,7 @@ return view.extend({
}
}, _('Apply Interface Settings'))
])
])
]),
// Firewall Bouncer quick control
E('div', { 'style': 'margin-top: 1em; padding: 1em; background: #fff; border-radius: 6px; border: 1px solid #e6e6e6;' }, [

View File

@ -1054,7 +1054,7 @@ repair_lapi() {
if [ -x "$CSCLI" ]; then
if run_with_timeout 5 "$CSCLI" lapi status >/dev/null 2>&1; then
lapi_ok=1
steps_done="${steps_done}LAPI OK"
steps_done="${steps_done}LAPI OK; "
else
errors="${errors}LAPI check failed; "
fi
@ -1067,6 +1067,31 @@ repair_lapi() {
[ -n "$log_err" ] && errors="${errors}${log_err}; "
fi
# Step 12: Update hub index (required for collections to work)
if [ "$lapi_ok" = "1" ] && [ -x "$CSCLI" ]; then
if run_with_timeout 30 "$CSCLI" hub update >/dev/null 2>&1; then
steps_done="${steps_done}Hub updated; "
else
errors="${errors}Hub update failed; "
fi
fi
# Step 13: Register with CAPI (required for console enrollment)
if [ "$lapi_ok" = "1" ] && [ -x "$CSCLI" ]; then
if run_with_timeout 15 "$CSCLI" capi register >/dev/null 2>&1; then
steps_done="${steps_done}CAPI registered; "
else
# CAPI registration may fail if already registered, which is OK
local capi_status=""
capi_status=$(run_with_timeout 5 "$CSCLI" capi status 2>&1)
if echo "$capi_status" | grep -qi "registered\|online"; then
steps_done="${steps_done}CAPI OK; "
else
errors="${errors}CAPI not registered; "
fi
fi
fi
if [ "$lapi_ok" = "1" ]; then
json_add_boolean "success" 1
json_add_string "message" "LAPI repaired successfully"
@ -1191,6 +1216,26 @@ console_enroll() {
secubox_log "Enrolling CrowdSec Console with key..."
# Step 0: Ensure CAPI is registered (prerequisite for console enrollment)
local capi_ok=0
local capi_status=""
capi_status=$(run_with_timeout 5 "$CSCLI" capi status 2>&1)
if echo "$capi_status" | grep -qi "registered\|online"; then
capi_ok=1
else
# Try to register with CAPI
secubox_log "CAPI not registered, attempting registration..."
if run_with_timeout 15 "$CSCLI" capi register >/dev/null 2>&1; then
capi_ok=1
secubox_log "CAPI registration successful"
else
json_add_boolean "success" 0
json_add_string "error" "CAPI registration failed. Please run LAPI repair first."
json_dump
return
fi
fi
# Build enroll command
local enroll_cmd="run_cscli console enroll $key"
if [ -n "$name" ]; then
@ -1527,10 +1572,322 @@ service_control() {
json_dump
}
# Complete health check for dashboard
get_health_check() {
json_init
# CrowdSec running status
local cs_running=0
if pgrep crowdsec >/dev/null 2>&1; then
cs_running=1
fi
json_add_boolean "crowdsec_running" "$cs_running"
# Version
local version=""
if [ -x "$CSCLI" ]; then
version=$(run_cscli version 2>/dev/null | grep "version:" | awk '{print $2}')
fi
json_add_string "version" "${version:-unknown}"
# LAPI status
local lapi_status="unavailable"
local lapi_url="http://127.0.0.1:8080"
if [ -x "$CSCLI" ]; then
if run_with_timeout 5 "$CSCLI" lapi status >/dev/null 2>&1; then
lapi_status="available"
fi
fi
json_add_string "lapi_status" "$lapi_status"
json_add_string "lapi_url" "$lapi_url"
# CAPI status - parse cscli capi status output
local capi_status="disconnected"
local capi_enrolled=0
local capi_subscription=""
local sharing_signals=0
local pulling_blocklist=0
local pulling_console=0
if [ -x "$CSCLI" ]; then
local capi_output=""
capi_output=$(run_with_timeout 10 "$CSCLI" capi status 2>&1)
if echo "$capi_output" | grep -qi "You can successfully interact with Central API"; then
capi_status="connected"
fi
if echo "$capi_output" | grep -qi "enrolled in the console"; then
capi_enrolled=1
fi
if echo "$capi_output" | grep -qi "COMMUNITY"; then
capi_subscription="COMMUNITY"
elif echo "$capi_output" | grep -qi "PRO"; then
capi_subscription="PRO"
fi
if echo "$capi_output" | grep -qi "Sharing signals is enabled"; then
sharing_signals=1
fi
if echo "$capi_output" | grep -qi "Pulling community blocklist is enabled"; then
pulling_blocklist=1
fi
if echo "$capi_output" | grep -qi "Pulling blocklists from the console is enabled"; then
pulling_console=1
fi
fi
json_add_string "capi_status" "$capi_status"
json_add_boolean "capi_enrolled" "$capi_enrolled"
json_add_string "capi_subscription" "$capi_subscription"
json_add_boolean "sharing_signals" "$sharing_signals"
json_add_boolean "pulling_blocklist" "$pulling_blocklist"
json_add_boolean "pulling_console" "$pulling_console"
# Machine info
local machine_id=""
local machine_version=""
if [ -x "$CSCLI" ]; then
local machines_output=""
machines_output=$(run_cscli machines list -o json 2>/dev/null)
if [ -n "$machines_output" ] && [ "$machines_output" != "null" ]; then
machine_id=$(echo "$machines_output" | jsonfilter -e '@[0].machineId' 2>/dev/null)
machine_version=$(echo "$machines_output" | jsonfilter -e '@[0].version' 2>/dev/null)
fi
fi
json_add_string "machine_id" "${machine_id:-localhost}"
json_add_string "machine_version" "$machine_version"
# Bouncer count
local bouncer_count=0
if [ -x "$CSCLI" ]; then
bouncer_count=$(run_cscli bouncers list -o json 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
fi
json_add_int "bouncer_count" "${bouncer_count:-0}"
# Total decisions count
local decisions_count=0
if [ -x "$CSCLI" ]; then
decisions_count=$(run_cscli decisions list -o json 2>/dev/null | jsonfilter -e '@[*].decisions[*]' 2>/dev/null | wc -l)
fi
json_add_int "decisions_count" "${decisions_count:-0}"
json_dump
}
# Get CAPI blocklist metrics (decisions by origin and reason)
get_capi_metrics() {
json_init
if [ ! -x "$CSCLI" ]; then
json_add_boolean "available" 0
json_add_string "error" "cscli not found"
json_dump
return
fi
# Get all decisions
local decisions_output=""
decisions_output=$(run_cscli decisions list -o json 2>/dev/null)
if [ -z "$decisions_output" ] || [ "$decisions_output" = "null" ]; then
json_add_boolean "available" 1
json_add_int "total_capi" 0
json_add_int "total_local" 0
json_add_string "breakdown" "[]"
json_dump
return
fi
json_add_boolean "available" 1
# Count by origin
local capi_count=0
local local_count=0
# Parse decisions and count by origin
# The structure is: [{decisions: [...], ...}, ...]
# We need to count decisions where origin = "CAPI" or "crowdsec"
# Use a temp file for aggregation
local tmp_file="/tmp/capi_metrics.$$"
# Extract all decisions with their origin and scenario
echo "$decisions_output" | jsonfilter -e '@[*].decisions[*]' 2>/dev/null | while read -r decision; do
local origin=$(echo "$decisions_output" | jsonfilter -e '@[*].decisions[*].origin' 2>/dev/null | head -1)
local scenario=$(echo "$decisions_output" | jsonfilter -e '@[*].scenario' 2>/dev/null | head -1)
echo "$origin|$scenario"
done > "$tmp_file" 2>/dev/null
# Count CAPI decisions by scenario using awk
capi_count=$(echo "$decisions_output" | grep -o '"origin":"CAPI"' 2>/dev/null | wc -l)
local_count=$(echo "$decisions_output" | grep -o '"origin":"crowdsec"' 2>/dev/null | wc -l)
json_add_int "total_capi" "${capi_count:-0}"
json_add_int "total_local" "${local_count:-0}"
# Build breakdown by scenario for CAPI decisions
# Parse the JSON more carefully
json_add_array "breakdown"
# Extract unique scenarios and their counts from CAPI decisions
local scenarios=""
scenarios=$(echo "$decisions_output" | grep -oE '"scenario":"[^"]*"' | sort | uniq -c | sort -rn | head -10)
echo "$scenarios" | while read -r count scenario; do
if [ -n "$count" ] && [ -n "$scenario" ]; then
local name=$(echo "$scenario" | sed 's/"scenario":"//; s/"$//')
json_add_object ""
json_add_string "scenario" "$name"
json_add_int "count" "$count"
json_close_object
fi
done
json_close_array
rm -f "$tmp_file" 2>/dev/null
json_dump
}
# Get available hub items (not installed)
get_hub_available() {
json_init
if [ ! -x "$CSCLI" ]; then
json_add_boolean "available" 0
json_add_string "error" "cscli not found"
json_dump
return
fi
json_add_boolean "available" 1
# Get hub list in JSON format (all items)
local hub_output=""
hub_output=$(run_with_timeout 30 "$CSCLI" hub list -a -o json 2>/dev/null)
if [ -z "$hub_output" ]; then
json_add_string "collections" "[]"
json_add_string "parsers" "[]"
json_add_string "scenarios" "[]"
json_dump
return
fi
# Output the raw hub data - frontend will parse it
echo "$hub_output"
}
# Install a hub item (collection, parser, scenario)
install_hub_item() {
local item_type="$1"
local item_name="$2"
json_init
if [ -z "$item_type" ] || [ -z "$item_name" ]; then
json_add_boolean "success" 0
json_add_string "error" "Item type and name are required"
json_dump
return
fi
# Validate item type
case "$item_type" in
collection|parser|scenario|postoverflow|context)
;;
*)
json_add_boolean "success" 0
json_add_string "error" "Invalid item type: $item_type"
json_dump
return
;;
esac
if [ ! -x "$CSCLI" ]; then
json_add_boolean "success" 0
json_add_string "error" "cscli not found"
json_dump
return
fi
secubox_log "Installing CrowdSec $item_type: $item_name"
# Install the item
local output=""
output=$(run_with_timeout 60 "$CSCLI" "${item_type}s" install "$item_name" 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Successfully installed $item_type: $item_name"
json_add_string "output" "$output"
secubox_log "Installed $item_type: $item_name"
else
json_add_boolean "success" 0
json_add_string "error" "Failed to install $item_type: $item_name"
json_add_string "output" "$output"
secubox_log "Failed to install $item_type: $item_name - $output"
fi
json_dump
}
# Remove a hub item
remove_hub_item() {
local item_type="$1"
local item_name="$2"
json_init
if [ -z "$item_type" ] || [ -z "$item_name" ]; then
json_add_boolean "success" 0
json_add_string "error" "Item type and name are required"
json_dump
return
fi
case "$item_type" in
collection|parser|scenario|postoverflow|context)
;;
*)
json_add_boolean "success" 0
json_add_string "error" "Invalid item type: $item_type"
json_dump
return
;;
esac
if [ ! -x "$CSCLI" ]; then
json_add_boolean "success" 0
json_add_string "error" "cscli not found"
json_dump
return
fi
secubox_log "Removing CrowdSec $item_type: $item_name"
local output=""
output=$(run_with_timeout 30 "$CSCLI" "${item_type}s" remove "$item_name" 2>&1)
local result=$?
if [ "$result" -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Successfully removed $item_type: $item_name"
secubox_log "Removed $item_type: $item_name"
else
json_add_boolean "success" 0
json_add_string "error" "Failed to remove $item_type: $item_name"
json_add_string "output" "$output"
fi
json_dump
}
# Main dispatcher
case "$1" in
list)
echo '{"decisions":{},"alerts":{"limit":"number"},"metrics":{},"bouncers":{},"machines":{},"hub":{},"status":{},"ban":{"ip":"string","duration":"string","reason":"string"},"unban":{"ip":"string"},"stats":{},"seccubox_logs":{},"collect_debug":{},"waf_status":{},"metrics_config":{},"configure_metrics":{"enable":"string"},"collections":{},"install_collection":{"collection":"string"},"remove_collection":{"collection":"string"},"update_hub":{},"register_bouncer":{"bouncer_name":"string"},"delete_bouncer":{"bouncer_name":"string"},"firewall_bouncer_status":{},"control_firewall_bouncer":{"action":"string"},"firewall_bouncer_config":{},"update_firewall_bouncer_config":{"key":"string","value":"string"},"nftables_stats":{},"check_wizard_needed":{},"wizard_state":{},"repair_lapi":{},"reset_wizard":{},"console_status":{},"console_enroll":{"key":"string","name":"string"},"console_disable":{},"service_control":{"action":"string"},"configure_acquisition":{"syslog_enabled":"string","firewall_enabled":"string","ssh_enabled":"string","http_enabled":"string","syslog_path":"string"},"acquisition_config":{},"acquisition_metrics":{}}'
echo '{"decisions":{},"alerts":{"limit":"number"},"metrics":{},"bouncers":{},"machines":{},"hub":{},"status":{},"ban":{"ip":"string","duration":"string","reason":"string"},"unban":{"ip":"string"},"stats":{},"seccubox_logs":{},"collect_debug":{},"waf_status":{},"metrics_config":{},"configure_metrics":{"enable":"string"},"collections":{},"install_collection":{"collection":"string"},"remove_collection":{"collection":"string"},"update_hub":{},"register_bouncer":{"bouncer_name":"string"},"delete_bouncer":{"bouncer_name":"string"},"firewall_bouncer_status":{},"control_firewall_bouncer":{"action":"string"},"firewall_bouncer_config":{},"update_firewall_bouncer_config":{"key":"string","value":"string"},"nftables_stats":{},"check_wizard_needed":{},"wizard_state":{},"repair_lapi":{},"reset_wizard":{},"console_status":{},"console_enroll":{"key":"string","name":"string"},"console_disable":{},"service_control":{"action":"string"},"configure_acquisition":{"syslog_enabled":"string","firewall_enabled":"string","ssh_enabled":"string","http_enabled":"string","syslog_path":"string"},"acquisition_config":{},"acquisition_metrics":{},"health_check":{},"capi_metrics":{},"hub_available":{},"install_hub_item":{"item_type":"string","item_name":"string"},"remove_hub_item":{"item_type":"string","item_name":"string"}}'
;;
call)
case "$2" in
@ -1679,6 +2036,27 @@ case "$1" in
acquisition_metrics)
get_acquisition_metrics
;;
health_check)
get_health_check
;;
capi_metrics)
get_capi_metrics
;;
hub_available)
get_hub_available
;;
install_hub_item)
read -r input
item_type=$(echo "$input" | jsonfilter -e '@.item_type' 2>/dev/null)
item_name=$(echo "$input" | jsonfilter -e '@.item_name' 2>/dev/null)
install_hub_item "$item_type" "$item_name"
;;
remove_hub_item)
read -r input
item_type=$(echo "$input" | jsonfilter -e '@.item_type' 2>/dev/null)
item_name=$(echo "$input" | jsonfilter -e '@.item_name' 2>/dev/null)
remove_hub_item "$item_type" "$item_name"
;;
*)
echo '{"error": "Unknown method"}'
;;

View File

@ -23,7 +23,10 @@
"wizard_state",
"console_status",
"acquisition_config",
"acquisition_metrics"
"acquisition_metrics",
"health_check",
"capi_metrics",
"hub_available"
],
"file": [ "read", "stat" ]
},
@ -48,7 +51,9 @@
"console_disable",
"service_control",
"configure_acquisition",
"reset_wizard"
"reset_wizard",
"install_hub_item",
"remove_hub_item"
]
},
"uci": [ "crowdsec", "crowdsec-dashboard" ]