test path visu
All checks were successful
Build and Deploy MITM Webserver / traffic_target (push) Successful in 1s
Build and Deploy MITM Webserver / build (push) Successful in 10s

This commit is contained in:
2026-03-30 23:21:36 +02:00
parent 5f6eedf859
commit cedc52eb8d
5 changed files with 345 additions and 8 deletions

View File

@@ -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>
),
},