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:
@@ -12,11 +12,18 @@ import uuid
|
||||
from typing import Any
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from pydantic import BaseModel, Field, StringConstraints, ValidationError
|
||||
from typing import Annotated
|
||||
|
||||
from app.core.auth_decorators import require_auth, require_perm
|
||||
from app.services import test_templates as svc
|
||||
|
||||
# Tag and IOC entries are stored as PG ARRAY(String(N)). Cap items at the wire
|
||||
# layer so over-sized inputs return 400 with a useful message rather than the
|
||||
# bare StringDataRightTruncation from the driver.
|
||||
TagStr = Annotated[str, StringConstraints(min_length=1, max_length=64)]
|
||||
IocStr = Annotated[str, StringConstraints(min_length=1, max_length=255)]
|
||||
|
||||
bp = Blueprint("test_templates", __name__, url_prefix="/test-templates")
|
||||
log = logging.getLogger("metamorph.api.test_templates")
|
||||
|
||||
@@ -40,8 +47,8 @@ class CreateTestTemplatePayload(BaseModel):
|
||||
expected_result_red_md: str | None = Field(default=None, max_length=32_000)
|
||||
expected_detection_blue_md: str | None = Field(default=None, max_length=32_000)
|
||||
opsec_level: str = Field(default="medium")
|
||||
tags: list[str] = Field(default_factory=list, max_length=64)
|
||||
expected_iocs: list[str] = Field(default_factory=list, max_length=128)
|
||||
tags: list[TagStr] = Field(default_factory=list, max_length=64)
|
||||
expected_iocs: list[IocStr] = Field(default_factory=list, max_length=128)
|
||||
mitre_tags: list[MitreTagIn] = Field(default_factory=list, max_length=64)
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
@@ -56,8 +63,8 @@ class UpdateTestTemplatePayload(BaseModel):
|
||||
expected_result_red_md: str | None = Field(default=None, max_length=32_000)
|
||||
expected_detection_blue_md: str | None = Field(default=None, max_length=32_000)
|
||||
opsec_level: str | None = None
|
||||
tags: list[str] | None = Field(default=None, max_length=64)
|
||||
expected_iocs: list[str] | None = Field(default=None, max_length=128)
|
||||
tags: list[TagStr] | None = Field(default=None, max_length=64)
|
||||
expected_iocs: list[IocStr] | None = Field(default=None, max_length=128)
|
||||
mitre_tags: list[MitreTagIn] | None = Field(default=None, max_length=64)
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
Reference in New Issue
Block a user