From 2c85f9b57e1a4b4af13cbb2bc67a03194dc9bbb8 Mon Sep 17 00:00:00 2001 From: Knacky Date: Tue, 12 May 2026 19:19:44 +0200 Subject: [PATCH] docs(m4): reconcile CHANGELOG + testing-m4 with the flat matrix + CR fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CHANGELOG M4 Added: rewrote the frontend bullet to describe the actual flat ATT&CK matrix that ships (full-bleed, 15-col grid with minmax(7rem, 1fr), name-only cells, ▸/▾ chevron). The original entry still described the abandoned 3-column drill-down picker. - New "Fixed (post-M4 code-review pass)" subsection enumerating the six CR-driven fixes that landed in this branch (SSRF allowlist, advisory lock, typed contract, N+1 elimination, version clearing, error scrub + the test additions and e2e count pinning). - DoD counts: 53 → 58 pytest, 34 e2e unchanged. testing-m4.md follows. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 17 ++++++++++++++--- tasks/testing-m4.md | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a71bd..ea2f1a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ All notable changes to this project will be documented here. Format: [Keep a Cha - **Persisted metadata** in `settings`: `mitre_last_sync`, `mitre_version`, `mitre_source_url`. - **Compose volume `metamorph_mitre`** mounted at `/data/mitre/` in the api container — caches the downloaded bundle across restarts. Owned by `metamorph:metamorph`. - **Frontend**: - - `` component: 3-column tactic → technique → sub-technique with multi-select chips, autocomplete on each column. Returns `MitreTag[]` (`kind`, `id`, `external_id`, `name`), ready for M5 templates. - - `/mitre` showcase page with status card, admin-gated **Trigger sync** button, picker preview, and `
` payload preview.
+  - `` component: flat ATT&CK matrix matching `attack.mitre.org/#` — full-bleed beyond `max-w-page`, 15 equal-width columns via `grid-template-columns: repeat(N, minmax(7rem, 1fr))`, sans-serif 12px, **name-only cells** (external_id surfaces on hover via `title` and in selection chips), `▸/▾` chevron expands sub-techniques inline within the column, multi-select with chip-removal at the top. Returns `MitreTag[]` (`kind`, `id`, `external_id`, `name`), ready for M5 templates.
+  - `/mitre` showcase page with status card, admin-gated **Trigger sync** button, the picker, and a JSON `
` preview of the current selection.
   - Nav adds **MITRE** link for any logged-in user.
 - **Testing**:
   - `backend/tests/test_mitre.py` — **12 pytest** (parser, idempotence, checksum mismatch, persisted settings, endpoint variants, perm enforcement) using a hand-crafted minimal STIX bundle (no network in tests).
@@ -27,6 +27,17 @@ All notable changes to this project will be documented here. Format: [Keep a Cha
 - **`/diag/reset` consistency**: now truncates the `mitre_*` tables alongside `settings` so `GET /mitre/status` and `GET /mitre/tactics` agree after a reset (previously: catalogue rows persisted, but `mitre_last_sync` got wiped → status lied).
 - **Spec drift §10 #4**: amended "14 tactics" → "≥ 14 tactics (v19 ships 15)" to reflect MITRE v8+ reality.
 
+### Fixed (post-M4 code-review pass)
+- **SSRF allowlist on `/mitre/sync`**: host must be in `MITRE_ALLOWED_HOSTS` (defaults to `raw.githubusercontent.com`, comma-separated env override). Closes the "admin holding `mitre.sync` can pivot the api container at cloud metadata (`169.254.169.254`) or internal mirrors" vector. New `MitreSourceForbidden` exception → 400 with `source_forbidden` error code.
+- **Concurrent sync race**: `seed_mitre()` now acquires `pg_advisory_xact_lock(hashtext('mitre.seed'))` at the top of the transaction so two `/mitre/sync` calls serialise cleanly across the `DELETE` + re-`INSERT` of `mitre_technique_tactics`.
+- **Typed sync contract end-to-end**: Pydantic `SyncResultOut` on the backend (`app/api/mitre.py`) mirrored by a `MitreSyncResult` TS interface (`frontend/src/lib/mitre.ts`). The MitrePage mutation no longer uses an `as Record` escape hatch.
+- **N+1 in dotted sub-technique fallback**: pre-built `{external_id → id}` dict at function entry; was firing one extra SELECT per orphan (currently 0 with MITRE, but a latent footgun for partial bundles).
+- **`SETTING_VERSION` cleared explicitly when source != default**: previously kept the stale pinned version after a custom-URL re-sync; now `_upsert_setting(..., None)` so `/mitre/status` doesn't lie.
+- **Internal error scrub on `/mitre/sync`**: 500 responses no longer leak URLError / DB driver text via `str(e)` — stack lands in JSON logs only.
+- **E2E pinned to exact MITRE v19 counts** (15/222/475/0 orphans) for parser-regression detection; previously `>=` thresholds could mask "revoked tactics silently included".
+- **E2E uses `crypto.randomUUID()`** instead of `Math.random()` for unique test emails.
+- **Test coverage for security guards**: `file://` rejection, disallowed HTTPS host, custom-URL-without-sha refusal, dotted-id fallback, version-clearing semantics — 5 new pytest covering paths the spec-review demanded but no test enforced.
+
 ### Decisions (intentional)
 - **Bundle "embarqué" interpreted as seed-time download + named-volume cache**, not "binary baked into the Docker image". Keeps the image ~150 MB, makes version bumps a constant edit, plays nicely with `make seed-mitre` re-runs. Air-gapped operators copy the file into the volume + pass `--source /data/mitre/`.
 - **Read endpoints unauthenticated-perm-wise but auth-required**: MITRE data is public reference material — no `mitre.read` perm. Status endpoint is similarly open (under `@require_auth`) to keep `/mitre/status` simple for the UI badge.
@@ -34,7 +45,7 @@ All notable changes to this project will be documented here. Format: [Keep a Cha
 
 ### Validated end-to-end (M4 DoD)
 - `make clean && make up && make migrate && make seed-mitre` → 15 tactics / 222 techniques / 475 sub-techniques / 254 links / 0 orphans / ~1.1 s.
-- `make test-api` → **53 pytest pass** (1 health + 8 schema + 15 auth + 15 RBAC + 14 MITRE) in ~5 s.
+- `make test-api` → **58 pytest pass** (1 health + 8 schema + 15 auth + 15 RBAC + 19 MITRE) in ~5 s.
 - `make e2e` → **34 Playwright pass** (8 M0 + 4 M1 + 8 M2 + 8 M3 + 6 M4) in ~18 s.
 - Spec-reviewer PASS after fixes applied.
 
diff --git a/tasks/testing-m4.md b/tasks/testing-m4.md
index 1de7211..e98910b 100644
--- a/tasks/testing-m4.md
+++ b/tasks/testing-m4.md
@@ -21,7 +21,7 @@ make seed-mitre  # télécharge le bundle pinné v19.0 (~50 MB, ~1 s parse)
 ## 2. Tests automatisés
 
 ```bash
-make test-api    # 53 tests pytest dont 14 nouveaux MITRE (parser + 5 read endpoints + matrix + status)
+make test-api    # 58 tests pytest dont 19 MITRE (parser, idempotence, security guards, all endpoints, dotted fallback, version clearing)
 make e2e         # 34 tests Playwright dont 6 M4
 ```