secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js
CyberMind-FR da5b88110a feat(crowdsec-dashboard): Add LAPI-only mode, enrollment key storage, improved contrast
- Add Local Protection Mode banner when CAPI unavailable (LAPI still works)
- Save enrollment key to UCI config for future repairs
- Improve text contrast in wizard (better readability)
- Simplify LAPI repair function based on official OpenWrt approach
- Never delete CAPI credentials to avoid rate-limiting
- Add get_settings/save_settings RPC methods
- Bump version to 0.7.0-r27

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 14:30:17 +01:00

863 lines
31 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require ui';
'require form';
'require rpc';
'require uci';
'require crowdsec-dashboard/api as API';
'require crowdsec-dashboard/nav as CsNav';
'require secubox-portal/header as SbHeader';
// Hide LuCI tabs immediately
(function() {
if (typeof document === 'undefined') return;
var style = document.getElementById('cs-hide-tabs') || document.createElement('style');
style.id = 'cs-hide-tabs';
style.textContent = 'ul.tabs, .cbi-tabmenu { display: none !important; }';
if (!style.parentNode) (document.head || document.documentElement).appendChild(style);
})();
return view.extend({
// Health check results
health: {
crowdsecRunning: false,
lapiAvailable: false,
capiConnected: false,
capiEnrolled: false,
bouncerRegistered: false,
bouncerRunning: false,
nftablesActive: false,
hubUpToDate: false,
acquisitionConfigured: false,
collectionsInstalled: 0,
checking: true,
error: null
},
// Configuration options (user selections)
config: {
// What needs to be fixed (auto-detected)
needsLapiRepair: false,
needsCapiRegister: false,
needsHubUpdate: false,
needsAcquisition: false,
needsCollections: false,
needsBouncer: false,
needsServices: false,
// User inputs
enrollmentKey: '',
machineName: '',
// Acquisition options
syslogEnabled: true,
firewallEnabled: true,
sshEnabled: true,
httpEnabled: false,
// Collections to install
selectedCollections: [],
selectedParsers: [],
// Bouncer options
ipv4Enabled: true,
ipv6Enabled: true,
updateFrequency: '10s',
// Apply state
applying: false,
applyStep: '',
applyProgress: 0,
applyComplete: false,
applyErrors: []
},
load: function() {
var self = this;
// Load saved settings first, then run health check
return API.getSettings().then(function(settings) {
if (settings && settings.enrollment_key) {
self.config.enrollmentKey = settings.enrollment_key;
}
if (settings && settings.machine_name) {
self.config.machineName = settings.machine_name;
}
return self.runHealthCheck();
}).catch(function() {
// Settings not available, continue with health check
return self.runHealthCheck();
});
},
runHealthCheck: function() {
var self = this;
this.health.checking = true;
this.health.error = null;
return Promise.all([
API.getStatus(),
API.getConsoleStatus(),
API.getBouncers(),
API.getFirewallBouncerStatus(),
API.getCollections()
]).then(function(results) {
var status = results[0] || {};
var consoleStatus = results[1] || {};
var bouncers = results[2] || {};
var bouncerStatus = results[3] || {};
var collections = results[4] || [];
// Update health status
self.health.crowdsecRunning = status.crowdsec === 'running';
self.health.lapiAvailable = status.lapi_status === 'available';
self.health.capiConnected = status.capi_status === 'connected' || status.capi_status === 'ok';
self.health.capiEnrolled = consoleStatus.enrolled === true;
// Check bouncer registration
var bouncerList = bouncers.bouncers || bouncers || [];
var firewallBouncer = bouncerList.find(function(b) {
return b.name === 'crowdsec-firewall-bouncer' || b.name === 'firewall-bouncer';
});
self.health.bouncerRegistered = !!firewallBouncer;
self.health.bouncerRunning = bouncerStatus.running === true;
self.health.nftablesActive = bouncerStatus.nftables_ipv4 || bouncerStatus.nftables_ipv6;
// Count installed collections
self.health.collectionsInstalled = Array.isArray(collections) ?
collections.filter(function(c) { return c.installed; }).length : 0;
self.health.checking = false;
// Determine what needs to be fixed
self.config.needsLapiRepair = self.health.crowdsecRunning && !self.health.lapiAvailable;
self.config.needsCapiRegister = !self.health.capiConnected && !self.health.capiEnrolled;
self.config.needsHubUpdate = self.health.capiConnected; // Only if CAPI works
self.config.needsCollections = self.health.collectionsInstalled < 3;
self.config.needsBouncer = !self.health.bouncerRegistered;
self.config.needsServices = !self.health.bouncerRunning || !self.health.nftablesActive;
return self.health;
}).catch(function(err) {
console.error('[Wizard] Health check error:', err);
self.health.checking = false;
self.health.error = err.message;
return self.health;
});
},
render: function(data) {
// Initialize theme
Theme.init();
// Load CSS
var head = document.head || document.getElementsByTagName('head')[0];
var cssLink = E('link', {
'rel': 'stylesheet',
'type': 'text/css',
'href': L.resource('crowdsec-dashboard/wizard.css')
});
head.appendChild(cssLink);
var themeLink = E('link', {
'rel': 'stylesheet',
'type': 'text/css',
'href': L.resource('secubox-theme/secubox-theme.css')
});
head.appendChild(themeLink);
// Main wrapper
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
wrapper.appendChild(SbHeader.render());
var container = E('div', { 'class': 'crowdsec-dashboard wizard-container' });
container.appendChild(CsNav.renderTabs('wizard'));
// Render content based on state
if (this.config.applyComplete) {
container.appendChild(this.renderComplete());
} else if (this.config.applying) {
container.appendChild(this.renderApplying());
} else {
container.appendChild(this.renderHealthAndConfig());
}
wrapper.appendChild(container);
return wrapper;
},
renderHealthAndConfig: function() {
var self = this;
return E('div', { 'class': 'wizard-step' }, [
// Header
E('h2', {}, _('CrowdSec Setup Wizard')),
E('p', {}, _('Health check and configuration in one step.')),
// Health Check Section
this.renderHealthCheck(),
// Separator
E('hr', { 'style': 'margin: 24px 0; border-color: rgba(255,255,255,0.1);' }),
// Configuration Section (only if health check passed basic requirements)
this.health.crowdsecRunning ? this.renderConfigOptions() : E([]),
// Apply Button
E('div', { 'class': 'wizard-nav', 'style': 'margin-top: 32px;' }, [
E('button', {
'class': 'cbi-button',
'click': function() {
window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview');
}
}, _('Cancel')),
E('button', {
'class': 'cbi-button cbi-button-positive',
'style': 'font-size: 1.1em; padding: 12px 32px;',
'disabled': !this.health.crowdsecRunning || this.health.checking ? true : null,
'click': L.bind(this.handleApplyAll, this)
}, _('Apply Configuration'))
])
]);
},
renderHealthCheck: function() {
var self = this;
// Determine protection mode
var lapiOnly = this.health.lapiAvailable && !this.health.capiConnected;
var fullProtection = this.health.lapiAvailable && this.health.capiConnected;
var checks = [
{
id: 'crowdsec',
label: _('CrowdSec Service'),
ok: this.health.crowdsecRunning,
status: this.health.crowdsecRunning ? _('Running') : _('Stopped'),
critical: true
},
{
id: 'lapi',
label: _('Local API (LAPI)'),
ok: this.health.lapiAvailable,
status: this.health.lapiAvailable ? _('Available') : _('Unavailable'),
critical: true, // LAPI is critical for protection
action: !this.health.lapiAvailable && this.health.crowdsecRunning ? _('Will repair') : null
},
{
id: 'capi',
label: _('Central API (CAPI)'),
ok: this.health.capiConnected,
status: this.health.capiConnected ? _('Connected') : (this.health.capiEnrolled ? _('Enrolled but not connected') : _('Not registered')),
// CAPI is NOT critical - local protection works without it
critical: false,
warning: !this.health.capiConnected && this.health.lapiAvailable,
action: !this.health.capiConnected ? _('Optional - enroll for community blocklists') : null
},
{
id: 'bouncer',
label: _('Firewall Bouncer'),
ok: this.health.bouncerRegistered && this.health.bouncerRunning,
status: this.health.bouncerRegistered ?
(this.health.bouncerRunning ? _('Running') : _('Registered but stopped')) :
_('Not registered'),
critical: true, // Bouncer is critical for enforcement
action: !this.health.bouncerRegistered ? _('Will register') : (!this.health.bouncerRunning ? _('Will start') : null)
},
{
id: 'nftables',
label: _('nftables Rules'),
ok: this.health.nftablesActive,
status: this.health.nftablesActive ? _('Active') : _('Not loaded'),
critical: true,
action: !this.health.nftablesActive ? _('Will configure') : null
},
{
id: 'collections',
label: _('Security Collections'),
ok: this.health.collectionsInstalled >= 3,
status: _('%d installed').format(this.health.collectionsInstalled),
action: this.health.collectionsInstalled < 3 ? _('Will install') : null
}
];
return E('div', { 'class': 'health-check-section' }, [
E('h3', { 'style': 'margin-bottom: 16px; display: flex; align-items: center;' }, [
E('span', { 'style': 'margin-right: 8px;' }, '🔍'),
_('Health Check'),
this.health.checking ? E('span', { 'class': 'spinning', 'style': 'margin-left: 12px; font-size: 14px;' }, '') : E([])
]),
// Protection Mode Banner
this.health.lapiAvailable ? E('div', {
'style': 'margin-bottom: 16px; padding: 12px 16px; border-radius: 8px; display: flex; align-items: center; ' +
(fullProtection ? 'background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3);' :
'background: rgba(234, 179, 8, 0.1); border: 1px solid rgba(234, 179, 8, 0.3);')
}, [
E('span', { 'style': 'font-size: 24px; margin-right: 12px;' }, fullProtection ? '🛡️' : '⚡'),
E('div', {}, [
E('strong', { 'style': 'color: ' + (fullProtection ? '#22c55e' : '#eab308') + ';' },
fullProtection ? _('Full Protection Mode') : _('Local Protection Mode')),
E('div', { 'style': 'font-size: 0.85em; color: #cbd5e1; margin-top: 2px;' },
fullProtection ?
_('Community blocklists + local detection active') :
_('Local auto-ban active. Enroll CAPI for community blocklists.'))
])
]) : E([]),
E('div', { 'class': 'status-checks', 'style': 'display: grid; gap: 8px;' },
checks.map(function(check) {
// Determine icon and color based on ok/warning/critical status
var iconColor = check.ok ? '#22c55e' : (check.warning ? '#eab308' : (check.critical ? '#ef4444' : '#eab308'));
var icon = check.ok ? '✓' : (check.warning ? '⚠' : (check.critical ? '✗' : '⚠'));
return E('div', {
'class': 'check-item',
'style': 'display: flex; align-items: center; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;'
}, [
E('span', {
'class': 'check-icon',
'style': 'font-size: 20px; margin-right: 12px; color: ' + iconColor + ';'
}, icon),
E('div', { 'style': 'flex: 1;' }, [
E('strong', {}, check.label),
E('div', { 'style': 'font-size: 0.85em; color: ' + (check.ok ? '#22c55e' : '#94a3b8') + ';' }, check.status)
]),
check.action ? E('span', {
'class': 'badge',
'style': 'background: rgba(102, 126, 234, 0.2); color: #818cf8; padding: 4px 8px; border-radius: 4px; font-size: 0.8em;'
}, check.action) : E([])
]);
})
),
// Error message if any
this.health.error ? E('div', {
'style': 'margin-top: 16px; padding: 12px; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 8px; color: #ef4444;'
}, this.health.error) : E([]),
// Refresh button
E('div', { 'style': 'margin-top: 12px;' }, [
E('button', {
'class': 'cbi-button',
'click': L.bind(function() {
this.runHealthCheck().then(L.bind(function() {
this.refreshView();
}, this));
}, this)
}, _('Refresh'))
])
]);
},
renderConfigOptions: function() {
var self = this;
return E('div', { 'class': 'config-section' }, [
E('h3', { 'style': 'margin-bottom: 16px; display: flex; align-items: center;' }, [
E('span', { 'style': 'margin-right: 8px;' }, '⚙️'),
_('Configuration Options')
]),
// CAPI Enrollment (if needed)
!this.health.capiConnected ? E('div', {
'class': 'config-group',
'style': 'margin-bottom: 20px; padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;'
}, [
E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Console Enrollment (Optional)')),
E('p', { 'style': 'margin: 0 0 12px 0; font-size: 0.9em; color: #cbd5e1;' },
_('Enroll to receive community blocklists. Leave empty to skip.')),
E('input', {
'type': 'text',
'id': 'enrollment-key',
'class': 'cbi-input-text',
'placeholder': _('Enrollment key from app.crowdsec.net'),
'style': 'width: 100%; padding: 10px; margin-bottom: 8px;',
'value': this.config.enrollmentKey,
'input': function(ev) { self.config.enrollmentKey = ev.target.value; }
}),
E('input', {
'type': 'text',
'id': 'machine-name',
'class': 'cbi-input-text',
'placeholder': _('Machine name (optional)'),
'style': 'width: 100%; padding: 10px;',
'value': this.config.machineName,
'input': function(ev) { self.config.machineName = ev.target.value; }
}),
E('p', { 'style': 'margin: 8px 0 0 0; font-size: 0.85em; color: #94a3b8;' }, [
_('After enrollment, validate on '),
E('a', { 'href': 'https://app.crowdsec.net', 'target': '_blank', 'style': 'color: #818cf8;' }, 'app.crowdsec.net'),
_('. Service will restart automatically.')
])
]) : E([]),
// Log Acquisition
E('div', {
'class': 'config-group',
'style': 'margin-bottom: 20px; padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;'
}, [
E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Log Sources to Monitor')),
this.renderToggle('syslog', _('System Syslog'), _('/var/log/messages'), this.config.syslogEnabled),
this.renderToggle('firewall', _('Firewall Logs'), _('Port scan detection'), this.config.firewallEnabled),
this.renderToggle('ssh', _('SSH/Dropbear'), _('Brute force detection'), this.config.sshEnabled),
this.renderToggle('http', _('HTTP Server'), _('Web attacks (if running)'), this.config.httpEnabled)
]),
// Collections
E('div', {
'class': 'config-group',
'style': 'margin-bottom: 20px; padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;'
}, [
E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Security Collections to Install')),
this.renderCollectionToggle('crowdsecurity/linux', _('Base Linux scenarios'), true),
this.renderCollectionToggle('crowdsecurity/sshd', _('SSH protection'), true),
this.renderCollectionToggle('crowdsecurity/iptables', _('Firewall log parser'), this.config.firewallEnabled),
this.renderCollectionToggle('crowdsecurity/http-cve', _('Web CVE protection'), this.config.httpEnabled),
E('div', { 'style': 'margin-top: 12px;' }, [
E('strong', { 'style': 'font-size: 0.9em; color: #cbd5e1;' }, _('OpenWrt Parsers:')),
]),
this.renderCollectionToggle('crowdsecurity/syslog-logs', _('Syslog parser'), true, 'parser'),
this.renderCollectionToggle('crowdsecurity/dropbear-logs', _('Dropbear SSH parser'), this.config.sshEnabled, 'parser')
]),
// Bouncer Options
E('div', {
'class': 'config-group',
'style': 'padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;'
}, [
E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Firewall Bouncer Options')),
this.renderToggle('ipv4', _('Block IPv4'), _('Enable IPv4 blocking'), this.config.ipv4Enabled),
this.renderToggle('ipv6', _('Block IPv6'), _('Enable IPv6 blocking'), this.config.ipv6Enabled),
E('div', { 'style': 'margin-top: 12px;' }, [
E('label', { 'style': 'display: block; margin-bottom: 4px; font-size: 0.9em;' }, _('Update Frequency:')),
E('select', {
'id': 'update-frequency',
'class': 'cbi-input-select',
'style': 'width: 100%; padding: 8px;',
'change': function(ev) { self.config.updateFrequency = ev.target.value; }
}, [
E('option', { 'value': '10s', 'selected': this.config.updateFrequency === '10s' }, _('10 seconds (recommended)')),
E('option', { 'value': '30s', 'selected': this.config.updateFrequency === '30s' }, _('30 seconds')),
E('option', { 'value': '1m', 'selected': this.config.updateFrequency === '1m' }, _('1 minute'))
])
])
])
]);
},
renderToggle: function(id, label, description, checked) {
var self = this;
return E('div', {
'class': 'toggle-item',
'data-id': id,
'data-checked': checked ? '1' : '0',
'style': 'display: flex; align-items: center; padding: 8px 0; cursor: pointer;',
'click': function(ev) {
var el = ev.currentTarget;
var current = el.getAttribute('data-checked') === '1';
var newState = !current;
el.setAttribute('data-checked', newState ? '1' : '0');
var checkbox = el.querySelector('.toggle-check');
if (checkbox) {
checkbox.textContent = newState ? '☑' : '☐';
checkbox.style.color = newState ? '#22c55e' : '#64748b';
}
// Update config
var configId = el.getAttribute('data-id');
if (configId === 'syslog') self.config.syslogEnabled = newState;
else if (configId === 'firewall') self.config.firewallEnabled = newState;
else if (configId === 'ssh') self.config.sshEnabled = newState;
else if (configId === 'http') self.config.httpEnabled = newState;
else if (configId === 'ipv4') self.config.ipv4Enabled = newState;
else if (configId === 'ipv6') self.config.ipv6Enabled = newState;
}
}, [
E('span', {
'class': 'toggle-check',
'style': 'font-size: 22px; margin-right: 12px; color: ' + (checked ? '#22c55e' : '#64748b') + ';'
}, checked ? '☑' : '☐'),
E('div', { 'style': 'flex: 1;' }, [
E('strong', { 'style': 'display: block;' }, label),
E('span', { 'style': 'font-size: 0.85em; color: #94a3b8;' }, description)
])
]);
},
renderCollectionToggle: function(name, description, checked, type) {
var self = this;
type = type || 'collection';
return E('div', {
'class': 'collection-toggle',
'data-name': name,
'data-type': type,
'data-checked': checked ? '1' : '0',
'style': 'display: flex; align-items: center; padding: 6px 0; cursor: pointer;',
'click': function(ev) {
var el = ev.currentTarget;
var current = el.getAttribute('data-checked') === '1';
var newState = !current;
el.setAttribute('data-checked', newState ? '1' : '0');
var checkbox = el.querySelector('.toggle-check');
if (checkbox) {
checkbox.textContent = newState ? '' : '';
checkbox.style.color = newState ? '#22c55e' : '#64748b';
}
}
}, [
E('span', {
'class': 'toggle-check',
'style': 'font-size: 20px; margin-right: 10px; color: ' + (checked ? '#22c55e' : '#64748b') + ';'
}, checked ? '' : ''),
E('div', { 'style': 'flex: 1;' }, [
E('code', { 'style': 'font-size: 0.9em;' }, name),
E('span', { 'style': 'margin-left: 8px; font-size: 0.85em; color: #94a3b8;' }, ' ' + description)
])
]);
},
renderApplying: function() {
var progressPercent = Math.round(this.config.applyProgress);
return E('div', { 'class': 'wizard-step', 'style': 'text-align: center; padding: 48px 24px;' }, [
E('div', { 'class': 'spinning', 'style': 'font-size: 48px; margin-bottom: 24px;' }, ''),
E('h2', {}, _('Applying Configuration...')),
E('p', { 'style': 'color: #cbd5e1; margin-bottom: 24px;' }, this.config.applyStep),
// Progress bar
E('div', { 'style': 'max-width: 400px; margin: 0 auto 24px;' }, [
E('div', {
'style': 'height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden;'
}, [
E('div', {
'style': 'height: 100%; width: ' + progressPercent + '%; background: linear-gradient(90deg, #667eea, #764ba2); transition: width 0.3s;'
})
]),
E('div', { 'style': 'margin-top: 8px; font-size: 0.9em; color: #94a3b8;' }, progressPercent + '%')
]),
// Errors if any
this.config.applyErrors.length > 0 ? E('div', {
'style': 'max-width: 500px; margin: 0 auto; text-align: left; padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px;'
}, [
E('strong', { 'style': 'color: #ef4444;' }, _('Warnings:')),
E('ul', { 'style': 'margin: 8px 0 0 0; padding-left: 20px; color: #f87171;' },
this.config.applyErrors.map(function(err) {
return E('li', {}, err);
})
)
]) : E([])
]);
},
renderComplete: function() {
var fullProtection = this.health.lapiAvailable && this.health.capiConnected;
var localOnly = this.health.lapiAvailable && !this.health.capiConnected;
return E('div', { 'class': 'wizard-step wizard-complete', 'style': 'text-align: center;' }, [
E('div', { 'class': 'success-hero', 'style': 'margin-bottom: 32px;' }, [
E('div', { 'style': 'font-size: 64px; margin-bottom: 16px;' }, fullProtection ? '🛡' : ''),
E('h2', {}, _('Setup Complete!'))
]),
// Protection Mode Banner
E('div', {
'style': 'max-width: 400px; margin: 0 auto 24px; padding: 16px; border-radius: 8px; ' +
(fullProtection ? 'background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3);' :
'background: rgba(234, 179, 8, 0.1); border: 1px solid rgba(234, 179, 8, 0.3);')
}, [
E('strong', { 'style': 'color: ' + (fullProtection ? '#22c55e' : '#eab308') + ';' },
fullProtection ? _('Full Protection Mode') : _('Local Protection Mode')),
E('div', { 'style': 'font-size: 0.85em; color: #cbd5e1; margin-top: 4px;' },
fullProtection ?
_('Community blocklists + local detection active') :
_('Local auto-ban protects against attacks detected on your network'))
]),
E('p', { 'style': 'color: #cbd5e1; margin-bottom: 32px;' },
_('CrowdSec is now protecting your network.')),
// Summary of what was done
E('div', { 'style': 'max-width: 400px; margin: 0 auto 32px; text-align: left;' }, [
this.health.lapiAvailable ? this.renderCompletedItem(_('LAPI available (local detection)')) : E([]),
this.health.capiConnected ? this.renderCompletedItem(_('CAPI connected (community blocklists)')) :
this.renderWarningItem(_('CAPI not connected (local protection only)')),
this.health.bouncerRegistered ? this.renderCompletedItem(_('Bouncer registered')) : E([]),
this.health.nftablesActive ? this.renderCompletedItem(_('nftables rules active')) : E([]),
this.health.collectionsInstalled > 0 ?
this.renderCompletedItem(_('%d collections installed').format(this.health.collectionsInstalled)) : E([])
]),
// Errors/warnings if any
this.config.applyErrors.length > 0 ? E('div', {
'style': 'max-width: 500px; margin: 0 auto 24px; padding: 16px; background: rgba(234, 179, 8, 0.1); border: 1px solid rgba(234, 179, 8, 0.3); border-radius: 8px; text-align: left;'
}, [
E('strong', { 'style': 'color: #eab308;' }, _('Some steps had issues:')),
E('ul', { 'style': 'margin: 8px 0 0 0; padding-left: 20px; color: #fbbf24;' },
this.config.applyErrors.map(function(err) {
return E('li', {}, err);
})
)
]) : E([]),
// Navigation
E('div', { 'class': 'wizard-nav' }, [
E('button', {
'class': 'cbi-button cbi-button-positive',
'style': 'font-size: 1.1em; padding: 12px 32px;',
'click': function() {
window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview');
}
}, _('Go to Dashboard'))
])
]);
},
renderCompletedItem: function(text) {
return E('div', { 'style': 'display: flex; align-items: center; padding: 8px 0;' }, [
E('span', { 'style': 'color: #22c55e; margin-right: 12px; font-size: 18px;' }, ''),
E('span', {}, text)
]);
},
renderWarningItem: function(text) {
return E('div', { 'style': 'display: flex; align-items: center; padding: 8px 0;' }, [
E('span', { 'style': 'color: #eab308; margin-right: 12px; font-size: 18px;' }, ''),
E('span', { 'style': 'color: #cbd5e1;' }, text)
]);
},
handleApplyAll: function() {
var self = this;
this.config.applying = true;
this.config.applyProgress = 0;
this.config.applyErrors = [];
this.refreshView();
// Gather selected collections and parsers from DOM
var collectionToggles = document.querySelectorAll('.collection-toggle[data-type="collection"][data-checked="1"]');
var parserToggles = document.querySelectorAll('.collection-toggle[data-type="parser"][data-checked="1"]');
this.config.selectedCollections = Array.from(collectionToggles).map(function(el) {
return el.getAttribute('data-name');
});
this.config.selectedParsers = Array.from(parserToggles).map(function(el) {
return el.getAttribute('data-name');
});
// Get enrollment key
var keyInput = document.getElementById('enrollment-key');
var nameInput = document.getElementById('machine-name');
this.config.enrollmentKey = keyInput ? keyInput.value.trim() : '';
this.config.machineName = nameInput ? nameInput.value.trim() : '';
// Save enrollment key for future repairs (if provided)
if (this.config.enrollmentKey) {
API.saveSettings(this.config.enrollmentKey, this.config.machineName, '1').catch(function(err) {
console.log('[Wizard] Failed to save enrollment key:', err);
});
}
// Define steps
var steps = [];
var stepWeight = 0;
// Step 1: Repair LAPI if needed
if (this.config.needsLapiRepair) {
steps.push({ name: 'lapi', label: _('Repairing LAPI...'), weight: 15 });
stepWeight += 15;
}
// Step 2: Enroll CAPI if key provided
if (this.config.enrollmentKey) {
steps.push({ name: 'capi', label: _('Enrolling in CrowdSec Console...'), weight: 10 });
stepWeight += 10;
}
// Step 3: Update hub if CAPI available
if (this.health.capiConnected || this.config.enrollmentKey) {
steps.push({ name: 'hub', label: _('Updating hub...'), weight: 10 });
stepWeight += 10;
}
// Step 4: Configure acquisition
steps.push({ name: 'acquisition', label: _('Configuring log acquisition...'), weight: 15 });
stepWeight += 15;
// Step 5: Install collections
if (this.config.selectedCollections.length > 0 || this.config.selectedParsers.length > 0) {
steps.push({ name: 'collections', label: _('Installing collections...'), weight: 20 });
stepWeight += 20;
}
// Step 6: Configure bouncer
if (this.config.needsBouncer) {
steps.push({ name: 'bouncer', label: _('Registering bouncer...'), weight: 15 });
stepWeight += 15;
}
// Step 7: Start services
steps.push({ name: 'services', label: _('Starting services...'), weight: 15 });
stepWeight += 15;
// Normalize weights
var totalWeight = stepWeight;
steps.forEach(function(s) {
s.weight = (s.weight / totalWeight) * 100;
});
// Execute steps
var currentProgress = 0;
return steps.reduce(function(promise, step) {
return promise.then(function() {
self.config.applyStep = step.label;
self.refreshView();
return self.executeStep(step.name).then(function(result) {
currentProgress += step.weight;
self.config.applyProgress = currentProgress;
if (result && result.error) {
self.config.applyErrors.push(step.label + ': ' + result.error);
}
}).catch(function(err) {
currentProgress += step.weight;
self.config.applyProgress = currentProgress;
self.config.applyErrors.push(step.label + ': ' + err.message);
});
});
}, Promise.resolve()).then(function() {
// Final health check
return self.runHealthCheck();
}).then(function() {
self.config.applying = false;
self.config.applyComplete = true;
self.refreshView();
});
},
executeStep: function(stepName) {
var self = this;
switch (stepName) {
case 'lapi':
return API.repairLapi();
case 'capi':
return API.consoleEnroll(this.config.enrollmentKey, this.config.machineName);
case 'hub':
return API.updateHub().catch(function(err) {
// Hub update failure is not critical
return { success: false, error: err.message };
});
case 'acquisition':
return API.configureAcquisition(
this.config.syslogEnabled ? '1' : '0',
this.config.firewallEnabled ? '1' : '0',
this.config.sshEnabled ? '1' : '0',
this.config.httpEnabled ? '1' : '0',
'/var/log/messages'
).catch(function(err) {
// XHR abort during restart is OK
if (err.message && err.message.indexOf('abort') !== -1) {
return { success: true };
}
throw err;
});
case 'collections':
// Install collections sequentially
var items = this.config.selectedCollections.map(function(c) {
return { type: 'collection', name: c };
}).concat(this.config.selectedParsers.map(function(p) {
return { type: 'parser', name: p };
}));
return items.reduce(function(promise, item) {
return promise.then(function() {
if (item.type === 'collection') {
return API.installCollection(item.name).catch(function() { return {}; });
} else {
return API.installHubItem('parser', item.name).catch(function() { return {}; });
}
});
}, Promise.resolve()).then(function() {
return { success: true };
});
case 'bouncer':
return API.registerBouncer('crowdsec-firewall-bouncer').then(function(result) {
if (!result.success) {
return result;
}
// Configure bouncer settings
return Promise.all([
API.updateFirewallBouncerConfig('enabled', '1'),
API.updateFirewallBouncerConfig('ipv4', self.config.ipv4Enabled ? '1' : '0'),
API.updateFirewallBouncerConfig('ipv6', self.config.ipv6Enabled ? '1' : '0'),
API.updateFirewallBouncerConfig('update_frequency', self.config.updateFrequency),
API.updateFirewallBouncerConfig('api_key', result.api_key)
]).then(function() {
return { success: true };
});
}).catch(function(err) {
// XHR abort during service restart is OK
if (err.message && err.message.indexOf('abort') !== -1) {
return { success: true };
}
throw err;
});
case 'services':
return API.controlFirewallBouncer('enable').then(function() {
return API.controlFirewallBouncer('start');
}).then(function() {
// Wait for service to start
return new Promise(function(resolve) { setTimeout(resolve, 2000); });
}).then(function() {
return { success: true };
}).catch(function(err) {
// XHR abort during service restart is OK
if (err.message && err.message.indexOf('abort') !== -1) {
return { success: true };
}
throw err;
});
default:
return Promise.resolve({ success: true });
}
},
refreshView: function() {
var container = document.querySelector('.wizard-container');
if (!container) return;
// Remove old content (keep nav tabs)
var tabs = container.querySelector('.cs-nav-tabs');
while (container.lastChild && container.lastChild !== tabs) {
container.removeChild(container.lastChild);
}
// Render new content
if (this.config.applyComplete) {
container.appendChild(this.renderComplete());
} else if (this.config.applying) {
container.appendChild(this.renderApplying());
} else {
container.appendChild(this.renderHealthAndConfig());
}
},
goToStep: function() {},
handleSaveAndApply: null,
handleSave: null,
handleReset: null
});