fix(tests): apply user edits when creating test from template
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

The form captured name/description/platform/procedure/tool edits but
never sent them — the created test always used the raw template values.

- TestTemplateInstantiate schema: add optional override fields
  (name, description, platform, procedure_text, tool_used)
- create_test_from_template service: accept *_override kwargs;
  use override value when provided, fall back to template value
- Router: pass all override fields from payload to service
- Frontend API createTestFromTemplate: accept overrides object, spread into body
- TestFromTemplateForm: pass all form state values as overrides

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-28 16:38:40 +02:00
parent fa8e7f311b
commit b248c2816e
5 changed files with 45 additions and 8 deletions

View File

@@ -185,6 +185,11 @@ def create_test_from_template(
template_id=payload.template_id, template_id=payload.template_id,
technique_id_or_mitre=payload.technique_id, technique_id_or_mitre=payload.technique_id,
creator_id=current_user.id, creator_id=current_user.id,
name_override=payload.name,
description_override=payload.description,
platform_override=payload.platform,
procedure_text_override=payload.procedure_text,
tool_used_override=payload.tool_used,
) )
log_action( log_action(
db, db,

View File

@@ -72,7 +72,17 @@ class TestTemplateSummary(BaseModel):
class TestTemplateInstantiate(BaseModel): class TestTemplateInstantiate(BaseModel):
"""Payload to create a real test from an existing template.""" """Payload to create a real test from an existing template.
Optional override fields take precedence over the template values when provided.
"""
template_id: uuid.UUID template_id: uuid.UUID
technique_id: str # accepts both UUID and MITRE ID (e.g. "T1059.001") technique_id: str # accepts both UUID and MITRE ID (e.g. "T1059.001")
# User-editable overrides (if omitted the template value is used)
name: str | None = None
description: str | None = None
platform: str | None = None
procedure_text: str | None = None
tool_used: str | None = None

View File

@@ -93,10 +93,17 @@ def create_test_from_template(
template_id: uuid.UUID, template_id: uuid.UUID,
technique_id_or_mitre: str, technique_id_or_mitre: str,
creator_id: uuid.UUID, creator_id: uuid.UUID,
# Optional user-edited overrides (take priority over template values)
name_override: str | None = None,
description_override: str | None = None,
platform_override: str | None = None,
procedure_text_override: str | None = None,
tool_used_override: str | None = None,
) -> Test: ) -> Test:
"""Instantiate a Test from a TestTemplate. """Instantiate a Test from a TestTemplate.
technique_id_or_mitre can be a UUID string or MITRE ID (e.g. T1059.001). technique_id_or_mitre can be a UUID string or MITRE ID (e.g. T1059.001).
Override fields, when provided, take precedence over the template's values.
Raises EntityNotFoundError if template or technique not found. Raises EntityNotFoundError if template or technique not found.
Does not commit; caller uses UnitOfWork. Does not commit; caller uses UnitOfWork.
""" """
@@ -121,11 +128,11 @@ def create_test_from_template(
test = Test( test = Test(
technique_id=technique.id, technique_id=technique.id,
name=template.name, name=name_override if name_override is not None else template.name,
description=template.description, description=description_override if description_override is not None else template.description,
platform=template.platform, platform=platform_override if platform_override is not None else template.platform,
procedure_text=template.attack_procedure, procedure_text=procedure_text_override if procedure_text_override is not None else template.attack_procedure,
tool_used=template.tool_suggested, tool_used=tool_used_override if tool_used_override is not None else template.tool_suggested,
remediation_steps=template.suggested_remediation, remediation_steps=template.suggested_remediation,
created_by=creator_id, created_by=creator_id,
state=TestState.draft, state=TestState.draft,

View File

@@ -91,14 +91,22 @@ export async function createTest(payload: TestCreatePayload): Promise<Test> {
return data; return data;
} }
/** Create a test from an existing template. */ /** Create a test from an existing template, with optional field overrides. */
export async function createTestFromTemplate( export async function createTestFromTemplate(
templateId: string, templateId: string,
techniqueId: string, techniqueId: string,
overrides?: {
name?: string;
description?: string;
platform?: string;
procedure_text?: string;
tool_used?: string;
},
): Promise<Test> { ): Promise<Test> {
const { data } = await client.post<Test>("/tests/from-template", { const { data } = await client.post<Test>("/tests/from-template", {
template_id: templateId, template_id: templateId,
technique_id: techniqueId, technique_id: techniqueId,
...overrides,
}); });
return data; return data;
} }

View File

@@ -68,7 +68,14 @@ export default function TestFromTemplateForm({
// ── Submit ───────────────────────────────────────────────────── // ── Submit ─────────────────────────────────────────────────────
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: () => createTestFromTemplate(templateId, technique), mutationFn: () =>
createTestFromTemplate(templateId, technique, {
name: name.trim() || undefined,
description: description.trim() || undefined,
platform: platform.trim() || undefined,
procedure_text: procedureText.trim() || undefined,
tool_used: toolUsed.trim() || undefined,
}),
onSuccess: (test) => { onSuccess: (test) => {
navigate(`/tests/${test.id}`); navigate(`/tests/${test.id}`);
}, },