import React, { useEffect, useRef, useState } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { ChevronLeft, ChevronRight, ChevronRight as ChevR, Filter, Fingerprint, Search, } from '../icons'; import api from '../utils/api'; import EmptyState from './EmptyState/EmptyState'; import { useFocusSearch } from '../hooks/useFocusSearch'; import { useIdentityStream } from './useIdentityStream'; import './Dashboard.css'; import './Attackers.css'; interface IdentityEntry { uuid: string; schema_version: number; campaign_id: string | null; first_seen_at: string | null; last_seen_at: string | null; updated_at: string; confidence: number | null; observation_count: number; ja3_hashes: string | null; hassh_hashes: string | null; payload_simhashes: string | null; c2_endpoints: string | null; merged_into_uuid: string | null; } const safeListLen = (raw: string | null): number => { if (!raw) return 0; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.length : 0; } catch { return 0; } }; const timeAgo = (dateStr: string | null): string => { if (!dateStr) return '—'; const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; return `${Math.floor(hrs / 24)}d ago`; }; const Identities: React.FC = () => { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const query = (searchParams.get('q') || '').toLowerCase(); const page = parseInt(searchParams.get('page') || '1'); const [identities, setIdentities] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [searchInput, setSearchInput] = useState(searchParams.get('q') || ''); const searchRef = useRef(null); useFocusSearch(searchRef); const limit = 50; const fetchIdentities = async () => { setLoading(true); try { const offset = (page - 1) * limit; const res = await api.get(`/identities?limit=${limit}&offset=${offset}`); setIdentities(res.data.data ?? []); setTotal(res.data.total ?? 0); } catch (err) { console.error('Failed to fetch identities', err); } finally { setLoading(false); } }; useEffect(() => { fetchIdentities(); }, [page]); // Live updates: refetch on any clusterer event so the list stays // current without polling. useIdentityStream({ enabled: true, onEvent: () => { fetchIdentities(); }, }); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setSearchParams({ q: searchInput, page: '1' }); }; const setPage = (p: number) => setSearchParams({ q: searchParams.get('q') || '', page: p.toString() }); const totalPages = Math.max(1, Math.ceil(total / limit)); const visible = query ? identities.filter((i) => i.uuid.toLowerCase().includes(query) || (i.campaign_id || '').toLowerCase().includes(query), ) : identities; const assignedCount = identities.filter((i) => i.campaign_id).length; return (

IDENTITY RESOLUTION

{total.toLocaleString()} IDENTITIES · {assignedCount} CAMPAIGN-ASSIGNED
setSearchInput(e.target.value)} />
{visible.length.toLocaleString()} IDENTITIES SHOWN
Page {page} of {totalPages}
{visible.length > 0 ? visible.map((i) => ( navigate(`/identities/${i.uuid}`)} > )) : ( )}
UUID FIRST SEEN LAST SEEN JA3 / HASSH PAYLOADS / C2 OBS CAMPAIGN
{i.uuid.slice(0, 12)}… {timeAgo(i.first_seen_at)} {timeAgo(i.last_seen_at)} {safeListLen(i.ja3_hashes)} JA3{' '} {safeListLen(i.hassh_hashes)} HASSH {safeListLen(i.payload_simhashes)} PAYLOAD{' '} {safeListLen(i.c2_endpoints)} C2 {i.observation_count} {i.campaign_id ? ( { e.stopPropagation(); navigate(`/campaigns/${i.campaign_id}`); }} > {i.campaign_id.slice(0, 8)}… ) : ( )}
); }; export default Identities;