secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/alerts.js
CyberMind-FR 31a87c5d7a feat(structure): reorganize luci-app packages into package/secubox/ + appstore migration
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>
2026-01-01 14:59:38 +01:00

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
});