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
- 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.
65 lines
3.0 KiB
Python
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")
|