feat: Add LAPI auto-repair and SecuBox theming to CrowdSec Dashboard (v0.6.0-r27)

- Add repair_lapi() RPCD method to auto-fix LAPI configuration issues:
  - Creates /srv/crowdsec/data directory if missing
  - Fixes data_dir and db_path in config.yaml
  - Re-registers localhost machine if needed
  - Restarts CrowdSec and verifies LAPI is working

- Fix register_bouncer() to handle existing bouncers:
  - Deletes existing bouncer before re-registering
  - Gets fresh API key on re-registration

- Fix update_firewall_bouncer_config() UCI path:
  - Changed from crowdsec.bouncer.$key to crowdsec.@bouncer[0].$key
  - Added api_key to allowed parameters

- Rewrite metrics.js with SecuBox cyber-card theming:
  - Use Theme.init() for proper theme initialization
  - Replace cs-* classes with cyber-* classes
  - Add CSS variable fallbacks for light/dark theme support
  - Fix hub data parsing for proper component counts

- Add theme require to wizard.js

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-08 14:11:16 +01:00
parent b7fb268f71
commit 4078b4d7a4
5 changed files with 434 additions and 192 deletions

View File

@ -191,6 +191,12 @@ var callWizardState = rpc.declare({
expect: { }
});
var callRepairLapi = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'repair_lapi',
expect: { }
});
function formatDuration(seconds) {
if (!seconds) return 'N/A';
if (seconds < 60) return seconds + 's';
@ -310,6 +316,7 @@ return baseclass.extend({
// Wizard Methods
checkWizardNeeded: callCheckWizardNeeded,
getWizardState: callWizardState,
repairLapi: callRepairLapi,
formatDuration: formatDuration,
formatDate: formatDate,

View File

@ -14,7 +14,7 @@
return view.extend({
title: _('Metrics'),
csApi: null,
metrics: {},
bouncers: [],
@ -22,11 +22,6 @@ return view.extend({
hub: {},
load: function() {
var cssLink = document.createElement('link');
cssLink.rel = 'stylesheet';
cssLink.href = L.resource('crowdsec-dashboard/dashboard.css');
document.head.appendChild(cssLink);
this.csApi = api;
return Promise.all([
@ -50,97 +45,111 @@ return view.extend({
if (!data || typeof data !== 'object') {
return null;
}
var entries = Object.entries(data);
if (entries.length === 0) {
return null;
}
var items = entries.map(function(entry) {
var value = entry[1];
if (typeof value === 'object') {
value = JSON.stringify(value);
}
return E('div', { 'class': 'cs-metric-item' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('span', { 'class': 'cs-metric-name' }, entry[0]),
E('span', { 'class': 'cs-metric-value' }, String(value))
return E('div', { 'class': 'cyber-metric-item', 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [
E('span', { 'class': 'cyber-metric-name', 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, entry[0]),
E('span', { 'class': 'cyber-metric-value', 'style': 'color: var(--cyber-text-primary, #fff); font-weight: 500;' }, String(value))
]);
});
return E('div', { 'class': 'cs-metric-section' }, [
E('div', { 'class': 'cs-metric-section-title' }, title),
E('div', { 'class': 'cs-metric-list' }, items)
return E('div', { 'class': 'cyber-metric-section', 'style': 'margin-bottom: 1rem;' }, [
E('div', { 'class': 'cyber-metric-section-title', 'style': 'font-weight: 600; color: var(--cyber-accent-primary, #667eea); margin-bottom: 0.5rem; font-size: 0.9rem;' }, title),
E('div', { 'class': 'cyber-metric-list' }, items)
]);
},
renderBouncersTable: function() {
var self = this;
if (!Array.isArray(this.bouncers) || this.bouncers.length === 0) {
return E('div', { 'class': 'cs-empty' }, [
E('div', { 'class': 'cs-empty-icon' }, '🔌'),
E('p', {}, 'No bouncers registered')
var bouncers = this.bouncers;
// Handle response structure: may be { bouncers: [...] } or direct array
if (bouncers && bouncers.bouncers) {
bouncers = bouncers.bouncers;
}
if (!Array.isArray(bouncers) || bouncers.length === 0) {
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('div', { 'style': 'font-size: 2rem; margin-bottom: 0.5rem;' }, '🔌'),
E('p', {}, _('No bouncers registered'))
]);
}
var rows = this.bouncers.map(function(b) {
var rows = bouncers.map(function(b) {
var isValid = b.is_valid !== false;
return E('tr', {}, [
E('td', {}, E('strong', {}, b.name || 'N/A')),
E('td', {}, b.ip_address || 'N/A'),
E('td', {}, b.type || 'N/A'),
E('td', {}, E('span', {
'class': 'cs-action ' + (isValid ? 'ban' : ''),
'style': isValid ? 'background: rgba(0,212,170,0.15); color: var(--cs-accent-green)' : ''
}, isValid ? 'Valid' : 'Invalid')),
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatRelativeTime(b.last_pull)))
E('td', {}, E('span', {
'class': 'cyber-badge ' + (isValid ? 'cyber-badge--success' : 'cyber-badge--danger')
}, isValid ? _('Valid') : _('Invalid'))),
E('td', {}, E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9em;' }, self.csApi.formatRelativeTime(b.last_pull)))
]);
});
return E('table', { 'class': 'cs-table' }, [
return E('table', { 'class': 'cyber-table', 'style': 'width: 100%; border-collapse: collapse;' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, 'Name'),
E('th', {}, 'IP Address'),
E('th', {}, 'Type'),
E('th', {}, 'Status'),
E('th', {}, 'Last Pull')
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Name')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('IP Address')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Type')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Status')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Last Pull'))
])),
E('tbody', {}, rows)
E('tbody', { 'style': 'color: var(--cyber-text-primary, #fff);' }, rows.map(function(row) {
row.querySelectorAll('td').forEach(function(td) {
td.style.cssText = 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));';
});
return row;
}))
]);
},
renderMachinesTable: function() {
var self = this;
if (!Array.isArray(this.machines) || this.machines.length === 0) {
return E('div', { 'class': 'cs-empty' }, [
E('div', { 'class': 'cs-empty-icon' }, '🖥️'),
E('p', {}, 'No machines registered')
var machines = this.machines;
// Handle response structure: may be { machines: [...] } or direct array
if (machines && machines.machines) {
machines = machines.machines;
}
if (!Array.isArray(machines) || machines.length === 0) {
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('div', { 'style': 'font-size: 2rem; margin-bottom: 0.5rem;' }, '🖥️'),
E('p', {}, _('No machines registered'))
]);
}
var rows = this.machines.map(function(m) {
var rows = machines.map(function(m) {
var isValid = m.is_validated !== false;
return E('tr', {}, [
E('td', {}, E('strong', {}, m.machineId || 'N/A')),
E('td', {}, m.ip_address || 'N/A'),
E('td', {}, E('span', {
'class': 'cs-action',
'style': isValid ? 'background: rgba(0,212,170,0.15); color: var(--cs-accent-green)' : 'background: rgba(255,107,107,0.15); color: var(--cs-accent-red)'
}, isValid ? 'Validated' : 'Pending')),
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatRelativeTime(m.last_heartbeat))),
E('td', {}, m.version || 'N/A')
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, E('strong', {}, m.machineId || m.machine_id || 'N/A')),
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, m.ip_address || 'N/A'),
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, E('span', {
'class': 'cyber-badge ' + (isValid ? 'cyber-badge--success' : 'cyber-badge--warning')
}, isValid ? _('Validated') : _('Pending'))),
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05)); color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9em;' }, self.csApi.formatRelativeTime(m.last_heartbeat)),
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, m.version || 'N/A')
]);
});
return E('table', { 'class': 'cs-table' }, [
return E('table', { 'class': 'cyber-table', 'style': 'width: 100%; border-collapse: collapse; color: var(--cyber-text-primary, #fff);' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, 'Machine ID'),
E('th', {}, 'IP Address'),
E('th', {}, 'Status'),
E('th', {}, 'Last Heartbeat'),
E('th', {}, 'Version')
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Machine ID')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('IP Address')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Status')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Last Heartbeat')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Version'))
])),
E('tbody', {}, rows)
]);
@ -148,18 +157,18 @@ return view.extend({
renderHubStats: function() {
var hub = this.hub;
if (!hub || typeof hub !== 'object') {
return E('div', { 'class': 'cs-empty' }, [
E('p', {}, 'Hub data not available')
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('p', {}, _('Hub data not available'))
]);
}
var collections = hub.collections || [];
var parsers = hub.parsers || [];
var scenarios = hub.scenarios || [];
var postoverflows = hub.postoverflows || [];
var countInstalled = function(items) {
if (!Array.isArray(items)) return 0;
// Check for status === 'enabled' or if local_version exists (means installed)
@ -167,85 +176,82 @@ return view.extend({
return i.status === 'enabled' || i.local_version;
}).length;
};
return E('div', { 'class': 'cs-stats-grid' }, [
E('div', { 'class': 'cs-stat-card' }, [
E('div', { 'class': 'cs-stat-label' }, 'Collections'),
E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(collections))),
E('div', { 'class': 'cs-stat-trend' }, 'installed')
]),
E('div', { 'class': 'cs-stat-card' }, [
E('div', { 'class': 'cs-stat-label' }, 'Parsers'),
E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(parsers))),
E('div', { 'class': 'cs-stat-trend' }, 'installed')
]),
E('div', { 'class': 'cs-stat-card' }, [
E('div', { 'class': 'cs-stat-label' }, 'Scenarios'),
E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(scenarios))),
E('div', { 'class': 'cs-stat-trend' }, 'installed')
]),
E('div', { 'class': 'cs-stat-card' }, [
E('div', { 'class': 'cs-stat-label' }, 'Postoverflows'),
E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(postoverflows))),
E('div', { 'class': 'cs-stat-trend' }, 'installed')
])
]);
var statCards = [
{ label: _('Collections'), count: countInstalled(collections), icon: '📦' },
{ label: _('Parsers'), count: countInstalled(parsers), icon: '📝' },
{ label: _('Scenarios'), count: countInstalled(scenarios), icon: '🎯' },
{ label: _('Postoverflows'), count: countInstalled(postoverflows), icon: '🔄' }
];
return E('div', { 'class': 'cyber-card-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;' },
statCards.map(function(stat) {
return E('div', { 'class': 'cyber-card cyber-card--compact', 'style': 'text-align: center;' }, [
E('div', { 'class': 'cyber-card-body' }, [
E('div', { 'style': 'font-size: 1.5rem; margin-bottom: 0.5rem;' }, stat.icon),
E('div', { 'style': 'font-size: 2rem; font-weight: 700; color: var(--cyber-success, #00d4aa);' }, String(stat.count)),
E('div', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.85rem; margin-top: 0.25rem;' }, stat.label),
E('div', { 'style': 'color: var(--cyber-text-muted, #666); font-size: 0.75rem;' }, _('installed'))
])
]);
})
);
},
renderCollectionsList: function() {
var collections = this.hub?.collections || [];
var collections = this.hub && this.hub.collections ? this.hub.collections : [];
if (!Array.isArray(collections) || collections.length === 0) {
return E('div', { 'class': 'cs-empty' }, [
E('p', {}, 'No collections data')
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('p', {}, _('No collections data'))
]);
}
var installed = collections.filter(function(c) {
return c.status === 'enabled' || c.local_version;
});
var items = installed.slice(0, 15).map(function(c) {
return E('div', { 'class': 'cs-metric-item' }, [
E('span', { 'class': 'cs-metric-name' }, c.name || 'N/A'),
E('span', {
'class': 'cs-scenario',
'style': c.up_to_date ? '' : 'background: rgba(255,169,77,0.15); color: var(--cs-accent-orange)'
}, c.up_to_date ? c.local_version || 'installed' : 'update available')
return E('div', { 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [
E('span', { 'style': 'color: var(--cyber-text-primary, #fff);' }, c.name || 'N/A'),
E('span', {
'class': 'cyber-badge ' + (c.up_to_date !== false ? 'cyber-badge--success' : 'cyber-badge--warning'),
'style': 'font-size: 0.75rem;'
}, c.up_to_date !== false ? (c.local_version || _('installed')) : _('update available'))
]);
});
return E('div', { 'class': 'cs-metric-list' }, items);
return E('div', { 'class': 'cyber-collections-list' }, items);
},
renderAcquisitionMetrics: function() {
var metrics = this.metrics;
if (!metrics || !metrics.acquisition) {
return E('div', { 'class': 'cs-empty' }, [
E('p', {}, 'Acquisition metrics not available')
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('p', {}, _('Acquisition metrics not available'))
]);
}
var acquisition = metrics.acquisition;
var items = [];
Object.entries(acquisition).forEach(function(entry) {
var source = entry[0];
var data = entry[1];
items.push(E('div', { 'class': 'cs-metric-item', 'style': 'flex-direction: column; align-items: flex-start; gap: 8px' }, [
E('strong', { 'style': 'font-size: 12px' }, source),
E('div', { 'style': 'display: flex; gap: 16px; font-size: 11px; color: var(--cs-text-muted)' }, [
E('span', {}, 'Read: ' + (data.lines_read || 0)),
E('span', {}, 'Parsed: ' + (data.lines_parsed || 0)),
E('span', {}, 'Unparsed: ' + (data.lines_unparsed || 0)),
E('span', {}, 'Buckets: ' + (data.lines_poured_to_bucket || 0))
items.push(E('div', { 'style': 'padding: 0.75rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [
E('strong', { 'style': 'font-size: 0.85rem; color: var(--cyber-text-primary, #fff); display: block; margin-bottom: 0.25rem;' }, source),
E('div', { 'style': 'display: flex; gap: 1rem; flex-wrap: wrap; font-size: 0.8rem; color: var(--cyber-text-secondary, #a0a0b0);' }, [
E('span', {}, _('Read: ') + (data.lines_read || 0)),
E('span', {}, _('Parsed: ') + (data.lines_parsed || 0)),
E('span', {}, _('Unparsed: ') + (data.lines_unparsed || 0)),
E('span', {}, _('Buckets: ') + (data.lines_poured_to_bucket || 0))
])
]));
});
return E('div', { 'class': 'cs-metric-list' }, items);
return E('div', { 'class': 'cyber-acquisition-list' }, items);
},
renderMetricsConfig: function(metricsConfig) {
@ -253,30 +259,30 @@ return view.extend({
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'),
return E('div', { 'class': 'cyber-card', 'style': 'margin-bottom: 1.5rem;' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '⚙️'),
_('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;'
'class': 'cyber-badge ' + (enabled ? 'cyber-badge--success' : 'cyber-badge--danger')
}, 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': 'cyber-card-body' }, [
E('div', { 'style': 'margin-bottom: 1rem;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [
E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, _('Metrics Export Status')),
E('span', { 'style': 'color: var(--cyber-text-primary, #fff);' }, 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': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [
E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, _('Prometheus Endpoint')),
E('code', { 'style': 'font-size: 0.85rem; color: var(--cyber-accent-primary, #667eea);' }, prometheusEndpoint)
])
]),
E('div', { 'style': 'margin-top: 16px; display: flex; gap: 12px; align-items: center;' }, [
E('div', { 'style': 'display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;' }, [
E('button', {
'class': 'cbi-button ' + (enabled ? 'cbi-button-negative' : 'cbi-button-positive'),
'class': 'cyber-btn ' + (enabled ? 'cyber-btn--danger' : 'cyber-btn--success'),
'click': function() {
var newState = !enabled;
ui.showModal(_('Updating Metrics Configuration...'), [
@ -296,14 +302,16 @@ return view.extend({
});
}
}, enabled ? _('Disable Metrics Export') : _('Enable Metrics Export')),
E('span', { 'style': 'color: var(--cs-text-muted); font-size: 13px;' },
E('span', { 'style': 'color: var(--cyber-text-muted, #666); font-size: 0.85rem;' },
_('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)
E('div', { 'class': 'cyber-card cyber-card--info cyber-card--compact', 'style': 'margin-top: 1rem;' }, [
E('div', { 'class': 'cyber-card-body' }, [
E('p', { 'style': 'margin: 0 0 0.5rem 0; color: var(--cyber-text-primary, #fff); font-weight: 600;' }, _('About Metrics Export')),
E('p', { 'style': 'margin: 0; color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9rem;' }, [
_('When enabled, CrowdSec exports Prometheus-compatible metrics that can be scraped by monitoring tools. Access metrics at: '),
E('code', { 'style': 'color: var(--cyber-accent-primary, #667eea);' }, prometheusEndpoint)
])
])
])
])
@ -313,75 +321,105 @@ return view.extend({
render: function(data) {
var self = this;
// Initialize theme
Theme.init();
this.metrics = data.metrics || {};
this.bouncers = data.bouncers || [];
this.machines = data.machines || {};
this.hub = data.hub || {};
var metricsConfig = data.metricsConfig || {};
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
var view = E('div', { 'class': 'cyber-container crowdsec-metrics' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
// Page Header
E('div', { 'style': 'margin-bottom: 1.5rem;' }, [
E('h2', { 'style': 'color: var(--cyber-text-primary, #fff); margin: 0 0 0.5rem 0;' }, _('CrowdSec Metrics')),
E('p', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0); margin: 0;' }, _('Detailed metrics and statistics from CrowdSec engine'))
]),
// 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' },
'🎯 Hub Components'),
this.renderHubStats()
E('div', { 'class': 'cyber-card', 'style': 'margin-bottom: 1.5rem;' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '🎯'),
_('Hub Components')
])
]),
E('div', { 'class': 'cyber-card-body' }, this.renderHubStats())
]),
// Grid of cards
E('div', { 'class': 'cs-metrics-grid' }, [
E('div', { 'class': 'cyber-card-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem;' }, [
// Bouncers
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, '🔒 Registered Bouncers')
E('div', { 'class': 'cyber-card' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '🔒'),
_('Registered Bouncers')
])
]),
E('div', { 'class': 'cs-card-body no-padding' }, this.renderBouncersTable())
E('div', { 'class': 'cyber-card-body' }, this.renderBouncersTable())
]),
// Machines
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, '🖥️ Registered Machines')
E('div', { 'class': 'cyber-card' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '🖥️'),
_('Registered Machines')
])
]),
E('div', { 'class': 'cs-card-body no-padding' }, this.renderMachinesTable())
E('div', { 'class': 'cyber-card-body' }, this.renderMachinesTable())
]),
// Collections
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, '📦 Installed Collections')
E('div', { 'class': 'cyber-card' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '📦'),
_('Installed Collections')
])
]),
E('div', { 'class': 'cs-card-body' }, this.renderCollectionsList())
E('div', { 'class': 'cyber-card-body' }, this.renderCollectionsList())
]),
// Acquisition
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, '📊 Acquisition Sources')
E('div', { 'class': 'cyber-card' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '📊'),
_('Acquisition Sources')
])
]),
E('div', { 'class': 'cs-card-body' }, this.renderAcquisitionMetrics())
E('div', { 'class': 'cyber-card-body' }, this.renderAcquisitionMetrics())
])
]),
// Raw metrics sections
E('div', { 'class': 'cs-card', 'style': 'margin-top: 24px' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, '📈 Raw Prometheus Metrics')
E('div', { 'class': 'cyber-card', 'style': 'margin-top: 1.5rem;' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '📈'),
_('Raw Prometheus Metrics')
])
]),
E('div', { 'class': 'cs-card-body' }, [
E('div', { 'class': 'cs-metrics-grid' }, [
this.renderMetricSection('Parsers', this.metrics.parsers),
this.renderMetricSection('Scenarios', this.metrics.scenarios),
this.renderMetricSection('Buckets', this.metrics.buckets),
this.renderMetricSection('LAPI', this.metrics.lapi),
this.renderMetricSection('Decisions', this.metrics.decisions)
E('div', { 'class': 'cyber-card-body' }, [
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;' }, [
this.renderMetricSection(_('Parsers'), this.metrics.parsers),
this.renderMetricSection(_('Scenarios'), this.metrics.scenarios),
this.renderMetricSection(_('Buckets'), this.metrics.buckets),
this.renderMetricSection(_('LAPI'), this.metrics.lapi),
this.renderMetricSection(_('Decisions'), this.metrics.decisions)
].filter(Boolean))
])
])
]);
// Setup polling (every 60 seconds for metrics)
poll.add(function() {
return Promise.all([
@ -392,10 +430,9 @@ return view.extend({
self.metrics = results[0];
self.bouncers = results[1];
self.machines = results[2];
// Note: Could update view here if needed
});
}, 60);
return view;
},

View File

@ -1,5 +1,6 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require ui';
'require form';
'require rpc';
@ -14,6 +15,8 @@ return view.extend({
// Step 1 data
crowdsecRunning: false,
lapiAvailable: false,
lapiRepairing: false,
lapiRepairAttempted: false,
// Step 2 data
hubUpdating: false,
@ -56,6 +59,39 @@ return view.extend({
this.wizardData.crowdsecRunning = status && status.crowdsec === 'running';
this.wizardData.lapiAvailable = status && status.lapi_status === 'available';
// Auto-repair LAPI if CrowdSec is running but LAPI is not available
if (this.wizardData.crowdsecRunning && !this.wizardData.lapiAvailable && !this.wizardData.lapiRepairAttempted) {
console.log('[Wizard] LAPI unavailable, triggering auto-repair...');
this.wizardData.lapiRepairing = true;
this.wizardData.lapiRepairAttempted = true;
return API.repairLapi().then(L.bind(function(repairResult) {
console.log('[Wizard] LAPI repair result:', repairResult);
this.wizardData.lapiRepairing = false;
if (repairResult && repairResult.success) {
ui.addNotification(null, E('p', _('LAPI auto-repaired successfully')), 'success');
// Re-fetch status after repair
return API.getStatus().then(L.bind(function(newStatus) {
this.wizardData.crowdsecRunning = newStatus && newStatus.crowdsec === 'running';
this.wizardData.lapiAvailable = newStatus && newStatus.lapi_status === 'available';
return {
status: newStatus,
wizardNeeded: wizardNeeded,
repaired: true
};
}, this));
} else {
console.log('[Wizard] LAPI repair failed:', repairResult);
return {
status: status,
wizardNeeded: wizardNeeded,
repairFailed: true
};
}
}, this));
}
return {
status: status,
wizardNeeded: wizardNeeded
@ -142,7 +178,16 @@ return view.extend({
console.log('[Wizard] status:', status);
var crowdsecRunning = status && status.crowdsec === 'running';
var lapiAvailable = status && status.lapi_status === 'available';
console.log('[Wizard] crowdsecRunning:', crowdsecRunning, 'lapiAvailable:', lapiAvailable);
var lapiRepairing = this.wizardData.lapiRepairing;
var repaired = data && data.repaired;
var repairFailed = data && data.repairFailed;
console.log('[Wizard] crowdsecRunning:', crowdsecRunning, 'lapiAvailable:', lapiAvailable, 'repairing:', lapiRepairing);
// Determine LAPI status display
var lapiStatusText = lapiAvailable ? _('AVAILABLE') : (lapiRepairing ? _('REPAIRING...') : _('UNAVAILABLE'));
var lapiStatusClass = lapiAvailable ? 'success' : (lapiRepairing ? 'warning' : 'error');
var lapiIconClass = lapiAvailable ? ' success' : (lapiRepairing ? ' warning' : ' error');
var lapiIcon = lapiAvailable ? '✓' : (lapiRepairing ? '⟳' : '✗');
return E('div', { 'class': 'wizard-step' }, [
E('h2', {}, _('Welcome to CrowdSec Setup')),
@ -158,14 +203,31 @@ return view.extend({
crowdsecRunning ? _('RUNNING') : _('STOPPED'))
]),
E('div', { 'class': 'check-item' }, [
E('span', { 'class': 'check-icon' + (lapiAvailable ? ' success' : ' error') },
lapiAvailable ? '✓' : '✗'),
E('span', { 'class': 'check-icon' + lapiIconClass + (lapiRepairing ? ' spinning' : '') },
lapiIcon),
E('span', {}, _('Local API (LAPI)')),
E('span', { 'class': 'badge badge-' + (lapiAvailable ? 'success' : 'error') },
lapiAvailable ? _('AVAILABLE') : _('UNAVAILABLE'))
E('span', { 'class': 'badge badge-' + lapiStatusClass },
lapiStatusText)
])
]),
// Repair status message
repaired ? E('div', { 'class': 'success-message', 'style': 'margin: 16px 0; padding: 12px; background: rgba(34, 197, 94, 0.15); border-radius: 8px; color: #16a34a;' }, [
E('span', { 'style': 'margin-right: 8px;' }, '✓'),
_('LAPI was automatically repaired!')
]) : E([]),
// Manual repair button if auto-repair failed
(repairFailed || (!lapiAvailable && !lapiRepairing && this.wizardData.lapiRepairAttempted)) ?
E('div', { 'style': 'margin: 16px 0; padding: 16px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; border: 1px solid rgba(239, 68, 68, 0.3);' }, [
E('p', { 'style': 'margin: 0 0 12px 0; color: #dc2626;' },
_('LAPI auto-repair failed. You can try manual repair or check the CrowdSec logs.')),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': L.bind(this.handleManualRepair, this)
}, _('🔧 Retry Repair'))
]) : E([]),
// Info box
E('div', { 'class': 'info-box' }, [
E('h4', {}, _('What will be configured:')),
@ -188,7 +250,7 @@ return view.extend({
}, _('Cancel')),
E('button', {
'class': 'cbi-button cbi-button-positive',
'disabled': (!crowdsecRunning || !lapiAvailable) ? true : null,
'disabled': (!crowdsecRunning || !lapiAvailable || lapiRepairing) ? true : null,
'click': L.bind(function(ev) {
console.log('[Wizard] Next button clicked!');
ev.preventDefault();
@ -780,6 +842,37 @@ return view.extend({
}, this));
},
handleManualRepair: function() {
console.log('[Wizard] Manual repair triggered');
this.wizardData.lapiRepairing = true;
this.wizardData.lapiRepairAttempted = false; // Reset to allow retry
this.refreshView();
return API.repairLapi().then(L.bind(function(result) {
console.log('[Wizard] Manual repair result:', result);
this.wizardData.lapiRepairing = false;
this.wizardData.lapiRepairAttempted = true;
if (result && result.success) {
ui.addNotification(null, E('p', _('LAPI repaired successfully: ') + (result.steps || '')), 'success');
// Re-check status
return API.getStatus().then(L.bind(function(status) {
this.wizardData.crowdsecRunning = status && status.crowdsec === 'running';
this.wizardData.lapiAvailable = status && status.lapi_status === 'available';
this.refreshView();
}, this));
} else {
ui.addNotification(null, E('p', _('LAPI repair failed: ') + (result.error || result.errors || 'Unknown error')), 'error');
this.refreshView();
}
}, this)).catch(L.bind(function(err) {
console.error('[Wizard] Manual repair error:', err);
this.wizardData.lapiRepairing = false;
ui.addNotification(null, E('p', _('LAPI repair failed: ') + err.message), 'error');
this.refreshView();
}, this));
},
handleSaveAndApply: null,
handleSave: null,
handleReset: null

View File

@ -430,18 +430,30 @@ register_bouncer() {
return
fi
# Generate API key
# Check if bouncer already exists
local exists=0
if $CSCLI bouncers list -o json 2>/dev/null | grep -q "\"name\":\"$bouncer_name\""; then
exists=1
fi
local api_key
if [ "$exists" = "1" ]; then
# Delete existing bouncer and re-register to get new API key
$CSCLI bouncers delete "$bouncer_name" >/dev/null 2>&1
secubox_log "Deleted existing bouncer: $bouncer_name"
fi
# Generate API key
api_key=$($CSCLI bouncers add "$bouncer_name" -o raw 2>&1)
if [ -n "$api_key" ] && [ "${#api_key}" -gt 10 ]; then
if [ -n "$api_key" ] && [ "${#api_key}" -gt 10 ] && ! echo "$api_key" | grep -qi "error"; 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'"
json_add_string "error" "Failed to register bouncer '$bouncer_name': $api_key"
fi
json_dump
@ -681,9 +693,9 @@ update_firewall_bouncer_config() {
json_dump
return
fi
uci set "crowdsec.bouncer.$key=$value"
uci set "crowdsec.@bouncer[0].$key=$value"
;;
api_url|update_frequency|deny_action|log_level)
api_url|update_frequency|deny_action|log_level|api_key)
# String values
if [ -z "$value" ]; then
json_add_boolean "success" 0
@ -691,7 +703,7 @@ update_firewall_bouncer_config() {
json_dump
return
fi
uci set "crowdsec.bouncer.$key=$value"
uci set "crowdsec.@bouncer[0].$key=$value"
;;
*)
json_add_boolean "success" 0
@ -830,10 +842,99 @@ get_wizard_state() {
json_dump
}
# Repair LAPI - auto-fix common configuration issues
repair_lapi() {
json_init
local steps_done=""
local errors=""
secubox_log "Starting LAPI repair..."
# Step 1: Create data directory
if [ ! -d "/srv/crowdsec/data" ]; then
mkdir -p /srv/crowdsec/data 2>/dev/null
if [ -d "/srv/crowdsec/data" ]; then
steps_done="${steps_done}Created /srv/crowdsec/data; "
else
errors="${errors}Failed to create data directory; "
fi
fi
# Step 2: Fix config.yaml - ensure data_dir and db_path are set
local config_file="/etc/crowdsec/config.yaml"
if [ -f "$config_file" ]; then
# Check if data_dir is empty or missing
local current_data_dir=$(grep "^ data_dir:" "$config_file" | awk '{print $2}')
if [ -z "$current_data_dir" ] || [ "$current_data_dir" = "" ]; then
sed -i 's|^ data_dir:.*| data_dir: /srv/crowdsec/data/|' "$config_file"
steps_done="${steps_done}Fixed data_dir in config; "
fi
# Check if db_path is empty or missing
local current_db_path=$(grep "^ db_path:" "$config_file" | awk '{print $2}')
if [ -z "$current_db_path" ] || [ "$current_db_path" = "" ]; then
sed -i 's|^ db_path:.*| db_path: /srv/crowdsec/data/crowdsec.db|' "$config_file"
steps_done="${steps_done}Fixed db_path in config; "
fi
else
errors="${errors}Config file not found; "
fi
# Step 3: Restart CrowdSec to apply config changes
if /etc/init.d/crowdsec restart >/dev/null 2>&1; then
steps_done="${steps_done}Restarted CrowdSec; "
sleep 2
else
errors="${errors}Failed to restart CrowdSec; "
fi
# Step 4: Re-register local machine if needed
if [ -x "$CSCLI" ]; then
# Check if machine is registered and working
if ! $CSCLI machines list >/dev/null 2>&1; then
# Force re-register
if $CSCLI machines add localhost --auto --force >/dev/null 2>&1; then
steps_done="${steps_done}Re-registered localhost machine; "
# Restart again to apply new credentials
/etc/init.d/crowdsec restart >/dev/null 2>&1
sleep 2
else
errors="${errors}Failed to register machine; "
fi
fi
fi
# Step 5: Verify LAPI is now working
local lapi_ok=0
if [ -x "$CSCLI" ]; then
if $CSCLI lapi status >/dev/null 2>&1; then
lapi_ok=1
steps_done="${steps_done}LAPI verified working"
else
errors="${errors}LAPI still not responding"
fi
fi
if [ "$lapi_ok" = "1" ]; then
json_add_boolean "success" 1
json_add_string "message" "LAPI repaired successfully"
json_add_string "steps" "$steps_done"
secubox_log "LAPI repair completed: $steps_done"
else
json_add_boolean "success" 0
json_add_string "error" "LAPI repair failed"
json_add_string "steps" "$steps_done"
json_add_string "errors" "$errors"
secubox_log "LAPI repair failed: $errors"
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":{},"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":{}}'
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":{},"repair_lapi":{}}'
;;
call)
case "$2" in
@ -944,6 +1045,9 @@ case "$1" in
wizard_state)
get_wizard_state
;;
repair_lapi)
repair_lapi
;;
*)
echo '{"error": "Unknown method"}'
;;

View File

@ -39,7 +39,8 @@
"register_bouncer",
"delete_bouncer",
"control_firewall_bouncer",
"update_firewall_bouncer_config"
"update_firewall_bouncer_config",
"repair_lapi"
]
},
"uci": [ "crowdsec-dashboard" ]