import React, { useEffect, useRef, useState } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { Search, ChevronLeft, ChevronRight, Users } from '../icons'; import api from '../utils/api'; import EmptyState from './EmptyState/EmptyState'; import { useFocusSearch } from '../hooks/useFocusSearch'; import './Dashboard.css'; import './Attackers.css'; interface AttackerEntry { 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: any[]; country_code: string | null; country_source: string | null; asn: number | null; as_name: string | null; asn_source: string | null; updated_at: string; } // Activity thresholds — tune here to adjust tier resolution. const ACTIVE_MIN_EVENTS = 50; const ACTIVE_MAX_AGE_MIN = 60; const PASSIVE_MIN_EVENTS = 5; const PASSIVE_MAX_AGE_HR = 24; type ActivityTier = 'active' | 'passive' | 'inactive'; function deriveActivity(a: AttackerEntry): ActivityTier { const ageMin = (Date.now() - new Date(a.last_seen).getTime()) / 60000; if (a.event_count >= ACTIVE_MIN_EVENTS && ageMin <= ACTIVE_MAX_AGE_MIN) return 'active'; if (a.event_count >= PASSIVE_MIN_EVENTS && ageMin <= PASSIVE_MAX_AGE_HR * 60) return 'passive'; return 'inactive'; } function timeAgo(dateStr: string): string { 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`; const days = Math.floor(hrs / 24); return `${days}d ago`; } const Attackers: React.FC = () => { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const query = searchParams.get('q') || ''; const sortBy = searchParams.get('sort_by') || 'recent'; const serviceFilter = searchParams.get('service') || ''; const page = parseInt(searchParams.get('page') || '1'); const [attackers, setAttackers] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [searchInput, setSearchInput] = useState(query); const searchRef = useRef(null); useFocusSearch(searchRef); const limit = 50; const fetchAttackers = async () => { setLoading(true); try { const offset = (page - 1) * limit; let url = `/attackers?limit=${limit}&offset=${offset}&sort_by=${sortBy}`; if (query) url += `&search=${encodeURIComponent(query)}`; if (serviceFilter) url += `&service=${encodeURIComponent(serviceFilter)}`; const res = await api.get(url); setAttackers(res.data.data); setTotal(res.data.total); } catch (err) { console.error('Failed to fetch attackers', err); } finally { setLoading(false); } }; useEffect(() => { fetchAttackers(); }, [query, sortBy, serviceFilter, page]); useEffect(() => { setSearchInput(query); }, [query]); const _params = (overrides: Record = {}) => { const base: Record = { q: query, sort_by: sortBy, service: serviceFilter, page: '1' }; return Object.fromEntries(Object.entries({ ...base, ...overrides }).filter(([, v]) => v !== '')); }; const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setSearchParams(_params({ q: searchInput })); }; const setPage = (p: number) => setSearchParams(_params({ page: p.toString() })); const setSort = (s: string) => setSearchParams(_params({ sort_by: s })); const clearService = () => setSearchParams(_params({ service: '' })); const totalPages = Math.max(1, Math.ceil(total / limit)); const activityCounts = attackers.reduce( (acc, a) => { acc[deriveActivity(a)]++; return acc; }, { active: 0, passive: 0, inactive: 0 } as Record, ); return (

ATTACKERS

{total.toLocaleString()} UNIQUE SOURCES · {activityCounts.active} ACTIVE · {activityCounts.passive} PASSIVE · {activityCounts.inactive} INACTIVE
setSearchInput(e.target.value)} />
SOURCE INTEL {serviceFilter && ( )}
Page {page} of {totalPages}
{loading ? ( ) : attackers.length === 0 ? ( ) : (
{attackers.map(a => { const activity = deriveActivity(a); const lastCmd = a.commands.length > 0 ? a.commands[a.commands.length - 1] : null; return (
navigate(`/attackers/${a.uuid}`)} >
{a.ip} {a.country_code && ( {a.country_code} )} {activity.toUpperCase()}
First: {new Date(a.first_seen).toLocaleDateString()} Last: {timeAgo(a.last_seen)} {a.asn != null && ( AS{a.asn} )} {a.is_traversal && TRAVERSAL}
EVENTS{a.event_count} BOUNTIES{a.bounty_count} CREDS{a.credential_count}
{a.services.length > 0 && (
{a.services.map(svc => ( { e.stopPropagation(); setSearchParams(_params({ service: svc })); }} > {svc.toUpperCase()} ))}
)} {a.traversal_path ? (
PATH{a.traversal_path}
) : a.deckies.length > 0 ? (
DECKIES{a.deckies.join(', ')}
) : null}
CMDS{a.commands.length} FPS{a.fingerprints.length}
{lastCmd && (
LAST {lastCmd.command}
)}
); })}
)}
); }; export default Attackers;