105 lines
4.6 KiB
TypeScript
105 lines
4.6 KiB
TypeScript
import { Card, Drawer, Space, Tag, Typography } from 'antd';
|
|
|
|
import type { HostIntelligenceEvidence } from '../../types/analysis';
|
|
import {
|
|
endpointText,
|
|
formatBytes,
|
|
formatTimestamp,
|
|
protocolColor,
|
|
renderLabelTags,
|
|
} from './shared.tsx';
|
|
|
|
const { Text } = Typography;
|
|
|
|
export function HostDetailDrawer({
|
|
host,
|
|
open,
|
|
onClose,
|
|
}: {
|
|
host: HostIntelligenceEvidence | null;
|
|
open: boolean;
|
|
onClose: () => void;
|
|
}) {
|
|
return (
|
|
<Drawer title={host ? endpointText(host.ip_address, host.mac_address) : 'Host details'} placement="right" width={520} open={open} onClose={onClose}>
|
|
{host == null ? null : (
|
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
|
<Card size="small" title="Identity">
|
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
|
<Text>IP: {host.ip_address ?? '—'}</Text>
|
|
<Text>MAC: {host.mac_address ?? '—'}</Text>
|
|
<Text>Interfaces: {host.interfaces.join(', ') || '—'}</Text>
|
|
<Text>First seen: {formatTimestamp(host.first_seen)} | Last seen: {formatTimestamp(host.last_seen)}</Text>
|
|
<div>{renderLabelTags(host.hostnames, 'geekblue')}</div>
|
|
</Space>
|
|
</Card>
|
|
|
|
<Card size="small" title="Protocol Profile">
|
|
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
|
{host.top_protocols.length === 0 ? (
|
|
<Text type="secondary">No protocol profile available yet.</Text>
|
|
) : (
|
|
host.top_protocols.map((protocol) => {
|
|
const maxCount = Math.max(...host.top_protocols.map((item) => item.packet_count), 1);
|
|
const widthPct = (protocol.packet_count / maxCount) * 100;
|
|
return (
|
|
<div key={protocol.label}>
|
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
|
<Tag color={protocolColor(protocol.label)}>{protocol.label}</Tag>
|
|
<Text>{protocol.packet_count}</Text>
|
|
</Space>
|
|
<div style={{ height: 8, background: '#eef3f8', borderRadius: 999, overflow: 'hidden' }}>
|
|
<div style={{ width: `${widthPct}%`, height: '100%', background: protocolColor(protocol.label) }} />
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</Space>
|
|
</Card>
|
|
|
|
<Card size="small" title="Top Peers">
|
|
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
|
{host.peers.length === 0 ? (
|
|
<Text type="secondary">No peer details available yet.</Text>
|
|
) : (
|
|
host.peers.map((peer) => {
|
|
const maxCount = Math.max(...host.peers.map((item) => item.packet_count), 1);
|
|
const widthPct = (peer.packet_count / maxCount) * 100;
|
|
return (
|
|
<div key={`${peer.ip_address ?? 'no-ip'}|${peer.mac_address ?? 'no-mac'}`}>
|
|
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
|
<Text>{endpointText(peer.ip_address, peer.mac_address)}</Text>
|
|
<Text type="secondary">{peer.packet_count} packets · {formatBytes(peer.byte_count)} · {formatTimestamp(peer.last_seen)}</Text>
|
|
<div style={{ height: 8, background: '#eef3f8', borderRadius: 999, overflow: 'hidden' }}>
|
|
<div style={{ width: `${widthPct}%`, height: '100%', background: '#5b8ff9' }} />
|
|
</div>
|
|
<div>{renderLabelTags(peer.protocols, 'purple')}</div>
|
|
</Space>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</Space>
|
|
</Card>
|
|
|
|
<Card size="small" title="Likely Services">
|
|
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
|
{host.services.length === 0 ? (
|
|
<Text type="secondary">No service evidence inferred yet.</Text>
|
|
) : (
|
|
host.services.map((service) => (
|
|
<div key={`${service.port ?? 'no-port'}|${service.protocol}`}>
|
|
<Text>Port {service.port ?? '—'} · {service.protocol} · {service.packet_count} packets · {formatBytes(service.byte_count)}</Text>
|
|
<div style={{ marginTop: 4 }}>{renderLabelTags(service.hostnames, 'geekblue')}</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</Space>
|
|
</Card>
|
|
</Space>
|
|
)}
|
|
</Drawer>
|
|
);
|
|
}
|