feat(tests): archive round history on reopen, stop pausing from setting Jira On Hold
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

- Reopening a test for rework (red or blue) now archives the round that's
  ending into a new test_round_history table before resetting the live
  fields — procedure/results/detection data is preserved instead of
  overwritten, and Phase Timeline renders every past round alongside the
  current one, so Blue Team keeps visibility into earlier Red attempts.
- Each archived round also posts a permanent Jira comment (push_round_archived)
  since Jira's custom fields only ever show the latest value once the next
  round resubmits — the comment thread is what preserves full history there.
- push_pause_event no longer transitions the Jira issue to "On Hold" — a
  timer pause is not a real Hold, and flipping Jira status for it was
  misleading. Only the explicit Hold action (push_hold_event) does that now.
This commit is contained in:
kitos
2026-07-13 12:08:45 +02:00
parent 0bb34f6834
commit 8fe38dc661
12 changed files with 526 additions and 26 deletions
+42
View File
@@ -101,6 +101,36 @@ class TestBlueUpdate(BaseModel):
blue_summary: str | None = None
# ── Round history (archived on reopen) ─────────────────────────────
class TestRoundHistoryOut(BaseModel):
"""One archived round snapshot, taken right before a reopen resets the live fields."""
id: uuid.UUID
team: str
round_number: int
started_at: datetime | None = None
ended_at: datetime | None = None
paused_seconds: int = 0
procedure_text: str | None = None
tool_used: str | None = None
attack_success: AttackSuccessResult | None = None
red_summary: str | None = None
execution_start_time: datetime | None = None
execution_end_time: datetime | None = None
detection_result: TestResult | None = None
containment_result: ContainmentResult | None = None
detection_time: datetime | None = None
containment_time: datetime | None = None
blue_summary: str | None = None
review_notes: str | None = None
reviewed_by: uuid.UUID | None = None
archived_at: datetime | None = None
model_config = ConfigDict(from_attributes=True)
# ── Red Lead validation ────────────────────────────────────────────
@@ -269,6 +299,8 @@ class TestOut(BaseModel):
red_paused_seconds: int = 0
# Assign blue_paused_seconds = 0
blue_paused_seconds: int = 0
red_round_number: int = 1
blue_round_number: int = 1
# Remediation fields
remediation_steps: str | None = None
@@ -313,6 +345,9 @@ class TestOut(BaseModel):
red_evidences: list[EvidenceOut] = []
blue_evidences: list[EvidenceOut] = []
# Archived rounds — full history, oldest first (populated from the ORM relationship)
round_history: list[TestRoundHistoryOut] = []
model_config = ConfigDict(from_attributes=True)
@model_validator(mode="before")
@@ -369,4 +404,11 @@ class TestOut(BaseModel):
obj.__dict__["red_evidences"] = red_evs
obj.__dict__["blue_evidences"] = blue_evs
# Only populate round history when already loaded (via joinedload)
raw_rounds = obj.__dict__.get("round_history")
if raw_rounds is not None:
obj.__dict__["round_history"] = [
TestRoundHistoryOut.model_validate(r) for r in raw_rounds
]
return obj