diff --git a/backend/alembic/versions/b052_add_containment_result.py b/backend/alembic/versions/b052_add_containment_result.py new file mode 100644 index 0000000..89ac866 --- /dev/null +++ b/backend/alembic/versions/b052_add_containment_result.py @@ -0,0 +1,31 @@ +"""Add containment_result to tests. + +Revision ID: b052 +Revises: b051 +Create Date: 2026-06-24 +""" +from alembic import op +import sqlalchemy as sa + +revision = "b052" +down_revision = "b051" +branch_labels = None +depends_on = None + +containment_enum = sa.Enum( + "contained", "partially_contained", "not_contained", + name="containmentresult", +) + + +def upgrade() -> None: + containment_enum.create(op.get_bind(), checkfirst=True) + op.add_column( + "tests", + sa.Column("containment_result", containment_enum, nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("tests", "containment_result") + containment_enum.drop(op.get_bind(), checkfirst=True) diff --git a/backend/app/domain/enums.py b/backend/app/domain/enums.py index 6161db8..b1b3782 100644 --- a/backend/app/domain/enums.py +++ b/backend/app/domain/enums.py @@ -68,6 +68,15 @@ class TestResult(str, enum.Enum): partially_detected = "partially_detected" +# Define class ContainmentResult +class ContainmentResult(str, enum.Enum): + """Outcome of the blue team's containment effort.""" + + contained = "contained" + partially_contained = "partially_contained" + not_contained = "not_contained" + + # Define class DataClassification class DataClassification(str, enum.Enum): """Data sensitivity classification levels for compliance and retention policies.""" diff --git a/backend/app/models/enums.py b/backend/app/models/enums.py index 909110d..fd302ce 100644 --- a/backend/app/models/enums.py +++ b/backend/app/models/enums.py @@ -7,6 +7,7 @@ working with ``from app.models.enums import ...``. # Import # noqa: F401 from app.domain.enums from app.domain.enums import ( # noqa: F401 + ContainmentResult, DataClassification, TeamSide, TechniqueStatus, diff --git a/backend/app/models/test.py b/backend/app/models/test.py index 77f65cd..928ba29 100644 --- a/backend/app/models/test.py +++ b/backend/app/models/test.py @@ -26,8 +26,8 @@ from sqlalchemy.orm import relationship # Import Base from app.database from app.database import Base -# Import TestResult, TestState from app.models.enums -from app.models.enums import TestResult, TestState +# Import ContainmentResult, TestResult, TestState from app.models.enums +from app.models.enums import ContainmentResult, TestResult, TestState # Define class Test @@ -83,6 +83,7 @@ class Test(Base): blue_summary = Column(Text, nullable=True) # Assign detection_result = Column(Enum(TestResult, name="testresult"), nullable=True) detection_result = Column(Enum(TestResult, name="testresult"), nullable=True) + containment_result = Column(Enum(ContainmentResult, name="containmentresult"), nullable=True) # Assign blue_validated_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) blue_validated_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) # Assign blue_validated_at = Column(DateTime, nullable=True) diff --git a/backend/app/schemas/test.py b/backend/app/schemas/test.py index 3e030b1..e3bd95d 100644 --- a/backend/app/schemas/test.py +++ b/backend/app/schemas/test.py @@ -9,7 +9,7 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, model_validator # Import DataClassification from app.domain.enums -from app.domain.enums import DataClassification +from app.domain.enums import ContainmentResult, DataClassification from app.models.enums import TestResult, TestState from app.schemas.evidence import EvidenceOut @@ -92,6 +92,7 @@ class TestBlueUpdate(BaseModel): # Assign detection_result = None detection_result: TestResult | None = None + containment_result: ContainmentResult | None = None # Assign blue_summary = None blue_summary: str | None = None @@ -207,6 +208,7 @@ class TestOut(BaseModel): blue_summary: str | None = None # Assign detection_result = None detection_result: TestResult | None = None + containment_result: ContainmentResult | None = None # Assign blue_validated_by = None blue_validated_by: uuid.UUID | None = None # Assign blue_validated_at = None diff --git a/frontend/src/api/tests.ts b/frontend/src/api/tests.ts index 8eff4b9..ad8db99 100644 --- a/frontend/src/api/tests.ts +++ b/frontend/src/api/tests.ts @@ -2,6 +2,7 @@ import client from "./client"; import type { Test, TestResult, + ContainmentResult, TestState, TestTimelineEntry, } from "../types/models"; @@ -37,6 +38,7 @@ export interface RedUpdatePayload { export interface BlueUpdatePayload { detection_result?: TestResult; + containment_result?: ContainmentResult; blue_summary?: string; } diff --git a/frontend/src/components/test-detail/TeamTabs.tsx b/frontend/src/components/test-detail/TeamTabs.tsx index ca2b4f6..8e8a665 100644 --- a/frontend/src/components/test-detail/TeamTabs.tsx +++ b/frontend/src/components/test-detail/TeamTabs.tsx @@ -17,6 +17,7 @@ import { import type { Test, TestResult, + ContainmentResult, TeamSide, Evidence, TestTimelineEntry, @@ -59,6 +60,14 @@ const DETECTION_RESULTS: { value: TestResult; label: string; color: string }[] = }, ]; +// ── Containment result options ───────────────────────────────────── + +const CONTAINMENT_RESULTS: { value: ContainmentResult; label: string; color: string }[] = [ + { value: "contained", label: "Contained", color: "border-green-500 bg-green-500/10 text-green-400" }, + { value: "partially_contained", label: "Partially Contained", color: "border-yellow-500 bg-yellow-500/10 text-yellow-400" }, + { value: "not_contained", label: "Not Contained", color: "border-red-500 bg-red-500/10 text-red-400" }, +]; + // ── Props ────────────────────────────────────────────────────────── interface TeamTabsProps { @@ -80,6 +89,7 @@ interface TeamTabsProps { onBlueFieldChange: (field: string, value: string) => void; blueDraft: { detection_result: TestResult | ""; + containment_result: ContainmentResult | ""; blue_summary: string; }; @@ -339,6 +349,53 @@ export default function TeamTabs({ )} + {/* Containment Result */} +
+ {test.containment_result ? ( + + {test.containment_result.replace(/_/g, " ")} + + ) : test.detection_result === "not_detected" ? ( + N/A + ) : ( + Not evaluated yet + )} +
+ )} +