feat(tests): add Blue detect_procedure field mapped from template suggestion
Blue Team gets a detect_procedure field on Test (what they actually did to detect the attack), Blue's counterpart to Red's procedure_text. It's seeded from a new detect_suggested_procedure field on TestTemplate at test-creation time, so a junior who later reuses the same template starts with prior guidance instead of a blank field. Also adds the procedure_suggestions review table and Test.source_template_id, laying the groundwork for suggesting template improvements from filled-in procedure fields (reviewed and approved by a lead, never auto-written). detect_procedure is archived (not cleared) on Blue reopen, matching blue_summary, and now appears in the Jira round-archived and blue_review comments alongside the existing detection/containment fields.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""Add detect_procedure fields and procedure_suggestions review table.
|
||||
|
||||
Blue Team gets a "Detect Procedure" field (what they actually did to
|
||||
detect the attack) mirroring Red's procedure_text, plus a matching
|
||||
"suggested" field on test_templates. Commands extracted from either
|
||||
side's procedure text are proposed as template improvements via a new
|
||||
procedure_suggestions table, reviewed and approved/rejected by a lead
|
||||
rather than written automatically.
|
||||
|
||||
Revision ID: b062
|
||||
Revises: b061
|
||||
Create Date: 2026-07-14
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision = "b062"
|
||||
down_revision = "b061"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("tests", sa.Column("detect_procedure", sa.Text(), nullable=True))
|
||||
op.add_column(
|
||||
"tests",
|
||||
sa.Column("source_template_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("test_templates.id"), nullable=True),
|
||||
)
|
||||
op.add_column("test_templates", sa.Column("detect_suggested_procedure", sa.Text(), nullable=True))
|
||||
op.add_column("test_round_history", sa.Column("detect_procedure", sa.Text(), nullable=True))
|
||||
|
||||
op.create_table(
|
||||
"procedure_suggestions",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("template_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("test_templates.id"), nullable=False),
|
||||
sa.Column("team", sa.String(10), nullable=False),
|
||||
sa.Column("suggested_text", sa.Text(), nullable=False),
|
||||
sa.Column("source_test_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("tests.id"), nullable=True),
|
||||
sa.Column("submitted_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("status", sa.String(10), nullable=False, server_default="pending"),
|
||||
sa.Column("reviewed_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("reviewed_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_procedure_suggestions_template_id", "procedure_suggestions", ["template_id"])
|
||||
op.create_index("ix_procedure_suggestions_status", "procedure_suggestions", ["status"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_procedure_suggestions_status", table_name="procedure_suggestions")
|
||||
op.drop_index("ix_procedure_suggestions_template_id", table_name="procedure_suggestions")
|
||||
op.drop_table("procedure_suggestions")
|
||||
op.drop_column("test_round_history", "detect_procedure")
|
||||
op.drop_column("test_templates", "detect_suggested_procedure")
|
||||
op.drop_column("tests", "source_template_id")
|
||||
op.drop_column("tests", "detect_procedure")
|
||||
Reference in New Issue
Block a user