Files
Aegis/backend/app/models/test_round_history.py
kitos 8bdbe48fbe 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.
2026-07-14 14:58:29 +02:00

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