feat(campaigns): start_date for threat-actor-generated campaigns
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Backend:
- campaign_service.generate_campaign_from_threat_actor: accept optional
  start_date kwarg and set it on the Campaign model
- campaigns router: new GenerateFromActorPayload schema, /from-threat-actor
  endpoint now accepts optional body with start_date

Frontend:
- generateCampaignFromThreatActor API: accept optional options param
- Generate Campaign modal: date picker + warning message, same UX as the
  manual create form

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-06-04 13:37:40 +02:00
parent 498536f3f1
commit f8418bc7ea
4 changed files with 48 additions and 3 deletions

View File

@@ -155,8 +155,14 @@ export async function getCampaignProgress(campaignId: string): Promise<CampaignP
}
/** Generate a campaign from a threat actor. */
export async function generateCampaignFromThreatActor(actorId: string): Promise<Campaign> {
const { data } = await client.post<Campaign>(`/campaigns/from-threat-actor/${actorId}`);
export async function generateCampaignFromThreatActor(
actorId: string,
options?: { start_date?: string },
): Promise<Campaign> {
const { data } = await client.post<Campaign>(
`/campaigns/from-threat-actor/${actorId}`,
options ?? {},
);
return data;
}

View File

@@ -49,6 +49,7 @@ export default function CampaignsPage() {
const [showCreateForm, setShowCreateForm] = useState(false);
const [showActorSelector, setShowActorSelector] = useState(false);
const [actorSearch, setActorSearch] = useState("");
const [actorStartDate, setActorStartDate] = useState("");
const [newCampaign, setNewCampaign] = useState({
name: "",
description: "",
@@ -87,10 +88,12 @@ export default function CampaignsPage() {
});
const generateMutation = useMutation({
mutationFn: (actorId: string) => generateCampaignFromThreatActor(actorId),
mutationFn: (actorId: string) =>
generateCampaignFromThreatActor(actorId, actorStartDate ? { start_date: actorStartDate } : undefined),
onSuccess: (campaign) => {
queryClient.invalidateQueries({ queryKey: ["campaigns"] });
setShowActorSelector(false);
setActorStartDate("");
navigate(`/campaigns/${campaign.id}`);
},
});
@@ -289,6 +292,29 @@ export default function CampaignsPage() {
/>
</div>
{/* Start date */}
<div className="mb-4">
<label className="mb-1.5 block text-sm font-medium text-gray-300">
Start date
<span className="ml-2 text-xs font-normal text-gray-500">
(optional — campaign activates automatically on this date)
</span>
</label>
<input
type="date"
value={actorStartDate}
min={new Date().toISOString().split("T")[0]}
onChange={(e) => setActorStartDate(e.target.value)}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none [color-scheme:dark]"
/>
{actorStartDate && (
<p className="mt-1.5 flex items-center gap-1.5 text-xs text-amber-400">
<span>⏰</span>
Tests won't be queued until {new Date(actorStartDate + "T00:00:00").toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" })}
</p>
)}
</div>
{/* Actor list */}
<div className="max-h-72 overflow-y-auto rounded-lg border border-gray-800">
{isLoadingActors ? (