flow showing
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-09 12:54:57 +01:00
parent a111d5ea00
commit a49ee5b9bd
4 changed files with 166 additions and 36 deletions

View File

@@ -80,6 +80,7 @@ class PacketDBModel(BaseModel):
length: Optional[int] = None length: Optional[int] = None
raw_present: Optional[bool] = Field(None, description="Whether raw packet bytes were captured for this row.") raw_present: Optional[bool] = Field(None, description="Whether raw packet bytes were captured for this row.")
capture_sources: Optional[list[str]] = Field(None, description="Capture sources that contributed to this row.") capture_sources: Optional[list[str]] = Field(None, description="Capture sources that contributed to this row.")
flow_id: Optional[str] = Field(None, description="Derived flow identifier from tshark stream metadata, if available.")
raw_b64: Optional[str] = Field(None, description="Base64-encoded packet bytes.") raw_b64: Optional[str] = Field(None, description="Base64-encoded packet bytes.")
app_protocol: Optional[str] = Field(None, description="Detected application protocol.") app_protocol: Optional[str] = Field(None, description="Detected application protocol.")
app_master_protocol: Optional[str] = Field(None, description="Detected application master protocol.") app_master_protocol: Optional[str] = Field(None, description="Detected application master protocol.")

View File

@@ -29,6 +29,7 @@ def _db_text(value: Any) -> Any:
def _serialize_row_for_broadcast(row: Dict[str, Any]) -> Dict[str, Any]: def _serialize_row_for_broadcast(row: Dict[str, Any]) -> Dict[str, Any]:
serialized = dict(row) serialized = dict(row)
_normalize_json_fields(serialized) _normalize_json_fields(serialized)
_attach_derived_fields(serialized)
raw_val = serialized.get("raw") raw_val = serialized.get("raw")
if isinstance(raw_val, (bytes, bytearray)): if isinstance(raw_val, (bytes, bytearray)):
@@ -54,6 +55,42 @@ def _normalize_json_fields(payload: Dict[str, Any]) -> None:
payload[key] = parsed payload[key] = parsed
def _derive_flow_id(payload: Dict[str, Any]) -> Optional[str]:
dpi_metadata = payload.get("dpi_metadata")
if not isinstance(dpi_metadata, dict):
return None
tcp_meta = dpi_metadata.get("tcp")
if isinstance(tcp_meta, dict):
stream = tcp_meta.get("stream")
if stream not in (None, "", []):
return f"tcp:{stream}"
udp_meta = dpi_metadata.get("udp")
if isinstance(udp_meta, dict):
stream = udp_meta.get("stream")
if stream not in (None, "", []):
return f"udp:{stream}"
tshark_meta = dpi_metadata.get("tshark")
if isinstance(tshark_meta, dict):
tcp_stream = tshark_meta.get("tcp_stream")
if tcp_stream not in (None, "", []):
return f"tcp:{tcp_stream}"
udp_stream = tshark_meta.get("udp_stream")
if udp_stream not in (None, "", []):
return f"udp:{udp_stream}"
return None
def _attach_derived_fields(payload: Dict[str, Any]) -> None:
if payload.get("flow_id") in (None, ""):
flow_id = _derive_flow_id(payload)
if flow_id is not None:
payload["flow_id"] = flow_id
class DatabasePool: class DatabasePool:
"""Asyncpg connection pool wrapper used by the packet APIs.""" """Asyncpg connection pool wrapper used by the packet APIs."""
@@ -509,7 +546,7 @@ class DatabasePool:
""" """
SELECT * SELECT *
FROM packets FROM packets
ORDER BY updated_at DESC, id DESC ORDER BY timestamp DESC, id DESC
LIMIT $1 LIMIT $1
""", """,
limit, limit,
@@ -519,6 +556,7 @@ class DatabasePool:
for row in rows: for row in rows:
data = dict(row) data = dict(row)
_normalize_json_fields(data) _normalize_json_fields(data)
_attach_derived_fields(data)
raw_val = data.get("raw") raw_val = data.get("raw")
if isinstance(raw_val, (bytes, bytearray)): if isinstance(raw_val, (bytes, bytearray)):

View File

@@ -26,6 +26,13 @@ const { Option } = Select;
const DEFAULT_LIMIT = 200; const DEFAULT_LIMIT = 200;
const MAX_PACKETS = 2000; // in-memory cap const MAX_PACKETS = 2000; // in-memory cap
type PacketTableRow = PacketRow & {
key: string;
__kind: 'packet' | 'flow';
flow_packet_count?: number;
children?: PacketTableRow[];
};
function base64ToHex(b64: string) { function base64ToHex(b64: string) {
try { try {
const bin = atob(b64); const bin = atob(b64);
@@ -97,6 +104,39 @@ function packetKey(packet: PacketRow) {
return String(packet.correlation_key ?? packet.packet_id ?? packet.packet_uid ?? packet.id ?? `${packet.ingress_if ?? 'if'}:${packet.timestamp ?? ''}`); return String(packet.correlation_key ?? packet.packet_id ?? packet.packet_uid ?? packet.id ?? `${packet.ingress_if ?? 'if'}:${packet.timestamp ?? ''}`);
} }
function packetTimestampMs(packet: PacketRow) {
const time = packet.timestamp ? new Date(packet.timestamp).getTime() : 0;
return Number.isNaN(time) ? 0 : time;
}
function sortPacketsByTimestampDesc(left: PacketRow, right: PacketRow) {
return packetTimestampMs(right) - packetTimestampMs(left);
}
function sortPacketsByTimestampAsc(left: PacketRow, right: PacketRow) {
return packetTimestampMs(left) - packetTimestampMs(right);
}
function getDpiDict(packet: PacketRow, key: string): Record<string, unknown> | null {
const dpi = packet.dpi_metadata;
if (!dpi || typeof dpi !== 'object' || Array.isArray(dpi)) return null;
const value = dpi[key];
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function getFlowId(packet: PacketRow) {
if (packet.flow_id) return String(packet.flow_id);
const tcp = getDpiDict(packet, 'tcp');
if (tcp?.stream != null) return `tcp:${String(tcp.stream)}`;
const udp = getDpiDict(packet, 'udp');
if (udp?.stream != null) return `udp:${String(udp.stream)}`;
const tshark = getDpiDict(packet, 'tshark');
if (tshark?.tcp_stream != null) return `tcp:${String(tshark.tcp_stream)}`;
if (tshark?.udp_stream != null) return `udp:${String(tshark.udp_stream)}`;
return null;
}
function formatEthType(packet: PacketRow) { function formatEthType(packet: PacketRow) {
if (packet.eth_type) return String(packet.eth_type); if (packet.eth_type) return String(packet.eth_type);
if (typeof packet.eth_type_raw === 'number') return `0x${packet.eth_type_raw.toString(16)}`; if (typeof packet.eth_type_raw === 'number') return `0x${packet.eth_type_raw.toString(16)}`;
@@ -109,6 +149,14 @@ function formatIpProto(packet: PacketRow) {
return '-'; return '-';
} }
function formatProtocolLabel(packet: PacketRow) {
const base = formatIpProto(packet);
const tcp = getDpiDict(packet, 'tcp');
const tshark = getDpiDict(packet, 'tshark');
const packetType = tcp?.packet_type ?? tshark?.tcp_packet_type;
return packetType ? `${base} ${String(packetType)}` : base;
}
/** Format timestamp to European locale with ms */ /** Format timestamp to European locale with ms */
function formatTimestamp(ts?: string) { function formatTimestamp(ts?: string) {
if (!ts) return '-'; if (!ts) return '-';
@@ -163,12 +211,7 @@ export default function PacketViewer(): ReactElement {
} }
next.sort((left, right) => { next.sort((left, right) => {
const leftId = Number(left.id ?? 0); return sortPacketsByTimestampDesc(left, right);
const rightId = Number(right.id ?? 0);
if (!Number.isNaN(leftId) && !Number.isNaN(rightId) && leftId !== rightId) {
return rightId - leftId;
}
return String(right.timestamp ?? '').localeCompare(String(left.timestamp ?? ''));
}); });
if (next.length > MAX_PACKETS) return next.slice(0, MAX_PACKETS); if (next.length > MAX_PACKETS) return next.slice(0, MAX_PACKETS);
@@ -182,7 +225,7 @@ export default function PacketViewer(): ReactElement {
try { try {
const res = await fetchPackets(limitVal); const res = await fetchPackets(limitVal);
const list: PacketRow[] = res.packets ?? []; const list: PacketRow[] = res.packets ?? [];
setPackets(list); setPackets([...list].sort(sortPacketsByTimestampDesc));
} catch (err: any) { } catch (err: any) {
console.error('fetchHistory error', err); console.error('fetchHistory error', err);
message.error(err?.message ?? 'Failed to fetch packet history'); message.error(err?.message ?? 'Failed to fetch packet history');
@@ -304,6 +347,57 @@ export default function PacketViewer(): ReactElement {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
const tableRows = useMemo<PacketTableRow[]>(() => {
const grouped = new Map<string, PacketRow[]>();
const standalone: PacketTableRow[] = [];
for (const packet of packets) {
const flowId = getFlowId(packet);
if (!flowId) {
standalone.push({ ...packet, key: packetKey(packet), __kind: 'packet' });
continue;
}
const items = grouped.get(flowId);
if (items) {
items.push(packet);
} else {
grouped.set(flowId, [packet]);
}
}
const flowRows: PacketTableRow[] = [];
for (const [flowId, flowPackets] of grouped.entries()) {
const sortedPackets = [...flowPackets].sort(sortPacketsByTimestampAsc);
if (sortedPackets.length === 1) {
flowRows.push({
...sortedPackets[0],
flow_id: flowId,
key: packetKey(sortedPackets[0]),
__kind: 'packet',
});
continue;
}
const latestPacket = [...flowPackets].sort(sortPacketsByTimestampDesc)[0];
const children = sortedPackets.map((packet) => ({
...packet,
flow_id: flowId,
key: packetKey(packet),
__kind: 'packet' as const,
}));
flowRows.push({
...latestPacket,
flow_id: flowId,
key: `flow:${flowId}`,
__kind: 'flow',
flow_packet_count: sortedPackets.length,
children,
});
}
return [...flowRows, ...standalone].sort(sortPacketsByTimestampDesc);
}, [packets]);
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
@@ -313,19 +407,6 @@ export default function PacketViewer(): ReactElement {
width: 120, width: 120,
render: (val: any) => <Text>{formatTimestamp(val)}</Text>, render: (val: any) => <Text>{formatTimestamp(val)}</Text>,
}, },
{
title: 'Corr',
key: 'correlation',
width: 140,
render: (_: any, rec: PacketRow) => (
<Space direction="vertical" size={0}>
<Text>{rec.packet_id ?? rec.packet_uid?.slice(0, 10) ?? '-'}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
{rec.correlation_source ?? '-'}
</Text>
</Space>
),
},
{ {
title: 'Path', title: 'Path',
key: 'path', key: 'path',
@@ -397,8 +478,14 @@ export default function PacketViewer(): ReactElement {
title: 'Protocol', title: 'Protocol',
dataIndex: 'ip_proto', dataIndex: 'ip_proto',
key: 'ip_proto', key: 'ip_proto',
width: 110, width: 130,
render: (_: any, rec: PacketRow) => <Text>{formatIpProto(rec)}</Text>, render: (_: any, rec: PacketTableRow) => <Text>{rec.__kind === 'flow' ? `${formatIpProto(rec)} flow` : formatProtocolLabel(rec)}</Text>,
},
{
title: 'Flow',
key: 'flow_id',
width: 130,
render: (_: any, rec: PacketTableRow) => <Text>{rec.flow_id ?? '-'}</Text>,
}, },
{ {
title: 'App', title: 'App',
@@ -452,24 +539,23 @@ export default function PacketViewer(): ReactElement {
), ),
}, },
{ {
title: 'Len', title: 'Len/Pkts',
dataIndex: 'length', dataIndex: 'length',
key: 'length', key: 'length',
width: 80, width: 90,
render: (n: any) => (typeof n === 'number' ? n.toLocaleString('de-DE') : '-'), render: (_: any, rec: PacketTableRow) =>
}, rec.__kind === 'flow'
{ ? `${rec.flow_packet_count ?? 0} pkts`
title: 'Risk', : typeof rec.length === 'number'
dataIndex: 'app_risk_score', ? rec.length.toLocaleString('de-DE')
key: 'app_risk_score', : '-',
width: 70,
render: (n: any) => (typeof n === 'number' ? n : '-'),
}, },
{ {
title: 'Actions', title: 'Actions',
key: 'actions', key: 'actions',
width: 96, width: 96,
render: (_: any, rec: PacketRow) => ( render: (_: any, rec: PacketTableRow) =>
rec.__kind === 'flow' ? null : (
<Space> <Space>
<Tooltip title="View raw + tshark"> <Tooltip title="View raw + tshark">
<Button <Button
@@ -551,12 +637,16 @@ export default function PacketViewer(): ReactElement {
</div> </div>
) : ( ) : (
<Table <Table
rowKey={(rec: PacketRow) => packetKey(rec)} rowKey={(rec: PacketTableRow) => rec.key}
dataSource={packets} dataSource={tableRows}
columns={columns as any} columns={columns as any}
pagination={{ pageSize: 25 }} pagination={{ pageSize: 25 }}
size="small" size="small"
bordered bordered
expandable={{
defaultExpandAllRows: true,
rowExpandable: (record: PacketTableRow) => Array.isArray(record.children) && record.children.length > 0,
}}
/> />
)} )}
</Card> </Card>

View File

@@ -24,6 +24,7 @@ export interface PacketRow {
length?: number | null; length?: number | null;
raw_present?: boolean | null; raw_present?: boolean | null;
capture_sources?: string[] | null; capture_sources?: string[] | null;
flow_id?: string | null;
raw_b64?: string | null; raw_b64?: string | null;
app_protocol?: string | null; app_protocol?: string | null;
app_master_protocol?: string | null; app_master_protocol?: string | null;