From 60f9464ec596b728626762ddc89db1888327c926 Mon Sep 17 00:00:00 2001 From: kitos Date: Wed, 15 Jul 2026 10:29:13 +0200 Subject: [PATCH] refactor(tests): merge detect_suggested_procedure into expected_detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two overlapping TestTemplate fields (expected_detection: narrative guidance from imports/leads; detect_suggested_procedure: concrete commands from approved suggestions) are now one. Blue-side procedure suggestions target expected_detection directly, appending onto whatever is already there rather than overwriting it — same merge-not-overwrite behavior already used for the red side. Existing detect_suggested_procedure data is folded into expected_detection before the column is dropped. --- ...63_merge_detect_suggested_into_expected.py | 40 +++++++++++++++++++ backend/app/models/test_template.py | 13 +++--- backend/app/routers/procedure_suggestions.py | 2 +- backend/app/schemas/test_template.py | 2 - .../services/procedure_suggestion_service.py | 2 +- backend/app/services/test_crud_service.py | 2 +- backend/tests/test_procedure_suggestions.py | 2 +- frontend/src/api/test-templates.ts | 1 - .../src/components/TestFromTemplateForm.tsx | 13 ------ frontend/src/pages/SystemPage.tsx | 32 ++------------- frontend/src/types/models.ts | 1 - 11 files changed, 53 insertions(+), 57 deletions(-) create mode 100644 backend/alembic/versions/b063_merge_detect_suggested_into_expected.py diff --git a/backend/alembic/versions/b063_merge_detect_suggested_into_expected.py b/backend/alembic/versions/b063_merge_detect_suggested_into_expected.py new file mode 100644 index 0000000..1f36ff8 --- /dev/null +++ b/backend/alembic/versions/b063_merge_detect_suggested_into_expected.py @@ -0,0 +1,40 @@ +"""Merge TestTemplate.detect_suggested_procedure into expected_detection. + +Two overlapping fields on TestTemplate — expected_detection (narrative +guidance, populated by imports and leads since long before this feature) +and detect_suggested_procedure (concrete commands, populated only via the +procedure-suggestion approval workflow) — are consolidated into one: +expected_detection. Any existing detect_suggested_procedure text is +appended (not overwritten) onto expected_detection before the column is +dropped, so nothing already approved is lost. + +Revision ID: b063 +Revises: b062 +Create Date: 2026-07-15 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "b063" +down_revision = "b062" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + UPDATE test_templates + SET expected_detection = CASE + WHEN expected_detection IS NULL OR expected_detection = '' THEN detect_suggested_procedure + ELSE expected_detection || E'\n' || detect_suggested_procedure + END + WHERE detect_suggested_procedure IS NOT NULL AND detect_suggested_procedure != '' + """ + ) + op.drop_column("test_templates", "detect_suggested_procedure") + + +def downgrade() -> None: + op.add_column("test_templates", sa.Column("detect_suggested_procedure", sa.Text(), nullable=True)) diff --git a/backend/app/models/test_template.py b/backend/app/models/test_template.py index d934efc..56af980 100644 --- a/backend/app/models/test_template.py +++ b/backend/app/models/test_template.py @@ -41,13 +41,12 @@ class TestTemplate(Base): source_url = Column(String, nullable=True) # Assign attack_procedure = Column(Text, nullable=True) # Suggested attack procedure attack_procedure = Column(Text, nullable=True) # Suggested attack procedure - # Assign expected_detection = Column(Text, nullable=True) # What blue team should detect - expected_detection = Column(Text, nullable=True) # What blue team should detect - # Suggested detection procedure — Blue's counterpart to attack_procedure. - # Only ever filled in via an approved procedure suggestion or a lead - # editing the template directly; external syncs never touch it (those - # only insert brand-new rows, never update existing ones). - detect_suggested_procedure = Column(Text, nullable=True) + # What blue team should detect — narrative guidance from imports/leads, + # plus concrete commands appended via approved procedure suggestions + # (Blue's counterpart to attack_procedure). External syncs never touch + # an existing row (they only insert brand-new ones), so anything added + # here is safe across re-syncs. + expected_detection = Column(Text, nullable=True) # Assign platform = Column(String, nullable=True) # windows / linux... platform = Column(String, nullable=True) # windows / linux / macos # Assign tool_suggested = Column(String, nullable=True) diff --git a/backend/app/routers/procedure_suggestions.py b/backend/app/routers/procedure_suggestions.py index ea0c8d0..da2f34c 100644 --- a/backend/app/routers/procedure_suggestions.py +++ b/backend/app/routers/procedure_suggestions.py @@ -52,7 +52,7 @@ def _to_out(suggestion, template_by_id: dict) -> ProcedureSuggestionOut: if template is not None: out.template_name = template.name out.template_current_text = ( - template.attack_procedure if suggestion.team == "red" else template.detect_suggested_procedure + template.attack_procedure if suggestion.team == "red" else template.expected_detection ) return out diff --git a/backend/app/schemas/test_template.py b/backend/app/schemas/test_template.py index 98bba32..4ff2830 100644 --- a/backend/app/schemas/test_template.py +++ b/backend/app/schemas/test_template.py @@ -31,7 +31,6 @@ class TestTemplateOut(BaseModel): attack_procedure: str | None = None # Assign expected_detection = None expected_detection: str | None = None - detect_suggested_procedure: str | None = None # Assign platform = None platform: str | None = None # Assign tool_suggested = None @@ -71,7 +70,6 @@ class TestTemplateCreate(BaseModel): attack_procedure: str | None = None # Assign expected_detection = None expected_detection: str | None = None - detect_suggested_procedure: str | None = None # Assign platform = None platform: str | None = None # Assign tool_suggested = None diff --git a/backend/app/services/procedure_suggestion_service.py b/backend/app/services/procedure_suggestion_service.py index 77be721..f829775 100644 --- a/backend/app/services/procedure_suggestion_service.py +++ b/backend/app/services/procedure_suggestion_service.py @@ -28,7 +28,7 @@ from app.services.procedure_extraction_service import extract_commands # Which Test field holds the operator's free-text procedure, and which # TestTemplate field it's proposed as an improvement to, per team. _SOURCE_FIELD = {"red": "procedure_text", "blue": "detect_procedure"} -_TEMPLATE_FIELD = {"red": "attack_procedure", "blue": "detect_suggested_procedure"} +_TEMPLATE_FIELD = {"red": "attack_procedure", "blue": "expected_detection"} _LEAD_ROLE = {"red": "red_lead", "blue": "blue_lead"} diff --git a/backend/app/services/test_crud_service.py b/backend/app/services/test_crud_service.py index c828c3a..ab68044 100644 --- a/backend/app/services/test_crud_service.py +++ b/backend/app/services/test_crud_service.py @@ -364,7 +364,7 @@ def create_test_from_template( platform=platform_override if platform_override is not None else template.platform, procedure_text=procedure_text_override if procedure_text_override is not None else template.attack_procedure, tool_used=tool_used_override if tool_used_override is not None else template.tool_suggested, - detect_procedure=template.detect_suggested_procedure, + detect_procedure=template.expected_detection, remediation_steps=template.suggested_remediation, # Keyword argument: created_by created_by=creator_id, diff --git a/backend/tests/test_procedure_suggestions.py b/backend/tests/test_procedure_suggestions.py index f02e484..45c05fa 100644 --- a/backend/tests/test_procedure_suggestions.py +++ b/backend/tests/test_procedure_suggestions.py @@ -45,7 +45,7 @@ def template(api, red_lead_headers, technique): "mitre_technique_id": "T1059.200", "name": "Suggestion source template", "attack_procedure": "Run a discovery command on the target.", - "detect_suggested_procedure": "Check process creation logs.", + "expected_detection": "Check process creation logs.", }, ) assert resp.status_code == 201, resp.text diff --git a/frontend/src/api/test-templates.ts b/frontend/src/api/test-templates.ts index 3ad7acd..6d5fea5 100644 --- a/frontend/src/api/test-templates.ts +++ b/frontend/src/api/test-templates.ts @@ -24,7 +24,6 @@ export interface CreateTemplatePayload { source_url?: string; attack_procedure?: string; expected_detection?: string; - detect_suggested_procedure?: string; platform?: string; tool_suggested?: string; severity?: string; diff --git a/frontend/src/components/TestFromTemplateForm.tsx b/frontend/src/components/TestFromTemplateForm.tsx index 5e8ba8c..932b8d7 100644 --- a/frontend/src/components/TestFromTemplateForm.tsx +++ b/frontend/src/components/TestFromTemplateForm.tsx @@ -221,19 +221,6 @@ export default function TestFromTemplateForm({ - {/* Detect Suggested Procedure (read-only reference for Blue Team) */} -
- -
-

- {template?.detect_suggested_procedure || "No detection procedure suggested yet."} -

-
-
- {/* Error */} {createMutation.isError && (
diff --git a/frontend/src/pages/SystemPage.tsx b/frontend/src/pages/SystemPage.tsx index 715ee04..0e14304 100644 --- a/frontend/src/pages/SystemPage.tsx +++ b/frontend/src/pages/SystemPage.tsx @@ -1355,7 +1355,6 @@ function CreateTemplateForm({ source: "custom", attack_procedure: "", expected_detection: "", - detect_suggested_procedure: "", platform: "", tool_suggested: "", severity: "", @@ -1486,22 +1485,8 @@ function CreateTemplateForm({