feat(crowdsec-dashboard): Add dropbear-logs parser and Hub Stats card

Wizard Step 5 improvements:
- Add OpenWrt Parsers section (dropbear-logs, syslog-logs)
- Auto-select dropbear-logs when SSH logging is enabled
- Install parsers via installHubItem API alongside collections

Overview Dashboard:
- Add Hub Components stats card showing:
  - Installed Parsers count (green)
  - Installed Scenarios count (blue)
  - Installed Collections count (purple)
  - List of installed parser names
- Load hub data in dashboard for component counts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-13 08:51:24 +01:00
parent 2cfeb682d8
commit 921db872ed
3 changed files with 156 additions and 52 deletions

View File

@ -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

View File

@ -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;

View File

@ -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