feat: Add CrowdSec Console enrollment to setup wizard (v0.6.0-r28)

- Add console_status, console_enroll, console_disable RPCD methods
- Insert Console enrollment as Step 2 in the 7-step wizard
- Add API declarations and ACL permissions for console operations
- Enable share_manual_decisions, share_tainted, share_context by default on enrollment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-08 14:40:24 +01:00
parent 4078b4d7a4
commit e5b6d1dd87
4 changed files with 392 additions and 63 deletions

View File

@ -197,6 +197,26 @@ var callRepairLapi = rpc.declare({
expect: { } 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) { function formatDuration(seconds) {
if (!seconds) return 'N/A'; if (!seconds) return 'N/A';
if (seconds < 60) return seconds + 's'; if (seconds < 60) return seconds + 's';
@ -318,6 +338,11 @@ return baseclass.extend({
getWizardState: callWizardState, getWizardState: callWizardState,
repairLapi: callRepairLapi, repairLapi: callRepairLapi,
// Console Methods
getConsoleStatus: callConsoleStatus,
consoleEnroll: callConsoleEnroll,
consoleDisable: callConsoleDisable,
formatDuration: formatDuration, formatDuration: formatDuration,
formatDate: formatDate, formatDate: formatDate,
formatRelativeTime: formatRelativeTime, formatRelativeTime: formatRelativeTime,

View File

@ -10,7 +10,7 @@
return view.extend({ return view.extend({
wizardData: { wizardData: {
currentStep: 1, currentStep: 1,
totalSteps: 6, totalSteps: 7,
// Step 1 data // Step 1 data
crowdsecRunning: false, crowdsecRunning: false,
@ -18,23 +18,29 @@ return view.extend({
lapiRepairing: false, lapiRepairing: false,
lapiRepairAttempted: false, lapiRepairAttempted: false,
// Step 2 data // Step 2 data (Console Enrollment)
consoleEnrolled: false,
enrolling: false,
enrollmentKey: '',
machineName: '',
// Step 3 data (Hub Update)
hubUpdating: false, hubUpdating: false,
hubUpdated: false, hubUpdated: false,
// Step 3 data // Step 4 data (Collections)
collections: [], collections: [],
installing: false, installing: false,
installed: false, installed: false,
installStatus: '', installStatus: '',
installedCount: 0, installedCount: 0,
// Step 4 data // Step 5 data (Bouncer)
configuring: false, configuring: false,
bouncerConfigured: false, bouncerConfigured: false,
apiKey: '', apiKey: '',
// Step 5 data // Step 6 data (Services)
starting: false, starting: false,
enabling: false, enabling: false,
enabled: false, enabled: false,
@ -42,7 +48,7 @@ return view.extend({
nftablesActive: false, nftablesActive: false,
lapiConnected: false, lapiConnected: false,
// Step 6 data // Step 7 data (Complete)
blockedIPs: 0, blockedIPs: 0,
activeDecisions: 0 activeDecisions: 0
}, },
@ -50,14 +56,17 @@ return view.extend({
load: function() { load: function() {
return Promise.all([ return Promise.all([
API.getStatus(), API.getStatus(),
API.checkWizardNeeded() API.checkWizardNeeded(),
API.getConsoleStatus()
]).then(L.bind(function(results) { ]).then(L.bind(function(results) {
var status = results[0]; var status = results[0];
var wizardNeeded = results[1]; var wizardNeeded = results[1];
var consoleStatus = results[2];
// Update wizard data from status // Update wizard data from status
this.wizardData.crowdsecRunning = status && status.crowdsec === 'running'; this.wizardData.crowdsecRunning = status && status.crowdsec === 'running';
this.wizardData.lapiAvailable = status && status.lapi_status === 'available'; 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 // Auto-repair LAPI if CrowdSec is running but LAPI is not available
if (this.wizardData.crowdsecRunning && !this.wizardData.lapiAvailable && !this.wizardData.lapiRepairAttempted) { if (this.wizardData.crowdsecRunning && !this.wizardData.lapiAvailable && !this.wizardData.lapiRepairAttempted) {
@ -78,6 +87,7 @@ return view.extend({
return { return {
status: newStatus, status: newStatus,
wizardNeeded: wizardNeeded, wizardNeeded: wizardNeeded,
consoleStatus: consoleStatus,
repaired: true repaired: true
}; };
}, this)); }, this));
@ -86,6 +96,7 @@ return view.extend({
return { return {
status: status, status: status,
wizardNeeded: wizardNeeded, wizardNeeded: wizardNeeded,
consoleStatus: consoleStatus,
repairFailed: true repairFailed: true
}; };
} }
@ -94,12 +105,16 @@ return view.extend({
return { return {
status: status, status: status,
wizardNeeded: wizardNeeded wizardNeeded: wizardNeeded,
consoleStatus: consoleStatus
}; };
}, this)); }, this));
}, },
render: function(data) { render: function(data) {
// Initialize theme
Theme.init();
// Load wizard CSS // Load wizard CSS
var head = document.head || document.getElementsByTagName('head')[0]; var head = document.head || document.getElementsByTagName('head')[0];
var cssLink = E('link', { var cssLink = E('link', {
@ -109,6 +124,14 @@ return view.extend({
}); });
head.appendChild(cssLink); 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' }); var container = E('div', { 'class': 'wizard-container' });
// Create stepper // Create stepper
@ -123,11 +146,12 @@ return view.extend({
createStepper: function() { createStepper: function() {
var steps = [ var steps = [
{ number: 1, title: _('Welcome') }, { number: 1, title: _('Welcome') },
{ number: 2, title: _('Update Hub') }, { number: 2, title: _('Console') },
{ number: 3, title: _('Install Packs') }, { number: 3, title: _('Update Hub') },
{ number: 4, title: _('Configure Bouncer') }, { number: 4, title: _('Install Packs') },
{ number: 5, title: _('Enable Services') }, { number: 5, title: _('Configure Bouncer') },
{ number: 6, title: _('Complete') } { number: 6, title: _('Enable Services') },
{ number: 7, title: _('Complete') }
]; ];
var stepper = E('div', { 'class': 'wizard-stepper' }); var stepper = E('div', { 'class': 'wizard-stepper' });
@ -158,15 +182,17 @@ return view.extend({
case 1: case 1:
return this.renderStep1(data); return this.renderStep1(data);
case 2: case 2:
return this.renderStep2(data); return this.renderStep2Console(data);
case 3: case 3:
return this.renderStep3(data); return this.renderStep3Hub(data);
case 4: case 4:
return this.renderStep4(data); return this.renderStep4Collections(data);
case 5: case 5:
return this.renderStep5(data); return this.renderStep5Bouncer(data);
case 6: case 6:
return this.renderStep6(data); return this.renderStep6Services(data);
case 7:
return this.renderStep7Complete(data);
default: default:
return E('div', {}, _('Invalid step')); return E('div', {}, _('Invalid step'));
} }
@ -225,18 +251,18 @@ return view.extend({
E('button', { E('button', {
'class': 'cbi-button cbi-button-action', 'class': 'cbi-button cbi-button-action',
'click': L.bind(this.handleManualRepair, this) 'click': L.bind(this.handleManualRepair, this)
}, _('🔧 Retry Repair')) }, _('Retry Repair'))
]) : E([]), ]) : E([]),
// Info box // Info box
E('div', { 'class': 'info-box' }, [ E('div', { 'class': 'info-box' }, [
E('h4', {}, _('What will be configured:')), E('h4', {}, _('What will be configured:')),
E('ul', {}, [ E('ul', {}, [
E('li', {}, _('Enroll in CrowdSec Console for community blocklists')),
E('li', {}, _('Update CrowdSec hub with latest collections')), E('li', {}, _('Update CrowdSec hub with latest collections')),
E('li', {}, _('Install essential security scenarios')), E('li', {}, _('Install essential security scenarios')),
E('li', {}, _('Register and configure firewall bouncer')), E('li', {}, _('Register and configure firewall bouncer')),
E('li', {}, _('Enable automatic IP blocking via nftables')), E('li', {}, _('Enable automatic IP blocking via nftables'))
E('li', {}, _('Start all services'))
]) ])
]), ]),
@ -257,12 +283,107 @@ return view.extend({
ev.stopPropagation(); ev.stopPropagation();
this.goToStep(2); this.goToStep(2);
}, this) }, 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' }, [ return E('div', { 'class': 'wizard-step' }, [
E('h2', {}, _('Update CrowdSec Hub')), E('h2', {}, _('Update CrowdSec Hub')),
E('p', {}, _('Fetching the latest security collections from 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('div', { 'class': 'wizard-nav' }, [
E('button', { E('button', {
'class': 'cbi-button', 'class': 'cbi-button',
'click': L.bind(this.goToStep, this, 1) 'click': L.bind(this.goToStep, this, 2)
}, _('Back')), }, _('Back')),
this.wizardData.hubUpdated ? this.wizardData.hubUpdated ?
E('button', { E('button', {
'class': 'cbi-button cbi-button-positive', 'class': 'cbi-button cbi-button-positive',
'click': L.bind(this.goToStep, this, 3) 'click': L.bind(this.goToStep, this, 4)
}, _('Next')) : }, _('Next')) :
E('button', { E('button', {
'class': 'cbi-button cbi-button-action', 'class': 'cbi-button cbi-button-action',
'click': L.bind(this.handleUpdateHub, this) 'click': L.bind(this.handleUpdateHub, this)
@ -297,7 +418,7 @@ return view.extend({
]); ]);
}, },
renderStep3: function(data) { renderStep4Collections: function(data) {
var recommendedCollections = [ var recommendedCollections = [
{ name: 'crowdsecurity/linux', description: 'Base Linux scenarios', preselected: true }, { name: 'crowdsecurity/linux', description: 'Base Linux scenarios', preselected: true },
{ name: 'crowdsecurity/ssh-bf', description: 'SSH brute force protection', 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('div', { 'class': 'wizard-nav' }, [
E('button', { E('button', {
'class': 'cbi-button', 'class': 'cbi-button',
'click': L.bind(this.goToStep, this, 2), 'click': L.bind(this.goToStep, this, 3),
'disabled': this.wizardData.installing ? true : null 'disabled': this.wizardData.installing ? true : null
}, _('Back')), }, _('Back')),
E('button', { E('button', {
'class': 'cbi-button', 'class': 'cbi-button',
'click': L.bind(this.goToStep, this, 4), 'click': L.bind(this.goToStep, this, 5),
'disabled': this.wizardData.installing ? true : null 'disabled': this.wizardData.installing ? true : null
}, _('Skip')), }, _('Skip')),
E('button', { E('button', {
'class': 'cbi-button cbi-button-positive', 'class': 'cbi-button cbi-button-positive',
'click': L.bind(this.handleInstallCollections, this), 'click': L.bind(this.handleInstallCollections, this),
'disabled': (this.wizardData.installing || this.wizardData.installed) ? true : null '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' }, [ return E('div', { 'class': 'wizard-step' }, [
E('h2', {}, _('Configure Firewall Bouncer')), E('h2', {}, _('Configure Firewall Bouncer')),
E('p', {}, _('The firewall bouncer will automatically block malicious IPs using nftables.')), 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('div', { 'class': 'wizard-nav' }, [
E('button', { E('button', {
'class': 'cbi-button', 'class': 'cbi-button',
'click': L.bind(this.goToStep, this, 3), 'click': L.bind(this.goToStep, this, 4),
'disabled': this.wizardData.configuring ? true : null 'disabled': this.wizardData.configuring ? true : null
}, _('Back')), }, _('Back')),
this.wizardData.bouncerConfigured ? this.wizardData.bouncerConfigured ?
E('button', { E('button', {
'class': 'cbi-button cbi-button-positive', 'class': 'cbi-button cbi-button-positive',
'click': L.bind(this.goToStep, this, 5) 'click': L.bind(this.goToStep, this, 6)
}, _('Next')) : }, _('Next')) :
E('button', { E('button', {
'class': 'cbi-button cbi-button-action', '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 'disabled': this.wizardData.configuring ? true : null
}, _('Configure Bouncer')) }, _('Configure Bouncer'))
]) ])
]); ]);
}, },
renderStep5: function(data) { renderStep6Services: function(data) {
return E('div', { 'class': 'wizard-step' }, [ return E('div', { 'class': 'wizard-step' }, [
E('h2', {}, _('Enable & Start Services')), E('h2', {}, _('Enable & Start Services')),
E('p', {}, _('Starting the firewall bouncer service and verifying operation...')), E('p', {}, _('Starting the firewall bouncer service and verifying operation...')),
@ -492,22 +613,22 @@ return view.extend({
E('div', { 'class': 'status-item' }, [ E('div', { 'class': 'status-item' }, [
E('span', { 'class': 'status-label' }, _('Enable at boot:')), E('span', { 'class': 'status-label' }, _('Enable at boot:')),
E('span', { 'class': 'status-value' + (this.wizardData.enabled ? ' success' : '') }, 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('div', { 'class': 'status-item' }, [
E('span', { 'class': 'status-label' }, _('Service status:')), E('span', { 'class': 'status-label' }, _('Service status:')),
E('span', { 'class': 'status-value' + (this.wizardData.running ? ' success' : '') }, 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('div', { 'class': 'status-item' }, [
E('span', { 'class': 'status-label' }, _('nftables rules:')), E('span', { 'class': 'status-label' }, _('nftables rules:')),
E('span', { 'class': 'status-value' + (this.wizardData.nftablesActive ? ' success' : '') }, 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('div', { 'class': 'status-item' }, [
E('span', { 'class': 'status-label' }, _('LAPI connection:')), E('span', { 'class': 'status-label' }, _('LAPI connection:')),
E('span', { 'class': 'status-value' + (this.wizardData.lapiConnected ? ' success' : '') }, 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('div', { 'class': 'wizard-nav' }, [
E('button', { E('button', {
'class': 'cbi-button', 'class': 'cbi-button',
'click': L.bind(this.goToStep, this, 4), 'click': L.bind(this.goToStep, this, 5),
'disabled': this.wizardData.starting ? true : null 'disabled': this.wizardData.starting ? true : null
}, _('Back')), }, _('Back')),
(this.wizardData.enabled && this.wizardData.running && this.wizardData.nftablesActive && this.wizardData.lapiConnected) ? (this.wizardData.enabled && this.wizardData.running && this.wizardData.nftablesActive && this.wizardData.lapiConnected) ?
E('button', { E('button', {
'class': 'cbi-button cbi-button-positive', 'class': 'cbi-button cbi-button-positive',
'click': L.bind(this.goToStep, this, 6) 'click': L.bind(this.goToStep, this, 7)
}, _('Next')) : }, _('Next')) :
E('button', { E('button', {
'class': 'cbi-button cbi-button-action', '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 'disabled': this.wizardData.starting ? true : null
}, _('Start Services')) }, _('Start Services'))
]) ])
]); ]);
}, },
renderStep6: function(data) { renderStep7Complete: function(data) {
return E('div', { 'class': 'wizard-step wizard-complete' }, [ return E('div', { 'class': 'wizard-step wizard-complete' }, [
E('div', { 'class': 'success-hero' }, [ E('div', { 'class': 'success-hero' }, [
E('div', { 'class': 'success-icon' }, '🎉'), E('div', { 'class': 'success-icon' }, '🎉'),
@ -550,6 +671,14 @@ return view.extend({
E('div', { 'class': 'summary-desc' }, _('Running and monitoring')) 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('div', { 'class': 'summary-item' }, [
E('span', { 'class': 'check-icon success' }, '✓'), E('span', { 'class': 'check-icon success' }, '✓'),
E('div', {}, [ E('div', {}, [
@ -607,7 +736,12 @@ return view.extend({
E('li', {}, _('View real-time decisions in the Decisions tab')), E('li', {}, _('View real-time decisions in the Decisions tab')),
E('li', {}, _('Monitor alerts in the Alerts tab')), E('li', {}, _('Monitor alerts in the Alerts tab')),
E('li', {}, _('Check blocked IPs in the Bouncers 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() { 'click': function() {
window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview'); 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() { handleUpdateHub: function() {
console.log('[Wizard] handleUpdateHub called'); console.log('[Wizard] handleUpdateHub called');
this.wizardData.hubUpdating = true; this.wizardData.hubUpdating = true;
@ -686,7 +856,7 @@ return view.extend({
console.log('[Wizard] Selected collections:', selected); console.log('[Wizard] Selected collections:', selected);
if (selected.length === 0) { if (selected.length === 0) {
this.goToStep(4); this.goToStep(5);
return; return;
} }
@ -711,7 +881,7 @@ return view.extend({
this.refreshView(); this.refreshView();
// Auto-advance after 2 seconds // 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)).catch(L.bind(function(err) {
this.wizardData.installing = false; this.wizardData.installing = false;
ui.addNotification(null, E('p', _('Installation failed: %s').format(err.message)), 'error'); ui.addNotification(null, E('p', _('Installation failed: %s').format(err.message)), 'error');
@ -760,8 +930,8 @@ return view.extend({
this.refreshView(); this.refreshView();
// Auto-advance after 2 seconds // Auto-advance after 2 seconds
console.log('[Wizard] Auto-advancing to Step 5 in 2 seconds...'); console.log('[Wizard] Auto-advancing to Step 6 in 2 seconds...');
setTimeout(L.bind(function() { this.goToStep(5); }, this), 2000); setTimeout(L.bind(function() { this.goToStep(6); }, this), 2000);
}, this)).catch(L.bind(function(err) { }, this)).catch(L.bind(function(err) {
console.error('[Wizard] Configuration error:', err); console.error('[Wizard] Configuration error:', err);
this.wizardData.configuring = false; this.wizardData.configuring = false;
@ -809,7 +979,8 @@ return view.extend({
return API.getBouncers(); return API.getBouncers();
}, this)).then(L.bind(function(bouncers) { }, this)).then(L.bind(function(bouncers) {
console.log('[Wizard] Bouncers list:', 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'; return b.name === 'crowdsec-firewall-bouncer';
}); });
@ -823,13 +994,13 @@ return view.extend({
this.refreshView(); this.refreshView();
// Success if enabled, running, and nftables active // Success if enabled, running, and nftables active
// LAPI connection may take a few seconds to establish, so it's optional // LAPI connection may take a few seconds to establish, so it's optional
if (this.wizardData.enabled && this.wizardData.running && if (this.wizardData.enabled && this.wizardData.running &&
this.wizardData.nftablesActive) { 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'); ui.addNotification(null, E('p', _('Services started successfully!')), 'info');
// Auto-advance after 2 seconds // Auto-advance after 2 seconds
setTimeout(L.bind(function() { this.goToStep(6); }, this), 2000); setTimeout(L.bind(function() { this.goToStep(7); }, this), 2000);
} else { } else {
console.log('[Wizard] Service startup incomplete'); console.log('[Wizard] Service startup incomplete');
ui.addNotification(null, E('p', _('Service startup incomplete. Check status and retry.')), 'warning'); ui.addNotification(null, E('p', _('Service startup incomplete. Check status and retry.')), 'warning');

View File

@ -931,10 +931,128 @@ repair_lapi() {
json_dump 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 # Main dispatcher
case "$1" in case "$1" in
list) 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) call)
case "$2" in case "$2" in
@ -1048,6 +1166,18 @@ case "$1" in
repair_lapi) repair_lapi)
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"}' echo '{"error": "Unknown method"}'
;; ;;

View File

@ -20,7 +20,8 @@
"firewall_bouncer_config", "firewall_bouncer_config",
"nftables_stats", "nftables_stats",
"check_wizard_needed", "check_wizard_needed",
"wizard_state" "wizard_state",
"console_status"
], ],
"file": [ "read", "stat" ] "file": [ "read", "stat" ]
}, },
@ -40,7 +41,9 @@
"delete_bouncer", "delete_bouncer",
"control_firewall_bouncer", "control_firewall_bouncer",
"update_firewall_bouncer_config", "update_firewall_bouncer_config",
"repair_lapi" "repair_lapi",
"console_enroll",
"console_disable"
] ]
}, },
"uci": [ "crowdsec-dashboard" ] "uci": [ "crowdsec-dashboard" ]