diff --git a/package/secubox/luci-app-crowdsec-dashboard/Makefile b/package/secubox/luci-app-crowdsec-dashboard/Makefile index 62c7cef9..d2f7d037 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/Makefile +++ b/package/secubox/luci-app-crowdsec-dashboard/Makefile @@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-crowdsec-dashboard -PKG_VERSION:=0.5.0 -PKG_RELEASE:=2 +PKG_VERSION:=0.6.0 +PKG_RELEASE:=1 PKG_ARCH:=all PKG_LICENSE:=Apache-2.0 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 fe02153e..f0207a9a 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 @@ -9,7 +9,7 @@ * CrowdSec Core: 1.7.4+ */ -// Version: 0.5.0 +// Version: 0.6.0 var callStatus = rpc.declare({ object: 'luci.crowdsec-dashboard', @@ -178,6 +178,19 @@ var callNftablesStats = rpc.declare({ expect: { } }); +// Wizard Methods +var callCheckWizardNeeded = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'check_wizard_needed', + expect: { } +}); + +var callWizardState = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'wizard_state', + expect: { } +}); + function formatDuration(seconds) { if (!seconds) return 'N/A'; if (seconds < 60) return seconds + 's'; @@ -228,6 +241,10 @@ return baseclass.extend({ updateFirewallBouncerConfig: callUpdateFirewallBouncerConfig, getNftablesStats: callNftablesStats, + // Wizard Methods + checkWizardNeeded: callCheckWizardNeeded, + getWizardState: callWizardState, + formatDuration: formatDuration, formatDate: formatDate, diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/wizard.css b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/wizard.css new file mode 100644 index 00000000..91e76f28 --- /dev/null +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/wizard.css @@ -0,0 +1,715 @@ +/* CrowdSec Setup Wizard Styles */ + +/* Wizard Container */ +.wizard-container { + max-width: 900px; + margin: 0 auto; + padding: 20px; +} + +/* Stepper */ +.wizard-stepper { + display: flex; + justify-content: space-between; + margin-bottom: 32px; + padding: 0 20px; + position: relative; +} + +.wizard-step-indicator { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + position: relative; + z-index: 1; +} + +.wizard-step-indicator:not(:last-child)::after { + content: ''; + position: absolute; + top: 18px; + left: 50%; + right: -50%; + height: 2px; + background: rgba(148, 163, 184, 0.25); + z-index: -1; +} + +.wizard-step-indicator.complete::after { + background: #22c55e; +} + +.wizard-step-index { + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 14px; + background: rgba(15, 23, 42, 0.85); + border: 2px solid rgba(148, 163, 184, 0.25); + margin-bottom: 8px; + transition: all 0.3s ease; + color: #94a3b8; +} + +.wizard-step-indicator.active .wizard-step-index { + border-color: #3b82f6; + background: #3b82f6; + color: white; + box-shadow: 0 0 12px rgba(59, 130, 246, 0.5); + animation: pulse-blue 2s infinite; +} + +.wizard-step-indicator.complete .wizard-step-index { + border-color: #22c55e; + background: #22c55e; + color: white; +} + +@keyframes pulse-blue { + 0%, 100% { + box-shadow: 0 0 12px rgba(59, 130, 246, 0.5); + } + 50% { + box-shadow: 0 0 20px rgba(59, 130, 246, 0.8); + } +} + +.wizard-step-title { + font-size: 12px; + color: #94a3b8; + text-align: center; + transition: all 0.3s ease; +} + +.wizard-step-indicator.active .wizard-step-title { + color: #3b82f6; + font-weight: 600; +} + +.wizard-step-indicator.complete .wizard-step-title { + color: #22c55e; +} + +/* Step Content */ +.wizard-step { + background: rgba(15, 23, 42, 0.85); + border: 1px solid rgba(148, 163, 184, 0.25); + border-radius: 12px; + padding: 32px; + margin-bottom: 24px; + min-height: 400px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.wizard-step h2 { + margin: 0 0 16px 0; + color: #f1f5f9; + font-size: 24px; + font-weight: 600; +} + +.wizard-step p { + color: #cbd5e1; + margin-bottom: 24px; + line-height: 1.6; +} + +.wizard-step.wizard-complete { + background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.95) 100%); +} + +/* Status Checks */ +.status-checks { + display: flex; + flex-direction: column; + gap: 12px; + margin: 24px 0; +} + +.check-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: rgba(15, 23, 42, 0.5); + border-radius: 8px; + border: 1px solid rgba(148, 163, 184, 0.15); + transition: all 0.2s ease; +} + +.check-item:hover { + border-color: rgba(148, 163, 184, 0.3); +} + +.check-icon { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 16px; + flex-shrink: 0; +} + +.check-icon.success { + background: #22c55e; + color: white; +} + +.check-icon.error { + background: #ef4444; + color: white; +} + +.status-area { + margin: 24px 0; + padding: 20px; + text-align: center; + background: rgba(15, 23, 42, 0.5); + border-radius: 8px; + border: 1px solid rgba(148, 163, 184, 0.15); +} + +/* Collections List */ +.collections-list { + display: flex; + flex-direction: column; + gap: 12px; + margin: 24px 0; +} + +.collection-item { + padding: 12px 16px; + background: rgba(15, 23, 42, 0.5); + border-radius: 8px; + border: 1px solid rgba(148, 163, 184, 0.15); + transition: all 0.2s ease; +} + +.collection-item:hover { + border-color: rgba(59, 130, 246, 0.3); + background: rgba(15, 23, 42, 0.7); +} + +.collection-item label { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + user-select: none; +} + +.collection-item input[type="checkbox"] { + cursor: pointer; + width: 18px; + height: 18px; +} + +.collection-info { + flex: 1; +} + +.collection-info strong { + color: #f1f5f9; + font-weight: 500; +} + +.collection-desc { + font-size: 12px; + color: #94a3b8; + margin-top: 4px; +} + +/* Configuration Section */ +.config-section { + display: flex; + flex-direction: column; + gap: 16px; + margin: 24px 0; + padding: 20px; + background: rgba(15, 23, 42, 0.5); + border-radius: 8px; + border: 1px solid rgba(148, 163, 184, 0.15); +} + +.config-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.config-group label { + color: #cbd5e1; + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.config-group input[type="checkbox"] { + cursor: pointer; + width: 18px; + height: 18px; +} + +.config-group select { + padding: 8px 12px; + background: rgba(15, 23, 42, 0.8); + border: 1px solid rgba(148, 163, 184, 0.25); + border-radius: 6px; + color: #f1f5f9; + font-size: 14px; +} + +/* API Key Display */ +.api-key-display { + margin: 24px 0; + padding: 16px; + background: rgba(15, 23, 42, 0.5); + border: 1px solid rgba(34, 197, 94, 0.3); + border-radius: 8px; +} + +.api-key-display strong { + display: block; + color: #22c55e; + margin-bottom: 8px; + font-size: 14px; +} + +.api-key-display code { + display: block; + padding: 12px; + background: rgba(0, 0, 0, 0.3); + border-radius: 6px; + color: #94a3b8; + font-family: 'Courier New', monospace; + font-size: 13px; + word-break: break-all; + margin-bottom: 12px; +} + +.api-key-display button { + margin-top: 8px; +} + +/* Service Status */ +.service-status { + display: flex; + flex-direction: column; + gap: 12px; + margin: 24px 0; +} + +.status-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: rgba(15, 23, 42, 0.5); + border-radius: 8px; + border: 1px solid rgba(148, 163, 184, 0.15); +} + +.status-label { + color: #cbd5e1; + font-weight: 500; + font-size: 14px; +} + +.status-value { + color: #94a3b8; + font-size: 14px; +} + +.status-value.success { + color: #22c55e; + font-weight: 600; +} + +/* Success Hero */ +.success-hero { + text-align: center; + margin-bottom: 32px; + padding: 20px; +} + +.success-icon { + font-size: 64px; + margin-bottom: 16px; + animation: celebrate 0.6s ease-in-out; +} + +@keyframes celebrate { + 0% { + transform: scale(0.5) rotate(-10deg); + opacity: 0; + } + 50% { + transform: scale(1.1) rotate(10deg); + } + 100% { + transform: scale(1) rotate(0deg); + opacity: 1; + } +} + +.success-hero h2 { + color: #22c55e; + font-size: 28px; + margin: 0; +} + +.text-center { + text-align: center; +} + +/* Summary Grid */ +.summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; + margin: 24px 0; +} + +.summary-item { + display: flex; + gap: 12px; + padding: 16px; + background: rgba(15, 23, 42, 0.5); + border-radius: 8px; + border: 1px solid rgba(34, 197, 94, 0.3); + transition: all 0.2s ease; +} + +.summary-item:hover { + border-color: rgba(34, 197, 94, 0.5); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2); +} + +.summary-item .check-icon { + flex-shrink: 0; +} + +.summary-item strong { + display: block; + color: #f1f5f9; + margin-bottom: 4px; + font-size: 14px; +} + +.summary-desc { + font-size: 12px; + color: #94a3b8; +} + +/* Stats Box */ +.stats-box { + background: rgba(15, 23, 42, 0.5); + border: 1px solid rgba(148, 163, 184, 0.25); + border-radius: 8px; + padding: 20px; + margin: 24px 0; +} + +.stats-box h4 { + margin: 0 0 16px 0; + color: #f1f5f9; + font-size: 16px; + font-weight: 600; +} + +.stats-row { + display: flex; + gap: 24px; + justify-content: space-around; + flex-wrap: wrap; +} + +.stat { + text-align: center; + min-width: 120px; +} + +.stat-value { + font-size: 32px; + font-weight: 700; + color: #3b82f6; + line-height: 1.2; +} + +.stat-label { + font-size: 12px; + color: #94a3b8; + margin-top: 4px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Info Box */ +.info-box { + background: rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 8px; + padding: 20px; + margin: 24px 0; +} + +.info-box h4 { + margin: 0 0 12px 0; + color: #3b82f6; + font-size: 16px; + font-weight: 600; +} + +.info-box ul { + margin: 0; + padding-left: 20px; +} + +.info-box li { + color: #cbd5e1; + margin-bottom: 8px; + line-height: 1.6; +} + +/* Success Message */ +.success-message { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); + border-radius: 8px; + margin: 24px 0; + color: #22c55e; + font-weight: 500; + animation: slideIn 0.4s ease-out; +} + +@keyframes slideIn { + from { + transform: translateY(-10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Install Progress */ +.install-progress { + text-align: center; + padding: 32px; + margin: 24px 0; +} + +.install-progress .spinning { + margin: 0 auto 16px; +} + +.install-progress p { + color: #3b82f6; + font-weight: 500; + margin-bottom: 12px; +} + +#install-status { + margin-top: 12px; + color: #3b82f6; + font-weight: 500; + font-size: 14px; +} + +/* Wizard Navigation */ +.wizard-nav { + display: flex; + justify-content: space-between; + gap: 12px; + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid rgba(148, 163, 184, 0.25); + flex-wrap: wrap; +} + +.wizard-nav button { + flex: 0 1 auto; + min-width: 120px; +} + +.wizard-nav button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Badges */ +.badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.badge-success { + background: #22c55e; + color: white; +} + +.badge-error { + background: #ef4444; + color: white; +} + +.badge-warning { + background: #f59e0b; + color: white; +} + +.badge-info { + background: #3b82f6; + color: white; +} + +/* Spinning Loader */ +.spinning { + width: 40px; + height: 40px; + border: 4px solid rgba(59, 130, 246, 0.2); + border-top-color: #3b82f6; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .wizard-container { + padding: 10px; + } + + .wizard-stepper { + flex-wrap: wrap; + padding: 0 10px; + } + + .wizard-step-indicator { + min-width: 60px; + } + + .wizard-step-indicator:not(:last-child)::after { + display: none; + } + + .wizard-step { + padding: 20px; + min-height: 300px; + } + + .wizard-step h2 { + font-size: 20px; + } + + .summary-grid { + grid-template-columns: 1fr; + } + + .stats-row { + flex-direction: column; + gap: 16px; + } + + .wizard-nav { + flex-direction: column; + } + + .wizard-nav button { + width: 100%; + min-width: 0; + } + + .config-section { + padding: 16px; + } +} + +@media (max-width: 480px) { + .wizard-step-title { + font-size: 10px; + } + + .wizard-step-index { + width: 32px; + height: 32px; + font-size: 12px; + } + + .success-icon { + font-size: 48px; + } + + .success-hero h2 { + font-size: 22px; + } + + .stat-value { + font-size: 24px; + } +} + +/* Dark Mode Enhancements */ +@media (prefers-color-scheme: dark) { + .wizard-step { + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + } + + .collection-item:hover, + .check-item:hover { + background: rgba(15, 23, 42, 0.8); + } +} + +/* Accessibility */ +.wizard-step:focus-within { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +button:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +/* Print Styles */ +@media print { + .wizard-nav { + display: none; + } + + .wizard-step { + border: 1px solid #000; + box-shadow: none; + } + + .spinning { + display: none; + } +} 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 new file mode 100644 index 00000000..6599e815 --- /dev/null +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js @@ -0,0 +1,686 @@ +'use strict'; +'require view'; +'require ui'; +'require form'; +'require rpc'; +'require uci'; +'require crowdsec-dashboard.api as API'; + +return view.extend({ + wizardData: { + currentStep: 1, + totalSteps: 6, + + // Step 1 data + crowdsecRunning: false, + lapiAvailable: false, + + // Step 2 data + hubUpdating: false, + hubUpdated: false, + + // Step 3 data + collections: [], + installing: false, + installed: false, + installStatus: '', + installedCount: 0, + + // Step 4 data + configuring: false, + bouncerConfigured: false, + apiKey: '', + + // Step 5 data + starting: false, + enabling: false, + enabled: false, + running: false, + nftablesActive: false, + lapiConnected: false, + + // Step 6 data + blockedIPs: 0, + activeDecisions: 0 + }, + + load: function() { + return Promise.all([ + API.getStatus(), + API.checkWizardNeeded() + ]).then(L.bind(function(results) { + var status = results[0]; + var wizardNeeded = results[1]; + + // Update wizard data from status + this.wizardData.crowdsecRunning = status && status.crowdsec === 'running'; + this.wizardData.lapiAvailable = status && status.lapi_status === 'available'; + + return { + status: status, + wizardNeeded: wizardNeeded + }; + }, this)); + }, + + render: function(data) { + var container = E('div', { 'class': 'wizard-container' }); + + // Create stepper + container.appendChild(this.createStepper()); + + // Create step content + container.appendChild(this.renderCurrentStep(data)); + + return container; + }, + + 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') } + ]; + + var stepper = E('div', { 'class': 'wizard-stepper' }); + + steps.forEach(L.bind(function(step) { + var classes = ['wizard-step-indicator']; + if (step.number < this.wizardData.currentStep) { + classes.push('complete'); + } else if (step.number === this.wizardData.currentStep) { + classes.push('active'); + } + + var indicator = E('div', { 'class': classes.join(' ') }, [ + E('div', { 'class': 'wizard-step-index' }, + step.number < this.wizardData.currentStep ? '✓' : step.number.toString() + ), + E('div', { 'class': 'wizard-step-title' }, step.title) + ]); + + stepper.appendChild(indicator); + }, this)); + + return stepper; + }, + + renderCurrentStep: function(data) { + switch (this.wizardData.currentStep) { + case 1: + return this.renderStep1(data); + case 2: + return this.renderStep2(data); + case 3: + return this.renderStep3(data); + case 4: + return this.renderStep4(data); + case 5: + return this.renderStep5(data); + case 6: + return this.renderStep6(data); + default: + return E('div', {}, _('Invalid step')); + } + }, + + renderStep1: function(data) { + var crowdsecRunning = this.wizardData.crowdsecRunning; + var lapiAvailable = this.wizardData.lapiAvailable; + + return E('div', { 'class': 'wizard-step' }, [ + E('h2', {}, _('Welcome to CrowdSec Setup')), + E('p', {}, _('This wizard will help you set up CrowdSec security suite with firewall bouncer protection.')), + + // Status checks + E('div', { 'class': 'status-checks' }, [ + E('div', { 'class': 'check-item' }, [ + E('span', { 'class': 'check-icon' + (crowdsecRunning ? ' success' : ' error') }, + crowdsecRunning ? '✓' : '✗'), + E('span', {}, _('CrowdSec Service')), + E('span', { 'class': 'badge badge-' + (crowdsecRunning ? 'success' : 'error') }, + crowdsecRunning ? _('RUNNING') : _('STOPPED')) + ]), + E('div', { 'class': 'check-item' }, [ + E('span', { 'class': 'check-icon' + (lapiAvailable ? ' success' : ' error') }, + lapiAvailable ? '✓' : '✗'), + E('span', {}, _('Local API (LAPI)')), + E('span', { 'class': 'badge badge-' + (lapiAvailable ? 'success' : 'error') }, + lapiAvailable ? _('AVAILABLE') : _('UNAVAILABLE')) + ]) + ]), + + // Info box + E('div', { 'class': 'info-box' }, [ + E('h4', {}, _('What will be configured:')), + E('ul', {}, [ + 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')) + ]) + ]), + + // Navigation + E('div', { 'class': 'wizard-nav' }, [ + E('button', { + 'class': 'cbi-button', + 'click': L.bind(function() { + window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview'); + }, this) + }, _('Cancel')), + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'disabled': !crowdsecRunning || !lapiAvailable, + 'click': L.bind(this.goToStep, this, 2) + }, _('Next →')) + ]) + ]); + }, + + renderStep2: function(data) { + return E('div', { 'class': 'wizard-step' }, [ + E('h2', {}, _('Update CrowdSec Hub')), + E('p', {}, _('Fetching the latest security collections from CrowdSec hub...')), + + E('div', { 'id': 'hub-update-status', 'class': 'status-area' }, [ + this.wizardData.hubUpdating ? + E('div', { 'class': 'spinning' }, _('Updating hub...')) : + this.wizardData.hubUpdated ? + E('div', { 'class': 'success-message' }, [ + E('span', { 'class': 'check-icon success' }, '✓'), + _('Hub updated successfully!') + ]) : + E('div', {}, _('Ready to update hub')) + ]), + + // Navigation + E('div', { 'class': 'wizard-nav' }, [ + E('button', { + 'class': 'cbi-button', + 'click': L.bind(this.goToStep, this, 1) + }, _('← Back')), + this.wizardData.hubUpdated ? + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': L.bind(this.goToStep, this, 3) + }, _('Next →')) : + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': L.bind(this.handleUpdateHub, this) + }, _('Update Hub')) + ]) + ]); + }, + + renderStep3: function(data) { + var recommendedCollections = [ + { name: 'crowdsecurity/linux', description: 'Base Linux scenarios', preselected: true }, + { name: 'crowdsecurity/ssh-bf', description: 'SSH brute force protection', preselected: true }, + { name: 'crowdsecurity/http-cve', description: 'Web CVE protection', preselected: true }, + { name: 'crowdsecurity/whitelist-good-actors', description: 'Whitelist known good bots', preselected: false } + ]; + + return E('div', { 'class': 'wizard-step' }, [ + E('h2', {}, _('Install Security Collections')), + E('p', {}, _('Select collections to install. Recommended collections are pre-selected.')), + + E('div', { 'class': 'collections-list' }, + recommendedCollections.map(L.bind(function(collection) { + var checkbox = E('input', { + 'type': 'checkbox', + 'id': 'collection-' + collection.name.replace('/', '-'), + 'checked': collection.preselected, + 'data-collection': collection.name + }); + + return E('div', { 'class': 'collection-item' }, [ + E('label', {}, [ + checkbox, + E('div', { 'class': 'collection-info' }, [ + E('strong', {}, collection.name), + E('div', { 'class': 'collection-desc' }, collection.description) + ]) + ]) + ]); + }, this)) + ), + + // Install progress + this.wizardData.installing ? + E('div', { 'class': 'install-progress' }, [ + E('div', { 'class': 'spinning' }), + E('p', {}, _('Installing collections...')), + E('div', { 'id': 'install-status' }, this.wizardData.installStatus || '') + ]) : null, + + // Navigation + E('div', { 'class': 'wizard-nav' }, [ + E('button', { + 'class': 'cbi-button', + 'click': L.bind(this.goToStep, this, 2), + 'disabled': this.wizardData.installing + }, _('← Back')), + E('button', { + 'class': 'cbi-button', + 'click': L.bind(this.goToStep, this, 4), + 'disabled': this.wizardData.installing + }, _('Skip')), + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': L.bind(this.handleInstallCollections, this), + 'disabled': this.wizardData.installing || this.wizardData.installed + }, this.wizardData.installed ? _('Installed ✓') : _('Install Selected')) + ]) + ]); + }, + + renderStep4: 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.')), + + // Configuration options + E('div', { 'class': 'config-section' }, [ + E('div', { 'class': 'config-group' }, [ + E('label', {}, [ + E('input', { + 'type': 'checkbox', + 'id': 'bouncer-ipv4', + 'checked': true + }), + ' ', + _('Enable IPv4 blocking') + ]) + ]), + E('div', { 'class': 'config-group' }, [ + E('label', {}, [ + E('input', { + 'type': 'checkbox', + 'id': 'bouncer-ipv6', + 'checked': true + }), + ' ', + _('Enable IPv6 blocking') + ]) + ]), + E('div', { 'class': 'config-group' }, [ + E('label', {}, _('Update Frequency:')), + E('select', { 'id': 'bouncer-frequency', 'class': 'cbi-input-select' }, [ + E('option', { 'value': '10s', 'selected': true }, _('10 seconds (recommended)')), + E('option', { 'value': '30s' }, _('30 seconds')), + E('option', { 'value': '1m' }, _('1 minute')) + ]) + ]) + ]), + + // Registration status + this.wizardData.bouncerConfigured ? + E('div', { 'class': 'success-message' }, [ + E('span', { 'class': 'check-icon success' }, '✓'), + _('Firewall bouncer configured successfully!') + ]) : + this.wizardData.configuring ? + E('div', { 'class': 'spinning' }, _('Configuring bouncer...')) : + null, + + // API key display (if registered) + this.wizardData.apiKey ? + E('div', { 'class': 'api-key-display' }, [ + E('strong', {}, _('API Key generated:')), + E('code', { 'id': 'bouncer-api-key' }, this.wizardData.apiKey), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { + var key = document.getElementById('bouncer-api-key').textContent; + navigator.clipboard.writeText(key); + ui.addNotification(null, E('p', _('API key copied')), 'info'); + } + }, _('Copy')) + ]) : null, + + // Navigation + E('div', { 'class': 'wizard-nav' }, [ + E('button', { + 'class': 'cbi-button', + 'click': L.bind(this.goToStep, this, 3), + 'disabled': this.wizardData.configuring + }, _('← Back')), + this.wizardData.bouncerConfigured ? + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': L.bind(this.goToStep, this, 5) + }, _('Next →')) : + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': L.bind(this.handleConfigureBouncer, this), + 'disabled': this.wizardData.configuring + }, _('Configure Bouncer')) + ]) + ]); + }, + + renderStep5: function(data) { + return E('div', { 'class': 'wizard-step' }, [ + E('h2', {}, _('Enable & Start Services')), + E('p', {}, _('Starting the firewall bouncer service and verifying operation...')), + + // Service startup progress + E('div', { 'class': 'service-status' }, [ + 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')) + ]), + 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')) + ]), + 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')) + ]), + 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')) + ]) + ]), + + // Navigation + E('div', { 'class': 'wizard-nav' }, [ + E('button', { + 'class': 'cbi-button', + 'click': L.bind(this.goToStep, this, 4), + 'disabled': this.wizardData.starting + }, _('← 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 →')) : + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': L.bind(this.handleStartServices, this), + 'disabled': this.wizardData.starting + }, _('Start Services')) + ]) + ]); + }, + + renderStep6: function(data) { + return E('div', { 'class': 'wizard-step wizard-complete' }, [ + E('div', { 'class': 'success-hero' }, [ + E('div', { 'class': 'success-icon' }, '🎉'), + E('h2', {}, _('Setup Complete!')) + ]), + + E('p', { 'class': 'text-center' }, _('CrowdSec is now protecting your network.')), + + // Summary + E('div', { 'class': 'summary-grid' }, [ + E('div', { 'class': 'summary-item' }, [ + E('span', { 'class': 'check-icon success' }, '✓'), + E('div', {}, [ + E('strong', {}, _('CrowdSec Service')), + E('div', { 'class': 'summary-desc' }, _('Running and monitoring')) + ]) + ]), + E('div', { 'class': 'summary-item' }, [ + E('span', { 'class': 'check-icon success' }, '✓'), + E('div', {}, [ + E('strong', {}, _('Hub Updated')), + E('div', { 'class': 'summary-desc' }, _('Latest collections available')) + ]) + ]), + E('div', { 'class': 'summary-item' }, [ + E('span', { 'class': 'check-icon success' }, '✓'), + E('div', {}, [ + E('strong', {}, _('Collections Installed')), + E('div', { 'class': 'summary-desc' }, + _('%d security packs active').format(this.wizardData.installedCount || 0)) + ]) + ]), + E('div', { 'class': 'summary-item' }, [ + E('span', { 'class': 'check-icon success' }, '✓'), + E('div', {}, [ + E('strong', {}, _('Firewall Bouncer')), + E('div', { 'class': 'summary-desc' }, _('Blocking malicious IPs')) + ]) + ]), + E('div', { 'class': 'summary-item' }, [ + E('span', { 'class': 'check-icon success' }, '✓'), + E('div', {}, [ + E('strong', {}, _('nftables Rules')), + E('div', { 'class': 'summary-desc' }, _('IPv4 and IPv6 protection active')) + ]) + ]) + ]), + + // Current stats + E('div', { 'class': 'stats-box' }, [ + E('h4', {}, _('Current Status')), + E('div', { 'class': 'stats-row' }, [ + E('div', { 'class': 'stat' }, [ + E('div', { 'class': 'stat-value' }, (this.wizardData.blockedIPs || 0).toString()), + E('div', { 'class': 'stat-label' }, _('IPs Blocked')) + ]), + E('div', { 'class': 'stat' }, [ + E('div', { 'class': 'stat-value' }, (this.wizardData.activeDecisions || 0).toString()), + E('div', { 'class': 'stat-label' }, _('Active Decisions')) + ]), + E('div', { 'class': 'stat' }, [ + E('div', { 'class': 'stat-value' }, (this.wizardData.installedCount || 0).toString()), + E('div', { 'class': 'stat-label' }, _('Scenarios')) + ]) + ]) + ]), + + // Next steps + E('div', { 'class': 'info-box' }, [ + E('h4', {}, _('Next Steps')), + E('ul', {}, [ + 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')) + ]) + ]), + + // Navigation + E('div', { 'class': 'wizard-nav' }, [ + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'style': 'font-size: 16px; padding: 12px 24px;', + 'click': function() { + window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview'); + } + }, _('Go to Dashboard →')) + ]) + ]); + }, + + goToStep: function(stepNumber) { + this.wizardData.currentStep = stepNumber; + this.refreshView(); + }, + + refreshView: function() { + var container = document.querySelector('.wizard-container'); + if (container) { + // Update stepper + var stepper = this.createStepper(); + container.replaceChild(stepper, container.firstChild); + + // Update step content + this.load().then(L.bind(function(data) { + var stepContent = this.renderCurrentStep(data); + container.replaceChild(stepContent, container.lastChild); + }, this)); + } + }, + + handleUpdateHub: function() { + this.wizardData.hubUpdating = true; + this.refreshView(); + + return API.updateHub().then(L.bind(function(result) { + this.wizardData.hubUpdating = false; + this.wizardData.hubUpdated = result.success; + + if (result.success) { + ui.addNotification(null, E('p', _('Hub updated successfully')), 'info'); + return API.getCollections(); + } else { + ui.addNotification(null, E('p', result.error || _('Hub update failed')), 'error'); + } + }, this)).then(L.bind(function(collections) { + if (collections) { + this.wizardData.collections = collections; + } + this.refreshView(); + }, this)); + }, + + handleInstallCollections: function() { + var checkboxes = document.querySelectorAll('[data-collection]'); + var selected = Array.from(checkboxes) + .filter(function(cb) { return cb.checked; }) + .map(function(cb) { return cb.dataset.collection; }); + + if (selected.length === 0) { + this.goToStep(4); + return; + } + + this.wizardData.installing = true; + this.wizardData.installStatus = _('Installing 0 of %d collections...').format(selected.length); + this.refreshView(); + + // Install collections sequentially + var installPromises = selected.reduce(L.bind(function(promise, collection, index) { + return promise.then(L.bind(function() { + this.wizardData.installStatus = _('Installing %d of %d: %s').format(index + 1, selected.length, collection); + this.refreshView(); + return API.installCollection(collection); + }, this)); + }, this), Promise.resolve()); + + return installPromises.then(L.bind(function() { + this.wizardData.installing = false; + this.wizardData.installed = true; + this.wizardData.installedCount = selected.length; + ui.addNotification(null, E('p', _('Installed %d collections').format(selected.length)), 'info'); + this.refreshView(); + + // Auto-advance after 2 seconds + setTimeout(L.bind(function() { this.goToStep(4); }, this), 2000); + }, this)).catch(L.bind(function(err) { + this.wizardData.installing = false; + ui.addNotification(null, E('p', _('Installation failed: %s').format(err.message)), 'error'); + this.refreshView(); + }, this)); + }, + + handleConfigureBouncer: function() { + this.wizardData.configuring = true; + this.refreshView(); + + var ipv4 = document.getElementById('bouncer-ipv4').checked; + var ipv6 = document.getElementById('bouncer-ipv6').checked; + var frequency = document.getElementById('bouncer-frequency').value; + + // Step 1: Register bouncer + return API.registerBouncer('crowdsec-firewall-bouncer').then(L.bind(function(result) { + if (!result.success) { + throw new Error(result.error || 'Bouncer registration failed'); + } + + this.wizardData.apiKey = result.api_key; + + // Step 2: Configure UCI settings + var configPromises = [ + API.updateFirewallBouncerConfig('enabled', '1'), + API.updateFirewallBouncerConfig('ipv4', ipv4 ? '1' : '0'), + API.updateFirewallBouncerConfig('ipv6', ipv6 ? '1' : '0'), + API.updateFirewallBouncerConfig('update_frequency', frequency), + API.updateFirewallBouncerConfig('api_key', result.api_key) + ]; + + return Promise.all(configPromises); + }, this)).then(L.bind(function() { + this.wizardData.configuring = false; + this.wizardData.bouncerConfigured = true; + ui.addNotification(null, E('p', _('Bouncer configured successfully')), 'info'); + this.refreshView(); + + // Auto-advance after 2 seconds + setTimeout(L.bind(function() { this.goToStep(5); }, this), 2000); + }, this)).catch(L.bind(function(err) { + this.wizardData.configuring = false; + ui.addNotification(null, E('p', _('Configuration failed: %s').format(err.message)), 'error'); + this.refreshView(); + }, this)); + }, + + handleStartServices: function() { + this.wizardData.starting = true; + this.wizardData.enabling = true; + this.refreshView(); + + // Step 1: Enable service + return API.controlFirewallBouncer('enable').then(L.bind(function(result) { + this.wizardData.enabling = false; + this.wizardData.enabled = result.success; + this.refreshView(); + + // Step 2: Start service + return API.controlFirewallBouncer('start'); + }, this)).then(L.bind(function(result) { + this.wizardData.running = result.success; + this.refreshView(); + + // Step 3: Wait 3 seconds for service to initialize + return new Promise(function(resolve) { setTimeout(resolve, 3000); }); + }, this)).then(L.bind(function() { + // Step 4: Check status + return API.getFirewallBouncerStatus(); + }, this)).then(L.bind(function(status) { + this.wizardData.nftablesActive = status.nftables_ipv4 || status.nftables_ipv6; + this.wizardData.starting = false; + + // Step 5: Verify LAPI connection (check if bouncer pulled decisions) + return API.getBouncers(); + }, this)).then(L.bind(function(bouncers) { + var bouncer = (bouncers || []).find(function(b) { + return b.name === 'crowdsec-firewall-bouncer'; + }); + + this.wizardData.lapiConnected = bouncer && bouncer.last_pull; + this.refreshView(); + + if (this.wizardData.enabled && this.wizardData.running && + this.wizardData.nftablesActive && this.wizardData.lapiConnected) { + ui.addNotification(null, E('p', _('Services started successfully!')), 'info'); + // Auto-advance after 2 seconds + setTimeout(L.bind(function() { this.goToStep(6); }, this), 2000); + } else { + ui.addNotification(null, E('p', _('Service startup incomplete. Check status and retry.')), 'warning'); + } + }, this)).catch(L.bind(function(err) { + this.wizardData.starting = false; + ui.addNotification(null, E('p', _('Service start failed: %s').format(err.message)), 'error'); + this.refreshView(); + }, this)); + }, + + handleSaveAndApply: null, + handleSave: null, + handleReset: null +}); 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 e85121e4..fb250123 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 @@ -773,10 +773,56 @@ get_nftables_stats() { json_dump } +# Check if wizard should be shown (first-time setup detection) +check_wizard_needed() { + json_init + + # Check if bouncer is configured + local bouncer_configured=0 + if uci -q get crowdsec.bouncer.enabled >/dev/null 2>&1; then + bouncer_configured=1 + fi + + # Check if collections are installed + local collections_installed=0 + if [ -x "$CSCLI" ]; then + if $CSCLI collections list 2>/dev/null | grep -q "INSTALLED"; then + collections_installed=1 + fi + fi + + # Show wizard if not configured + local show_wizard=0 + if [ "$bouncer_configured" = "0" ] || [ "$collections_installed" = "0" ]; then + show_wizard=1 + fi + + json_add_boolean "show_wizard" "$show_wizard" + json_add_boolean "bouncer_configured" "$bouncer_configured" + json_add_boolean "collections_installed" "$collections_installed" + + json_dump +} + +# Get wizard initial state +get_wizard_state() { + json_init + + # Get collections count + local collections_count=0 + if [ -x "$CSCLI" ]; then + collections_count=$($CSCLI collections list 2>/dev/null | grep -c "INSTALLED" || echo "0") + fi + + json_add_int "collections_count" "$collections_count" + + 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":{}}' + 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":{}}' ;; call) case "$2" in @@ -881,6 +927,12 @@ case "$1" in nftables_stats) get_nftables_stats ;; + check_wizard_needed) + check_wizard_needed + ;; + wizard_state) + get_wizard_state + ;; *) echo '{"error": "Unknown method"}' ;; diff --git a/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/luci/menu.d/luci-app-crowdsec-dashboard.json b/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/luci/menu.d/luci-app-crowdsec-dashboard.json index 83c1c925..e189f004 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/luci/menu.d/luci-app-crowdsec-dashboard.json +++ b/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/luci/menu.d/luci-app-crowdsec-dashboard.json @@ -9,6 +9,14 @@ "acl": ["luci-app-crowdsec-dashboard"] } }, + "admin/secubox/security/crowdsec/wizard": { + "title": "Setup Wizard", + "order": 5, + "action": { + "type": "view", + "path": "crowdsec-dashboard/wizard" + } + }, "admin/secubox/security/crowdsec/overview": { "title": "Overview", "order": 10, 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 a91f6d40..53246b36 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 @@ -18,7 +18,9 @@ "collections", "firewall_bouncer_status", "firewall_bouncer_config", - "nftables_stats" + "nftables_stats", + "check_wizard_needed", + "wizard_state" ], "file": [ "read", "stat" ] },