import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Activity, ArrowLeft, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Crosshair, Fingerprint, Shield, Clock, Wifi, Lock, FileKey, Radio, Timer, Paperclip, Terminal, Package, FileText, Mail, AtSign } from 'lucide-react'; import api from '../utils/api'; import ArtifactDrawer from './ArtifactDrawer'; import MailDrawer from './MailDrawer'; import SessionDrawer from './SessionDrawer'; import EmptyState from './EmptyState/EmptyState'; import './Dashboard.css'; interface AttackerBehavior { os_guess: string | null; hop_distance: number | null; tcp_fingerprint: { window?: number | null; wscale?: number | null; mss?: number | null; options_sig?: string; has_sack?: boolean; has_timestamps?: boolean; } | null; retransmit_count: number; behavior_class: string | null; beacon_interval_s: number | null; beacon_jitter_pct: number | null; tool_guesses: string[] | null; timing_stats: { event_count?: number; duration_s?: number; mean_iat_s?: number | null; median_iat_s?: number | null; stdev_iat_s?: number | null; min_iat_s?: number | null; max_iat_s?: number | null; cv?: number | null; } | null; phase_sequence: { recon_end_ts?: string | null; exfil_start_ts?: string | null; exfil_latency_s?: number | null; large_payload_count?: number; } | null; updated_at?: string; } interface AttackerData { uuid: string; ip: string; first_seen: string; last_seen: string; event_count: number; service_count: number; decky_count: number; services: string[]; deckies: string[]; traversal_path: string | null; is_traversal: boolean; bounty_count: number; credential_count: number; fingerprints: any[]; commands: { service: string; decky: string; command: string; timestamp: string }[]; country_code: string | null; country_source: string | null; ptr_record: string | null; updated_at: string; behavior: AttackerBehavior | null; service_activity?: { interacted: string[]; scanned: string[]; }; ip_leaks?: Array<{ timestamp: string; decky?: string; service?: string; bounty_type: string; payload: { source_ip?: string; real_ip_claim?: string; source_header?: string; headers_seen?: Record; path?: string; method?: string; }; }>; } // ─── Fingerprint rendering ─────────────────────────────────────────────────── const fpTypeLabel: Record = { ja3: 'TLS FINGERPRINT', ja4l: 'LATENCY (JA4L)', tls_resumption: 'SESSION RESUMPTION', tls_certificate: 'CERTIFICATE', http_useragent: 'HTTP USER-AGENT', http_quirks: 'HTTP HEADER QUIRKS', vnc_client_version: 'VNC CLIENT', jarm: 'JARM', hassh_server: 'HASSH SERVER', tcpfp: 'TCP/IP STACK', }; const fpTypeIcon: Record = { ja3: , ja4l: , tls_resumption: , tls_certificate: , http_useragent: , http_quirks: , vnc_client_version: , jarm: , hassh_server: , tcpfp: , }; function getPayload(bounty: any): any { if (bounty?.payload && typeof bounty.payload === 'object') return bounty.payload; if (bounty?.payload && typeof bounty.payload === 'string') { try { return JSON.parse(bounty.payload); } catch { return bounty; } } return bounty; } const HashRow: React.FC<{ label: string; value?: string | null }> = ({ label, value }) => { if (!value) return null; return (
{label} {value}
); }; const Tag: React.FC<{ children: React.ReactNode; color?: string }> = ({ children, color }) => ( {children} ); const FpTlsHashes: React.FC<{ p: any }> = ({ p }) => (
{(p.tls_version || p.sni || p.alpn) && (
{p.tls_version && {p.tls_version}} {p.sni && SNI: {p.sni}} {p.alpn && ALPN: {p.alpn}} {p.dst_port && :{p.dst_port}}
)}
); const FpLatency: React.FC<{ p: any }> = ({ p }) => (
RTT {p.rtt_ms} ms
{p.client_ttl && (
TTL {p.client_ttl}
)}
); const FpResumption: React.FC<{ p: any }> = ({ p }) => { const mechanisms = typeof p.mechanisms === 'string' ? p.mechanisms.split(',') : Array.isArray(p.mechanisms) ? p.mechanisms : []; return (
{mechanisms.map((m: string) => ( {m.trim().toUpperCase().replace(/_/g, ' ')} ))}
); }; const FpCertificate: React.FC<{ p: any }> = ({ p }) => (
{p.subject_cn} {p.self_signed === 'true' && ( SELF-SIGNED )}
{p.issuer && (
ISSUER: {p.issuer}
)} {(p.not_before || p.not_after) && (
VALIDITY: {p.not_before || '?'} — {p.not_after || '?'}
)} {p.sans && (
SANs: {(typeof p.sans === 'string' ? p.sans.split(',') : p.sans).map((san: string) => ( {san.trim()} ))}
)}
); const FpJarm: React.FC<{ p: any }> = ({ p }) => (
{(p.target_ip || p.target_port) && (
{p.target_ip && {p.target_ip}} {p.target_port && :{p.target_port}}
)}
); const FpHassh: React.FC<{ p: any }> = ({ p }) => (
{p.ssh_banner && (
BANNER: {p.ssh_banner}
)} {p.kex_algorithms && (
KEX ALGORITHMS
{p.kex_algorithms.split(',').map((algo: string) => ( {algo.trim()} ))}
)} {p.encryption_s2c && (
ENCRYPTION (S→C)
{p.encryption_s2c.split(',').map((algo: string) => ( {algo.trim()} ))}
)} {(p.target_ip || p.target_port) && (
{p.target_ip && {p.target_ip}} {p.target_port && :{p.target_port}}
)}
); const FpTcpStack: React.FC<{ p: any }> = ({ p }) => (
{p.ttl && (
TTL {p.ttl}
)} {p.window_size && (
WIN {p.window_size}
)} {p.mss && (
MSS {p.mss}
)}
{p.df_bit === '1' && DF} {p.sack_ok === '1' && SACK} {p.timestamp === '1' && TS} {p.window_scale && p.window_scale !== '-1' && WSCALE:{p.window_scale}}
{p.options_order && (
OPTS: {p.options_order}
)} {(p.target_ip || p.target_port) && (
{p.target_ip && {p.target_ip}} {p.target_port && :{p.target_port}}
)}
); const FpGeneric: React.FC<{ p: any }> = ({ p }) => (
{p.value ? ( {p.value} ) : ( {JSON.stringify(p)} )}
); const FpHttpQuirks: React.FC<{ p: any }> = ({ p }) => { const order: string[] = Array.isArray(p.order) ? p.order : []; return (
{p.tool_guess && ( {String(p.tool_guess).toUpperCase()} )} {p.casing_category && ( CASE · {String(p.casing_category).toUpperCase()} )} {typeof p.stable_count === 'number' && ( {p.stable_count} STABLE HEADERS )}
{order.length > 0 && (
HEADER ORDER
{order.map((h, i) => ( {h} ))}
)}
); }; const FingerprintGroup: React.FC<{ fpType: string; items: any[] }> = ({ fpType, items }) => { const label = fpTypeLabel[fpType] || fpType.toUpperCase().replace(/_/g, ' '); const icon = fpTypeIcon[fpType] || ; return (
{icon} {label} {items.length > 1 && ( ({items.length}) )}
{items.map((fp, i) => { const p = getPayload(fp); switch (fpType) { case 'ja3': return ; case 'ja4l': return ; case 'tls_resumption': return ; case 'tls_certificate': return ; case 'jarm': return ; case 'hassh_server': return ; case 'tcpfp': return ; case 'http_quirks': return ; default: return ; } })}
); }; // ─── Behavioral profile blocks ────────────────────────────────────────────── const OS_LABELS: Record = { linux: 'LINUX', windows: 'WINDOWS', macos_ios: 'macOS / iOS', freebsd: 'FREEBSD', openbsd: 'OPENBSD', embedded: 'EMBEDDED', nmap: 'NMAP (SCANNER)', unknown: 'UNKNOWN', }; const BEHAVIOR_LABELS: Record = { beaconing: 'BEACONING', interactive: 'INTERACTIVE', scanning: 'SCANNING', brute_force: 'BRUTE FORCE', slow_scan: 'SLOW SCAN', mixed: 'MIXED', unknown: 'UNKNOWN', }; const BEHAVIOR_COLORS: Record = { beaconing: '#ff6b6b', interactive: 'var(--accent-color)', scanning: '#e5c07b', brute_force: '#ff9f43', slow_scan: '#c8a96e', mixed: 'var(--text-color)', unknown: 'var(--text-color)', }; const TOOL_LABELS: Record = { cobalt_strike: 'COBALT STRIKE', sliver: 'SLIVER', havoc: 'HAVOC', mythic: 'MYTHIC', nmap: 'NMAP', gophish: 'GOPHISH', nikto: 'NIKTO', sqlmap: 'SQLMAP', nuclei: 'NUCLEI', masscan: 'MASSCAN', zgrab: 'ZGRAB', metasploit: 'METASPLOIT', gobuster: 'GOBUSTER', dirbuster: 'DIRBUSTER', hydra: 'HYDRA', wfuzz: 'WFUZZ', curl: 'CURL', python_requests: 'PYTHON-REQUESTS', }; const fmtOpt = (v: number | null | undefined): string => v === null || v === undefined ? '—' : String(v); const fmtSecs = (v: number | null | undefined): string => { if (v === null || v === undefined) return '—'; if (v < 1) return `${(v * 1000).toFixed(0)} ms`; if (v < 60) return `${v.toFixed(2)} s`; if (v < 3600) return `${(v / 60).toFixed(2)} m`; return `${(v / 3600).toFixed(2)} h`; }; const StatBlock: React.FC<{ label: string; value: React.ReactNode; color?: string }> = ({ label, value, color, }) => (
{value}
{label}
); const KeyValueRow: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => (
{label} {value}
); // Tools detected via beacon timing (C2 frameworks). const _C2_TOOLS = new Set(['cobalt_strike', 'sliver', 'havoc', 'mythic']); const BehaviorHeadline: React.FC<{ b: AttackerBehavior }> = ({ b }) => { const osLabel = b.os_guess ? (OS_LABELS[b.os_guess] || b.os_guess.toUpperCase()) : '—'; const behaviorLabel = b.behavior_class ? (BEHAVIOR_LABELS[b.behavior_class] || b.behavior_class.toUpperCase()) : 'UNKNOWN'; const behaviorColor = b.behavior_class ? BEHAVIOR_COLORS[b.behavior_class] : undefined; return (
); }; const DetectedToolsBlock: React.FC<{ b: AttackerBehavior }> = ({ b }) => { const tools = b.tool_guesses && b.tool_guesses.length > 0 ? b.tool_guesses : null; if (!tools) return null; return (
DETECTED TOOLS
{tools.map(t => (
{TOOL_LABELS[t] || t.toUpperCase()} {_C2_TOOLS.has(t) ? 'BEACON TIMING' : 'HTTP HEADER'}
))}
); }; const BeaconBlock: React.FC<{ b: AttackerBehavior }> = ({ b }) => { if (b.behavior_class !== 'beaconing' || b.beacon_interval_s === null) return null; return (
BEACON CADENCE
INTERVAL {fmtSecs(b.beacon_interval_s)}
{b.beacon_jitter_pct !== null && (
JITTER {b.beacon_jitter_pct.toFixed(1)}%
)}
); }; const TcpStackBlock: React.FC<{ b: AttackerBehavior }> = ({ b }) => { const fp = b.tcp_fingerprint; if (!fp || (!fp.window && !fp.mss && !fp.options_sig)) return null; return (
TCP STACK (PASSIVE)
{fp.window !== null && fp.window !== undefined && (
WIN {fp.window}
)} {fp.wscale !== null && fp.wscale !== undefined && (
WSCALE {fp.wscale}
)} {fp.mss !== null && fp.mss !== undefined && (
MSS {fp.mss}
)}
RETRANSMITS 0 ? '#e5c07b' : undefined, }} > {b.retransmit_count}
{fp.has_sack && SACK} {fp.has_timestamps && TS}
{fp.options_sig && (
OPTS: {fp.options_sig}
)}
); }; const TimingStatsBlock: React.FC<{ b: AttackerBehavior }> = ({ b }) => { const s = b.timing_stats; if (!s || !s.event_count || s.event_count < 2) return null; return (
INTER-EVENT TIMING
); }; const PhaseSequenceBlock: React.FC<{ b: AttackerBehavior }> = ({ b }) => { const p = b.phase_sequence; if (!p || (!p.recon_end_ts && !p.exfil_start_ts && !p.large_payload_count)) return null; return (
PHASE SEQUENCE
); }; // ─── Collapsible section ──────────────────────────────────────────────────── const Section: React.FC<{ title: React.ReactNode; right?: React.ReactNode; open: boolean; onToggle: () => void; children: React.ReactNode; }> = ({ title, right, open, onToggle, children }) => (
{open ? : }

{title}

{right &&
e.stopPropagation()}>{right}
}
{open && children}
); // ─── Main component ───────────────────────────────────────────────────────── const AttackerDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [attacker, setAttacker] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [serviceFilter, setServiceFilter] = useState(null); // Section collapse state const [openSections, setOpenSections] = useState>({ timeline: true, services: true, deckies: true, behavior: true, commands: true, fingerprints: true, artifacts: true, sessions: true, smtpTargets: true, mail: true, }); // Captured file-drop artifacts (ssh inotify farm) for this attacker. type ArtifactLog = { id: number; timestamp: string; decky: string; service: string; fields: string; // JSON-encoded SD params (parsed lazily below) }; const [artifacts, setArtifacts] = useState([]); const [artifact, setArtifact] = useState<{ decky: string; storedAs: string; fields: Record } | null>(null); // PTY session transcripts (sessrec) for this attacker. type SessionLog = { id: number; timestamp: string; decky: string; service: string; fields: string; }; const [sessions, setSessions] = useState([]); const [session, setSession] = useState<{ decky: string; sid: string; fields: Record } | null>(null); // SMTP victim-domain rollup (viewer-safe: domains only, no local parts). type SmtpTargetRow = { domain: string; count: number; first_seen: string; last_seen: string; }; const [smtpTargets, setSmtpTargets] = useState([]); // Stored SMTP messages (admin-gated: full attacker-controlled bodies). type MailLog = { id: number; timestamp: string; decky: string; service: string; fields: string; }; const [mail, setMail] = useState([]); const [mailForbidden, setMailForbidden] = useState(false); const [mailItem, setMailItem] = useState<{ decky: string; storedAs: string; fields: Record } | null>(null); const toggle = (key: string) => setOpenSections((prev) => ({ ...prev, [key]: !prev[key] })); // Commands pagination state const [commands, setCommands] = useState([]); const [cmdTotal, setCmdTotal] = useState(0); const [cmdPage, setCmdPage] = useState(1); const cmdLimit = 50; useEffect(() => { const fetchAttacker = async () => { setLoading(true); try { const res = await api.get(`/attackers/${id}`); setAttacker(res.data); } catch (err: any) { if (err.response?.status === 404) { setError('ATTACKER NOT FOUND'); } else { setError('FAILED TO LOAD ATTACKER PROFILE'); } } finally { setLoading(false); } }; fetchAttacker(); }, [id]); useEffect(() => { if (!id) return; const fetchCommands = async () => { try { const offset = (cmdPage - 1) * cmdLimit; let url = `/attackers/${id}/commands?limit=${cmdLimit}&offset=${offset}`; if (serviceFilter) url += `&service=${encodeURIComponent(serviceFilter)}`; const res = await api.get(url); setCommands(res.data.data); setCmdTotal(res.data.total); } catch (err: any) { if (err.response?.status === 422) { alert("Fuck off."); } setCommands([]); setCmdTotal(0); } }; fetchCommands(); }, [id, cmdPage, serviceFilter]); // Reset command page when service filter changes useEffect(() => { setCmdPage(1); }, [serviceFilter]); useEffect(() => { if (!id) return; const fetchArtifacts = async () => { try { const res = await api.get(`/attackers/${id}/artifacts`); setArtifacts(res.data.data ?? []); } catch { setArtifacts([]); } }; fetchArtifacts(); }, [id]); useEffect(() => { if (!id) return; const fetchSmtpTargets = async () => { try { const res = await api.get(`/attackers/${id}/smtp-targets`); setSmtpTargets(res.data.data ?? []); } catch { setSmtpTargets([]); } }; fetchSmtpTargets(); }, [id]); useEffect(() => { if (!id) return; const fetchMail = async () => { try { const res = await api.get(`/attackers/${id}/mail`); setMail(res.data.data ?? []); setMailForbidden(false); } catch (err: any) { setMail([]); setMailForbidden(err?.response?.status === 403); } }; fetchMail(); }, [id]); useEffect(() => { if (!id) return; const fetchSessions = async () => { try { const res = await api.get(`/attackers/${id}/transcripts`); setSessions(res.data.data ?? []); } catch { setSessions([]); } }; fetchSessions(); }, [id]); if (loading) { return (
LOADING THREAT PROFILE...
); } if (error || !attacker) { return (
{error || 'ATTACKER NOT FOUND'}
); } return (
{/* Back Button */} {/* Header */}

{attacker.ip}

{attacker.country_code && ( {attacker.country_code} )} {attacker.is_traversal && ( TRAVERSAL )}
{/* Stats Row */}
{attacker.event_count}
EVENTS
{attacker.bounty_count}
BOUNTIES
{attacker.credential_count}
CREDENTIALS
{attacker.service_count}
SERVICES
{attacker.decky_count}
DECKIES
{/* Scanned vs. Interacted — activity-depth signal */} {attacker.service_activity && (attacker.service_activity.scanned.length > 0 || attacker.service_activity.interacted.length > 0) && (
0 ? `Services: ${attacker.service_activity.scanned.join(', ')}` : 'No services were scanned without engagement.' } >
{attacker.service_activity.scanned.length}
SCANNED · SERVICES
0 ? `Services: ${attacker.service_activity.interacted.join(', ')}` : 'No services were interacted with — scan-only attacker.' } >
{attacker.service_activity.interacted.length}
INTERACTED WITH · SERVICES
)} {/* Timestamps */}
toggle('timeline')}>
FIRST SEEN: {new Date(attacker.first_seen).toLocaleString()}
LAST SEEN: {new Date(attacker.last_seen).toLocaleString()}
UPDATED: {new Date(attacker.updated_at).toLocaleString()}
ORIGIN: {attacker.country_code ? ( {attacker.country_code} {attacker.country_source && ( ({attacker.country_source}) )} ) : ( unknown )}
REVERSE DNS: {attacker.ptr_record ? ( {attacker.ptr_record} ) : ( )}
{attacker.ip_leaks && attacker.ip_leaks.length > 0 && (
LEAKED IPs:{' '} {Array.from( new Set( (attacker.ip_leaks || []) .map((l) => l.payload?.real_ip_claim) .filter((v): v is string => !!v), ), ).map((ip, i, arr) => { const latest = (attacker.ip_leaks || []).find( (l) => l.payload?.real_ip_claim === ip, ); const tooltip = latest ? `Leaked via ${latest.payload.source_header ?? '?'}; source ${latest.payload.source_ip ?? '?'}` : ''; return ( {ip} {i < arr.length - 1 ? ', ' : ''} ); })}
)}
{/* Services */}
toggle('services')}>
{attacker.services.length > 0 ? attacker.services.map((svc) => { const isActive = serviceFilter === svc; return ( setServiceFilter(isActive ? null : svc)} title={isActive ? 'Clear filter' : `Filter by ${svc.toUpperCase()}`} > {svc.toUpperCase()} ); }) : ( No services recorded )}
{/* Deckies & Traversal */}
toggle('deckies')}>
{attacker.traversal_path ? (
TRAVERSAL PATH: {attacker.traversal_path}
) : (
{attacker.deckies.map((d) => ( {d} ))} {attacker.deckies.length === 0 && No deckies recorded}
)}
{/* Behavioral Profile */}
toggle('behavior')} > {attacker.behavior ? (
) : ( )}
{/* Commands */} {(() => { const cmdTotalPages = Math.ceil(cmdTotal / cmdLimit); return (
COMMANDS ({cmdTotal}{serviceFilter ? ` ${serviceFilter.toUpperCase()}` : ''})} open={openSections.commands} onToggle={() => toggle('commands')} right={openSections.commands && cmdTotalPages > 1 ? (
Page {cmdPage} of {cmdTotalPages}
) : undefined} > {commands.length > 0 ? (
{commands.map((cmd, i) => ( ))}
TIMESTAMP SERVICE DECKY COMMAND
{cmd.timestamp ? new Date(cmd.timestamp).toLocaleString() : '-'} {cmd.service} {cmd.decky} {cmd.command}
) : ( )}
); })()} {/* Fingerprints — grouped by type */} {(() => { const filteredFps = serviceFilter ? attacker.fingerprints.filter((fp) => { const p = getPayload(fp); return p.service === serviceFilter; }) : attacker.fingerprints; // Group fingerprints by type const groups: Record = {}; filteredFps.forEach((fp) => { const p = getPayload(fp); const fpType: string = p.fingerprint_type || 'unknown'; if (!groups[fpType]) groups[fpType] = []; groups[fpType].push(fp); }); // Active probes first, then passive, then unknown const activeTypes = ['jarm', 'hassh_server', 'tcpfp']; const passiveTypes = ['ja3', 'ja4l', 'tls_resumption', 'tls_certificate', 'http_useragent', 'http_quirks', 'vnc_client_version']; const knownTypes = [...activeTypes, ...passiveTypes]; const unknownTypes = Object.keys(groups).filter((t) => !knownTypes.includes(t)); const hasActive = activeTypes.some((t) => groups[t]); const hasPassive = [...passiveTypes, ...unknownTypes].some((t) => groups[t]); return (
FINGERPRINTS ({filteredFps.length}{serviceFilter ? ` / ${attacker.fingerprints.length}` : ''})} open={openSections.fingerprints} onToggle={() => toggle('fingerprints')} > {filteredFps.length > 0 ? (
{/* Active probes section */} {hasActive && (
ACTIVE PROBES
{activeTypes.filter((t) => groups[t]).map((fpType) => ( ))}
)} {/* Passive fingerprints section */} {hasPassive && (
PASSIVE FINGERPRINTS
{[...passiveTypes, ...unknownTypes].filter((t) => groups[t]).map((fpType) => ( ))}
)}
) : (
{serviceFilter ? `NO ${serviceFilter.toUpperCase()} FINGERPRINTS CAPTURED` : 'NO FINGERPRINTS CAPTURED'}
)}
); })()} {/* Captured Artifacts */}
CAPTURED ARTIFACTS ({artifacts.length})} open={openSections.artifacts} onToggle={() => toggle('artifacts')} > {artifacts.length > 0 ? (
{artifacts.map((row) => { let fields: Record = {}; try { fields = JSON.parse(row.fields || '{}'); } catch {} const storedAs = fields.stored_as ? String(fields.stored_as) : null; const sha = fields.sha256 ? String(fields.sha256) : ''; return ( ); })}
TIMESTAMP DECKY FILENAME SIZE SHA-256
{new Date(row.timestamp).toLocaleString()} {row.decky} {fields.orig_path ?? storedAs ?? '—'} {fields.size ? `${fields.size} B` : '—'} {sha ? `${sha.slice(0, 12)}…` : '—'} {storedAs && ( )}
) : ( )}
{artifact && ( setArtifact(null)} /> )} {/* SMTP Victim Domains (viewer-safe rollup) */}
SMTP VICTIM DOMAINS ({smtpTargets.length})} open={openSections.smtpTargets} onToggle={() => toggle('smtpTargets')} > {smtpTargets.length > 0 ? (
{smtpTargets.map((row) => ( ))}
DOMAIN COUNT FIRST SEEN LAST SEEN
{row.domain} {row.count} {new Date(row.first_seen).toLocaleString()} {new Date(row.last_seen).toLocaleString()}
) : ( )}
{/* Stored Mail (admin only — bodies are attacker-controlled) */}
STORED MAIL ({mail.length})} open={openSections.mail} onToggle={() => toggle('mail')} > {mailForbidden ? ( ) : mail.length > 0 ? (
{mail.map((row) => { let fields: Record = {}; try { fields = JSON.parse(row.fields || '{}'); } catch {} const storedAs = fields.stored_as ? String(fields.stored_as) : null; return ( ); })}
TIMESTAMP DECKY SUBJECT FROM SIZE
{new Date(row.timestamp).toLocaleString()} {row.decky} {fields.subject || '—'} {fields.from_addr || fields.mail_from || '—'} {fields.size ? `${fields.size} B` : '—'} {storedAs && ( )}
) : ( )}
{mailItem && ( setMailItem(null)} /> )} {/* Recorded PTY Sessions (SSH / Telnet) */}
SESSION TRANSCRIPTS ({sessions.length})} open={openSections.sessions} onToggle={() => toggle('sessions')} > {sessions.length > 0 ? (
{sessions.map((row) => { let fields: Record = {}; try { fields = JSON.parse(row.fields || '{}'); } catch {} const sid = fields.sid ? String(fields.sid) : null; const dur = fields.duration_s; const bytes = fields.bytes; return ( ); })}
TIMESTAMP DECKY SERVICE DURATION BYTES
{new Date(row.timestamp).toLocaleString()} {row.decky} {fields.service ?? row.service} {dur ? `${dur}s` : '—'} {bytes ? `${bytes} B` : '—'} {sid && ( )}
) : ( )}
{session && ( setSession(null)} /> )} {/* UUID footer */}
UUID: {attacker.uuid}
); }; export default AttackerDetail;