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:
parent
7dd5f7cb8e
commit
bda567ed98
@ -26,6 +26,12 @@ var callGetSecurityStats = rpc.declare({
|
|||||||
expect: { }
|
expect: { }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var callGetVisitStats = rpc.declare({
|
||||||
|
object: 'luci.secubox-security-threats',
|
||||||
|
method: 'get_visit_stats',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
var callBlockThreat = rpc.declare({
|
var callBlockThreat = rpc.declare({
|
||||||
object: 'luci.secubox-security-threats',
|
object: 'luci.secubox-security-threats',
|
||||||
method: 'block_threat',
|
method: 'block_threat',
|
||||||
@ -106,7 +112,8 @@ function getDashboardData() {
|
|||||||
callGetSecurityStats(),
|
callGetSecurityStats(),
|
||||||
callGetThreatIntel().catch(function() { return {}; }),
|
callGetThreatIntel().catch(function() { return {}; }),
|
||||||
callGetMeshIocs().catch(function() { return { iocs: [] }; }),
|
callGetMeshIocs().catch(function() { return { iocs: [] }; }),
|
||||||
callGetMeshPeers().catch(function() { return { peers: [] }; })
|
callGetMeshPeers().catch(function() { return { peers: [] }; }),
|
||||||
|
callGetVisitStats().catch(function() { return {}; })
|
||||||
]).then(function(results) {
|
]).then(function(results) {
|
||||||
return {
|
return {
|
||||||
status: results[0] || {},
|
status: results[0] || {},
|
||||||
@ -115,7 +122,8 @@ function getDashboardData() {
|
|||||||
securityStats: results[3] || {},
|
securityStats: results[3] || {},
|
||||||
threatIntel: results[4] || {},
|
threatIntel: results[4] || {},
|
||||||
meshIocs: results[5].iocs || [],
|
meshIocs: results[5].iocs || [],
|
||||||
meshPeers: results[6].peers || []
|
meshPeers: results[6].peers || [],
|
||||||
|
visitStats: results[7] || {}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -125,6 +133,7 @@ return baseclass.extend({
|
|||||||
getActiveThreats: callGetActiveThreats,
|
getActiveThreats: callGetActiveThreats,
|
||||||
getBlockedIPs: callGetBlockedIPs,
|
getBlockedIPs: callGetBlockedIPs,
|
||||||
getSecurityStats: callGetSecurityStats,
|
getSecurityStats: callGetSecurityStats,
|
||||||
|
getVisitStats: callGetVisitStats,
|
||||||
blockThreat: callBlockThreat,
|
blockThreat: callBlockThreat,
|
||||||
whitelistHost: callWhitelistHost,
|
whitelistHost: callWhitelistHost,
|
||||||
removeWhitelist: callRemoveWhitelist,
|
removeWhitelist: callRemoveWhitelist,
|
||||||
|
|||||||
@ -20,6 +20,7 @@ return L.view.extend({
|
|||||||
var intel = data.threatIntel || {};
|
var intel = data.threatIntel || {};
|
||||||
var meshIocs = data.meshIocs || [];
|
var meshIocs = data.meshIocs || [];
|
||||||
var meshPeers = data.meshPeers || [];
|
var meshPeers = data.meshPeers || [];
|
||||||
|
var visitStats = data.visitStats || {};
|
||||||
|
|
||||||
poll.add(L.bind(function() { this.handleRefresh(); }, this), 15);
|
poll.add(L.bind(function() { this.handleRefresh(); }, this), 15);
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ return L.view.extend({
|
|||||||
E('style', {}, this.getStyles()),
|
E('style', {}, this.getStyles()),
|
||||||
this.renderStatusBar(status),
|
this.renderStatusBar(status),
|
||||||
this.renderFirewallStats(stats),
|
this.renderFirewallStats(stats),
|
||||||
|
this.renderVisitStats(visitStats),
|
||||||
this.renderMeshIntel(intel, meshIocs, meshPeers),
|
this.renderMeshIntel(intel, meshIocs, meshPeers),
|
||||||
this.renderThreats(threats),
|
this.renderThreats(threats),
|
||||||
this.renderBlocked(blocked)
|
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) {
|
renderMeshIntel: function(intel, iocs, peers) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var enabled = intel.enabled;
|
var enabled = intel.enabled;
|
||||||
|
|||||||
@ -112,6 +112,7 @@ case "$1" in
|
|||||||
json_init
|
json_init
|
||||||
json_add_object "status"; json_close_object
|
json_add_object "status"; json_close_object
|
||||||
json_add_object "get_security_stats"; 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_active_threats"; json_close_object
|
||||||
json_add_object "get_blocked_ips"; json_close_object
|
json_add_object "get_blocked_ips"; json_close_object
|
||||||
json_add_object "block_threat"
|
json_add_object "block_threat"
|
||||||
@ -150,6 +151,30 @@ case "$1" in
|
|||||||
get_security_stats
|
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)
|
get_active_threats)
|
||||||
_log_file="/srv/mitmproxy/threats.log"
|
_log_file="/srv/mitmproxy/threats.log"
|
||||||
if [ -f "$_log_file" ]; then
|
if [ -f "$_log_file" ]; then
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user