import { useParams, useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { Loader2, AlertCircle, ArrowLeft, Globe, Target, Shield, ExternalLink, BookOpen, AlertTriangle, CheckCircle, XCircle, Clock, Crosshair, FlaskConical, } from "lucide-react"; import { getThreatActor, getThreatActorCoverage, getThreatActorGaps, type ThreatActorTechnique, type GapItem, } from "../api/threat-actors"; // ── MITRE ATT&CK Tactics in kill chain order ────────────────────── const TACTICS_ORDER = [ "reconnaissance", "resource-development", "initial-access", "execution", "persistence", "privilege-escalation", "defense-evasion", "credential-access", "discovery", "lateral-movement", "collection", "command-and-control", "exfiltration", "impact", ]; /** Status → cell colour. */ function statusCellColor(status: string | null) { switch (status) { case "validated": return "bg-green-500/80 border-green-400/40"; case "partial": return "bg-yellow-500/80 border-yellow-400/40"; case "in_progress": return "bg-blue-500/60 border-blue-400/40"; case "not_covered": return "bg-red-500/70 border-red-400/40"; case "not_evaluated": default: return "bg-gray-700/50 border-gray-600/40"; } } function statusLabel(status: string | null) { switch (status) { case "validated": return "Validated"; case "partial": return "Partial"; case "in_progress": return "In Progress"; case "not_covered": return "Not Covered"; case "review_required": return "Review Required"; case "not_evaluated": default: return "Not Evaluated"; } } function statusIcon(status: string | null) { switch (status) { case "validated": return ; case "partial": return ; case "in_progress": return ; case "not_covered": return ; default: return ; } } export default function ThreatActorDetailPage() { const { actorId } = useParams(); const navigate = useNavigate(); // ── Queries ───────────────────────────────────────────────────── const { data: actor, isLoading, error } = useQuery({ queryKey: ["threat-actor", actorId], queryFn: () => getThreatActor(actorId!), enabled: !!actorId, }); const { data: coverage } = useQuery({ queryKey: ["threat-actor-coverage", actorId], queryFn: () => getThreatActorCoverage(actorId!), enabled: !!actorId, }); const { data: gaps } = useQuery({ queryKey: ["threat-actor-gaps", actorId], queryFn: () => getThreatActorGaps(actorId!), enabled: !!actorId, }); // ── Loading / Error ───────────────────────────────────────────── if (isLoading) { return (
); } if (error || !actor) { return (

{error ? (error as Error)?.message : "Threat actor not found"}

); } // ── Organise techniques by tactic for heatmap ─────────────────── const techniquesByTactic: Record = {}; for (const tech of actor.techniques) { const tactic = tech.tactic || "unknown"; // A technique's tactic field may be comma-separated const tactics = tactic.split(",").map((t) => t.trim().toLowerCase().replace(/\s+/g, "-")); for (const t of tactics) { if (!techniquesByTactic[t]) techniquesByTactic[t] = []; techniquesByTactic[t].push(tech); } } // Sort tactics by kill chain order const orderedTactics = TACTICS_ORDER.filter((t) => techniquesByTactic[t]); const unknownTactics = Object.keys(techniquesByTactic).filter( (t) => !TACTICS_ORDER.includes(t) ); const allTactics = [...orderedTactics, ...unknownTactics]; return (
{/* Back Button */} {/* ── SECTION 1: Header ──────────────────────────────────────── */}

{actor.name}

{actor.mitre_id && ( {actor.mitre_id} )}
{/* Aliases */} {actor.aliases && actor.aliases.length > 0 && (
{actor.aliases.map((alias, i) => ( {alias} ))}
)}
{/* MITRE link */} {actor.mitre_url && ( MITRE ATT&CK )}
{/* Meta badges */}
{actor.country && ( {actor.country} )} {actor.motivation && ( {actor.motivation} )} {actor.sophistication && ( {actor.sophistication} )} {actor.first_seen && ( First seen: {actor.first_seen} )} {actor.last_seen && ( Last seen: {actor.last_seen} )}
{/* Target sectors */} {actor.target_sectors && actor.target_sectors.length > 0 && (
{actor.target_sectors.map((s, i) => ( {s} ))}
)}
{/* ── SECTION 2: Description ─────────────────────────────────── */} {actor.description && (

Description

{actor.description}

)} {/* ── SECTION 3: Coverage Overview ───────────────────────────── */} {coverage && (

Coverage Overview

{coverage.total_techniques}

Total Techniques

{coverage.covered}

Covered

{coverage.total_techniques - coverage.covered}

Gaps

= 80 ? "text-green-400" : coverage.coverage_pct >= 50 ? "text-yellow-400" : "text-red-400" }`}> {coverage.coverage_pct}%

Coverage

{/* Breakdown bar */} {coverage.total_techniques > 0 && (
{coverage.breakdown.validated && (
)} {coverage.breakdown.partial && (
)} {coverage.breakdown.in_progress && (
)} {coverage.breakdown.not_covered && (
)}
{Object.entries(coverage.breakdown).map(([status, count]) => ( {statusIcon(status)} {statusLabel(status)}: {count} ))}
)}
)} {/* ── SECTION 4: Technique Heatmap ───────────────────────────── */} {allTactics.length > 0 && (

Technique Heatmap

{/* Legend */}
Validated Partial In Progress Not Covered Not Evaluated
{/* Heatmap grid — one column per tactic */}
{allTactics.map((tactic) => { const techs = techniquesByTactic[tactic] || []; return (
{/* Tactic header */}
{tactic.replace(/-/g, " ")} ({techs.length})
{/* Technique cells */} {techs.map((tech) => ( ))}
); })}
)} {/* ── SECTION 5: Gap Analysis ────────────────────────────────── */} {gaps && gaps.gaps.length > 0 && (

Coverage Gap Analysis ({gaps.total_gaps} gaps)

{gaps.gaps.map((gap: GapItem) => ( navigate(`/techniques/${gap.mitre_id}`)} > ))}
Technique Tactic Status Templates Tests
{gap.mitre_id} {gap.name}
{gap.tactic || "-"} {statusIcon(gap.status_global)} {statusLabel(gap.status_global)} {gap.has_templates ? ( {gap.available_templates} ) : ( 0 )} {gap.existing_tests > 0 ? ( {gap.existing_tests} ) : ( 0 )}
)} {/* ── SECTION 6: All Techniques List ─────────────────────────── */} {actor.techniques.length > 0 && (

All Techniques ({actor.techniques.length})

{actor.techniques.map((tech: ThreatActorTechnique) => ( navigate(`/techniques/${tech.mitre_id}`)} > ))}
ID Name Tactic Status
{tech.mitre_id} {tech.name} {tech.tactic || "-"} {statusIcon(tech.status_global)} {statusLabel(tech.status_global)}
)} {/* ── References ─────────────────────────────────────────────── */} {actor.references && actor.references.length > 0 && (

References

    {actor.references.map((ref, i) => (
  • {ref.url ? ( {ref.source || ref.url} ) : ( {ref.source} )} {ref.description && ( {ref.description} )}
  • ))}
)}
); }