fix(m5): post-review pass — AND filter, advisory lock, N+1, item caps, mutation cache
Spec-reviewer + code-reviewer findings applied:
Must-fix
- Filter combinator AND-semantics: tactic+technique+subtechnique now intersect
(one IN subquery per facet) instead of being pooled into one OR. Reviewers
flagged both the wrong default semantics and the theoretical UUID-collision
risk of pooling tactic/technique/sub UUIDs into a shared list across
three columns.
- Front-end mutation cache hygiene: updateMeta + setTests both
`onSettled: invalidate` so a partial failure leaves the cache consistent.
Should-fix
- Per-scenario pg_advisory_xact_lock on set_scenario_tests — serialises
concurrent reorders, mirrors M4 /mitre/sync pattern.
- Backend/front consistency on duplicate tests in a scenario: the
UNIQUE(scenario_id, position) constraint already allows the same
test_template multiple times (chained ops), so the catalogue picker no
longer excludes already-picked items.
Nice-to-have
- N+1 eradicated in test_template view rendering: _to_views_batch
builds {uuid → MitreRow} maps in 3 queries up-front; list endpoint
now issues 4 queries total regardless of list size.
- Wire-level item length caps on tags (64) and expected_iocs (255)
via Annotated[str, StringConstraints(...)] — returns 400 instead of
bubbling up StringDataRightTruncation.
- 4 new pytest covering the AND-filter, extra="forbid" rejection,
empty mitre_tags clearing, and the 65-char tag cap. Total now
81 pytest + 38 e2e pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -145,14 +145,19 @@ export function AdminScenariosPage() {
|
||||
onError: (e) => setError(humanError(e)),
|
||||
});
|
||||
|
||||
// updateMeta and setTests both invalidate on success so a partial failure
|
||||
// (metadata saved, reorder rejected) still leaves the cache consistent
|
||||
// with whichever step landed.
|
||||
const updateMeta = useMutation({
|
||||
mutationFn: ({ id, name, description }: { id: string; name: string; description: string | null }) =>
|
||||
apiPatch<ScenarioTemplate>(`/scenario-templates/${id}`, { name, description }),
|
||||
onSettled: () => qc.invalidateQueries({ queryKey: ['templates', 'scenarios'] }),
|
||||
});
|
||||
|
||||
const setTests = useMutation({
|
||||
mutationFn: ({ id, test_template_ids }: { id: string; test_template_ids: string[] }) =>
|
||||
apiPut<ScenarioTemplate>(`/scenario-templates/${id}/tests`, { test_template_ids }),
|
||||
onSettled: () => qc.invalidateQueries({ queryKey: ['templates', 'scenarios'] }),
|
||||
});
|
||||
|
||||
const remove = useMutation({
|
||||
@@ -223,12 +228,15 @@ export function AdminScenariosPage() {
|
||||
return map;
|
||||
}, [catalogue.data]);
|
||||
|
||||
// Tests available for inclusion (excluding already-picked + soft-deleted).
|
||||
// Tests offered for inclusion. The same test_template MAY appear multiple
|
||||
// times in a scenario (chained operations are a real purple-team pattern,
|
||||
// cf. `scenario_template_tests` UNIQUE on `(scenario_id, position)`, not
|
||||
// on `test_template_id`). So we do NOT exclude already-picked items —
|
||||
// only soft-deleted ones, which the backend would reject.
|
||||
const availableTests = useMemo<TestTemplate[]>(() => {
|
||||
if (!catalogue.data) return [];
|
||||
const picked = new Set(form.test_ids);
|
||||
return catalogue.data.items.filter((t) => !picked.has(t.id) && !t.deleted_at);
|
||||
}, [catalogue.data, form.test_ids]);
|
||||
return catalogue.data.items.filter((t) => !t.deleted_at);
|
||||
}, [catalogue.data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user