import { ReloadOutlined } from '@ant-design/icons'; import { Button, Card, Checkbox, Col, InputNumber, Row, Space, Spin, Table, Tabs, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'; import { fetchConversationAnalysis, fetchConversationFlowDetail, fetchHostIntelligenceAnalysis, fetchInterfaceHostProtocolAnalysis, fetchInterfaceProtocolPathAnalysis, } from '../api/apiClient'; import { ConversationFlowDrawer, ConversationMatrix, ConversationTimeline, } from '../components/analysis/CommunicationViews'; import { HostDetailDrawer, } from '../components/analysis/IntelligenceRiskViews'; import { ForceTopology, PacketPathLanes, ProtocolHeatmap, SankeyTopology } from '../components/analysis/TopologyViews'; import { buildTopologyData, conversationRowKey, endpointText, formatBytes, formatDurationMs, formatTimestamp, protocolColor, renderLabelCountTags, renderLabelTags, type ProtocolTableRow, } from '../components/analysis/shared.tsx'; import type { ConversationAnalysisResponse, ConversationEvidence, ConversationFlowDetailResponse, HostIntelligenceAnalysisResponse, HostIntelligenceEvidence, InterfaceHostProtocolAnalysisResponse, InterfaceProtocolPathAnalysisResponse, } from '../types/analysis'; const { Title, Text, Paragraph } = Typography; export default function Analysis(): ReactElement { const [sinceMinutes, setSinceMinutes] = useState(null); const [limitPerInterface, setLimitPerInterface] = useState(50); const [limitProtocolsPerHost, setLimitProtocolsPerHost] = useState(12); const [limitPaths, setLimitPaths] = useState(500); const [limitConversations, setLimitConversations] = useState(300); const [limitHostIntelligence, setLimitHostIntelligence] = useState(40); const [includeEthernetLayer, setIncludeEthernetLayer] = useState(false); const [includeIpLayer, setIncludeIpLayer] = useState(false); const [data, setData] = useState(null); const [pathData, setPathData] = useState(null); const [conversationData, setConversationData] = useState(null); const [hostIntelligenceData, setHostIntelligenceData] = useState(null); const [selectedHost, setSelectedHost] = useState(null); const [selectedConversation, setSelectedConversation] = useState(null); const [conversationDetail, setConversationDetail] = useState(null); const [conversationDetailLoading, setConversationDetailLoading] = useState(false); const [loading, setLoading] = useState(false); const loadData = useCallback(async () => { setLoading(true); try { const [hostResponse, pathResponse, conversationsResponse, hostIntelResponse] = await Promise.all([ fetchInterfaceHostProtocolAnalysis(sinceMinutes, limitPerInterface, limitProtocolsPerHost), fetchInterfaceProtocolPathAnalysis(sinceMinutes, limitPaths), fetchConversationAnalysis(sinceMinutes, limitConversations), fetchHostIntelligenceAnalysis(sinceMinutes, limitHostIntelligence), ]); setData(hostResponse); setPathData(pathResponse); setConversationData(conversationsResponse); setHostIntelligenceData(hostIntelResponse); } catch (error: any) { message.error(error?.message ?? 'Failed to load analysis data'); } finally { setLoading(false); } }, [ sinceMinutes, limitPerInterface, limitProtocolsPerHost, limitPaths, limitConversations, limitHostIntelligence, ]); const openConversationDetail = useCallback( async (conversation: ConversationEvidence) => { setSelectedConversation(conversation); setConversationDetail(null); setConversationDetailLoading(true); try { const detailResponse = await fetchConversationFlowDetail({ flowId: conversation.flow_ids.length === 1 ? conversation.flow_ids[0] : null, srcIpAddress: conversation.src_ip_address, srcMacAddress: conversation.src_mac_address, dstIpAddress: conversation.dst_ip_address, dstMacAddress: conversation.dst_mac_address, srcPort: conversation.src_port, dstPort: conversation.dst_port, protocol: conversation.protocol, sinceMinutes, limitPackets: 1500, }); setConversationDetail(detailResponse); } catch (error: any) { message.error(error?.message ?? 'Failed to load conversation detail'); } finally { setConversationDetailLoading(false); } }, [sinceMinutes], ); useEffect(() => { loadData().catch(() => undefined); }, [loadData]); const topologyData = useMemo( () => buildTopologyData(data?.interfaces ?? [], { includeEthernetLayer, includeIpLayer, }), [data, includeEthernetLayer, includeIpLayer], ); const analysisNotes = useMemo( () => Array.from( new Set([ ...(data?.notes ?? []), ...(conversationData?.notes ?? []), ...(hostIntelligenceData?.notes ?? []), ]), ), [data, conversationData, hostIntelligenceData], ); const columns = useMemo>( () => [ { title: 'Interface', dataIndex: 'interface', key: 'interface', width: 140, render: (value: string) => {value}, }, { title: 'IP', dataIndex: 'ip_address', key: 'ip_address', render: (value?: string | null) => value ?? '—', }, { title: 'MAC', dataIndex: 'mac_address', key: 'mac_address', render: (value?: string | null) => value ?? '—', }, { title: 'Protocol', dataIndex: 'protocol', key: 'protocol', width: 140, render: (value: string) => {value}, }, { title: 'Host Packets', dataIndex: 'host_packet_count', key: 'host_packet_count', width: 110 }, { title: 'Protocol Packets', dataIndex: 'protocol_packet_count', key: 'protocol_packet_count', width: 130 }, { title: 'Accept', dataIndex: 'accept_count', key: 'accept_count', width: 90 }, { title: 'Drop', dataIndex: 'drop_count', key: 'drop_count', width: 90 }, { title: 'Reject', dataIndex: 'reject_count', key: 'reject_count', width: 90 }, { title: 'Unknown', dataIndex: 'unknown_count', key: 'unknown_count', width: 90 }, { title: 'Last Seen', dataIndex: 'last_seen', key: 'last_seen', width: 220, render: (value: string) => formatTimestamp(value), }, ], [], ); const conversationColumns = useMemo>( () => [ { title: 'Source', key: 'source', render: (_, row) => endpointText(row.src_ip_address, row.src_mac_address), }, { title: 'Destination', key: 'destination', render: (_, row) => endpointText(row.dst_ip_address, row.dst_mac_address), }, { title: 'Ports', key: 'ports', width: 130, render: (_, row) => `${row.src_port ?? '—'} -> ${row.dst_port ?? '—'}`, }, { title: 'Protocol', dataIndex: 'protocol', key: 'protocol', width: 140, render: (value: string) => {value}, }, { title: 'Hostnames', key: 'hostnames', render: (_, row) => renderLabelTags(row.hostnames.slice(0, 4), 'geekblue'), }, { title: 'Packets', dataIndex: 'packet_count', key: 'packet_count', width: 90 }, { title: 'Flows', dataIndex: 'flow_count', key: 'flow_count', width: 80 }, { title: 'Bytes', dataIndex: 'byte_count', key: 'byte_count', width: 110, render: (value: number) => formatBytes(value), }, { title: 'Duration', dataIndex: 'duration_ms', key: 'duration_ms', width: 110, render: (value: number) => formatDurationMs(value), }, { title: 'Verdict', key: 'verdict', width: 180, render: (_, row) => ( A {row.accept_count} / D {row.drop_count} / R {row.reject_count} ), }, { title: 'Last Seen', dataIndex: 'last_seen', key: 'last_seen', width: 220, render: (value: string) => formatTimestamp(value), }, ], [], ); const hostColumns = useMemo>( () => [ { title: 'Host', key: 'host', render: (_, row) => (
{row.ip_address ?? '—'}
{row.mac_address ?? '—'}
), }, { title: 'Interfaces', key: 'interfaces', render: (_, row) => renderLabelTags(row.interfaces, 'blue'), }, { title: 'Hostnames', key: 'hostnames', render: (_, row) => renderLabelTags(row.hostnames.slice(0, 4), 'geekblue'), }, { title: 'Top Protocols', key: 'top_protocols', render: (_, row) => renderLabelCountTags(row.top_protocols), }, { title: 'Packets', dataIndex: 'packet_count', key: 'packet_count', width: 90 }, { title: 'Bytes', dataIndex: 'byte_count', key: 'byte_count', width: 110, render: (value: number) => formatBytes(value), }, { title: 'Role Bias', key: 'role_bias', width: 140, render: (_, row) => ( src {row.source_count} / dst {row.destination_count} ), }, { title: 'Last Seen', dataIndex: 'last_seen', key: 'last_seen', width: 220, render: (value: string) => formatTimestamp(value), }, ], [], ); return (
Analysis Explore inferred interface, host, and protocol relationships from captured traffic. Look back setSinceMinutes(value == null ? null : Number(value))} /> minutes, blank = all history Max hosts per interface setLimitPerInterface(value ?? 50)} /> Max protocols per host setLimitProtocolsPerHost(value ?? 12)} /> Max packet paths setLimitPaths(value ?? 500)} /> setIncludeEthernetLayer(event.target.checked)}> Ethernet layer setIncludeIpLayer(event.target.checked)}> IP layer Max conversations setLimitConversations(value ?? 300)} /> Max host intelligence rows setLimitHostIntelligence(value ?? 40)} /> Since {formatTimestamp(data.since)} : null} > Shows aggregated interface, host, and protocol relationships across the captured traffic.
), }, { key: 'force', label: 'Force Graph', children: (
Useful for exploring clusters and protocol neighborhoods across interfaces and hosts.
), }, { key: 'heatmap', label: 'Heatmap', children: (
Useful for comparing which hosts are most active in which protocols.
), }, ]} /> This is the underlying aggregated evidence used by the topology views, including verdict counts per interface, host, and protocol. ), }, { key: 'communication', label: 'Communication', children: ( Parallel-coordinates view of grouped packet paths as source endpoint to ingress to protocol to egress to destination endpoint. Time-ordered view of the busiest conversations. Click a bar to inspect the full packet sequence, subflows, and derived request/response events. Source-to-destination adjacency matrix for the busiest conversations. Cell color and value reflect packet volume, which makes the dominant communication relationships stand out quickly. Directional conversations grouped by source, destination, ports, protocol, and verdict outcome. Click a row for packet-level drill-down.
({ onClick: () => openConversationDetail(row), style: { cursor: 'pointer' }, })} pagination={{ pageSize: 20 }} locale={{ emptyText: loading ? 'Loading…' : 'No conversation evidence available yet.' }} /> ), }, { key: 'identity', label: 'Identity & Services', children: ( Asset-focused view combining interfaces, hostname hints, dominant protocols, likely services, and peer relationships. Click a row to open a focused host detail drawer.
`${row.ip_address ?? 'no-ip'}|${row.mac_address ?? 'no-mac'}`} columns={hostColumns} dataSource={hostIntelligenceData?.hosts ?? []} size="small" bordered onRow={(row) => ({ onClick: () => setSelectedHost(row), style: { cursor: 'pointer' }, })} pagination={{ pageSize: 15 }} locale={{ emptyText: loading ? 'Loading…' : 'No host intelligence available yet.' }} /> ), }, ]} /> setSelectedHost(null)} /> { setSelectedConversation(null); setConversationDetail(null); setConversationDetailLoading(false); }} /> ); }