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:
parent
b7fb268f71
commit
4078b4d7a4
@ -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,
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"}'
|
||||
;;
|
||||
|
||||
@ -39,7 +39,8 @@
|
||||
"register_bouncer",
|
||||
"delete_bouncer",
|
||||
"control_firewall_bouncer",
|
||||
"update_firewall_bouncer_config"
|
||||
"update_firewall_bouncer_config",
|
||||
"repair_lapi"
|
||||
]
|
||||
},
|
||||
"uci": [ "crowdsec-dashboard" ]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user