Files
Aegis/backend/alembic/versions/b061_test_round_history.py
kitos 8fe38dc661
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
feat(tests): archive round history on reopen, stop pausing from setting Jira On Hold
- 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.
2026-07-13 12:08:45 +02:00

65 lines
3.0 KiB
Python

"""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")