feat(templates,campaigns): validate MITRE technique IDs, surface template suggestions first, manager can re-approve rejected campaigns
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

- TestTemplate/TemplateSuggestion creation and updates now reject any
  mitre_technique_id that doesn't match a real, already-synced MITRE
  ATT&CK technique. Frontend swaps the free-text ID input for a
  technique picker.
- Test Catalog now shows pending template suggestions before the
  catalog grid, with full field detail (platform, severity, tool,
  atomic ID, source URL, remediation) instead of just name/procedure.
- A manager can edit and directly re-approve a campaign they previously
  rejected, instead of needing the original lead to resubmit it.
This commit is contained in:
kitos
2026-07-16 14:29:01 +02:00
parent 4809c4a662
commit 82ac9c7014
9 changed files with 327 additions and 21 deletions
@@ -417,3 +417,56 @@ def test_lead_created_campaign_is_still_draft_not_auto_approved(api, db, red_lea
)
assert resp.status_code == 201, resp.text
assert resp.json()["status"] == "draft"
def test_manager_can_edit_a_campaign_they_rejected(api, db, red_lead_user, red_lead_headers, manager_headers):
campaign = _make_draft_campaign(db, red_lead_user.id)
api("post", f"/api/v1/campaigns/{campaign.id}/submit", red_lead_headers)
reject = api(
"post", f"/api/v1/campaigns/{campaign.id}/reject", manager_headers,
json={"reason": "Needs a clearer scope"},
)
assert reject.status_code == 200, reject.text
assert reject.json()["status"] == "draft"
resp = api(
"patch", f"/api/v1/campaigns/{campaign.id}", manager_headers,
json={"name": "Edited by manager after rejection"},
)
assert resp.status_code == 200, resp.text
assert resp.json()["name"] == "Edited by manager after rejection"
def test_manager_cannot_edit_a_draft_campaign_that_was_never_rejected(
api, db, red_lead_user, red_lead_headers, manager_headers,
):
campaign = _make_draft_campaign(db, red_lead_user.id)
resp = api(
"patch", f"/api/v1/campaigns/{campaign.id}", manager_headers,
json={"name": "Manager should not be able to do this"},
)
assert resp.status_code == 403
def test_manager_can_approve_a_campaign_they_previously_rejected(
api, db, red_lead_user, red_lead_headers, manager_headers,
):
campaign = _make_draft_campaign(db, red_lead_user.id)
api("post", f"/api/v1/campaigns/{campaign.id}/submit", red_lead_headers)
api(
"post", f"/api/v1/campaigns/{campaign.id}/reject", manager_headers,
json={"reason": "Needs a clearer scope"},
)
api(
"patch", f"/api/v1/campaigns/{campaign.id}", manager_headers,
json={"description": "Scope clarified after rejection"},
)
resp = api(
"post", f"/api/v1/campaigns/{campaign.id}/approve", manager_headers,
json={"start_date": "2020-01-01T00:00:00"},
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["status"] == "active"
assert body["rejection_reason"] is None