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)
| Technique |
Tactic |
Status |
Templates |
Tests |
{gaps.gaps.map((gap: GapItem) => (
navigate(`/techniques/${gap.mitre_id}`)}
>
|
{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})
| ID |
Name |
Tactic |
Status |
{actor.techniques.map((tech: ThreatActorTechnique) => (
navigate(`/techniques/${tech.mitre_id}`)}
>
|
{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}
)}
))}
)}
);
}