feat: add flow-based country traffic visualization to agent summary globe and fix compilation errors
This commit is contained in:
1 parent
e9f35c5a6b
commit
8cd4f95856
5 files changed
+164
-11
No files matched your search
+101
-9
@@ -1582,21 +1582,113 @@ function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) {
|
||||
|
||||
function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) {
|
||||
const d = getDB();
|
||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM intel_ip_reputation`).get();
|
||||
if (!latest?.t) return [];
|
||||
|
||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||
const macs = AGENT_MAC_MAP[agentUuid];
|
||||
const placeholders = macs.map(() => '?').join(',');
|
||||
return d.prepare(`
|
||||
SELECT country AS country_name, SUM(download) AS download, SUM(upload) AS upload, COUNT(*) AS flow_count
|
||||
FROM intel_ip_reputation
|
||||
WHERE (mac_address IN (${placeholders}) OR local_ip IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})))
|
||||
AND country IS NOT NULL AND fetched_at = ?
|
||||
GROUP BY country
|
||||
|
||||
const rows = d.prepare(`
|
||||
SELECT g.country AS country_name,
|
||||
SUM(f.bytes_download) AS download,
|
||||
SUM(f.bytes_upload) AS upload,
|
||||
COUNT(*) AS flow_count
|
||||
FROM (
|
||||
SELECT dst_ip, bytes_download, bytes_upload
|
||||
FROM flows
|
||||
WHERE src_mac IN (${placeholders})
|
||||
ORDER BY last_seen DESC, fetched_at DESC
|
||||
LIMIT 500
|
||||
) f
|
||||
JOIN geoip_cache g ON f.dst_ip = g.ip_address
|
||||
WHERE g.country IS NOT NULL
|
||||
AND g.country != 'Local'
|
||||
GROUP BY g.country
|
||||
ORDER BY download DESC
|
||||
LIMIT ?
|
||||
`).all(...macs, ...macs, latest.t, limit);
|
||||
`).all(...macs, limit);
|
||||
|
||||
// Build dynamic name-to-code mapping from bandwidth_countries
|
||||
const nameToCodeMap = {};
|
||||
try {
|
||||
const mappingRows = d.prepare("SELECT DISTINCT country_code, country_name FROM bandwidth_countries WHERE country_code IS NOT NULL").all();
|
||||
for (const r of mappingRows) {
|
||||
if (r.country_name) {
|
||||
nameToCodeMap[r.country_name.toLowerCase().trim()] = r.country_code;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[DB] Failed to build country code mapping:', err);
|
||||
}
|
||||
|
||||
// Fallback/standard mapping
|
||||
const fallbackMap = {
|
||||
'indonesia': 'ID',
|
||||
'united states': 'US',
|
||||
'united kingdom': 'GB',
|
||||
'great britain': 'GB',
|
||||
'singapore': 'SG',
|
||||
'hong kong': 'HK',
|
||||
'japan': 'JP',
|
||||
'canada': 'CA',
|
||||
'australia': 'AU',
|
||||
'germany': 'DE',
|
||||
'france': 'FR',
|
||||
'india': 'IN',
|
||||
'china': 'CN',
|
||||
'south korea': 'KR',
|
||||
'russia': 'RU',
|
||||
'russian federation': 'RU',
|
||||
'brazil': 'BR',
|
||||
'italy': 'IT',
|
||||
'spain': 'ES',
|
||||
'netherlands': 'NL',
|
||||
'the netherlands': 'NL',
|
||||
'switzerland': 'CH',
|
||||
'sweden': 'SE',
|
||||
'norway': 'NO',
|
||||
'finland': 'FI',
|
||||
'denmark': 'DK',
|
||||
'ireland': 'IE',
|
||||
'belgium': 'BE',
|
||||
'austria': 'AT',
|
||||
'malaysia': 'MY',
|
||||
'thailand': 'TH',
|
||||
'vietnam': 'VN',
|
||||
'philippines': 'PH',
|
||||
'taiwan': 'TW',
|
||||
'new zealand': 'NZ',
|
||||
'south africa': 'ZA',
|
||||
'mexico': 'MX',
|
||||
'argentina': 'AR',
|
||||
'chile': 'CL',
|
||||
'colombia': 'CO',
|
||||
'turkey': 'TR',
|
||||
'saudi arabia': 'SA',
|
||||
'united arab emirates': 'AE',
|
||||
'egypt': 'EG',
|
||||
'israel': 'IL',
|
||||
'ukraine': 'UA',
|
||||
'poland': 'PL',
|
||||
'romania': 'RO',
|
||||
'greece': 'GR',
|
||||
'hungary': 'HU',
|
||||
'bangladesh': 'BD',
|
||||
'seychelles': 'SC',
|
||||
'luxembourg': 'LU',
|
||||
'pakistan': 'PK'
|
||||
};
|
||||
|
||||
return rows.map(r => {
|
||||
const normalizedName = r.country_name.toLowerCase().trim();
|
||||
const code = nameToCodeMap[normalizedName] || fallbackMap[normalizedName] || null;
|
||||
return {
|
||||
country_code: code,
|
||||
country_name: r.country_name,
|
||||
download: r.download ?? 0,
|
||||
upload: r.upload ?? 0,
|
||||
flow_count: r.flow_count ?? 0
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const countryLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries`).get();
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
const db = require('../database');
|
||||
|
||||
function runTest() {
|
||||
console.log('=== STARTING TDD TEST FOR AGENT GLOBE TRAFFIC ===');
|
||||
|
||||
const agentUuid = '8A-V3-PB-85';
|
||||
console.log(`- Fetching countries for agent: ${agentUuid}`);
|
||||
const countries = db.getLatestCountries(15, null, agentUuid);
|
||||
|
||||
if (!Array.isArray(countries)) {
|
||||
throw new Error('Countries result should be an array');
|
||||
}
|
||||
|
||||
console.log(`- Retrieved ${countries.length} country stats`);
|
||||
if (countries.length === 0) {
|
||||
throw new Error('No country records found for agent flows. Verify if sample flows/geoip cache is set up.');
|
||||
}
|
||||
|
||||
// Check each record has country_code and country_name, and valid values
|
||||
for (const r of countries) {
|
||||
console.log(` Country: ${r.country_name} (${r.country_code}) | Download: ${r.download} | Upload: ${r.upload} | Flows: ${r.flow_count}`);
|
||||
|
||||
if (!r.country_name) {
|
||||
throw new Error('Record has missing country_name');
|
||||
}
|
||||
if (!r.country_code) {
|
||||
throw new Error(`Record for ${r.country_name} is missing country_code!`);
|
||||
}
|
||||
if (r.country_code.length !== 2) {
|
||||
throw new Error(`Invalid country_code format for ${r.country_name}: ${r.country_code}`);
|
||||
}
|
||||
if (typeof r.download !== 'number' || typeof r.upload !== 'number') {
|
||||
throw new Error(`Invalid download/upload type for ${r.country_name}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== ALL AGENT GLOBE TRAFFIC TESTS PASSED SUCCESSFULLY! ===');
|
||||
}
|
||||
|
||||
try {
|
||||
runTest();
|
||||
} catch (err) {
|
||||
console.error('\n❌ TEST FAILED:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -313,7 +313,7 @@ export function DeviceDetailModal({ ip, deviceLabel, macAddress, onClose }: Prop
|
||||
data.flows.length === 0 ? <Empty msg="Tidak ada flows untuk device ini." /> : (
|
||||
<TableWrap head={["Destination IP", "Port", "Domain / App", "Proto", "↓", "↑", "Last Seen"]}>
|
||||
{data.flows.map((f, i) => {
|
||||
const exp = explainAppOrPort(f.app_label, f.domain, f.dst_port || 0);
|
||||
const exp = explainAppOrPort(f.app_label ?? null, f.domain ?? null, f.dst_port || 0);
|
||||
return (
|
||||
<tr key={i} className="hover:bg-white/[0.03] transition-colors">
|
||||
<td className="px-3 py-2 whitespace-nowrap">
|
||||
|
||||
@@ -23,7 +23,22 @@ const COUNTRY_COORDS: Record<string, [number, number]> = {
|
||||
"LU": [6.1296, 49.8153],
|
||||
"NL": [5.2913, 52.1326],
|
||||
"TR": [35.2433, 38.9637],
|
||||
"UA": [31.1656, 48.3794]
|
||||
"UA": [31.1656, 48.3794],
|
||||
"RU": [105.3188, 61.5240],
|
||||
"HU": [19.5033, 47.1625],
|
||||
"BD": [90.3563, 23.6850],
|
||||
"AU": [133.7751, -25.2744],
|
||||
"IN": [78.9629, 20.5937],
|
||||
"DE": [10.4515, 51.1657],
|
||||
"FR": [2.2137, 46.2276],
|
||||
"ES": [-3.7492, 40.4637],
|
||||
"BR": [-51.9253, -14.2350],
|
||||
"MY": [101.9758, 4.2105],
|
||||
"TH": [100.9925, 15.8700],
|
||||
"VN": [108.2772, 14.0583],
|
||||
"PH": [121.7740, 12.8797],
|
||||
"TW": [120.9605, 23.6978],
|
||||
"GR": [21.8243, 39.0742]
|
||||
};
|
||||
|
||||
// Jakarta, Indonesia as Origin
|
||||
|
||||
@@ -129,6 +129,7 @@ export interface DeviceStat {
|
||||
download: number;
|
||||
upload: number;
|
||||
last_seen: string;
|
||||
is_gateway_routed?: number | null;
|
||||
}
|
||||
|
||||
export interface ThreatStat {
|
||||
|
||||
Reference in new issue
Block a user