feat(crowdsec+haproxy): Dashboard refactor, custom parsers & scenarios
CrowdSec Dashboard: - Refactor all views (alerts, bouncers, decisions, overview, settings) - Add soc.css for Security Operations Center styling - Remove 3000+ lines of redundant code CrowdSec Custom Parsers & Scenarios: - Add secubox-gitea parser and bruteforce scenario - Add secubox-haproxy parser and bruteforce scenario - Add secubox-streamlit parser and bruteforce scenario - Add secubox-webapp parser and bruteforce scenario - Update Makefile for new parser/scenario files HAProxy: - Update api.js, backends.js view improvements - Update luci.haproxy RPCD backend - Update haproxyctl helper script Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8c062b6d60
commit
1fb2b11d4a
@ -0,0 +1,381 @@
|
||||
/* CrowdSec SOC Dashboard - Minimal Professional Theme */
|
||||
/* Version: 1.0.0 - SOC Compliant */
|
||||
|
||||
:root {
|
||||
--soc-bg: #0d1117;
|
||||
--soc-surface: #161b22;
|
||||
--soc-border: #30363d;
|
||||
--soc-text: #c9d1d9;
|
||||
--soc-text-muted: #8b949e;
|
||||
--soc-primary: #58a6ff;
|
||||
--soc-success: #3fb950;
|
||||
--soc-warning: #d29922;
|
||||
--soc-danger: #f85149;
|
||||
--soc-info: #79c0ff;
|
||||
}
|
||||
|
||||
/* Hide LuCI sidebar for full-width SOC view */
|
||||
body.cs-soc-fullwidth #maincontainer > .pull-left,
|
||||
body.cs-soc-fullwidth #mainmenu { display: none !important; }
|
||||
body.cs-soc-fullwidth #maincontent { margin: 0 !important; width: 100% !important; }
|
||||
|
||||
.soc-dashboard {
|
||||
background: var(--soc-bg);
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
|
||||
color: var(--soc-text);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.soc-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--soc-border);
|
||||
}
|
||||
|
||||
.soc-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.soc-title svg { width: 28px; height: 28px; fill: var(--soc-primary); }
|
||||
|
||||
.soc-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.soc-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.soc-status-dot.online { background: var(--soc-success); }
|
||||
.soc-status-dot.offline { background: var(--soc-danger); animation: none; }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.soc-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.soc-stat {
|
||||
background: var(--soc-surface);
|
||||
border: 1px solid var(--soc-border);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.soc-stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--soc-primary);
|
||||
}
|
||||
|
||||
.soc-stat-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--soc-text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.soc-stat.danger .soc-stat-value { color: var(--soc-danger); }
|
||||
.soc-stat.warning .soc-stat-value { color: var(--soc-warning); }
|
||||
.soc-stat.success .soc-stat-value { color: var(--soc-success); }
|
||||
|
||||
/* Cards */
|
||||
.soc-card {
|
||||
background: var(--soc-surface);
|
||||
border: 1px solid var(--soc-border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.soc-card-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--soc-border);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.soc-card-body { padding: 16px; }
|
||||
|
||||
/* Tables */
|
||||
.soc-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.soc-table th {
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
background: var(--soc-bg);
|
||||
color: var(--soc-text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid var(--soc-border);
|
||||
}
|
||||
|
||||
.soc-table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--soc-border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.soc-table tr:last-child td { border-bottom: none; }
|
||||
|
||||
.soc-table tr:hover { background: rgba(88, 166, 255, 0.05); }
|
||||
|
||||
/* IP & Geo */
|
||||
.soc-ip {
|
||||
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||
font-size: 12px;
|
||||
background: var(--soc-bg);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.soc-geo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.soc-flag { font-size: 16px; }
|
||||
|
||||
.soc-country {
|
||||
font-size: 11px;
|
||||
color: var(--soc-text-muted);
|
||||
}
|
||||
|
||||
/* Severity Badges */
|
||||
.soc-severity {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.soc-severity.critical { background: var(--soc-danger); color: #fff; }
|
||||
.soc-severity.high { background: #b62324; color: #fff; }
|
||||
.soc-severity.medium { background: var(--soc-warning); color: #000; }
|
||||
.soc-severity.low { background: var(--soc-info); color: #000; }
|
||||
|
||||
/* Scenario Tags */
|
||||
.soc-scenario {
|
||||
font-size: 11px;
|
||||
color: var(--soc-primary);
|
||||
background: rgba(88, 166, 255, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Time */
|
||||
.soc-time {
|
||||
font-size: 11px;
|
||||
color: var(--soc-text-muted);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.soc-btn {
|
||||
background: var(--soc-surface);
|
||||
border: 1px solid var(--soc-border);
|
||||
color: var(--soc-text);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.soc-btn:hover {
|
||||
background: var(--soc-border);
|
||||
border-color: var(--soc-text-muted);
|
||||
}
|
||||
|
||||
.soc-btn.primary {
|
||||
background: var(--soc-primary);
|
||||
border-color: var(--soc-primary);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.soc-btn.danger {
|
||||
background: var(--soc-danger);
|
||||
border-color: var(--soc-danger);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.soc-btn-sm { padding: 3px 8px; font-size: 11px; }
|
||||
|
||||
/* Geo Distribution */
|
||||
.soc-geo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.soc-geo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: var(--soc-bg);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.soc-geo-count {
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.soc-empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--soc-text-muted);
|
||||
}
|
||||
|
||||
.soc-empty-icon { font-size: 32px; margin-bottom: 12px; opacity: 0.5; }
|
||||
|
||||
/* Nav */
|
||||
.soc-nav {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--soc-border);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.soc-nav a {
|
||||
color: var(--soc-text-muted);
|
||||
text-decoration: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.soc-nav a:hover { color: var(--soc-text); background: var(--soc-surface); }
|
||||
.soc-nav a.active { color: var(--soc-primary); background: rgba(88, 166, 255, 0.1); }
|
||||
|
||||
/* Health Check */
|
||||
.soc-health {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.soc-health-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--soc-bg);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.soc-health-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.soc-health-icon.ok { background: rgba(63, 185, 80, 0.2); color: var(--soc-success); }
|
||||
.soc-health-icon.error { background: rgba(248, 81, 73, 0.2); color: var(--soc-danger); }
|
||||
.soc-health-icon.warn { background: rgba(210, 153, 34, 0.2); color: var(--soc-warning); }
|
||||
|
||||
.soc-health-label { font-size: 12px; color: var(--soc-text-muted); }
|
||||
.soc-health-value { font-size: 13px; font-weight: 500; }
|
||||
|
||||
/* Two Column Layout */
|
||||
.soc-grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.soc-grid-2 { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.soc-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
color: var(--soc-text-muted);
|
||||
}
|
||||
|
||||
.soc-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--soc-border);
|
||||
border-top-color: var(--soc-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Toast */
|
||||
.soc-toast {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: var(--soc-surface);
|
||||
border: 1px solid var(--soc-border);
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.soc-toast.success { border-left: 3px solid var(--soc-success); }
|
||||
.soc-toast.error { border-left: 3px solid var(--soc-danger); }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@ -1,309 +1,151 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require crowdsec-dashboard/api as api';
|
||||
'require crowdsec-dashboard/nav as CsNav';
|
||||
'require crowdsec-dashboard.api as api';
|
||||
|
||||
/**
|
||||
* CrowdSec Dashboard - Alerts View
|
||||
* Historical view of all security alerts
|
||||
* Copyright (C) 2024 CyberMind.fr - Gandalf
|
||||
* CrowdSec SOC - Alerts View
|
||||
* Security alerts timeline with GeoIP
|
||||
*/
|
||||
|
||||
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 = 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)
|
||||
])
|
||||
])
|
||||
]);
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = L.resource('crowdsec-dashboard/soc.css');
|
||||
document.head.appendChild(link);
|
||||
document.body.classList.add('cs-soc-fullwidth');
|
||||
return api.getAlerts(100);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
this.alerts = Array.isArray(data) ? data : [];
|
||||
this.filterAlerts();
|
||||
|
||||
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
|
||||
CsNav.renderTabs('alerts'),
|
||||
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')
|
||||
})
|
||||
])
|
||||
])
|
||||
this.alerts = (data && data.alerts) || data || [];
|
||||
|
||||
return E('div', { 'class': 'soc-dashboard' }, [
|
||||
this.renderHeader(),
|
||||
this.renderNav('alerts'),
|
||||
E('div', { 'class': 'soc-stats', 'style': 'margin-bottom: 20px;' }, this.renderAlertStats()),
|
||||
E('div', { 'class': 'soc-card' }, [
|
||||
E('div', { 'class': 'soc-card-header' }, [
|
||||
'Security Alerts (' + this.alerts.length + ')',
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'class': 'soc-btn',
|
||||
'placeholder': 'Search...',
|
||||
'id': 'alert-search',
|
||||
'style': 'width: 200px;',
|
||||
'keyup': function() { self.filterAlerts(); }
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'cs-card-body no-padding', 'id': 'alerts-table-container' },
|
||||
this.renderTable()
|
||||
)
|
||||
E('div', { 'class': 'soc-card-body', 'id': 'alerts-list' }, this.renderAlerts(this.alerts))
|
||||
])
|
||||
]);
|
||||
|
||||
// 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
|
||||
renderHeader: function() {
|
||||
return E('div', { 'class': 'soc-header' }, [
|
||||
E('div', { 'class': 'soc-title' }, [
|
||||
E('svg', { 'viewBox': '0 0 24 24' }, [E('path', { 'd': 'M12 2L2 7v10l10 5 10-5V7L12 2z' })]),
|
||||
'CrowdSec Security Operations'
|
||||
]),
|
||||
E('div', { 'class': 'soc-status' }, [E('span', { 'class': 'soc-status-dot online' }), 'ALERTS'])
|
||||
]);
|
||||
},
|
||||
|
||||
renderNav: function(active) {
|
||||
var tabs = ['overview', 'alerts', 'decisions', 'bouncers', 'settings'];
|
||||
return E('div', { 'class': 'soc-nav' }, tabs.map(function(t) {
|
||||
return E('a', {
|
||||
'href': L.url('admin/secubox/security/crowdsec/' + t),
|
||||
'class': active === t ? 'active' : ''
|
||||
}, t.charAt(0).toUpperCase() + t.slice(1));
|
||||
}));
|
||||
},
|
||||
|
||||
renderAlertStats: function() {
|
||||
var scenarios = {}, countries = {};
|
||||
this.alerts.forEach(function(a) {
|
||||
var s = a.scenario || 'unknown';
|
||||
scenarios[s] = (scenarios[s] || 0) + 1;
|
||||
var c = a.source?.cn || a.source?.country || 'Unknown';
|
||||
countries[c] = (countries[c] || 0) + 1;
|
||||
});
|
||||
|
||||
var topScenario = Object.entries(scenarios).sort(function(a, b) { return b[1] - a[1]; })[0];
|
||||
var topCountry = Object.entries(countries).sort(function(a, b) { return b[1] - a[1]; })[0];
|
||||
|
||||
return [
|
||||
E('div', { 'class': 'soc-stat' }, [
|
||||
E('div', { 'class': 'soc-stat-value' }, String(this.alerts.length)),
|
||||
E('div', { 'class': 'soc-stat-label' }, 'Total Alerts')
|
||||
]),
|
||||
E('div', { 'class': 'soc-stat' }, [
|
||||
E('div', { 'class': 'soc-stat-value' }, String(Object.keys(scenarios).length)),
|
||||
E('div', { 'class': 'soc-stat-label' }, 'Scenarios')
|
||||
]),
|
||||
E('div', { 'class': 'soc-stat' }, [
|
||||
E('div', { 'class': 'soc-stat-value' }, String(Object.keys(countries).length)),
|
||||
E('div', { 'class': 'soc-stat-label' }, 'Countries')
|
||||
]),
|
||||
E('div', { 'class': 'soc-stat danger' }, [
|
||||
E('div', { 'class': 'soc-stat-value' }, topScenario ? api.parseScenario(topScenario[0]).split(' ')[0] : '-'),
|
||||
E('div', { 'class': 'soc-stat-label' }, 'Top Threat')
|
||||
])
|
||||
];
|
||||
},
|
||||
|
||||
renderAlerts: function(alerts) {
|
||||
if (!alerts.length) {
|
||||
return E('div', { 'class': 'soc-empty' }, [
|
||||
E('div', { 'class': 'soc-empty-icon' }, '\u2713'),
|
||||
'No alerts'
|
||||
]);
|
||||
}
|
||||
|
||||
return E('table', { 'class': 'soc-table' }, [
|
||||
E('thead', {}, E('tr', {}, [
|
||||
E('th', {}, 'Time'),
|
||||
E('th', {}, 'Source IP'),
|
||||
E('th', {}, 'Country'),
|
||||
E('th', {}, 'Scenario'),
|
||||
E('th', {}, 'Events'),
|
||||
E('th', {}, 'Severity')
|
||||
])),
|
||||
E('tbody', {}, alerts.slice(0, 50).map(function(a) {
|
||||
var src = a.source || {};
|
||||
var country = src.cn || src.country || '';
|
||||
var severity = a.scenario?.includes('bf') ? 'high' :
|
||||
a.scenario?.includes('cve') ? 'critical' : 'medium';
|
||||
return E('tr', {}, [
|
||||
E('td', { 'class': 'soc-time' }, api.formatRelativeTime(a.created_at)),
|
||||
E('td', {}, E('span', { 'class': 'soc-ip' }, src.ip || 'N/A')),
|
||||
E('td', { 'class': 'soc-geo' }, [
|
||||
E('span', { 'class': 'soc-flag' }, api.getCountryFlag(country)),
|
||||
E('span', { 'class': 'soc-country' }, country)
|
||||
]),
|
||||
E('td', {}, E('span', { 'class': 'soc-scenario' }, api.parseScenario(a.scenario))),
|
||||
E('td', {}, String(a.events_count || 0)),
|
||||
E('td', {}, E('span', { 'class': 'soc-severity ' + severity }, severity.toUpperCase()))
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
},
|
||||
|
||||
filterAlerts: function() {
|
||||
var query = (document.getElementById('alert-search')?.value || '').toLowerCase();
|
||||
var filtered = this.alerts.filter(function(a) {
|
||||
if (!query) return true;
|
||||
var fields = [a.source?.ip, a.scenario, a.source?.country, a.source?.cn].join(' ').toLowerCase();
|
||||
return fields.includes(query);
|
||||
});
|
||||
var el = document.getElementById('alerts-list');
|
||||
if (el) dom.content(el, this.renderAlerts(filtered));
|
||||
},
|
||||
|
||||
handleSaveApply: null, handleSave: null, handleReset: null
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,682 +1,199 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require crowdsec-dashboard/api as api';
|
||||
'require crowdsec-dashboard/nav as CsNav';
|
||||
'require crowdsec-dashboard.api as api';
|
||||
|
||||
/**
|
||||
* CrowdSec Dashboard - Decisions View
|
||||
* Detailed view and management of all active decisions
|
||||
* Copyright (C) 2024 CyberMind.fr - Gandalf
|
||||
* CrowdSec SOC - Decisions View
|
||||
* Active bans and blocks with GeoIP
|
||||
*/
|
||||
|
||||
return view.extend({
|
||||
title: _('Decisions'),
|
||||
|
||||
csApi: null,
|
||||
decisions: [],
|
||||
filteredDecisions: [],
|
||||
searchQuery: '',
|
||||
sortField: 'value',
|
||||
sortOrder: 'asc',
|
||||
// Advanced filters
|
||||
filterType: 'all', // all, ban, captcha
|
||||
filterDuration: 'all', // all, short (<1h), medium (1-24h), long (>24h), permanent
|
||||
filterCountry: 'all', // all, or specific country code
|
||||
showFilters: false,
|
||||
|
||||
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 this.csApi.getDecisions();
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = L.resource('crowdsec-dashboard/soc.css');
|
||||
document.head.appendChild(link);
|
||||
document.body.classList.add('cs-soc-fullwidth');
|
||||
return api.getDecisions();
|
||||
},
|
||||
|
||||
parseDurationToSeconds: function(duration) {
|
||||
if (!duration) return 0;
|
||||
var match = duration.match(/^(\d+)(h|m|s)?$/);
|
||||
if (!match) {
|
||||
// Try ISO 8601 duration
|
||||
var hours = 0;
|
||||
var hoursMatch = duration.match(/(\d+)h/i);
|
||||
if (hoursMatch) hours = parseInt(hoursMatch[1]);
|
||||
var minsMatch = duration.match(/(\d+)m/i);
|
||||
if (minsMatch) hours += parseInt(minsMatch[1]) / 60;
|
||||
return hours * 3600;
|
||||
}
|
||||
var value = parseInt(match[1]);
|
||||
var unit = match[2] || 's';
|
||||
if (unit === 'h') return value * 3600;
|
||||
if (unit === 'm') return value * 60;
|
||||
return value;
|
||||
},
|
||||
|
||||
filterDecisions: function() {
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
var query = this.searchQuery.toLowerCase();
|
||||
this.decisions = this.parseDecisions(data);
|
||||
|
||||
this.filteredDecisions = this.decisions.filter(function(d) {
|
||||
// Text search filter
|
||||
if (query) {
|
||||
var searchFields = [
|
||||
d.value,
|
||||
d.scenario,
|
||||
d.country,
|
||||
d.type,
|
||||
d.origin
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
|
||||
if (searchFields.indexOf(query) === -1) return false;
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if (self.filterType !== 'all') {
|
||||
if ((d.type || 'ban').toLowerCase() !== self.filterType) return false;
|
||||
}
|
||||
|
||||
// Country filter
|
||||
if (self.filterCountry !== 'all') {
|
||||
if ((d.country || '').toUpperCase() !== self.filterCountry) return false;
|
||||
}
|
||||
|
||||
// Duration filter
|
||||
if (self.filterDuration !== 'all') {
|
||||
var durationSecs = self.parseDurationToSeconds(d.duration);
|
||||
switch (self.filterDuration) {
|
||||
case 'short': // < 1 hour
|
||||
if (durationSecs >= 3600) return false;
|
||||
break;
|
||||
case 'medium': // 1-24 hours
|
||||
if (durationSecs < 3600 || durationSecs >= 86400) return false;
|
||||
break;
|
||||
case 'long': // > 24 hours
|
||||
if (durationSecs < 86400) return false;
|
||||
break;
|
||||
case 'permanent': // > 7 days or explicit permanent
|
||||
if (durationSecs < 604800 && d.duration !== 'permanent') return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort
|
||||
this.filteredDecisions.sort(function(a, b) {
|
||||
var aVal = a[self.sortField] || '';
|
||||
var bVal = b[self.sortField] || '';
|
||||
|
||||
if (self.sortOrder === 'asc') {
|
||||
return aVal.localeCompare(bVal);
|
||||
} else {
|
||||
return bVal.localeCompare(aVal);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getUniqueCountries: function() {
|
||||
var countries = {};
|
||||
this.decisions.forEach(function(d) {
|
||||
if (d.country) {
|
||||
countries[d.country.toUpperCase()] = true;
|
||||
}
|
||||
});
|
||||
return Object.keys(countries).sort();
|
||||
},
|
||||
|
||||
handleFilterChange: function(filterName, value, ev) {
|
||||
this[filterName] = value;
|
||||
this.filterDecisions();
|
||||
this.updateTable();
|
||||
this.updateFilterBadge();
|
||||
},
|
||||
|
||||
toggleFilters: function() {
|
||||
this.showFilters = !this.showFilters;
|
||||
var panel = document.getElementById('advanced-filters');
|
||||
if (panel) {
|
||||
panel.style.display = this.showFilters ? 'block' : 'none';
|
||||
}
|
||||
},
|
||||
|
||||
clearFilters: function() {
|
||||
this.filterType = 'all';
|
||||
this.filterDuration = 'all';
|
||||
this.filterCountry = 'all';
|
||||
this.searchQuery = '';
|
||||
var searchInput = document.querySelector('.cs-search-box input');
|
||||
if (searchInput) searchInput.value = '';
|
||||
this.filterDecisions();
|
||||
this.updateTable();
|
||||
this.updateFilterBadge();
|
||||
this.updateFilterSelects();
|
||||
},
|
||||
|
||||
updateFilterSelects: function() {
|
||||
var typeSelect = document.getElementById('filter-type');
|
||||
var durationSelect = document.getElementById('filter-duration');
|
||||
var countrySelect = document.getElementById('filter-country');
|
||||
if (typeSelect) typeSelect.value = this.filterType;
|
||||
if (durationSelect) durationSelect.value = this.filterDuration;
|
||||
if (countrySelect) countrySelect.value = this.filterCountry;
|
||||
},
|
||||
|
||||
updateFilterBadge: function() {
|
||||
var count = 0;
|
||||
if (this.filterType !== 'all') count++;
|
||||
if (this.filterDuration !== 'all') count++;
|
||||
if (this.filterCountry !== 'all') count++;
|
||||
|
||||
var badge = document.getElementById('filter-badge');
|
||||
if (badge) {
|
||||
badge.textContent = count;
|
||||
badge.style.display = count > 0 ? 'inline-block' : 'none';
|
||||
}
|
||||
},
|
||||
|
||||
exportToCSV: function() {
|
||||
var self = this;
|
||||
var csv = 'IP Address,Scenario,Country,Type,Duration,Origin,Created\n';
|
||||
this.filteredDecisions.forEach(function(d) {
|
||||
csv += [
|
||||
d.value || '',
|
||||
(d.scenario || '').replace(/,/g, ';'),
|
||||
d.country || '',
|
||||
d.type || 'ban',
|
||||
d.duration || '',
|
||||
d.origin || 'crowdsec',
|
||||
d.created_at || ''
|
||||
].join(',') + '\n';
|
||||
});
|
||||
|
||||
var blob = new Blob([csv], { type: 'text/csv' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'crowdsec-decisions-' + new Date().toISOString().slice(0, 10) + '.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
this.showToast('Exported ' + this.filteredDecisions.length + ' decisions to CSV', 'success');
|
||||
},
|
||||
|
||||
handleSearch: function(ev) {
|
||||
this.searchQuery = ev.target.value;
|
||||
this.filterDecisions();
|
||||
this.updateTable();
|
||||
},
|
||||
|
||||
handleSort: function(field, ev) {
|
||||
if (this.sortField === field) {
|
||||
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortField = field;
|
||||
this.sortOrder = 'asc';
|
||||
}
|
||||
this.filterDecisions();
|
||||
this.updateTable();
|
||||
},
|
||||
|
||||
handleUnban: function(ip, ev) {
|
||||
var self = this;
|
||||
|
||||
if (!confirm('Remove ban for ' + ip + '?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.csApi.unbanIP(ip).then(function(result) {
|
||||
if (result.success) {
|
||||
self.showToast('IP ' + ip + ' unbanned successfully', 'success');
|
||||
return self.csApi.getDecisions();
|
||||
} else {
|
||||
self.showToast('Failed to unban: ' + (result.error || 'Unknown error'), 'error');
|
||||
return null;
|
||||
}
|
||||
}).then(function(data) {
|
||||
if (data) {
|
||||
// Flatten alerts->decisions structure
|
||||
self.decisions = [];
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(function(alert) {
|
||||
if (alert.decisions && Array.isArray(alert.decisions)) {
|
||||
self.decisions = self.decisions.concat(alert.decisions);
|
||||
}
|
||||
});
|
||||
}
|
||||
self.filterDecisions();
|
||||
self.updateTable();
|
||||
}
|
||||
}).catch(function(err) {
|
||||
self.showToast('Error: ' + err.message, 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleBulkUnban: function(ev) {
|
||||
var self = this;
|
||||
var checkboxes = document.querySelectorAll('.cs-decision-checkbox:checked');
|
||||
|
||||
if (checkboxes.length === 0) {
|
||||
self.showToast('No decisions selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Remove ban for ' + checkboxes.length + ' IP(s)?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var promises = [];
|
||||
checkboxes.forEach(function(cb) {
|
||||
promises.push(self.csApi.unbanIP(cb.dataset.ip));
|
||||
});
|
||||
|
||||
Promise.all(promises).then(function(results) {
|
||||
var success = results.filter(function(r) { return r.success; }).length;
|
||||
var failed = results.length - success;
|
||||
|
||||
if (success > 0) {
|
||||
self.showToast(success + ' IP(s) unbanned' + (failed > 0 ? ', ' + failed + ' failed' : ''),
|
||||
failed > 0 ? 'warning' : 'success');
|
||||
} else {
|
||||
self.showToast('Failed to unban IPs', 'error');
|
||||
}
|
||||
|
||||
return self.csApi.getDecisions();
|
||||
}).then(function(data) {
|
||||
if (data) {
|
||||
// Flatten alerts->decisions structure
|
||||
self.decisions = [];
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(function(alert) {
|
||||
if (alert.decisions && Array.isArray(alert.decisions)) {
|
||||
self.decisions = self.decisions.concat(alert.decisions);
|
||||
}
|
||||
});
|
||||
}
|
||||
self.filterDecisions();
|
||||
self.updateTable();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleSelectAll: function(ev) {
|
||||
var checked = ev.target.checked;
|
||||
document.querySelectorAll('.cs-decision-checkbox').forEach(function(cb) {
|
||||
cb.checked = checked;
|
||||
});
|
||||
},
|
||||
|
||||
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('decisions-table-container');
|
||||
if (container) {
|
||||
dom.content(container, this.renderTable());
|
||||
}
|
||||
|
||||
var countEl = document.getElementById('decisions-count');
|
||||
if (countEl) {
|
||||
countEl.textContent = this.filteredDecisions.length + ' of ' + this.decisions.length + ' decisions';
|
||||
}
|
||||
},
|
||||
|
||||
renderSortIcon: function(field) {
|
||||
if (this.sortField !== field) return ' ↕';
|
||||
return this.sortOrder === 'asc' ? ' ↑' : ' ↓';
|
||||
},
|
||||
|
||||
renderTable: function() {
|
||||
var self = this;
|
||||
|
||||
if (this.filteredDecisions.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 decisions found' : 'No active decisions')
|
||||
]);
|
||||
}
|
||||
|
||||
var rows = this.filteredDecisions.map(function(d, i) {
|
||||
return E('tr', {}, [
|
||||
E('td', {}, E('input', {
|
||||
'type': 'checkbox',
|
||||
'class': 'cs-decision-checkbox',
|
||||
'data-ip': d.value
|
||||
})),
|
||||
E('td', {}, E('span', { 'class': 'cs-ip' }, d.value || 'N/A')),
|
||||
E('td', {}, E('span', { 'class': 'cs-scenario' }, self.csApi.parseScenario(d.scenario))),
|
||||
E('td', {}, E('span', { 'class': 'cs-country' }, [
|
||||
E('span', { 'class': 'cs-country-flag' }, self.csApi.getCountryFlag(d.country)),
|
||||
' ',
|
||||
d.country || 'N/A'
|
||||
])),
|
||||
E('td', {}, d.origin || 'crowdsec'),
|
||||
E('td', {}, E('span', { 'class': 'cs-action ' + (d.type || 'ban') }, d.type || 'ban')),
|
||||
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatDuration(d.duration))),
|
||||
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatRelativeTime(d.created_at))),
|
||||
E('td', {}, E('button', {
|
||||
'class': 'cs-btn cs-btn-danger cs-btn-sm',
|
||||
'click': ui.createHandlerFn(self, 'handleUnban', d.value)
|
||||
}, 'Unban'))
|
||||
]);
|
||||
});
|
||||
|
||||
return E('table', { 'class': 'cs-table' }, [
|
||||
E('thead', {}, E('tr', {}, [
|
||||
E('th', { 'style': 'width: 40px' }, E('input', {
|
||||
'type': 'checkbox',
|
||||
'id': 'select-all',
|
||||
'change': ui.createHandlerFn(this, 'handleSelectAll')
|
||||
})),
|
||||
E('th', {
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'value'),
|
||||
'style': 'cursor: pointer'
|
||||
}, 'IP Address' + this.renderSortIcon('value')),
|
||||
E('th', {
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'scenario'),
|
||||
'style': 'cursor: pointer'
|
||||
}, 'Scenario' + this.renderSortIcon('scenario')),
|
||||
E('th', {
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'country'),
|
||||
'style': 'cursor: pointer'
|
||||
}, 'Country' + this.renderSortIcon('country')),
|
||||
E('th', {}, 'Origin'),
|
||||
E('th', {}, 'Action'),
|
||||
E('th', {}, 'Expires'),
|
||||
E('th', {}, 'Created'),
|
||||
E('th', {}, 'Actions')
|
||||
])),
|
||||
E('tbody', {}, rows)
|
||||
return E('div', { 'class': 'soc-dashboard' }, [
|
||||
this.renderHeader(),
|
||||
this.renderNav('decisions'),
|
||||
E('div', { 'class': 'soc-card' }, [
|
||||
E('div', { 'class': 'soc-card-header' }, [
|
||||
'Active Decisions (' + this.decisions.length + ')',
|
||||
E('div', { 'style': 'display: flex; gap: 8px;' }, [
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'class': 'soc-btn',
|
||||
'placeholder': 'Search IP...',
|
||||
'id': 'search-input',
|
||||
'style': 'width: 150px;',
|
||||
'keyup': function() { self.filterDecisions(); }
|
||||
}),
|
||||
E('button', { 'class': 'soc-btn primary', 'click': function() { self.showBanModal(); } }, '+ Ban IP')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'soc-card-body', 'id': 'decisions-list' }, this.renderDecisions(this.decisions))
|
||||
]),
|
||||
this.renderBanModal()
|
||||
]);
|
||||
},
|
||||
|
||||
renderHeader: function() {
|
||||
return E('div', { 'class': 'soc-header' }, [
|
||||
E('div', { 'class': 'soc-title' }, [
|
||||
E('svg', { 'viewBox': '0 0 24 24' }, [E('path', { 'd': 'M12 2L2 7v10l10 5 10-5V7L12 2z' })]),
|
||||
'CrowdSec Security Operations'
|
||||
]),
|
||||
E('div', { 'class': 'soc-status' }, [E('span', { 'class': 'soc-status-dot online' }), 'DECISIONS'])
|
||||
]);
|
||||
},
|
||||
|
||||
renderNav: function(active) {
|
||||
var tabs = ['overview', 'alerts', 'decisions', 'bouncers', 'settings'];
|
||||
return E('div', { 'class': 'soc-nav' }, tabs.map(function(t) {
|
||||
return E('a', {
|
||||
'href': L.url('admin/secubox/security/crowdsec/' + t),
|
||||
'class': active === t ? 'active' : ''
|
||||
}, t.charAt(0).toUpperCase() + t.slice(1));
|
||||
}));
|
||||
},
|
||||
|
||||
parseDecisions: function(data) {
|
||||
var decisions = [];
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(function(alert) {
|
||||
if (alert.decisions && Array.isArray(alert.decisions)) {
|
||||
alert.decisions.forEach(function(d) {
|
||||
d.source = alert.source || {};
|
||||
decisions.push(d);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return decisions;
|
||||
},
|
||||
|
||||
renderDecisions: function(decisions) {
|
||||
if (!decisions.length) {
|
||||
return E('div', { 'class': 'soc-empty' }, [
|
||||
E('div', { 'class': 'soc-empty-icon' }, '\u2713'),
|
||||
'No active decisions'
|
||||
]);
|
||||
}
|
||||
|
||||
return E('table', { 'class': 'soc-table' }, [
|
||||
E('thead', {}, E('tr', {}, [
|
||||
E('th', {}, 'IP Address'),
|
||||
E('th', {}, 'Country'),
|
||||
E('th', {}, 'Scenario'),
|
||||
E('th', {}, 'Type'),
|
||||
E('th', {}, 'Duration'),
|
||||
E('th', {}, 'Actions')
|
||||
])),
|
||||
E('tbody', {}, decisions.map(L.bind(function(d) {
|
||||
var country = d.source?.cn || d.source?.country || '';
|
||||
return E('tr', {}, [
|
||||
E('td', {}, E('span', { 'class': 'soc-ip' }, d.value || 'N/A')),
|
||||
E('td', { 'class': 'soc-geo' }, [
|
||||
E('span', { 'class': 'soc-flag' }, api.getCountryFlag(country)),
|
||||
E('span', { 'class': 'soc-country' }, country)
|
||||
]),
|
||||
E('td', {}, E('span', { 'class': 'soc-scenario' }, api.parseScenario(d.scenario))),
|
||||
E('td', {}, E('span', { 'class': 'soc-severity ' + (d.type === 'ban' ? 'critical' : 'medium') }, d.type || 'ban')),
|
||||
E('td', { 'class': 'soc-time' }, api.formatDuration(d.duration)),
|
||||
E('td', {}, E('button', {
|
||||
'class': 'soc-btn soc-btn-sm danger',
|
||||
'click': L.bind(this.handleUnban, this, d.value)
|
||||
}, 'Unban'))
|
||||
]);
|
||||
}, this)))
|
||||
]);
|
||||
},
|
||||
|
||||
filterDecisions: function() {
|
||||
var query = (document.getElementById('search-input')?.value || '').toLowerCase();
|
||||
var filtered = this.decisions.filter(function(d) {
|
||||
return !query || (d.value || '').toLowerCase().includes(query);
|
||||
});
|
||||
var el = document.getElementById('decisions-list');
|
||||
if (el) dom.content(el, this.renderDecisions(filtered));
|
||||
},
|
||||
|
||||
handleUnban: function(ip) {
|
||||
var self = this;
|
||||
if (!confirm('Unban ' + ip + '?')) return;
|
||||
api.removeBan(ip).then(function(r) {
|
||||
if (r.success) {
|
||||
self.showToast('Unbanned ' + ip, 'success');
|
||||
return api.getDecisions().then(function(data) {
|
||||
self.decisions = self.parseDecisions(data);
|
||||
self.filterDecisions();
|
||||
});
|
||||
} else {
|
||||
self.showToast('Failed: ' + (r.error || 'Unknown'), 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
renderBanModal: function() {
|
||||
return E('div', { 'class': 'cs-modal-overlay', 'id': 'ban-modal', 'style': 'display: none' }, [
|
||||
E('div', { 'class': 'cs-modal' }, [
|
||||
E('div', { 'class': 'cs-modal-header' }, [
|
||||
E('div', { 'class': 'cs-modal-title' }, 'Add IP Ban'),
|
||||
E('button', {
|
||||
'class': 'cs-modal-close',
|
||||
'click': ui.createHandlerFn(this, 'closeBanModal')
|
||||
}, '×')
|
||||
]),
|
||||
E('div', { 'class': 'cs-modal-body' }, [
|
||||
E('div', { 'class': 'cs-form-group' }, [
|
||||
E('label', { 'class': 'cs-form-label' }, 'IP Address or Range'),
|
||||
E('input', {
|
||||
'class': 'cs-input',
|
||||
'id': 'ban-ip',
|
||||
'type': 'text',
|
||||
'placeholder': '192.168.1.100 or 10.0.0.0/24'
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'cs-form-group' }, [
|
||||
E('label', { 'class': 'cs-form-label' }, 'Duration'),
|
||||
E('input', {
|
||||
'class': 'cs-input',
|
||||
'id': 'ban-duration',
|
||||
'type': 'text',
|
||||
'placeholder': '4h, 24h, 7d...',
|
||||
'value': '4h'
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'cs-form-group' }, [
|
||||
E('label', { 'class': 'cs-form-label' }, 'Reason'),
|
||||
E('input', {
|
||||
'class': 'cs-input',
|
||||
'id': 'ban-reason',
|
||||
'type': 'text',
|
||||
'placeholder': 'Manual ban from dashboard'
|
||||
})
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cs-modal-footer' }, [
|
||||
E('button', {
|
||||
'class': 'cs-btn',
|
||||
'click': ui.createHandlerFn(this, 'closeBanModal')
|
||||
}, 'Cancel'),
|
||||
E('button', {
|
||||
'class': 'cs-btn cs-btn-primary',
|
||||
'click': ui.createHandlerFn(this, 'submitBan')
|
||||
}, 'Add Ban')
|
||||
var self = this;
|
||||
return E('div', { 'id': 'ban-modal', 'class': 'soc-modal', 'style': 'display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:9999; align-items:center; justify-content:center;' }, [
|
||||
E('div', { 'style': 'background:var(--soc-surface); padding:24px; border-radius:8px; min-width:300px;' }, [
|
||||
E('h3', { 'style': 'margin:0 0 16px 0;' }, 'Ban IP Address'),
|
||||
E('input', { 'id': 'ban-ip', 'class': 'soc-btn', 'style': 'width:100%; margin-bottom:12px;', 'placeholder': 'IP Address' }),
|
||||
E('input', { 'id': 'ban-duration', 'class': 'soc-btn', 'style': 'width:100%; margin-bottom:12px;', 'placeholder': 'Duration (e.g. 4h)', 'value': '4h' }),
|
||||
E('input', { 'id': 'ban-reason', 'class': 'soc-btn', 'style': 'width:100%; margin-bottom:16px;', 'placeholder': 'Reason' }),
|
||||
E('div', { 'style': 'display:flex; gap:8px; justify-content:flex-end;' }, [
|
||||
E('button', { 'class': 'soc-btn', 'click': function() { self.closeBanModal(); } }, 'Cancel'),
|
||||
E('button', { 'class': 'soc-btn primary', 'click': function() { self.submitBan(); } }, 'Ban')
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
openBanModal: function(ev) {
|
||||
document.getElementById('ban-modal').style.display = 'flex';
|
||||
},
|
||||
showBanModal: function() { document.getElementById('ban-modal').style.display = 'flex'; },
|
||||
closeBanModal: function() { document.getElementById('ban-modal').style.display = 'none'; },
|
||||
|
||||
closeBanModal: function(ev) {
|
||||
document.getElementById('ban-modal').style.display = 'none';
|
||||
document.getElementById('ban-ip').value = '';
|
||||
document.getElementById('ban-duration').value = '4h';
|
||||
document.getElementById('ban-reason').value = '';
|
||||
},
|
||||
|
||||
submitBan: function(ev) {
|
||||
submitBan: function() {
|
||||
var self = this;
|
||||
var ip = document.getElementById('ban-ip').value.trim();
|
||||
var duration = document.getElementById('ban-duration').value.trim() || '4h';
|
||||
var reason = document.getElementById('ban-reason').value.trim() || 'Manual ban from dashboard';
|
||||
|
||||
if (!ip) {
|
||||
self.showToast('Please enter an IP address', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.csApi.isValidIP(ip)) {
|
||||
self.showToast('Invalid IP address format', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Decisions] Banning IP:', ip, 'Duration:', duration, 'Reason:', reason);
|
||||
self.csApi.banIP(ip, duration, reason).then(function(result) {
|
||||
console.log('[Decisions] Ban result:', result);
|
||||
if (result.success) {
|
||||
self.showToast('IP ' + ip + ' banned for ' + duration, 'success');
|
||||
var reason = document.getElementById('ban-reason').value.trim() || 'Manual ban';
|
||||
if (!ip || !api.isValidIP(ip)) { self.showToast('Invalid IP', 'error'); return; }
|
||||
api.addBan(ip, duration, reason).then(function(r) {
|
||||
if (r.success) {
|
||||
self.showToast('Banned ' + ip, 'success');
|
||||
self.closeBanModal();
|
||||
// Wait 1 second for CrowdSec to process the decision
|
||||
console.log('[Decisions] Waiting 1 second before refreshing...');
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(function() {
|
||||
console.log('[Decisions] Refreshing decisions list...');
|
||||
resolve(self.csApi.getDecisions());
|
||||
}, 1000);
|
||||
return api.getDecisions().then(function(data) {
|
||||
self.decisions = self.parseDecisions(data);
|
||||
self.filterDecisions();
|
||||
});
|
||||
} else {
|
||||
self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error');
|
||||
return null;
|
||||
self.showToast('Failed: ' + (r.error || 'Unknown'), 'error');
|
||||
}
|
||||
}).then(function(data) {
|
||||
console.log('[Decisions] Updated decisions data:', data);
|
||||
if (data) {
|
||||
// Flatten alerts->decisions structure
|
||||
self.decisions = [];
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(function(alert) {
|
||||
if (alert.decisions && Array.isArray(alert.decisions)) {
|
||||
self.decisions = self.decisions.concat(alert.decisions);
|
||||
}
|
||||
});
|
||||
}
|
||||
self.filterDecisions();
|
||||
self.updateTable();
|
||||
console.log('[Decisions] Table updated with', self.decisions.length, 'decisions');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.error('[Decisions] Ban error:', err);
|
||||
self.showToast('Error: ' + err.message, 'error');
|
||||
});
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
// Flatten alerts->decisions structure
|
||||
// data is an array of alerts, each containing a decisions array
|
||||
this.decisions = [];
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(function(alert) {
|
||||
if (alert.decisions && Array.isArray(alert.decisions)) {
|
||||
self.decisions = self.decisions.concat(alert.decisions);
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log('[Decisions] Flattened', this.decisions.length, 'decisions from', data ? data.length : 0, 'alerts');
|
||||
this.filterDecisions();
|
||||
|
||||
var countries = this.getUniqueCountries();
|
||||
|
||||
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
|
||||
CsNav.renderTabs('decisions'),
|
||||
E('div', { 'class': 'cs-card' }, [
|
||||
E('div', { 'class': 'cs-card-header' }, [
|
||||
E('div', { 'class': 'cs-card-title' }, [
|
||||
'Active Decisions',
|
||||
E('span', {
|
||||
'id': 'decisions-count',
|
||||
'style': 'font-weight: normal; margin-left: 12px; font-size: 12px; color: var(--cs-text-muted)'
|
||||
}, this.filteredDecisions.length + ' of ' + this.decisions.length + ' decisions')
|
||||
]),
|
||||
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('button', {
|
||||
'class': 'cs-btn',
|
||||
'style': 'position: relative;',
|
||||
'click': ui.createHandlerFn(this, 'toggleFilters')
|
||||
}, [
|
||||
'Filters ',
|
||||
E('span', {
|
||||
'id': 'filter-badge',
|
||||
'style': 'display: none; background: #dc3545; color: white; padding: 2px 6px; border-radius: 10px; font-size: 10px; position: absolute; top: -5px; right: -5px;'
|
||||
}, '0')
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'cs-btn',
|
||||
'click': ui.createHandlerFn(this, 'exportToCSV'),
|
||||
'title': 'Export to CSV'
|
||||
}, 'Export CSV'),
|
||||
E('button', {
|
||||
'class': 'cs-btn cs-btn-danger',
|
||||
'click': ui.createHandlerFn(this, 'handleBulkUnban')
|
||||
}, 'Unban Selected'),
|
||||
E('button', {
|
||||
'class': 'cs-btn cs-btn-primary',
|
||||
'click': ui.createHandlerFn(this, 'openBanModal')
|
||||
}, '+ Add Ban')
|
||||
])
|
||||
]),
|
||||
// Advanced Filters Panel
|
||||
E('div', {
|
||||
'id': 'advanced-filters',
|
||||
'style': 'display: none; padding: 1em; background: #f8f9fa; border-bottom: 1px solid #ddd;'
|
||||
}, [
|
||||
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 1em; align-items: flex-end;' }, [
|
||||
E('div', {}, [
|
||||
E('label', { 'style': 'display: block; font-size: 0.85em; margin-bottom: 4px; color: #666;' }, _('Action Type')),
|
||||
E('select', {
|
||||
'id': 'filter-type',
|
||||
'class': 'cs-input',
|
||||
'style': 'min-width: 120px;',
|
||||
'change': function(ev) {
|
||||
self.handleFilterChange('filterType', ev.target.value);
|
||||
}
|
||||
}, [
|
||||
E('option', { 'value': 'all' }, 'All Types'),
|
||||
E('option', { 'value': 'ban' }, 'Ban'),
|
||||
E('option', { 'value': 'captcha' }, 'Captcha')
|
||||
])
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('label', { 'style': 'display: block; font-size: 0.85em; margin-bottom: 4px; color: #666;' }, _('Duration')),
|
||||
E('select', {
|
||||
'id': 'filter-duration',
|
||||
'class': 'cs-input',
|
||||
'style': 'min-width: 140px;',
|
||||
'change': function(ev) {
|
||||
self.handleFilterChange('filterDuration', ev.target.value);
|
||||
}
|
||||
}, [
|
||||
E('option', { 'value': 'all' }, 'All Durations'),
|
||||
E('option', { 'value': 'short' }, '< 1 hour'),
|
||||
E('option', { 'value': 'medium' }, '1-24 hours'),
|
||||
E('option', { 'value': 'long' }, '> 24 hours'),
|
||||
E('option', { 'value': 'permanent' }, 'Permanent (>7d)')
|
||||
])
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('label', { 'style': 'display: block; font-size: 0.85em; margin-bottom: 4px; color: #666;' }, _('Country')),
|
||||
E('select', {
|
||||
'id': 'filter-country',
|
||||
'class': 'cs-input',
|
||||
'style': 'min-width: 140px;',
|
||||
'change': function(ev) {
|
||||
self.handleFilterChange('filterCountry', ev.target.value);
|
||||
}
|
||||
}, [
|
||||
E('option', { 'value': 'all' }, 'All Countries')
|
||||
].concat(countries.map(function(c) {
|
||||
return E('option', { 'value': c }, self.csApi.getCountryFlag(c) + ' ' + c);
|
||||
})))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'cs-btn',
|
||||
'style': 'margin-left: auto;',
|
||||
'click': ui.createHandlerFn(this, 'clearFilters')
|
||||
}, 'Clear Filters')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cs-card-body no-padding', 'id': 'decisions-table-container' },
|
||||
this.renderTable()
|
||||
)
|
||||
]),
|
||||
this.renderBanModal()
|
||||
]);
|
||||
|
||||
// Setup polling
|
||||
poll.add(function() {
|
||||
return self.csApi.getDecisions().then(function(newData) {
|
||||
// Flatten alerts->decisions structure
|
||||
self.decisions = [];
|
||||
if (Array.isArray(newData)) {
|
||||
newData.forEach(function(alert) {
|
||||
if (alert.decisions && Array.isArray(alert.decisions)) {
|
||||
self.decisions = self.decisions.concat(alert.decisions);
|
||||
}
|
||||
});
|
||||
}
|
||||
self.filterDecisions();
|
||||
self.updateTable();
|
||||
});
|
||||
}, 30);
|
||||
|
||||
return view;
|
||||
showToast: function(msg, type) {
|
||||
var t = document.querySelector('.soc-toast');
|
||||
if (t) t.remove();
|
||||
t = E('div', { 'class': 'soc-toast ' + type }, msg);
|
||||
document.body.appendChild(t);
|
||||
setTimeout(function() { t.remove(); }, 4000);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
handleSaveApply: null, handleSave: null, handleReset: null
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -76,14 +76,14 @@ var callGetBackend = rpc.declare({
|
||||
var callCreateBackend = rpc.declare({
|
||||
object: 'luci.haproxy',
|
||||
method: 'create_backend',
|
||||
params: ['name', 'mode', 'balance', 'health_check', 'enabled'],
|
||||
params: ['name', 'mode', 'balance', 'health_check', 'health_check_uri', 'enabled'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callUpdateBackend = rpc.declare({
|
||||
object: 'luci.haproxy',
|
||||
method: 'update_backend',
|
||||
params: ['id', 'name', 'mode', 'balance', 'health_check', 'enabled'],
|
||||
params: ['id', 'name', 'mode', 'balance', 'health_check', 'health_check_uri', 'enabled'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
|
||||
@ -99,12 +99,19 @@ return view.extend({
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'hp-form-group' }, [
|
||||
E('label', { 'class': 'hp-form-label' }, 'Health Check (optional)'),
|
||||
E('label', { 'class': 'hp-form-label' }, 'Health Check'),
|
||||
E('select', { 'id': 'new-backend-health', 'class': 'hp-form-input' }, [
|
||||
E('option', { 'value': '' }, 'None'),
|
||||
E('option', { 'value': 'httpchk' }, 'HTTP Check')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'hp-form-group' }, [
|
||||
E('label', { 'class': 'hp-form-label' }, 'Health Check URI'),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'id': 'new-backend-health',
|
||||
'id': 'new-backend-health-uri',
|
||||
'class': 'hp-form-input',
|
||||
'placeholder': 'httpchk GET /health'
|
||||
'placeholder': '/_stcore/health or /health'
|
||||
})
|
||||
])
|
||||
]),
|
||||
@ -171,7 +178,7 @@ return view.extend({
|
||||
// Health check info
|
||||
backend.health_check ? E('div', { 'style': 'padding: 8px 16px; background: var(--hp-bg-tertiary, #f5f5f5); font-size: 12px; color: var(--hp-text-muted);' }, [
|
||||
'\u{1F3E5} Health Check: ',
|
||||
E('code', {}, backend.health_check)
|
||||
E('code', {}, backend.health_check + (backend.health_check_uri ? ' ' + backend.health_check_uri : ''))
|
||||
]) : null,
|
||||
|
||||
// Servers
|
||||
@ -241,7 +248,8 @@ return view.extend({
|
||||
var name = document.getElementById('new-backend-name').value.trim();
|
||||
var mode = document.getElementById('new-backend-mode').value;
|
||||
var balance = document.getElementById('new-backend-balance').value;
|
||||
var healthCheck = document.getElementById('new-backend-health').value.trim();
|
||||
var healthCheck = document.getElementById('new-backend-health').value;
|
||||
var healthCheckUri = document.getElementById('new-backend-health-uri').value.trim();
|
||||
|
||||
if (!name) {
|
||||
self.showToast('Backend name is required', 'error');
|
||||
@ -253,7 +261,7 @@ return view.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
return api.createBackend(name, mode, balance, healthCheck, 1).then(function(res) {
|
||||
return api.createBackend(name, mode, balance, healthCheck, healthCheckUri, 1).then(function(res) {
|
||||
if (res.success) {
|
||||
self.showToast('Backend "' + name + '" created', 'success');
|
||||
window.location.reload();
|
||||
@ -303,15 +311,25 @@ return view.extend({
|
||||
]),
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, 'Health Check'),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
E('select', { 'id': 'edit-backend-health', 'class': 'cbi-input-select', 'style': 'width: 100%;' }, [
|
||||
E('option', { 'value': '', 'selected': !backend.health_check }, 'None'),
|
||||
E('option', { 'value': 'httpchk', 'selected': backend.health_check === 'httpchk' }, 'HTTP Check')
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, 'Health Check URI'),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'id': 'edit-backend-health',
|
||||
'id': 'edit-backend-health-uri',
|
||||
'class': 'cbi-input-text',
|
||||
'value': backend.health_check || '',
|
||||
'placeholder': 'httpchk GET /health',
|
||||
'value': backend.health_check_uri || '',
|
||||
'placeholder': '/_stcore/health or /health',
|
||||
'style': 'width: 100%;'
|
||||
})
|
||||
}),
|
||||
E('small', { 'style': 'color: var(--hp-text-muted);' }, 'For Streamlit use: /_stcore/health')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
@ -335,7 +353,8 @@ return view.extend({
|
||||
var name = document.getElementById('edit-backend-name').value.trim();
|
||||
var mode = document.getElementById('edit-backend-mode').value;
|
||||
var balance = document.getElementById('edit-backend-balance').value;
|
||||
var healthCheck = document.getElementById('edit-backend-health').value.trim();
|
||||
var healthCheck = document.getElementById('edit-backend-health').value;
|
||||
var healthCheckUri = document.getElementById('edit-backend-health-uri').value.trim();
|
||||
var enabled = document.getElementById('edit-backend-enabled').checked ? 1 : 0;
|
||||
|
||||
if (!name) {
|
||||
@ -344,7 +363,7 @@ return view.extend({
|
||||
}
|
||||
|
||||
ui.hideModal();
|
||||
api.updateBackend(backend.id, name, mode, balance, healthCheck, enabled).then(function(res) {
|
||||
api.updateBackend(backend.id, name, mode, balance, healthCheck, healthCheckUri, enabled).then(function(res) {
|
||||
if (res.success) {
|
||||
self.showToast('Backend updated', 'success');
|
||||
window.location.reload();
|
||||
@ -363,7 +382,7 @@ return view.extend({
|
||||
var newEnabled = backend.enabled ? 0 : 1;
|
||||
var action = newEnabled ? 'enabled' : 'disabled';
|
||||
|
||||
return api.updateBackend(backend.id, null, null, null, null, newEnabled).then(function(res) {
|
||||
return api.updateBackend(backend.id, null, null, null, null, null, newEnabled).then(function(res) {
|
||||
if (res.success) {
|
||||
self.showToast('Backend ' + action, 'success');
|
||||
window.location.reload();
|
||||
|
||||
@ -291,12 +291,13 @@ method_list_backends() {
|
||||
|
||||
_add_backend() {
|
||||
local section="$1"
|
||||
local name mode balance health_check enabled server_line
|
||||
local name mode balance health_check health_check_uri enabled server_line
|
||||
|
||||
config_get name "$section" name "$section"
|
||||
config_get mode "$section" mode "http"
|
||||
config_get balance "$section" balance "roundrobin"
|
||||
config_get health_check "$section" health_check ""
|
||||
config_get health_check_uri "$section" health_check_uri ""
|
||||
config_get enabled "$section" enabled "1"
|
||||
config_get server_line "$section" server ""
|
||||
|
||||
@ -306,6 +307,7 @@ _add_backend() {
|
||||
json_add_string "mode" "$mode"
|
||||
json_add_string "balance" "$balance"
|
||||
json_add_string "health_check" "$health_check"
|
||||
json_add_string "health_check_uri" "$health_check_uri"
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
|
||||
# Include servers array - parse inline server option if present
|
||||
@ -432,7 +434,7 @@ _add_server_for_backend() {
|
||||
|
||||
# Create backend
|
||||
method_create_backend() {
|
||||
local name mode balance health_check enabled
|
||||
local name mode balance health_check health_check_uri enabled
|
||||
local section_id
|
||||
|
||||
read -r input
|
||||
@ -441,6 +443,7 @@ method_create_backend() {
|
||||
json_get_var mode mode "http"
|
||||
json_get_var balance balance "roundrobin"
|
||||
json_get_var health_check health_check ""
|
||||
json_get_var health_check_uri health_check_uri ""
|
||||
json_get_var enabled enabled "1"
|
||||
|
||||
if [ -z "$name" ]; then
|
||||
@ -458,6 +461,7 @@ method_create_backend() {
|
||||
uci set "$UCI_CONFIG.$section_id.mode=$mode"
|
||||
uci set "$UCI_CONFIG.$section_id.balance=$balance"
|
||||
[ -n "$health_check" ] && uci set "$UCI_CONFIG.$section_id.health_check=$health_check"
|
||||
[ -n "$health_check_uri" ] && uci set "$UCI_CONFIG.$section_id.health_check_uri=$health_check_uri"
|
||||
uci set "$UCI_CONFIG.$section_id.enabled=$enabled"
|
||||
uci commit "$UCI_CONFIG"
|
||||
|
||||
@ -471,7 +475,7 @@ method_create_backend() {
|
||||
|
||||
# Update backend
|
||||
method_update_backend() {
|
||||
local id name mode balance health_check enabled
|
||||
local id name mode balance health_check health_check_uri enabled
|
||||
|
||||
read -r input
|
||||
json_load "$input"
|
||||
@ -480,6 +484,7 @@ method_update_backend() {
|
||||
json_get_var mode mode
|
||||
json_get_var balance balance
|
||||
json_get_var health_check health_check
|
||||
json_get_var health_check_uri health_check_uri
|
||||
json_get_var enabled enabled
|
||||
|
||||
if [ -z "$id" ]; then
|
||||
@ -494,6 +499,7 @@ method_update_backend() {
|
||||
[ -n "$mode" ] && uci set "$UCI_CONFIG.$id.mode=$mode"
|
||||
[ -n "$balance" ] && uci set "$UCI_CONFIG.$id.balance=$balance"
|
||||
[ -n "$health_check" ] && uci set "$UCI_CONFIG.$id.health_check=$health_check"
|
||||
[ -n "$health_check_uri" ] && uci set "$UCI_CONFIG.$id.health_check_uri=$health_check_uri"
|
||||
[ -n "$enabled" ] && uci set "$UCI_CONFIG.$id.enabled=$enabled"
|
||||
uci commit "$UCI_CONFIG"
|
||||
|
||||
@ -1367,8 +1373,8 @@ case "$1" in
|
||||
"delete_vhost": { "id": "string" },
|
||||
"list_backends": {},
|
||||
"get_backend": { "id": "string" },
|
||||
"create_backend": { "name": "string", "mode": "string", "balance": "string", "health_check": "string", "enabled": "boolean" },
|
||||
"update_backend": { "id": "string", "name": "string", "mode": "string", "balance": "string", "health_check": "string", "enabled": "boolean" },
|
||||
"create_backend": { "name": "string", "mode": "string", "balance": "string", "health_check": "string", "health_check_uri": "string", "enabled": "boolean" },
|
||||
"update_backend": { "id": "string", "name": "string", "mode": "string", "balance": "string", "health_check": "string", "health_check_uri": "string", "enabled": "boolean" },
|
||||
"delete_backend": { "id": "string" },
|
||||
"list_servers": { "backend": "string" },
|
||||
"create_server": { "backend": "string", "name": "string", "address": "string", "port": "integer", "weight": "integer", "check": "boolean", "enabled": "boolean" },
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-crowdsec-custom
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_VERSION:=1.1.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_ARCH:=all
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
@ -28,6 +28,10 @@ define Package/secubox-app-crowdsec-custom/description
|
||||
- Path scanning/enumeration detection
|
||||
- LuCI/uhttpd auth failure monitoring
|
||||
- Nginx reverse proxy monitoring (if used)
|
||||
- HAProxy backend protection and auth monitoring
|
||||
- Gitea web/SSH/API bruteforce detection
|
||||
- Streamlit app flooding and auth protection
|
||||
- Webapp generic auth bruteforce protection
|
||||
- Whitelist for trusted networks
|
||||
endef
|
||||
|
||||
@ -40,10 +44,18 @@ define Package/secubox-app-crowdsec-custom/install
|
||||
$(INSTALL_DATA) ./files/acquis.d/secubox-uhttpd.yaml $(1)/etc/crowdsec/acquis.d/
|
||||
$(INSTALL_DATA) ./files/acquis.d/secubox-nginx.yaml $(1)/etc/crowdsec/acquis.d/
|
||||
$(INSTALL_DATA) ./files/acquis.d/secubox-auth.yaml $(1)/etc/crowdsec/acquis.d/
|
||||
$(INSTALL_DATA) ./files/acquis.d/secubox-haproxy.yaml $(1)/etc/crowdsec/acquis.d/
|
||||
$(INSTALL_DATA) ./files/acquis.d/secubox-gitea.yaml $(1)/etc/crowdsec/acquis.d/
|
||||
$(INSTALL_DATA) ./files/acquis.d/secubox-streamlit.yaml $(1)/etc/crowdsec/acquis.d/
|
||||
$(INSTALL_DATA) ./files/acquis.d/secubox-webapp.yaml $(1)/etc/crowdsec/acquis.d/
|
||||
|
||||
# Custom parsers
|
||||
$(INSTALL_DIR) $(1)/etc/crowdsec/parsers/s01-parse
|
||||
$(INSTALL_DATA) ./files/parsers/s01-parse/secubox-luci-auth.yaml $(1)/etc/crowdsec/parsers/s01-parse/
|
||||
$(INSTALL_DATA) ./files/parsers/s01-parse/secubox-haproxy.yaml $(1)/etc/crowdsec/parsers/s01-parse/
|
||||
$(INSTALL_DATA) ./files/parsers/s01-parse/secubox-gitea.yaml $(1)/etc/crowdsec/parsers/s01-parse/
|
||||
$(INSTALL_DATA) ./files/parsers/s01-parse/secubox-streamlit.yaml $(1)/etc/crowdsec/parsers/s01-parse/
|
||||
$(INSTALL_DATA) ./files/parsers/s01-parse/secubox-webapp.yaml $(1)/etc/crowdsec/parsers/s01-parse/
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/crowdsec/parsers/s02-enrich
|
||||
$(INSTALL_DATA) ./files/parsers/s02-enrich/secubox-whitelist.yaml $(1)/etc/crowdsec/parsers/s02-enrich/
|
||||
@ -52,6 +64,10 @@ define Package/secubox-app-crowdsec-custom/install
|
||||
$(INSTALL_DIR) $(1)/etc/crowdsec/scenarios
|
||||
$(INSTALL_DATA) ./files/scenarios/secubox-auth-bruteforce.yaml $(1)/etc/crowdsec/scenarios/
|
||||
$(INSTALL_DATA) ./files/scenarios/secubox-http-bruteforce.yaml $(1)/etc/crowdsec/scenarios/
|
||||
$(INSTALL_DATA) ./files/scenarios/secubox-haproxy-bruteforce.yaml $(1)/etc/crowdsec/scenarios/
|
||||
$(INSTALL_DATA) ./files/scenarios/secubox-gitea-bruteforce.yaml $(1)/etc/crowdsec/scenarios/
|
||||
$(INSTALL_DATA) ./files/scenarios/secubox-streamlit-bruteforce.yaml $(1)/etc/crowdsec/scenarios/
|
||||
$(INSTALL_DATA) ./files/scenarios/secubox-webapp-bruteforce.yaml $(1)/etc/crowdsec/scenarios/
|
||||
|
||||
# UCI defaults for first boot setup
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
@ -70,11 +86,13 @@ define Package/secubox-app-crowdsec-custom/postinst
|
||||
cscli collections install crowdsecurity/http-cve 2>/dev/null || true
|
||||
cscli collections install crowdsecurity/nginx 2>/dev/null || true
|
||||
cscli collections install crowdsecurity/http-dos 2>/dev/null || true
|
||||
cscli collections install crowdsecurity/haproxy 2>/dev/null || true
|
||||
|
||||
# Install parsers
|
||||
cscli parsers install crowdsecurity/syslog-logs 2>/dev/null || true
|
||||
cscli parsers install crowdsecurity/http-logs 2>/dev/null || true
|
||||
cscli parsers install crowdsecurity/nginx-logs 2>/dev/null || true
|
||||
cscli parsers install crowdsecurity/haproxy-logs 2>/dev/null || true
|
||||
|
||||
# Run uci-defaults
|
||||
/etc/uci-defaults/99-secubox-app-crowdsec-custom 2>/dev/null || true
|
||||
@ -90,7 +108,7 @@ define Package/secubox-app-crowdsec-custom/postinst
|
||||
|
||||
echo ""
|
||||
echo "SecuBox CrowdSec protection installed!"
|
||||
echo "Protected paths: /secubox/, /cgi-bin/luci, /ubus"
|
||||
echo "Protected services: LuCI, uhttpd, nginx, HAProxy, Gitea, Streamlit, Webapp"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " cscli metrics - View detection metrics"
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
# CrowdSec acquisition for Gitea logs
|
||||
# Monitors Gitea authentication and access events
|
||||
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/gitea/gitea.log
|
||||
- /srv/gitea/log/gitea.log
|
||||
- /opt/gitea/log/gitea.log
|
||||
labels:
|
||||
type: gitea
|
||||
---
|
||||
# Gitea access logs (NCSA format)
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/gitea/access.log
|
||||
- /srv/gitea/log/http.log
|
||||
labels:
|
||||
type: gitea_access
|
||||
@ -0,0 +1,18 @@
|
||||
# CrowdSec acquisition for HAProxy logs
|
||||
# Monitors HAProxy authentication and access events
|
||||
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/haproxy.log
|
||||
- /var/log/haproxy/access.log
|
||||
labels:
|
||||
type: haproxy
|
||||
---
|
||||
# HAProxy syslog entries
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/messages
|
||||
- /tmp/log/messages
|
||||
labels:
|
||||
type: syslog
|
||||
filter: "contains(Line, 'haproxy')"
|
||||
@ -0,0 +1,17 @@
|
||||
# CrowdSec acquisition for Streamlit logs
|
||||
# Monitors Streamlit application access
|
||||
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/streamlit/*.log
|
||||
- /srv/streamlit/*/logs/*.log
|
||||
labels:
|
||||
type: streamlit
|
||||
---
|
||||
# Streamlit via HAProxy backend
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/haproxy.log
|
||||
labels:
|
||||
type: haproxy
|
||||
filter: "contains(Line, 'streamlit')"
|
||||
@ -0,0 +1,17 @@
|
||||
# CrowdSec acquisition for SecuBox Webapp logs
|
||||
# Monitors general web application access and auth events
|
||||
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/webapp/*.log
|
||||
- /srv/webapp/logs/*.log
|
||||
labels:
|
||||
type: webapp
|
||||
---
|
||||
# Webapp via HAProxy/Nginx
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/nginx/access.log
|
||||
- /var/log/nginx/error.log
|
||||
labels:
|
||||
type: nginx
|
||||
@ -0,0 +1,53 @@
|
||||
# CrowdSec parser for Gitea logs
|
||||
# Parses Gitea authentication and access events
|
||||
|
||||
onsuccess: next_stage
|
||||
name: secubox/gitea-logs
|
||||
description: "Parse Gitea application logs"
|
||||
filter: "evt.Line.Labels.type == 'gitea'"
|
||||
grok:
|
||||
pattern: '%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:message}'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: log_type
|
||||
value: gitea
|
||||
- meta: service
|
||||
value: gitea
|
||||
---
|
||||
# Parse Gitea authentication failures
|
||||
onsuccess: next_stage
|
||||
name: secubox/gitea-auth-failure
|
||||
description: "Parse Gitea authentication failures"
|
||||
filter: "evt.Meta.log_type == 'gitea' && (evt.Parsed.message contains 'Failed authentication' || evt.Parsed.message contains 'login attempt')"
|
||||
grok:
|
||||
pattern: '.*from %{IP:source_ip}.*(?:Failed|failed|invalid|denied)'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: auth_success
|
||||
value: "false"
|
||||
---
|
||||
# Parse Gitea SSH authentication failures
|
||||
onsuccess: next_stage
|
||||
name: secubox/gitea-ssh-failure
|
||||
description: "Parse Gitea SSH authentication failures"
|
||||
filter: "evt.Meta.log_type == 'gitea' && (evt.Parsed.message contains 'SSH' || evt.Parsed.message contains 'ssh')"
|
||||
grok:
|
||||
pattern: '.*SSH.*%{IP:source_ip}.*(?:Failed|failed|denied|invalid)'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: auth_success
|
||||
value: "false"
|
||||
- meta: protocol
|
||||
value: ssh
|
||||
---
|
||||
# Parse Gitea access logs (NCSA format)
|
||||
onsuccess: next_stage
|
||||
name: secubox/gitea-access
|
||||
description: "Parse Gitea HTTP access logs"
|
||||
filter: "evt.Line.Labels.type == 'gitea_access'"
|
||||
grok:
|
||||
pattern: '%{IP:source_ip} - %{NOTSPACE:user} \[%{HTTPDATE:timestamp}\] "%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}" %{INT:http_status} %{INT:bytes}'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: log_type
|
||||
value: gitea_access
|
||||
@ -0,0 +1,36 @@
|
||||
# CrowdSec parser for HAProxy logs
|
||||
# Parses HAProxy access and error logs for auth events
|
||||
|
||||
onsuccess: next_stage
|
||||
name: secubox/haproxy-logs
|
||||
description: "Parse HAProxy access logs"
|
||||
filter: "evt.Parsed.program == 'haproxy' || evt.Line contains 'haproxy'"
|
||||
grok:
|
||||
pattern: '%{IP:source_ip}:%{INT:source_port} \[%{HAPROXYDATE:timestamp}\] %{NOTSPACE:frontend} %{NOTSPACE:backend}/%{NOTSPACE:server} %{INT:tq}/%{INT:tw}/%{INT:tc}/%{INT:tr}/%{INT:tt} %{INT:http_status} %{INT:bytes_read}'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: log_type
|
||||
value: haproxy
|
||||
- meta: service
|
||||
value: haproxy
|
||||
---
|
||||
# Parse HAProxy auth failures (401/403 responses)
|
||||
onsuccess: next_stage
|
||||
name: secubox/haproxy-auth-failure
|
||||
description: "Parse HAProxy authentication failures"
|
||||
filter: "evt.Meta.log_type == 'haproxy' && evt.Parsed.http_status in ['401', '403']"
|
||||
statics:
|
||||
- meta: auth_success
|
||||
value: "false"
|
||||
---
|
||||
# Parse HAProxy backend connection failures
|
||||
onsuccess: next_stage
|
||||
name: secubox/haproxy-backend-failure
|
||||
description: "Parse HAProxy backend connection failures"
|
||||
filter: "evt.Line contains 'haproxy' && (evt.Line contains 'no server available' || evt.Line contains 'Connection refused')"
|
||||
grok:
|
||||
pattern: "%{IP:source_ip}.*%{GREEDYDATA:error_message}"
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: log_type
|
||||
value: haproxy_error
|
||||
@ -0,0 +1,38 @@
|
||||
# CrowdSec parser for Streamlit logs
|
||||
# Parses Streamlit access and connection events
|
||||
|
||||
onsuccess: next_stage
|
||||
name: secubox/streamlit-logs
|
||||
description: "Parse Streamlit application logs"
|
||||
filter: "evt.Line.Labels.type == 'streamlit' || evt.Line contains 'streamlit'"
|
||||
grok:
|
||||
pattern: '%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:message}'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: log_type
|
||||
value: streamlit
|
||||
- meta: service
|
||||
value: streamlit
|
||||
---
|
||||
# Parse Streamlit via HAProxy (401/403 auth failures)
|
||||
onsuccess: next_stage
|
||||
name: secubox/streamlit-auth-failure
|
||||
description: "Parse Streamlit authentication failures via HAProxy"
|
||||
filter: "evt.Meta.log_type == 'haproxy' && evt.Parsed.backend contains 'streamlit' && evt.Parsed.http_status in ['401', '403']"
|
||||
statics:
|
||||
- meta: auth_success
|
||||
value: "false"
|
||||
- meta: service
|
||||
value: streamlit
|
||||
---
|
||||
# Parse Streamlit WebSocket connection failures
|
||||
onsuccess: next_stage
|
||||
name: secubox/streamlit-ws-failure
|
||||
description: "Parse Streamlit WebSocket connection issues"
|
||||
filter: "evt.Line contains 'streamlit' && evt.Line contains 'WebSocket'"
|
||||
grok:
|
||||
pattern: '%{IP:source_ip}.*WebSocket.*(?:failed|error|closed)'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: log_type
|
||||
value: streamlit_ws
|
||||
@ -0,0 +1,41 @@
|
||||
# CrowdSec parser for SecuBox Webapp logs
|
||||
# Parses generic web application authentication events
|
||||
|
||||
onsuccess: next_stage
|
||||
name: secubox/webapp-logs
|
||||
description: "Parse SecuBox Webapp logs"
|
||||
filter: "evt.Line.Labels.type == 'webapp'"
|
||||
grok:
|
||||
pattern: '%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:message}'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: log_type
|
||||
value: webapp
|
||||
- meta: service
|
||||
value: webapp
|
||||
---
|
||||
# Parse webapp authentication failures
|
||||
onsuccess: next_stage
|
||||
name: secubox/webapp-auth-failure
|
||||
description: "Parse webapp authentication failures"
|
||||
filter: "evt.Meta.log_type == 'webapp' && (evt.Parsed.message contains 'auth' || evt.Parsed.message contains 'login')"
|
||||
grok:
|
||||
pattern: '.*%{IP:source_ip}.*(?:failed|denied|invalid|error)'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: auth_success
|
||||
value: "false"
|
||||
---
|
||||
# Parse Nginx access for webapp (401/403)
|
||||
onsuccess: next_stage
|
||||
name: secubox/webapp-nginx-auth
|
||||
description: "Parse Nginx auth failures for webapp"
|
||||
filter: "evt.Line.Labels.type == 'nginx' && evt.Parsed.http_status in ['401', '403']"
|
||||
grok:
|
||||
pattern: '%{IP:source_ip} - %{NOTSPACE:user} \[%{HTTPDATE:timestamp}\] "%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}" %{INT:http_status}'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: auth_success
|
||||
value: "false"
|
||||
- meta: log_type
|
||||
value: webapp_nginx
|
||||
@ -0,0 +1,57 @@
|
||||
# CrowdSec scenario for Gitea authentication bruteforce
|
||||
# Detects repeated authentication failures on Gitea
|
||||
|
||||
type: leaky
|
||||
name: secubox/gitea-auth-bruteforce
|
||||
description: "Detect bruteforce attempts on Gitea web interface"
|
||||
filter: "evt.Meta.service == 'gitea' && evt.Meta.auth_success == 'false' && evt.Meta.protocol != 'ssh'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 5
|
||||
leakspeed: 30s
|
||||
blackhole: 5m
|
||||
labels:
|
||||
service: gitea
|
||||
type: bruteforce
|
||||
remediation: true
|
||||
---
|
||||
# Detect Gitea SSH bruteforce
|
||||
type: leaky
|
||||
name: secubox/gitea-ssh-bruteforce
|
||||
description: "Detect SSH bruteforce attempts on Gitea"
|
||||
filter: "evt.Meta.service == 'gitea' && evt.Meta.protocol == 'ssh' && evt.Meta.auth_success == 'false'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 5
|
||||
leakspeed: 60s
|
||||
blackhole: 10m
|
||||
labels:
|
||||
service: gitea
|
||||
type: ssh_bruteforce
|
||||
remediation: true
|
||||
---
|
||||
# Detect Gitea repository enumeration
|
||||
type: leaky
|
||||
name: secubox/gitea-repo-scan
|
||||
description: "Detect repository enumeration on Gitea"
|
||||
filter: "evt.Meta.log_type == 'gitea_access' && evt.Parsed.http_status == '404' && evt.Parsed.request contains '.git'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 20
|
||||
leakspeed: 30s
|
||||
blackhole: 5m
|
||||
labels:
|
||||
service: gitea
|
||||
type: repo_scan
|
||||
remediation: true
|
||||
---
|
||||
# Detect Gitea API abuse
|
||||
type: leaky
|
||||
name: secubox/gitea-api-abuse
|
||||
description: "Detect API abuse on Gitea"
|
||||
filter: "evt.Meta.log_type == 'gitea_access' && evt.Parsed.request contains '/api/v1'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 50
|
||||
leakspeed: 10s
|
||||
blackhole: 5m
|
||||
labels:
|
||||
service: gitea
|
||||
type: api_abuse
|
||||
remediation: true
|
||||
@ -0,0 +1,43 @@
|
||||
# CrowdSec scenario for HAProxy authentication bruteforce
|
||||
# Detects repeated 401/403 responses indicating auth failures
|
||||
|
||||
type: leaky
|
||||
name: secubox/haproxy-auth-bruteforce
|
||||
description: "Detect bruteforce attempts via HAProxy"
|
||||
filter: "evt.Meta.log_type == 'haproxy' && evt.Meta.auth_success == 'false'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 5
|
||||
leakspeed: 30s
|
||||
blackhole: 5m
|
||||
labels:
|
||||
service: haproxy
|
||||
type: bruteforce
|
||||
remediation: true
|
||||
---
|
||||
# Detect rapid HAProxy requests (potential DDoS/scan)
|
||||
type: leaky
|
||||
name: secubox/haproxy-flooding
|
||||
description: "Detect request flooding via HAProxy"
|
||||
filter: "evt.Meta.log_type == 'haproxy'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 100
|
||||
leakspeed: 5s
|
||||
blackhole: 10m
|
||||
labels:
|
||||
service: haproxy
|
||||
type: flooding
|
||||
remediation: true
|
||||
---
|
||||
# Detect HAProxy backend targeting (scanning backends)
|
||||
type: leaky
|
||||
name: secubox/haproxy-backend-scan
|
||||
description: "Detect backend scanning via HAProxy"
|
||||
filter: "evt.Meta.log_type == 'haproxy' && evt.Parsed.http_status == '503'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 10
|
||||
leakspeed: 20s
|
||||
blackhole: 10m
|
||||
labels:
|
||||
service: haproxy
|
||||
type: backend_scan
|
||||
remediation: true
|
||||
@ -0,0 +1,43 @@
|
||||
# CrowdSec scenario for Streamlit authentication bruteforce
|
||||
# Detects repeated authentication failures on Streamlit apps
|
||||
|
||||
type: leaky
|
||||
name: secubox/streamlit-auth-bruteforce
|
||||
description: "Detect bruteforce attempts on Streamlit applications"
|
||||
filter: "evt.Meta.service == 'streamlit' && evt.Meta.auth_success == 'false'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 5
|
||||
leakspeed: 30s
|
||||
blackhole: 5m
|
||||
labels:
|
||||
service: streamlit
|
||||
type: bruteforce
|
||||
remediation: true
|
||||
---
|
||||
# Detect Streamlit flooding (rapid requests)
|
||||
type: leaky
|
||||
name: secubox/streamlit-flooding
|
||||
description: "Detect request flooding on Streamlit apps"
|
||||
filter: "evt.Meta.log_type == 'haproxy' && evt.Parsed.backend contains 'streamlit'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 50
|
||||
leakspeed: 5s
|
||||
blackhole: 5m
|
||||
labels:
|
||||
service: streamlit
|
||||
type: flooding
|
||||
remediation: true
|
||||
---
|
||||
# Detect Streamlit WebSocket abuse
|
||||
type: leaky
|
||||
name: secubox/streamlit-ws-abuse
|
||||
description: "Detect WebSocket abuse on Streamlit"
|
||||
filter: "evt.Meta.log_type == 'streamlit_ws'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 20
|
||||
leakspeed: 10s
|
||||
blackhole: 5m
|
||||
labels:
|
||||
service: streamlit
|
||||
type: ws_abuse
|
||||
remediation: true
|
||||
@ -0,0 +1,62 @@
|
||||
# CrowdSec scenario for SecuBox Webapp authentication bruteforce
|
||||
# Detects repeated authentication failures on web applications
|
||||
|
||||
type: leaky
|
||||
name: secubox/webapp-auth-bruteforce
|
||||
description: "Detect bruteforce attempts on SecuBox Webapp"
|
||||
filter: "evt.Meta.service == 'webapp' && evt.Meta.auth_success == 'false'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 5
|
||||
leakspeed: 30s
|
||||
blackhole: 5m
|
||||
labels:
|
||||
service: webapp
|
||||
type: bruteforce
|
||||
remediation: true
|
||||
---
|
||||
# Detect webapp login page abuse
|
||||
type: leaky
|
||||
name: secubox/webapp-login-abuse
|
||||
description: "Detect login page abuse on webapp"
|
||||
filter: |
|
||||
evt.Meta.log_type == 'webapp_nginx' &&
|
||||
(evt.Parsed.request contains '/login' ||
|
||||
evt.Parsed.request contains '/auth' ||
|
||||
evt.Parsed.request contains '/signin')
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 10
|
||||
leakspeed: 30s
|
||||
blackhole: 5m
|
||||
labels:
|
||||
service: webapp
|
||||
type: login_abuse
|
||||
remediation: true
|
||||
---
|
||||
# Detect webapp path enumeration
|
||||
type: leaky
|
||||
name: secubox/webapp-path-enum
|
||||
description: "Detect path enumeration on webapp"
|
||||
filter: "evt.Meta.log_type == 'webapp_nginx' && evt.Parsed.http_status == '404'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 30
|
||||
leakspeed: 20s
|
||||
blackhole: 10m
|
||||
labels:
|
||||
service: webapp
|
||||
type: path_enum
|
||||
remediation: true
|
||||
---
|
||||
# Detect webapp credential stuffing (many different users from same IP)
|
||||
type: leaky
|
||||
name: secubox/webapp-credential-stuffing
|
||||
description: "Detect credential stuffing on webapp"
|
||||
filter: "evt.Meta.service == 'webapp' && evt.Meta.auth_success == 'false'"
|
||||
groupby: evt.Meta.source_ip
|
||||
distinct: evt.Parsed.user
|
||||
capacity: 10
|
||||
leakspeed: 60s
|
||||
blackhole: 15m
|
||||
labels:
|
||||
service: webapp
|
||||
type: credential_stuffing
|
||||
remediation: true
|
||||
@ -525,7 +525,7 @@ EOF
|
||||
|
||||
_generate_backend() {
|
||||
local section="$1"
|
||||
local enabled name mode balance health_check
|
||||
local enabled name mode balance health_check health_check_uri
|
||||
|
||||
config_get enabled "$section" enabled "0"
|
||||
[ "$enabled" = "1" ] || return
|
||||
@ -534,6 +534,7 @@ _generate_backend() {
|
||||
config_get mode "$section" mode "http"
|
||||
config_get balance "$section" balance "roundrobin"
|
||||
config_get health_check "$section" health_check ""
|
||||
config_get health_check_uri "$section" health_check_uri ""
|
||||
|
||||
# Track generated backend
|
||||
_generated_backends="$_generated_backends $name"
|
||||
@ -543,7 +544,15 @@ _generate_backend() {
|
||||
echo " mode $mode"
|
||||
echo " balance $balance"
|
||||
|
||||
[ -n "$health_check" ] && echo " option $health_check"
|
||||
# Health check configuration
|
||||
if [ -n "$health_check" ]; then
|
||||
echo " option $health_check"
|
||||
# If health_check_uri specified, use HTTP check with GET method
|
||||
if [ -n "$health_check_uri" ]; then
|
||||
echo " http-check send meth GET uri $health_check_uri"
|
||||
echo " http-check expect status 200"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if there are separate server sections for this backend
|
||||
local has_server_sections=0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user