diff --git a/package/secubox/luci-app-crowdsec-dashboard/Makefile b/package/secubox/luci-app-crowdsec-dashboard/Makefile index dc13866e..227c3059 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/Makefile +++ b/package/secubox/luci-app-crowdsec-dashboard/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-crowdsec-dashboard PKG_VERSION:=0.7.0 -PKG_RELEASE:=17 +PKG_RELEASE:=18 PKG_ARCH:=all PKG_LICENSE:=Apache-2.0 diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js index 778da489..fa91dc65 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js @@ -38,7 +38,8 @@ return view.extend({ this.csApi.getHealthCheck().catch(function() { return {}; }), this.csApi.getCapiMetrics().catch(function() { return {}; }), this.csApi.getCollections().catch(function() { return { collections: [] }; }), - this.csApi.getNftablesStats().catch(function() { return {}; }) + this.csApi.getNftablesStats().catch(function() { return {}; }), + this.csApi.getHub().catch(function() { return {}; }) ]); }, @@ -450,7 +451,8 @@ return view.extend({ E('div', { 'class': 'cs-charts-row' }, [ this.renderCapiBlocklist(), - this.renderCollectionsCard() + this.renderCollectionsCard(), + this.renderHubStatsCard() ]), E('div', { 'class': 'cs-charts-row' }, [ @@ -520,6 +522,7 @@ return view.extend({ this.capiMetrics = payload[3] || {}; this.collections = (payload[4] && payload[4].collections) || []; this.nftablesStats = payload[5] || {}; + this.hubData = payload[6] || {}; // Main wrapper with SecuBox header var wrapper = E('div', { 'class': 'secubox-page-wrapper' }); @@ -548,7 +551,8 @@ refreshDashboard: function() { self.csApi.getHealthCheck().catch(function() { return {}; }), self.csApi.getCapiMetrics().catch(function() { return {}; }), self.csApi.getCollections().catch(function() { return { collections: [] }; }), - self.csApi.getNftablesStats().catch(function() { return {}; }) + self.csApi.getNftablesStats().catch(function() { return {}; }), + self.csApi.getHub().catch(function() { return {}; }) ]).then(function(results) { self.data = results[0]; self.logs = (results[1] && results[1].entries) || []; @@ -556,6 +560,7 @@ refreshDashboard: function() { self.capiMetrics = results[3] || {}; self.collections = (results[4] && results[4].collections) || []; self.nftablesStats = results[5] || {}; + self.hubData = results[6] || {}; self.updateView(); }); }, @@ -739,6 +744,63 @@ refreshDashboard: function() { }); }, + // Hub Stats Card - Shows installed parsers, scenarios, collections counts + renderHubStatsCard: function() { + var hub = this.hubData || {}; + + // Count installed items by type + var parsers = hub.parsers || []; + var scenarios = hub.scenarios || []; + var collections = hub.collections || []; + var postoverflows = hub.postoverflows || []; + + var installedParsers = parsers.filter(function(p) { + return p.installed || (p.status && p.status.toLowerCase().indexOf('enabled') >= 0); + }); + var installedScenarios = scenarios.filter(function(s) { + return s.installed || (s.status && s.status.toLowerCase().indexOf('enabled') >= 0); + }); + var installedCollections = collections.filter(function(c) { + return c.installed || (c.status && c.status.toLowerCase().indexOf('enabled') >= 0); + }); + + // Get parser details for display + var parserList = installedParsers.slice(0, 8).map(function(p) { + var name = p.name || p.Name || 'unknown'; + var shortName = name.split('/').pop(); + return E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.35em 0; border-bottom: 1px solid rgba(255,255,255,0.05);' }, [ + E('span', { 'style': 'color: #22c55e;' }, '✓'), + E('span', { 'style': 'font-size: 0.85em;' }, shortName) + ]); + }); + + return E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, [ + E('div', { 'class': 'cs-card-title' }, _('Hub Components')) + ]), + E('div', { 'class': 'cs-card-body' }, [ + // Mini stats row + E('div', { 'style': 'display: flex; gap: 1em; margin-bottom: 1em;' }, [ + E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(34,197,94,0.1); border-radius: 8px;' }, [ + E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #22c55e;' }, String(installedParsers.length)), + E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Parsers')) + ]), + E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(59,130,246,0.1); border-radius: 8px;' }, [ + E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #3b82f6;' }, String(installedScenarios.length)), + E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Scenarios')) + ]), + E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(168,85,247,0.1); border-radius: 8px;' }, [ + E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #a855f7;' }, String(installedCollections.length)), + E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Collections')) + ]) + ]), + // Parser list + E('div', { 'style': 'font-size: 0.8em; font-weight: 600; color: #94a3b8; margin-bottom: 0.5em;' }, _('Installed Parsers')), + parserList.length > 0 ? E('div', {}, parserList) : E('div', { 'style': 'color: #666; font-size: 0.85em;' }, _('No parsers installed')) + ]) + ]); + }, + // Firewall Blocks - Shows IPs blocked in nftables renderFirewallBlocks: function() { var self = this; diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js index a3749163..511df4d9 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js @@ -637,50 +637,69 @@ return view.extend({ { name: 'crowdsecurity/whitelist-good-actors', description: 'Whitelist known good bots', preselected: false } ]; + // OpenWrt-specific parsers for log sources + var recommendedParsers = [ + { name: 'crowdsecurity/dropbear-logs', description: 'Dropbear SSH daemon logs (OpenWrt)', preselected: this.wizardData.sshEnabled }, + { name: 'crowdsecurity/syslog-logs', description: 'Generic syslog parser', preselected: this.wizardData.syslogEnabled } + ]; + + var renderCheckboxItem = function(item, type) { + return E('div', { + 'class': 'collection-item', + 'data-' + type: item.name, + 'data-type': type, + 'data-checked': item.preselected ? '1' : '0', + 'style': 'display: flex; align-items: center; cursor: pointer;', + 'click': function(ev) { + var el = ev.currentTarget; + var currentState = el.getAttribute('data-checked') === '1'; + var newState = !currentState; + el.setAttribute('data-checked', newState ? '1' : '0'); + var checkbox = el.querySelector('.checkbox-indicator'); + if (checkbox) { + checkbox.textContent = newState ? '☑' : '☐'; + checkbox.style.color = newState ? '#22c55e' : '#94a3b8'; + } + } + }, [ + E('span', { + 'class': 'checkbox-indicator', + 'style': 'display: inline-block; font-size: 28px; margin-right: 16px; user-select: none; color: ' + (item.preselected ? '#22c55e' : '#94a3b8') + '; line-height: 1; min-width: 28px;' + }, item.preselected ? '☑' : '☐'), + E('div', { 'class': 'collection-info', 'style': 'flex: 1;' }, [ + E('strong', {}, item.name), + E('div', { 'class': 'collection-desc' }, item.description) + ]) + ]); + }; + return E('div', { 'class': 'wizard-step' }, [ - E('h2', {}, _('Install Security Collections')), - E('p', {}, _('Select collections to install. Recommended collections are pre-selected.')), + E('h2', {}, _('Install Security Collections & Parsers')), + E('p', {}, _('Select collections and parsers to install. Recommended items are pre-selected based on your log configuration.')), + // Collections section + E('h3', { 'style': 'margin-top: 20px; margin-bottom: 12px; font-size: 1.1em; color: #94a3b8;' }, [ + E('span', { 'style': 'margin-right: 8px;' }, '📦'), + _('Collections') + ]), E('div', { 'class': 'collections-list' }, - recommendedCollections.map(L.bind(function(collection) { - var checkboxId = 'collection-' + collection.name.replace('/', '-'); + recommendedCollections.map(function(c) { return renderCheckboxItem(c, 'collection'); }) + ), - return E('div', { - 'class': 'collection-item', - 'data-collection': collection.name, - 'data-checked': collection.preselected ? '1' : '0', - 'style': 'display: flex; align-items: center; cursor: pointer;', - 'click': function(ev) { - var item = ev.currentTarget; - var currentState = item.getAttribute('data-checked') === '1'; - var newState = !currentState; - item.setAttribute('data-checked', newState ? '1' : '0'); - - // Update visual indicator - var checkbox = item.querySelector('.checkbox-indicator'); - if (checkbox) { - checkbox.textContent = newState ? '☑' : '☐'; - checkbox.style.color = newState ? '#22c55e' : '#94a3b8'; - } - } - }, [ - E('span', { - 'class': 'checkbox-indicator', - 'style': 'display: inline-block; font-size: 28px; margin-right: 16px; user-select: none; color: ' + (collection.preselected ? '#22c55e' : '#94a3b8') + '; line-height: 1; min-width: 28px;' - }, collection.preselected ? '☑' : '☐'), - E('div', { 'class': 'collection-info', 'style': 'flex: 1;' }, [ - E('strong', {}, collection.name), - E('div', { 'class': 'collection-desc' }, collection.description) - ]) - ]); - }, this)) + // Parsers section + E('h3', { 'style': 'margin-top: 24px; margin-bottom: 12px; font-size: 1.1em; color: #94a3b8;' }, [ + E('span', { 'style': 'margin-right: 8px;' }, '📝'), + _('OpenWrt Parsers') + ]), + E('div', { 'class': 'collections-list' }, + recommendedParsers.map(function(p) { return renderCheckboxItem(p, 'parser'); }) ), // Install progress this.wizardData.installing ? E('div', { 'class': 'install-progress' }, [ E('div', { 'class': 'spinning' }), - E('p', {}, _('Installing collections...')), + E('p', {}, _('Installing...')), E('div', { 'id': 'install-status' }, this.wizardData.installStatus || '') ]) : E([]), @@ -1123,37 +1142,60 @@ return view.extend({ }, handleInstallCollections: function() { - // Read from data-checked attributes (Unicode checkbox approach) - var items = document.querySelectorAll('.collection-item[data-collection]'); - var selected = Array.from(items) + // Read collections from data-checked attributes + var collectionItems = document.querySelectorAll('.collection-item[data-collection]'); + var selectedCollections = Array.from(collectionItems) .filter(function(item) { return item.getAttribute('data-checked') === '1'; }) .map(function(item) { return item.getAttribute('data-collection'); }); - console.log('[Wizard] Selected collections:', selected); + // Read parsers from data-checked attributes + var parserItems = document.querySelectorAll('.collection-item[data-parser]'); + var selectedParsers = Array.from(parserItems) + .filter(function(item) { return item.getAttribute('data-checked') === '1'; }) + .map(function(item) { return item.getAttribute('data-parser'); }); - if (selected.length === 0) { + console.log('[Wizard] Selected collections:', selectedCollections); + console.log('[Wizard] Selected parsers:', selectedParsers); + + var totalItems = selectedCollections.length + selectedParsers.length; + + if (totalItems === 0) { this.goToStep(6); return; } this.wizardData.installing = true; - this.wizardData.installStatus = _('Installing 0 of %d collections...').format(selected.length); + this.wizardData.installStatus = _('Installing 0 of %d items...').format(totalItems); this.refreshView(); + var currentIndex = 0; + var self = this; + // 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(); + var installPromises = selectedCollections.reduce(function(promise, collection) { + return promise.then(function() { + currentIndex++; + self.wizardData.installStatus = _('Installing %d of %d: %s').format(currentIndex, totalItems, collection); + self.refreshView(); return API.installCollection(collection); - }, this)); - }, this), Promise.resolve()); + }); + }, Promise.resolve()); + + // Then install parsers sequentially + installPromises = selectedParsers.reduce(function(promise, parser) { + return promise.then(function() { + currentIndex++; + self.wizardData.installStatus = _('Installing %d of %d: %s').format(currentIndex, totalItems, parser); + self.refreshView(); + return API.installHubItem('parser', parser); + }); + }, installPromises); 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.wizardData.installedCount = totalItems; + ui.addNotification(null, E('p', _('Installed %d collections and %d parsers').format(selectedCollections.length, selectedParsers.length)), 'info'); this.refreshView(); // Auto-advance to Step 6 (Configure Bouncer) after 2 seconds