diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js index 2ee47d21..d76236f5 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js @@ -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, diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js index e8f25457..fded8497 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js @@ -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;' }, [ 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 fb5bd66d..d161ca98 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 @@ -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' }, [ diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js index 8def8c8b..1e8d75c7 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js @@ -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;' }, [ 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 2638339a..9a63dfdc 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 @@ -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"}' ;; diff --git a/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json b/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json index 7b7427ef..4e390e27 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json +++ b/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json @@ -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" ]