diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/soc.css b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/soc.css new file mode 100644 index 00000000..a26755f1 --- /dev/null +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/soc.css @@ -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; } +} diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/alerts.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/alerts.js index 38d1d82f..7ebaf078 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/alerts.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/alerts.js @@ -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 }); diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js index c5f9724b..b76a79ed 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js @@ -1,785 +1,323 @@ '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 SOC - Bouncers View + * Bouncer management with firewall integration + */ return view.extend({ + title: _('Bouncers'), + bouncers: [], + fwStatus: {}, + load: function() { - var cssLink = document.createElement('link'); - cssLink.rel = 'stylesheet'; - cssLink.href = L.resource('crowdsec-dashboard/dashboard.css'); - document.head.appendChild(cssLink); + 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 Promise.all([ - API.getBouncers(), - API.getStatus(), - API.getFirewallBouncerStatus(), - API.getNftablesStats() + api.getBouncers(), + api.getFirewallBouncerStatus(), + api.getNftablesStats() ]); }, render: function(data) { - var bouncers = data[0] || []; - var status = data[1] || {}; - var fwStatus = data[2] || {}; - var nftStats = data[3] || {}; + var self = this; + this.bouncers = (data[0] && data[0].bouncers) || data[0] || []; + this.fwStatus = data[1] || {}; + this.nftStats = data[2] || {}; - var view = E('div', { 'class': 'crowdsec-dashboard' }, [ - CsNav.renderTabs('bouncers'), - - // Status Card - E('div', { 'class': 'cbi-section', 'style': 'background: ' + (status.crowdsec === 'running' ? '#d4edda' : '#f8d7da') + '; border-left: 4px solid ' + (status.crowdsec === 'running' ? '#28a745' : '#dc3545') + '; padding: 1em; margin-bottom: 1.5em;' }, [ - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [ - E('div', {}, [ - E('strong', {}, _('CrowdSec Status:')), - ' ', - E('span', { 'class': 'badge', 'style': 'background: ' + (status.crowdsec === 'running' ? '#28a745' : '#dc3545') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px; margin-left: 0.5em;' }, - status.crowdsec === 'running' ? _('RUNNING') : _('STOPPED')) - ]), - E('div', {}, [ - E('strong', {}, _('Active Bouncers:')), - ' ', - E('span', { 'style': 'font-size: 1.3em; color: #0088cc; font-weight: bold; margin-left: 0.5em;' }, - bouncers.length.toString()) - ]) - ]) - ]), - - // Firewall Bouncer Management Card - this.renderFirewallBouncerCard(fwStatus, nftStats), - - // Bouncers Table - E('div', { 'class': 'cbi-section' }, [ - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;' }, [ - E('h3', { 'style': 'margin: 0;' }, _('Registered Bouncers')), - E('div', { 'style': 'display: flex; gap: 0.5em;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-positive', - 'click': L.bind(this.openRegisterWizard, this) - }, _('➕ Register Bouncer')), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleRefresh, this) - }, _('Refresh')) - ]) - ]), - - E('div', { 'class': 'table-wrapper' }, [ - E('table', { 'class': 'table', 'id': 'bouncers-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, _('Name')), - E('th', {}, _('IP Address')), - E('th', {}, _('Type')), - E('th', {}, _('Version')), - E('th', {}, _('Last Pull')), - E('th', {}, _('Status')), - E('th', {}, _('Authentication')), - E('th', {}, _('Actions')) - ]) - ]), - E('tbody', { 'id': 'bouncers-tbody' }, - this.renderBouncerRows(bouncers) - ) - ]) - ]) - ]), - - // Help Section - E('div', { 'class': 'cbi-section', 'style': 'background: #e8f4f8; padding: 1em; margin-top: 2em;' }, [ - E('h3', {}, _('About Bouncers')), - E('p', {}, _('Bouncers are remediation components that connect to the CrowdSec Local API to fetch decisions and apply them on your infrastructure.')), - E('div', { 'style': 'margin-top: 1em;' }, [ - E('strong', {}, _('Common Bouncer Types:')), - E('ul', { 'style': 'margin-top: 0.5em;' }, [ - E('li', {}, [ - E('strong', {}, 'cs-firewall-bouncer:'), - ' ', - _('Manages iptables/nftables rules to block IPs at the firewall level') - ]), - E('li', {}, [ - E('strong', {}, 'cs-nginx-bouncer:'), - ' ', - _('Blocks IPs directly in Nginx web server') - ]), - E('li', {}, [ - E('strong', {}, 'cs-haproxy-bouncer:'), - ' ', - _('Integrates with HAProxy load balancer') - ]), - E('li', {}, [ - E('strong', {}, 'cs-cloudflare-bouncer:'), - ' ', - _('Pushes decisions to Cloudflare firewall') + var view = E('div', { 'class': 'soc-dashboard' }, [ + this.renderHeader(), + this.renderNav('bouncers'), + E('div', { 'class': 'soc-stats' }, this.renderBouncerStats()), + E('div', { 'class': 'soc-grid-2' }, [ + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, [ + 'Firewall Bouncer', + E('div', { 'style': 'display: flex; gap: 8px;' }, [ + E('button', { + 'class': 'soc-btn soc-btn-sm ' + (this.fwStatus.running ? 'danger' : 'primary'), + 'click': L.bind(this.handleFwControl, this, this.fwStatus.running ? 'stop' : 'start') + }, this.fwStatus.running ? 'Stop' : 'Start'), + E('button', { + 'class': 'soc-btn soc-btn-sm', + 'click': L.bind(this.handleFwControl, this, 'restart') + }, 'Restart') ]) - ]) + ]), + E('div', { 'class': 'soc-card-body' }, this.renderFirewallStatus()) ]), - E('p', { 'style': 'margin-top: 1em; padding: 0.75em; background: #fff3cd; border-radius: 4px;' }, [ - E('strong', {}, _('Note:')), - ' ', - _('To register a new bouncer, use the command: '), - E('code', {}, 'cscli bouncers add ') + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, [ + 'Blocked IPs', + E('button', { 'class': 'soc-btn soc-btn-sm', 'click': L.bind(this.showBlockedIPs, this) }, 'View All') + ]), + E('div', { 'class': 'soc-card-body' }, this.renderBlockedStats()) ]) - ]) + ]), + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, [ + 'Registered Bouncers (' + this.bouncers.length + ')', + E('button', { 'class': 'soc-btn soc-btn-sm primary', 'click': L.bind(this.showRegisterModal, this) }, '+ Register') + ]), + E('div', { 'class': 'soc-card-body', 'id': 'bouncers-list' }, this.renderBouncers(this.bouncers)) + ]), + this.renderRegisterModal() ]); - // Setup auto-refresh - poll.add(L.bind(function() { - return Promise.all([ - API.getBouncers(), - API.getFirewallBouncerStatus(), - API.getNftablesStats() - ]).then(L.bind(function(refreshData) { - // Update bouncer table - var tbody = document.getElementById('bouncers-tbody'); - if (tbody) { - dom.content(tbody, this.renderBouncerRows(refreshData[0] || [])); - } - - // Update firewall bouncer status - var fwStatus = refreshData[1] || {}; - var nftStats = refreshData[2] || {}; - - var statusBadge = document.getElementById('fw-bouncer-status'); - if (statusBadge) { - var running = fwStatus.running || false; - statusBadge.textContent = running ? _('ACTIVE') : _('STOPPED'); - statusBadge.style.background = running ? '#28a745' : '#dc3545'; - } - - var enabledBadge = document.getElementById('fw-bouncer-enabled'); - if (enabledBadge) { - var enabled = fwStatus.enabled || false; - enabledBadge.textContent = enabled ? _('ENABLED') : _('DISABLED'); - enabledBadge.style.background = enabled ? '#17a2b8' : '#6c757d'; - } - - var ipv4Count = document.getElementById('fw-bouncer-ipv4-count'); - if (ipv4Count) { - ipv4Count.textContent = (fwStatus.blocked_ipv4 || 0).toString(); - } - - var ipv6Count = document.getElementById('fw-bouncer-ipv6-count'); - if (ipv6Count) { - ipv6Count.textContent = (fwStatus.blocked_ipv6 || 0).toString(); - } - }, this)); - }, this), 10); - + poll.add(L.bind(this.pollData, this), 15); return view; }, - renderBouncerRows: function(bouncers) { - if (!bouncers || bouncers.length === 0) { - return E('tr', {}, [ - E('td', { 'colspan': 8, 'style': 'text-align: center; padding: 2em; color: #999;' }, - _('No bouncers registered. Click "Register Bouncer" to add one.')) - ]); - } - - return bouncers.map(L.bind(function(bouncer) { - var lastPull = bouncer.last_pull || bouncer.lastPull || 'Never'; - var isRecent = this.isRecentPull(lastPull); - var bouncerName = bouncer.name || 'Unknown'; - - return E('tr', { - 'style': isRecent ? '' : 'opacity: 0.6;' - }, [ - E('td', {}, [ - E('strong', {}, bouncerName) - ]), - E('td', {}, [ - E('code', { 'style': 'font-size: 0.9em;' }, bouncer.ip_address || bouncer.ipAddress || 'N/A') - ]), - E('td', {}, bouncer.type || 'Unknown'), - E('td', {}, bouncer.version || 'N/A'), - E('td', {}, this.formatLastPull(lastPull)), - E('td', {}, [ - E('span', { - 'class': 'badge', - 'style': 'background: ' + (isRecent ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' - }, isRecent ? _('Active') : _('Inactive')) - ]), - E('td', {}, [ - E('span', { - 'class': 'badge', - 'style': 'background: ' + (bouncer.revoked ? '#dc3545' : '#28a745') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' - }, bouncer.revoked ? _('Revoked') : _('Valid')) - ]), - E('td', {}, [ - E('button', { - 'class': 'cbi-button cbi-button-remove', - 'click': L.bind(this.handleDeleteBouncer, this, bouncerName) - }, _('Delete')) - ]) - ]); - }, this)); + 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' }), 'BOUNCERS']) + ]); }, - formatLastPull: function(lastPull) { - if (!lastPull || lastPull === 'Never' || lastPull === 'never') { - return E('span', { 'style': 'color: #999;' }, _('Never')); + 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)); + })); + }, + + renderBouncerStats: function() { + var active = this.bouncers.filter(function(b) { return !b.revoked; }).length; + var fw = this.fwStatus; + return [ + E('div', { 'class': 'soc-stat ' + (active > 0 ? 'success' : 'warning') }, [ + E('div', { 'class': 'soc-stat-value' }, String(active)), + E('div', { 'class': 'soc-stat-label' }, 'Active Bouncers') + ]), + E('div', { 'class': 'soc-stat ' + (fw.running ? 'success' : 'danger') }, [ + E('div', { 'class': 'soc-stat-value' }, fw.running ? 'ON' : 'OFF'), + E('div', { 'class': 'soc-stat-label' }, 'Firewall Bouncer') + ]), + E('div', { 'class': 'soc-stat danger' }, [ + E('div', { 'class': 'soc-stat-value' }, String(fw.blocked_ipv4 || 0)), + E('div', { 'class': 'soc-stat-label' }, 'Blocked IPv4') + ]), + E('div', { 'class': 'soc-stat danger' }, [ + E('div', { 'class': 'soc-stat-value' }, String(fw.blocked_ipv6 || 0)), + E('div', { 'class': 'soc-stat-label' }, 'Blocked IPv6') + ]) + ]; + }, + + renderFirewallStatus: function() { + var fw = this.fwStatus; + var checks = [ + { label: 'Service', value: fw.running ? 'Running' : 'Stopped', ok: fw.running }, + { label: 'Boot Start', value: fw.enabled ? 'Enabled' : 'Disabled', ok: fw.enabled }, + { label: 'Configured', value: fw.configured ? 'Yes' : 'No', ok: fw.configured }, + { label: 'IPv4 Table', value: fw.nftables_ipv4 ? 'Active' : 'Inactive', ok: fw.nftables_ipv4 }, + { label: 'IPv6 Table', value: fw.nftables_ipv6 ? 'Active' : 'Inactive', ok: fw.nftables_ipv6 } + ]; + return E('div', { 'class': 'soc-health' }, checks.map(function(c) { + return E('div', { 'class': 'soc-health-item' }, [ + E('div', { 'class': 'soc-health-icon ' + (c.ok ? 'ok' : 'error') }, c.ok ? '\u2713' : '\u2717'), + E('div', {}, [ + E('div', { 'class': 'soc-health-label' }, c.label), + E('div', { 'class': 'soc-health-value' }, c.value) + ]) + ]); + })); + }, + + renderBlockedStats: function() { + var fw = this.fwStatus; + var total = (fw.blocked_ipv4 || 0) + (fw.blocked_ipv6 || 0); + if (total === 0) { + return E('div', { 'class': 'soc-empty' }, [ + E('div', { 'class': 'soc-empty-icon' }, '\u2713'), + 'No IPs currently blocked' + ]); + } + return E('div', { 'style': 'text-align: center; padding: 20px;' }, [ + E('div', { 'style': 'font-size: 48px; font-weight: 700; color: var(--soc-danger);' }, String(total)), + E('div', { 'style': 'color: var(--soc-text-muted); margin-top: 8px;' }, 'Total Blocked IPs'), + E('div', { 'style': 'margin-top: 16px; display: flex; justify-content: center; gap: 24px;' }, [ + E('div', {}, [ + E('div', { 'style': 'font-size: 20px; font-weight: 600;' }, String(fw.blocked_ipv4 || 0)), + E('div', { 'style': 'font-size: 11px; color: var(--soc-text-muted);' }, 'IPv4') + ]), + E('div', {}, [ + E('div', { 'style': 'font-size: 20px; font-weight: 600;' }, String(fw.blocked_ipv6 || 0)), + E('div', { 'style': 'font-size: 11px; color: var(--soc-text-muted);' }, 'IPv6') + ]) + ]) + ]); + }, + + renderBouncers: function(bouncers) { + if (!bouncers || !bouncers.length) { + return E('div', { 'class': 'soc-empty' }, [ + E('div', { 'class': 'soc-empty-icon' }, '\u26A0'), + 'No bouncers registered' + ]); } - try { - var pullDate = new Date(lastPull); - var now = new Date(); - var diffMinutes = Math.floor((now - pullDate) / 60000); - - if (diffMinutes < 1) return _('Just now'); - if (diffMinutes < 60) return diffMinutes + 'm ago'; - if (diffMinutes < 1440) return Math.floor(diffMinutes / 60) + 'h ago'; - return Math.floor(diffMinutes / 1440) + 'd ago'; - } catch(e) { - return lastPull; - } + return E('table', { 'class': 'soc-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Name'), + E('th', {}, 'IP Address'), + E('th', {}, 'Type'), + E('th', {}, 'Last Pull'), + E('th', {}, 'Status'), + E('th', {}, 'Actions') + ])), + E('tbody', {}, bouncers.map(L.bind(function(b) { + var lastPull = b.last_pull || b.lastPull; + var isActive = this.isRecentPull(lastPull); + return E('tr', {}, [ + E('td', {}, E('strong', {}, b.name || 'Unknown')), + E('td', {}, E('span', { 'class': 'soc-ip' }, b.ip_address || b.ipAddress || 'N/A')), + E('td', {}, E('span', { 'class': 'soc-scenario' }, b.type || 'Unknown')), + E('td', { 'class': 'soc-time' }, api.formatRelativeTime(lastPull) || 'Never'), + E('td', {}, E('span', { 'class': 'soc-severity ' + (isActive ? 'low' : b.revoked ? 'critical' : 'medium') }, + b.revoked ? 'REVOKED' : isActive ? 'ACTIVE' : 'IDLE')), + E('td', {}, E('button', { + 'class': 'soc-btn soc-btn-sm danger', + 'click': L.bind(this.handleDelete, this, b.name) + }, 'Delete')) + ]); + }, this))) + ]); }, isRecentPull: function(lastPull) { - if (!lastPull || lastPull === 'Never' || lastPull === 'never') { - return false; - } - + if (!lastPull) return false; try { - var pullDate = new Date(lastPull); - var now = new Date(); - var diffMinutes = Math.floor((now - pullDate) / 60000); - // Consider active if pulled within last 5 minutes - return diffMinutes < 5; - } catch(e) { - return false; - } + var diff = (new Date() - new Date(lastPull)) / 60000; + return diff < 5; + } catch(e) { return false; } }, - handleRefresh: function() { - poll.start(); - - return Promise.all([ - API.getBouncers(), - API.getStatus() - ]).then(L.bind(function(data) { - var tbody = document.getElementById('bouncers-tbody'); - if (tbody) { - var bouncers = data[0] || []; - dom.content(tbody, this.renderBouncerRows(bouncers)); - } - - ui.addNotification(null, E('p', _('Bouncer list refreshed')), 'info'); - }, this)).catch(function(err) { - ui.addNotification(null, E('p', _('Failed to refresh: %s').format(err.message || err)), 'error'); - }); - }, - - openRegisterWizard: function() { + handleFwControl: function(action) { var self = this; - var nameInput; - - ui.showModal(_('Register New Bouncer'), [ - E('div', { 'class': 'cbi-section' }, [ - E('div', { 'class': 'cbi-section-descr' }, - _('Register a new bouncer to enforce CrowdSec decisions. The bouncer will receive an API key to connect to the Local API.')), - E('div', { 'class': 'cbi-value', 'style': 'margin-top: 1em;' }, [ - E('label', { 'class': 'cbi-value-title', 'for': 'bouncer-name-input' }, - _('Bouncer Name')), - E('div', { 'class': 'cbi-value-field' }, [ - nameInput = E('input', { - 'type': 'text', - 'id': 'bouncer-name-input', - 'class': 'cbi-input-text', - 'placeholder': _('e.g., firewall-bouncer-1'), - 'style': 'width: 100%;' - }), - E('div', { 'class': 'cbi-value-description' }, - _('Choose a descriptive name (lowercase, hyphens allowed)')) - ]) - ]), - E('div', { 'class': 'cbi-section', 'style': 'background: #e8f4f8; padding: 1em; margin-top: 1em; border-radius: 4px;' }, [ - E('strong', {}, _('What happens next?')), - E('ol', { 'style': 'margin: 0.5em 0 0 1.5em; padding: 0;' }, [ - E('li', {}, _('CrowdSec will generate a unique API key for this bouncer')), - E('li', {}, _('Copy the API key and configure your bouncer with it')), - E('li', {}, _('The bouncer will start pulling and applying decisions')) - ]) - ]) - ]), - E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Cancel')), - ' ', - E('button', { - 'class': 'btn cbi-button-positive', - 'click': function() { - var bouncerName = nameInput.value.trim(); - - if (!bouncerName) { - ui.addNotification(null, E('p', _('Please enter a bouncer name')), 'error'); - return; - } - - // Validate name (alphanumeric, hyphens, underscores) - if (!/^[a-z0-9_-]+$/i.test(bouncerName)) { - ui.addNotification(null, E('p', _('Bouncer name can only contain letters, numbers, hyphens and underscores')), 'error'); - return; - } - - ui.hideModal(); - ui.showModal(_('Registering Bouncer...'), [ - E('p', {}, _('Creating bouncer: %s').format(bouncerName)), - E('div', { 'class': 'spinning' }) - ]); - - API.registerBouncer(bouncerName).then(function(result) { - ui.hideModal(); - - if (result && result.success && result.api_key) { - // Show API key in a modal - ui.showModal(_('Bouncer Registered Successfully'), [ - E('div', { 'class': 'cbi-section' }, [ - E('p', { 'style': 'color: #28a745; font-weight: bold;' }, - _('✓ Bouncer "%s" has been registered!').format(bouncerName)), - E('div', { 'class': 'cbi-value', 'style': 'margin-top: 1em;' }, [ - E('label', { 'class': 'cbi-value-title' }, _('API Key')), - E('div', { 'class': 'cbi-value-field' }, [ - E('code', { - 'id': 'api-key-display', - 'style': 'display: block; padding: 0.75em; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; word-break: break-all; font-size: 0.9em;' - }, result.api_key), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'style': 'margin-top: 0.5em;', - 'click': function() { - navigator.clipboard.writeText(result.api_key).then(function() { - ui.addNotification(null, E('p', _('API key copied to clipboard')), 'info'); - }).catch(function() { - ui.addNotification(null, E('p', _('Failed to copy. Please select and copy manually.')), 'error'); - }); - } - }, _('📋 Copy to Clipboard')) - ]) - ]), - E('div', { 'class': 'cbi-section', 'style': 'background: #fff3cd; padding: 1em; margin-top: 1em; border-radius: 4px;' }, [ - E('strong', { 'style': 'color: #856404;' }, _('⚠️ Important:')), - E('p', { 'style': 'margin: 0.5em 0 0 0; color: #856404;' }, - _('Save this API key now! It will not be shown again. Use it to configure your bouncer.')) - ]) - ]), - E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': function() { - ui.hideModal(); - self.handleRefresh(); - } - }, _('Close')) - ]) - ]); - } else { - ui.addNotification(null, E('p', result.error || _('Failed to register bouncer')), 'error'); - } - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', err.message || err), 'error'); - }); - } - }, _('Register')) - ]) - ]); - - // Focus the input field - setTimeout(function() { - if (nameInput) nameInput.focus(); - }, 100); - }, - - handleDeleteBouncer: function(bouncerName) { - var self = this; - - ui.showModal(_('Delete Bouncer'), [ - E('p', {}, _('Are you sure you want to delete bouncer "%s"?').format(bouncerName)), - E('p', { 'style': 'color: #dc3545; font-weight: bold;' }, - _('⚠️ This action cannot be undone. The bouncer will no longer be able to connect to the Local API.')), - E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Cancel')), - ' ', - E('button', { - 'class': 'btn cbi-button-negative', - 'click': function() { - ui.hideModal(); - ui.showModal(_('Deleting Bouncer...'), [ - E('p', {}, _('Removing bouncer: %s').format(bouncerName)), - E('div', { 'class': 'spinning' }) - ]); - - API.deleteBouncer(bouncerName).then(function(result) { - ui.hideModal(); - if (result && result.success) { - ui.addNotification(null, E('p', _('Bouncer "%s" deleted successfully').format(bouncerName)), 'info'); - self.handleRefresh(); - } else { - ui.addNotification(null, E('p', result.error || _('Failed to delete bouncer')), 'error'); - } - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', err.message || err), 'error'); - }); - } - }, _('Delete')) - ]) - ]); - }, - - renderFirewallBouncerCard: function(fwStatus, nftStats) { - var running = fwStatus.running || false; - var enabled = fwStatus.enabled || false; - var configured = fwStatus.configured || false; - var blockedIPv4 = fwStatus.blocked_ipv4 || 0; - var blockedIPv6 = fwStatus.blocked_ipv6 || 0; - var nftIPv4 = fwStatus.nftables_ipv4 || false; - var nftIPv6 = fwStatus.nftables_ipv6 || false; - - return E('div', { 'class': 'cbi-section', 'id': 'firewall-bouncer-card' }, [ - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;' }, [ - E('h3', { 'style': 'margin: 0;' }, _('Firewall Bouncer')), - E('div', { 'style': 'display: flex; gap: 0.5em;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleFirewallBouncerRefresh, this) - }, _('Refresh')) - ]) - ]), - - E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1em; margin-bottom: 1em;' }, [ - // Status Card - E('div', { 'style': 'background: ' + (running ? '#d4edda' : '#f8d7da') + '; border-left: 4px solid ' + (running ? '#28a745' : '#dc3545') + '; padding: 1em; border-radius: 4px;' }, [ - E('div', { 'style': 'font-weight: bold; margin-bottom: 0.5em; color: #333;' }, _('Service Status')), - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [ - E('span', {}, _('Running:')), - E('span', { - 'class': 'badge', - 'id': 'fw-bouncer-status', - 'style': 'background: ' + (running ? '#28a745' : '#dc3545') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' - }, running ? _('ACTIVE') : _('STOPPED')) - ]), - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-top: 0.5em;' }, [ - E('span', {}, _('Boot Start:')), - E('span', { - 'class': 'badge', - 'id': 'fw-bouncer-enabled', - 'style': 'background: ' + (enabled ? '#17a2b8' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' - }, enabled ? _('ENABLED') : _('DISABLED')) - ]), - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-top: 0.5em;' }, [ - E('span', {}, _('Configured:')), - E('span', { - 'class': 'badge', - 'style': 'background: ' + (configured ? '#28a745' : '#ffc107') + '; color: ' + (configured ? 'white' : '#333') + '; padding: 0.25em 0.6em; border-radius: 3px;' - }, configured ? _('YES') : _('NO')) - ]) - ]), - - // Blocked IPs Card - E('div', { 'style': 'background: #e8f4f8; border-left: 4px solid #0088cc; padding: 1em; border-radius: 4px;' }, [ - E('div', { 'style': 'font-weight: bold; margin-bottom: 0.5em; color: #333;' }, _('Blocked IPs')), - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [ - E('span', {}, _('IPv4:')), - E('span', { - 'id': 'fw-bouncer-ipv4-count', - 'style': 'font-size: 1.5em; color: #dc3545; font-weight: bold;' - }, blockedIPv4.toString()) - ]), - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-top: 0.5em;' }, [ - E('span', {}, _('IPv6:')), - E('span', { - 'id': 'fw-bouncer-ipv6-count', - 'style': 'font-size: 1.5em; color: #dc3545; font-weight: bold;' - }, blockedIPv6.toString()) - ]), - E('div', { 'style': 'margin-top: 0.75em; padding-top: 0.75em; border-top: 1px solid #d1e7f0;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'style': 'width: 100%; font-size: 0.9em;', - 'click': L.bind(this.showNftablesDetails, this, nftStats) - }, _('View Details')) - ]) - ]), - - // nftables Status Card - E('div', { 'style': 'background: #fff3cd; border-left: 4px solid #ffc107; padding: 1em; border-radius: 4px;' }, [ - E('div', { 'style': 'font-weight: bold; margin-bottom: 0.5em; color: #333;' }, _('nftables Status')), - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [ - E('span', {}, _('IPv4 Table:')), - E('span', { - 'class': 'badge', - 'style': 'background: ' + (nftIPv4 ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' - }, nftIPv4 ? _('ACTIVE') : _('INACTIVE')) - ]), - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-top: 0.5em;' }, [ - E('span', {}, _('IPv6 Table:')), - E('span', { - 'class': 'badge', - 'style': 'background: ' + (nftIPv6 ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' - }, nftIPv6 ? _('ACTIVE') : _('INACTIVE')) - ]) - ]) - ]), - - // Control Buttons - E('div', { 'style': 'display: flex; gap: 0.5em; flex-wrap: wrap;' }, [ - running ? - E('button', { - 'class': 'cbi-button cbi-button-negative', - 'click': L.bind(this.handleFirewallBouncerControl, this, 'stop') - }, _('Stop Service')) : - E('button', { - 'class': 'cbi-button cbi-button-positive', - 'click': L.bind(this.handleFirewallBouncerControl, this, 'start') - }, _('Start Service')), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleFirewallBouncerControl, this, 'restart') - }, _('Restart')), - enabled ? - E('button', { - 'class': 'cbi-button', - 'click': L.bind(this.handleFirewallBouncerControl, this, 'disable') - }, _('Disable Boot Start')) : - E('button', { - 'class': 'cbi-button cbi-button-apply', - 'click': L.bind(this.handleFirewallBouncerControl, this, 'enable') - }, _('Enable Boot Start')), - E('button', { - 'class': 'cbi-button', - 'click': L.bind(this.showFirewallBouncerConfig, this) - }, _('Configuration')) - ]) - ]); - }, - - handleFirewallBouncerControl: function(action) { - var actionLabels = { - 'start': _('Starting'), - 'stop': _('Stopping'), - 'restart': _('Restarting'), - 'enable': _('Enabling'), - 'disable': _('Disabling') - }; - - ui.showModal(_('Firewall Bouncer Control'), [ - E('p', {}, _('%s firewall bouncer...').format(actionLabels[action] || action)), - E('div', { 'class': 'spinning' }) - ]); - - return API.controlFirewallBouncer(action).then(L.bind(function(result) { - ui.hideModal(); - - if (result && result.success) { - ui.addNotification(null, E('p', result.message || _('Operation completed successfully')), 'info'); - this.handleFirewallBouncerRefresh(); + api.controlFirewallBouncer(action).then(function(r) { + if (r.success) { + self.showToast('Firewall bouncer ' + action + ' successful', 'success'); + self.pollData(); } else { - ui.addNotification(null, E('p', result.error || _('Operation failed')), 'error'); + self.showToast('Failed: ' + (r.error || 'Unknown'), 'error'); } - }, this)).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', err.message || err), 'error'); }); }, - handleFirewallBouncerRefresh: function() { - return Promise.all([ - API.getFirewallBouncerStatus(), - API.getNftablesStats() - ]).then(L.bind(function(data) { - var fwStatus = data[0] || {}; - var nftStats = data[1] || {}; - - // Update status badges - var statusBadge = document.getElementById('fw-bouncer-status'); - if (statusBadge) { - var running = fwStatus.running || false; - statusBadge.textContent = running ? _('ACTIVE') : _('STOPPED'); - statusBadge.style.background = running ? '#28a745' : '#dc3545'; + handleDelete: function(name) { + var self = this; + if (!confirm('Delete bouncer "' + name + '"?')) return; + api.deleteBouncer(name).then(function(r) { + if (r.success) { + self.showToast('Bouncer deleted', 'success'); + self.pollData(); + } else { + self.showToast('Failed: ' + (r.error || 'Unknown'), 'error'); } - - var enabledBadge = document.getElementById('fw-bouncer-enabled'); - if (enabledBadge) { - var enabled = fwStatus.enabled || false; - enabledBadge.textContent = enabled ? _('ENABLED') : _('DISABLED'); - enabledBadge.style.background = enabled ? '#17a2b8' : '#6c757d'; - } - - // Update blocked IP counts - var ipv4Count = document.getElementById('fw-bouncer-ipv4-count'); - if (ipv4Count) { - ipv4Count.textContent = (fwStatus.blocked_ipv4 || 0).toString(); - } - - var ipv6Count = document.getElementById('fw-bouncer-ipv6-count'); - if (ipv6Count) { - ipv6Count.textContent = (fwStatus.blocked_ipv6 || 0).toString(); - } - - // Re-render the entire card to update buttons - var card = document.getElementById('firewall-bouncer-card'); - if (card) { - dom.content(card, this.renderFirewallBouncerCard(fwStatus, nftStats).childNodes); - } - - ui.addNotification(null, E('p', _('Firewall bouncer status refreshed')), 'info'); - }, this)).catch(function(err) { - ui.addNotification(null, E('p', _('Failed to refresh: %s').format(err.message || err)), 'error'); }); }, - showNftablesDetails: function(nftStats) { - var ipv4Blocked = nftStats.ipv4_blocked || []; - var ipv6Blocked = nftStats.ipv6_blocked || []; - var ipv4Rules = nftStats.ipv4_rules || 0; - var ipv6Rules = nftStats.ipv6_rules || 0; + showBlockedIPs: function() { + var nft = this.nftStats || {}; + var ipv4 = nft.ipv4_blocked || []; + var ipv6 = nft.ipv6_blocked || []; + var content = E('div', { 'style': 'max-height: 400px; overflow-y: auto;' }, [ + E('h4', { 'style': 'margin-bottom: 8px;' }, 'IPv4 (' + ipv4.length + ')'), + ipv4.length ? E('div', { 'style': 'background: var(--soc-bg); padding: 8px; border-radius: 4px; margin-bottom: 16px;' }, + ipv4.map(function(ip) { return E('div', { 'class': 'soc-ip', 'style': 'margin: 4px 0;' }, ip); }) + ) : E('p', { 'style': 'color: var(--soc-text-muted);' }, 'None'), + E('h4', { 'style': 'margin-bottom: 8px;' }, 'IPv6 (' + ipv6.length + ')'), + ipv6.length ? E('div', { 'style': 'background: var(--soc-bg); padding: 8px; border-radius: 4px;' }, + ipv6.map(function(ip) { return E('div', { 'class': 'soc-ip', 'style': 'margin: 4px 0;' }, ip); }) + ) : E('p', { 'style': 'color: var(--soc-text-muted);' }, 'None') + ]); + ui.showModal('Blocked IP Addresses', [content, E('div', { 'class': 'right' }, [ + E('button', { 'class': 'soc-btn', 'click': ui.hideModal }, 'Close') + ])]); + }, - ui.showModal(_('nftables Blocked IPs'), [ - E('div', { 'class': 'cbi-section' }, [ - E('h4', {}, _('IPv4 Blocked Addresses (%d)').format(ipv4Blocked.length)), - ipv4Blocked.length > 0 ? - E('div', { 'style': 'max-height: 200px; overflow-y: auto; background: #f5f5f5; padding: 0.5em; border-radius: 4px; margin-bottom: 1em;' }, - ipv4Blocked.map(function(ip) { - return E('div', { 'style': 'font-family: monospace; padding: 0.25em 0;' }, ip); - }) - ) : - E('p', { 'style': 'color: #999; margin-bottom: 1em;' }, _('No IPv4 addresses blocked')), - - E('h4', {}, _('IPv6 Blocked Addresses (%d)').format(ipv6Blocked.length)), - ipv6Blocked.length > 0 ? - E('div', { 'style': 'max-height: 200px; overflow-y: auto; background: #f5f5f5; padding: 0.5em; border-radius: 4px; margin-bottom: 1em;' }, - ipv6Blocked.map(function(ip) { - return E('div', { 'style': 'font-family: monospace; padding: 0.25em 0;' }, ip); - }) - ) : - E('p', { 'style': 'color: #999; margin-bottom: 1em;' }, _('No IPv6 addresses blocked')), - - E('div', { 'style': 'background: #e8f4f8; padding: 1em; border-radius: 4px;' }, [ - E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.5em;' }, [ - E('strong', {}, _('IPv4 Rules:')), - E('span', {}, ipv4Rules.toString()) - ]), - E('div', { 'style': 'display: flex; justify-content: space-between;' }, [ - E('strong', {}, _('IPv6 Rules:')), - E('span', {}, ipv6Rules.toString()) - ]) + renderRegisterModal: function() { + var self = this; + return E('div', { 'id': 'register-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:320px;' }, [ + E('h3', { 'style': 'margin:0 0 16px 0;' }, 'Register New Bouncer'), + E('input', { 'id': 'bouncer-name', 'class': 'soc-btn', 'style': 'width:100%; margin-bottom:16px;', 'placeholder': 'Bouncer name (e.g. firewall-bouncer)' }), + E('div', { 'style': 'display:flex; gap:8px; justify-content:flex-end;' }, [ + E('button', { 'class': 'soc-btn', 'click': function() { self.closeRegisterModal(); } }, 'Cancel'), + E('button', { 'class': 'soc-btn primary', 'click': function() { self.submitRegister(); } }, 'Register') ]) - ]), - E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Close')) ]) ]); }, - showFirewallBouncerConfig: function() { - ui.showModal(_('Loading Configuration...'), [ - E('div', { 'class': 'spinning' }) - ]); + showRegisterModal: function() { document.getElementById('register-modal').style.display = 'flex'; }, + closeRegisterModal: function() { document.getElementById('register-modal').style.display = 'none'; }, - return API.getFirewallBouncerConfig().then(function(config) { - if (!config.configured) { - ui.hideModal(); - ui.showModal(_('Firewall Bouncer Configuration'), [ - E('div', { 'class': 'cbi-section' }, [ - E('p', { 'style': 'color: #ffc107; font-weight: bold;' }, - _('⚠️ Firewall bouncer is not configured yet.')), - E('p', {}, - _('Please install the secubox-app-cs-firewall-bouncer package to configure the firewall bouncer.')) - ]), - E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Close')) + submitRegister: function() { + var self = this; + var name = document.getElementById('bouncer-name').value.trim(); + if (!name || !/^[a-z0-9_-]+$/i.test(name)) { + self.showToast('Invalid bouncer name', 'error'); + return; + } + api.registerBouncer(name).then(function(r) { + self.closeRegisterModal(); + if (r.success && r.api_key) { + ui.showModal('Bouncer Registered', [ + E('p', { 'style': 'color: var(--soc-success);' }, 'Bouncer "' + name + '" registered!'), + E('p', { 'style': 'margin-top: 12px;' }, 'API Key:'), + E('code', { 'style': 'display: block; background: var(--soc-bg); padding: 12px; border-radius: 4px; word-break: break-all; margin: 8px 0;' }, r.api_key), + E('p', { 'style': 'color: var(--soc-warning); font-size: 12px;' }, 'Save this key now - it will not be shown again!'), + E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [ + E('button', { 'class': 'soc-btn', 'click': function() { ui.hideModal(); self.pollData(); } }, 'Close') ]) ]); - return; + } else { + self.showToast('Failed: ' + (r.error || 'Unknown'), 'error'); } - - ui.hideModal(); - ui.showModal(_('Firewall Bouncer Configuration'), [ - E('div', { 'class': 'cbi-section' }, [ - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Enabled')), - E('div', { 'class': 'cbi-value-field' }, [ - E('span', { - 'class': 'badge', - 'style': 'background: ' + (config.enabled === '1' ? '#28a745' : '#dc3545') + '; color: white; padding: 0.25em 0.6em;' - }, config.enabled === '1' ? _('YES') : _('NO')) - ]) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('IPv4 Support')), - E('div', { 'class': 'cbi-value-field' }, config.ipv4 === '1' ? _('Enabled') : _('Disabled')) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('IPv6 Support')), - E('div', { 'class': 'cbi-value-field' }, config.ipv6 === '1' ? _('Enabled') : _('Disabled')) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('API URL')), - E('div', { 'class': 'cbi-value-field' }, E('code', {}, config.api_url || 'N/A')) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Update Frequency')), - E('div', { 'class': 'cbi-value-field' }, config.update_frequency || 'N/A') - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Deny Action')), - E('div', { 'class': 'cbi-value-field' }, config.deny_action || 'drop') - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Deny Logging')), - E('div', { 'class': 'cbi-value-field' }, config.deny_log === '1' ? _('Enabled') : _('Disabled')) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Log Prefix')), - E('div', { 'class': 'cbi-value-field' }, E('code', {}, config.log_prefix || 'N/A')) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Interfaces')), - E('div', { 'class': 'cbi-value-field' }, - config.interfaces && config.interfaces.length > 0 ? - config.interfaces.join(', ') : - _('None configured') - ) - ]), - E('div', { 'class': 'cbi-section', 'style': 'background: #e8f4f8; padding: 1em; margin-top: 1em; border-radius: 4px;' }, [ - E('p', { 'style': 'margin: 0;' }, [ - E('strong', {}, _('Note:')), - ' ', - _('To modify these settings, edit /etc/config/crowdsec using UCI commands or the configuration file.') - ]) - ]) - ]), - E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Close')) - ]) - ]); - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', _('Failed to load configuration: %s').format(err.message || err)), 'error'); }); }, - handleSaveApply: null, - handleSave: null, - handleReset: null + pollData: function() { + var self = this; + return Promise.all([ + api.getBouncers(), + api.getFirewallBouncerStatus(), + api.getNftablesStats() + ]).then(function(data) { + self.bouncers = (data[0] && data[0].bouncers) || data[0] || []; + self.fwStatus = data[1] || {}; + self.nftStats = data[2] || {}; + var el = document.getElementById('bouncers-list'); + if (el) dom.content(el, self.renderBouncers(self.bouncers)); + }); + }, + + 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 }); diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/decisions.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/decisions.js index d2c570d9..569f219b 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/decisions.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/decisions.js @@ -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 }); diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js index 9a3eeb42..cebcb868 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js @@ -1,1086 +1,236 @@ 'use strict'; 'require view'; -'require secubox-theme/theme as Theme'; 'require dom'; 'require poll'; 'require ui'; -'require fs'; -'require crowdsec-dashboard/api as api'; -'require crowdsec-dashboard/nav as CsNav'; -'require secubox-portal/header as SbHeader'; +'require crowdsec-dashboard.api as api'; /** - * CrowdSec Dashboard - Overview View - * Main dashboard with stats, charts, and recent activity - * Copyright (C) 2024 CyberMind.fr - Gandalf + * CrowdSec SOC Dashboard - Overview + * Minimal SOC-compliant design with GeoIP + * Version 1.0.0 */ return view.extend({ - title: _('CrowdSec Dashboard'), - - css: null, - data: null, - csApi: null, + title: _('CrowdSec SOC'), load: function() { - // Load CSS - var cssLink = document.createElement('link'); - cssLink.rel = 'stylesheet'; - cssLink.href = L.resource('crowdsec-dashboard/dashboard.css'); - document.head.appendChild(cssLink); + 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'); - // Load API - this.csApi = api; - - // Use consolidated API call + secondary calls for extended data - return Promise.all([ - this.csApi.getOverview().catch(function() { return {}; }), - this.csApi.getCapiMetrics().catch(function() { return {}; }), - this.csApi.getCollections().catch(function() { return { collections: [] }; }), - this.csApi.getNftablesStats().catch(function() { return {}; }), - this.csApi.getHub().catch(function() { return {}; }) - ]).then(this.transformOverviewData.bind(this)); + return api.getOverview().catch(function() { return {}; }); }, - // Transform getOverview response to expected data structure - transformOverviewData: function(results) { - var overview = results[0] || {}; - var capiMetrics = results[1] || {}; - var collectionsData = results[2] || {}; - var nftablesStats = results[3] || {}; - var hubData = results[4] || {}; - - // Parse raw JSON strings for scenarios and countries - var topScenarios = []; - var topCountries = []; - try { - if (overview.top_scenarios_raw) { - topScenarios = JSON.parse(overview.top_scenarios_raw); - } - } catch(e) { topScenarios = []; } - try { - if (overview.top_countries_raw) { - topCountries = JSON.parse(overview.top_countries_raw); - } - } catch(e) { topCountries = []; } - - // Build compatible data structure - var dashboardData = { - status: { - crowdsec: overview.crowdsec || 'unknown', - bouncer: overview.bouncer || 'unknown', - version: overview.version || 'unknown' - }, - stats: { - total_decisions: overview.total_decisions || 0, - alerts_today: overview.alerts_24h || 0, - alerts_week: overview.alerts_24h || 0, - scenarios_triggered: topScenarios.length, - top_countries: topCountries.map(function(c) { - return { country: c.country, count: c.count }; - }) - }, - decisions: overview.decisions || [], - alerts: overview.alerts || [], - error: null - }; - - var logsData = { - entries: overview.logs || [] - }; - - var healthCheck = { - crowdsec_running: overview.crowdsec === 'running', - lapi_status: overview.lapi_status || 'unavailable', - capi_status: overview.capi_enrolled ? 'connected' : 'disconnected', - capi_enrolled: overview.capi_enrolled || false, - capi_subscription: 'COMMUNITY', - sharing_signals: overview.capi_enrolled || false, - pulling_blocklist: overview.capi_enrolled || false, - version: overview.version || 'N/A', - decisions_count: overview.total_decisions || 0 - }; - - // Return array matching expected payload structure - return [ - dashboardData, - logsData, - healthCheck, - capiMetrics, - collectionsData, - nftablesStats, - hubData - ]; - }, - - renderHeader: function(status) { - var header = E('div', { 'class': 'cs-header' }, [ - E('div', { 'class': 'cs-logo' }, [ - E('div', { 'class': 'cs-logo-icon' }, '🛡️'), - E('div', { 'class': 'cs-logo-text' }, [ - 'Crowd', - E('span', {}, 'Sec'), - ' Dashboard' - ]) - ]), - E('div', { 'class': 'cs-status-badges' }, [ - E('div', { 'class': 'cs-badge' }, [ - E('span', { - 'class': 'cs-badge-dot ' + (status.crowdsec === 'running' ? 'running' : 'stopped') - }), - 'Engine: ' + (status.crowdsec || 'unknown') - ]), - E('div', { 'class': 'cs-badge' }, [ - E('span', { - 'class': 'cs-badge-dot ' + (status.bouncer === 'running' ? 'running' : 'stopped') - }), - 'Bouncer: ' + (status.bouncer || 'unknown') - ]), - E('div', { 'class': 'cs-badge' }, [ - 'v' + (status.version || 'N/A') - ]) - ]) - ]); - - return header; - }, - - renderStatsGrid: function(stats, decisions) { + render: function(data) { var self = this; - - // Count by action type - var banCount = 0; - var captchaCount = 0; - - if (Array.isArray(decisions)) { - decisions.forEach(function(d) { - if (d.type === 'ban') banCount++; - else if (d.type === 'captcha') captchaCount++; - }); - } - - var grid = E('div', { 'class': 'cs-stats-grid' }, [ - E('div', { 'class': 'cs-stat-card' }, [ - E('div', { 'class': 'cs-stat-label' }, 'Active Bans'), - E('div', { 'class': 'cs-stat-value danger' }, String(stats.total_decisions || 0)), - E('div', { 'class': 'cs-stat-trend' }, 'Currently blocked IPs'), - E('div', { 'class': 'cs-stat-icon' }, '🚫') - ]), - E('div', { 'class': 'cs-stat-card' }, [ - E('div', { 'class': 'cs-stat-label' }, 'Alerts (24h)'), - E('div', { 'class': 'cs-stat-value warning' }, String(stats.alerts_24h || 0)), - E('div', { 'class': 'cs-stat-trend' }, 'Detected threats'), - E('div', { 'class': 'cs-stat-icon' }, '⚠️') - ]), - E('div', { 'class': 'cs-stat-card' }, [ - E('div', { 'class': 'cs-stat-label' }, 'Bouncers'), - E('div', { 'class': 'cs-stat-value success' }, String(stats.bouncers || 0)), - E('div', { 'class': 'cs-stat-trend' }, 'Active remediation'), - E('div', { 'class': 'cs-stat-icon' }, '🔒') - ]), - E('div', { 'class': 'cs-stat-card' }, [ - E('div', { 'class': 'cs-stat-label' }, 'Ban Rate'), - E('div', { 'class': 'cs-stat-value' }, banCount > 0 ? '100%' : '0%'), - E('div', { 'class': 'cs-stat-trend' }, banCount + ' bans / ' + captchaCount + ' captchas'), - E('div', { 'class': 'cs-stat-icon' }, '📊') - ]) - ]); - - return grid; - }, + var status = data || {}; - renderDecisionsTable: function(decisions) { - var self = this; - - if (!Array.isArray(decisions) || decisions.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' }, '✅'), - E('p', {}, 'No active decisions - All clear!') - ]); - } - - var rows = decisions.slice(0, 10).map(function(d) { - return E('tr', {}, [ - 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', {}, 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('button', { - 'class': 'cs-btn cs-btn-danger cs-btn-sm', - 'data-ip': d.value, - 'click': ui.createHandlerFn(self, 'handleUnban', d.value) - }, 'Unban')) - ]); - }); - - return E('table', { 'class': 'cs-table' }, [ - E('thead', {}, E('tr', {}, [ - E('th', {}, 'IP Address'), - E('th', {}, 'Scenario'), - E('th', {}, 'Country'), - E('th', {}, 'Action'), - E('th', {}, 'Expires'), - E('th', {}, 'Actions') - ])), - E('tbody', {}, rows) - ]); - }, - - renderAlertsTimeline: function(alerts) { - var self = this; - - if (!Array.isArray(alerts) || alerts.length === 0) { - return E('div', { 'class': 'cs-empty' }, [ - E('div', { 'class': 'cs-empty-icon' }, '📭'), - E('p', {}, 'No recent alerts') - ]); - } - - var items = alerts.slice(0, 8).map(function(a) { - return E('div', { 'class': 'cs-timeline-item alert' }, [ - E('div', { 'class': 'cs-timeline-time' }, self.csApi.formatRelativeTime(a.created_at)), - E('div', { 'class': 'cs-timeline-content' }, [ - E('strong', {}, self.csApi.parseScenario(a.scenario)), - E('br', {}), - E('span', { 'class': 'cs-ip' }, a.source?.ip || 'N/A'), - ' → ', - E('span', {}, (a.events_count || 0) + ' events') - ]) - ]); - }); - - return E('div', { 'class': 'cs-timeline' }, items); - }, - - renderTopScenarios: function(stats) { - var scenarios = []; - - try { - if (stats.top_scenarios_raw) { - scenarios = JSON.parse(stats.top_scenarios_raw); - } - } catch(e) { - scenarios = []; - } - - if (scenarios.length === 0) { - return E('div', { 'class': 'cs-empty' }, [ - E('p', {}, 'No scenario data available') - ]); - } - - var maxCount = Math.max.apply(null, scenarios.map(function(s) { return s.count; })); - - var bars = scenarios.map(function(s) { - var pct = maxCount > 0 ? (s.count / maxCount * 100) : 0; - return E('div', { 'class': 'cs-bar-item' }, [ - E('div', { 'class': 'cs-bar-label', 'title': s.scenario }, s.scenario.split('/').pop()), - E('div', { 'class': 'cs-bar-track' }, [ - E('div', { 'class': 'cs-bar-fill', 'style': 'width: ' + pct + '%' }) - ]), - E('div', { 'class': 'cs-bar-value' }, String(s.count)) - ]); - }); - - return E('div', { 'class': 'cs-bar-chart' }, bars); - }, - - renderTopCountries: function(stats) { - var self = this; - var countries = []; - - try { - if (stats.top_countries_raw) { - countries = JSON.parse(stats.top_countries_raw); - } - } catch(e) { - countries = []; - } - - if (countries.length === 0) { - return E('div', { 'class': 'cs-empty' }, [ - E('p', {}, 'No country data available') - ]); - } - - var maxCount = Math.max.apply(null, countries.map(function(c) { return c.count; })); - - var bars = countries.map(function(c) { - var pct = maxCount > 0 ? (c.count / maxCount * 100) : 0; - return E('div', { 'class': 'cs-bar-item' }, [ - E('div', { 'class': 'cs-bar-label' }, [ - self.csApi.getCountryFlag(c.country), - ' ', - c.country || 'N/A' - ]), - E('div', { 'class': 'cs-bar-track' }, [ - E('div', { 'class': 'cs-bar-fill', 'style': 'width: ' + pct + '%' }) - ]), - E('div', { 'class': 'cs-bar-value' }, String(c.count)) - ]); - }); - - return E('div', { 'class': 'cs-bar-chart' }, bars); - }, - - 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'), - 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', - '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') - ]) - ]) - ]); - }, - - 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'); - // Refresh data - return self.refreshDashboard(); - } else { - self.showToast('Failed to unban: ' + (result.error || 'Unknown error'), 'error'); - } - }).then(function(data) { - if (data) { - self.data = data; - self.updateView(); - } - }).catch(function(err) { - self.showToast('Error: ' + err.message, 'error'); - }); - }, - - openBanModal: function(ev) { - document.getElementById('ban-modal').style.display = 'flex'; - }, - - 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) { - 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; - } - - self.csApi.banIP(ip, duration, reason).then(function(result) { - if (result.success) { - self.showToast('IP ' + ip + ' banned for ' + duration, 'success'); - self.closeBanModal(); - return self.refreshDashboard(); - } else { - self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error'); - } - }).then(function(data) { - if (data) { - self.data = data; - self.updateView(); - } - }).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); - }, - - updateView: function() { - var container = document.getElementById('cs-dashboard-content'); - if (!container || !this.data) return; - - dom.content(container, this.renderContent(this.data)); - }, - - renderContent: function(data) { - var status = data.status || {}; - var stats = data.stats || {}; - var decisions = data.decisions || []; - var alerts = data.alerts || []; - var logs = this.logs || []; - - // Check if service is not running - var serviceWarning = null; - if (data.error && status.crowdsec !== 'running') { - serviceWarning = E('div', { 'class': 'cs-warning-banner' }, [ - E('div', { 'class': 'cs-warning-icon' }, '⚠️'), - E('div', { 'class': 'cs-warning-content' }, [ - E('div', { 'class': 'cs-warning-title' }, 'CrowdSec Service Not Running'), - E('div', { 'class': 'cs-warning-message' }, [ - 'The CrowdSec engine is currently stopped. ', - E('a', { - 'href': '#', - 'click': ui.createHandlerFn(this, 'startCrowdSec') - }, 'Click here to start the service'), - ' or use the command: ', - E('code', {}, '/etc/init.d/crowdsec start') - ]) - ]) - ]); - } - - return E('div', {}, [ + var view = E('div', { 'class': 'soc-dashboard' }, [ this.renderHeader(status), - serviceWarning || E([]), - this.renderHealthCheck(), - this.renderStatsGrid(stats, decisions), - - E('div', { 'class': 'cs-charts-row' }, [ - E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, 'Top Scenarios'), - ]), - E('div', { 'class': 'cs-card-body' }, this.renderTopScenarios(stats)) + this.renderNav('overview'), + E('div', { 'id': 'soc-stats' }, this.renderStats(status)), + E('div', { 'class': 'soc-grid-2' }, [ + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, ['Recent Alerts', E('span', { 'class': 'soc-time' }, 'Last 24h')]), + E('div', { 'class': 'soc-card-body', 'id': 'recent-alerts' }, this.renderAlerts(status.alerts || [])) ]), - E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, 'Top Countries'), - ]), - E('div', { 'class': 'cs-card-body' }, this.renderTopCountries(stats)) + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, 'Threat Origins'), + E('div', { 'class': 'soc-card-body', 'id': 'geo-dist' }, this.renderGeo(status.countries || {})) ]) ]), - - E('div', { 'class': 'cs-charts-row' }, [ - this.renderCapiBlocklist(), - this.renderCollectionsCard(), - this.renderHubStatsCard() - ]), - - E('div', { 'class': 'cs-charts-row' }, [ - this.renderFirewallHealth(), - this.renderFirewallBlocks() - ]), - - E('div', { 'class': 'cs-charts-row' }, [ - E('div', { 'class': 'cs-card', 'style': 'flex: 2' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, 'Active Decisions'), - E('button', { - 'class': 'cs-btn cs-btn-primary cs-btn-sm', - 'click': ui.createHandlerFn(this, 'openBanModal') - }, '+ Add Ban') - ]), - E('div', { 'class': 'cs-card-body no-padding' }, this.renderDecisionsTable(decisions)) + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, [ + 'System Health', + E('button', { 'class': 'soc-btn soc-btn-sm', 'click': function() { self.runHealthCheck(); } }, 'Test') ]), - E('div', { 'class': 'cs-card', 'style': 'flex: 1' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, 'Recent Alerts'), - ]), - E('div', { 'class': 'cs-card-body' }, this.renderAlertsTimeline(alerts)) - ]), - this.renderLogCard(logs) + E('div', { 'class': 'soc-card-body', 'id': 'health-check' }, this.renderHealth(status)) ]), - - this.renderBanModal() + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, 'Active Scenarios'), + E('div', { 'class': 'soc-card-body' }, this.renderScenarios(status.scenarios || [])) + ]) ]); + + poll.add(L.bind(this.pollData, this), 30); + return view; }, - startCrowdSec: function(ev) { - var self = this; - ev.preventDefault(); - - ui.showModal(_('Start CrowdSec'), [ - E('p', {}, _('Do you want to start the CrowdSec service?')), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Cancel')), - ' ', - E('button', { - 'class': 'btn cbi-button-positive', - 'click': function() { - return fs.exec('/etc/init.d/crowdsec', ['start']).then(function() { - ui.hideModal(); - self.showToast('CrowdSec service started', 'success'); - setTimeout(function() { - return self.refreshDashboard(); - }, 2000); - }).catch(function(err) { - ui.hideModal(); - self.showToast('Failed to start service: ' + err, 'error'); - }); - } - }, _('Start Service')) + renderHeader: function(s) { + 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 2zm0 2.18l6.9 3.45L12 11.09 5.1 7.63 12 4.18zM4 8.82l7 3.5v7.36l-7-3.5V8.82zm9 10.86v-7.36l7-3.5v7.36l-7 3.5z' }) + ]), + 'CrowdSec Security Operations' + ]), + E('div', { 'class': 'soc-status' }, [ + E('span', { 'class': 'soc-status-dot ' + (s.crowdsec === 'running' ? 'online' : 'offline') }), + s.crowdsec === 'running' ? 'OPERATIONAL' : 'OFFLINE' ]) ]); }, - render: function(payload) { - var self = this; - this.data = payload[0] || {}; - this.logs = (payload[1] && payload[1].entries) || []; - this.healthCheck = payload[2] || {}; - this.capiMetrics = payload[3] || {}; - this.collections = (payload[4] && payload[4].collections) || []; - this.nftablesStats = payload[5] || {}; - this.hubData = payload[6] || {}; - - // Main wrapper with SecuBox header - var wrapper = E('div', { 'class': 'secubox-page-wrapper' }); - wrapper.appendChild(SbHeader.render()); - - var view = E('div', { 'class': 'crowdsec-dashboard' }, [ - CsNav.renderTabs('overview'), - E('div', { 'id': 'cs-dashboard-content' }, this.renderContent(this.data)) - ]); - - wrapper.appendChild(view); - - // Setup polling for auto-refresh (every 60 seconds) - poll.add(function() { - return self.refreshDashboard(); - }, 60); - - return wrapper; - }, - - refreshDashboard: function() { - var self = this; - // Use consolidated API call + secondary calls for extended data - return Promise.all([ - self.csApi.getOverview().catch(function() { return {}; }), - self.csApi.getCapiMetrics().catch(function() { return {}; }), - self.csApi.getCollections().catch(function() { return { collections: [] }; }), - self.csApi.getNftablesStats().catch(function() { return {}; }), - self.csApi.getHub().catch(function() { return {}; }) - ]).then(function(results) { - var transformed = self.transformOverviewData(results); - self.data = transformed[0]; - self.logs = (transformed[1] && transformed[1].entries) || []; - self.healthCheck = transformed[2] || {}; - self.capiMetrics = transformed[3] || {}; - self.collections = (transformed[4] && transformed[4].collections) || []; - self.nftablesStats = transformed[5] || {}; - self.hubData = transformed[6] || {}; - self.updateView(); - }); - }, - - // Health Check Section - Shows LAPI/CAPI/Console status - renderHealthCheck: function() { - var health = this.healthCheck || {}; - var csRunning = health.crowdsec_running; - var lapiStatus = health.lapi_status || 'unavailable'; - var capiStatus = health.capi_status || 'disconnected'; - var capiEnrolled = health.capi_enrolled; - var capiSubscription = health.capi_subscription || '-'; - var sharingSignals = health.sharing_signals; - var pullingBlocklist = health.pulling_blocklist; - var version = health.version || 'N/A'; - var decisionsCount = health.decisions_count || 0; - - return E('div', { 'class': 'cs-health-check', 'style': 'margin-bottom: 1.5em;' }, [ - E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, _('System Health')) - ]), - E('div', { 'class': 'cs-card-body' }, [ - E('div', { 'class': 'cs-health-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1em;' }, [ - // CrowdSec Status - E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [ - E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, csRunning ? '✅' : '❌'), - E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'CrowdSec'), - E('div', { 'style': 'font-size: 0.85em; color: ' + (csRunning ? '#00d4aa' : '#ff4757') + ';' }, csRunning ? 'Running' : 'Stopped'), - E('div', { 'style': 'font-size: 0.75em; color: #888;' }, version ? (version.charAt(0) === 'v' ? version : 'v' + version) : '') - ]), - // LAPI Status - E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [ - E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, lapiStatus === 'available' ? '✅' : '❌'), - E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'LAPI'), - E('div', { 'style': 'font-size: 0.85em; color: ' + (lapiStatus === 'available' ? '#00d4aa' : '#ff4757') + ';' }, lapiStatus === 'available' ? 'Available' : 'Unavailable'), - E('div', { 'style': 'font-size: 0.75em; color: #888;' }, ':8080') - ]), - // CAPI Status - E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [ - E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, capiStatus === 'connected' ? '✅' : '⚠️'), - E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'CAPI'), - E('div', { 'style': 'font-size: 0.85em; color: ' + (capiStatus === 'connected' ? '#00d4aa' : '#ffa500') + ';' }, capiStatus === 'connected' ? 'Connected' : 'Disconnected'), - E('div', { 'style': 'font-size: 0.75em; color: #888;' }, capiSubscription) - ]), - // Console Status - E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [ - E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, capiEnrolled ? '✅' : '⚪'), - E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'Console'), - E('div', { 'style': 'font-size: 0.85em; color: ' + (capiEnrolled ? '#00d4aa' : '#888') + ';' }, capiEnrolled ? 'Enrolled' : 'Not Enrolled'), - E('div', { 'style': 'font-size: 0.75em; color: #888;' }, sharingSignals ? 'Sharing: ON' : 'Sharing: OFF') - ]), - // Blocklist Status - E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [ - E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, pullingBlocklist ? '🛡️' : '⚪'), - E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'Blocklist'), - E('div', { 'style': 'font-size: 0.85em; color: ' + (pullingBlocklist ? '#00d4aa' : '#888') + ';' }, pullingBlocklist ? 'Active' : 'Inactive'), - E('div', { 'style': 'font-size: 0.75em; color: #667eea; font-weight: 600;' }, decisionsCount.toLocaleString() + ' IPs') - ]) - ]) - ]) - ]) - ]); - }, - - // CAPI Blocklist Metrics - Shows blocked IPs by category - renderCapiBlocklist: function() { - var metrics = this.capiMetrics || {}; - var totalCapi = metrics.total_capi || 0; - var totalLocal = metrics.total_local || 0; - var breakdown = metrics.breakdown || []; - - if (totalCapi === 0 && totalLocal === 0) { - return E('span'); // Empty if no data - } - - // Build breakdown bars - var maxCount = Math.max.apply(null, breakdown.map(function(b) { return b.count || 0; }).concat([1])); - var breakdownBars = breakdown.slice(0, 5).map(function(item) { - var scenario = item.scenario || 'unknown'; - var count = item.count || 0; - var pct = Math.round((count / maxCount) * 100); - var displayName = scenario.split('/').pop().replace(/-/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); }); - - return E('div', { 'style': 'margin-bottom: 0.75em;' }, [ - E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.25em;' }, [ - E('span', { 'style': 'font-size: 0.85em;' }, displayName), - E('span', { 'style': 'font-size: 0.85em; font-weight: 600; color: #667eea;' }, count.toLocaleString()) - ]), - E('div', { 'style': 'height: 8px; background: rgba(102,126,234,0.2); border-radius: 4px; overflow: hidden;' }, [ - E('div', { 'style': 'height: 100%; width: ' + pct + '%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px;' }) - ]) - ]); - }); - - return E('div', { 'class': 'cs-capi-blocklist', 'style': 'margin-bottom: 1.5em;' }, [ - E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, _('Community Blocklist (CAPI)')) - ]), - E('div', { 'class': 'cs-card-body' }, [ - E('div', { 'style': 'display: flex; gap: 2em; margin-bottom: 1em;' }, [ - E('div', { 'style': 'text-align: center;' }, [ - E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #667eea;' }, totalCapi.toLocaleString()), - E('div', { 'style': 'font-size: 0.8em; color: #888;' }, 'CAPI Blocked') - ]), - E('div', { 'style': 'text-align: center;' }, [ - E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #00d4aa;' }, totalLocal.toLocaleString()), - E('div', { 'style': 'font-size: 0.8em; color: #888;' }, 'Local Blocked') - ]) - ]), - breakdownBars.length > 0 ? E('div', { 'style': 'margin-top: 1em;' }, [ - E('div', { 'style': 'font-size: 0.85em; font-weight: 600; margin-bottom: 0.75em; color: #888;' }, _('Top Blocked Categories')), - E('div', {}, breakdownBars) - ]) : E('span') - ]) - ]) - ]); - }, - - // Collections Card - Shows installed collections with quick actions - renderCollectionsCard: function() { - var self = this; - var collections = this.collections || []; - - if (!collections.length) { - return E('span'); // Empty if no collections - } - - var collectionItems = collections.slice(0, 6).map(function(col) { - var name = col.name || col.Name || 'unknown'; - var status = col.status || col.Status || ''; - var version = col.version || col.Version || ''; - var isInstalled = status.toLowerCase().indexOf('enabled') >= 0 || status.toLowerCase().indexOf('installed') >= 0; - var hasUpdate = status.toLowerCase().indexOf('update') >= 0; - - return E('div', { 'style': 'display: flex; align-items: center; justify-content: space-between; padding: 0.5em 0; border-bottom: 1px solid rgba(255,255,255,0.1);' }, [ - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ - E('span', { 'style': 'font-size: 1.2em;' }, isInstalled ? '✅' : '⬜'), - E('span', { 'style': 'font-size: 0.9em;' }, name) - ]), - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ - E('span', { 'style': 'font-size: 0.75em; color: #888;' }, 'v' + version), - hasUpdate ? E('span', { 'style': 'font-size: 0.7em; padding: 0.15em 0.4em; background: #ffa500; color: #000; border-radius: 3px;' }, 'UPDATE') : E('span') - ]) - ]); - }); - - return E('div', { 'class': 'cs-collections-card' }, [ - E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, _('Installed Collections')), - E('button', { - 'class': 'cs-btn cs-btn-secondary cs-btn-sm', - 'click': ui.createHandlerFn(this, 'handleUpdateHub') - }, _('Update Hub')) - ]), - E('div', { 'class': 'cs-card-body' }, collectionItems) - ]) - ]); - }, - - handleUpdateHub: function() { - var self = this; - ui.showModal(_('Updating Hub'), [ - E('p', {}, _('Downloading latest hub index...')), - E('div', { 'class': 'spinning' }) - ]); - - this.csApi.updateHub().then(function(result) { - ui.hideModal(); - if (result && result.success) { - self.showToast(_('Hub updated successfully'), 'success'); - self.refreshDashboard(); - } else { - self.showToast((result && result.error) || _('Hub update failed'), 'error'); - } - }).catch(function(err) { - ui.hideModal(); - self.showToast(err.message || _('Hub update failed'), 'error'); - }); - }, - - // Hub Stats Card - Shows installed parsers, scenarios, collections counts - renderHubStatsCard: function() { - var hub = this.hubData || {}; - - // Count installed items by type - var parsers = hub.parsers || []; - var scenarios = hub.scenarios || []; - var collections = hub.collections || []; - var postoverflows = hub.postoverflows || []; - - var installedParsers = parsers.filter(function(p) { - return p.installed || (p.status && p.status.toLowerCase().indexOf('enabled') >= 0); - }); - var installedScenarios = scenarios.filter(function(s) { - return s.installed || (s.status && s.status.toLowerCase().indexOf('enabled') >= 0); - }); - var installedCollections = collections.filter(function(c) { - return c.installed || (c.status && c.status.toLowerCase().indexOf('enabled') >= 0); - }); - - // Get parser details for display - var parserList = installedParsers.slice(0, 8).map(function(p) { - var name = p.name || p.Name || 'unknown'; - var shortName = name.split('/').pop(); - return E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.35em 0; border-bottom: 1px solid rgba(255,255,255,0.05);' }, [ - E('span', { 'style': 'color: #22c55e;' }, '✓'), - E('span', { 'style': 'font-size: 0.85em;' }, shortName) - ]); - }); - - return E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, _('Hub Components')) - ]), - E('div', { 'class': 'cs-card-body' }, [ - // Mini stats row - E('div', { 'style': 'display: flex; gap: 1em; margin-bottom: 1em;' }, [ - E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(34,197,94,0.1); border-radius: 8px;' }, [ - E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #22c55e;' }, String(installedParsers.length)), - E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Parsers')) - ]), - E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(59,130,246,0.1); border-radius: 8px;' }, [ - E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #3b82f6;' }, String(installedScenarios.length)), - E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Scenarios')) - ]), - E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(168,85,247,0.1); border-radius: 8px;' }, [ - E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #a855f7;' }, String(installedCollections.length)), - E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Collections')) - ]) - ]), - // Parser list - E('div', { 'style': 'font-size: 0.8em; font-weight: 600; color: #94a3b8; margin-bottom: 0.5em;' }, _('Installed Parsers')), - parserList.length > 0 ? E('div', {}, parserList) : E('div', { 'style': 'color: #666; font-size: 0.85em;' }, _('No parsers installed')) - ]) - ]); - }, - - // Firewall Health Status Card - renderFirewallHealth: function() { - var stats = this.nftablesStats || {}; - var health = stats.firewall_health || {}; - - var status = health.status || 'unknown'; - var issues = health.issues || ''; - var bouncerRunning = health.bouncer_running; - var uciEnabled = health.uci_enabled; - var apiKeyConfigured = health.api_key_configured; - var inputHooked = health.input_chain_hooked; - var forwardHooked = health.forward_chain_hooked; - var setsHaveTimeout = health.sets_have_timeout; - var decisionsSynced = health.decisions_synced; - var cscliDecisions = health.cscli_decisions_count || 0; - var nftElements = health.nft_elements_count || 0; - - var statusColor = status === 'ok' ? '#00d4aa' : (status === 'warning' ? '#ffa500' : '#ff4757'); - var statusIcon = status === 'ok' ? '✅' : (status === 'warning' ? '⚠️' : '❌'); - var statusText = status === 'ok' ? 'Healthy' : (status === 'warning' ? 'Warning' : 'Error'); - - var checkItems = [ - { label: 'Bouncer Process', ok: bouncerRunning, detail: bouncerRunning ? 'Running' : 'Not running' }, - { label: 'UCI Enabled', ok: uciEnabled, detail: uciEnabled ? 'Enabled' : 'Disabled' }, - { label: 'API Key', ok: apiKeyConfigured, detail: apiKeyConfigured ? 'Configured' : 'Missing or default' }, - { label: 'Input Chain', ok: inputHooked, detail: inputHooked ? 'Hooked' : 'Not hooked' }, - { label: 'Forward Chain', ok: forwardHooked, detail: forwardHooked ? 'Hooked' : 'Not hooked' }, - { label: 'Set Timeout', ok: setsHaveTimeout, detail: setsHaveTimeout ? 'Enabled' : 'Disabled' }, - { label: 'Decisions Sync', ok: decisionsSynced, detail: decisionsSynced ? (nftElements + ' synced') : 'Out of sync' } + renderNav: function(active) { + var tabs = [ + { id: 'overview', label: 'Overview' }, + { id: 'alerts', label: 'Alerts' }, + { id: 'decisions', label: 'Decisions' }, + { id: 'bouncers', label: 'Bouncers' }, + { id: 'settings', label: 'Settings' } ]; - - var checkRows = checkItems.map(function(item) { - return E('div', { 'style': 'display: flex; align-items: center; justify-content: space-between; padding: 0.4em 0; border-bottom: 1px solid rgba(255,255,255,0.05);' }, [ - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ - E('span', { 'style': 'font-size: 1em;' }, item.ok ? '✅' : '❌'), - E('span', { 'style': 'font-size: 0.85em;' }, item.label) - ]), - E('span', { 'style': 'font-size: 0.75em; color: ' + (item.ok ? '#00d4aa' : '#ff4757') + ';' }, item.detail) - ]); - }); - - return E('div', { 'class': 'cs-card', 'style': 'flex: 1;' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, [ - _('Firewall Health'), - E('span', { - 'style': 'margin-left: 0.75em; font-size: 0.8em; padding: 0.2em 0.6em; background: ' + statusColor + '; border-radius: 12px;' - }, statusIcon + ' ' + statusText) - ]) - ]), - E('div', { 'class': 'cs-card-body' }, [ - // Status summary - issues ? E('div', { 'style': 'background: rgba(255,71,87,0.1); border: 1px solid rgba(255,71,87,0.3); border-radius: 8px; padding: 0.75em; margin-bottom: 1em;' }, [ - E('div', { 'style': 'font-size: 0.85em; color: #ff4757;' }, issues) - ]) : E('span'), - // Sync stats - E('div', { 'style': 'display: flex; gap: 1em; margin-bottom: 1em;' }, [ - E('div', { 'style': 'flex: 1; text-align: center; padding: 0.5em; background: rgba(102,126,234,0.1); border-radius: 8px;' }, [ - E('div', { 'style': 'font-size: 1.25em; font-weight: 700; color: #667eea;' }, String(cscliDecisions)), - E('div', { 'style': 'font-size: 0.7em; color: #888;' }, 'Decisions') - ]), - E('div', { 'style': 'flex: 1; text-align: center; padding: 0.5em; background: rgba(0,212,170,0.1); border-radius: 8px;' }, [ - E('div', { 'style': 'font-size: 1.25em; font-weight: 700; color: #00d4aa;' }, String(nftElements)), - E('div', { 'style': 'font-size: 0.7em; color: #888;' }, 'In Firewall') - ]) - ]), - // Check items - E('div', {}, checkRows) - ]) - ]); + return E('div', { 'class': 'soc-nav' }, tabs.map(function(t) { + return E('a', { + 'href': L.url('admin/secubox/security/crowdsec/' + t.id), + 'class': active === t.id ? 'active' : '' + }, t.label); + })); }, - // Firewall Blocks - Shows IPs blocked in nftables - renderFirewallBlocks: function() { - var self = this; - var stats = this.nftablesStats || {}; + renderStats: function(d) { + var stats = [ + { label: 'Active Bans', value: d.total_decisions || 0, type: (d.total_decisions || 0) > 0 ? 'danger' : '' }, + { label: 'Alerts (24h)', value: d.alerts_24h || 0, type: (d.alerts_24h || 0) > 10 ? 'warning' : '' }, + { label: 'Scenarios', value: d.scenario_count || 0, type: 'success' }, + { label: 'Parsers', value: d.parser_count || 0, type: '' }, + { label: 'Bouncers', value: d.bouncer_count || 0, type: (d.bouncer_count || 0) > 0 ? 'success' : 'warning' }, + { label: 'Countries', value: Object.keys(d.countries || {}).length, type: '' } + ]; + return E('div', { 'class': 'soc-stats' }, stats.map(function(s) { + return E('div', { 'class': 'soc-stat ' + s.type }, [ + E('div', { 'class': 'soc-stat-value' }, String(s.value)), + E('div', { 'class': 'soc-stat-label' }, s.label) + ]); + })); + }, - // Check if nftables available - if (stats.error) { - return E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, _('Firewall Blocks')) - ]), - E('div', { 'class': 'cs-card-body' }, [ - E('div', { 'class': 'cs-empty' }, [ - E('div', { 'class': 'cs-empty-icon' }, '⚠️'), - E('p', {}, stats.error) - ]) - ]) + renderAlerts: function(alerts) { + if (!alerts || !alerts.length) { + return E('div', { 'class': 'soc-empty' }, [ + E('div', { 'class': 'soc-empty-icon' }, '\u2713'), + 'No recent alerts' ]); } + return E('table', { 'class': 'soc-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Time'), + E('th', {}, 'Source'), + E('th', {}, 'Scenario'), + E('th', {}, 'Country') + ])), + E('tbody', {}, alerts.slice(0, 10).map(function(a) { + var src = a.source || {}; + var ip = src.ip || a.source_ip || 'N/A'; + var country = src.cn || src.country || ''; + return E('tr', {}, [ + E('td', { 'class': 'soc-time' }, api.formatRelativeTime(a.created_at)), + E('td', {}, E('span', { 'class': 'soc-ip' }, ip)), + E('td', {}, E('span', { 'class': 'soc-scenario' }, api.parseScenario(a.scenario))), + E('td', { 'class': 'soc-geo' }, [ + E('span', { 'class': 'soc-flag' }, api.getCountryFlag(country)), + E('span', { 'class': 'soc-country' }, country) + ]) + ]); + })) + ]); + }, - var ipv4Active = stats.ipv4_table_exists; - var ipv6Active = stats.ipv6_table_exists; - var ipv4List = stats.ipv4_blocked_ips || []; - var ipv6List = stats.ipv6_blocked_ips || []; - var ipv4Rules = stats.ipv4_rules_count || 0; - var ipv6Rules = stats.ipv6_rules_count || 0; - // Use total counts from API (includes all IPs, not just sample) - var ipv4Total = stats.ipv4_total_count || ipv4List.length; - var ipv6Total = stats.ipv6_total_count || ipv6List.length; - var ipv4Capi = stats.ipv4_capi_count || 0; - var ipv4Cscli = stats.ipv4_cscli_count || 0; - var ipv6Capi = stats.ipv6_capi_count || 0; - var ipv6Cscli = stats.ipv6_cscli_count || 0; - var totalBlocked = ipv4Total + ipv6Total; - - // Build IP list (combine IPv4 and IPv6, limit to 20) - var allIps = []; - ipv4List.forEach(function(ip) { allIps.push({ ip: ip, type: 'IPv4' }); }); - ipv6List.forEach(function(ip) { allIps.push({ ip: ip, type: 'IPv6' }); }); - var displayIps = allIps.slice(0, 20); - - var ipRows = displayIps.map(function(item) { - return E('div', { - 'style': 'display: flex; align-items: center; justify-content: space-between; padding: 0.5em 0; border-bottom: 1px solid rgba(255,255,255,0.1);' - }, [ - E('div', { 'style': 'display: flex; align-items: center; gap: 0.75em;' }, [ - E('span', { 'style': 'font-size: 1.1em;' }, '🚫'), - E('code', { 'style': 'font-size: 0.85em; background: rgba(0,0,0,0.2); padding: 0.2em 0.5em; border-radius: 4px;' }, item.ip), - E('span', { - 'style': 'font-size: 0.7em; padding: 0.15em 0.4em; background: ' + (item.type === 'IPv4' ? '#667eea' : '#764ba2') + '; border-radius: 3px;' - }, item.type) - ]), - E('button', { - 'class': 'cs-btn cs-btn-danger cs-btn-sm', - 'style': 'font-size: 0.75em; padding: 0.25em 0.5em;', - 'click': ui.createHandlerFn(self, 'handleUnban', item.ip) - }, _('Unban')) + renderGeo: function(countries) { + var entries = Object.entries(countries || {}); + if (!entries.length) { + return E('div', { 'class': 'soc-empty' }, [ + E('div', { 'class': 'soc-empty-icon' }, '\u{1F30D}'), + 'No geographic data' ]); - }); + } + entries.sort(function(a, b) { return b[1] - a[1]; }); + return E('div', { 'class': 'soc-geo-grid' }, entries.slice(0, 12).map(function(e) { + return E('div', { 'class': 'soc-geo-item' }, [ + E('span', { 'class': 'soc-flag' }, api.getCountryFlag(e[0])), + E('span', { 'class': 'soc-geo-count' }, String(e[1])), + E('span', { 'class': 'soc-country' }, e[0]) + ]); + })); + }, - // Status indicators with breakdown by origin - var statusRow = E('div', { 'style': 'display: flex; gap: 1.5em; margin-bottom: 1em; flex-wrap: wrap;' }, [ - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ - E('span', { 'style': 'font-size: 1.2em;' }, ipv4Active ? '✅' : '❌'), - E('span', { 'style': 'font-size: 0.85em;' }, 'IPv4'), - E('span', { 'style': 'font-size: 0.75em; color: #888;' }, ipv4Total.toLocaleString() + ' IPs') - ]), - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ - E('span', { 'style': 'font-size: 1.2em;' }, ipv6Active ? '✅' : '❌'), - E('span', { 'style': 'font-size: 0.85em;' }, 'IPv6'), - E('span', { 'style': 'font-size: 0.75em; color: #888;' }, ipv6Total.toLocaleString() + ' IPs') - ]), - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding-left: 1em; border-left: 1px solid rgba(255,255,255,0.2);' }, [ - E('span', { 'style': 'font-size: 0.8em; padding: 0.2em 0.5em; background: #667eea; border-radius: 4px;' }, 'CAPI'), - E('span', { 'style': 'font-size: 0.85em; color: #667eea;' }, (ipv4Capi + ipv6Capi).toLocaleString()) - ]), - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ - E('span', { 'style': 'font-size: 0.8em; padding: 0.2em 0.5em; background: #00d4aa; border-radius: 4px;' }, 'Local'), - E('span', { 'style': 'font-size: 0.85em; color: #00d4aa;' }, (ipv4Cscli + ipv6Cscli).toLocaleString()) - ]) - ]); - - return E('div', { 'class': 'cs-card', 'style': 'flex: 2;' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, [ - _('Firewall Blocks'), - E('span', { - 'style': 'margin-left: 0.75em; font-size: 0.8em; padding: 0.2em 0.6em; background: linear-gradient(90deg, #ff4757, #ff6b81); border-radius: 12px;' - }, totalBlocked + ' blocked') + renderHealth: function(d) { + var checks = [ + { label: 'CrowdSec', value: d.crowdsec === 'running' ? 'Running' : 'Stopped', ok: d.crowdsec === 'running' }, + { label: 'LAPI', value: d.lapi_status === 'available' ? 'OK' : 'Down', ok: d.lapi_status === 'available' }, + { label: 'CAPI', value: d.capi_enrolled ? 'Connected' : 'Disconnected', ok: d.capi_enrolled }, + { label: 'Bouncer', value: (d.bouncer_count || 0) > 0 ? 'Active' : 'None', ok: (d.bouncer_count || 0) > 0 }, + { label: 'GeoIP', value: d.geoip_enabled ? 'Enabled' : 'Disabled', ok: d.geoip_enabled }, + { label: 'Acquisition', value: (d.acquisition_count || 0) + ' sources', ok: (d.acquisition_count || 0) > 0 } + ]; + return E('div', { 'class': 'soc-health' }, checks.map(function(c) { + return E('div', { 'class': 'soc-health-item' }, [ + E('div', { 'class': 'soc-health-icon ' + (c.ok ? 'ok' : 'error') }, c.ok ? '\u2713' : '\u2717'), + E('div', {}, [ + E('div', { 'class': 'soc-health-label' }, c.label), + E('div', { 'class': 'soc-health-value' }, c.value) ]) - ]), - E('div', { 'class': 'cs-card-body' }, [ - statusRow, - ipRows.length > 0 ? - E('div', { 'style': 'max-height: 300px; overflow-y: auto;' }, ipRows) : - E('div', { 'class': 'cs-empty', 'style': 'padding: 1em;' }, [ - E('div', { 'class': 'cs-empty-icon' }, '✅'), - E('p', {}, _('No IPs currently blocked in firewall')) - ]), - allIps.length > 20 ? E('div', { 'style': 'text-align: center; padding: 0.5em; font-size: 0.8em; color: #888;' }, - _('Showing 20 of ') + allIps.length + _(' blocked IPs') - ) : E('span') - ]) + ]); + })); + }, + + renderScenarios: function(scenarios) { + if (!scenarios || !scenarios.length) { + return E('div', { 'class': 'soc-empty' }, 'No scenarios loaded'); + } + return E('table', { 'class': 'soc-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Scenario'), + E('th', {}, 'Status'), + E('th', {}, 'Type') + ])), + E('tbody', {}, scenarios.slice(0, 12).map(function(s) { + var name = s.name || s; + var enabled = !s.status || s.status.includes('enabled'); + var isLocal = s.status && s.status.includes('local'); + return E('tr', {}, [ + E('td', {}, E('span', { 'class': 'soc-scenario' }, api.parseScenario(name))), + E('td', {}, E('span', { 'class': 'soc-severity ' + (enabled ? 'low' : 'medium') }, enabled ? 'ENABLED' : 'DISABLED')), + E('td', { 'class': 'soc-time' }, isLocal ? 'Local' : 'Hub') + ]); + })) ]); }, - renderLogCard: function(entries) { - return E('div', { 'class': 'cs-card cs-log-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, _('CrowdSec Logs')) - ]), - entries && entries.length ? - E('pre', { 'class': 'cs-log-output' }, entries.slice(-30).join('\n')) : - E('p', { 'class': 'cs-empty' }, _('No log entries')) - ]); - }, - - handleSnapshot: function() { + pollData: function() { var self = this; - ui.showModal(_('Collecting snapshot'), [ - E('p', {}, _('Aggregating dmesg/logread into SecuBox log…')), - E('div', { 'class': 'spinning' }) - ]); - this.csApi.collectDebugSnapshot().then(function(result) { - ui.hideModal(); - if (result && result.success) { - self.refreshDashboard(); - self.showToast(_('Snapshot appended to /var/log/seccubox.log'), 'success'); - } else { - self.showToast((result && result.error) || _('Snapshot failed'), 'error'); - } - }).catch(function(err) { - ui.hideModal(); - self.showToast(err.message || _('Snapshot failed'), 'error'); + return api.getOverview().then(function(data) { + var el = document.getElementById('soc-stats'); + if (el) dom.content(el, self.renderStats(data)); + el = document.getElementById('recent-alerts'); + if (el) dom.content(el, self.renderAlerts(data.alerts || [])); + el = document.getElementById('geo-dist'); + if (el) dom.content(el, self.renderGeo(data.countries || {})); }); }, + runHealthCheck: function() { + var self = this; + var el = document.getElementById('health-check'); + if (el) dom.content(el, E('div', { 'class': 'soc-loading' }, [E('div', { 'class': 'soc-spinner' }), 'Testing...'])); + return api.getHealthCheck().then(function(r) { + if (el) dom.content(el, self.renderHealth(r)); + self.showToast('Health check completed', 'success'); + }).catch(function(e) { + self.showToast('Health check failed', 'error'); + }); + }, + + 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 diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js index 1e8d75c7..56e39bc1 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js @@ -1,720 +1,312 @@ 'use strict'; 'require view'; -'require secubox-theme/theme as Theme'; +'require dom'; 'require ui'; -'require crowdsec-dashboard/api as API'; -'require crowdsec-dashboard/nav as CsNav'; +'require crowdsec-dashboard.api as api'; + +/** + * CrowdSec SOC - Settings View + * System configuration and management + */ return view.extend({ + title: _('Settings'), + status: {}, + machines: [], + collections: [], + load: function() { + 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 Promise.all([ - API.getStatus(), - API.getMachines(), - API.getHub(), - API.getCollections() + api.getStatus(), + api.getMachines(), + api.getCollections(), + api.getAcquisitionConfig() ]); }, render: function(data) { - var status = data[0] || {}; + var self = this; + this.status = data[0] || {}; var machinesData = data[1] || {}; - var machines = Array.isArray(machinesData) ? machinesData : (machinesData.machines || []); - var hub = data[2] || {}; - var collectionsData = data[3] || {}; - var collections = collectionsData.collections || []; - if (collections.collections) collections = collections.collections; + this.machines = Array.isArray(machinesData) ? machinesData : (machinesData.machines || []); + var collectionsData = data[2] || {}; + this.collections = collectionsData.collections || []; + if (this.collections.collections) this.collections = this.collections.collections; + this.acquisition = data[3] || {}; - // Load CSS - var head = document.head || document.getElementsByTagName('head')[0]; - var cssLink = E('link', { - 'rel': 'stylesheet', - 'href': L.resource('crowdsec-dashboard/dashboard.css') - }); - head.appendChild(cssLink); - - var view = E('div', { 'class': 'crowdsec-dashboard' }, [ - E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - CsNav.renderTabs('settings'), - E('h2', { 'class': 'cs-page-title' }, _('CrowdSec Settings')), - E('p', { 'style': 'color: var(--cs-text-secondary); margin-bottom: 1.5rem;' }, - _('Configure and manage your CrowdSec installation, machines, and collections.')), - - // Service Status - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Service Status')), - E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1em; margin-top: 1em;' }, [ - // CrowdSec Status - E('div', { 'class': 'cbi-value', 'style': 'background: ' + (status.crowdsec === 'running' ? '#d4edda' : '#f8d7da') + '; padding: 1em; border-radius: 4px; border-left: 4px solid ' + (status.crowdsec === 'running' ? '#28a745' : '#dc3545') + ';' }, [ - E('label', { 'class': 'cbi-value-title' }, _('CrowdSec Agent')), - E('div', { 'class': 'cbi-value-field' }, [ - E('span', { - 'class': 'badge', - 'style': 'background: ' + (status.crowdsec === 'running' ? '#28a745' : '#dc3545') + '; color: white; padding: 0.5em 1em; border-radius: 4px; font-size: 1em;' - }, status.crowdsec === 'running' ? _('RUNNING') : _('STOPPED')) - ]) + return E('div', { 'class': 'soc-dashboard' }, [ + this.renderHeader(), + this.renderNav('settings'), + E('div', { 'class': 'soc-stats' }, this.renderServiceStats()), + E('div', { 'class': 'soc-grid-2' }, [ + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, [ + 'Service Control', + E('span', { 'class': 'soc-severity ' + (this.status.crowdsec === 'running' ? 'low' : 'critical') }, + this.status.crowdsec === 'running' ? 'RUNNING' : 'STOPPED') ]), - - // LAPI Status - E('div', { 'class': 'cbi-value', 'style': 'background: ' + (status.lapi_status === 'available' ? '#d4edda' : '#f8d7da') + '; padding: 1em; border-radius: 4px; border-left: 4px solid ' + (status.lapi_status === 'available' ? '#28a745' : '#dc3545') + ';' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Local API (LAPI)')), - E('div', { 'class': 'cbi-value-field' }, [ - E('span', { - 'class': 'badge', - 'style': 'background: ' + (status.lapi_status === 'available' ? '#28a745' : '#dc3545') + '; color: white; padding: 0.5em 1em; border-radius: 4px; font-size: 1em;' - }, status.lapi_status === 'available' ? _('AVAILABLE') : _('UNAVAILABLE')) - ]) - ]), - - // Version Info - E('div', { 'class': 'cbi-value', 'style': 'background: #e8f4f8; padding: 1em; border-radius: 4px; border-left: 4px solid #0088cc;' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Version')), - E('div', { 'class': 'cbi-value-field' }, [ - E('code', { 'style': 'font-size: 1em;' }, status.version || 'Unknown') - ]) - ]) + E('div', { 'class': 'soc-card-body' }, this.renderServiceControl()) + ]), + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, 'Acquisition Sources'), + E('div', { 'class': 'soc-card-body' }, this.renderAcquisition()) ]) ]), - - // Registered Machines - E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [ - E('h3', {}, _('Registered Machines')), - E('p', { 'style': 'color: #666;' }, - _('Machines are CrowdSec agents that send alerts to the Local API.')), - - E('div', { 'class': 'table-wrapper', 'style': 'margin-top: 1em;' }, [ - E('table', { 'class': 'table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, _('Machine ID')), - E('th', {}, _('IP Address')), - E('th', {}, _('Last Update')), - E('th', {}, _('Version')), - E('th', {}, _('Status')) - ]) - ]), - E('tbody', {}, - machines.length > 0 ? - machines.map(function(machine) { - var isActive = machine.isValidated || machine.is_validated; - return E('tr', {}, [ - E('td', {}, [ - E('strong', {}, machine.machineId || machine.machine_id || 'Unknown') - ]), - E('td', {}, [ - E('code', {}, machine.ipAddress || machine.ip_address || 'N/A') - ]), - E('td', {}, API.formatDate(machine.updated_at || machine.updatedAt)), - E('td', {}, machine.version || 'N/A'), - E('td', {}, [ - E('span', { - 'class': 'badge', - 'style': 'background: ' + (isActive ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' - }, isActive ? _('Active') : _('Pending')) - ]) - ]); - }) : - E('tr', {}, [ - E('td', { 'colspan': 5, 'style': 'text-align: center; padding: 2em; color: #999;' }, - _('No machines registered')) - ]) - ) - ]) - ]) - ]), - - // Collections Browser - E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [ - E('h3', {}, _('CrowdSec Collections')), - E('p', { 'style': 'color: #666;' }, - _('Collections are bundles of parsers, scenarios, and post-overflow stages for specific services.')), - - E('div', { 'style': 'display: flex; gap: 1em; margin: 1em 0;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function() { - ui.showModal(_('Updating Hub...'), [ - E('p', {}, _('Fetching latest collections from CrowdSec Hub...')), - E('div', { 'class': 'spinning' }) - ]); - API.updateHub().then(function(result) { - ui.hideModal(); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Hub index updated successfully. Please refresh the page.')), 'info'); - } else { - ui.addNotification(null, E('p', {}, result.error || _('Failed to update hub')), 'error'); - } - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, err.message || err), 'error'); - }); - } - }, _('🔄 Update Hub')) + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, [ + 'Installed Collections (' + this.collections.filter(function(c) { return c.status === 'enabled' || c.installed; }).length + ')', + E('button', { 'class': 'soc-btn soc-btn-sm', 'click': L.bind(this.updateHub, this) }, 'Update Hub') ]), - - E('div', { 'class': 'table-wrapper', 'style': 'margin-top: 1em;' }, [ - E('table', { 'class': 'table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, _('Collection')), - E('th', {}, _('Description')), - E('th', {}, _('Version')), - E('th', {}, _('Status')), - E('th', {}, _('Actions')) - ]) - ]), - E('tbody', {}, - collections.length > 0 ? - collections.map(function(collection) { - var isInstalled = collection.status === 'enabled' || collection.status === 'installed' || collection.installed === 'ok'; - var collectionName = collection.name || 'Unknown'; - return E('tr', {}, [ - E('td', {}, [ - E('strong', {}, collectionName) - ]), - E('td', {}, collection.description || 'N/A'), - E('td', {}, collection.version || collection.local_version || 'N/A'), - E('td', {}, [ - E('span', { - 'class': 'badge', - 'style': 'background: ' + (isInstalled ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' - }, isInstalled ? _('Installed') : _('Available')) - ]), - E('td', {}, [ - isInstalled ? - E('button', { - 'class': 'cbi-button cbi-button-remove', - 'click': function() { - ui.showModal(_('Removing Collection...'), [ - E('p', {}, _('Removing %s...').format(collectionName)), - E('div', { 'class': 'spinning' }) - ]); - API.removeCollection(collectionName).then(function(result) { - ui.hideModal(); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Collection removed. Please reload CrowdSec and refresh this page.')), 'info'); - } else { - ui.addNotification(null, E('p', {}, result.error || _('Failed to remove collection')), 'error'); - } - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, err.message || err), 'error'); - }); - } - }, _('Remove')) : - E('button', { - 'class': 'cbi-button cbi-button-add', - 'click': function() { - ui.showModal(_('Installing Collection...'), [ - E('p', {}, _('Installing %s...').format(collectionName)), - E('div', { 'class': 'spinning' }) - ]); - API.installCollection(collectionName).then(function(result) { - ui.hideModal(); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Collection installed. Please reload CrowdSec and refresh this page.')), 'info'); - } else { - ui.addNotification(null, E('p', {}, result.error || _('Failed to install collection')), 'error'); - } - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, err.message || err), 'error'); - }); - } - }, _('Install')) - ]) - ]); - }) : - E('tr', {}, [ - E('td', { 'colspan': 5, 'style': 'text-align: center; padding: 2em; color: #999;' }, [ - E('p', {}, _('No collections found. Click "Update Hub" to fetch the collection list.')), - E('p', { 'style': 'margin-top: 0.5em; font-size: 0.9em;' }, [ - _('Or use: '), - E('code', {}, 'cscli hub update') - ]) - ]) - ]) - ) - ]) - ]) + E('div', { 'class': 'soc-card-body', 'id': 'collections-list' }, this.renderCollections()) ]), + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, 'Registered Machines'), + E('div', { 'class': 'soc-card-body' }, this.renderMachines()) + ]), + E('div', { 'class': 'soc-card' }, [ + E('div', { 'class': 'soc-card-header' }, 'Configuration Files'), + E('div', { 'class': 'soc-card-body' }, this.renderConfigFiles()) + ]) + ]); + }, - // Quick Actions - E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [ - E('h3', {}, _('Quick Actions')), + 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' }), 'SETTINGS']) + ]); + }, - // Service Control - E('div', { 'style': 'margin-top: 1em;' }, [ - E('h4', { 'style': 'margin-bottom: 0.5em; color: var(--cyber-text-secondary, #888);' }, _('Service Control')), - E('div', { 'style': 'display: flex; gap: 0.5em; flex-wrap: wrap;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-positive', - 'style': 'min-width: 80px;', - 'click': function(ev) { - ev.target.disabled = true; - ev.target.classList.add('spinning'); - API.serviceControl('start').then(function(result) { - ev.target.disabled = false; - ev.target.classList.remove('spinning'); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('CrowdSec started successfully')), 'info'); - window.setTimeout(function() { location.reload(); }, 1500); - } else { - ui.addNotification(null, E('p', {}, result.error || _('Failed to start service')), 'error'); - } - }); - } - }, _('▶ Start')), - E('button', { - 'class': 'cbi-button cbi-button-negative', - 'style': 'min-width: 80px;', - 'click': function(ev) { - ev.target.disabled = true; - ev.target.classList.add('spinning'); - API.serviceControl('stop').then(function(result) { - ev.target.disabled = false; - ev.target.classList.remove('spinning'); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('CrowdSec stopped')), 'info'); - window.setTimeout(function() { location.reload(); }, 1500); - } else { - ui.addNotification(null, E('p', {}, result.error || _('Failed to stop service')), 'error'); - } - }); - } - }, _('■ Stop')), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'style': 'min-width: 80px;', - 'click': function(ev) { - ev.target.disabled = true; - ev.target.classList.add('spinning'); - API.serviceControl('restart').then(function(result) { - ev.target.disabled = false; - ev.target.classList.remove('spinning'); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('CrowdSec restarted')), 'info'); - window.setTimeout(function() { location.reload(); }, 2000); - } else { - ui.addNotification(null, E('p', {}, result.error || _('Failed to restart service')), 'error'); - } - }); - } - }, _('↻ Restart')), - E('button', { - 'class': 'cbi-button', - 'style': 'min-width: 80px;', - 'click': function(ev) { - ev.target.disabled = true; - ev.target.classList.add('spinning'); - API.serviceControl('reload').then(function(result) { - ev.target.disabled = false; - ev.target.classList.remove('spinning'); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Configuration reloaded')), 'info'); - } else { - ui.addNotification(null, E('p', {}, result.error || _('Failed to reload')), 'error'); - } - }); - } - }, _('⟳ Reload')) + 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)); + })); + }, + + renderServiceStats: function() { + var s = this.status; + return [ + E('div', { 'class': 'soc-stat ' + (s.crowdsec === 'running' ? 'success' : 'danger') }, [ + E('div', { 'class': 'soc-stat-value' }, s.crowdsec === 'running' ? 'ON' : 'OFF'), + E('div', { 'class': 'soc-stat-label' }, 'CrowdSec Agent') + ]), + E('div', { 'class': 'soc-stat ' + (s.lapi_status === 'available' ? 'success' : 'danger') }, [ + E('div', { 'class': 'soc-stat-value' }, s.lapi_status === 'available' ? 'OK' : 'DOWN'), + E('div', { 'class': 'soc-stat-label' }, 'Local API') + ]), + E('div', { 'class': 'soc-stat' }, [ + E('div', { 'class': 'soc-stat-value' }, s.version || 'N/A'), + E('div', { 'class': 'soc-stat-label' }, 'Version') + ]), + E('div', { 'class': 'soc-stat' }, [ + E('div', { 'class': 'soc-stat-value' }, String(this.machines.length)), + E('div', { 'class': 'soc-stat-label' }, 'Machines') + ]) + ]; + }, + + renderServiceControl: function() { + var self = this; + var running = this.status.crowdsec === 'running'; + return E('div', {}, [ + E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px;' }, [ + E('button', { + 'class': 'soc-btn ' + (running ? '' : 'primary'), + 'click': function() { self.serviceAction('start'); } + }, 'Start'), + E('button', { + 'class': 'soc-btn ' + (running ? 'danger' : ''), + 'click': function() { self.serviceAction('stop'); } + }, 'Stop'), + E('button', { + 'class': 'soc-btn', + 'click': function() { self.serviceAction('restart'); } + }, 'Restart'), + E('button', { + 'class': 'soc-btn', + 'click': function() { self.serviceAction('reload'); } + }, 'Reload') + ]), + E('div', { 'class': 'soc-health' }, [ + E('div', { 'class': 'soc-health-item' }, [ + E('div', { 'class': 'soc-health-icon ' + (running ? 'ok' : 'error') }, running ? '\u2713' : '\u2717'), + E('div', {}, [ + E('div', { 'class': 'soc-health-label' }, 'Agent'), + E('div', { 'class': 'soc-health-value' }, running ? 'Running' : 'Stopped') ]) ]), - - // Register Bouncer - E('div', { 'style': 'margin-top: 1.5em;' }, [ - E('h4', { 'style': 'margin-bottom: 0.5em; color: var(--cyber-text-secondary, #888);' }, _('Register New Bouncer')), - E('div', { 'style': 'display: flex; gap: 0.5em; flex-wrap: wrap; align-items: center;' }, [ - E('input', { - 'type': 'text', - 'id': 'new-bouncer-name', - 'placeholder': _('Bouncer name...'), - 'style': 'padding: 0.5em; border: 1px solid var(--cyber-border, #444); border-radius: 4px; background: var(--cyber-bg-secondary, #1a1a2e); color: var(--cyber-text-primary, #fff); min-width: 200px;' - }), - E('button', { - 'class': 'cbi-button cbi-button-add', - 'click': function(ev) { - var nameInput = document.getElementById('new-bouncer-name'); - var name = nameInput.value.trim(); - if (!name) { - ui.addNotification(null, E('p', {}, _('Please enter a bouncer name')), 'error'); - return; - } - ev.target.disabled = true; - ev.target.classList.add('spinning'); - API.registerBouncer(name).then(function(result) { - ev.target.disabled = false; - ev.target.classList.remove('spinning'); - if (result && result.success) { - nameInput.value = ''; - ui.showModal(_('Bouncer Registered'), [ - E('p', {}, _('Bouncer "%s" registered successfully!').format(name)), - E('p', { 'style': 'margin-top: 1em;' }, _('API Key:')), - E('pre', { - 'style': 'background: var(--cyber-bg-tertiary, #252538); padding: 1em; border-radius: 4px; word-break: break-all; user-select: all;' - }, result.api_key || result.key || 'Check console'), - E('p', { 'style': 'margin-top: 1em; color: #f39c12;' }, - _('Save this key! It will not be shown again.')), - E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function() { - ui.hideModal(); - location.reload(); - } - }, _('Close')) - ]) - ]); - } else { - ui.addNotification(null, E('p', {}, result.error || _('Failed to register bouncer')), 'error'); - } - }); - } - }, _('+ Register')) + E('div', { 'class': 'soc-health-item' }, [ + E('div', { 'class': 'soc-health-icon ' + (this.status.lapi_status === 'available' ? 'ok' : 'error') }, + this.status.lapi_status === 'available' ? '\u2713' : '\u2717'), + E('div', {}, [ + E('div', { 'class': 'soc-health-label' }, 'LAPI'), + E('div', { 'class': 'soc-health-value' }, this.status.lapi_status === 'available' ? 'Available' : 'Unavailable') ]) ]), - - // Hub Update - E('div', { 'style': 'margin-top: 1.5em;' }, [ - E('h4', { 'style': 'margin-bottom: 0.5em; color: var(--cyber-text-secondary, #888);' }, _('Hub Management')), - E('div', { 'style': 'display: flex; gap: 0.5em; flex-wrap: wrap;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function(ev) { - ev.target.disabled = true; - ev.target.classList.add('spinning'); - API.updateHub().then(function(result) { - ev.target.disabled = false; - ev.target.classList.remove('spinning'); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Hub index updated successfully')), 'info'); - window.setTimeout(function() { location.reload(); }, 1500); - } else { - ui.addNotification(null, E('p', {}, result.error || _('Failed to update hub')), 'error'); - } - }); - } - }, _('⬇ Update Hub Index')) - ]) - ]), - - // CrowdSec Console - E('div', { 'style': 'margin-top: 1.5em;' }, [ - E('h4', { 'style': 'margin-bottom: 0.5em; color: var(--cyber-text-secondary, #888);' }, _('CrowdSec Console')), - E('div', { 'style': 'display: flex; gap: 0.5em; flex-wrap: wrap;' }, [ - E('a', { - 'href': 'https://app.crowdsec.net', - 'target': '_blank', - 'class': 'cbi-button cbi-button-action', - 'style': 'text-decoration: none; display: inline-flex; align-items: center; gap: 0.5em;' - }, _('🌐 Open CrowdSec Console')) - ]) - ]) - ]), - - // Notification Settings - E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [ - E('h3', {}, _('Notification Settings')), - E('p', { 'style': 'color: #666;' }, - _('Configure email notifications for security alerts and decisions.')), - - E('div', { 'style': 'background: #f8f9fa; padding: 1.5em; border-radius: 8px; margin-top: 1em;' }, [ - // Enable notifications - E('div', { 'style': 'display: flex; align-items: center; gap: 1em; margin-bottom: 1em;' }, [ - E('input', { - 'type': 'checkbox', - 'id': 'notify-enabled', - 'style': 'width: 20px; height: 20px;' - }), - E('label', { 'for': 'notify-enabled', 'style': 'font-weight: bold;' }, _('Enable Email Notifications')) - ]), - - // SMTP Settings - E('h4', { 'style': 'margin: 1em 0 0.5em 0; color: #555;' }, _('SMTP Configuration')), - E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 1em;' }, [ - E('div', {}, [ - E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('SMTP Server')), - E('input', { - 'type': 'text', - 'id': 'smtp-server', - 'placeholder': 'smtp.example.com', - 'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;' - }) - ]), - E('div', {}, [ - E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('SMTP Port')), - E('input', { - 'type': 'number', - 'id': 'smtp-port', - 'placeholder': '587', - 'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;' - }) - ]), - E('div', {}, [ - E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('Username')), - E('input', { - 'type': 'text', - 'id': 'smtp-username', - 'placeholder': _('user@example.com'), - 'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;' - }) - ]), - E('div', {}, [ - E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('Password')), - E('input', { - 'type': 'password', - 'id': 'smtp-password', - 'placeholder': '••••••••', - 'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;' - }) - ]), - E('div', {}, [ - E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('From Address')), - E('input', { - 'type': 'email', - 'id': 'smtp-from', - 'placeholder': 'crowdsec@example.com', - 'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;' - }) - ]), - E('div', {}, [ - E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('To Address')), - E('input', { - 'type': 'email', - 'id': 'smtp-to', - 'placeholder': 'admin@example.com', - 'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;' - }) - ]) - ]), - - // TLS Option - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em; margin-top: 1em;' }, [ - E('input', { 'type': 'checkbox', 'id': 'smtp-tls', 'checked': true }), - E('label', { 'for': 'smtp-tls' }, _('Use TLS/STARTTLS')) - ]), - - // Notification Types - E('h4', { 'style': 'margin: 1.5em 0 0.5em 0; color: #555;' }, _('Notification Types')), - E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 0.5em;' }, [ - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ - E('input', { 'type': 'checkbox', 'id': 'notify-new-ban', 'checked': true }), - E('label', { 'for': 'notify-new-ban' }, _('New IP Bans')) - ]), - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ - E('input', { 'type': 'checkbox', 'id': 'notify-high-alert' }), - E('label', { 'for': 'notify-high-alert' }, _('High Severity Alerts')) - ]), - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ - E('input', { 'type': 'checkbox', 'id': 'notify-service-down' }), - E('label', { 'for': 'notify-service-down' }, _('Service Down')) - ]), - E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [ - E('input', { 'type': 'checkbox', 'id': 'notify-mass-ban' }), - E('label', { 'for': 'notify-mass-ban' }, _('Mass Ban Events (>10 IPs)')) - ]) - ]), - - // Actions - E('div', { 'style': 'margin-top: 1.5em; display: flex; gap: 0.5em;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-positive', - 'click': function() { - ui.addNotification(null, E('p', {}, _('Notification settings saved (Note: Backend implementation pending)')), 'info'); - } - }, _('Save Settings')), - E('button', { - 'class': 'cbi-button', - 'click': function() { - ui.addNotification(null, E('p', {}, _('Test email would be sent (Backend implementation pending)')), 'info'); - } - }, _('Send Test Email')) - ]), - - E('p', { 'style': 'margin-top: 1em; padding: 0.75em; background: #fff3cd; border-radius: 4px; font-size: 0.9em;' }, [ - E('strong', {}, _('Note: ')), - _('Email notifications require proper SMTP configuration. Ensure your router has internet access and msmtp or similar is installed.') - ]) - ]) - ]), - - // Interface Configuration - E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [ - E('h3', {}, _('Firewall Bouncer Interface Configuration')), - E('p', { 'style': 'color: #666;' }, - _('Configure which interfaces and chains the firewall bouncer protects.')), - - E('div', { 'style': 'background: #f8f9fa; padding: 1.5em; border-radius: 8px; margin-top: 1em;' }, [ - // Interface Selection - E('div', { 'style': 'margin-bottom: 1em;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 0.5em; font-weight: bold;' }, _('Protected Interfaces')), - E('p', { 'style': 'font-size: 0.9em; color: #666; margin-bottom: 0.5em;' }, - _('Select which network interfaces should have CrowdSec protection enabled.')), - E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 1em;' }, [ - E('label', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ddd; border-radius: 4px;' }, [ - E('input', { 'type': 'checkbox', 'name': 'iface', 'value': 'wan', 'checked': true }), - E('span', {}, 'WAN') - ]), - E('label', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ddd; border-radius: 4px;' }, [ - E('input', { 'type': 'checkbox', 'name': 'iface', 'value': 'wan6' }), - E('span', {}, 'WAN6') - ]), - E('label', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ddd; border-radius: 4px;' }, [ - E('input', { 'type': 'checkbox', 'name': 'iface', 'value': 'lan' }), - E('span', {}, 'LAN') - ]) - ]) - ]), - - // Chain Configuration - E('div', { 'style': 'margin-bottom: 1em;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 0.5em; font-weight: bold;' }, _('Firewall Chains')), - E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 1em;' }, [ - E('label', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ddd; border-radius: 4px;' }, [ - E('input', { 'type': 'checkbox', 'name': 'chain', 'value': 'input', 'checked': true }), - E('span', {}, 'INPUT'), - E('span', { 'style': 'font-size: 0.8em; color: #666;' }, _('(connections to router)')) - ]), - E('label', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ddd; border-radius: 4px;' }, [ - E('input', { 'type': 'checkbox', 'name': 'chain', 'value': 'forward', 'checked': true }), - E('span', {}, 'FORWARD'), - E('span', { 'style': 'font-size: 0.8em; color: #666;' }, _('(connections through router)')) - ]) - ]) - ]), - - // Deny Action - E('div', { 'style': 'margin-bottom: 1em;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 0.5em; font-weight: bold;' }, _('Deny Action')), - E('select', { - 'id': 'deny-action', - 'style': 'padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; min-width: 150px;' - }, [ - E('option', { 'value': 'DROP', 'selected': true }, 'DROP (silent)'), - E('option', { 'value': 'REJECT' }, 'REJECT (with ICMP)') - ]) - ]), - - // Priority - E('div', { 'style': 'margin-bottom: 1em;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 0.5em; font-weight: bold;' }, _('Rule Priority')), - E('input', { - 'type': 'number', - 'id': 'rule-priority', - 'value': '-10', - 'style': 'padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;' - }), - E('span', { 'style': 'margin-left: 0.5em; font-size: 0.9em; color: #666;' }, - _('Lower = higher priority. Default: -10')) - ]), - - // Save button - E('div', { 'style': 'margin-top: 1.5em;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-positive', - 'click': function() { - ui.addNotification(null, E('p', {}, _('Interface configuration saved (Note: Uses UCI crowdsec-firewall-bouncer)')), 'info'); - } - }, _('Apply Interface Settings')) - ]) - ]), - - // Firewall Bouncer quick control - E('div', { 'style': 'margin-top: 1em; padding: 1em; background: #fff; border-radius: 6px; border: 1px solid #e6e6e6;' }, [ - E('h4', { 'style': 'margin-bottom: 0.5em; color: var(--cyber-text-secondary, #888);' }, _('Firewall Bouncer')), - E('p', { 'style': 'color: #666; margin-bottom: 0.5em;' }, _('Enable or disable the CrowdSec firewall bouncer (requires nftables and the firewall-bouncer collection).')), - E('div', { 'id': 'bouncer-control', 'style': 'display: flex; gap: 0.5em; align-items: center;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function(ev) { - ev.target.disabled = true; - ev.target.classList.add('spinning'); - API.getFirewallBouncerStatus().then(function(res) { - var enabled = res && res.enabled; - var action = enabled ? 'disable' : 'enable'; - API.controlFirewallBouncer(action).then(function(result) { - ev.target.disabled = false; - ev.target.classList.remove('spinning'); - if (result && result.success) { - ui.addNotification(null, E('p', {}, enabled ? _('Firewall bouncer disabled') : _('Firewall bouncer enabled')), 'info'); - window.setTimeout(function() { location.reload(); }, 1200); - } else { - ui.addNotification(null, E('p', {}, result.error || _('Failed to change bouncer state')), 'error'); - } - }); - }).catch(function(err) { - ev.target.disabled = false; - ev.target.classList.remove('spinning'); - ui.addNotification(null, E('p', {}, err.message || err), 'error'); - }); - } - }, _('Toggle Firewall Bouncer')), - E('div', { 'id': 'bouncer-status', 'style': 'color: #666; font-size: 0.95em;' }, _('Checking status...')) - ]) - ]), - ]), - - // Configuration Files - E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [ - E('h3', {}, _('Configuration Files')), - E('div', { 'style': 'background: #f8f9fa; padding: 1em; border-radius: 4px; margin-top: 1em;' }, [ - E('p', {}, [ - E('strong', {}, _('Main Configuration:')), - ' ', - E('code', {}, '/etc/crowdsec/config.yaml') - ]), - E('p', {}, [ - E('strong', {}, _('Acquisition:')), - ' ', - E('code', {}, '/etc/crowdsec/acquis.yaml') - ]), - E('p', {}, [ - E('strong', {}, _('Profiles:')), - ' ', - E('code', {}, '/etc/crowdsec/profiles.yaml') - ]), - E('p', {}, [ - E('strong', {}, _('Local API:')), - ' ', - E('code', {}, '/etc/crowdsec/local_api_credentials.yaml') - ]), - E('p', { 'style': 'margin-top: 1em; padding: 0.75em; background: #fff3cd; border-radius: 4px;' }, [ - E('strong', {}, _('Note:')), - ' ', - _('After modifying configuration files, restart CrowdSec: '), - E('code', {}, '/etc/init.d/crowdsec restart') - ]) - ]) - ]), - - // Documentation Links - E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em; background: #e8f4f8; padding: 1em;' }, [ - E('h3', {}, _('Documentation & Resources')), - E('ul', { 'style': 'margin-top: 0.5em;' }, [ - E('li', {}, [ - E('a', { 'href': 'https://docs.crowdsec.net/', 'target': '_blank' }, - _('Official Documentation')) - ]), - E('li', {}, [ - E('a', { 'href': 'https://hub.crowdsec.net/', 'target': '_blank' }, - _('CrowdSec Hub - Collections & Scenarios')) - ]), - E('li', {}, [ - E('a', { 'href': 'https://app.crowdsec.net/', 'target': '_blank' }, - _('CrowdSec Console - Global Statistics')) - ]), - E('li', {}, [ - E('code', {}, 'cscli --help'), - ' - ', - _('CLI help and commands') + E('div', { 'class': 'soc-health-item' }, [ + E('div', { 'class': 'soc-health-icon ' + (this.status.capi_enrolled ? 'ok' : 'warn') }, + this.status.capi_enrolled ? '\u2713' : '!'), + E('div', {}, [ + E('div', { 'class': 'soc-health-label' }, 'CAPI'), + E('div', { 'class': 'soc-health-value' }, this.status.capi_enrolled ? 'Enrolled' : 'Not enrolled') ]) ]) ]) ]); - - return view; }, - handleSaveApply: null, - handleSave: null, - handleReset: null + renderAcquisition: function() { + var acq = this.acquisition; + var sources = [ + { name: 'Syslog', enabled: acq.syslog_enabled, path: acq.syslog_path }, + { name: 'SSH', enabled: acq.ssh_enabled }, + { name: 'Firewall', enabled: acq.firewall_enabled }, + { name: 'HTTP', enabled: acq.http_enabled } + ]; + return E('div', { 'class': 'soc-health' }, sources.map(function(src) { + return E('div', { 'class': 'soc-health-item' }, [ + E('div', { 'class': 'soc-health-icon ' + (src.enabled ? 'ok' : 'error') }, src.enabled ? '\u2713' : '\u2717'), + E('div', {}, [ + E('div', { 'class': 'soc-health-label' }, src.name), + E('div', { 'class': 'soc-health-value' }, src.enabled ? (src.path || 'Enabled') : 'Disabled') + ]) + ]); + })); + }, + + renderCollections: function() { + var self = this; + var installed = this.collections.filter(function(c) { + return c.status === 'enabled' || c.installed === 'ok'; + }); + + if (!installed.length) { + return E('div', { 'class': 'soc-empty' }, [ + E('div', { 'class': 'soc-empty-icon' }, '\u26A0'), + 'No collections installed. Click "Update Hub" to fetch available collections.' + ]); + } + + return E('table', { 'class': 'soc-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Collection'), + E('th', {}, 'Version'), + E('th', {}, 'Status'), + E('th', {}, 'Actions') + ])), + E('tbody', {}, installed.map(function(c) { + return E('tr', {}, [ + E('td', {}, E('span', { 'class': 'soc-scenario' }, c.name || 'Unknown')), + E('td', { 'class': 'soc-time' }, c.version || c.local_version || 'N/A'), + E('td', {}, E('span', { 'class': 'soc-severity low' }, 'INSTALLED')), + E('td', {}, E('button', { + 'class': 'soc-btn soc-btn-sm danger', + 'click': function() { self.removeCollection(c.name); } + }, 'Remove')) + ]); + })) + ]); + }, + + renderMachines: function() { + if (!this.machines.length) { + return E('div', { 'class': 'soc-empty' }, 'No machines registered'); + } + + return E('table', { 'class': 'soc-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Machine ID'), + E('th', {}, 'IP Address'), + E('th', {}, 'Last Update'), + E('th', {}, 'Status') + ])), + E('tbody', {}, this.machines.map(function(m) { + var isActive = m.isValidated || m.is_validated; + return E('tr', {}, [ + E('td', {}, E('strong', {}, m.machineId || m.machine_id || 'Unknown')), + E('td', {}, E('span', { 'class': 'soc-ip' }, m.ipAddress || m.ip_address || 'N/A')), + E('td', { 'class': 'soc-time' }, api.formatRelativeTime(m.updated_at || m.updatedAt)), + E('td', {}, E('span', { 'class': 'soc-severity ' + (isActive ? 'low' : 'medium') }, + isActive ? 'ACTIVE' : 'PENDING')) + ]); + })) + ]); + }, + + renderConfigFiles: function() { + var configs = [ + { label: 'Main Config', path: '/etc/crowdsec/config.yaml' }, + { label: 'Acquisition', path: '/etc/crowdsec/acquis.yaml' }, + { label: 'Profiles', path: '/etc/crowdsec/profiles.yaml' }, + { label: 'Local API', path: '/etc/crowdsec/local_api_credentials.yaml' }, + { label: 'Firewall Bouncer', path: '/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml' } + ]; + + return E('div', { 'style': 'display: grid; gap: 8px;' }, configs.map(function(cfg) { + return E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 8px; background: var(--soc-bg); border-radius: 4px;' }, [ + E('span', { 'style': 'color: var(--soc-text-muted);' }, cfg.label), + E('code', { 'class': 'soc-ip' }, cfg.path) + ]); + })); + }, + + serviceAction: function(action) { + var self = this; + api.serviceControl(action).then(function(r) { + if (r.success) { + self.showToast('Service ' + action + ' successful', 'success'); + setTimeout(function() { location.reload(); }, 1500); + } else { + self.showToast('Failed: ' + (r.error || 'Unknown'), 'error'); + } + }); + }, + + updateHub: function() { + var self = this; + api.updateHub().then(function(r) { + if (r.success) { + self.showToast('Hub updated', 'success'); + setTimeout(function() { location.reload(); }, 1500); + } else { + self.showToast('Failed: ' + (r.error || 'Unknown'), 'error'); + } + }); + }, + + removeCollection: function(name) { + var self = this; + if (!confirm('Remove collection "' + name + '"?')) return; + api.removeCollection(name).then(function(r) { + if (r.success) { + self.showToast('Collection removed', 'success'); + setTimeout(function() { location.reload(); }, 1500); + } else { + self.showToast('Failed: ' + (r.error || 'Unknown'), 'error'); + } + }); + }, + + 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 }); diff --git a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/api.js b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/api.js index 6d388deb..41bf543f 100644 --- a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/api.js +++ b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/api.js @@ -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: {} }); diff --git a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/backends.js b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/backends.js index 92b360ce..047148ab 100644 --- a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/backends.js +++ b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/backends.js @@ -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(); diff --git a/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy b/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy index 13a37c5e..ec5e77f8 100755 --- a/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy +++ b/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy @@ -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" }, diff --git a/package/secubox/secubox-app-crowdsec-custom/Makefile b/package/secubox/secubox-app-crowdsec-custom/Makefile index cd160fb7..22e2a0f0 100644 --- a/package/secubox/secubox-app-crowdsec-custom/Makefile +++ b/package/secubox/secubox-app-crowdsec-custom/Makefile @@ -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" diff --git a/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-gitea.yaml b/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-gitea.yaml new file mode 100644 index 00000000..3fd44ae3 --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-gitea.yaml @@ -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 diff --git a/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-haproxy.yaml b/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-haproxy.yaml new file mode 100644 index 00000000..7c57bbf5 --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-haproxy.yaml @@ -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')" diff --git a/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-streamlit.yaml b/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-streamlit.yaml new file mode 100644 index 00000000..bb475774 --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-streamlit.yaml @@ -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')" diff --git a/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-webapp.yaml b/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-webapp.yaml new file mode 100644 index 00000000..b9937824 --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/acquis.d/secubox-webapp.yaml @@ -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 diff --git a/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-gitea.yaml b/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-gitea.yaml new file mode 100644 index 00000000..ccb76ece --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-gitea.yaml @@ -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 diff --git a/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-haproxy.yaml b/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-haproxy.yaml new file mode 100644 index 00000000..af4f8cde --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-haproxy.yaml @@ -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 diff --git a/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-streamlit.yaml b/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-streamlit.yaml new file mode 100644 index 00000000..319fe044 --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-streamlit.yaml @@ -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 diff --git a/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-webapp.yaml b/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-webapp.yaml new file mode 100644 index 00000000..ab351da6 --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/parsers/s01-parse/secubox-webapp.yaml @@ -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 diff --git a/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-gitea-bruteforce.yaml b/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-gitea-bruteforce.yaml new file mode 100644 index 00000000..39f83ea2 --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-gitea-bruteforce.yaml @@ -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 diff --git a/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-haproxy-bruteforce.yaml b/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-haproxy-bruteforce.yaml new file mode 100644 index 00000000..6ec3627c --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-haproxy-bruteforce.yaml @@ -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 diff --git a/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-streamlit-bruteforce.yaml b/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-streamlit-bruteforce.yaml new file mode 100644 index 00000000..109db7b2 --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-streamlit-bruteforce.yaml @@ -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 diff --git a/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-webapp-bruteforce.yaml b/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-webapp-bruteforce.yaml new file mode 100644 index 00000000..00390c3d --- /dev/null +++ b/package/secubox/secubox-app-crowdsec-custom/files/scenarios/secubox-webapp-bruteforce.yaml @@ -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 diff --git a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl index 9ae5d1dc..7a3779eb 100644 --- a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl +++ b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl @@ -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