test path visu
This commit is contained in:
@@ -21,12 +21,13 @@ import * as d3 from 'd3';
|
||||
import { sankey as d3Sankey, sankeyLinkHorizontal, type SankeyGraph, type SankeyLink, type SankeyNode } from 'd3-sankey';
|
||||
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { fetchInterfaceHostProtocolAnalysis } from '../api/apiClient';
|
||||
import { fetchInterfaceHostProtocolAnalysis, fetchInterfaceProtocolPathAnalysis } from '../api/apiClient';
|
||||
import type {
|
||||
InterfaceHostProtocolAnalysisResponse,
|
||||
InterfaceHostProtocolEvidence,
|
||||
InterfaceProtocolPathAnalysisResponse,
|
||||
InterfaceProtocolPathEvidence,
|
||||
InterfaceProtocolAttachment,
|
||||
ProtocolEvidence,
|
||||
} from '../types/analysis';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
@@ -317,6 +318,88 @@ function buildTopologyData(interfaces: InterfaceProtocolAttachment[], options: T
|
||||
};
|
||||
}
|
||||
|
||||
function endpointText(ipAddress?: string | null, macAddress?: string | null) {
|
||||
return ipAddress ?? macAddress ?? 'unknown endpoint';
|
||||
}
|
||||
|
||||
function buildDirectionalSankeyData(paths: InterfaceProtocolPathEvidence[], options: TopologyOptions): TopologyData {
|
||||
const nodes = new Map<string, TopologyNode>();
|
||||
const links = new Map<string, TopologyLink>();
|
||||
|
||||
for (const path of paths) {
|
||||
const packetCount = path.packet_count;
|
||||
const ingressLabel = path.ingress_interface ? `${path.ingress_interface} (ingress)` : 'Unknown ingress';
|
||||
const ingressId = `ingress:${path.ingress_interface ?? 'unknown'}`;
|
||||
const sourceLabel = endpointText(path.src_ip_address, path.src_mac_address);
|
||||
const sourceId = `source:${path.src_ip_address ?? 'no-ip'}|${path.src_mac_address ?? 'no-mac'}`;
|
||||
const protocolLabel = path.protocol;
|
||||
const protocolId = `protocol:${protocolLabel}`;
|
||||
const destinationLabel = endpointText(path.dst_ip_address, path.dst_mac_address);
|
||||
const destinationId = `destination:${path.dst_ip_address ?? 'no-ip'}|${path.dst_mac_address ?? 'no-mac'}`;
|
||||
const egressLabel = path.egress_interface ? `${path.egress_interface} (egress)` : 'Unknown egress';
|
||||
const egressId = `egress:${path.egress_interface ?? 'unknown'}`;
|
||||
|
||||
ensureProtocolNode(nodes, ingressId, ingressLabel, 'interface').packetCount += packetCount;
|
||||
nodes.set(sourceId, {
|
||||
...(nodes.get(sourceId) ?? {
|
||||
id: sourceId,
|
||||
label: sourceLabel,
|
||||
kind: 'host' as const,
|
||||
packetCount: 0,
|
||||
ipAddress: path.src_ip_address,
|
||||
macAddress: path.src_mac_address,
|
||||
}),
|
||||
packetCount: (nodes.get(sourceId)?.packetCount ?? 0) + packetCount,
|
||||
});
|
||||
ensureProtocolNode(nodes, protocolId, protocolLabel, 'protocol').packetCount += packetCount;
|
||||
nodes.set(destinationId, {
|
||||
...(nodes.get(destinationId) ?? {
|
||||
id: destinationId,
|
||||
label: destinationLabel,
|
||||
kind: 'host' as const,
|
||||
packetCount: 0,
|
||||
ipAddress: path.dst_ip_address,
|
||||
macAddress: path.dst_mac_address,
|
||||
}),
|
||||
packetCount: (nodes.get(destinationId)?.packetCount ?? 0) + packetCount,
|
||||
});
|
||||
ensureProtocolNode(nodes, egressId, egressLabel, 'interface').packetCount += packetCount;
|
||||
|
||||
addOrUpdateLink(links, ingressId, sourceId, packetCount, `${ingressLabel} -> ${sourceLabel}`);
|
||||
|
||||
let currentNodeId = sourceId;
|
||||
let currentLabel = sourceLabel;
|
||||
|
||||
if (options.includeEthernetLayer && path.ethernet_protocol && path.ethernet_protocol !== protocolLabel) {
|
||||
const ethernetId = `ethernet:${path.ethernet_protocol}`;
|
||||
ensureProtocolNode(nodes, ethernetId, path.ethernet_protocol, 'ethernet').packetCount += packetCount;
|
||||
addOrUpdateLink(links, currentNodeId, ethernetId, packetCount, `${currentLabel} -> ${path.ethernet_protocol}`);
|
||||
currentNodeId = ethernetId;
|
||||
currentLabel = path.ethernet_protocol;
|
||||
}
|
||||
|
||||
if (options.includeIpLayer && path.ip_protocol && path.ip_protocol !== currentLabel && path.ip_protocol !== protocolLabel) {
|
||||
const ipId = `ip:${path.ip_protocol}`;
|
||||
ensureProtocolNode(nodes, ipId, path.ip_protocol, 'ip').packetCount += packetCount;
|
||||
addOrUpdateLink(links, currentNodeId, ipId, packetCount, `${currentLabel} -> ${path.ip_protocol}`);
|
||||
currentNodeId = ipId;
|
||||
currentLabel = path.ip_protocol;
|
||||
}
|
||||
|
||||
addOrUpdateLink(links, currentNodeId, protocolId, packetCount, `${currentLabel} -> ${protocolLabel}`);
|
||||
addOrUpdateLink(links, protocolId, destinationId, packetCount, `${protocolLabel} -> ${destinationLabel}`);
|
||||
addOrUpdateLink(links, destinationId, egressId, packetCount, `${destinationLabel} -> ${egressLabel}`);
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: Array.from(nodes.values()),
|
||||
links: Array.from(links.values()),
|
||||
heatmapRows: [],
|
||||
protocols: [],
|
||||
tableRows: [],
|
||||
};
|
||||
}
|
||||
|
||||
function SankeyTopology({ data }: { data: TopologyData }) {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
|
||||
@@ -685,22 +768,28 @@ export default function Analysis(): ReactElement {
|
||||
const [sinceMinutes, setSinceMinutes] = useState<number | null>(null);
|
||||
const [limitPerInterface, setLimitPerInterface] = useState(50);
|
||||
const [limitProtocolsPerHost, setLimitProtocolsPerHost] = useState(12);
|
||||
const [limitPaths, setLimitPaths] = useState(500);
|
||||
const [includeEthernetLayer, setIncludeEthernetLayer] = useState(false);
|
||||
const [includeIpLayer, setIncludeIpLayer] = useState(false);
|
||||
const [data, setData] = useState<InterfaceHostProtocolAnalysisResponse | null>(null);
|
||||
const [pathData, setPathData] = useState<InterfaceProtocolPathAnalysisResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetchInterfaceHostProtocolAnalysis(sinceMinutes, limitPerInterface, limitProtocolsPerHost);
|
||||
setData(response);
|
||||
const [hostResponse, pathResponse] = await Promise.all([
|
||||
fetchInterfaceHostProtocolAnalysis(sinceMinutes, limitPerInterface, limitProtocolsPerHost),
|
||||
fetchInterfaceProtocolPathAnalysis(sinceMinutes, limitPaths),
|
||||
]);
|
||||
setData(hostResponse);
|
||||
setPathData(pathResponse);
|
||||
} catch (error: any) {
|
||||
message.error(error?.message ?? 'Failed to load analysis data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [sinceMinutes, limitPerInterface, limitProtocolsPerHost]);
|
||||
}, [sinceMinutes, limitPerInterface, limitProtocolsPerHost, limitPaths]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData().catch(() => undefined);
|
||||
@@ -714,6 +803,14 @@ export default function Analysis(): ReactElement {
|
||||
}),
|
||||
[data, includeEthernetLayer, includeIpLayer],
|
||||
);
|
||||
const sankeyData = useMemo(
|
||||
() =>
|
||||
buildDirectionalSankeyData(pathData?.paths ?? [], {
|
||||
includeEthernetLayer,
|
||||
includeIpLayer,
|
||||
}),
|
||||
[pathData, includeEthernetLayer, includeIpLayer],
|
||||
);
|
||||
|
||||
const columns = useMemo<ColumnsType<ProtocolTableRow>>(
|
||||
() => [
|
||||
@@ -792,6 +889,10 @@ export default function Analysis(): ReactElement {
|
||||
<Text>Max protocols per host</Text>
|
||||
<InputNumber min={1} max={100} value={limitProtocolsPerHost} onChange={(value) => setLimitProtocolsPerHost(value ?? 12)} />
|
||||
</Space>
|
||||
<Space>
|
||||
<Text>Max Sankey paths</Text>
|
||||
<InputNumber min={1} max={5000} value={limitPaths} onChange={(value) => setLimitPaths(value ?? 500)} />
|
||||
</Space>
|
||||
<Checkbox checked={includeEthernetLayer} onChange={(event) => setIncludeEthernetLayer(event.target.checked)}>
|
||||
Ethernet layer
|
||||
</Checkbox>
|
||||
@@ -834,9 +935,9 @@ export default function Analysis(): ReactElement {
|
||||
children: (
|
||||
<div>
|
||||
<Paragraph type="secondary">
|
||||
Best for understanding how traffic flows from MITM interfaces to inferred hosts and then into protocols.
|
||||
Shows grouped packet paths as ingress interface to source endpoint to protocol to destination endpoint to egress interface.
|
||||
</Paragraph>
|
||||
<SankeyTopology data={topologyData} />
|
||||
<SankeyTopology data={sankeyData} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user