add more sankey options
This commit is contained in:
@@ -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.")
|
||||||
|
|
||||||
|
|||||||
@@ -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: (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user