secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/alerts.js
CyberMind-FR 1bbd345cee refactor(luci): Mass KissTheme UI rework across all LuCI apps
Convert 90+ LuCI view files from legacy cbi-button-* classes to
KissTheme kiss-btn-* classes for consistent dark theme styling.

Pattern conversions applied:
- cbi-button-positive → kiss-btn-green
- cbi-button-negative/remove → kiss-btn-red
- cbi-button-apply → kiss-btn-cyan
- cbi-button-action → kiss-btn-blue
- cbi-button (plain) → kiss-btn

Also replaced hardcoded colors (#080, #c00, #888, etc.) with
CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.)
for proper dark theme compatibility.

Apps updated include: ai-gateway, auth-guardian, bandwidth-manager,
cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure,
glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager,
mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer,
metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status,
secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats,
service-registry, simplex, streamlit, system-hub, tor-shield,
traffic-shaper, vhost-manager, vortex-dns, vortex-firewall,
webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-12 11:09:34 +01:00

292 lines
9.7 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require poll';
'require ui';
'require crowdsec-dashboard.api as api';
'require secubox/kiss-theme';
return view.extend({
alerts: [],
bannedIPs: new Set(),
ipOrgCache: {},
load: function() {
var self = this;
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = L.resource('crowdsec-dashboard/dashboard.css');
document.head.appendChild(link);
// Load both alerts and current decisions to know which IPs are already banned
return Promise.all([
api.getAlerts(100).catch(function() { return []; }),
api.getDecisions().catch(function() { return []; })
]).then(function(results) {
var decisions = results[1];
// Track banned IPs
if (Array.isArray(decisions)) {
decisions.forEach(function(d) {
if (d.decisions) {
d.decisions.forEach(function(dec) {
if (dec.value) self.bannedIPs.add(dec.value);
});
} else if (d.value) {
self.bannedIPs.add(d.value);
}
});
}
var alerts = results[0];
// Extract unique IPs for org lookup
var ips = [];
(Array.isArray(alerts) ? alerts : []).forEach(function(a) {
var ip = a.source && a.source.ip;
if (ip && !self.ipOrgCache[ip] && ips.indexOf(ip) === -1 && !ip.startsWith('192.168.') && !ip.startsWith('10.') && !ip.startsWith('127.')) {
ips.push(ip);
}
});
// Batch lookup orgs (max 100 per request)
if (ips.length > 0) {
return self.lookupOrgs(ips.slice(0, 100)).then(function() { return alerts; });
}
return alerts;
});
},
lookupOrgs: function(ips) {
var self = this;
// Use ip-api.com batch endpoint
return new Promise(function(resolve) {
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://ip-api.com/batch?fields=query,org,isp,as', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.timeout = 5000;
xhr.onload = function() {
if (xhr.status === 200) {
try {
var results = JSON.parse(xhr.responseText);
results.forEach(function(r) {
if (r.query) {
self.ipOrgCache[r.query] = r.org || r.isp || r.as || '';
}
});
} catch (e) {}
}
resolve();
};
xhr.onerror = xhr.ontimeout = function() { resolve(); };
xhr.send(JSON.stringify(ips));
});
},
render: function(data) {
var self = this;
this.alerts = Array.isArray(data) ? data : (data.alerts || []);
var content = [
// Header
E('div', { 'style': 'margin-bottom: 24px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'CrowdSec Alerts'),
KissTheme.badge(this.alerts.length + ' ALERTS',
this.alerts.length > 0 ? 'yellow' : 'green')
]),
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' }, 'Security event monitoring')
]),
// Navigation
this.renderNav('alerts'),
// Stats
E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin: 20px 0;' }, this.renderStats()),
// Alerts card with search
KissTheme.card([
E('span', {}, 'Security Alerts'),
E('input', {
'type': 'text', 'id': 'alert-search',
'placeholder': 'Search...',
'style': 'margin-left: auto; padding: 6px 12px; background: var(--kiss-bg2); border: 1px solid var(--kiss-line); border-radius: 6px; color: var(--kiss-text); font-size: 13px; width: 150px;',
'keyup': function() { self.filterAlerts(); }
})
], E('div', { 'id': 'alerts-list' }, this.renderAlerts(this.alerts)))
];
poll.add(L.bind(this.pollData, this), 30);
return KissTheme.wrap(content, 'admin/secubox/security/crowdsec/alerts');
},
renderNav: function(active) {
var tabs = [
{ id: 'overview', label: 'Overview' },
{ id: 'alerts', label: 'Alerts' },
{ id: 'decisions', label: 'Decisions' },
{ id: 'bouncers', label: 'Bouncers' },
{ id: 'settings', label: 'Settings' }
];
return E('div', { 'style': 'display: flex; gap: 8px; margin-bottom: 20px; border-bottom: 1px solid var(--kiss-line); padding-bottom: 12px;' }, tabs.map(function(t) {
var isActive = active === t.id;
return E('a', {
'href': L.url('admin/secubox/security/crowdsec/' + t.id),
'style': 'padding: 8px 16px; text-decoration: none; border-radius: 6px; font-size: 13px; ' +
(isActive ? 'background: rgba(0,200,83,0.1); color: var(--kiss-green); border: 1px solid rgba(0,200,83,0.3);' :
'color: var(--kiss-muted); border: 1px solid transparent;')
}, t.label);
}));
},
renderStats: function() {
var c = KissTheme.colors;
var scenarios = {}, countries = {};
this.alerts.forEach(function(a) {
var s = a.scenario || 'unknown';
scenarios[s] = (scenarios[s] || 0) + 1;
var cn = (a.source && (a.source.cn || a.source.country)) || 'Unknown';
countries[cn] = (countries[cn] || 0) + 1;
});
var topScenario = Object.entries(scenarios).sort(function(a, b) { return b[1] - a[1]; })[0];
return [
KissTheme.stat(this.alerts.length, 'Total Alerts', this.alerts.length > 0 ? c.orange : c.muted),
KissTheme.stat(Object.keys(scenarios).length, 'Scenarios', c.blue),
KissTheme.stat(Object.keys(countries).length, 'Countries', c.purple),
KissTheme.stat(topScenario ? api.parseScenario(topScenario[0]).split(' ')[0] : '-', 'Top Threat', c.red)
];
},
renderAlerts: function(alerts) {
var self = this;
if (!alerts.length) {
return E('div', { 'style': 'text-align: center; padding: 40px; color: var(--kiss-green);' }, [
E('div', { 'style': 'font-size: 32px; margin-bottom: 12px;' }, '\u2713'),
E('div', {}, 'No alerts - all clear!')
]);
}
return E('table', { 'class': 'kiss-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, 'Time'),
E('th', {}, 'Source'),
E('th', {}, 'Organization'),
E('th', {}, 'Country'),
E('th', {}, 'Scenario'),
E('th', {}, 'Events'),
E('th', { 'style': 'width: 80px;' }, 'Action')
])),
E('tbody', {}, alerts.slice(0, 50).map(function(a) {
var src = a.source || {};
var ip = src.ip || '';
var country = src.cn || src.country || '';
var org = self.ipOrgCache[ip] || '';
var orgDisplay = org.length > 25 ? org.substring(0, 22) + '...' : org;
var isBanned = self.bannedIPs.has(ip);
return E('tr', {}, [
E('td', { 'style': 'font-family: monospace; font-size: 12px; color: var(--kiss-muted);' }, api.formatRelativeTime(a.created_at)),
E('td', {}, E('span', { 'style': 'font-family: monospace; color: var(--kiss-cyan);' }, ip || '-')),
E('td', { 'title': org }, E('span', { 'style': 'font-size: 11px; color: var(--kiss-muted);' }, orgDisplay || '-')),
E('td', {}, [
E('span', { 'style': 'font-size: 16px; margin-right: 4px;' }, api.getCountryFlag(country)),
E('span', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, country)
]),
E('td', {}, E('span', { 'style': 'font-size: 12px;' }, api.parseScenario(a.scenario))),
E('td', { 'style': 'font-family: monospace;' }, String(a.events_count || 0)),
E('td', {}, ip ? self.renderBanButton(ip, a.scenario, isBanned) : '-')
]);
}))
]);
},
renderBanButton: function(ip, scenario, isBanned) {
var self = this;
if (isBanned) {
return E('button', {
'class': 'kiss-btn',
'style': 'padding: 4px 10px; font-size: 11px; opacity: 0.5; cursor: not-allowed;',
'disabled': 'disabled',
'title': 'Already banned'
}, 'Banned');
}
return E('button', {
'class': 'kiss-btn kiss-btn-red',
'style': 'padding: 4px 10px; font-size: 11px;',
'click': function(ev) {
ev.preventDefault();
self.banIP(ip, scenario);
},
'title': 'Ban this IP for 24 hours'
}, 'Ban');
},
banIP: function(ip, scenario) {
var self = this;
var reason = 'Manual ban from alert: ' + (scenario || 'unknown');
ui.showModal('Ban IP', [
E('p', {}, 'Ban ' + ip + ' for 24 hours?'),
E('p', { 'style': 'font-size: 12px; color: #666;' }, 'Reason: ' + reason),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'kiss-btn',
'click': ui.hideModal
}, 'Cancel'),
' ',
E('button', {
'class': 'kiss-btn kiss-btn-red',
'click': function() {
ui.hideModal();
ui.showModal('Banning...', [
E('p', { 'class': 'spinning' }, 'Adding ban for ' + ip + '...')
]);
api.addBan(ip, '24h', reason).then(function(result) {
ui.hideModal();
if (result && result.success !== false) {
self.bannedIPs.add(ip);
ui.addNotification(null, E('p', {}, 'IP ' + ip + ' has been banned for 24 hours'), 'success');
// Refresh the alerts list
var el = document.getElementById('alerts-list');
if (el) dom.content(el, self.renderAlerts(self.alerts));
} else {
ui.addNotification(null, E('p', {}, 'Failed to ban IP: ' + (result.error || 'Unknown error')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, 'Failed to ban IP: ' + err), 'error');
});
}
}, 'Ban')
])
]);
},
filterAlerts: function() {
var self = this;
var query = (document.getElementById('alert-search').value || '').toLowerCase();
var filtered = this.alerts.filter(function(a) {
if (!query) return true;
var src = a.source || {};
var org = self.ipOrgCache[src.ip] || '';
var fields = [src.ip, a.scenario, src.country, src.cn, org].join(' ').toLowerCase();
return fields.includes(query);
});
var el = document.getElementById('alerts-list');
if (el) dom.content(el, this.renderAlerts(filtered));
},
pollData: function() {
var self = this;
return api.getAlerts(100).then(function(data) {
self.alerts = Array.isArray(data) ? data : (data.alerts || []);
var el = document.getElementById('alerts-list');
if (el) dom.content(el, self.renderAlerts(self.alerts));
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});