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 088e1762..55ef1361 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 @@ -197,6 +197,26 @@ var callRepairLapi = rpc.declare({ expect: { } }); +// Console Methods +var callConsoleStatus = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'console_status', + expect: { } +}); + +var callConsoleEnroll = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'console_enroll', + params: ['key', 'name'], + expect: { } +}); + +var callConsoleDisable = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'console_disable', + expect: { } +}); + function formatDuration(seconds) { if (!seconds) return 'N/A'; if (seconds < 60) return seconds + 's'; @@ -318,6 +338,11 @@ return baseclass.extend({ getWizardState: callWizardState, repairLapi: callRepairLapi, + // Console Methods + getConsoleStatus: callConsoleStatus, + consoleEnroll: callConsoleEnroll, + consoleDisable: callConsoleDisable, + formatDuration: formatDuration, formatDate: formatDate, formatRelativeTime: formatRelativeTime, diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js index 31ac5554..4a472cbc 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js @@ -10,7 +10,7 @@ return view.extend({ wizardData: { currentStep: 1, - totalSteps: 6, + totalSteps: 7, // Step 1 data crowdsecRunning: false, @@ -18,23 +18,29 @@ return view.extend({ lapiRepairing: false, lapiRepairAttempted: false, - // Step 2 data + // Step 2 data (Console Enrollment) + consoleEnrolled: false, + enrolling: false, + enrollmentKey: '', + machineName: '', + + // Step 3 data (Hub Update) hubUpdating: false, hubUpdated: false, - // Step 3 data + // Step 4 data (Collections) collections: [], installing: false, installed: false, installStatus: '', installedCount: 0, - // Step 4 data + // Step 5 data (Bouncer) configuring: false, bouncerConfigured: false, apiKey: '', - // Step 5 data + // Step 6 data (Services) starting: false, enabling: false, enabled: false, @@ -42,7 +48,7 @@ return view.extend({ nftablesActive: false, lapiConnected: false, - // Step 6 data + // Step 7 data (Complete) blockedIPs: 0, activeDecisions: 0 }, @@ -50,14 +56,17 @@ return view.extend({ load: function() { return Promise.all([ API.getStatus(), - API.checkWizardNeeded() + API.checkWizardNeeded(), + API.getConsoleStatus() ]).then(L.bind(function(results) { var status = results[0]; var wizardNeeded = results[1]; + var consoleStatus = results[2]; // Update wizard data from status this.wizardData.crowdsecRunning = status && status.crowdsec === 'running'; this.wizardData.lapiAvailable = status && status.lapi_status === 'available'; + this.wizardData.consoleEnrolled = consoleStatus && consoleStatus.enrolled; // Auto-repair LAPI if CrowdSec is running but LAPI is not available if (this.wizardData.crowdsecRunning && !this.wizardData.lapiAvailable && !this.wizardData.lapiRepairAttempted) { @@ -78,6 +87,7 @@ return view.extend({ return { status: newStatus, wizardNeeded: wizardNeeded, + consoleStatus: consoleStatus, repaired: true }; }, this)); @@ -86,6 +96,7 @@ return view.extend({ return { status: status, wizardNeeded: wizardNeeded, + consoleStatus: consoleStatus, repairFailed: true }; } @@ -94,12 +105,16 @@ return view.extend({ return { status: status, - wizardNeeded: wizardNeeded + wizardNeeded: wizardNeeded, + consoleStatus: consoleStatus }; }, this)); }, render: function(data) { + // Initialize theme + Theme.init(); + // Load wizard CSS var head = document.head || document.getElementsByTagName('head')[0]; var cssLink = E('link', { @@ -109,6 +124,14 @@ return view.extend({ }); head.appendChild(cssLink); + // Load SecuBox theme CSS + var themeLink = E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('secubox-theme/secubox-theme.css') + }); + head.appendChild(themeLink); + var container = E('div', { 'class': 'wizard-container' }); // Create stepper @@ -123,11 +146,12 @@ return view.extend({ createStepper: function() { var steps = [ { number: 1, title: _('Welcome') }, - { number: 2, title: _('Update Hub') }, - { number: 3, title: _('Install Packs') }, - { number: 4, title: _('Configure Bouncer') }, - { number: 5, title: _('Enable Services') }, - { number: 6, title: _('Complete') } + { number: 2, title: _('Console') }, + { number: 3, title: _('Update Hub') }, + { number: 4, title: _('Install Packs') }, + { number: 5, title: _('Configure Bouncer') }, + { number: 6, title: _('Enable Services') }, + { number: 7, title: _('Complete') } ]; var stepper = E('div', { 'class': 'wizard-stepper' }); @@ -158,15 +182,17 @@ return view.extend({ case 1: return this.renderStep1(data); case 2: - return this.renderStep2(data); + return this.renderStep2Console(data); case 3: - return this.renderStep3(data); + return this.renderStep3Hub(data); case 4: - return this.renderStep4(data); + return this.renderStep4Collections(data); case 5: - return this.renderStep5(data); + return this.renderStep5Bouncer(data); case 6: - return this.renderStep6(data); + return this.renderStep6Services(data); + case 7: + return this.renderStep7Complete(data); default: return E('div', {}, _('Invalid step')); } @@ -225,18 +251,18 @@ return view.extend({ E('button', { 'class': 'cbi-button cbi-button-action', 'click': L.bind(this.handleManualRepair, this) - }, _('🔧 Retry Repair')) + }, _('Retry Repair')) ]) : E([]), // Info box E('div', { 'class': 'info-box' }, [ E('h4', {}, _('What will be configured:')), E('ul', {}, [ + E('li', {}, _('Enroll in CrowdSec Console for community blocklists')), E('li', {}, _('Update CrowdSec hub with latest collections')), E('li', {}, _('Install essential security scenarios')), E('li', {}, _('Register and configure firewall bouncer')), - E('li', {}, _('Enable automatic IP blocking via nftables')), - E('li', {}, _('Start all services')) + E('li', {}, _('Enable automatic IP blocking via nftables')) ]) ]), @@ -257,12 +283,107 @@ return view.extend({ ev.stopPropagation(); this.goToStep(2); }, this) - }, _('Next →')) + }, _('Next')) ]) ]); }, - renderStep2: function(data) { + renderStep2Console: function(data) { + var consoleStatus = data && data.consoleStatus ? data.consoleStatus : {}; + var enrolled = consoleStatus.enrolled || this.wizardData.consoleEnrolled; + + return E('div', { 'class': 'wizard-step' }, [ + E('h2', {}, _('CrowdSec Console Enrollment')), + E('p', {}, _('Connect to CrowdSec Console to receive community blocklists and monitor your security.')), + + // Benefits + E('div', { 'class': 'info-box', 'style': 'margin-bottom: 24px;' }, [ + E('h4', {}, _('Benefits of enrolling:')), + E('ul', {}, [ + E('li', {}, _('Access to community-curated blocklists')), + E('li', {}, _('Real-time threat intelligence sharing')), + E('li', {}, _('Centralized monitoring dashboard')), + E('li', {}, _('Free tier available')) + ]) + ]), + + // Enrollment status + enrolled ? + E('div', { 'class': 'success-message', 'style': 'margin: 24px 0; padding: 16px; background: rgba(34, 197, 94, 0.15); border-radius: 8px;' }, [ + E('span', { 'class': 'check-icon success', 'style': 'font-size: 24px; margin-right: 12px;' }, '✓'), + E('div', { 'style': 'display: inline-block;' }, [ + E('strong', { 'style': 'display: block; color: #16a34a;' }, _('Already enrolled!')), + E('span', { 'style': 'color: #15803d; font-size: 0.9em;' }, + _('Your instance is connected to CrowdSec Console')) + ]) + ]) : + E('div', { 'class': 'enrollment-form', 'style': 'margin: 24px 0;' }, [ + // Enrollment key input + E('div', { 'class': 'form-group', 'style': 'margin-bottom: 16px;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 8px; font-weight: 500;' }, + _('Enrollment Key')), + E('input', { + 'type': 'text', + 'id': 'console-enrollment-key', + 'class': 'cbi-input-text', + 'placeholder': _('Paste your enrollment key from console.crowdsec.net'), + 'style': 'width: 100%; padding: 12px; font-family: monospace;' + }), + E('p', { 'style': 'margin-top: 8px; color: var(--cyber-text-secondary, #666); font-size: 0.9em;' }, [ + _('Get your key from '), + E('a', { + 'href': 'https://app.crowdsec.net/security-engines', + 'target': '_blank', + 'style': 'color: var(--cyber-accent-primary, #667eea);' + }, 'app.crowdsec.net') + ]) + ]), + + // Machine name (optional) + E('div', { 'class': 'form-group', 'style': 'margin-bottom: 16px;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 8px; font-weight: 500;' }, + _('Machine Name (optional)')), + E('input', { + 'type': 'text', + 'id': 'console-machine-name', + 'class': 'cbi-input-text', + 'placeholder': _('e.g., secubox-router'), + 'style': 'width: 100%; padding: 12px;' + }) + ]), + + // Enrolling status + this.wizardData.enrolling ? + E('div', { 'style': 'text-align: center; padding: 16px;' }, [ + E('div', { 'class': 'spinning' }), + E('p', {}, _('Enrolling...')) + ]) : E([]), + + // Enroll button + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'width: 100%; padding: 12px; font-size: 1em;', + 'disabled': this.wizardData.enrolling ? true : null, + 'click': L.bind(this.handleConsoleEnroll, this) + }, _('Enroll in CrowdSec Console')) + ]), + + // Navigation + E('div', { 'class': 'wizard-nav' }, [ + E('button', { + 'class': 'cbi-button', + 'click': L.bind(this.goToStep, this, 1) + }, _('Back')), + E('button', { + 'class': 'cbi-button', + 'click': L.bind(this.goToStep, this, 3), + 'disabled': this.wizardData.enrolling ? true : null + }, enrolled ? _('Next') : _('Skip')) + ]) + ]); + }, + + renderStep3Hub: function(data) { return E('div', { 'class': 'wizard-step' }, [ E('h2', {}, _('Update CrowdSec Hub')), E('p', {}, _('Fetching the latest security collections from CrowdSec hub...')), @@ -282,13 +403,13 @@ return view.extend({ E('div', { 'class': 'wizard-nav' }, [ E('button', { 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 1) - }, _('← Back')), + 'click': L.bind(this.goToStep, this, 2) + }, _('Back')), this.wizardData.hubUpdated ? E('button', { 'class': 'cbi-button cbi-button-positive', - 'click': L.bind(this.goToStep, this, 3) - }, _('Next →')) : + 'click': L.bind(this.goToStep, this, 4) + }, _('Next')) : E('button', { 'class': 'cbi-button cbi-button-action', 'click': L.bind(this.handleUpdateHub, this) @@ -297,7 +418,7 @@ return view.extend({ ]); }, - renderStep3: function(data) { + renderStep4Collections: function(data) { var recommendedCollections = [ { name: 'crowdsecurity/linux', description: 'Base Linux scenarios', preselected: true }, { name: 'crowdsecurity/ssh-bf', description: 'SSH brute force protection', preselected: true }, @@ -356,24 +477,24 @@ return view.extend({ E('div', { 'class': 'wizard-nav' }, [ E('button', { 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 2), + 'click': L.bind(this.goToStep, this, 3), 'disabled': this.wizardData.installing ? true : null - }, _('← Back')), + }, _('Back')), E('button', { 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 4), + 'click': L.bind(this.goToStep, this, 5), 'disabled': this.wizardData.installing ? true : null }, _('Skip')), E('button', { 'class': 'cbi-button cbi-button-positive', 'click': L.bind(this.handleInstallCollections, this), 'disabled': (this.wizardData.installing || this.wizardData.installed) ? true : null - }, this.wizardData.installed ? _('Installed ✓') : _('Install Selected')) + }, this.wizardData.installed ? _('Installed') : _('Install Selected')) ]) ]); }, - renderStep4: function(data) { + renderStep5Bouncer: function(data) { return E('div', { 'class': 'wizard-step' }, [ E('h2', {}, _('Configure Firewall Bouncer')), E('p', {}, _('The firewall bouncer will automatically block malicious IPs using nftables.')), @@ -465,24 +586,24 @@ return view.extend({ E('div', { 'class': 'wizard-nav' }, [ E('button', { 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 3), + 'click': L.bind(this.goToStep, this, 4), 'disabled': this.wizardData.configuring ? true : null - }, _('← Back')), + }, _('Back')), this.wizardData.bouncerConfigured ? E('button', { 'class': 'cbi-button cbi-button-positive', - 'click': L.bind(this.goToStep, this, 5) - }, _('Next →')) : + 'click': L.bind(this.goToStep, this, 6) + }, _('Next')) : E('button', { 'class': 'cbi-button cbi-button-action', - 'click': L.bind(function(ev) { console.log('[Wizard] Configure Bouncer button clicked!'); ev.preventDefault(); ev.stopPropagation(); this.handleConfigureBouncer(); }, this), + 'click': L.bind(this.handleConfigureBouncer, this), 'disabled': this.wizardData.configuring ? true : null }, _('Configure Bouncer')) ]) ]); }, - renderStep5: function(data) { + renderStep6Services: function(data) { return E('div', { 'class': 'wizard-step' }, [ E('h2', {}, _('Enable & Start Services')), E('p', {}, _('Starting the firewall bouncer service and verifying operation...')), @@ -492,22 +613,22 @@ return view.extend({ E('div', { 'class': 'status-item' }, [ E('span', { 'class': 'status-label' }, _('Enable at boot:')), E('span', { 'class': 'status-value' + (this.wizardData.enabled ? ' success' : '') }, - this.wizardData.enabled ? _('Enabled ✓') : this.wizardData.enabling ? _('Enabling...') : _('Not enabled')) + this.wizardData.enabled ? _('Enabled') : this.wizardData.enabling ? _('Enabling...') : _('Not enabled')) ]), E('div', { 'class': 'status-item' }, [ E('span', { 'class': 'status-label' }, _('Service status:')), E('span', { 'class': 'status-value' + (this.wizardData.running ? ' success' : '') }, - this.wizardData.running ? _('Running ✓') : this.wizardData.starting ? _('Starting...') : _('Stopped')) + this.wizardData.running ? _('Running') : this.wizardData.starting ? _('Starting...') : _('Stopped')) ]), E('div', { 'class': 'status-item' }, [ E('span', { 'class': 'status-label' }, _('nftables rules:')), E('span', { 'class': 'status-value' + (this.wizardData.nftablesActive ? ' success' : '') }, - this.wizardData.nftablesActive ? _('Loaded ✓') : _('Not loaded')) + this.wizardData.nftablesActive ? _('Loaded') : _('Not loaded')) ]), E('div', { 'class': 'status-item' }, [ E('span', { 'class': 'status-label' }, _('LAPI connection:')), E('span', { 'class': 'status-value' + (this.wizardData.lapiConnected ? ' success' : '') }, - this.wizardData.lapiConnected ? _('Connected ✓') : _('Not connected')) + this.wizardData.lapiConnected ? _('Connected') : _('Not connected')) ]) ]), @@ -515,24 +636,24 @@ return view.extend({ E('div', { 'class': 'wizard-nav' }, [ E('button', { 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 4), + 'click': L.bind(this.goToStep, this, 5), 'disabled': this.wizardData.starting ? true : null - }, _('← Back')), + }, _('Back')), (this.wizardData.enabled && this.wizardData.running && this.wizardData.nftablesActive && this.wizardData.lapiConnected) ? E('button', { 'class': 'cbi-button cbi-button-positive', - 'click': L.bind(this.goToStep, this, 6) - }, _('Next →')) : + 'click': L.bind(this.goToStep, this, 7) + }, _('Next')) : E('button', { 'class': 'cbi-button cbi-button-action', - 'click': L.bind(function(ev) { console.log('[Wizard] Start Services button clicked!'); ev.preventDefault(); ev.stopPropagation(); this.handleStartServices(); }, this), + 'click': L.bind(this.handleStartServices, this), 'disabled': this.wizardData.starting ? true : null }, _('Start Services')) ]) ]); }, - renderStep6: function(data) { + renderStep7Complete: function(data) { return E('div', { 'class': 'wizard-step wizard-complete' }, [ E('div', { 'class': 'success-hero' }, [ E('div', { 'class': 'success-icon' }, '🎉'), @@ -550,6 +671,14 @@ return view.extend({ E('div', { 'class': 'summary-desc' }, _('Running and monitoring')) ]) ]), + this.wizardData.consoleEnrolled ? + E('div', { 'class': 'summary-item' }, [ + E('span', { 'class': 'check-icon success' }, '✓'), + E('div', {}, [ + E('strong', {}, _('Console Enrolled')), + E('div', { 'class': 'summary-desc' }, _('Receiving community blocklists')) + ]) + ]) : E([]), E('div', { 'class': 'summary-item' }, [ E('span', { 'class': 'check-icon success' }, '✓'), E('div', {}, [ @@ -607,7 +736,12 @@ return view.extend({ E('li', {}, _('View real-time decisions in the Decisions tab')), E('li', {}, _('Monitor alerts in the Alerts tab')), E('li', {}, _('Check blocked IPs in the Bouncers tab')), - E('li', {}, _('Review metrics in the Metrics tab')) + this.wizardData.consoleEnrolled ? + E('li', {}, [ + _('Monitor from '), + E('a', { 'href': 'https://app.crowdsec.net', 'target': '_blank' }, 'CrowdSec Console') + ]) : + E('li', {}, _('Consider enrolling in CrowdSec Console for blocklists')) ]) ]), @@ -619,7 +753,7 @@ return view.extend({ 'click': function() { window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview'); } - }, _('Go to Dashboard →')) + }, _('Go to Dashboard')) ]) ]); }, @@ -644,6 +778,42 @@ return view.extend({ } }, + handleConsoleEnroll: function() { + var keyInput = document.getElementById('console-enrollment-key'); + var nameInput = document.getElementById('console-machine-name'); + var key = keyInput ? keyInput.value.trim() : ''; + var name = nameInput ? nameInput.value.trim() : ''; + + if (!key) { + ui.addNotification(null, E('p', _('Please enter an enrollment key')), 'warning'); + return; + } + + console.log('[Wizard] Enrolling with key:', key.substring(0, 10) + '...'); + this.wizardData.enrolling = true; + this.refreshView(); + + return API.consoleEnroll(key, name).then(L.bind(function(result) { + console.log('[Wizard] Enrollment result:', result); + this.wizardData.enrolling = false; + + if (result && result.success) { + this.wizardData.consoleEnrolled = true; + ui.addNotification(null, E('p', _('Successfully enrolled in CrowdSec Console!')), 'success'); + // Auto-advance after 2 seconds + setTimeout(L.bind(function() { this.goToStep(3); }, this), 2000); + } else { + ui.addNotification(null, E('p', _('Enrollment failed: ') + (result.error || result.output || 'Unknown error')), 'error'); + } + this.refreshView(); + }, this)).catch(L.bind(function(err) { + console.error('[Wizard] Enrollment error:', err); + this.wizardData.enrolling = false; + ui.addNotification(null, E('p', _('Enrollment failed: ') + err.message), 'error'); + this.refreshView(); + }, this)); + }, + handleUpdateHub: function() { console.log('[Wizard] handleUpdateHub called'); this.wizardData.hubUpdating = true; @@ -686,7 +856,7 @@ return view.extend({ console.log('[Wizard] Selected collections:', selected); if (selected.length === 0) { - this.goToStep(4); + this.goToStep(5); return; } @@ -711,7 +881,7 @@ return view.extend({ this.refreshView(); // Auto-advance after 2 seconds - setTimeout(L.bind(function() { this.goToStep(4); }, this), 2000); + setTimeout(L.bind(function() { this.goToStep(5); }, this), 2000); }, this)).catch(L.bind(function(err) { this.wizardData.installing = false; ui.addNotification(null, E('p', _('Installation failed: %s').format(err.message)), 'error'); @@ -760,8 +930,8 @@ return view.extend({ this.refreshView(); // Auto-advance after 2 seconds - console.log('[Wizard] Auto-advancing to Step 5 in 2 seconds...'); - setTimeout(L.bind(function() { this.goToStep(5); }, this), 2000); + console.log('[Wizard] Auto-advancing to Step 6 in 2 seconds...'); + setTimeout(L.bind(function() { this.goToStep(6); }, this), 2000); }, this)).catch(L.bind(function(err) { console.error('[Wizard] Configuration error:', err); this.wizardData.configuring = false; @@ -809,7 +979,8 @@ return view.extend({ return API.getBouncers(); }, this)).then(L.bind(function(bouncers) { console.log('[Wizard] Bouncers list:', bouncers); - var bouncer = (bouncers || []).find(function(b) { + var bouncerList = bouncers && bouncers.bouncers ? bouncers.bouncers : bouncers; + var bouncer = (bouncerList || []).find(function(b) { return b.name === 'crowdsec-firewall-bouncer'; }); @@ -823,13 +994,13 @@ return view.extend({ this.refreshView(); // Success if enabled, running, and nftables active - // LAPI connection may take a few seconds to establish, so it's optional - if (this.wizardData.enabled && this.wizardData.running && + // LAPI connection may take a few seconds to establish, so it's optional + if (this.wizardData.enabled && this.wizardData.running && this.wizardData.nftablesActive) { - console.log('[Wizard] All critical services started! Auto-advancing to Step 6...'); + console.log('[Wizard] All critical services started! Auto-advancing to Step 7...'); ui.addNotification(null, E('p', _('Services started successfully!')), 'info'); // Auto-advance after 2 seconds - setTimeout(L.bind(function() { this.goToStep(6); }, this), 2000); + setTimeout(L.bind(function() { this.goToStep(7); }, this), 2000); } else { console.log('[Wizard] Service startup incomplete'); ui.addNotification(null, E('p', _('Service startup incomplete. Check status and retry.')), 'warning'); 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 b90fe2f4..2e90cf08 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 @@ -931,10 +931,128 @@ repair_lapi() { json_dump } +# Console enrollment status +get_console_status() { + json_init + check_cscli + + local enrolled=0 + local name="" + local company="" + + # Check if console is enrolled by checking if there's a console config + if [ -f "/etc/crowdsec/console.yaml" ]; then + # Check if the console is actually configured (has credentials) + if grep -q "share_manual_decisions\|share_tainted\|share_context" /etc/crowdsec/console.yaml 2>/dev/null; then + enrolled=1 + fi + fi + + # Try to get console status from cscli + local console_status + console_status=$($CSCLI console status 2>/dev/null) + if [ -n "$console_status" ]; then + # Check if enrolled by looking for "Enrolled" or similar in output + if echo "$console_status" | grep -qi "enrolled\|connected\|active"; then + enrolled=1 + fi + # Extract name if present + name=$(echo "$console_status" | grep -i "name\|machine" | head -1 | awk -F: '{print $2}' | xargs 2>/dev/null) + fi + + json_add_boolean "enrolled" "$enrolled" + json_add_string "name" "$name" + + # Get share settings if enrolled + if [ "$enrolled" = "1" ] && [ -f "/etc/crowdsec/console.yaml" ]; then + local share_manual=$(grep "share_manual_decisions:" /etc/crowdsec/console.yaml 2>/dev/null | awk '{print $2}') + local share_tainted=$(grep "share_tainted:" /etc/crowdsec/console.yaml 2>/dev/null | awk '{print $2}') + local share_context=$(grep "share_context:" /etc/crowdsec/console.yaml 2>/dev/null | awk '{print $2}') + + json_add_boolean "share_manual_decisions" "$([ \"$share_manual\" = \"true\" ] && echo 1 || echo 0)" + json_add_boolean "share_tainted" "$([ \"$share_tainted\" = \"true\" ] && echo 1 || echo 0)" + json_add_boolean "share_context" "$([ \"$share_context\" = \"true\" ] && echo 1 || echo 0)" + fi + + json_dump +} + +# Console enroll +console_enroll() { + local key="$1" + local name="$2" + + json_init + check_cscli + + if [ -z "$key" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Enrollment key is required" + json_dump + return + fi + + secubox_log "Enrolling CrowdSec Console with key..." + + # Build enroll command + local enroll_cmd="$CSCLI console enroll $key" + if [ -n "$name" ]; then + enroll_cmd="$enroll_cmd --name \"$name\"" + fi + + # Run enrollment + local output + output=$(eval "$enroll_cmd" 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Successfully enrolled in CrowdSec Console" + json_add_string "output" "$output" + secubox_log "Console enrollment successful" + + # Enable sharing options by default + $CSCLI console enable share_manual_decisions >/dev/null 2>&1 + $CSCLI console enable share_tainted >/dev/null 2>&1 + $CSCLI console enable share_context >/dev/null 2>&1 + else + json_add_boolean "success" 0 + json_add_string "error" "Enrollment failed" + json_add_string "output" "$output" + secubox_log "Console enrollment failed: $output" + fi + + json_dump +} + +# Console disable (unenroll) +console_disable() { + json_init + check_cscli + + secubox_log "Disabling CrowdSec Console enrollment..." + + # Disable all sharing + $CSCLI console disable share_manual_decisions >/dev/null 2>&1 + $CSCLI console disable share_tainted >/dev/null 2>&1 + $CSCLI console disable share_context >/dev/null 2>&1 + + # Remove console config + if [ -f "/etc/crowdsec/console.yaml" ]; then + rm -f /etc/crowdsec/console.yaml + fi + + json_add_boolean "success" 1 + json_add_string "message" "Console enrollment disabled" + secubox_log "Console enrollment disabled" + + 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":{}}' + 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":{},"console_status":{},"console_enroll":{"key":"string","name":"string"},"console_disable":{}}' ;; call) case "$2" in @@ -1048,6 +1166,18 @@ case "$1" in repair_lapi) repair_lapi ;; + console_status) + get_console_status + ;; + console_enroll) + read -r input + key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null) + name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + console_enroll "$key" "$name" + ;; + console_disable) + console_disable + ;; *) 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 4aa17d04..feb6c038 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 @@ -20,7 +20,8 @@ "firewall_bouncer_config", "nftables_stats", "check_wizard_needed", - "wizard_state" + "wizard_state", + "console_status" ], "file": [ "read", "stat" ] }, @@ -40,7 +41,9 @@ "delete_bouncer", "control_firewall_bouncer", "update_firewall_bouncer_config", - "repair_lapi" + "repair_lapi", + "console_enroll", + "console_disable" ] }, "uci": [ "crowdsec-dashboard" ]