import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; import { Loader2, AlertCircle, Search, Users, Shield, ChevronLeft, ChevronRight, Globe, Target, Crosshair, } from "lucide-react"; import { getThreatActors, type ThreatActorSummary, type ListThreatActorsParams, } from "../api/threat-actors"; /** Coverage colour based on percentage. */ function coverageColor(pct: number) { if (pct >= 80) return "text-green-400"; if (pct >= 50) return "text-yellow-400"; if (pct >= 20) return "text-orange-400"; return "text-red-400"; } function coverageBg(pct: number) { if (pct >= 80) return "bg-green-500"; if (pct >= 50) return "bg-yellow-500"; if (pct >= 20) return "bg-orange-500"; return "bg-red-500"; } /** Motivation badge colour. */ function motivationColor(m: string | null) { switch (m?.toLowerCase()) { case "espionage": return "border-purple-500/30 bg-purple-900/50 text-purple-400"; case "financial": return "border-yellow-500/30 bg-yellow-900/50 text-yellow-400"; case "destruction": return "border-red-500/30 bg-red-900/50 text-red-400"; case "hacktivism": return "border-cyan-500/30 bg-cyan-900/50 text-cyan-400"; default: return "border-gray-600/30 bg-gray-800/50 text-gray-400"; } } export default function ThreatActorsPage() { const navigate = useNavigate(); const [search, setSearch] = useState(""); const [motivation, setMotivation] = useState(""); const [page, setPage] = useState(0); const limit = 24; const params: ListThreatActorsParams = { offset: page * limit, limit, ...(search ? { search } : {}), ...(motivation ? { motivation } : {}), }; const { data, isLoading, error } = useQuery({ queryKey: ["threat-actors", params], queryFn: () => getThreatActors(params), }); const totalPages = data ? Math.ceil(data.total / limit) : 0; return (
APT groups and threat actor profiles from MITRE ATT&CK with coverage analysis
Failed to load threat actors: {(error as Error)?.message}
Import threat actors from MITRE CTI via the Data Sources panel.