+ {/* 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}
+ )}
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/ThreatActorsPage.tsx b/frontend/src/pages/ThreatActorsPage.tsx
new file mode 100644
index 0000000..65e6383
--- /dev/null
+++ b/frontend/src/pages/ThreatActorsPage.tsx
@@ -0,0 +1,246 @@
+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 (
+
+ {/* Header */}
+
+
+
+ Threat Actors
+
+
+ APT groups and threat actor profiles from MITRE ATT&CK with coverage analysis
+
+
+
+ {/* Filters */}
+
+ {/* Search */}
+
+
+ { setSearch(e.target.value); setPage(0); }}
+ className="w-full rounded-lg border border-gray-700 bg-gray-800 py-2 pl-10 pr-4 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
+ />
+
+
+ {/* Motivation filter */}
+
+
+
+ {/* Loading */}
+ {isLoading && (
+
+
+
+ )}
+
+ {/* Error */}
+ {error && (
+
+
+
+ Failed to load threat actors: {(error as Error)?.message}
+
+
+ )}
+
+ {/* Grid */}
+ {data && data.items.length > 0 && (
+ <>
+
+ {data.items.map((actor: ThreatActorSummary) => (
+
+ ))}
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+ Showing {page * limit + 1}–{Math.min((page + 1) * limit, data.total)} of{" "}
+ {data.total}
+
+
+
+
+ Page {page + 1} of {totalPages}
+
+
+
+
+ )}
+ >
+ )}
+
+ {/* Empty */}
+ {data && data.items.length === 0 && (
+
+
+
No Threat Actors Found
+
+ Import threat actors from MITRE CTI via the Data Sources panel.
+
+
+ )}
+
+ );
+}