8bdbe48fbe
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.
62 lines
2.8 KiB
Python
62 lines
2.8 KiB
Python
"""SQLAlchemy model for archived test rounds.
|
|
|
|
Each row is a snapshot of one team's round of work (procedure/results for
|
|
Red, detection/containment for Blue) taken right before a lead reopens the
|
|
test for rework. Reopening resets the live fields on ``Test`` for a fresh
|
|
attempt, but the prior attempt's data must not be lost — it's archived here
|
|
so the full history stays visible (e.g. to Blue Team, who need to see what
|
|
Red actually did on earlier rounds, not just the latest one).
|
|
"""
|
|
|
|
import uuid
|
|
|
|
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, Text, func
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from app.database import Base
|
|
from app.models.enums import AttackSuccessResult, ContainmentResult, TestResult
|
|
|
|
|
|
class TestRoundHistory(Base):
|
|
"""Archived snapshot of one red/blue round of a test, taken on reopen."""
|
|
|
|
__tablename__ = "test_round_history"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
test_id = Column(UUID(as_uuid=True), ForeignKey("tests.id"), nullable=False)
|
|
team = Column(String(10), nullable=False) # "red" or "blue"
|
|
round_number = Column(Integer, nullable=False)
|
|
|
|
started_at = Column(DateTime, nullable=True)
|
|
ended_at = Column(DateTime, nullable=True)
|
|
paused_seconds = Column(Integer, default=0)
|
|
|
|
# Red-side round output
|
|
procedure_text = Column(Text, nullable=True)
|
|
tool_used = Column(String, nullable=True)
|
|
# native_enum=False — stored as plain VARCHAR, not the shared Postgres
|
|
# enum type used by tests.attack_success. This is an archive table with
|
|
# no DB-level constraint needs, and reusing the native type name here
|
|
# would make Alembic try to (re)create it.
|
|
attack_success = Column(Enum(AttackSuccessResult, name="test_round_history_attack_success", native_enum=False), nullable=True)
|
|
red_summary = Column(Text, nullable=True)
|
|
execution_start_time = Column(DateTime, nullable=True)
|
|
execution_end_time = Column(DateTime, nullable=True)
|
|
|
|
# Blue-side round output
|
|
detection_result = Column(Enum(TestResult, name="test_round_history_detection_result", native_enum=False), nullable=True)
|
|
containment_result = Column(Enum(ContainmentResult, name="test_round_history_containment_result", native_enum=False), nullable=True)
|
|
detection_time = Column(DateTime, nullable=True)
|
|
containment_time = Column(DateTime, nullable=True)
|
|
blue_summary = Column(Text, nullable=True)
|
|
detect_procedure = Column(Text, nullable=True)
|
|
|
|
# Why the round was closed
|
|
review_notes = Column(Text, nullable=True)
|
|
reviewed_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
|
archived_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
test = relationship("Test", back_populates="round_history")
|
|
reviewer = relationship("User", foreign_keys=[reviewed_by])
|