From 131817cc81c599bfabbc4f732bca7ab3a9cb7d5c Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 4 Jun 2026 15:45:55 +0200 Subject: [PATCH] feat(threat-actors): Generate Campaign button on actor detail page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Generate Campaign button (purple, visible to leads/admin) in the threat actor header. Opens a modal with: - Actor name shown as context - Start date picker (required — validated: must be today or future) - Warning message showing when tests will be queued - Error display for API failures - On success: redirects to the new campaign detail page Start date is mandatory here (unlike the CampaignsPage flow where it is optional) to enforce scheduling discipline when generating from actors. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/ThreatActorDetailPage.tsx | 188 +++++++++++++++++-- 1 file changed, 175 insertions(+), 13 deletions(-) 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 && ( - - - MITRE ATT&CK - - )} + {/* Action buttons */} +
+ {canGenerate && ( + + )} + {actor.mitre_url && ( + + + MITRE ATT&CK + + )} +
{/* Meta badges */} @@ -563,6 +623,108 @@ export default function ThreatActorDetailPage() { )} + {/* ── Generate Campaign Modal ─────────────────────────────────── */} + {showCampaignModal && ( +
+
+ {/* Header */} +
+
+ +

Generate Campaign

+
+ +
+ +
+ {/* Actor info */} +
+ +
+

Threat actor

+

{actor.name}

+
+
+ + {/* What this does */} +

+ 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 */} +
+ + { + setCampaignStartDate(e.target.value); + if (dateError) setDateError(""); + }} + className={`w-full rounded-lg border px-3 py-2 text-sm text-gray-200 bg-gray-800 focus:outline-none focus:border-purple-500 transition-colors [color-scheme:dark] ${ + dateError ? "border-red-500" : "border-gray-700" + }`} + /> + {dateError && ( +

+ + {dateError} +

+ )} + {campaignStartDate && !dateError && ( +

+ ⏰ Tests will be queued for the team on{" "} + {new Date(campaignStartDate + "T00:00:00").toLocaleDateString("en-GB", { + day: "numeric", month: "long", year: "numeric", + })} +

+ )} +
+ + {/* API error */} + {generateMutation.isError && ( +
+ + {(generateMutation.error as Error)?.message || "Failed to generate campaign"} +
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+ )} ); }