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:
parent
290eed2ba1
commit
d1bc9a9b63
@ -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,
|
||||
|
||||
@ -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;' }, [
|
||||
|
||||
@ -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' }, [
|
||||
|
||||
@ -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;' }, [
|
||||
|
||||
@ -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"}'
|
||||
;;
|
||||
|
||||
@ -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" ]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user