refactor(tests): merge detect_suggested_procedure into expected_detection
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
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.
This commit is contained in:
@@ -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))
|
||||||
@@ -41,13 +41,12 @@ class TestTemplate(Base):
|
|||||||
source_url = Column(String, nullable=True)
|
source_url = Column(String, nullable=True)
|
||||||
# Assign attack_procedure = Column(Text, nullable=True) # Suggested attack procedure
|
# Assign attack_procedure = Column(Text, nullable=True) # Suggested attack procedure
|
||||||
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
|
# What blue team should detect — narrative guidance from imports/leads,
|
||||||
expected_detection = Column(Text, nullable=True) # What blue team should detect
|
# plus concrete commands appended via approved procedure suggestions
|
||||||
# Suggested detection procedure — Blue's counterpart to attack_procedure.
|
# (Blue's counterpart to attack_procedure). External syncs never touch
|
||||||
# Only ever filled in via an approved procedure suggestion or a lead
|
# an existing row (they only insert brand-new ones), so anything added
|
||||||
# editing the template directly; external syncs never touch it (those
|
# here is safe across re-syncs.
|
||||||
# only insert brand-new rows, never update existing ones).
|
expected_detection = Column(Text, nullable=True)
|
||||||
detect_suggested_procedure = Column(Text, nullable=True)
|
|
||||||
# Assign platform = Column(String, nullable=True) # windows / linux...
|
# Assign platform = Column(String, nullable=True) # windows / linux...
|
||||||
platform = Column(String, nullable=True) # windows / linux / macos
|
platform = Column(String, nullable=True) # windows / linux / macos
|
||||||
# Assign tool_suggested = Column(String, nullable=True)
|
# Assign tool_suggested = Column(String, nullable=True)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ def _to_out(suggestion, template_by_id: dict) -> ProcedureSuggestionOut:
|
|||||||
if template is not None:
|
if template is not None:
|
||||||
out.template_name = template.name
|
out.template_name = template.name
|
||||||
out.template_current_text = (
|
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
|
return out
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class TestTemplateOut(BaseModel):
|
|||||||
attack_procedure: str | None = None
|
attack_procedure: str | None = None
|
||||||
# Assign expected_detection = None
|
# Assign expected_detection = None
|
||||||
expected_detection: str | None = None
|
expected_detection: str | None = None
|
||||||
detect_suggested_procedure: str | None = None
|
|
||||||
# Assign platform = None
|
# Assign platform = None
|
||||||
platform: str | None = None
|
platform: str | None = None
|
||||||
# Assign tool_suggested = None
|
# Assign tool_suggested = None
|
||||||
@@ -71,7 +70,6 @@ class TestTemplateCreate(BaseModel):
|
|||||||
attack_procedure: str | None = None
|
attack_procedure: str | None = None
|
||||||
# Assign expected_detection = None
|
# Assign expected_detection = None
|
||||||
expected_detection: str | None = None
|
expected_detection: str | None = None
|
||||||
detect_suggested_procedure: str | None = None
|
|
||||||
# Assign platform = None
|
# Assign platform = None
|
||||||
platform: str | None = None
|
platform: str | None = None
|
||||||
# Assign tool_suggested = None
|
# Assign tool_suggested = None
|
||||||
|
|||||||
@@ -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
|
# Which Test field holds the operator's free-text procedure, and which
|
||||||
# TestTemplate field it's proposed as an improvement to, per team.
|
# TestTemplate field it's proposed as an improvement to, per team.
|
||||||
_SOURCE_FIELD = {"red": "procedure_text", "blue": "detect_procedure"}
|
_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"}
|
_LEAD_ROLE = {"red": "red_lead", "blue": "blue_lead"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ def create_test_from_template(
|
|||||||
platform=platform_override if platform_override is not None else template.platform,
|
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,
|
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,
|
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,
|
remediation_steps=template.suggested_remediation,
|
||||||
# Keyword argument: created_by
|
# Keyword argument: created_by
|
||||||
created_by=creator_id,
|
created_by=creator_id,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def template(api, red_lead_headers, technique):
|
|||||||
"mitre_technique_id": "T1059.200",
|
"mitre_technique_id": "T1059.200",
|
||||||
"name": "Suggestion source template",
|
"name": "Suggestion source template",
|
||||||
"attack_procedure": "Run a discovery command on the target.",
|
"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
|
assert resp.status_code == 201, resp.text
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export interface CreateTemplatePayload {
|
|||||||
source_url?: string;
|
source_url?: string;
|
||||||
attack_procedure?: string;
|
attack_procedure?: string;
|
||||||
expected_detection?: string;
|
expected_detection?: string;
|
||||||
detect_suggested_procedure?: string;
|
|
||||||
platform?: string;
|
platform?: string;
|
||||||
tool_suggested?: string;
|
tool_suggested?: string;
|
||||||
severity?: string;
|
severity?: string;
|
||||||
|
|||||||
@@ -221,19 +221,6 @@ export default function TestFromTemplateForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Detect Suggested Procedure (read-only reference for Blue Team) */}
|
|
||||||
<div>
|
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-300">
|
|
||||||
Detect Suggested Procedure
|
|
||||||
<span className="ml-2 text-xs text-gray-500">(read-only reference for Blue Team)</span>
|
|
||||||
</label>
|
|
||||||
<div className="rounded-lg border border-gray-700 bg-gray-800/50 p-3">
|
|
||||||
<p className="whitespace-pre-wrap font-mono text-sm text-gray-400">
|
|
||||||
{template?.detect_suggested_procedure || "No detection procedure suggested yet."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{createMutation.isError && (
|
{createMutation.isError && (
|
||||||
<div className="rounded-lg border border-red-500/30 bg-red-900/20 p-3 text-sm text-red-400">
|
<div className="rounded-lg border border-red-500/30 bg-red-900/20 p-3 text-sm text-red-400">
|
||||||
|
|||||||
@@ -1355,7 +1355,6 @@ function CreateTemplateForm({
|
|||||||
source: "custom",
|
source: "custom",
|
||||||
attack_procedure: "",
|
attack_procedure: "",
|
||||||
expected_detection: "",
|
expected_detection: "",
|
||||||
detect_suggested_procedure: "",
|
|
||||||
platform: "",
|
platform: "",
|
||||||
tool_suggested: "",
|
tool_suggested: "",
|
||||||
severity: "",
|
severity: "",
|
||||||
@@ -1486,22 +1485,8 @@ function CreateTemplateForm({
|
|||||||
<textarea
|
<textarea
|
||||||
value={form.expected_detection || ""}
|
value={form.expected_detection || ""}
|
||||||
onChange={(e) => setForm({ ...form, expected_detection: e.target.value })}
|
onChange={(e) => setForm({ ...form, expected_detection: e.target.value })}
|
||||||
placeholder="What the blue team should detect..."
|
placeholder="What the blue team should detect, and how (commands/queries)..."
|
||||||
rows={2}
|
rows={4}
|
||||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Detect Suggested Procedure */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
|
||||||
Detect Suggested Procedure
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={form.detect_suggested_procedure || ""}
|
|
||||||
onChange={(e) => setForm({ ...form, detect_suggested_procedure: e.target.value })}
|
|
||||||
placeholder="Steps for the blue team to detect this technique..."
|
|
||||||
rows={3}
|
|
||||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
|
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1580,7 +1565,6 @@ function TemplateDetailModal({
|
|||||||
description: template.description ?? "",
|
description: template.description ?? "",
|
||||||
attack_procedure: template.attack_procedure ?? "",
|
attack_procedure: template.attack_procedure ?? "",
|
||||||
expected_detection: template.expected_detection ?? "",
|
expected_detection: template.expected_detection ?? "",
|
||||||
detect_suggested_procedure: template.detect_suggested_procedure ?? "",
|
|
||||||
platform: template.platform ?? "",
|
platform: template.platform ?? "",
|
||||||
tool_suggested: template.tool_suggested ?? "",
|
tool_suggested: template.tool_suggested ?? "",
|
||||||
severity: template.severity ?? "",
|
severity: template.severity ?? "",
|
||||||
@@ -1707,17 +1691,7 @@ function TemplateDetailModal({
|
|||||||
<textarea
|
<textarea
|
||||||
value={form.expected_detection ?? ""}
|
value={form.expected_detection ?? ""}
|
||||||
onChange={(e) => setForm({ ...form, expected_detection: e.target.value })}
|
onChange={(e) => setForm({ ...form, expected_detection: e.target.value })}
|
||||||
rows={2}
|
rows={4}
|
||||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">Detect Suggested Procedure</label>
|
|
||||||
<textarea
|
|
||||||
value={form.detect_suggested_procedure ?? ""}
|
|
||||||
onChange={(e) => setForm({ ...form, detect_suggested_procedure: e.target.value })}
|
|
||||||
rows={3}
|
|
||||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
|
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -230,7 +230,6 @@ export interface TestTemplate {
|
|||||||
source_url: string | null;
|
source_url: string | null;
|
||||||
attack_procedure: string | null;
|
attack_procedure: string | null;
|
||||||
expected_detection: string | null;
|
expected_detection: string | null;
|
||||||
detect_suggested_procedure: string | null;
|
|
||||||
platform: string | null;
|
platform: string | null;
|
||||||
tool_suggested: string | null;
|
tool_suggested: string | null;
|
||||||
severity: string | null;
|
severity: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user