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:
@@ -0,0 +1,64 @@
|
||||
"""Archive round data on reopen instead of overwriting it.
|
||||
|
||||
Reopening a test for rework used to reset the live procedure/result fields
|
||||
in place, losing the prior round's data (and clobbering the Phase Timeline,
|
||||
which read those same mutable columns). This adds an append-only
|
||||
``test_round_history`` table that snapshots a round's fields right before
|
||||
they get reset, plus per-team round counters on ``tests`` so each round is
|
||||
numbered.
|
||||
|
||||
Revision ID: b061
|
||||
Revises: b060
|
||||
Create Date: 2026-07-13
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision = "b061"
|
||||
down_revision = "b060"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("tests", sa.Column("red_round_number", sa.Integer(), nullable=False, server_default="1"))
|
||||
op.add_column("tests", sa.Column("blue_round_number", sa.Integer(), nullable=False, server_default="1"))
|
||||
|
||||
op.create_table(
|
||||
"test_round_history",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("test_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("tests.id"), nullable=False),
|
||||
sa.Column("team", sa.String(10), nullable=False),
|
||||
sa.Column("round_number", sa.Integer(), nullable=False),
|
||||
sa.Column("started_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("ended_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("paused_seconds", sa.Integer(), nullable=True),
|
||||
sa.Column("procedure_text", sa.Text(), nullable=True),
|
||||
sa.Column("tool_used", sa.String(), nullable=True),
|
||||
# Plain strings, not the shared Postgres enum types tests.* uses —
|
||||
# this is an archive table with no DB-level constraint needs, and
|
||||
# reusing those native type names here would make Alembic try to
|
||||
# (re)create them.
|
||||
sa.Column("attack_success", sa.String(), nullable=True),
|
||||
sa.Column("red_summary", sa.Text(), nullable=True),
|
||||
sa.Column("execution_start_time", sa.DateTime(), nullable=True),
|
||||
sa.Column("execution_end_time", sa.DateTime(), nullable=True),
|
||||
sa.Column("detection_result", sa.String(), nullable=True),
|
||||
sa.Column("containment_result", sa.String(), nullable=True),
|
||||
sa.Column("detection_time", sa.DateTime(), nullable=True),
|
||||
sa.Column("containment_time", sa.DateTime(), nullable=True),
|
||||
sa.Column("blue_summary", sa.Text(), nullable=True),
|
||||
sa.Column("review_notes", sa.Text(), nullable=True),
|
||||
sa.Column("reviewed_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("archived_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_test_round_history_test_id", "test_round_history", ["test_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_test_round_history_test_id", table_name="test_round_history")
|
||||
op.drop_table("test_round_history")
|
||||
op.drop_column("tests", "blue_round_number")
|
||||
op.drop_column("tests", "red_round_number")
|
||||
Reference in New Issue
Block a user