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:
Knacky
2026-05-12 20:05:00 +02:00
parent a559823386
commit ce4bd40551
7 changed files with 267 additions and 56 deletions

View File

@@ -157,24 +157,122 @@ def _resolve_mitre_refs(s: Session, refs: list[MitreTagRef]) -> list[TestTemplat
return rows
def _resolve_mitre_views(s: Session, tags: list[TestTemplateMitreTag]) -> list[MitreTagView]:
"""Batch-resolve polymorphic MITRE FKs into MitreTagViews in 3 queries
total — one per kind — regardless of how many tags or templates the
caller is rendering.
"""
tactic_ids = {t.tactic_id for t in tags if t.mitre_kind == "tactic" and t.tactic_id is not None}
technique_ids = {t.technique_id for t in tags if t.mitre_kind == "technique" and t.technique_id is not None}
sub_ids = {t.subtechnique_id for t in tags if t.mitre_kind == "subtechnique" and t.subtechnique_id is not None}
tactic_map: dict[uuid.UUID, MitreTactic] = {}
technique_map: dict[uuid.UUID, MitreTechnique] = {}
sub_map: dict[uuid.UUID, MitreSubtechnique] = {}
if tactic_ids:
tactic_map = {row.id: row for row in s.scalars(select(MitreTactic).where(MitreTactic.id.in_(tactic_ids))).all()}
if technique_ids:
technique_map = {
row.id: row
for row in s.scalars(select(MitreTechnique).where(MitreTechnique.id.in_(technique_ids))).all()
}
if sub_ids:
sub_map = {
row.id: row
for row in s.scalars(select(MitreSubtechnique).where(MitreSubtechnique.id.in_(sub_ids))).all()
}
views: list[MitreTagView] = []
for tag in tags:
if tag.mitre_kind == "tactic" and tag.tactic_id in tactic_map:
row_t = tactic_map[tag.tactic_id]
views.append(MitreTagView(kind="tactic", external_id=row_t.external_id, name=row_t.name, url=row_t.url))
elif tag.mitre_kind == "technique" and tag.technique_id in technique_map:
row_te = technique_map[tag.technique_id]
views.append(MitreTagView(kind="technique", external_id=row_te.external_id, name=row_te.name, url=row_te.url))
elif tag.mitre_kind == "subtechnique" and tag.subtechnique_id in sub_map:
row_sb = sub_map[tag.subtechnique_id]
views.append(MitreTagView(kind="subtechnique", external_id=row_sb.external_id, name=row_sb.name, url=row_sb.url))
views.sort(key=lambda v: (v.kind, v.external_id))
return views
def _to_views_batch(s: Session, templates: list[TestTemplate]) -> list[TestTemplateView]:
"""List-level batcher: one bulk MITRE resolve for all templates' tags.
For a list of K templates with ~T tags each, this issues 3 queries total
(one per MITRE kind) instead of 3K. We build (kind, uuid) → row maps
once, then assemble each template's view in memory.
"""
tactic_ids: set[uuid.UUID] = set()
technique_ids: set[uuid.UUID] = set()
sub_ids: set[uuid.UUID] = set()
for t in templates:
for tag in t.mitre_tags:
if tag.mitre_kind == "tactic" and tag.tactic_id is not None:
tactic_ids.add(tag.tactic_id)
elif tag.mitre_kind == "technique" and tag.technique_id is not None:
technique_ids.add(tag.technique_id)
elif tag.mitre_kind == "subtechnique" and tag.subtechnique_id is not None:
sub_ids.add(tag.subtechnique_id)
tactic_map: dict[uuid.UUID, MitreTactic] = (
{row.id: row for row in s.scalars(select(MitreTactic).where(MitreTactic.id.in_(tactic_ids))).all()}
if tactic_ids
else {}
)
technique_map: dict[uuid.UUID, MitreTechnique] = (
{row.id: row for row in s.scalars(select(MitreTechnique).where(MitreTechnique.id.in_(technique_ids))).all()}
if technique_ids
else {}
)
sub_map: dict[uuid.UUID, MitreSubtechnique] = (
{row.id: row for row in s.scalars(select(MitreSubtechnique).where(MitreSubtechnique.id.in_(sub_ids))).all()}
if sub_ids
else {}
)
def _views_for(tags: list[TestTemplateMitreTag]) -> list[MitreTagView]:
out: list[MitreTagView] = []
for tag in tags:
if tag.mitre_kind == "tactic" and tag.tactic_id in tactic_map:
row_t = tactic_map[tag.tactic_id]
out.append(MitreTagView(kind="tactic", external_id=row_t.external_id, name=row_t.name, url=row_t.url))
elif tag.mitre_kind == "technique" and tag.technique_id in technique_map:
row_te = technique_map[tag.technique_id]
out.append(MitreTagView(kind="technique", external_id=row_te.external_id, name=row_te.name, url=row_te.url))
elif tag.mitre_kind == "subtechnique" and tag.subtechnique_id in sub_map:
row_sb = sub_map[tag.subtechnique_id]
out.append(MitreTagView(kind="subtechnique", external_id=row_sb.external_id, name=row_sb.name, url=row_sb.url))
out.sort(key=lambda v: (v.kind, v.external_id))
return out
views: list[TestTemplateView] = []
for t in templates:
views.append(
TestTemplateView(
id=t.id,
name=t.name,
description=t.description,
objective=t.objective,
procedure_md=t.procedure_md,
prerequisites_md=t.prerequisites_md,
expected_result_red_md=t.expected_result_red_md,
expected_detection_blue_md=t.expected_detection_blue_md,
opsec_level=t.opsec_level,
tags=list(t.tags or []),
expected_iocs=list(t.expected_iocs or []),
mitre_tags=_views_for(list(t.mitre_tags)),
deleted_at=t.deleted_at,
created_at=t.created_at,
updated_at=t.updated_at,
)
)
return views
def _to_view(s: Session, t: TestTemplate) -> TestTemplateView:
tag_views: list[MitreTagView] = []
for tag in t.mitre_tags:
if tag.mitre_kind == "tactic" and tag.tactic_id is not None:
row = s.get(MitreTactic, tag.tactic_id)
if row is not None:
tag_views.append(MitreTagView(kind="tactic", external_id=row.external_id, name=row.name, url=row.url))
elif tag.mitre_kind == "technique" and tag.technique_id is not None:
row = s.get(MitreTechnique, tag.technique_id)
if row is not None:
tag_views.append(MitreTagView(kind="technique", external_id=row.external_id, name=row.name, url=row.url))
elif tag.mitre_kind == "subtechnique" and tag.subtechnique_id is not None:
row = s.get(MitreSubtechnique, tag.subtechnique_id)
if row is not None:
tag_views.append(
MitreTagView(kind="subtechnique", external_id=row.external_id, name=row.name, url=row.url)
)
tag_views.sort(key=lambda v: (v.kind, v.external_id))
tag_views = _resolve_mitre_views(s, list(t.mitre_tags))
return TestTemplateView(
id=t.id,
name=t.name,
@@ -232,41 +330,43 @@ def list_test_templates(
stmt = stmt.where(TestTemplate.tags.any(tag))
count_stmt = count_stmt.where(TestTemplate.tags.any(tag))
# MITRE facet: resolve external_id → uuid then filter via join subquery.
if tactic or technique or subtechnique:
tag_ids: list[uuid.UUID] = []
if tactic:
tac = s.scalar(select(MitreTactic).where(MitreTactic.external_id == tactic))
if tac is None:
return [], 0
tag_ids.append(tac.id)
if technique:
tech = s.scalar(select(MitreTechnique).where(MitreTechnique.external_id == technique))
if tech is None:
return [], 0
tag_ids.append(tech.id)
if subtechnique:
sub = s.scalar(select(MitreSubtechnique).where(MitreSubtechnique.external_id == subtechnique))
if sub is None:
return [], 0
tag_ids.append(sub.id)
sub_q = (
# MITRE facets: each provided facet (tactic, technique, subtechnique) is
# AND-combined — a template tagged BOTH `TA0006` AND `T1003` matches a
# query with `?tactic=TA0006&technique=T1003`, but a template tagged
# only `TA0006` does NOT. Each facet matches strictly its own column
# (no cross-column UUID collision risk).
def _facet_subquery(column, mitre_id: uuid.UUID):
return (
select(TestTemplateMitreTag.test_template_id)
.where(
or_(
TestTemplateMitreTag.tactic_id.in_(tag_ids),
TestTemplateMitreTag.technique_id.in_(tag_ids),
TestTemplateMitreTag.subtechnique_id.in_(tag_ids),
)
)
.where(column == mitre_id)
.distinct()
)
if tactic:
tac = s.scalar(select(MitreTactic).where(MitreTactic.external_id == tactic))
if tac is None:
return [], 0
sub_q = _facet_subquery(TestTemplateMitreTag.tactic_id, tac.id)
stmt = stmt.where(TestTemplate.id.in_(sub_q))
count_stmt = count_stmt.where(TestTemplate.id.in_(sub_q))
if technique:
tech = s.scalar(select(MitreTechnique).where(MitreTechnique.external_id == technique))
if tech is None:
return [], 0
sub_q = _facet_subquery(TestTemplateMitreTag.technique_id, tech.id)
stmt = stmt.where(TestTemplate.id.in_(sub_q))
count_stmt = count_stmt.where(TestTemplate.id.in_(sub_q))
if subtechnique:
sub = s.scalar(select(MitreSubtechnique).where(MitreSubtechnique.external_id == subtechnique))
if sub is None:
return [], 0
sub_q = _facet_subquery(TestTemplateMitreTag.subtechnique_id, sub.id)
stmt = stmt.where(TestTemplate.id.in_(sub_q))
count_stmt = count_stmt.where(TestTemplate.id.in_(sub_q))
total = s.scalar(count_stmt) or 0
rows = s.scalars(stmt.limit(max(1, min(limit, 500))).offset(max(0, offset))).all()
return [_to_view(s, t) for t in rows], int(total)
return _to_views_batch(s, list(rows)), int(total)
def get_test_template(template_id: uuid.UUID, *, include_deleted: bool = False) -> TestTemplateView: