add more sankey options
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:07:14 +02:00
parent 1a0d220f11
commit 5f6eedf859
4 changed files with 239 additions and 42 deletions

View File

@@ -20,6 +20,17 @@ class InterfaceHostEvidence(BaseModel):
destination_on_egress_count: int = Field(..., description="Packets where this endpoint appeared as the destination on egress.") destination_on_egress_count: int = Field(..., description="Packets where this endpoint appeared as the destination on egress.")
class ProtocolLayerPathEvidence(BaseModel):
ethernet_protocol: Optional[str] = Field(None, description="Ethernet protocol label for this path, if known.")
ip_protocol: Optional[str] = Field(None, description="IP protocol label for this path, if known.")
packet_count: int = Field(..., description="Packet observations supporting this path.")
last_seen: datetime = Field(..., description="Most recent packet timestamp supporting this path.")
accept_count: int = Field(0, description="Packets with verdict=accept for this path.")
drop_count: int = Field(0, description="Packets with verdict=drop for this path.")
reject_count: int = Field(0, description="Packets with verdict=reject for this path.")
unknown_count: int = Field(0, description="Packets with verdict pending/unknown or without a verdict.")
class ProtocolEvidence(BaseModel): class ProtocolEvidence(BaseModel):
protocol: str = Field(..., description="Detected application or fallback transport/network protocol.") protocol: str = Field(..., description="Detected application or fallback transport/network protocol.")
packet_count: int = Field(..., description="Packet observations supporting this interface-host-protocol mapping.") packet_count: int = Field(..., description="Packet observations supporting this interface-host-protocol mapping.")
@@ -30,23 +41,12 @@ class ProtocolEvidence(BaseModel):
unknown_count: int = Field(0, description="Packets with verdict pending/unknown or without a verdict.") unknown_count: int = Field(0, description="Packets with verdict pending/unknown or without a verdict.")
ethernet_protocol: Optional[str] = Field(None, description="Dominant Ethernet protocol associated with this protocol evidence.") ethernet_protocol: Optional[str] = Field(None, description="Dominant Ethernet protocol associated with this protocol evidence.")
ip_protocol: Optional[str] = Field(None, description="Dominant IP protocol associated with this protocol evidence.") ip_protocol: Optional[str] = Field(None, description="Dominant IP protocol associated with this protocol evidence.")
layer_paths: List["ProtocolLayerPathEvidence"] = Field( layer_paths: List[ProtocolLayerPathEvidence] = Field(
default_factory=list, default_factory=list,
description="Optional Ethernet/IP breakdown contributing to this protocol evidence.", description="Optional Ethernet/IP breakdown contributing to this protocol evidence.",
) )
class ProtocolLayerPathEvidence(BaseModel):
ethernet_protocol: Optional[str] = Field(None, description="Ethernet protocol label for this path, if known.")
ip_protocol: Optional[str] = Field(None, description="IP protocol label for this path, if known.")
packet_count: int = Field(..., description="Packet observations supporting this path.")
last_seen: datetime = Field(..., description="Most recent packet timestamp supporting this path.")
accept_count: int = Field(0, description="Packets with verdict=accept for this path.")
drop_count: int = Field(0, description="Packets with verdict=drop for this path.")
reject_count: int = Field(0, description="Packets with verdict=reject for this path.")
unknown_count: int = Field(0, description="Packets with verdict pending/unknown or without a verdict.")
class InterfaceHostProtocolEvidence(InterfaceHostEvidence): class InterfaceHostProtocolEvidence(InterfaceHostEvidence):
protocols: List[ProtocolEvidence] = Field(default_factory=list, description="Protocols observed for this host on the interface.") protocols: List[ProtocolEvidence] = Field(default_factory=list, description="Protocols observed for this host on the interface.")

View File

@@ -32,19 +32,35 @@ def _analysis_protocol_name(app_protocol: Any, ip_proto_raw: Any, eth_type_raw:
if app_protocol not in (None, ""): if app_protocol not in (None, ""):
return str(app_protocol) return str(app_protocol)
ip_protocol_name = _analysis_ip_protocol_name(ip_proto_raw)
if ip_protocol_name is not None:
return ip_protocol_name
ethernet_protocol_name = _analysis_ethernet_protocol_name(eth_type_raw)
if ethernet_protocol_name is not None:
return ethernet_protocol_name
return "UNKNOWN"
def _analysis_ip_protocol_name(ip_proto_raw: Any) -> Optional[str]:
if ip_proto_raw is not None: if ip_proto_raw is not None:
try: try:
return str(protocol_from_number(int(ip_proto_raw))) return str(protocol_from_number(int(ip_proto_raw)))
except Exception: except Exception:
return f"IP_PROTO_{ip_proto_raw}" return f"IP_PROTO_{ip_proto_raw}"
return None
def _analysis_ethernet_protocol_name(eth_type_raw: Any) -> Optional[str]:
if eth_type_raw is not None: if eth_type_raw is not None:
try: try:
return str(ethertype_from_int(int(eth_type_raw))) return str(ethertype_from_int(int(eth_type_raw)))
except Exception: except Exception:
return f"ETH_TYPE_{eth_type_raw}" return f"ETH_TYPE_{eth_type_raw}"
return "UNKNOWN" return None
def _serialize_row_for_broadcast(row: Dict[str, Any]) -> Dict[str, Any]: def _serialize_row_for_broadcast(row: Dict[str, Any]) -> Dict[str, Any]:
@@ -958,6 +974,8 @@ class DatabasePool:
) )
if protocol_name not in (None, ""): if protocol_name not in (None, ""):
protocol_key = str(protocol_name) protocol_key = str(protocol_name)
ip_protocol_name = _analysis_ip_protocol_name(record.get("ip_proto_raw"))
ethernet_protocol_name = _analysis_ethernet_protocol_name(record.get("eth_type_raw"))
protocol_last_seen_raw = record.get("protocol_last_seen") protocol_last_seen_raw = record.get("protocol_last_seen")
protocol_last_seen = ( protocol_last_seen = (
protocol_last_seen_raw.isoformat() protocol_last_seen_raw.isoformat()
@@ -975,6 +993,10 @@ class DatabasePool:
"drop_count": 0, "drop_count": 0,
"reject_count": 0, "reject_count": 0,
"unknown_count": 0, "unknown_count": 0,
"ethernet_protocol": ethernet_protocol_name,
"ip_protocol": ip_protocol_name,
"layer_paths": [],
"_layer_index": {},
} }
protocol_index[protocol_key] = protocol_record protocol_index[protocol_key] = protocol_record
host_record["protocols"].append(protocol_record) host_record["protocols"].append(protocol_record)
@@ -989,11 +1011,54 @@ class DatabasePool:
or str(protocol_last_seen) > str(protocol_record.get("last_seen")) or str(protocol_last_seen) > str(protocol_record.get("last_seen"))
): ):
protocol_record["last_seen"] = protocol_last_seen protocol_record["last_seen"] = protocol_last_seen
if protocol_record.get("ethernet_protocol") is None and ethernet_protocol_name is not None:
protocol_record["ethernet_protocol"] = ethernet_protocol_name
if protocol_record.get("ip_protocol") is None and ip_protocol_name is not None:
protocol_record["ip_protocol"] = ip_protocol_name
layer_key = f"{ethernet_protocol_name or 'no-eth'}|{ip_protocol_name or 'no-ip'}"
layer_index = protocol_record["_layer_index"]
layer_record = layer_index.get(layer_key)
if layer_record is None:
layer_record = {
"ethernet_protocol": ethernet_protocol_name,
"ip_protocol": ip_protocol_name,
"packet_count": 0,
"last_seen": protocol_last_seen,
"accept_count": 0,
"drop_count": 0,
"reject_count": 0,
"unknown_count": 0,
}
layer_index[layer_key] = layer_record
protocol_record["layer_paths"].append(layer_record)
layer_record["packet_count"] += int(record.get("protocol_packet_count") or 0)
layer_record["accept_count"] += int(record.get("accept_count") or 0)
layer_record["drop_count"] += int(record.get("drop_count") or 0)
layer_record["reject_count"] += int(record.get("reject_count") or 0)
layer_record["unknown_count"] += int(record.get("unknown_count") or 0)
if protocol_last_seen and (
layer_record.get("last_seen") in (None, "")
or str(protocol_last_seen) > str(layer_record.get("last_seen"))
):
layer_record["last_seen"] = protocol_last_seen
result: List[Dict[str, Any]] = [] result: List[Dict[str, Any]] = []
for iface, hosts in sorted(grouped.items()): for iface, hosts in sorted(grouped.items()):
for host in hosts.values(): for host in hosts.values():
host.pop("_protocol_index", None) host.pop("_protocol_index", None)
for protocol in host["protocols"]:
protocol.pop("_layer_index", None)
protocol["layer_paths"] = sorted(
protocol["layer_paths"],
key=lambda item: (
-int(item.get("packet_count") or 0),
str(item.get("last_seen") or ""),
str(item.get("ethernet_protocol") or ""),
str(item.get("ip_protocol") or ""),
),
)
host["protocols"] = sorted( host["protocols"] = sorted(
host["protocols"], host["protocols"],
key=lambda item: ( key=lambda item: (

View File

@@ -3,6 +3,7 @@ import {
Alert, Alert,
Button, Button,
Card, Card,
Checkbox,
Col, Col,
Empty, Empty,
InputNumber, InputNumber,
@@ -31,11 +32,12 @@ import type {
const { Title, Text, Paragraph } = Typography; const { Title, Text, Paragraph } = Typography;
type GraphNodeKind = 'interface' | 'host' | 'protocol'; type GraphNodeKind = 'interface' | 'host' | 'protocol';
type TopologyLayerKind = GraphNodeKind | 'ethernet' | 'ip';
type TopologyNode = { type TopologyNode = {
id: string; id: string;
label: string; label: string;
kind: GraphNodeKind; kind: TopologyLayerKind;
packetCount: number; packetCount: number;
interfaceName?: string; interfaceName?: string;
ipAddress?: string | null; ipAddress?: string | null;
@@ -81,6 +83,11 @@ type TopologyData = {
tableRows: ProtocolTableRow[]; tableRows: ProtocolTableRow[];
}; };
type TopologyOptions = {
includeEthernetLayer: boolean;
includeIpLayer: boolean;
};
type ForceNode = d3.SimulationNodeDatum & TopologyNode; type ForceNode = d3.SimulationNodeDatum & TopologyNode;
type ForceLink = d3.SimulationLinkDatum<ForceNode> & TopologyLink; type ForceLink = d3.SimulationLinkDatum<ForceNode> & TopologyLink;
@@ -133,7 +140,38 @@ function sankeyVisualWeight(packetCount: number) {
return Math.max(1, Math.sqrt(Math.max(0, packetCount))); return Math.max(1, Math.sqrt(Math.max(0, packetCount)));
} }
function buildTopologyData(interfaces: InterfaceProtocolAttachment[]): TopologyData { function addOrUpdateLink(links: Map<string, TopologyLink>, source: string, target: string, packetCount: number, label: string) {
const linkId = `${source}->${target}`;
const existing = links.get(linkId);
if (existing) {
existing.packetCount += packetCount;
existing.value = sankeyVisualWeight(existing.packetCount);
existing.label = `${existing.label.split(' (')[0]} (${existing.packetCount})`;
return;
}
links.set(linkId, {
source,
target,
value: sankeyVisualWeight(packetCount),
packetCount,
label: `${label} (${packetCount})`,
});
}
function ensureProtocolNode(nodes: Map<string, TopologyNode>, id: string, label: string, kind: TopologyLayerKind) {
if (!nodes.has(id)) {
nodes.set(id, {
id,
label,
kind,
packetCount: 0,
protocol: label,
});
}
return nodes.get(id)!;
}
function buildTopologyData(interfaces: InterfaceProtocolAttachment[], options: TopologyOptions): TopologyData {
const nodes = new Map<string, TopologyNode>(); const nodes = new Map<string, TopologyNode>();
const links = new Map<string, TopologyLink>(); const links = new Map<string, TopologyLink>();
const heatmapByHost = new Map<string, HeatmapRow>(); const heatmapByHost = new Map<string, HeatmapRow>();
@@ -179,28 +217,74 @@ function buildTopologyData(interfaces: InterfaceProtocolAttachment[]): TopologyD
}; };
for (const protocol of host.protocols) { for (const protocol of host.protocols) {
const protocolId = `protocol:${protocol.protocol}`;
protocols.add(protocol.protocol); protocols.add(protocol.protocol);
if (!nodes.has(protocolId)) { const layerPaths =
nodes.set(protocolId, { protocol.layer_paths.length > 0
id: protocolId, ? protocol.layer_paths
label: protocol.protocol, : [
kind: 'protocol', {
packetCount: 0, ethernet_protocol: protocol.ethernet_protocol ?? null,
protocol: protocol.protocol, ip_protocol: protocol.ip_protocol ?? null,
}); packet_count: protocol.packet_count,
} last_seen: protocol.last_seen,
const existingProtocolNode = nodes.get(protocolId)!; accept_count: protocol.accept_count,
existingProtocolNode.packetCount += protocol.packet_count; drop_count: protocol.drop_count,
reject_count: protocol.reject_count,
unknown_count: protocol.unknown_count,
},
];
const hostProtocolLinkId = `${hostId}->${protocolId}`; for (const layerPath of layerPaths) {
links.set(hostProtocolLinkId, { let currentNodeId = hostId;
source: hostId, let currentLabel = host.ip_address ?? host.mac_address ?? 'host';
target: protocolId,
value: sankeyVisualWeight(protocol.packet_count), if (
packetCount: protocol.packet_count, options.includeEthernetLayer &&
label: `${host.ip_address ?? host.mac_address ?? 'host'} -> ${protocol.protocol} (${protocol.packet_count})`, layerPath.ethernet_protocol &&
}); layerPath.ethernet_protocol !== protocol.protocol
) {
const ethernetId = `ethernet:${layerPath.ethernet_protocol}`;
const ethernetNode = ensureProtocolNode(nodes, ethernetId, layerPath.ethernet_protocol, 'ethernet');
ethernetNode.packetCount += layerPath.packet_count;
addOrUpdateLink(
links,
currentNodeId,
ethernetId,
layerPath.packet_count,
`${currentLabel} -> ${layerPath.ethernet_protocol}`,
);
currentNodeId = ethernetId;
currentLabel = layerPath.ethernet_protocol;
}
if (options.includeIpLayer && layerPath.ip_protocol && layerPath.ip_protocol !== currentLabel && layerPath.ip_protocol !== protocol.protocol) {
const ipId = `ip:${layerPath.ip_protocol}`;
const ipNode = ensureProtocolNode(nodes, ipId, layerPath.ip_protocol, 'ip');
ipNode.packetCount += layerPath.packet_count;
addOrUpdateLink(
links,
currentNodeId,
ipId,
layerPath.packet_count,
`${currentLabel} -> ${layerPath.ip_protocol}`,
);
currentNodeId = ipId;
currentLabel = layerPath.ip_protocol;
}
if (currentLabel !== protocol.protocol || currentNodeId === hostId) {
const protocolId = `protocol:${protocol.protocol}`;
const protocolNode = ensureProtocolNode(nodes, protocolId, protocol.protocol, 'protocol');
protocolNode.packetCount += layerPath.packet_count;
addOrUpdateLink(
links,
currentNodeId,
protocolId,
layerPath.packet_count,
`${currentLabel} -> ${protocol.protocol}`,
);
}
}
heatmapRow.values[protocol.protocol] = protocol.packet_count; heatmapRow.values[protocol.protocol] = protocol.packet_count;
@@ -243,9 +327,11 @@ function SankeyTopology({ data }: { data: TopologyData }) {
const nodesPerLayer = { const nodesPerLayer = {
interface: data.nodes.filter((node) => node.kind === 'interface').length, interface: data.nodes.filter((node) => node.kind === 'interface').length,
host: data.nodes.filter((node) => node.kind === 'host').length, host: data.nodes.filter((node) => node.kind === 'host').length,
ethernet: data.nodes.filter((node) => node.kind === 'ethernet').length,
ip: data.nodes.filter((node) => node.kind === 'ip').length,
protocol: data.nodes.filter((node) => node.kind === 'protocol').length, protocol: data.nodes.filter((node) => node.kind === 'protocol').length,
}; };
const maxNodesInLayer = Math.max(nodesPerLayer.interface, nodesPerLayer.host, nodesPerLayer.protocol, 1); const maxNodesInLayer = Math.max(nodesPerLayer.interface, nodesPerLayer.host, nodesPerLayer.ethernet, nodesPerLayer.ip, nodesPerLayer.protocol, 1);
const height = clamp(maxNodesInLayer * 56 + 120, 260, 760); const height = clamp(maxNodesInLayer * 56 + 120, 260, 760);
const svg = d3.select(svgRef.current); const svg = d3.select(svgRef.current);
svg.selectAll('*').remove(); svg.selectAll('*').remove();
@@ -279,7 +365,7 @@ function SankeyTopology({ data }: { data: TopologyData }) {
.attr('d', sankeyLinkHorizontal()) .attr('d', sankeyLinkHorizontal())
.attr('stroke', (link) => { .attr('stroke', (link) => {
const target = link.target as SankeyNodeDatum; const target = link.target as SankeyNodeDatum;
if (target.kind === 'protocol' && target.protocol) { if ((target.kind === 'protocol' || target.kind === 'ethernet' || target.kind === 'ip') && target.protocol) {
return protocolColor(target.protocol); return protocolColor(target.protocol);
} }
return '#9aa7b5'; return '#9aa7b5';
@@ -303,6 +389,8 @@ function SankeyTopology({ data }: { data: TopologyData }) {
.attr('fill', (d) => { .attr('fill', (d) => {
if (d.kind === 'interface') return '#20405d'; if (d.kind === 'interface') return '#20405d';
if (d.kind === 'host') return '#d7e7f5'; if (d.kind === 'host') return '#d7e7f5';
if (d.kind === 'ethernet') return '#d7c09c';
if (d.kind === 'ip') return '#a8c8df';
return d.protocol ? protocolColor(d.protocol) : '#d8d8d8'; return d.protocol ? protocolColor(d.protocol) : '#d8d8d8';
}) })
.attr('stroke', (d) => (d.kind === 'host' ? '#9bb8d6' : 'none')) .attr('stroke', (d) => (d.kind === 'host' ? '#9bb8d6' : 'none'))
@@ -353,6 +441,8 @@ function ForceTopology({ data }: { data: TopologyData }) {
const groupedNodes = { const groupedNodes = {
interface: nodes.filter((node) => node.kind === 'interface').sort((left, right) => left.label.localeCompare(right.label)), interface: nodes.filter((node) => node.kind === 'interface').sort((left, right) => left.label.localeCompare(right.label)),
host: nodes.filter((node) => node.kind === 'host').sort((left, right) => (left.ipAddress ?? left.macAddress ?? left.label).localeCompare(right.ipAddress ?? right.macAddress ?? right.label)), host: nodes.filter((node) => node.kind === 'host').sort((left, right) => (left.ipAddress ?? left.macAddress ?? left.label).localeCompare(right.ipAddress ?? right.macAddress ?? right.label)),
ethernet: nodes.filter((node) => node.kind === 'ethernet').sort((left, right) => left.label.localeCompare(right.label)),
ip: nodes.filter((node) => node.kind === 'ip').sort((left, right) => left.label.localeCompare(right.label)),
protocol: nodes.filter((node) => node.kind === 'protocol').sort((left, right) => left.label.localeCompare(right.label)), protocol: nodes.filter((node) => node.kind === 'protocol').sort((left, right) => left.label.localeCompare(right.label)),
}; };
@@ -374,8 +464,11 @@ function ForceTopology({ data }: { data: TopologyData }) {
const interfaceY = distributedY(groupedNodes.interface, 120, height - 120); const interfaceY = distributedY(groupedNodes.interface, 120, height - 120);
const hostY = distributedY(groupedNodes.host, 90, height - 90); const hostY = distributedY(groupedNodes.host, 90, height - 90);
const ethernetY = distributedY(groupedNodes.ethernet, 120, height - 120);
const ipY = distributedY(groupedNodes.ip, 120, height - 120);
const protocolY = distributedY(groupedNodes.protocol, 120, height - 120); const protocolY = distributedY(groupedNodes.protocol, 120, height - 120);
const targetY = (node: ForceNode) => interfaceY.get(node.id) ?? hostY.get(node.id) ?? protocolY.get(node.id) ?? height / 2; const targetY = (node: ForceNode) =>
interfaceY.get(node.id) ?? hostY.get(node.id) ?? ethernetY.get(node.id) ?? ipY.get(node.id) ?? protocolY.get(node.id) ?? height / 2;
const simulation = d3 const simulation = d3
.forceSimulation<ForceNode>(nodes) .forceSimulation<ForceNode>(nodes)
@@ -397,13 +490,17 @@ function ForceTopology({ data }: { data: TopologyData }) {
.force('collision', d3.forceCollide<ForceNode>().radius((node) => { .force('collision', d3.forceCollide<ForceNode>().radius((node) => {
if (node.kind === 'interface') return 52; if (node.kind === 'interface') return 52;
if (node.kind === 'host') return 44; if (node.kind === 'host') return 44;
if (node.kind === 'ethernet') return 36;
if (node.kind === 'ip') return 35;
return 34; return 34;
})) }))
.force( .force(
'x', 'x',
d3.forceX<ForceNode>().x((node) => { d3.forceX<ForceNode>().x((node) => {
if (node.kind === 'interface') return 180; if (node.kind === 'interface') return 180;
if (node.kind === 'host') return width / 2; if (node.kind === 'host') return 420;
if (node.kind === 'ethernet') return 690;
if (node.kind === 'ip') return 930;
return width - 200; return width - 200;
}).strength(0.42), }).strength(0.42),
) )
@@ -420,11 +517,13 @@ function ForceTopology({ data }: { data: TopologyData }) {
.join('line') .join('line')
.attr('stroke', (d) => { .attr('stroke', (d) => {
const target = d.target as ForceNode; const target = d.target as ForceNode;
return target.kind === 'protocol' && target.protocol ? protocolColor(target.protocol) : '#92a1b2'; return (target.kind === 'protocol' || target.kind === 'ethernet' || target.kind === 'ip') && target.protocol
? protocolColor(target.protocol)
: '#92a1b2';
}) })
.attr('stroke-width', (d) => Math.max(1.5, Math.sqrt(d.value))); .attr('stroke-width', (d) => Math.max(1.5, Math.sqrt(d.value)));
link.append('title').text((d) => `${d.label}\nPackets: ${d.value}`); link.append('title').text((d) => `${d.label}\nPackets: ${d.packetCount}`);
const node = svg const node = svg
.append('g') .append('g')
@@ -437,11 +536,15 @@ function ForceTopology({ data }: { data: TopologyData }) {
.attr('r', (d) => { .attr('r', (d) => {
if (d.kind === 'interface') return 26; if (d.kind === 'interface') return 26;
if (d.kind === 'host') return 22; if (d.kind === 'host') return 22;
if (d.kind === 'ethernet') return 19;
if (d.kind === 'ip') return 18;
return 18; return 18;
}) })
.attr('fill', (d) => { .attr('fill', (d) => {
if (d.kind === 'interface') return '#20405d'; if (d.kind === 'interface') return '#20405d';
if (d.kind === 'host') return '#d7e7f5'; if (d.kind === 'host') return '#d7e7f5';
if (d.kind === 'ethernet') return '#d7c09c';
if (d.kind === 'ip') return '#a8c8df';
return d.protocol ? protocolColor(d.protocol) : '#cfd7df'; return d.protocol ? protocolColor(d.protocol) : '#cfd7df';
}) })
.attr('stroke', (d) => (d.kind === 'host' ? '#8aa8c6' : '#ffffff')) .attr('stroke', (d) => (d.kind === 'host' ? '#8aa8c6' : '#ffffff'))
@@ -582,6 +685,8 @@ export default function Analysis(): ReactElement {
const [sinceMinutes, setSinceMinutes] = useState<number | null>(null); const [sinceMinutes, setSinceMinutes] = useState<number | null>(null);
const [limitPerInterface, setLimitPerInterface] = useState(50); const [limitPerInterface, setLimitPerInterface] = useState(50);
const [limitProtocolsPerHost, setLimitProtocolsPerHost] = useState(12); const [limitProtocolsPerHost, setLimitProtocolsPerHost] = useState(12);
const [includeEthernetLayer, setIncludeEthernetLayer] = useState(false);
const [includeIpLayer, setIncludeIpLayer] = useState(false);
const [data, setData] = useState<InterfaceHostProtocolAnalysisResponse | null>(null); const [data, setData] = useState<InterfaceHostProtocolAnalysisResponse | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -601,7 +706,14 @@ export default function Analysis(): ReactElement {
loadData().catch(() => undefined); loadData().catch(() => undefined);
}, [loadData]); }, [loadData]);
const topologyData = useMemo(() => buildTopologyData(data?.interfaces ?? []), [data]); const topologyData = useMemo(
() =>
buildTopologyData(data?.interfaces ?? [], {
includeEthernetLayer,
includeIpLayer,
}),
[data, includeEthernetLayer, includeIpLayer],
);
const columns = useMemo<ColumnsType<ProtocolTableRow>>( const columns = useMemo<ColumnsType<ProtocolTableRow>>(
() => [ () => [
@@ -680,6 +792,12 @@ export default function Analysis(): ReactElement {
<Text>Max protocols per host</Text> <Text>Max protocols per host</Text>
<InputNumber min={1} max={100} value={limitProtocolsPerHost} onChange={(value) => setLimitProtocolsPerHost(value ?? 12)} /> <InputNumber min={1} max={100} value={limitProtocolsPerHost} onChange={(value) => setLimitProtocolsPerHost(value ?? 12)} />
</Space> </Space>
<Checkbox checked={includeEthernetLayer} onChange={(event) => setIncludeEthernetLayer(event.target.checked)}>
Ethernet layer
</Checkbox>
<Checkbox checked={includeIpLayer} onChange={(event) => setIncludeIpLayer(event.target.checked)}>
IP layer
</Checkbox>
<Button icon={<ReloadOutlined />} onClick={() => loadData()} loading={loading} type="primary"> <Button icon={<ReloadOutlined />} onClick={() => loadData()} loading={loading} type="primary">
Refresh Refresh
</Button> </Button>

View File

@@ -26,6 +26,20 @@ export interface ProtocolEvidence {
drop_count: number; drop_count: number;
reject_count: number; reject_count: number;
unknown_count: number; unknown_count: number;
ethernet_protocol?: string | null;
ip_protocol?: string | null;
layer_paths: ProtocolLayerPathEvidence[];
}
export interface ProtocolLayerPathEvidence {
ethernet_protocol?: string | null;
ip_protocol?: string | null;
packet_count: number;
last_seen: string;
accept_count: number;
drop_count: number;
reject_count: number;
unknown_count: number;
} }
export interface InterfaceHostProtocolEvidence extends InterfaceHostEvidence { export interface InterfaceHostProtocolEvidence extends InterfaceHostEvidence {