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:
parent
2cfeb682d8
commit
921db872ed
@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-crowdsec-dashboard
|
PKG_NAME:=luci-app-crowdsec-dashboard
|
||||||
PKG_VERSION:=0.7.0
|
PKG_VERSION:=0.7.0
|
||||||
PKG_RELEASE:=17
|
PKG_RELEASE:=18
|
||||||
PKG_ARCH:=all
|
PKG_ARCH:=all
|
||||||
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
|||||||
@ -38,7 +38,8 @@ return view.extend({
|
|||||||
this.csApi.getHealthCheck().catch(function() { return {}; }),
|
this.csApi.getHealthCheck().catch(function() { return {}; }),
|
||||||
this.csApi.getCapiMetrics().catch(function() { return {}; }),
|
this.csApi.getCapiMetrics().catch(function() { return {}; }),
|
||||||
this.csApi.getCollections().catch(function() { return { collections: [] }; }),
|
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' }, [
|
E('div', { 'class': 'cs-charts-row' }, [
|
||||||
this.renderCapiBlocklist(),
|
this.renderCapiBlocklist(),
|
||||||
this.renderCollectionsCard()
|
this.renderCollectionsCard(),
|
||||||
|
this.renderHubStatsCard()
|
||||||
]),
|
]),
|
||||||
|
|
||||||
E('div', { 'class': 'cs-charts-row' }, [
|
E('div', { 'class': 'cs-charts-row' }, [
|
||||||
@ -520,6 +522,7 @@ return view.extend({
|
|||||||
this.capiMetrics = payload[3] || {};
|
this.capiMetrics = payload[3] || {};
|
||||||
this.collections = (payload[4] && payload[4].collections) || [];
|
this.collections = (payload[4] && payload[4].collections) || [];
|
||||||
this.nftablesStats = payload[5] || {};
|
this.nftablesStats = payload[5] || {};
|
||||||
|
this.hubData = payload[6] || {};
|
||||||
|
|
||||||
// Main wrapper with SecuBox header
|
// Main wrapper with SecuBox header
|
||||||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||||
@ -548,7 +551,8 @@ refreshDashboard: function() {
|
|||||||
self.csApi.getHealthCheck().catch(function() { return {}; }),
|
self.csApi.getHealthCheck().catch(function() { return {}; }),
|
||||||
self.csApi.getCapiMetrics().catch(function() { return {}; }),
|
self.csApi.getCapiMetrics().catch(function() { return {}; }),
|
||||||
self.csApi.getCollections().catch(function() { return { collections: [] }; }),
|
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) {
|
]).then(function(results) {
|
||||||
self.data = results[0];
|
self.data = results[0];
|
||||||
self.logs = (results[1] && results[1].entries) || [];
|
self.logs = (results[1] && results[1].entries) || [];
|
||||||
@ -556,6 +560,7 @@ refreshDashboard: function() {
|
|||||||
self.capiMetrics = results[3] || {};
|
self.capiMetrics = results[3] || {};
|
||||||
self.collections = (results[4] && results[4].collections) || [];
|
self.collections = (results[4] && results[4].collections) || [];
|
||||||
self.nftablesStats = results[5] || {};
|
self.nftablesStats = results[5] || {};
|
||||||
|
self.hubData = results[6] || {};
|
||||||
self.updateView();
|
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
|
// Firewall Blocks - Shows IPs blocked in nftables
|
||||||
renderFirewallBlocks: function() {
|
renderFirewallBlocks: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|||||||
@ -637,50 +637,69 @@ return view.extend({
|
|||||||
{ name: 'crowdsecurity/whitelist-good-actors', description: 'Whitelist known good bots', preselected: false }
|
{ 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' }, [
|
return E('div', { 'class': 'wizard-step' }, [
|
||||||
E('h2', {}, _('Install Security Collections')),
|
E('h2', {}, _('Install Security Collections & Parsers')),
|
||||||
E('p', {}, _('Select collections to install. Recommended collections are pre-selected.')),
|
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' },
|
E('div', { 'class': 'collections-list' },
|
||||||
recommendedCollections.map(L.bind(function(collection) {
|
recommendedCollections.map(function(c) { return renderCheckboxItem(c, 'collection'); })
|
||||||
var checkboxId = 'collection-' + collection.name.replace('/', '-');
|
),
|
||||||
|
|
||||||
return E('div', {
|
// Parsers section
|
||||||
'class': 'collection-item',
|
E('h3', { 'style': 'margin-top: 24px; margin-bottom: 12px; font-size: 1.1em; color: #94a3b8;' }, [
|
||||||
'data-collection': collection.name,
|
E('span', { 'style': 'margin-right: 8px;' }, '📝'),
|
||||||
'data-checked': collection.preselected ? '1' : '0',
|
_('OpenWrt Parsers')
|
||||||
'style': 'display: flex; align-items: center; cursor: pointer;',
|
]),
|
||||||
'click': function(ev) {
|
E('div', { 'class': 'collections-list' },
|
||||||
var item = ev.currentTarget;
|
recommendedParsers.map(function(p) { return renderCheckboxItem(p, 'parser'); })
|
||||||
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))
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Install progress
|
// Install progress
|
||||||
this.wizardData.installing ?
|
this.wizardData.installing ?
|
||||||
E('div', { 'class': 'install-progress' }, [
|
E('div', { 'class': 'install-progress' }, [
|
||||||
E('div', { 'class': 'spinning' }),
|
E('div', { 'class': 'spinning' }),
|
||||||
E('p', {}, _('Installing collections...')),
|
E('p', {}, _('Installing...')),
|
||||||
E('div', { 'id': 'install-status' }, this.wizardData.installStatus || '')
|
E('div', { 'id': 'install-status' }, this.wizardData.installStatus || '')
|
||||||
]) : E([]),
|
]) : E([]),
|
||||||
|
|
||||||
@ -1123,37 +1142,60 @@ return view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleInstallCollections: function() {
|
handleInstallCollections: function() {
|
||||||
// Read from data-checked attributes (Unicode checkbox approach)
|
// Read collections from data-checked attributes
|
||||||
var items = document.querySelectorAll('.collection-item[data-collection]');
|
var collectionItems = document.querySelectorAll('.collection-item[data-collection]');
|
||||||
var selected = Array.from(items)
|
var selectedCollections = Array.from(collectionItems)
|
||||||
.filter(function(item) { return item.getAttribute('data-checked') === '1'; })
|
.filter(function(item) { return item.getAttribute('data-checked') === '1'; })
|
||||||
.map(function(item) { return item.getAttribute('data-collection'); });
|
.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);
|
this.goToStep(6);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wizardData.installing = true;
|
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();
|
this.refreshView();
|
||||||
|
|
||||||
|
var currentIndex = 0;
|
||||||
|
var self = this;
|
||||||
|
|
||||||
// Install collections sequentially
|
// Install collections sequentially
|
||||||
var installPromises = selected.reduce(L.bind(function(promise, collection, index) {
|
var installPromises = selectedCollections.reduce(function(promise, collection) {
|
||||||
return promise.then(L.bind(function() {
|
return promise.then(function() {
|
||||||
this.wizardData.installStatus = _('Installing %d of %d: %s').format(index + 1, selected.length, collection);
|
currentIndex++;
|
||||||
this.refreshView();
|
self.wizardData.installStatus = _('Installing %d of %d: %s').format(currentIndex, totalItems, collection);
|
||||||
|
self.refreshView();
|
||||||
return API.installCollection(collection);
|
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() {
|
return installPromises.then(L.bind(function() {
|
||||||
this.wizardData.installing = false;
|
this.wizardData.installing = false;
|
||||||
this.wizardData.installed = true;
|
this.wizardData.installed = true;
|
||||||
this.wizardData.installedCount = selected.length;
|
this.wizardData.installedCount = totalItems;
|
||||||
ui.addNotification(null, E('p', _('Installed %d collections').format(selected.length)), 'info');
|
ui.addNotification(null, E('p', _('Installed %d collections and %d parsers').format(selectedCollections.length, selectedParsers.length)), 'info');
|
||||||
this.refreshView();
|
this.refreshView();
|
||||||
|
|
||||||
// Auto-advance to Step 6 (Configure Bouncer) after 2 seconds
|
// Auto-advance to Step 6 (Configure Bouncer) after 2 seconds
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user