From bda567ed98a3f8cb584ecc5f52f1f34136f0c292 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 10 Feb 2026 10:40:30 +0100 Subject: [PATCH] feat(security-threats): Add visit stats with country and URL metrics - Add get_visit_stats RPCD method parsing mitmproxy threats.log - Returns total requests, by_country, by_host, by_type, by_severity, bots_vs_humans breakdown, and top_urls (all top 10) - Add callGetVisitStats RPC declaration to api.js - Add renderVisitStats function to dashboard with traffic analytics grid - Shows traffic breakdown by country, host, and URL patterns Co-Authored-By: Claude Opus 4.5 --- .../resources/secubox-security-threats/api.js | 13 ++- .../secubox-security-threats/dashboard.js | 85 +++++++++++++++++++ .../rpcd/luci.secubox-security-threats | 25 ++++++ 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js index 500dfa1f..6962f03d 100644 --- a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js +++ b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js @@ -26,6 +26,12 @@ var callGetSecurityStats = rpc.declare({ expect: { } }); +var callGetVisitStats = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'get_visit_stats', + expect: { } +}); + var callBlockThreat = rpc.declare({ object: 'luci.secubox-security-threats', method: 'block_threat', @@ -106,7 +112,8 @@ function getDashboardData() { callGetSecurityStats(), callGetThreatIntel().catch(function() { return {}; }), callGetMeshIocs().catch(function() { return { iocs: [] }; }), - callGetMeshPeers().catch(function() { return { peers: [] }; }) + callGetMeshPeers().catch(function() { return { peers: [] }; }), + callGetVisitStats().catch(function() { return {}; }) ]).then(function(results) { return { status: results[0] || {}, @@ -115,7 +122,8 @@ function getDashboardData() { securityStats: results[3] || {}, threatIntel: results[4] || {}, meshIocs: results[5].iocs || [], - meshPeers: results[6].peers || [] + meshPeers: results[6].peers || [], + visitStats: results[7] || {} }; }); } @@ -125,6 +133,7 @@ return baseclass.extend({ getActiveThreats: callGetActiveThreats, getBlockedIPs: callGetBlockedIPs, getSecurityStats: callGetSecurityStats, + getVisitStats: callGetVisitStats, blockThreat: callBlockThreat, whitelistHost: callWhitelistHost, removeWhitelist: callRemoveWhitelist, diff --git a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js index f2aad786..eafe7061 100644 --- a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js +++ b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js @@ -20,6 +20,7 @@ return L.view.extend({ var intel = data.threatIntel || {}; var meshIocs = data.meshIocs || []; var meshPeers = data.meshPeers || []; + var visitStats = data.visitStats || {}; poll.add(L.bind(function() { this.handleRefresh(); }, this), 15); @@ -27,6 +28,7 @@ return L.view.extend({ E('style', {}, this.getStyles()), this.renderStatusBar(status), this.renderFirewallStats(stats), + this.renderVisitStats(visitStats), this.renderMeshIntel(intel, meshIocs, meshPeers), this.renderThreats(threats), this.renderBlocked(blocked) @@ -85,6 +87,89 @@ return L.view.extend({ ]); }, + renderVisitStats: function(stats) { + if (!stats || !stats.total_requests) return null; + + var countries = stats.by_country || []; + var hosts = stats.by_host || []; + var urls = stats.top_urls || []; + var bots = stats.bots_vs_humans || {}; + + return E('div', { 'class': 'si-section' }, [ + E('h3', {}, 'Traffic Analytics (' + stats.total_requests + ' requests)'), + + // Summary cards + E('div', { 'class': 'si-stats-grid' }, [ + { label: 'Total Requests', value: API.formatNumber(stats.total_requests), cls: 'blue' }, + { label: 'Bot Traffic', value: API.formatNumber(bots.bots || 0), cls: 'orange' }, + { label: 'Human Traffic', value: API.formatNumber(bots.humans || 0), cls: 'green' }, + { label: 'Countries', value: String(countries.length), cls: 'purple' } + ].map(function(item) { + return E('div', { 'class': 'si-stat ' + item.cls }, [ + E('div', { 'class': 'si-stat-val' }, item.value), + E('div', { 'class': 'si-stat-label' }, item.label) + ]); + })), + + // Two-column layout for tables + E('div', { 'style': 'display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px;' }, [ + // Countries table + E('div', { 'class': 'si-subsection' }, [ + E('h4', {}, 'Top Countries'), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Country'), + E('th', { 'class': 'th' }, 'Requests') + ]) + ].concat( + countries.slice(0, 8).map(function(c) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, c.country || '??'), + E('td', { 'class': 'td' }, String(c.count || 0)) + ]); + }) + )) + ]), + + // Hosts table + E('div', { 'class': 'si-subsection' }, [ + E('h4', {}, 'Top Hosts'), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Host'), + E('th', { 'class': 'th' }, 'Requests') + ]) + ].concat( + hosts.slice(0, 8).map(function(h) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td si-mono' }, h.host || '-'), + E('td', { 'class': 'td' }, String(h.count || 0)) + ]); + }) + )) + ]) + ]), + + // Top URLs + E('div', { 'class': 'si-subsection', 'style': 'margin-top:20px;' }, [ + E('h4', {}, 'Top URLs'), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'URL'), + E('th', { 'class': 'th' }, 'Hits') + ]) + ].concat( + urls.slice(0, 10).map(function(u) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td si-mono si-pattern', 'style': 'max-width:400px;' }, u.url || '-'), + E('td', { 'class': 'td' }, String(u.count || 0)) + ]); + }) + )) + ]) + ]); + }, + renderMeshIntel: function(intel, iocs, peers) { var self = this; var enabled = intel.enabled; diff --git a/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats b/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats index 18269343..39024cdd 100755 --- a/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats +++ b/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats @@ -112,6 +112,7 @@ case "$1" in json_init json_add_object "status"; json_close_object json_add_object "get_security_stats"; json_close_object + json_add_object "get_visit_stats"; json_close_object json_add_object "get_active_threats"; json_close_object json_add_object "get_blocked_ips"; json_close_object json_add_object "block_threat" @@ -150,6 +151,30 @@ case "$1" in get_security_stats ;; + get_visit_stats) + # Parse threats.log for visit statistics + _log_file="/srv/mitmproxy/threats.log" + if [ -f "$_log_file" ] && command -v jq >/dev/null 2>&1; then + # Get stats from last 1000 entries + tail -1000 "$_log_file" 2>/dev/null | jq -sc ' + { + total_requests: length, + by_country: (group_by(.country) | map({country: .[0].country, count: length}) | sort_by(.count) | reverse | .[0:10]), + by_host: (group_by(.host) | map({host: .[0].host, count: length}) | sort_by(.count) | reverse | .[0:10]), + by_type: (group_by(.type) | map({type: .[0].type, count: length}) | sort_by(.count) | reverse), + by_severity: (group_by(.severity) | map({severity: .[0].severity, count: length})), + bots_vs_humans: { + bots: (map(select(.is_bot == true)) | length), + humans: (map(select(.is_bot != true)) | length) + }, + top_urls: (group_by(.request) | map({url: .[0].request, count: length}) | sort_by(.count) | reverse | .[0:10]) + } + ' 2>/dev/null || echo '{"total_requests":0,"by_country":[],"by_host":[],"by_type":[],"by_severity":[],"bots_vs_humans":{"bots":0,"humans":0},"top_urls":[]}' + else + echo '{"total_requests":0,"by_country":[],"by_host":[],"by_type":[],"by_severity":[],"bots_vs_humans":{"bots":0,"humans":0},"top_urls":[]}' + fi + ;; + get_active_threats) _log_file="/srv/mitmproxy/threats.log" if [ -f "$_log_file" ]; then