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: { }
|
expect: { }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var callRepairLapi = rpc.declare({
|
||||||
|
object: 'luci.crowdsec-dashboard',
|
||||||
|
method: 'repair_lapi',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
function formatDuration(seconds) {
|
function formatDuration(seconds) {
|
||||||
if (!seconds) return 'N/A';
|
if (!seconds) return 'N/A';
|
||||||
if (seconds < 60) return seconds + 's';
|
if (seconds < 60) return seconds + 's';
|
||||||
@ -310,6 +316,7 @@ return baseclass.extend({
|
|||||||
// Wizard Methods
|
// Wizard Methods
|
||||||
checkWizardNeeded: callCheckWizardNeeded,
|
checkWizardNeeded: callCheckWizardNeeded,
|
||||||
getWizardState: callWizardState,
|
getWizardState: callWizardState,
|
||||||
|
repairLapi: callRepairLapi,
|
||||||
|
|
||||||
formatDuration: formatDuration,
|
formatDuration: formatDuration,
|
||||||
formatDate: formatDate,
|
formatDate: formatDate,
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
title: _('Metrics'),
|
title: _('Metrics'),
|
||||||
|
|
||||||
csApi: null,
|
csApi: null,
|
||||||
metrics: {},
|
metrics: {},
|
||||||
bouncers: [],
|
bouncers: [],
|
||||||
@ -22,11 +22,6 @@ return view.extend({
|
|||||||
hub: {},
|
hub: {},
|
||||||
|
|
||||||
load: function() {
|
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;
|
this.csApi = api;
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
@ -50,97 +45,111 @@ return view.extend({
|
|||||||
if (!data || typeof data !== 'object') {
|
if (!data || typeof data !== 'object') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var entries = Object.entries(data);
|
var entries = Object.entries(data);
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = entries.map(function(entry) {
|
var items = entries.map(function(entry) {
|
||||||
var value = entry[1];
|
var value = entry[1];
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
value = JSON.stringify(value);
|
value = JSON.stringify(value);
|
||||||
}
|
}
|
||||||
return E('div', { 'class': 'cs-metric-item' }, [
|
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('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
E('span', { 'class': 'cyber-metric-name', 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, entry[0]),
|
||||||
E('span', { 'class': 'cs-metric-name' }, entry[0]),
|
E('span', { 'class': 'cyber-metric-value', 'style': 'color: var(--cyber-text-primary, #fff); font-weight: 500;' }, String(value))
|
||||||
E('span', { 'class': 'cs-metric-value' }, String(value))
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return E('div', { 'class': 'cs-metric-section' }, [
|
return E('div', { 'class': 'cyber-metric-section', 'style': 'margin-bottom: 1rem;' }, [
|
||||||
E('div', { 'class': 'cs-metric-section-title' }, title),
|
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': 'cs-metric-list' }, items)
|
E('div', { 'class': 'cyber-metric-list' }, items)
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderBouncersTable: function() {
|
renderBouncersTable: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var bouncers = this.bouncers;
|
||||||
if (!Array.isArray(this.bouncers) || this.bouncers.length === 0) {
|
|
||||||
return E('div', { 'class': 'cs-empty' }, [
|
// Handle response structure: may be { bouncers: [...] } or direct array
|
||||||
E('div', { 'class': 'cs-empty-icon' }, '🔌'),
|
if (bouncers && bouncers.bouncers) {
|
||||||
E('p', {}, 'No bouncers registered')
|
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;
|
var isValid = b.is_valid !== false;
|
||||||
return E('tr', {}, [
|
return E('tr', {}, [
|
||||||
E('td', {}, E('strong', {}, b.name || 'N/A')),
|
E('td', {}, E('strong', {}, b.name || 'N/A')),
|
||||||
E('td', {}, b.ip_address || 'N/A'),
|
E('td', {}, b.ip_address || 'N/A'),
|
||||||
E('td', {}, b.type || 'N/A'),
|
E('td', {}, b.type || 'N/A'),
|
||||||
E('td', {}, E('span', {
|
E('td', {}, E('span', {
|
||||||
'class': 'cs-action ' + (isValid ? 'ban' : ''),
|
'class': 'cyber-badge ' + (isValid ? 'cyber-badge--success' : 'cyber-badge--danger')
|
||||||
'style': isValid ? 'background: rgba(0,212,170,0.15); color: var(--cs-accent-green)' : ''
|
}, isValid ? _('Valid') : _('Invalid'))),
|
||||||
}, isValid ? 'Valid' : 'Invalid')),
|
E('td', {}, E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9em;' }, self.csApi.formatRelativeTime(b.last_pull)))
|
||||||
E('td', {}, E('span', { 'class': 'cs-time' }, 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('thead', {}, E('tr', {}, [
|
||||||
E('th', {}, '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;' }, _('Name')),
|
||||||
E('th', {}, '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;' }, _('IP Address')),
|
||||||
E('th', {}, '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;' }, _('Type')),
|
||||||
E('th', {}, '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;' }, _('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;' }, _('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() {
|
renderMachinesTable: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var machines = this.machines;
|
||||||
if (!Array.isArray(this.machines) || this.machines.length === 0) {
|
|
||||||
return E('div', { 'class': 'cs-empty' }, [
|
// Handle response structure: may be { machines: [...] } or direct array
|
||||||
E('div', { 'class': 'cs-empty-icon' }, '🖥️'),
|
if (machines && machines.machines) {
|
||||||
E('p', {}, 'No machines registered')
|
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;
|
var isValid = m.is_validated !== false;
|
||||||
return E('tr', {}, [
|
return E('tr', {}, [
|
||||||
E('td', {}, E('strong', {}, m.machineId || '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', {}, 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));' }, m.ip_address || 'N/A'),
|
||||||
E('td', {}, E('span', {
|
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, E('span', {
|
||||||
'class': 'cs-action',
|
'class': 'cyber-badge ' + (isValid ? 'cyber-badge--success' : 'cyber-badge--warning')
|
||||||
'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'))),
|
||||||
}, 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', {}, E('span', { 'class': 'cs-time' }, 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')
|
||||||
E('td', {}, 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('thead', {}, E('tr', {}, [
|
||||||
E('th', {}, '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;' }, _('Machine ID')),
|
||||||
E('th', {}, '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;' }, _('IP Address')),
|
||||||
E('th', {}, '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;' }, _('Status')),
|
||||||
E('th', {}, '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;' }, _('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;' }, _('Version'))
|
||||||
])),
|
])),
|
||||||
E('tbody', {}, rows)
|
E('tbody', {}, rows)
|
||||||
]);
|
]);
|
||||||
@ -148,18 +157,18 @@ return view.extend({
|
|||||||
|
|
||||||
renderHubStats: function() {
|
renderHubStats: function() {
|
||||||
var hub = this.hub;
|
var hub = this.hub;
|
||||||
|
|
||||||
if (!hub || typeof hub !== 'object') {
|
if (!hub || typeof hub !== 'object') {
|
||||||
return E('div', { 'class': 'cs-empty' }, [
|
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
|
||||||
E('p', {}, 'Hub data not available')
|
E('p', {}, _('Hub data not available'))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var collections = hub.collections || [];
|
var collections = hub.collections || [];
|
||||||
var parsers = hub.parsers || [];
|
var parsers = hub.parsers || [];
|
||||||
var scenarios = hub.scenarios || [];
|
var scenarios = hub.scenarios || [];
|
||||||
var postoverflows = hub.postoverflows || [];
|
var postoverflows = hub.postoverflows || [];
|
||||||
|
|
||||||
var countInstalled = function(items) {
|
var countInstalled = function(items) {
|
||||||
if (!Array.isArray(items)) return 0;
|
if (!Array.isArray(items)) return 0;
|
||||||
// Check for status === 'enabled' or if local_version exists (means installed)
|
// 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;
|
return i.status === 'enabled' || i.local_version;
|
||||||
}).length;
|
}).length;
|
||||||
};
|
};
|
||||||
|
|
||||||
return E('div', { 'class': 'cs-stats-grid' }, [
|
var statCards = [
|
||||||
E('div', { 'class': 'cs-stat-card' }, [
|
{ label: _('Collections'), count: countInstalled(collections), icon: '📦' },
|
||||||
E('div', { 'class': 'cs-stat-label' }, 'Collections'),
|
{ label: _('Parsers'), count: countInstalled(parsers), icon: '📝' },
|
||||||
E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(collections))),
|
{ label: _('Scenarios'), count: countInstalled(scenarios), icon: '🎯' },
|
||||||
E('div', { 'class': 'cs-stat-trend' }, 'installed')
|
{ label: _('Postoverflows'), count: countInstalled(postoverflows), icon: '🔄' }
|
||||||
]),
|
];
|
||||||
E('div', { 'class': 'cs-stat-card' }, [
|
|
||||||
E('div', { 'class': 'cs-stat-label' }, 'Parsers'),
|
return E('div', { 'class': 'cyber-card-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;' },
|
||||||
E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(parsers))),
|
statCards.map(function(stat) {
|
||||||
E('div', { 'class': 'cs-stat-trend' }, 'installed')
|
return E('div', { 'class': 'cyber-card cyber-card--compact', 'style': 'text-align: center;' }, [
|
||||||
]),
|
E('div', { 'class': 'cyber-card-body' }, [
|
||||||
E('div', { 'class': 'cs-stat-card' }, [
|
E('div', { 'style': 'font-size: 1.5rem; margin-bottom: 0.5rem;' }, stat.icon),
|
||||||
E('div', { 'class': 'cs-stat-label' }, 'Scenarios'),
|
E('div', { 'style': 'font-size: 2rem; font-weight: 700; color: var(--cyber-success, #00d4aa);' }, String(stat.count)),
|
||||||
E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(scenarios))),
|
E('div', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.85rem; margin-top: 0.25rem;' }, stat.label),
|
||||||
E('div', { 'class': 'cs-stat-trend' }, 'installed')
|
E('div', { 'style': 'color: var(--cyber-text-muted, #666); font-size: 0.75rem;' }, _('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')
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderCollectionsList: function() {
|
renderCollectionsList: function() {
|
||||||
var collections = this.hub?.collections || [];
|
var collections = this.hub && this.hub.collections ? this.hub.collections : [];
|
||||||
|
|
||||||
if (!Array.isArray(collections) || collections.length === 0) {
|
if (!Array.isArray(collections) || collections.length === 0) {
|
||||||
return E('div', { 'class': 'cs-empty' }, [
|
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
|
||||||
E('p', {}, 'No collections data')
|
E('p', {}, _('No collections data'))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var installed = collections.filter(function(c) {
|
var installed = collections.filter(function(c) {
|
||||||
return c.status === 'enabled' || c.local_version;
|
return c.status === 'enabled' || c.local_version;
|
||||||
});
|
});
|
||||||
|
|
||||||
var items = installed.slice(0, 15).map(function(c) {
|
var items = installed.slice(0, 15).map(function(c) {
|
||||||
return E('div', { 'class': 'cs-metric-item' }, [
|
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', { 'class': 'cs-metric-name' }, c.name || 'N/A'),
|
E('span', { 'style': 'color: var(--cyber-text-primary, #fff);' }, c.name || 'N/A'),
|
||||||
E('span', {
|
E('span', {
|
||||||
'class': 'cs-scenario',
|
'class': 'cyber-badge ' + (c.up_to_date !== false ? 'cyber-badge--success' : 'cyber-badge--warning'),
|
||||||
'style': c.up_to_date ? '' : 'background: rgba(255,169,77,0.15); color: var(--cs-accent-orange)'
|
'style': 'font-size: 0.75rem;'
|
||||||
}, c.up_to_date ? c.local_version || 'installed' : 'update available')
|
}, 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() {
|
renderAcquisitionMetrics: function() {
|
||||||
var metrics = this.metrics;
|
var metrics = this.metrics;
|
||||||
|
|
||||||
if (!metrics || !metrics.acquisition) {
|
if (!metrics || !metrics.acquisition) {
|
||||||
return E('div', { 'class': 'cs-empty' }, [
|
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
|
||||||
E('p', {}, 'Acquisition metrics not available')
|
E('p', {}, _('Acquisition metrics not available'))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var acquisition = metrics.acquisition;
|
var acquisition = metrics.acquisition;
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
Object.entries(acquisition).forEach(function(entry) {
|
Object.entries(acquisition).forEach(function(entry) {
|
||||||
var source = entry[0];
|
var source = entry[0];
|
||||||
var data = entry[1];
|
var data = entry[1];
|
||||||
|
|
||||||
items.push(E('div', { 'class': 'cs-metric-item', 'style': 'flex-direction: column; align-items: flex-start; gap: 8px' }, [
|
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: 12px' }, source),
|
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: 16px; font-size: 11px; color: var(--cs-text-muted)' }, [
|
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', {}, _('Read: ') + (data.lines_read || 0)),
|
||||||
E('span', {}, 'Parsed: ' + (data.lines_parsed || 0)),
|
E('span', {}, _('Parsed: ') + (data.lines_parsed || 0)),
|
||||||
E('span', {}, 'Unparsed: ' + (data.lines_unparsed || 0)),
|
E('span', {}, _('Unparsed: ') + (data.lines_unparsed || 0)),
|
||||||
E('span', {}, 'Buckets: ' + (data.lines_poured_to_bucket || 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) {
|
renderMetricsConfig: function(metricsConfig) {
|
||||||
@ -253,30 +259,30 @@ return view.extend({
|
|||||||
var enabled = metricsConfig && (metricsConfig.metrics_enabled === true || metricsConfig.metrics_enabled === 1);
|
var enabled = metricsConfig && (metricsConfig.metrics_enabled === true || metricsConfig.metrics_enabled === 1);
|
||||||
var prometheusEndpoint = metricsConfig && metricsConfig.prometheus_endpoint || 'http://127.0.0.1:6060/metrics';
|
var prometheusEndpoint = metricsConfig && metricsConfig.prometheus_endpoint || 'http://127.0.0.1:6060/metrics';
|
||||||
|
|
||||||
return E('div', { 'class': 'cs-card', 'style': 'margin-bottom: 24px;' }, [
|
return E('div', { 'class': 'cyber-card', 'style': 'margin-bottom: 1.5rem;' }, [
|
||||||
E('div', { 'class': 'cs-card-header' }, [
|
E('div', { 'class': 'cyber-card-header' }, [
|
||||||
E('div', { 'class': 'cs-card-title' }, '⚙️ Metrics Export Configuration'),
|
E('div', { 'class': 'cyber-card-title' }, [
|
||||||
|
E('span', { 'style': 'margin-right: 0.5rem;' }, '⚙️'),
|
||||||
|
_('Metrics Export Configuration')
|
||||||
|
]),
|
||||||
E('span', {
|
E('span', {
|
||||||
'class': 'cs-action',
|
'class': 'cyber-badge ' + (enabled ? 'cyber-badge--success' : 'cyber-badge--danger')
|
||||||
'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;'
|
|
||||||
}, enabled ? _('Enabled') : _('Disabled'))
|
}, enabled ? _('Enabled') : _('Disabled'))
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'cs-card-body' }, [
|
E('div', { 'class': 'cyber-card-body' }, [
|
||||||
E('div', { 'class': 'cs-metric-list' }, [
|
E('div', { 'style': 'margin-bottom: 1rem;' }, [
|
||||||
E('div', { 'class': 'cs-metric-item' }, [
|
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', { 'class': 'cs-metric-name' }, _('Metrics Export Status')),
|
E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, _('Metrics Export Status')),
|
||||||
E('span', { 'class': 'cs-metric-value' }, enabled ? _('Enabled') : _('Disabled'))
|
E('span', { 'style': 'color: var(--cyber-text-primary, #fff);' }, enabled ? _('Enabled') : _('Disabled'))
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'cs-metric-item' }, [
|
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', { 'class': 'cs-metric-name' }, _('Prometheus Endpoint')),
|
E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, _('Prometheus Endpoint')),
|
||||||
E('code', { 'class': 'cs-metric-value', 'style': 'font-size: 13px;' }, prometheusEndpoint)
|
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', {
|
E('button', {
|
||||||
'class': 'cbi-button ' + (enabled ? 'cbi-button-negative' : 'cbi-button-positive'),
|
'class': 'cyber-btn ' + (enabled ? 'cyber-btn--danger' : 'cyber-btn--success'),
|
||||||
'click': function() {
|
'click': function() {
|
||||||
var newState = !enabled;
|
var newState = !enabled;
|
||||||
ui.showModal(_('Updating Metrics Configuration...'), [
|
ui.showModal(_('Updating Metrics Configuration...'), [
|
||||||
@ -296,14 +302,16 @@ return view.extend({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, enabled ? _('Disable Metrics Export') : _('Enable Metrics Export')),
|
}, 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'))
|
_('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('div', { 'class': 'cyber-card cyber-card--info cyber-card--compact', 'style': 'margin-top: 1rem;' }, [
|
||||||
E('p', { 'style': 'margin: 0 0 8px 0; color: var(--cs-text-primary); font-weight: 600;' }, _('About Metrics Export')),
|
E('div', { 'class': 'cyber-card-body' }, [
|
||||||
E('p', { 'style': 'margin: 0; color: var(--cs-text-secondary); font-size: 14px;' }, [
|
E('p', { 'style': 'margin: 0 0 0.5rem 0; color: var(--cyber-text-primary, #fff); font-weight: 600;' }, _('About Metrics Export')),
|
||||||
_('When enabled, CrowdSec exports Prometheus-compatible metrics that can be scraped by monitoring tools. Access metrics at: '),
|
E('p', { 'style': 'margin: 0; color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9rem;' }, [
|
||||||
E('code', {}, prometheusEndpoint)
|
_('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) {
|
render: function(data) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
// Initialize theme
|
||||||
|
Theme.init();
|
||||||
|
|
||||||
this.metrics = data.metrics || {};
|
this.metrics = data.metrics || {};
|
||||||
this.bouncers = data.bouncers || [];
|
this.bouncers = data.bouncers || [];
|
||||||
this.machines = data.machines || {};
|
this.machines = data.machines || {};
|
||||||
this.hub = data.hub || {};
|
this.hub = data.hub || {};
|
||||||
var metricsConfig = data.metricsConfig || {};
|
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
|
// Metrics Configuration
|
||||||
this.renderMetricsConfig(metricsConfig),
|
this.renderMetricsConfig(metricsConfig),
|
||||||
|
|
||||||
// Hub Stats
|
// Hub Stats
|
||||||
E('div', { 'style': 'margin-bottom: 24px' }, [
|
E('div', { 'class': 'cyber-card', 'style': 'margin-bottom: 1.5rem;' }, [
|
||||||
E('h3', { 'style': 'color: var(--cs-text-primary); margin-bottom: 16px; font-size: 16px' },
|
E('div', { 'class': 'cyber-card-header' }, [
|
||||||
'🎯 Hub Components'),
|
E('div', { 'class': 'cyber-card-title' }, [
|
||||||
this.renderHubStats()
|
E('span', { 'style': 'margin-right: 0.5rem;' }, '🎯'),
|
||||||
|
_('Hub Components')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-card-body' }, this.renderHubStats())
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Grid of cards
|
// 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
|
// Bouncers
|
||||||
E('div', { 'class': 'cs-card' }, [
|
E('div', { 'class': 'cyber-card' }, [
|
||||||
E('div', { 'class': 'cs-card-header' }, [
|
E('div', { 'class': 'cyber-card-header' }, [
|
||||||
E('div', { 'class': 'cs-card-title' }, '🔒 Registered Bouncers')
|
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
|
// Machines
|
||||||
E('div', { 'class': 'cs-card' }, [
|
E('div', { 'class': 'cyber-card' }, [
|
||||||
E('div', { 'class': 'cs-card-header' }, [
|
E('div', { 'class': 'cyber-card-header' }, [
|
||||||
E('div', { 'class': 'cs-card-title' }, '🖥️ Registered Machines')
|
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
|
// Collections
|
||||||
E('div', { 'class': 'cs-card' }, [
|
E('div', { 'class': 'cyber-card' }, [
|
||||||
E('div', { 'class': 'cs-card-header' }, [
|
E('div', { 'class': 'cyber-card-header' }, [
|
||||||
E('div', { 'class': 'cs-card-title' }, '📦 Installed Collections')
|
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
|
// Acquisition
|
||||||
E('div', { 'class': 'cs-card' }, [
|
E('div', { 'class': 'cyber-card' }, [
|
||||||
E('div', { 'class': 'cs-card-header' }, [
|
E('div', { 'class': 'cyber-card-header' }, [
|
||||||
E('div', { 'class': 'cs-card-title' }, '📊 Acquisition Sources')
|
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
|
// Raw metrics sections
|
||||||
E('div', { 'class': 'cs-card', 'style': 'margin-top: 24px' }, [
|
E('div', { 'class': 'cyber-card', 'style': 'margin-top: 1.5rem;' }, [
|
||||||
E('div', { 'class': 'cs-card-header' }, [
|
E('div', { 'class': 'cyber-card-header' }, [
|
||||||
E('div', { 'class': 'cs-card-title' }, '📈 Raw Prometheus Metrics')
|
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': 'cyber-card-body' }, [
|
||||||
E('div', { 'class': 'cs-metrics-grid' }, [
|
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;' }, [
|
||||||
this.renderMetricSection('Parsers', this.metrics.parsers),
|
this.renderMetricSection(_('Parsers'), this.metrics.parsers),
|
||||||
this.renderMetricSection('Scenarios', this.metrics.scenarios),
|
this.renderMetricSection(_('Scenarios'), this.metrics.scenarios),
|
||||||
this.renderMetricSection('Buckets', this.metrics.buckets),
|
this.renderMetricSection(_('Buckets'), this.metrics.buckets),
|
||||||
this.renderMetricSection('LAPI', this.metrics.lapi),
|
this.renderMetricSection(_('LAPI'), this.metrics.lapi),
|
||||||
this.renderMetricSection('Decisions', this.metrics.decisions)
|
this.renderMetricSection(_('Decisions'), this.metrics.decisions)
|
||||||
].filter(Boolean))
|
].filter(Boolean))
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Setup polling (every 60 seconds for metrics)
|
// Setup polling (every 60 seconds for metrics)
|
||||||
poll.add(function() {
|
poll.add(function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
@ -392,10 +430,9 @@ return view.extend({
|
|||||||
self.metrics = results[0];
|
self.metrics = results[0];
|
||||||
self.bouncers = results[1];
|
self.bouncers = results[1];
|
||||||
self.machines = results[2];
|
self.machines = results[2];
|
||||||
// Note: Could update view here if needed
|
|
||||||
});
|
});
|
||||||
}, 60);
|
}, 60);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
'require view';
|
'require view';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
'require ui';
|
'require ui';
|
||||||
'require form';
|
'require form';
|
||||||
'require rpc';
|
'require rpc';
|
||||||
@ -14,6 +15,8 @@ return view.extend({
|
|||||||
// Step 1 data
|
// Step 1 data
|
||||||
crowdsecRunning: false,
|
crowdsecRunning: false,
|
||||||
lapiAvailable: false,
|
lapiAvailable: false,
|
||||||
|
lapiRepairing: false,
|
||||||
|
lapiRepairAttempted: false,
|
||||||
|
|
||||||
// Step 2 data
|
// Step 2 data
|
||||||
hubUpdating: false,
|
hubUpdating: false,
|
||||||
@ -56,6 +59,39 @@ return view.extend({
|
|||||||
this.wizardData.crowdsecRunning = status && status.crowdsec === 'running';
|
this.wizardData.crowdsecRunning = status && status.crowdsec === 'running';
|
||||||
this.wizardData.lapiAvailable = status && status.lapi_status === 'available';
|
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 {
|
return {
|
||||||
status: status,
|
status: status,
|
||||||
wizardNeeded: wizardNeeded
|
wizardNeeded: wizardNeeded
|
||||||
@ -142,7 +178,16 @@ return view.extend({
|
|||||||
console.log('[Wizard] status:', status);
|
console.log('[Wizard] status:', status);
|
||||||
var crowdsecRunning = status && status.crowdsec === 'running';
|
var crowdsecRunning = status && status.crowdsec === 'running';
|
||||||
var lapiAvailable = status && status.lapi_status === 'available';
|
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' }, [
|
return E('div', { 'class': 'wizard-step' }, [
|
||||||
E('h2', {}, _('Welcome to CrowdSec Setup')),
|
E('h2', {}, _('Welcome to CrowdSec Setup')),
|
||||||
@ -158,14 +203,31 @@ return view.extend({
|
|||||||
crowdsecRunning ? _('RUNNING') : _('STOPPED'))
|
crowdsecRunning ? _('RUNNING') : _('STOPPED'))
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'check-item' }, [
|
E('div', { 'class': 'check-item' }, [
|
||||||
E('span', { 'class': 'check-icon' + (lapiAvailable ? ' success' : ' error') },
|
E('span', { 'class': 'check-icon' + lapiIconClass + (lapiRepairing ? ' spinning' : '') },
|
||||||
lapiAvailable ? '✓' : '✗'),
|
lapiIcon),
|
||||||
E('span', {}, _('Local API (LAPI)')),
|
E('span', {}, _('Local API (LAPI)')),
|
||||||
E('span', { 'class': 'badge badge-' + (lapiAvailable ? 'success' : 'error') },
|
E('span', { 'class': 'badge badge-' + lapiStatusClass },
|
||||||
lapiAvailable ? _('AVAILABLE') : _('UNAVAILABLE'))
|
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
|
// Info box
|
||||||
E('div', { 'class': 'info-box' }, [
|
E('div', { 'class': 'info-box' }, [
|
||||||
E('h4', {}, _('What will be configured:')),
|
E('h4', {}, _('What will be configured:')),
|
||||||
@ -188,7 +250,7 @@ return view.extend({
|
|||||||
}, _('Cancel')),
|
}, _('Cancel')),
|
||||||
E('button', {
|
E('button', {
|
||||||
'class': 'cbi-button cbi-button-positive',
|
'class': 'cbi-button cbi-button-positive',
|
||||||
'disabled': (!crowdsecRunning || !lapiAvailable) ? true : null,
|
'disabled': (!crowdsecRunning || !lapiAvailable || lapiRepairing) ? true : null,
|
||||||
'click': L.bind(function(ev) {
|
'click': L.bind(function(ev) {
|
||||||
console.log('[Wizard] Next button clicked!');
|
console.log('[Wizard] Next button clicked!');
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
@ -780,6 +842,37 @@ return view.extend({
|
|||||||
}, this));
|
}, 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,
|
handleSaveAndApply: null,
|
||||||
handleSave: null,
|
handleSave: null,
|
||||||
handleReset: null
|
handleReset: null
|
||||||
|
|||||||
@ -430,18 +430,30 @@ register_bouncer() {
|
|||||||
return
|
return
|
||||||
fi
|
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
|
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)
|
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_boolean "success" 1
|
||||||
json_add_string "api_key" "$api_key"
|
json_add_string "api_key" "$api_key"
|
||||||
json_add_string "message" "Bouncer '$bouncer_name' registered successfully"
|
json_add_string "message" "Bouncer '$bouncer_name' registered successfully"
|
||||||
secubox_log "Registered bouncer: $bouncer_name"
|
secubox_log "Registered bouncer: $bouncer_name"
|
||||||
else
|
else
|
||||||
json_add_boolean "success" 0
|
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
|
fi
|
||||||
|
|
||||||
json_dump
|
json_dump
|
||||||
@ -681,9 +693,9 @@ update_firewall_bouncer_config() {
|
|||||||
json_dump
|
json_dump
|
||||||
return
|
return
|
||||||
fi
|
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
|
# String values
|
||||||
if [ -z "$value" ]; then
|
if [ -z "$value" ]; then
|
||||||
json_add_boolean "success" 0
|
json_add_boolean "success" 0
|
||||||
@ -691,7 +703,7 @@ update_firewall_bouncer_config() {
|
|||||||
json_dump
|
json_dump
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
uci set "crowdsec.bouncer.$key=$value"
|
uci set "crowdsec.@bouncer[0].$key=$value"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
json_add_boolean "success" 0
|
json_add_boolean "success" 0
|
||||||
@ -830,10 +842,99 @@ get_wizard_state() {
|
|||||||
json_dump
|
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
|
# Main dispatcher
|
||||||
case "$1" in
|
case "$1" in
|
||||||
list)
|
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)
|
call)
|
||||||
case "$2" in
|
case "$2" in
|
||||||
@ -944,6 +1045,9 @@ case "$1" in
|
|||||||
wizard_state)
|
wizard_state)
|
||||||
get_wizard_state
|
get_wizard_state
|
||||||
;;
|
;;
|
||||||
|
repair_lapi)
|
||||||
|
repair_lapi
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo '{"error": "Unknown method"}'
|
echo '{"error": "Unknown method"}'
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -39,7 +39,8 @@
|
|||||||
"register_bouncer",
|
"register_bouncer",
|
||||||
"delete_bouncer",
|
"delete_bouncer",
|
||||||
"control_firewall_bouncer",
|
"control_firewall_bouncer",
|
||||||
"update_firewall_bouncer_config"
|
"update_firewall_bouncer_config",
|
||||||
|
"repair_lapi"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uci": [ "crowdsec-dashboard" ]
|
"uci": [ "crowdsec-dashboard" ]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user