add visualizations, better scaling
All checks were successful
Build and Deploy MITM Webserver / traffic_target (push) Successful in 1s
Build and Deploy MITM Webserver / build (push) Successful in 11s

This commit is contained in:
2026-04-01 23:10:59 +02:00
parent 8f6d795e87
commit 74b961e152
7 changed files with 3494 additions and 2844 deletions

View File

@@ -4,8 +4,6 @@ import {
Button,
Card,
Col,
Descriptions,
Modal,
Row,
Segmented,
Select,
@@ -21,6 +19,7 @@ import {
} from 'antd';
import { ReactElement, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { clearPackets, fetchPackets, getPacketsWebSocketUrl } from '../api/apiClient';
import PacketInspectModal from './PacketInspectModal';
import type { PacketRow } from '../types/packets';
const { Text, Title } = Typography;
@@ -927,8 +926,6 @@ export default function PacketViewer(): ReactElement {
const [paused, setPaused] = useState<boolean>(false);
const wsRef = useRef<WebSocket | null>(null);
const [hexModalOpen, setHexModalOpen] = useState(false);
const [rawHex, setRawHex] = useState<string | null>(null);
const [rawB64ForDownload, setRawB64ForDownload] = useState<string | null>(null);
const [selectedPacket, setSelectedPacket] = useState<PacketRow | null>(null);
const queuedDuringPause = useRef<PacketRow[]>([]);
@@ -1070,27 +1067,10 @@ export default function PacketViewer(): ReactElement {
message.info('No inspect data available for this packet');
return;
}
setRawHex(row.raw_b64 ? base64ToHex(row.raw_b64) : '(no raw bytes available)');
setRawB64ForDownload(row.raw_b64 ?? null);
setSelectedPacket(row);
setHexModalOpen(true);
};
const downloadRaw = () => {
if (!rawB64ForDownload) return;
const blob = base64ToBlob(rawB64ForDownload);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `packet_${selectedPacket?.id ?? 'pkt'}.bin`;
a.click();
URL.revokeObjectURL(url);
};
const reducedMetadata = useMemo(() => buildReducedMetadata(selectedPacket), [selectedPacket]);
const fullPacketJson = useMemo(() => formatJson(selectedPacket), [selectedPacket]);
const decodedPayload = useMemo(() => getDecodedPayload(selectedPacket), [selectedPacket]);
const tableRows = useMemo<PacketTableRow[]>(() => {
const grouped = new Map<string, PacketRow[]>();
const standalone: PacketTableRow[] = [];
@@ -1489,113 +1469,14 @@ export default function PacketViewer(): ReactElement {
)}
</Card>
<Modal
title={`Inspect packet ${selectedPacket?.id ?? ''}`}
<PacketInspectModal
packet={selectedPacket}
open={hexModalOpen}
onCancel={() => {
onClose={() => {
setHexModalOpen(false);
setSelectedPacket(null);
}}
footer={
<Space>
<Button
onClick={() => {
setHexModalOpen(false);
}}
>
Close
</Button>
<Button icon={<DownloadOutlined />} onClick={downloadRaw} type="primary">
Download raw
</Button>
</Space>
}
width={1100}
>
<Tabs
items={[
{
key: 'summary',
label: 'Summary',
children: renderProtocolSummary(selectedPacket),
},
{
key: 'metadata',
label: 'Metadata',
children: (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Descriptions bordered size="small" column={2}>
<Descriptions.Item label="Timestamp">{formatTimestamp(selectedPacket?.timestamp)}</Descriptions.Item>
<Descriptions.Item label="Flow ID">{selectedPacket?.flow_id ?? getFlowId(selectedPacket ?? {}) ?? '-'}</Descriptions.Item>
<Descriptions.Item label="Protocol">{selectedPacket ? formatProtocolLabel(selectedPacket) : '-'}</Descriptions.Item>
<Descriptions.Item label="Application">{selectedPacket?.app_protocol ?? selectedPacket?.app_master_protocol ?? '-'}</Descriptions.Item>
<Descriptions.Item label="Host">{selectedPacket?.app_hostname ?? '-'}</Descriptions.Item>
<Descriptions.Item label="Verdict">{selectedPacket?.verdict ?? '-'}</Descriptions.Item>
<Descriptions.Item label="Source">
{selectedPacket?.src_ip ? `${selectedPacket.src_ip}${selectedPacket?.src_port ? `:${selectedPacket.src_port}` : ''}` : '-'}
</Descriptions.Item>
<Descriptions.Item label="Destination">
{selectedPacket?.dst_ip ? `${selectedPacket.dst_ip}${selectedPacket?.dst_port ? `:${selectedPacket.dst_port}` : ''}` : '-'}
</Descriptions.Item>
<Descriptions.Item label="Path">
{[selectedPacket?.ingress_if, selectedPacket?.egress_if].filter(Boolean).join(' -> ') || '-'}
</Descriptions.Item>
<Descriptions.Item label="Capture Sources">
{selectedPacket?.capture_sources?.join(', ') || '-'}
</Descriptions.Item>
</Descriptions>
<div>
<Text strong>Reduced metadata</Text>
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, marginTop: 8 }}>
{formatJson(reducedMetadata)}
</pre>
</div>
{decodedPayload?.headersText ? (
<div>
<Text strong>Decoded headers</Text>
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, marginTop: 8 }}>
{decodedPayload.headersText}
</pre>
</div>
) : null}
{decodedPayload?.bodyText ? (
<div>
<Text strong>Decoded body</Text>
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, marginTop: 8 }}>
{decodedPayload.bodyText}
</pre>
</div>
) : decodedPayload?.payloadText ? (
<div>
<Text strong>Decoded payload</Text>
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, marginTop: 8 }}>
{decodedPayload.payloadText}
</pre>
</div>
) : null}
</Space>
),
},
{
key: 'raw',
label: 'Raw',
children: (
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12 }}>
{rawHex}
</pre>
),
},
{
key: 'full',
label: 'Full JSON',
children: (
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12 }}>
{fullPacketJson}
</pre>
),
},
]}
/>
</Modal>
/>
</div>
);
}