feat(campaigns): campaign start date — scheduled activation, Jira start_date
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

DB: migration b047 adds start_date (DateTime nullable) + index to campaigns.

Backend:
- Campaign model: start_date field
- CampaignCreate/Update schemas: accept start_date (ISO string)
- CRUD service: persist + serialize start_date in both serializers
- Activation endpoint: blocks manual activation if start_date is in the future
  (campaign will auto-activate via scheduler)
- Scheduler: new hourly job _run_scheduled_campaign_activation — finds draft
  campaigns with start_date <= now, activates them, creates Jira tickets,
  notifies red_tech team
- Jira: campaign + test tickets now include JIRA_START_DATE_FIELD (configurable,
  default customfield_10015). Campaign uses start_date if set, else created_at.
  Tests inherit campaign start_date.
- config.py: JIRA_START_DATE_FIELD setting

Frontend:
- Campaign type: start_date field on Campaign + CampaignSummary
- CampaignCreatePayload: start_date optional field
- Create form: date picker with min=today, warning message explaining behavior
- Campaign detail header: start_date badge showing days remaining or started date

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-06-03 16:57:06 +02:00
parent 3db9809be5
commit c62dafbc1f
10 changed files with 218 additions and 2 deletions

View File

@@ -72,6 +72,7 @@ def serialize_campaign(db: Session, campaign: Campaign) -> dict:
"threat_actor_id": str(campaign.threat_actor_id) if campaign.threat_actor_id else None,
"threat_actor_name": actor.name if actor else None,
"created_by": str(campaign.created_by) if campaign.created_by else None,
"start_date": campaign.start_date.isoformat() if campaign.start_date else None,
"scheduled_at": campaign.scheduled_at.isoformat() if campaign.scheduled_at else None,
"completed_at": campaign.completed_at.isoformat() if campaign.completed_at else None,
"target_platform": campaign.target_platform,
@@ -100,6 +101,7 @@ def serialize_campaign_summary(db: Session, campaign: Campaign) -> dict:
"status": campaign.status,
"threat_actor_id": str(campaign.threat_actor_id) if campaign.threat_actor_id else None,
"threat_actor_name": actor.name if actor else None,
"start_date": campaign.start_date.isoformat() if campaign.start_date else None,
"target_platform": campaign.target_platform,
"tags": campaign.tags or [],
"created_at": campaign.created_at.isoformat() if campaign.created_at else None,
@@ -160,6 +162,7 @@ def create_campaign(
target_platform: Optional[str] = None,
tags: Optional[list[str]] = None,
scheduled_at: Optional[str] = None,
start_date: Optional[str] = None,
) -> dict:
"""Create a new campaign. Does not commit; caller commits."""
campaign = Campaign(
@@ -171,6 +174,7 @@ def create_campaign(
tags=tags or [],
created_by=creator_id,
scheduled_at=datetime.fromisoformat(scheduled_at) if scheduled_at else None,
start_date=datetime.fromisoformat(start_date) if start_date else None,
)
db.add(campaign)
db.flush()
@@ -213,6 +217,8 @@ def update_campaign(
if "scheduled_at" in fields and fields["scheduled_at"]:
fields["scheduled_at"] = datetime.fromisoformat(fields["scheduled_at"])
if "start_date" in fields and fields["start_date"]:
fields["start_date"] = datetime.fromisoformat(fields["start_date"])
for field, value in fields.items():
setattr(campaign, field, value)

View File

@@ -407,6 +407,11 @@ def auto_create_campaign_issue(
"customfield_10011": campaign.name,
}
# Set start date: use campaign.start_date if set, otherwise today
effective_start = campaign.start_date or campaign.created_at
if effective_start:
fields[settings.JIRA_START_DATE_FIELD] = effective_start.strftime("%Y-%m-%d")
# Nest under the configured parent ticket (Initiative, e.g. OFS-20795)
if parent_ticket:
fields["parent"] = {"key": parent_ticket}
@@ -446,6 +451,7 @@ def auto_create_test_issue(
*,
technique: Optional[Technique] = None,
parent_ticket_override: Optional[str] = None,
campaign_start_date=None, # datetime | None — inherited from campaign when available
) -> Optional[str]:
"""Create a Jira issue for *test* and store the link.
@@ -496,6 +502,12 @@ def auto_create_test_issue(
"customfield_10309": f"{{code}}{poc}{{code}}",
}
# Inherit campaign start date if available, otherwise use today
from datetime import date as _date
effective_start = campaign_start_date or _date.today()
if hasattr(effective_start, "strftime"):
fields[settings.JIRA_START_DATE_FIELD] = effective_start.strftime("%Y-%m-%d")
if parent:
fields["parent"] = {"key": parent}