diff --git a/backend/app/routers/campaigns.py b/backend/app/routers/campaigns.py index 669e712..f31071e 100644 --- a/backend/app/routers/campaigns.py +++ b/backend/app/routers/campaigns.py @@ -395,9 +395,14 @@ def get_campaign_progress_endpoint( # POST /campaigns/from-threat-actor/{actor_id} — Auto-generate campaign # --------------------------------------------------------------------------- +class GenerateFromActorPayload(BaseModel): + start_date: Optional[str] = None # ISO date YYYY-MM-DD + + @router.post("/from-threat-actor/{actor_id}", status_code=201) def generate_campaign_from_actor( actor_id: str, + payload: GenerateFromActorPayload = GenerateFromActorPayload(), db: Session = Depends(get_db), current_user: User = Depends(require_any_role("red_lead", "blue_lead")), ): @@ -406,10 +411,14 @@ def generate_campaign_from_actor( Creates tests from the best available templates and orders them by kill chain phase. """ + start_date_parsed = ( + datetime.fromisoformat(payload.start_date) if payload.start_date else None + ) campaign = generate_campaign_from_threat_actor( db, uuid.UUID(actor_id), current_user, + start_date=start_date_parsed, ) with UnitOfWork(db) as uow: diff --git a/backend/app/services/campaign_service.py b/backend/app/services/campaign_service.py index e1b27c7..cb2b0dc 100644 --- a/backend/app/services/campaign_service.py +++ b/backend/app/services/campaign_service.py @@ -7,6 +7,7 @@ threat actors, and progress calculation. import logging import uuid from datetime import datetime +from typing import Optional from sqlalchemy.orm import Session @@ -106,6 +107,8 @@ def generate_campaign_from_threat_actor( db: Session, actor_id: uuid.UUID, user: User, + *, + start_date: Optional[datetime] = None, ) -> Campaign: """Auto-generate a campaign from a threat actor's uncovered techniques. @@ -146,6 +149,7 @@ def generate_campaign_from_threat_actor( status="draft", created_by=user.id, tags=[actor.name, "auto-generated"], + start_date=start_date, ) db.add(campaign) db.flush() # Get campaign.id diff --git a/frontend/src/api/campaigns.ts b/frontend/src/api/campaigns.ts index 69d200b..701f8d0 100644 --- a/frontend/src/api/campaigns.ts +++ b/frontend/src/api/campaigns.ts @@ -155,8 +155,14 @@ export async function getCampaignProgress(campaignId: string): Promise { - const { data } = await client.post(`/campaigns/from-threat-actor/${actorId}`); +export async function generateCampaignFromThreatActor( + actorId: string, + options?: { start_date?: string }, +): Promise { + const { data } = await client.post( + `/campaigns/from-threat-actor/${actorId}`, + options ?? {}, + ); return data; } diff --git a/frontend/src/pages/CampaignsPage.tsx b/frontend/src/pages/CampaignsPage.tsx index 8a204e2..74de7d1 100644 --- a/frontend/src/pages/CampaignsPage.tsx +++ b/frontend/src/pages/CampaignsPage.tsx @@ -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() { /> + {/* Start date */} +
+ + 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 && ( +

+ + Tests won't be queued until {new Date(actorStartDate + "T00:00:00").toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" })} +

+ )} +
+ {/* Actor list */}
{isLoadingActors ? (