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:
CyberMind-FR 2026-01-27 11:16:17 +01:00
parent 8c062b6d60
commit 1fb2b11d4a
23 changed files with 1919 additions and 3404 deletions

View File

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

View File

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

View File

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

View File

@ -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: {}
});

View File

@ -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();

View File

@ -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" },

View File

@ -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"

View File

@ -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

View File

@ -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')"

View File

@ -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')"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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