Fix devices bandwidth and MAC descriptors
This commit is contained in:
1 parent
4eee901d76
commit
448736bc03
4 files changed
+171
-117
No files matched your search
+81
-107
@@ -883,125 +883,99 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) {
|
|||||||
const intelList = d.prepare("SELECT * FROM intel_device_discovery").all();
|
const intelList = d.prepare("SELECT * FROM intel_device_discovery").all();
|
||||||
const intelMap = new Map(intelList.map(i => [i.ip_address, i]));
|
const intelMap = new Map(intelList.map(i => [i.ip_address, i]));
|
||||||
|
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
// Get all devices in latest snapshot from devices table
|
||||||
const macs = AGENT_MAC_MAP[agentUuid];
|
const devices = d.prepare(`SELECT * FROM devices WHERE fetched_at = ?`).all(latest.t);
|
||||||
const placeholders = macs.map(() => '?').join(',');
|
|
||||||
|
|
||||||
// Find latest fetched_at in flows
|
|
||||||
const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
|
||||||
|
|
||||||
// Get unique flow client IPs with bandwidth aggregation
|
|
||||||
let flowClients = [];
|
|
||||||
if (latestFlowFetch?.t) {
|
|
||||||
flowClients = d.prepare(`
|
|
||||||
SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload
|
|
||||||
FROM flows
|
|
||||||
WHERE src_mac IN (${placeholders}) AND fetched_at = ?
|
|
||||||
GROUP BY src_ip
|
|
||||||
ORDER BY flow_download DESC
|
|
||||||
`).all(...macs, latestFlowFetch.t);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all devices in latest snapshot
|
// Get active flow bandwidth in latest flows snapshot
|
||||||
const devices = d.prepare(`SELECT * FROM devices WHERE fetched_at = ?`).all(latest.t);
|
const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
||||||
const devicesMap = new Map(devices.map(dev => [dev.ip_address, dev]));
|
let flowsBandwidth = [];
|
||||||
|
if (latestFlowFetch?.t) {
|
||||||
const resolved = [];
|
flowsBandwidth = d.prepare(`
|
||||||
const seenIps = new Set();
|
SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload
|
||||||
|
FROM flows
|
||||||
// First, add all devices that are explicitly in flows (active)
|
WHERE fetched_at = ?
|
||||||
for (const fc of flowClients) {
|
GROUP BY src_ip
|
||||||
if (!fc.src_ip) continue;
|
`).all(latestFlowFetch.t);
|
||||||
seenIps.add(fc.src_ip);
|
|
||||||
|
|
||||||
const devInfo = devicesMap.get(fc.src_ip);
|
|
||||||
const intelInfo = intelMap.get(fc.src_ip);
|
|
||||||
|
|
||||||
const dbLabel = devInfo ? devInfo.device_label : (intelInfo ? intelInfo.device_label : null);
|
|
||||||
const dbMan = devInfo ? devInfo.manufacturer : (intelInfo ? intelInfo.manufacturer : null);
|
|
||||||
const dbType = intelInfo ? intelInfo.device_type : null;
|
|
||||||
|
|
||||||
const meta = resolveDeviceMetadata(fc.src_ip, fc.src_mac, dbLabel, dbMan, dbType);
|
|
||||||
|
|
||||||
const isRouted = fc.src_mac === '04:f4:1c:ce:c2:e6' && fc.src_ip !== '10.6.50.25' && fc.src_ip !== '10.6.12.242';
|
|
||||||
|
|
||||||
resolved.push({
|
|
||||||
id: devInfo ? devInfo.id : null,
|
|
||||||
site_uuid: devInfo ? devInfo.site_uuid : siteUuid,
|
|
||||||
fetched_at: latest.t,
|
|
||||||
mac_address: fc.src_mac,
|
|
||||||
ip_address: fc.src_ip,
|
|
||||||
device_label: meta.label,
|
|
||||||
device_type: meta.type,
|
|
||||||
os_label: meta.os,
|
|
||||||
manufacturer: meta.manufacturer,
|
|
||||||
download: fc.flow_download,
|
|
||||||
upload: fc.flow_upload,
|
|
||||||
total: fc.flow_download + fc.flow_upload,
|
|
||||||
is_gateway_routed: isRouted ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, add other devices in devices table belonging to the agent's subnet (e.g. inactive/idle)
|
|
||||||
for (const dev of devices) {
|
|
||||||
if (seenIps.has(dev.ip_address)) continue;
|
|
||||||
|
|
||||||
// Check if this device belongs to the agent
|
|
||||||
let isAgentDevice = macs.includes(dev.mac_address);
|
|
||||||
if (!isAgentDevice && dev.ip_address) {
|
|
||||||
const devOctets = dev.ip_address.split('.');
|
|
||||||
if (devOctets.length === 4) {
|
|
||||||
const prefix3 = devOctets.slice(0, 3).join('.');
|
|
||||||
isAgentDevice = flowClients.some(fc => fc.src_ip && fc.src_ip.startsWith(prefix3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAgentDevice) {
|
|
||||||
const intelInfo = intelMap.get(dev.ip_address);
|
|
||||||
const meta = resolveDeviceMetadata(dev.ip_address, dev.mac_address, dev.device_label, dev.manufacturer, intelInfo ? intelInfo.device_type : null);
|
|
||||||
|
|
||||||
const isRouted = dev.mac_address === '04:f4:1c:ce:c2:e6' && dev.ip_address !== '10.6.50.25' && dev.ip_address !== '10.6.12.242';
|
|
||||||
|
|
||||||
resolved.push({
|
|
||||||
...dev,
|
|
||||||
device_label: meta.label,
|
|
||||||
device_type: meta.type,
|
|
||||||
os_label: meta.os,
|
|
||||||
manufacturer: meta.manufacturer,
|
|
||||||
download: dev.download || 0,
|
|
||||||
upload: dev.upload || 0,
|
|
||||||
total: dev.total || 0,
|
|
||||||
is_gateway_routed: isRouted ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by download desc
|
|
||||||
resolved.sort((a, b) => b.download - a.download);
|
|
||||||
return resolved.slice(0, limit);
|
|
||||||
}
|
}
|
||||||
|
const flowMap = new Map(flowsBandwidth.map(f => [f.src_ip, f]));
|
||||||
|
|
||||||
// Admin view: apply metadata parsing and gateway routed check for all devices
|
const resolved = [];
|
||||||
const rows = d.prepare(`
|
const seenIps = new Set();
|
||||||
SELECT * FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
|
||||||
`).all({ fetched_at: latest.t, limit, siteUuid });
|
|
||||||
|
|
||||||
return rows.map(dev => {
|
function processDevice(ip, mac, dbDev, flowInfo) {
|
||||||
const intelInfo = intelMap.get(dev.ip_address);
|
const intelInfo = intelMap.get(ip);
|
||||||
const meta = resolveDeviceMetadata(dev.ip_address, dev.mac_address, dev.device_label, dev.manufacturer, intelInfo ? intelInfo.device_type : null);
|
const dbLabel = dbDev ? dbDev.device_label : (intelInfo ? intelInfo.device_label : null);
|
||||||
|
const dbMan = dbDev ? dbDev.manufacturer : (intelInfo ? intelInfo.manufacturer : null);
|
||||||
const isRouted = dev.mac_address === '04:f4:1c:ce:c2:e6' && dev.ip_address !== '10.6.50.25' && dev.ip_address !== '10.6.12.242';
|
const dbType = intelInfo ? intelInfo.device_type : null;
|
||||||
|
|
||||||
|
const meta = resolveDeviceMetadata(ip, mac, dbLabel, dbMan, dbType);
|
||||||
|
const isRouted = mac === '04:f4:1c:ce:c2:e6' && ip !== '10.6.50.25' && ip !== '10.6.12.242';
|
||||||
|
|
||||||
|
const download = flowInfo ? flowInfo.flow_download : (dbDev ? dbDev.download : 0);
|
||||||
|
const upload = flowInfo ? flowInfo.flow_upload : (dbDev ? dbDev.upload : 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...dev,
|
id: dbDev ? dbDev.id : null,
|
||||||
mac_address: dev.mac_address,
|
site_uuid: dbDev ? dbDev.site_uuid : siteUuid,
|
||||||
|
fetched_at: latest.t,
|
||||||
|
mac_address: mac,
|
||||||
|
ip_address: ip,
|
||||||
device_label: meta.label,
|
device_label: meta.label,
|
||||||
device_type: meta.type,
|
device_type: meta.type,
|
||||||
os_label: meta.os,
|
os_label: meta.os,
|
||||||
manufacturer: meta.manufacturer,
|
manufacturer: meta.manufacturer,
|
||||||
|
download: download || 0,
|
||||||
|
upload: upload || 0,
|
||||||
|
total: (download || 0) + (upload || 0),
|
||||||
is_gateway_routed: isRouted ? 1 : 0
|
is_gateway_routed: isRouted ? 1 : 0
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// 1. Process all devices in the devices table
|
||||||
|
for (const dev of devices) {
|
||||||
|
seenIps.add(dev.ip_address);
|
||||||
|
const flowInfo = flowMap.get(dev.ip_address);
|
||||||
|
resolved.push(processDevice(dev.ip_address, dev.mac_address, dev, flowInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Process any active flow IPs that are NOT present in the devices table (e.g. dynamic client IPs)
|
||||||
|
for (const flow of flowsBandwidth) {
|
||||||
|
if (!flow.src_ip || seenIps.has(flow.src_ip)) continue;
|
||||||
|
seenIps.add(flow.src_ip);
|
||||||
|
resolved.push(processDevice(flow.src_ip, flow.src_mac, null, flow));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Filter list based on role (Admin vs Agent)
|
||||||
|
let filtered = resolved;
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
filtered = resolved.filter(dev => {
|
||||||
|
// Direct MAC check
|
||||||
|
if (macs.includes(dev.mac_address)) {
|
||||||
|
// But if it is the routed gateway MAC, only keep if it belongs to the agent's subnet/IP range
|
||||||
|
if (dev.mac_address === '04:f4:1c:ce:c2:e6') {
|
||||||
|
return dev.ip_address.startsWith('10.6.');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Subnet check for client IPs
|
||||||
|
if (dev.ip_address) {
|
||||||
|
const octets = dev.ip_address.split('.');
|
||||||
|
if (octets.length === 4) {
|
||||||
|
if (agentUuid === '8A-V3-PB-85') return octets[0] === '10' && octets[1] === '6';
|
||||||
|
if (agentUuid === '2F-TF-1D-GK') return octets[0] === '10' && octets[1] === '6';
|
||||||
|
if (agentUuid === 'F6-2V-DT-8A') return octets[0] === '10' && octets[1] === '6';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} else if (siteUuid) {
|
||||||
|
filtered = resolved.filter(dev => dev.site_uuid === siteUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Sort by download DESC
|
||||||
|
filtered.sort((a, b) => b.download - a.download);
|
||||||
|
return filtered.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) {
|
function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) {
|
||||||
|
|||||||
+2
-2
@@ -1756,8 +1756,8 @@ async function fetchDeviceDetails(ip) {
|
|||||||
return {
|
return {
|
||||||
ip,
|
ip,
|
||||||
mac_address : mac,
|
mac_address : mac,
|
||||||
total_download : sumRow?.total_download ?? 0,
|
total_download : deviceRow?.download || sumRow?.total_download || 0,
|
||||||
total_upload : sumRow?.total_upload ?? 0,
|
total_upload : deviceRow?.upload || sumRow?.total_upload || 0,
|
||||||
flow_count : sumRow?.flow_count ?? 0,
|
flow_count : sumRow?.flow_count ?? 0,
|
||||||
device_info,
|
device_info,
|
||||||
top_apps,
|
top_apps,
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
const { getLatestDevices } = require('../database');
|
||||||
|
|
||||||
|
function runTest() {
|
||||||
|
console.log('=== STARTING TDD TEST FOR DEVICES BANDWIDTH ===');
|
||||||
|
|
||||||
|
// Test 1: Admin View
|
||||||
|
console.log('\nRunning Test 1: Admin View...');
|
||||||
|
const adminDevices = getLatestDevices(100, null, null);
|
||||||
|
if (!Array.isArray(adminDevices)) {
|
||||||
|
throw new Error('Admin devices should be an array');
|
||||||
|
}
|
||||||
|
console.log(`- Retrieved ${adminDevices.length} devices for Admin`);
|
||||||
|
|
||||||
|
// Verify descending sort
|
||||||
|
for (let i = 1; i < adminDevices.length; i++) {
|
||||||
|
if (adminDevices[i].download > adminDevices[i-1].download) {
|
||||||
|
throw new Error(`Admin devices not sorted correctly at index ${i}: ${adminDevices[i].download} > ${adminDevices[i-1].download}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('- Verified sort order is descending');
|
||||||
|
|
||||||
|
// Find any active reconstructed flow client (which has null id because it is not in the devices table)
|
||||||
|
const activeReconstructed = adminDevices.find(d => d.id === null && d.download > 0 && d.ip_address.startsWith('10.6.'));
|
||||||
|
if (!activeReconstructed) {
|
||||||
|
throw new Error('Should find at least one active reconstructed flow client (IP starting with 10.6. and id = null) in Admin view');
|
||||||
|
}
|
||||||
|
console.log(`- Active reconstructed device ${activeReconstructed.ip_address} has download: ${activeReconstructed.download} bytes (OK)`);
|
||||||
|
|
||||||
|
// Verify specific historical device (e.g., 10.250.192.202) has non-zero fallback bandwidth
|
||||||
|
const historicalDevice = adminDevices.find(d => d.ip_address === '10.250.192.202');
|
||||||
|
if (!historicalDevice) {
|
||||||
|
throw new Error('Historical device 10.250.192.202 should be present in Admin view');
|
||||||
|
}
|
||||||
|
if (historicalDevice.download === 0) {
|
||||||
|
throw new Error(`Historical device 10.250.192.202 should have non-zero fallback download, got ${historicalDevice.download}`);
|
||||||
|
}
|
||||||
|
console.log(`- Historical device 10.250.192.202 has download: ${historicalDevice.download} bytes (OK)`);
|
||||||
|
|
||||||
|
// Verify no duplicate IP addresses
|
||||||
|
const seenIps = new Set();
|
||||||
|
for (const d of adminDevices) {
|
||||||
|
if (seenIps.has(d.ip_address)) {
|
||||||
|
throw new Error(`Duplicate IP address found: ${d.ip_address}`);
|
||||||
|
}
|
||||||
|
seenIps.add(d.ip_address);
|
||||||
|
}
|
||||||
|
console.log('- Verified no duplicate IP addresses exist in the output');
|
||||||
|
|
||||||
|
// Test 2: Agent View
|
||||||
|
console.log('\nRunning Test 2: Agent View...');
|
||||||
|
const agentUuid = '8A-V3-PB-85'; // IFG LT.18
|
||||||
|
const agentDevices = getLatestDevices(100, null, agentUuid);
|
||||||
|
if (!Array.isArray(agentDevices)) {
|
||||||
|
throw new Error('Agent devices should be an array');
|
||||||
|
}
|
||||||
|
console.log(`- Retrieved ${agentDevices.length} devices for Agent ${agentUuid}`);
|
||||||
|
|
||||||
|
// Verify only agent devices are returned (IP starts with 10.6.)
|
||||||
|
for (const d of agentDevices) {
|
||||||
|
if (d.ip_address && !d.ip_address.startsWith('10.6.')) {
|
||||||
|
throw new Error(`Non-agent device IP ${d.ip_address} found in Agent view`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('- Verified all returned devices belong to the agent\'s subnet (10.6.x.x)');
|
||||||
|
|
||||||
|
console.log('\n=== ALL TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
runTest();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ TEST FAILED:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -38,21 +38,27 @@ export default function DevicesPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "MAC Address",
|
header: "MAC Address",
|
||||||
accessor: (row) =>
|
accessor: (row) => {
|
||||||
row.mac_address ? (
|
const isRouted = row.is_gateway_routed === 1;
|
||||||
<div className="flex flex-col gap-1">
|
const hasMan = row.manufacturer && row.manufacturer !== "-" && row.manufacturer !== "Unknown";
|
||||||
<code className="text-xs bg-secondary/50 px-1 py-0.5 rounded text-muted-foreground w-fit">
|
const info = hasMan ? row.manufacturer : (row.device_type && row.device_type !== "-" && row.device_type !== "Generic Client" ? row.device_type : "");
|
||||||
|
|
||||||
|
return row.mac_address ? (
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<code className="text-xs bg-secondary/50 px-1 py-0.5 rounded text-muted-foreground w-fit font-mono">
|
||||||
{row.mac_address}
|
{row.mac_address}
|
||||||
</code>
|
</code>
|
||||||
{row.is_gateway_routed === 1 && (
|
{(isRouted || info) && (
|
||||||
<span className="text-[10px] text-amber-500/80 font-sans italic">
|
<span className={`text-[10px] font-sans font-medium ${isRouted ? "text-amber-500/90 italic" : "text-slate-400/80"}`}>
|
||||||
Via Routed Gateway
|
{isRouted ? "Via Routed Gateway" : ""}
|
||||||
|
{isRouted && info ? ` • ${info}` : (!isRouted ? info : "")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
"-"
|
||||||
),
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ header: "Type", accessor: (row) => row.device_type || "-" },
|
{ header: "Type", accessor: (row) => row.device_type || "-" },
|
||||||
{ header: "OS", accessor: (row) => row.os_label || "-" },
|
{ header: "OS", accessor: (row) => row.os_label || "-" },
|
||||||
|
|||||||
Reference in new issue
Block a user