import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { Lock, Search, ChevronLeft, ChevronRight, Filter, ChevronRight as ChevR, Target, RefreshCw, } from '../icons'; import api from '../utils/api'; import CredentialsInspector from './CredentialsInspector'; import type { CredentialEntry } from './CredentialsInspector'; import CredentialReuseInspector from './CredentialReuseInspector'; import type { CredentialReuseRow } from './CredentialReuseInspector'; import EmptyState from './EmptyState/EmptyState'; import { useFocusSearch } from '../hooks/useFocusSearch'; import './Dashboard.css'; import './Credentials.css'; const truncHash = (h: string | null | undefined, n = 12): string => h ? `${h.slice(0, n)}…` : '—'; const CREDS_LIMIT = 50; const REUSE_LIMIT = 25; const REUSE_MAP_CAP = 500; type Tab = 'creds' | 'reuse'; const reuseKey = (sha: string, kind: string, principal: string | null): string => `${sha}|${kind}|${principal ?? ''}`; const Credentials: React.FC = () => { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const query = searchParams.get('q') || ''; const serviceFilter = searchParams.get('service') || ''; const tab = (searchParams.get('tab') === 'reuse' ? 'reuse' : 'creds') as Tab; const page = parseInt(searchParams.get('page') || '1'); const [creds, setCreds] = useState([]); const [credsTotal, setCredsTotal] = useState(0); const [reuseRows, setReuseRows] = useState([]); const [reuseTotal, setReuseTotal] = useState(0); const [reuseMap, setReuseMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [searchInput, setSearchInput] = useState(query); const searchRef = useRef(null); useFocusSearch(searchRef); const [selectedCred, setSelectedCred] = useState(null); const [selectedReuse, setSelectedReuse] = useState(null); const [refreshTick, setRefreshTick] = useState(0); // ── Fetch credentials (CREDS tab + always for badge totals) useEffect(() => { if (tab !== 'creds') return; let cancelled = false; (async () => { setLoading(true); try { const offset = (page - 1) * CREDS_LIMIT; let url = `/credentials?limit=${CREDS_LIMIT}&offset=${offset}`; if (query) url += `&search=${encodeURIComponent(query)}`; if (serviceFilter) url += `&service=${encodeURIComponent(serviceFilter)}`; const res = await api.get(url); if (cancelled) return; setCreds(res.data.data); setCredsTotal(res.data.total); } catch (err) { console.error('Failed to fetch credentials', err); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [tab, query, serviceFilter, page, refreshTick]); // ── Fetch reuse rows (REUSE tab) useEffect(() => { if (tab !== 'reuse') return; let cancelled = false; (async () => { setLoading(true); try { const offset = (page - 1) * REUSE_LIMIT; const res = await api.get(`/credential-reuse?limit=${REUSE_LIMIT}&offset=${offset}`); if (cancelled) return; setReuseRows(res.data.data); setReuseTotal(res.data.total); } catch (err) { console.error('Failed to fetch credential-reuse', err); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [tab, page, refreshTick]); // ── Build reuse-map for the badge column on the CREDS tab useEffect(() => { let cancelled = false; (async () => { try { const res = await api.get(`/credential-reuse?limit=${REUSE_MAP_CAP}&offset=0`); if (cancelled) return; const m = new Map(); (res.data.data as CredentialReuseRow[]).forEach(r => { m.set(reuseKey(r.secret_sha256, r.secret_kind, r.principal), { id: r.id, target_count: r.target_count, }); }); setReuseMap(m); } catch { /* badge column degrades silently to "—" */ } })(); return () => { cancelled = true; }; }, [refreshTick]); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setSearchParams({ q: searchInput, service: serviceFilter, tab, page: '1' }); }; const setPage = (p: number) => setSearchParams({ q: query, service: serviceFilter, tab, page: p.toString() }); const setService = (s: string) => setSearchParams({ q: query, service: s, tab, page: '1' }); const setTab = (t: Tab) => setSearchParams({ q: query, service: serviceFilter, tab: t, page: '1' }); const limit = tab === 'creds' ? CREDS_LIMIT : REUSE_LIMIT; const total = tab === 'creds' ? credsTotal : reuseTotal; const totalPages = Math.max(1, Math.ceil(total / limit)); // Service chips derived from visible creds page const services = useMemo(() => { const set = new Set(); creds.forEach(c => set.add(c.service)); return Array.from(set).sort(); }, [creds]); const plaintextCount = creds.filter(c => c.secret_kind === 'plaintext').length; const hashedCount = creds.length - plaintextCount; const openReuseFromCred = async (key: string) => { const hit = reuseMap.get(key); if (!hit) return; try { const res = await api.get(`/credential-reuse/${hit.id}`); setSelectedReuse(res.data as CredentialReuseRow); } catch (err) { console.error('Failed to fetch reuse detail', err); } }; return (

CREDENTIAL VAULT

{tab === 'creds' ? `${credsTotal.toLocaleString()} CAPTURED · ${plaintextCount} PLAINTEXT · ${hashedCount} CHALLENGED` : `${reuseTotal.toLocaleString()} REUSE FINDINGS`}
{tab === 'creds' && (
setSearchInput(e.target.value)} />
{services.map(svc => ( ))}
)}
{tab === 'creds' ? `${credsTotal.toLocaleString()} CREDENTIALS CAPTURED` : `${reuseTotal.toLocaleString()} REUSE FINDINGS`}
Page {page} of {totalPages}
{tab === 'creds' ? ( {creds.length > 0 ? creds.map(c => { const isPlain = c.secret_kind === 'plaintext'; const secretText = isPlain ? (c.secret_printable ?? '—') : truncHash(c.secret_sha256, 16); const key = reuseKey(c.secret_sha256, c.secret_kind, c.principal); const reuseHit = reuseMap.get(key); return ( setSelectedCred(c)}> ); }) : ( )}
LAST SEEN DECKY SVC ATTACKER PRINCIPAL SECRET KIND HITS REUSE
{new Date(c.last_seen).toLocaleTimeString()} {c.decky_name} {c.service} { e.stopPropagation(); navigate(`/attackers?q=${encodeURIComponent(c.attacker_ip)}`); }} > {c.attacker_ip} {c.principal ?? } {secretText} {c.secret_kind.toUpperCase()} {c.attempt_count} {reuseHit ? ( { e.stopPropagation(); openReuseFromCred(key); }} > ×{reuseHit.target_count} ) : ( )}
) : ( {reuseRows.length > 0 ? reuseRows.map(r => { const isPlain = r.secret_kind === 'plaintext'; const moreDeckies = Math.max(0, r.deckies.length - 3); const moreServices = Math.max(0, r.services.length - 3); return ( setSelectedReuse(r)}> ); }) : ( )}
LAST SEEN PRINCIPAL KIND TARGETS ATTEMPTS DECKIES SERVICES
{new Date(r.last_seen).toLocaleTimeString()} {r.principal ?? } {r.secret_kind.toUpperCase()} {r.target_count} {r.attempt_count} {r.deckies.slice(0, 3).map(d => ( {d} ))} {moreDeckies > 0 && +{moreDeckies}} {r.services.slice(0, 3).map(s => ( {s} ))} {moreServices > 0 && +{moreServices}}
)}
{selectedCred && ( setSelectedCred(null)} onSelectAttacker={(ip) => { setSelectedCred(null); navigate(`/attackers?q=${encodeURIComponent(ip)}`); }} /> )} {selectedReuse && ( setSelectedReuse(null)} /> )}
); }; export default Credentials;