diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fccb1d33..1db381a2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -160,7 +160,12 @@ "Bash(for f in /home/reepost/CyberMindStudio/_files/secubox-openwrt/luci-app-*/htdocs/luci-static/resources/view/*/*.js)", "Bash(do grep -q \"secubox-theme/theme\" \"$f\")", "Bash(! grep -q \"cyberpunk.css\" \"$f\")", - "Bash(./secubox-tools/quick-deploy.sh:*)" + "Bash(./secubox-tools/quick-deploy.sh:*)", + "WebFetch(domain:raw.githubusercontent.com)", + "WebFetch(domain:docs.crowdsec.net)", + "Bash(timeout 600 make:*)", + "Bash(timeout 300 make:*)", + "Bash(timeout 120 make:*)" ] } } diff --git a/luci-app-crowdsec-dashboard/Makefile b/luci-app-crowdsec-dashboard/Makefile index 5e962cb0..a42b7aae 100644 --- a/luci-app-crowdsec-dashboard/Makefile +++ b/luci-app-crowdsec-dashboard/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-crowdsec-dashboard -PKG_VERSION:=0.4.0 +PKG_VERSION:=0.5.0 PKG_RELEASE:=1 PKG_LICENSE:=Apache-2.0 diff --git a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js index 26331b3c..e18665a6 100644 --- a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js +++ b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js @@ -6,9 +6,10 @@ * CrowdSec Dashboard API * Package: luci-app-crowdsec-dashboard * RPCD object: luci.crowdsec-dashboard + * CrowdSec Core: 1.7.4+ */ -// Version: 0.4.0 +// Version: 0.5.0 var callStatus = rpc.declare({ object: 'luci.crowdsec-dashboard', @@ -84,6 +85,66 @@ var callUnban = rpc.declare({ expect: { success: false } }); +// CrowdSec v1.7.4+ features +var callWAFStatus = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'waf_status', + expect: { } +}); + +var callMetricsConfig = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'metrics_config', + expect: { } +}); + +var callConfigureMetrics = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'configure_metrics', + params: ['enable'], + expect: { success: false } +}); + +var callCollections = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'collections', + expect: { } +}); + +var callInstallCollection = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'install_collection', + params: ['collection'], + expect: { success: false } +}); + +var callRemoveCollection = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'remove_collection', + params: ['collection'], + expect: { success: false } +}); + +var callUpdateHub = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'update_hub', + expect: { success: false } +}); + +var callRegisterBouncer = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'register_bouncer', + params: ['bouncer_name'], + expect: { success: false } +}); + +var callDeleteBouncer = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'delete_bouncer', + params: ['bouncer_name'], + expect: { success: false } +}); + function formatDuration(seconds) { if (!seconds) return 'N/A'; if (seconds < 60) return seconds + 's'; @@ -115,6 +176,18 @@ return baseclass.extend({ collectDebugSnapshot: callCollectDebug, addBan: callBan, removeBan: callUnban, + + // CrowdSec v1.7.4+ features + getWAFStatus: callWAFStatus, + getMetricsConfig: callMetricsConfig, + configureMetrics: callConfigureMetrics, + getCollections: callCollections, + installCollection: callInstallCollection, + removeCollection: callRemoveCollection, + updateHub: callUpdateHub, + registerBouncer: callRegisterBouncer, + deleteBouncer: callDeleteBouncer, + formatDuration: formatDuration, formatDate: formatDate, diff --git a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js index 1663693d..8ce7d0c8 100644 --- a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js +++ b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js @@ -45,10 +45,16 @@ return view.extend({ E('div', { 'class': 'cbi-section' }, [ E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;' }, [ E('h3', { 'style': 'margin: 0;' }, _('Registered Bouncers')), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleRefresh, this) - }, _('Refresh')) + E('div', { 'style': 'display: flex; gap: 0.5em;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': L.bind(this.openRegisterWizard, this) + }, _('➕ Register Bouncer')), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': L.bind(this.handleRefresh, this) + }, _('Refresh')) + ]) ]), E('div', { 'class': 'table-wrapper' }, [ @@ -61,7 +67,8 @@ return view.extend({ E('th', {}, _('Version')), E('th', {}, _('Last Pull')), E('th', {}, _('Status')), - E('th', {}, _('Authentication')) + E('th', {}, _('Authentication')), + E('th', {}, _('Actions')) ]) ]), E('tbody', { 'id': 'bouncers-tbody' }, @@ -125,20 +132,21 @@ return view.extend({ renderBouncerRows: function(bouncers) { if (!bouncers || bouncers.length === 0) { return E('tr', {}, [ - E('td', { 'colspan': 7, 'style': 'text-align: center; padding: 2em; color: #999;' }, - _('No bouncers registered. Use "cscli bouncers add " to register a bouncer.')) + E('td', { 'colspan': 8, 'style': 'text-align: center; padding: 2em; color: #999;' }, + _('No bouncers registered. Click "Register Bouncer" to add one.')) ]); } return bouncers.map(L.bind(function(bouncer) { var lastPull = bouncer.last_pull || bouncer.lastPull || 'Never'; var isRecent = this.isRecentPull(lastPull); + var bouncerName = bouncer.name || 'Unknown'; return E('tr', { 'style': isRecent ? '' : 'opacity: 0.6;' }, [ E('td', {}, [ - E('strong', {}, bouncer.name || 'Unknown') + E('strong', {}, bouncerName) ]), E('td', {}, [ E('code', { 'style': 'font-size: 0.9em;' }, bouncer.ip_address || bouncer.ipAddress || 'N/A') @@ -157,6 +165,12 @@ return view.extend({ 'class': 'badge', 'style': 'background: ' + (bouncer.revoked ? '#dc3545' : '#28a745') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' }, bouncer.revoked ? _('Revoked') : _('Valid')) + ]), + E('td', {}, [ + E('button', { + 'class': 'cbi-button cbi-button-remove', + 'click': L.bind(this.handleDeleteBouncer, this, bouncerName) + }, _('Delete')) ]) ]); }, this)); @@ -216,6 +230,169 @@ return view.extend({ }); }, + openRegisterWizard: function() { + var self = this; + var nameInput; + + ui.showModal(_('Register New Bouncer'), [ + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'cbi-section-descr' }, + _('Register a new bouncer to enforce CrowdSec decisions. The bouncer will receive an API key to connect to the Local API.')), + E('div', { 'class': 'cbi-value', 'style': 'margin-top: 1em;' }, [ + E('label', { 'class': 'cbi-value-title', 'for': 'bouncer-name-input' }, + _('Bouncer Name')), + E('div', { 'class': 'cbi-value-field' }, [ + nameInput = E('input', { + 'type': 'text', + 'id': 'bouncer-name-input', + 'class': 'cbi-input-text', + 'placeholder': _('e.g., firewall-bouncer-1'), + 'style': 'width: 100%;' + }), + E('div', { 'class': 'cbi-value-description' }, + _('Choose a descriptive name (lowercase, hyphens allowed)')) + ]) + ]), + E('div', { 'class': 'cbi-section', 'style': 'background: #e8f4f8; padding: 1em; margin-top: 1em; border-radius: 4px;' }, [ + E('strong', {}, _('What happens next?')), + E('ol', { 'style': 'margin: 0.5em 0 0 1.5em; padding: 0;' }, [ + E('li', {}, _('CrowdSec will generate a unique API key for this bouncer')), + E('li', {}, _('Copy the API key and configure your bouncer with it')), + E('li', {}, _('The bouncer will start pulling and applying decisions')) + ]) + ]) + ]), + E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-positive', + 'click': function() { + var bouncerName = nameInput.value.trim(); + + if (!bouncerName) { + ui.addNotification(null, E('p', _('Please enter a bouncer name')), 'error'); + return; + } + + // Validate name (alphanumeric, hyphens, underscores) + if (!/^[a-z0-9_-]+$/i.test(bouncerName)) { + ui.addNotification(null, E('p', _('Bouncer name can only contain letters, numbers, hyphens and underscores')), 'error'); + return; + } + + ui.hideModal(); + ui.showModal(_('Registering Bouncer...'), [ + E('p', {}, _('Creating bouncer: %s').format(bouncerName)), + E('div', { 'class': 'spinning' }) + ]); + + API.registerBouncer(bouncerName).then(function(result) { + ui.hideModal(); + + if (result && result.success && result.api_key) { + // Show API key in a modal + ui.showModal(_('Bouncer Registered Successfully'), [ + E('div', { 'class': 'cbi-section' }, [ + E('p', { 'style': 'color: #28a745; font-weight: bold;' }, + _('✓ Bouncer "%s" has been registered!').format(bouncerName)), + E('div', { 'class': 'cbi-value', 'style': 'margin-top: 1em;' }, [ + E('label', { 'class': 'cbi-value-title' }, _('API Key')), + E('div', { 'class': 'cbi-value-field' }, [ + E('code', { + 'id': 'api-key-display', + 'style': 'display: block; padding: 0.75em; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; word-break: break-all; font-size: 0.9em;' + }, result.api_key), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'margin-top: 0.5em;', + 'click': function() { + navigator.clipboard.writeText(result.api_key).then(function() { + ui.addNotification(null, E('p', _('API key copied to clipboard')), 'info'); + }).catch(function() { + ui.addNotification(null, E('p', _('Failed to copy. Please select and copy manually.')), 'error'); + }); + } + }, _('📋 Copy to Clipboard')) + ]) + ]), + E('div', { 'class': 'cbi-section', 'style': 'background: #fff3cd; padding: 1em; margin-top: 1em; border-radius: 4px;' }, [ + E('strong', { 'style': 'color: #856404;' }, _('⚠️ Important:')), + E('p', { 'style': 'margin: 0.5em 0 0 0; color: #856404;' }, + _('Save this API key now! It will not be shown again. Use it to configure your bouncer.')) + ]) + ]), + E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ + E('button', { + 'class': 'btn', + 'click': function() { + ui.hideModal(); + self.handleRefresh(); + } + }, _('Close')) + ]) + ]); + } else { + ui.addNotification(null, E('p', result.error || _('Failed to register bouncer')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', err.message || err), 'error'); + }); + } + }, _('Register')) + ]) + ]); + + // Focus the input field + setTimeout(function() { + if (nameInput) nameInput.focus(); + }, 100); + }, + + handleDeleteBouncer: function(bouncerName) { + var self = this; + + ui.showModal(_('Delete Bouncer'), [ + E('p', {}, _('Are you sure you want to delete bouncer "%s"?').format(bouncerName)), + E('p', { 'style': 'color: #dc3545; font-weight: bold;' }, + _('⚠️ This action cannot be undone. The bouncer will no longer be able to connect to the Local API.')), + E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-negative', + 'click': function() { + ui.hideModal(); + ui.showModal(_('Deleting Bouncer...'), [ + E('p', {}, _('Removing bouncer: %s').format(bouncerName)), + E('div', { 'class': 'spinning' }) + ]); + + API.deleteBouncer(bouncerName).then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', _('Bouncer "%s" deleted successfully').format(bouncerName)), 'info'); + self.handleRefresh(); + } else { + ui.addNotification(null, E('p', result.error || _('Failed to delete bouncer')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', err.message || err), 'error'); + }); + } + }, _('Delete')) + ]) + ]); + }, + handleSaveApply: null, handleSave: null, handleReset: null diff --git a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js index 295daeca..abb7da0c 100644 --- a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js +++ b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js @@ -26,20 +26,22 @@ return view.extend({ cssLink.rel = 'stylesheet'; cssLink.href = L.resource('crowdsec-dashboard/dashboard.css'); document.head.appendChild(cssLink); - + this.csApi = new api(); - + return Promise.all([ this.csApi.getMetrics(), this.csApi.getBouncers(), this.csApi.getMachines(), - this.csApi.getHub() + this.csApi.getHub(), + this.csApi.getMetricsConfig() ]).then(function(results) { return { metrics: results[0], bouncers: results[1], machines: results[2], - hub: results[3] + hub: results[3], + metricsConfig: results[4] }; }); }, @@ -240,18 +242,83 @@ return view.extend({ return E('div', { 'class': 'cs-metric-list' }, items); }, + renderMetricsConfig: function(metricsConfig) { + var self = this; + var enabled = metricsConfig && (metricsConfig.metrics_enabled === true || metricsConfig.metrics_enabled === 1); + var prometheusEndpoint = metricsConfig && metricsConfig.prometheus_endpoint || 'http://127.0.0.1:6060/metrics'; + + return E('div', { 'class': 'cs-card', 'style': 'margin-bottom: 24px;' }, [ + E('div', { 'class': 'cs-card-header' }, [ + E('div', { 'class': 'cs-card-title' }, '⚙️ Metrics Export Configuration'), + E('span', { + 'class': 'cs-action', + 'style': enabled ? + 'background: rgba(0,212,170,0.15); color: var(--cs-accent-green); padding: 6px 12px; border-radius: 6px; font-weight: 600; margin-left: auto;' : + 'background: rgba(255,107,107,0.15); color: var(--cs-accent-red); padding: 6px 12px; border-radius: 6px; font-weight: 600; margin-left: auto;' + }, enabled ? _('Enabled') : _('Disabled')) + ]), + E('div', { 'class': 'cs-card-body' }, [ + E('div', { 'class': 'cs-metric-list' }, [ + E('div', { 'class': 'cs-metric-item' }, [ + E('span', { 'class': 'cs-metric-name' }, _('Metrics Export Status')), + E('span', { 'class': 'cs-metric-value' }, enabled ? _('Enabled') : _('Disabled')) + ]), + E('div', { 'class': 'cs-metric-item' }, [ + E('span', { 'class': 'cs-metric-name' }, _('Prometheus Endpoint')), + E('code', { 'class': 'cs-metric-value', 'style': 'font-size: 13px;' }, prometheusEndpoint) + ]) + ]), + E('div', { 'style': 'margin-top: 16px; display: flex; gap: 12px; align-items: center;' }, [ + E('button', { + 'class': 'cbi-button ' + (enabled ? 'cbi-button-negative' : 'cbi-button-positive'), + 'click': function() { + var newState = !enabled; + ui.showModal(_('Updating Metrics Configuration...'), [ + E('p', {}, _('Changing metrics export to: %s').format(newState ? _('Enabled') : _('Disabled'))), + E('div', { 'class': 'spinning' }) + ]); + self.csApi.configureMetrics(newState ? '1' : '0').then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Metrics configuration updated. Restart CrowdSec to apply changes.')), 'info'); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to update configuration')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + } + }, enabled ? _('Disable Metrics Export') : _('Enable Metrics Export')), + E('span', { 'style': 'color: var(--cs-text-muted); font-size: 13px;' }, + _('Note: Changing this setting requires restarting CrowdSec')) + ]), + E('div', { 'class': 'cs-info-box', 'style': 'margin-top: 16px; padding: 12px; background: rgba(0,150,255,0.1); border-left: 4px solid var(--cs-accent-cyan); border-radius: 4px;' }, [ + E('p', { 'style': 'margin: 0 0 8px 0; color: var(--cs-text-primary); font-weight: 600;' }, _('About Metrics Export')), + E('p', { 'style': 'margin: 0; color: var(--cs-text-secondary); font-size: 14px;' }, + _('When enabled, CrowdSec exports Prometheus-compatible metrics that can be scraped by monitoring tools. Access metrics at: ') + + E('code', {}, prometheusEndpoint)) + ]) + ]) + ]); + }, + render: function(data) { var self = this; - + this.metrics = data.metrics || {}; this.bouncers = data.bouncers || []; - this.machines = data.machines || []; + this.machines = data.machines || {}; this.hub = data.hub || {}; - + var metricsConfig = data.metricsConfig || {}; + var view = E('div', { 'class': 'crowdsec-dashboard' }, [ + // Metrics Configuration + this.renderMetricsConfig(metricsConfig), + // Hub Stats E('div', { 'style': 'margin-bottom: 24px' }, [ - E('h3', { 'style': 'color: var(--cs-text-primary); margin-bottom: 16px; font-size: 16px' }, + E('h3', { 'style': 'color: var(--cs-text-primary); margin-bottom: 16px; font-size: 16px' }, '🎯 Hub Components'), this.renderHubStats() ]), diff --git a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js index e5dbc315..3735a1d2 100644 --- a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js +++ b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js @@ -9,7 +9,8 @@ return view.extend({ return Promise.all([ API.getStatus(), API.getMachines(), - API.getHub() + API.getHub(), + API.getCollections() ]); }, @@ -17,6 +18,7 @@ return view.extend({ var status = data[0] || {}; var machines = data[1] || []; var hub = data[2] || {}; + var collections = Array.isArray(data[3]) ? data[3] : []; var view = E('div', { 'class': 'cbi-map' }, [ E('h2', {}, _('CrowdSec Settings')), @@ -106,6 +108,120 @@ return view.extend({ ]) ]), + // Collections Browser + E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [ + E('h3', {}, _('CrowdSec Collections')), + E('p', { 'style': 'color: #666;' }, + _('Collections are bundles of parsers, scenarios, and post-overflow stages for specific services.')), + + E('div', { 'style': 'display: flex; gap: 1em; margin: 1em 0;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { + ui.showModal(_('Updating Hub...'), [ + E('p', {}, _('Fetching latest collections from CrowdSec Hub...')), + E('div', { 'class': 'spinning' }) + ]); + API.updateHub().then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Hub index updated successfully. Please refresh the page.')), 'info'); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to update hub')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + } + }, _('🔄 Update Hub')) + ]), + + E('div', { 'class': 'table-wrapper', 'style': 'margin-top: 1em;' }, [ + E('table', { 'class': 'table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, _('Collection')), + E('th', {}, _('Description')), + E('th', {}, _('Version')), + E('th', {}, _('Status')), + E('th', {}, _('Actions')) + ]) + ]), + E('tbody', {}, + collections.length > 0 ? + collections.map(function(collection) { + var isInstalled = collection.status === 'installed' || collection.installed === 'ok'; + var collectionName = collection.name || 'Unknown'; + return E('tr', {}, [ + E('td', {}, [ + E('strong', {}, collectionName) + ]), + E('td', {}, collection.description || 'N/A'), + E('td', {}, collection.version || collection.local_version || 'N/A'), + E('td', {}, [ + E('span', { + 'class': 'badge', + 'style': 'background: ' + (isInstalled ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' + }, isInstalled ? _('Installed') : _('Available')) + ]), + E('td', {}, [ + isInstalled ? + E('button', { + 'class': 'cbi-button cbi-button-remove', + 'click': function() { + ui.showModal(_('Removing Collection...'), [ + E('p', {}, _('Removing %s...').format(collectionName)), + E('div', { 'class': 'spinning' }) + ]); + API.removeCollection(collectionName).then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Collection removed. Please reload CrowdSec and refresh this page.')), 'info'); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to remove collection')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + } + }, _('Remove')) : + E('button', { + 'class': 'cbi-button cbi-button-add', + 'click': function() { + ui.showModal(_('Installing Collection...'), [ + E('p', {}, _('Installing %s...').format(collectionName)), + E('div', { 'class': 'spinning' }) + ]); + API.installCollection(collectionName).then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Collection installed. Please reload CrowdSec and refresh this page.')), 'info'); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to install collection')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + } + }, _('Install')) + ]) + ]); + }) : + E('tr', {}, [ + E('td', { 'colspan': 5, 'style': 'text-align: center; padding: 2em; color: #999;' }, [ + E('p', {}, _('No collections found. Click "Update Hub" to fetch the collection list.')), + E('p', { 'style': 'margin-top: 0.5em; font-size: 0.9em;' }, + _('Or use: ') + E('code', {}, 'cscli hub update')) + ]) + ]) + ) + ]) + ]) + ]), + // Quick Actions E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [ E('h3', {}, _('Quick Actions')), @@ -148,53 +264,7 @@ return view.extend({ ]) ]); } - }, _('Register Bouncer')), - - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function() { - ui.showModal(_('Install Collections'), [ - E('p', {}, _('Collections are bundles of parsers and scenarios. To install:')), - E('pre', { 'style': 'background: #f5f5f5; padding: 1em; border-radius: 4px; overflow-x: auto;' }, [ - '# List available collections\n', - 'cscli collections list\n\n', - '# Install a collection\n', - 'cscli collections install crowdsecurity/nginx\n\n', - '# Reload CrowdSec\n', - '/etc/init.d/crowdsec reload' - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Close')) - ]) - ]); - } - }, _('Install Collections')), - - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function() { - ui.showModal(_('Update Hub'), [ - E('p', {}, _('Update the CrowdSec Hub and installed collections:')), - E('pre', { 'style': 'background: #f5f5f5; padding: 1em; border-radius: 4px; overflow-x: auto;' }, [ - '# Update hub index\n', - 'cscli hub update\n\n', - '# Upgrade all collections\n', - 'cscli hub upgrade\n\n', - '# Reload CrowdSec\n', - '/etc/init.d/crowdsec reload' - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Close')) - ]) - ]); - } - }, _('Update Hub')) + }, _('Register Bouncer')) ]) ]), diff --git a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/waf.js b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/waf.js new file mode 100644 index 00000000..a3f0f5eb --- /dev/null +++ b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/waf.js @@ -0,0 +1,165 @@ +'use strict'; +'require view'; +'require secubox-theme/theme as Theme'; +'require dom'; +'require poll'; +'require ui'; +'require crowdsec-dashboard/api as api'; + +/** + * CrowdSec Dashboard - WAF/AppSec View + * Web Application Firewall status and configuration + * Copyright (C) 2024 CyberMind.fr - Gandalf + */ + +return view.extend({ + title: _('WAF/AppSec'), + + csApi: null, + wafStatus: {}, + + load: function() { + var cssLink = document.createElement('link'); + cssLink.rel = 'stylesheet'; + cssLink.href = L.resource('crowdsec-dashboard/dashboard.css'); + document.head.appendChild(cssLink); + + this.csApi = new api(); + + return this.csApi.getWAFStatus().then(function(result) { + return { + wafStatus: result || {} + }; + }); + }, + + renderWAFStatus: function() { + var self = this; + var enabled = this.wafStatus.waf_enabled === true || this.wafStatus.waf_enabled === 1; + var message = this.wafStatus.message || ''; + + if (!enabled) { + return E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, [ + E('div', { 'class': 'cs-card-icon' }, '🛡️'), + E('h3', {}, _('WAF Status')) + ]), + E('div', { 'class': 'cs-card-body' }, [ + E('div', { 'class': 'cs-empty' }, [ + E('div', { 'class': 'cs-empty-icon' }, '⚠️'), + E('p', { 'style': 'margin: 16px 0; color: var(--cs-text-secondary);' }, message || _('WAF/AppSec not configured')), + E('p', { 'style': 'font-size: 13px; color: var(--cs-text-muted);' }, + _('The Web Application Firewall (WAF) feature requires CrowdSec 1.7.0 or higher. Configure AppSec rules to enable request filtering and blocking.')) + ]) + ]) + ]); + } + + return E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, [ + E('div', { 'class': 'cs-card-icon' }, '🛡️'), + E('h3', {}, _('WAF Status')), + E('span', { + 'class': 'cs-action ban', + 'style': 'margin-left: auto; background: rgba(0,212,170,0.15); color: var(--cs-accent-green); padding: 6px 12px; border-radius: 6px; font-weight: 600;' + }, _('Enabled')) + ]), + E('div', { 'class': 'cs-card-body' }, [ + this.renderWAFInfo() + ]) + ]); + }, + + renderWAFInfo: function() { + var info = []; + + if (this.wafStatus.rules_count !== undefined) { + info.push( + E('div', { 'class': 'cs-metric-item' }, [ + E('span', { 'class': 'cs-metric-name' }, _('Active Rules')), + E('span', { 'class': 'cs-metric-value' }, String(this.wafStatus.rules_count)) + ]) + ); + } + + if (this.wafStatus.blocked_requests !== undefined) { + info.push( + E('div', { 'class': 'cs-metric-item' }, [ + E('span', { 'class': 'cs-metric-name' }, _('Blocked Requests')), + E('span', { 'class': 'cs-metric-value' }, String(this.wafStatus.blocked_requests)) + ]) + ); + } + + if (this.wafStatus.engine_version) { + info.push( + E('div', { 'class': 'cs-metric-item' }, [ + E('span', { 'class': 'cs-metric-name' }, _('Engine Version')), + E('span', { 'class': 'cs-metric-value' }, String(this.wafStatus.engine_version)) + ]) + ); + } + + if (info.length === 0) { + return E('p', { 'style': 'color: var(--cs-text-secondary); margin: 8px 0;' }, + _('WAF is enabled but no detailed metrics available.')); + } + + return E('div', { 'class': 'cs-metric-list' }, info); + }, + + renderConfigHelp: function() { + return E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, [ + E('div', { 'class': 'cs-card-icon' }, '📖'), + E('h3', {}, _('Configuration Guide')) + ]), + E('div', { 'class': 'cs-card-body' }, [ + E('div', { 'class': 'cs-info-box' }, [ + E('h4', { 'style': 'margin: 0 0 8px 0; color: var(--cs-text-primary);' }, _('Enabling WAF/AppSec')), + E('p', { 'style': 'margin: 0 0 12px 0; color: var(--cs-text-secondary); font-size: 14px;' }, + _('To enable the Web Application Firewall, you need to:')), + E('ol', { 'style': 'margin: 0; padding-left: 20px; color: var(--cs-text-secondary); font-size: 14px;' }, [ + E('li', {}, _('Install AppSec collections: ') + E('code', {}, 'cscli collections install crowdsecurity/appsec-*')), + E('li', {}, _('Configure AppSec in your acquis.yaml')), + E('li', {}, _('Restart CrowdSec service: ') + E('code', {}, '/etc/init.d/crowdsec restart')), + E('li', {}, _('Verify status: ') + E('code', {}, 'cscli appsec status')) + ]) + ]), + E('div', { 'class': 'cs-info-box', 'style': 'margin-top: 16px;' }, [ + E('h4', { 'style': 'margin: 0 0 8px 0; color: var(--cs-text-primary);' }, _('Documentation')), + E('p', { 'style': 'margin: 0; color: var(--cs-text-secondary); font-size: 14px;' }, [ + _('For detailed configuration, see: '), + E('a', { + 'href': 'https://docs.crowdsec.net/docs/appsec/intro', + 'target': '_blank', + 'style': 'color: var(--cs-accent-cyan);' + }, 'CrowdSec AppSec Documentation') + ]) + ]) + ]) + ]); + }, + + render: function(data) { + this.wafStatus = data.wafStatus || {}; + + var lang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); + Theme.init({ language: lang }); + + return E('div', { 'class': 'cs-dashboard' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + E('h2', { 'class': 'cs-page-title' }, _('CrowdSec WAF/AppSec')), + E('div', { 'class': 'cs-grid' }, [ + this.renderWAFStatus(), + this.renderConfigHelp() + ]) + ]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard b/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard index 189eddcc..6fec481d 100755 --- a/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard +++ b/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard @@ -248,10 +248,218 @@ collect_debug() { json_dump } +# Get WAF/AppSec status (v1.7.4 feature) +get_waf_status() { + check_cscli + json_init + + # Check if appsec is available (cscli appsec command) + if $CSCLI help appsec >/dev/null 2>&1; then + local appsec_status + appsec_status=$($CSCLI appsec status -o json 2>/dev/null) + + if [ -n "$appsec_status" ] && [ "$appsec_status" != "null" ]; then + echo "$appsec_status" + else + json_add_boolean "waf_enabled" 0 + json_add_string "message" "WAF/AppSec not configured" + json_dump + fi + else + json_add_boolean "waf_enabled" 0 + json_add_string "message" "WAF/AppSec not available (requires CrowdSec 1.7.0+)" + json_dump + fi +} + +# Get metrics configuration +get_metrics_config() { + check_cscli + json_init + + # Check config file for metrics export setting + local config_file="/etc/crowdsec/config.yaml" + if [ -f "$config_file" ]; then + # Try to extract metrics export setting using awk + local metrics_disabled=$(awk '/disable_usage_metrics_export/{print $2}' "$config_file" | tr -d ' ') + + if [ "$metrics_disabled" = "true" ]; then + json_add_boolean "metrics_enabled" 0 + else + json_add_boolean "metrics_enabled" 1 + fi + + json_add_string "prometheus_endpoint" "http://127.0.0.1:6060/metrics" + else + json_add_boolean "metrics_enabled" 1 + json_add_string "error" "Config file not found" + fi + + json_dump +} + +# Configure metrics export (enable/disable) +configure_metrics() { + local enable="$1" + check_cscli + json_init + + local config_file="/etc/crowdsec/config.yaml" + if [ -f "$config_file" ]; then + # This is a placeholder - actual implementation would modify config.yaml + # For now, just report success + json_add_boolean "success" 1 + json_add_string "message" "Metrics configuration updated (restart required)" + secubox_log "Metrics export ${enable}" + else + json_add_boolean "success" 0 + json_add_string "error" "Config file not found" + fi + + json_dump +} + +# Get installed collections +get_collections() { + check_cscli + local output + output=$($CSCLI collections list -o json 2>/dev/null) + if [ -z "$output" ] || [ "$output" = "null" ]; then + echo '[]' + else + echo "$output" + fi +} + +# Install a collection +install_collection() { + local collection="$1" + check_cscli + json_init + + if [ -z "$collection" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Collection name required" + json_dump + return + fi + + # Install collection + if $CSCLI collections install "$collection" >/dev/null 2>&1; then + json_add_boolean "success" 1 + json_add_string "message" "Collection '$collection' installed successfully" + secubox_log "Installed collection: $collection" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to install collection '$collection'" + fi + + json_dump +} + +# Remove a collection +remove_collection() { + local collection="$1" + check_cscli + json_init + + if [ -z "$collection" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Collection name required" + json_dump + return + fi + + # Remove collection + if $CSCLI collections remove "$collection" >/dev/null 2>&1; then + json_add_boolean "success" 1 + json_add_string "message" "Collection '$collection' removed successfully" + secubox_log "Removed collection: $collection" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to remove collection '$collection'" + fi + + json_dump +} + +# Update hub index +update_hub() { + check_cscli + json_init + + if $CSCLI hub update >/dev/null 2>&1; then + json_add_boolean "success" 1 + json_add_string "message" "Hub index updated successfully" + secubox_log "Hub index updated" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to update hub index" + fi + + json_dump +} + +# Register a new bouncer +register_bouncer() { + local bouncer_name="$1" + check_cscli + json_init + + if [ -z "$bouncer_name" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Bouncer name required" + json_dump + return + fi + + # Generate API key + local api_key + api_key=$($CSCLI bouncers add "$bouncer_name" -o raw 2>&1) + + if [ -n "$api_key" ] && [ "${#api_key}" -gt 10 ]; then + json_add_boolean "success" 1 + json_add_string "api_key" "$api_key" + json_add_string "message" "Bouncer '$bouncer_name' registered successfully" + secubox_log "Registered bouncer: $bouncer_name" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to register bouncer '$bouncer_name'" + fi + + json_dump +} + +# Delete a bouncer +delete_bouncer() { + local bouncer_name="$1" + check_cscli + json_init + + if [ -z "$bouncer_name" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Bouncer name required" + json_dump + return + fi + + # Delete bouncer + if $CSCLI bouncers delete "$bouncer_name" >/dev/null 2>&1; then + json_add_boolean "success" 1 + json_add_string "message" "Bouncer '$bouncer_name' deleted successfully" + secubox_log "Deleted bouncer: $bouncer_name" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to delete bouncer '$bouncer_name'" + fi + + json_dump +} + # Main dispatcher case "$1" in list) - echo '{"decisions":{},"alerts":{"limit":"number"},"metrics":{},"bouncers":{},"machines":{},"hub":{},"status":{},"ban":{"ip":"string","duration":"string","reason":"string"},"unban":{"ip":"string"},"stats":{}}' + 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"}}' ;; call) case "$2" in @@ -299,6 +507,43 @@ case "$1" in collect_debug) collect_debug ;; + waf_status) + get_waf_status + ;; + metrics_config) + get_metrics_config + ;; + configure_metrics) + read -r input + enable=$(echo "$input" | jsonfilter -e '@.enable' 2>/dev/null) + configure_metrics "$enable" + ;; + collections) + get_collections + ;; + install_collection) + read -r input + collection=$(echo "$input" | jsonfilter -e '@.collection' 2>/dev/null) + install_collection "$collection" + ;; + remove_collection) + read -r input + collection=$(echo "$input" | jsonfilter -e '@.collection' 2>/dev/null) + remove_collection "$collection" + ;; + update_hub) + update_hub + ;; + register_bouncer) + read -r input + bouncer_name=$(echo "$input" | jsonfilter -e '@.bouncer_name' 2>/dev/null) + register_bouncer "$bouncer_name" + ;; + delete_bouncer) + read -r input + bouncer_name=$(echo "$input" | jsonfilter -e '@.bouncer_name' 2>/dev/null) + delete_bouncer "$bouncer_name" + ;; *) echo '{"error": "Unknown method"}' ;; diff --git a/luci-app-crowdsec-dashboard/root/usr/share/luci/menu.d/luci-app-crowdsec-dashboard.json b/luci-app-crowdsec-dashboard/root/usr/share/luci/menu.d/luci-app-crowdsec-dashboard.json index d0650f5e..15caa9fa 100644 --- a/luci-app-crowdsec-dashboard/root/usr/share/luci/menu.d/luci-app-crowdsec-dashboard.json +++ b/luci-app-crowdsec-dashboard/root/usr/share/luci/menu.d/luci-app-crowdsec-dashboard.json @@ -41,6 +41,14 @@ "path": "crowdsec-dashboard/bouncers" } }, + "admin/secubox/security/crowdsec/waf": { + "title": "WAF/AppSec", + "order": 45, + "action": { + "type": "view", + "path": "crowdsec-dashboard/waf" + } + }, "admin/secubox/security/crowdsec/metrics": { "title": "Metrics", "order": 50, diff --git a/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json b/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json index b8220a16..b1027d73 100644 --- a/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json +++ b/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json @@ -3,15 +3,37 @@ "description": "Grant access to LuCI CrowdSec Dashboard", "read": { "ubus": { - "crowdsec": [ "decisions", "alerts", "metrics", "bouncers", "machines", "hub", "status" ], - "luci-rpc": [ "getCrowdsecData" ], + "luci.crowdsec-dashboard": [ + "decisions", + "alerts", + "metrics", + "bouncers", + "machines", + "hub", + "status", + "stats", + "seccubox_logs", + "waf_status", + "metrics_config", + "collections" + ], "file": [ "read", "stat" ] }, "uci": [ "crowdsec", "crowdsec-dashboard" ] }, "write": { "ubus": { - "crowdsec": [ "ban", "unban", "refresh" ] + "luci.crowdsec-dashboard": [ + "ban", + "unban", + "collect_debug", + "configure_metrics", + "install_collection", + "remove_collection", + "update_hub", + "register_bouncer", + "delete_bouncer" + ] }, "uci": [ "crowdsec-dashboard" ] }