Major structural reorganization and feature additions: ## Folder Reorganization - Move 17 luci-app-* packages to package/secubox/ (except luci-app-secubox core hub) - Update all tooling to support new structure: - secubox-tools/quick-deploy.sh: search both locations - secubox-tools/validate-modules.sh: validate both directories - secubox-tools/fix-permissions.sh: fix permissions in both locations - .github/workflows/test-validate.yml: build from both paths - Update README.md links to new package/secubox/ paths ## AppStore Migration (Complete) - Add catalog entries for all remaining luci-app packages: - network-tweaks.json: Network optimization tools - secubox-bonus.json: Documentation & demos hub - Total: 24 apps in AppStore catalog (22 existing + 2 new) - New category: 'documentation' for docs/demos/tutorials ## VHost Manager v2.0 Enhancements - Add profile activation system for Internal Services and Redirects - Implement createVHost() API wrapper for template-based deployment - Fix Virtual Hosts view rendering with proper LuCI patterns - Fix RPCD backend shell script errors (remove invalid local declarations) - Extend backend validation for nginx return directives (redirect support) - Add section_id parameter for named VHost profiles - Add Remove button to Redirects page for feature parity - Update README to v2.0 with comprehensive feature documentation ## Network Tweaks Dashboard - Close button added to component details modal Files changed: 340+ (336 renames with preserved git history) Packages affected: 19 luci-app, 2 secubox-app, 1 theme, 4 tools 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
308 lines
8.7 KiB
JavaScript
308 lines
8.7 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require secubox-theme/theme as Theme';
|
|
'require dom';
|
|
'require poll';
|
|
'require ui';
|
|
'require crowdsec-dashboard/api as api';
|
|
|
|
/**
|
|
* CrowdSec Dashboard - Alerts View
|
|
* Historical view of all security alerts
|
|
* Copyright (C) 2024 CyberMind.fr - Gandalf
|
|
*/
|
|
|
|
return view.extend({
|
|
title: _('Alerts'),
|
|
|
|
csApi: null,
|
|
alerts: [],
|
|
filteredAlerts: [],
|
|
searchQuery: '',
|
|
limit: 100,
|
|
|
|
load: function() {
|
|
var cssLink = document.createElement('link');
|
|
cssLink.rel = 'stylesheet';
|
|
cssLink.href = L.resource('crowdsec-dashboard/dashboard.css');
|
|
document.head.appendChild(cssLink);
|
|
|
|
this.csApi = new api();
|
|
return this.csApi.getAlerts(this.limit);
|
|
},
|
|
|
|
filterAlerts: function() {
|
|
var query = this.searchQuery.toLowerCase();
|
|
|
|
this.filteredAlerts = this.alerts.filter(function(a) {
|
|
if (!query) return true;
|
|
|
|
var searchFields = [
|
|
a.source?.ip,
|
|
a.scenario,
|
|
a.source?.country,
|
|
a.message
|
|
].filter(Boolean).join(' ').toLowerCase();
|
|
|
|
return searchFields.indexOf(query) !== -1;
|
|
});
|
|
},
|
|
|
|
handleSearch: function(ev) {
|
|
this.searchQuery = ev.target.value;
|
|
this.filterAlerts();
|
|
this.updateTable();
|
|
},
|
|
|
|
handleLoadMore: function(ev) {
|
|
var self = this;
|
|
this.limit += 100;
|
|
|
|
this.csApi.getAlerts(this.limit).then(function(data) {
|
|
self.alerts = Array.isArray(data) ? data : [];
|
|
self.filterAlerts();
|
|
self.updateTable();
|
|
});
|
|
},
|
|
|
|
handleBanFromAlert: function(ip, scenario, ev) {
|
|
var self = this;
|
|
var duration = '4h';
|
|
var reason = 'Manual ban from alert: ' + scenario;
|
|
|
|
if (!confirm('Ban IP ' + ip + ' for ' + duration + '?')) {
|
|
return;
|
|
}
|
|
|
|
this.csApi.banIP(ip, duration, reason).then(function(result) {
|
|
if (result.success) {
|
|
self.showToast('IP ' + ip + ' banned successfully', 'success');
|
|
} else {
|
|
self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
self.showToast('Error: ' + err.message, 'error');
|
|
});
|
|
},
|
|
|
|
showToast: function(message, type) {
|
|
var existing = document.querySelector('.cs-toast');
|
|
if (existing) existing.remove();
|
|
|
|
var toast = E('div', { 'class': 'cs-toast ' + (type || '') }, message);
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(function() { toast.remove(); }, 4000);
|
|
},
|
|
|
|
updateTable: function() {
|
|
var container = document.getElementById('alerts-table-container');
|
|
if (container) {
|
|
dom.content(container, this.renderTable());
|
|
}
|
|
|
|
var countEl = document.getElementById('alerts-count');
|
|
if (countEl) {
|
|
countEl.textContent = this.filteredAlerts.length + ' of ' + this.alerts.length + ' alerts';
|
|
}
|
|
},
|
|
|
|
renderAlertDetails: function(alert) {
|
|
var details = [];
|
|
|
|
if (alert.events_count) {
|
|
details.push(alert.events_count + ' events');
|
|
}
|
|
|
|
if (alert.source?.as_name) {
|
|
details.push('AS: ' + alert.source.as_name);
|
|
}
|
|
|
|
if (alert.capacity) {
|
|
details.push('Capacity: ' + alert.capacity);
|
|
}
|
|
|
|
return details.join(' | ');
|
|
},
|
|
|
|
renderTable: function() {
|
|
var self = this;
|
|
|
|
if (this.filteredAlerts.length === 0) {
|
|
return E('div', { 'class': 'cs-empty' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
E('div', { 'class': 'cs-empty-icon' }, this.searchQuery ? '🔍' : '📭'),
|
|
E('p', {}, this.searchQuery ? 'No matching alerts found' : 'No alerts recorded')
|
|
]);
|
|
}
|
|
|
|
var rows = this.filteredAlerts.map(function(a, i) {
|
|
var sourceIp = a.source?.ip || 'N/A';
|
|
var hasDecisions = a.decisions && a.decisions.length > 0;
|
|
|
|
return E('tr', {}, [
|
|
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatRelativeTime(a.created_at))),
|
|
E('td', {}, E('span', { 'class': 'cs-ip' }, sourceIp)),
|
|
E('td', {}, E('span', { 'class': 'cs-scenario' }, self.csApi.parseScenario(a.scenario))),
|
|
E('td', {}, E('span', { 'class': 'cs-country' }, [
|
|
E('span', { 'class': 'cs-country-flag' }, self.csApi.getCountryFlag(a.source?.country)),
|
|
' ',
|
|
a.source?.country || 'N/A'
|
|
])),
|
|
E('td', {}, String(a.events_count || 0)),
|
|
E('td', {}, [
|
|
hasDecisions
|
|
? E('span', { 'class': 'cs-action ban' }, 'Banned')
|
|
: E('span', { 'style': 'color: var(--cs-text-muted)' }, 'No action')
|
|
]),
|
|
E('td', {}, E('span', {
|
|
'style': 'font-size: 11px; color: var(--cs-text-muted)',
|
|
'title': self.renderAlertDetails(a)
|
|
}, self.renderAlertDetails(a).substring(0, 40) + '...')),
|
|
E('td', {}, sourceIp !== 'N/A' ? E('button', {
|
|
'class': 'cs-btn cs-btn-sm',
|
|
'click': ui.createHandlerFn(self, 'handleBanFromAlert', sourceIp, a.scenario)
|
|
}, 'Ban') : '-')
|
|
]);
|
|
});
|
|
|
|
return E('div', {}, [
|
|
E('table', { 'class': 'cs-table' }, [
|
|
E('thead', {}, E('tr', {}, [
|
|
E('th', {}, 'Time'),
|
|
E('th', {}, 'Source IP'),
|
|
E('th', {}, 'Scenario'),
|
|
E('th', {}, 'Country'),
|
|
E('th', {}, 'Events'),
|
|
E('th', {}, 'Decision'),
|
|
E('th', {}, 'Details'),
|
|
E('th', {}, 'Actions')
|
|
])),
|
|
E('tbody', {}, rows)
|
|
]),
|
|
this.alerts.length >= this.limit ? E('div', {
|
|
'style': 'text-align: center; padding: 20px'
|
|
}, [
|
|
E('button', {
|
|
'class': 'cs-btn',
|
|
'click': ui.createHandlerFn(this, 'handleLoadMore')
|
|
}, 'Load More Alerts')
|
|
]) : null
|
|
]);
|
|
},
|
|
|
|
renderStats: function() {
|
|
var self = this;
|
|
|
|
// Aggregate by scenario
|
|
var scenarioCounts = {};
|
|
var countryCounts = {};
|
|
var last24h = 0;
|
|
var now = new Date();
|
|
|
|
this.alerts.forEach(function(a) {
|
|
var scenario = self.csApi.parseScenario(a.scenario);
|
|
scenarioCounts[scenario] = (scenarioCounts[scenario] || 0) + 1;
|
|
|
|
var country = a.source?.country || 'Unknown';
|
|
countryCounts[country] = (countryCounts[country] || 0) + 1;
|
|
|
|
var created = new Date(a.created_at);
|
|
if ((now - created) < 86400000) {
|
|
last24h++;
|
|
}
|
|
});
|
|
|
|
// Top 5 scenarios
|
|
var topScenarios = Object.entries(scenarioCounts)
|
|
.sort(function(a, b) { return b[1] - a[1]; })
|
|
.slice(0, 5);
|
|
|
|
var maxScenarioCount = topScenarios.length > 0 ? topScenarios[0][1] : 0;
|
|
|
|
var scenarioBars = topScenarios.map(function(s) {
|
|
var pct = maxScenarioCount > 0 ? (s[1] / maxScenarioCount * 100) : 0;
|
|
return E('div', { 'class': 'cs-bar-item' }, [
|
|
E('div', { 'class': 'cs-bar-label', 'title': s[0] }, s[0]),
|
|
E('div', { 'class': 'cs-bar-track' }, [
|
|
E('div', { 'class': 'cs-bar-fill', 'style': 'width: ' + pct + '%' })
|
|
]),
|
|
E('div', { 'class': 'cs-bar-value' }, String(s[1]))
|
|
]);
|
|
});
|
|
|
|
return E('div', { 'class': 'cs-charts-row', 'style': 'margin-bottom: 24px' }, [
|
|
E('div', { 'class': 'cs-stat-card' }, [
|
|
E('div', { 'class': 'cs-stat-label' }, 'Total Alerts'),
|
|
E('div', { 'class': 'cs-stat-value' }, String(this.alerts.length)),
|
|
E('div', { 'class': 'cs-stat-trend' }, last24h + ' in last 24h')
|
|
]),
|
|
E('div', { 'class': 'cs-stat-card' }, [
|
|
E('div', { 'class': 'cs-stat-label' }, 'Unique Scenarios'),
|
|
E('div', { 'class': 'cs-stat-value' }, String(Object.keys(scenarioCounts).length))
|
|
]),
|
|
E('div', { 'class': 'cs-stat-card' }, [
|
|
E('div', { 'class': 'cs-stat-label' }, 'Countries'),
|
|
E('div', { 'class': 'cs-stat-value' }, String(Object.keys(countryCounts).length))
|
|
]),
|
|
E('div', { 'class': 'cs-card', 'style': 'flex: 2' }, [
|
|
E('div', { 'class': 'cs-card-header' }, [
|
|
E('div', { 'class': 'cs-card-title' }, 'Top Attack Scenarios')
|
|
]),
|
|
E('div', { 'class': 'cs-card-body' }, [
|
|
E('div', { 'class': 'cs-bar-chart' }, scenarioBars)
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
this.alerts = Array.isArray(data) ? data : [];
|
|
this.filterAlerts();
|
|
|
|
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
|
|
this.renderStats(),
|
|
E('div', { 'class': 'cs-card' }, [
|
|
E('div', { 'class': 'cs-card-header' }, [
|
|
E('div', { 'class': 'cs-card-title' }, [
|
|
'Alert History',
|
|
E('span', {
|
|
'id': 'alerts-count',
|
|
'style': 'font-weight: normal; margin-left: 12px; font-size: 12px; color: var(--cs-text-muted)'
|
|
}, this.filteredAlerts.length + ' of ' + this.alerts.length + ' alerts')
|
|
]),
|
|
E('div', { 'class': 'cs-actions-bar' }, [
|
|
E('div', { 'class': 'cs-search-box' }, [
|
|
E('input', {
|
|
'class': 'cs-input',
|
|
'type': 'text',
|
|
'placeholder': 'Search IP, scenario, country...',
|
|
'input': ui.createHandlerFn(this, 'handleSearch')
|
|
})
|
|
])
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cs-card-body no-padding', 'id': 'alerts-table-container' },
|
|
this.renderTable()
|
|
)
|
|
])
|
|
]);
|
|
|
|
// Setup polling
|
|
poll.add(function() {
|
|
return self.csApi.getAlerts(self.limit).then(function(newData) {
|
|
self.alerts = Array.isArray(newData) ? newData : [];
|
|
self.filterAlerts();
|
|
self.updateTable();
|
|
});
|
|
}, 60);
|
|
|
|
return view;
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|