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: { }
|
||||
});
|
||||
|
||||
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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user