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
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user