diff --git a/frontend/src/pages/ThreatActorDetailPage.tsx b/frontend/src/pages/ThreatActorDetailPage.tsx
index 77b434f..2152c8d 100644
--- a/frontend/src/pages/ThreatActorDetailPage.tsx
+++ b/frontend/src/pages/ThreatActorDetailPage.tsx
@@ -1,7 +1,8 @@
+import { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import MarkdownText from "../components/MarkdownText";
import MotivationBadge from "../components/MotivationBadge";
-import { useQuery } from "@tanstack/react-query";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Loader2,
AlertCircle,
@@ -17,6 +18,9 @@ import {
Clock,
Crosshair,
FlaskConical,
+ Swords,
+ X,
+ Calendar,
} from "lucide-react";
import {
getThreatActor,
@@ -25,6 +29,8 @@ import {
type ThreatActorTechnique,
type GapItem,
} from "../api/threat-actors";
+import { generateCampaignFromThreatActor } from "../api/campaigns";
+import { useAuth } from "../context/AuthContext";
// ── MITRE ATT&CK Tactics in kill chain order ──────────────────────
const TACTICS_ORDER = [
@@ -97,6 +103,44 @@ function statusIcon(status: string | null) {
export default function ThreatActorDetailPage() {
const { actorId } = useParams();
const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const { user } = useAuth();
+
+ const canGenerate =
+ user?.role === "admin" || user?.role === "red_lead" || user?.role === "blue_lead";
+
+ // ── Campaign generation modal state ─────────────────────────────
+ const [showCampaignModal, setShowCampaignModal] = useState(false);
+ const [campaignStartDate, setCampaignStartDate] = useState("");
+ const [dateError, setDateError] = useState("");
+
+ const generateMutation = useMutation({
+ mutationFn: () =>
+ generateCampaignFromThreatActor(actorId!, { start_date: campaignStartDate }),
+ onSuccess: (campaign) => {
+ queryClient.invalidateQueries({ queryKey: ["campaigns"] });
+ setShowCampaignModal(false);
+ setCampaignStartDate("");
+ setDateError("");
+ navigate(`/campaigns/${campaign.id}`);
+ },
+ });
+
+ const handleGenerateSubmit = () => {
+ if (!campaignStartDate) {
+ setDateError("Start date is required");
+ return;
+ }
+ const selected = new Date(campaignStartDate + "T00:00:00");
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ if (selected < today) {
+ setDateError("Start date must be today or in the future");
+ return;
+ }
+ setDateError("");
+ generateMutation.mutate();
+ };
// ── Queries ─────────────────────────────────────────────────────
const { data: actor, isLoading, error } = useQuery({
@@ -196,18 +240,34 @@ export default function ThreatActorDetailPage() {
)}
- {/* MITRE link */}
- {actor.mitre_url && (
-
-
Threat actor
+{actor.name}
++ A campaign will be auto-generated covering all uncovered techniques for this actor, + ordered by kill chain phase. Tests will be hidden from the team queue until the start date. +
+ + {/* Start date — required */} +
+
+ ⏰ Tests will be queued for the team on{" "} + {new Date(campaignStartDate + "T00:00:00").toLocaleDateString("en-GB", { + day: "numeric", month: "long", year: "numeric", + })} +
+ )} +