From fe7f160de909c7aaa5ec65a8e69d5dc49783808d Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Wed, 7 Jan 2026 11:01:54 +0100 Subject: [PATCH] fix: Add missing API utility functions and fix data structure handling (v0.6.0-r9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add parseScenario() to format scenario names - Add getCountryFlag() to display country flag emojis - Add formatRelativeTime() for relative timestamps - Fix decisions data flattening in handleUnban, handleBulkUnban, submitBan, and polling - Fix getDashboardData to properly flatten alerts->decisions structure - Fix context error in overview renderDecisionsTable (this vs self) πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../resources/crowdsec-dashboard/api.js | 120 +++++++++++++++--- .../view/crowdsec-dashboard/decisions.js | 76 +++++++++-- .../view/crowdsec-dashboard/overview.js | 2 +- 3 files changed, 168 insertions(+), 30 deletions(-) diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js index 0e75c563..ad09e321 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js @@ -20,7 +20,7 @@ var callStatus = rpc.declare({ var callDecisions = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'decisions', - expect: { decisions: [] } + expect: { alerts: [] } }); var callAlerts = rpc.declare({ @@ -68,21 +68,21 @@ var callSecuboxLogs = rpc.declare({ var callCollectDebug = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'collect_debug', - expect: { success: false } + expect: { } }); var callBan = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'ban', params: ['ip', 'duration', 'reason'], - expect: { success: false } + expect: { } }); var callUnban = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'unban', params: ['ip'], - expect: { success: false } + expect: { } }); // CrowdSec v1.7.4+ features @@ -102,27 +102,27 @@ var callConfigureMetrics = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'configure_metrics', params: ['enable'], - expect: { success: false } + expect: { } }); var callCollections = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'collections', - expect: { } + expect: { collections: [] } }); var callInstallCollection = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'install_collection', params: ['collection'], - expect: { success: false } + expect: { } }); var callRemoveCollection = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'remove_collection', params: ['collection'], - expect: { success: false } + expect: { } }); var callUpdateHub = rpc.declare({ @@ -135,14 +135,14 @@ var callRegisterBouncer = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'register_bouncer', params: ['bouncer_name'], - expect: { success: false } + expect: { } }); var callDeleteBouncer = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'delete_bouncer', params: ['bouncer_name'], - expect: { success: false } + expect: { } }); // Firewall Bouncer Management @@ -156,7 +156,7 @@ var callControlFirewallBouncer = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'control_firewall_bouncer', params: ['action'], - expect: { success: false } + expect: { } }); var callFirewallBouncerConfig = rpc.declare({ @@ -169,7 +169,7 @@ var callUpdateFirewallBouncerConfig = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'update_firewall_bouncer_config', params: ['key', 'value'], - expect: { success: false } + expect: { } }); var callNftablesStats = rpc.declare({ @@ -209,9 +209,75 @@ function formatDate(dateStr) { } } +function isValidIP(ip) { + if (!ip) return false; + + // IPv4 regex + var ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + + // IPv6 regex (simplified) + var ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; + + return ipv4Regex.test(ip) || ipv6Regex.test(ip); +} + +function parseScenario(scenario) { + if (!scenario) return 'N/A'; + + // Extract human-readable part from scenario name + // e.g., "crowdsecurity/ssh-bruteforce" -> "SSH Bruteforce" + var parts = scenario.split('/'); + var name = parts[parts.length - 1]; + + // Convert dash-separated to title case + return name.split('-').map(function(word) { + return word.charAt(0).toUpperCase() + word.slice(1); + }).join(' '); +} + +function getCountryFlag(countryCode) { + if (!countryCode || countryCode === 'N/A') return ''; + + // Convert country code to flag emoji + // e.g., "US" -> "πŸ‡ΊπŸ‡Έ" + var code = countryCode.toUpperCase(); + if (code.length !== 2) return ''; + + var codePoints = []; + for (var i = 0; i < code.length; i++) { + codePoints.push(0x1F1E6 - 65 + code.charCodeAt(i)); + } + return String.fromCodePoint.apply(null, codePoints); +} + +function formatRelativeTime(dateStr) { + if (!dateStr) return 'N/A'; + + try { + var date = new Date(dateStr); + var now = new Date(); + var seconds = Math.floor((now - date) / 1000); + + if (seconds < 60) return seconds + 's ago'; + if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago'; + if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago'; + if (seconds < 2592000) return Math.floor(seconds / 86400) + 'd ago'; + return Math.floor(seconds / 2592000) + 'mo ago'; + } catch(e) { + return dateStr; + } +} + return baseclass.extend({ getStatus: callStatus, - getDecisions: callDecisions, + getDecisions: function() { + return callDecisions().then(function(result) { + console.log('[API] getDecisions raw result:', result); + console.log('[API] getDecisions result type:', typeof result); + console.log('[API] getDecisions is array:', Array.isArray(result)); + return result; + }); + }, getAlerts: callAlerts, getBouncers: callBouncers, getMetrics: callMetrics, @@ -247,6 +313,14 @@ return baseclass.extend({ formatDuration: formatDuration, formatDate: formatDate, + formatRelativeTime: formatRelativeTime, + isValidIP: isValidIP, + parseScenario: parseScenario, + getCountryFlag: getCountryFlag, + + // Aliases for compatibility + banIP: callBan, + unbanIP: callUnban, getDashboardData: function() { return Promise.all([ @@ -258,15 +332,25 @@ return baseclass.extend({ // Check if any result has an error (service not running) var status = results[0] || {}; var stats = results[1] || {}; - var decisions = results[2] || {}; - var alerts = results[3] || {}; + var decisionsRaw = results[2] || []; + var alerts = results[3] || []; + + // Flatten alerts->decisions structure + var decisions = []; + if (Array.isArray(decisionsRaw)) { + decisionsRaw.forEach(function(alert) { + if (alert.decisions && Array.isArray(alert.decisions)) { + decisions = decisions.concat(alert.decisions); + } + }); + } return { status: status, stats: (stats.error) ? {} : stats, - decisions: (decisions.error) ? [] : (decisions.decisions || []), - alerts: (alerts.error) ? [] : (alerts.alerts || []), - error: stats.error || decisions.error || alerts.error || null + decisions: decisions, + alerts: alerts, + error: stats.error || 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 4c37bca1..0f7ef911 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 @@ -82,11 +82,11 @@ return view.extend({ 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'); @@ -97,7 +97,15 @@ return view.extend({ } }).then(function(data) { if (data) { - self.decisions = 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(); } @@ -127,18 +135,26 @@ return view.extend({ 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' : ''), + 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) { - self.decisions = 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(); } @@ -325,29 +341,59 @@ return view.extend({ 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'); self.closeBanModal(); - return self.csApi.getDecisions(); + // 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); + }); } else { self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error'); return null; } }).then(function(data) { + console.log('[Decisions] Updated decisions data:', data); if (data) { - self.decisions = 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; - this.decisions = Array.isArray(data) ? data : []; + // 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 view = E('div', { 'class': 'crowdsec-dashboard' }, [ @@ -389,7 +435,15 @@ return view.extend({ // Setup polling poll.add(function() { return self.csApi.getDecisions().then(function(newData) { - self.decisions = Array.isArray(newData) ? 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(); }); 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 6af4ae81..d2aeb93c 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 @@ -136,7 +136,7 @@ return view.extend({ E('td', {}, E('button', { 'class': 'cs-btn cs-btn-danger cs-btn-sm', 'data-ip': d.value, - 'click': ui.createHandlerFn(this, 'handleUnban', d.value) + 'click': ui.createHandlerFn(self, 'handleUnban', d.value) }, 'Unban')) ]); });