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 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-10 10:40:30 +01:00
parent 7dd5f7cb8e
commit bda567ed98
3 changed files with 121 additions and 2 deletions

View File

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

View File

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

View File

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