From 37e9e03f0258fe742da6ea518178d8296f287f1a Mon Sep 17 00:00:00 2001 From: Knacky Date: Tue, 12 May 2026 13:54:46 +0200 Subject: [PATCH] docs(m4): CHANGELOG, README, lessons, spec drift fix, todo tick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CHANGELOG: added M4 section listing endpoints, CLI, volume, persisted settings, picker, and the post-spec-review fixes (custom-URL integrity requirement + /diag/reset consistency + spec drift). Includes the intentional decisions paragraph (seed-time download not image-baked, read endpoints unauthenticated-perm-wise, stdlib over httpx). - README: status bumped to M0–M4, added MITRE quickstart (make seed-mitre + air-gapped path with --source /data/mitre/ + --skip-checksum), testing-m.md pointer updated to testing-m4.md, roadmap line. - tasks/spec.md §10 #4: amended "14 tactics Enterprise" → "≥14 tactics Enterprise (la v19 du pin actuel en ship 15)". - tasks/lessons.md: 7 M4 lessons captured (stdlib STIX parsing, decoupling DoD asserts from upstream versions, subtechnique parent resolution, single- transaction safety, custom-URL footgun mitigation, /diag/reset consistency, named-volume permission caveat, podman build cache surprise). - tasks/todo.md: M4 marked ☑. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ README.md | 19 ++++++++++++++++--- tasks/lessons.md | 11 +++++++++++ tasks/spec.md | 2 +- tasks/todo.md | 2 +- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9072044..de4897e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ All notable changes to this project will be documented here. Format: [Keep a Cha ## [Unreleased] +### Added — M4 (MITRE ATT&CK Enterprise) +- **STIX 2.1 parser + upsert** (`app/services/mitre_seed.py`): stdlib-only (`urllib.request` + `hashlib`), pinned to Enterprise v19.0 (`enterprise-attack-19.0.json`, sha256 `df520ea0…`). Parses 25k+ STIX objects → 15 tactics, 222 techniques, 475 sub-techniques in ~1.1 s. Skips revoked + deprecated, resolves sub-technique parents via `relationship[subtechnique-of]` with a `T1003.001 → T1003` dotted-id fallback, copies kill-chain phases into the `mitre_technique_tactics` M2M. +- **CLI**: `flask metamorph seed-mitre [--source ] [--checksum-sha256 ] [--skip-checksum]` (`app/cli.py`). `make seed-mitre` wraps it. +- **REST endpoints** (`app/api/mitre.py`): + - `GET /api/v1/mitre/tactics`, `/mitre/techniques?tactic=…&q=…`, `/mitre/subtechniques?technique=…&q=…` (paginated, search on name/external_id). + - `GET /api/v1/mitre/status` (last_sync, version, source_url, defaults). + - `POST /api/v1/mitre/sync` (perm `mitre.sync`) — re-pull on demand. +- **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.
+  - 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).
+  - `e2e/tests/m4-mitre.spec.ts` — **6 Playwright** against the live stack (calls `/mitre/sync` once in `beforeAll`).
+  - `tasks/testing-m4.md`.
+
+### Fixed (post-M4 spec-review pass)
+- **Sync integrity guarantee**: `seed_mitre()` now refuses a custom URL without either `expected_sha256` or an explicit `allow_unverified=true`. Closes a "typo in `mitre_source_url` setting routes the seed to attacker JSON" footgun. CLI surfaces this via `--checksum-sha256` / `--skip-checksum`; API via `{"source", "expected_sha256", "allow_unverified"}` body.
+- **`/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.
+
+### 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.
+- **No `requests` / `httpx` dep added**: stdlib `urllib.request` is enough and avoids inflating the image.
+
+### 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` → **51 pytest pass** (1 health + 8 schema + 15 auth + 15 RBAC + 12 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.
+
 ### Added — M3 (RBAC: groups, permissions, users)
 - **Permission catalogue** (`app/services/permissions_seed.py`): 31 atomic codes across 10 families (`user`, `group`, `invitation`, `test_template`, `scenario_template`, `mission`, `detection_level`, `setting`, `mitre.sync`). Seeded at boot **and** after `/setup` to handle a freshly truncated DB. Idempotent + additive on system groups (never removes a perm).
 - **Default group bindings**: `admin` = all 31 codes; `redteam` = 8 (catalogue read + mission.{read,create,update,archive,write_red_fields} + detection_level.read); `blueteam` = 5 (catalogue read + mission.{read,write_blue_fields} + detection_level.read).
diff --git a/README.md b/README.md
index 411eb67..da324e7 100644
--- a/README.md
+++ b/README.md
@@ -2,13 +2,15 @@
 
 Collaborative purple-team platform. Red team logs the tests they execute (procedure, command, timestamp); blue team annotates each test with detection evidence (alerts, logs, files). At the end of an engagement, Metamorph generates a standalone reveal.js slide deck classified by MITRE ATT&CK tactic.
 
-> **Status**: M0 (bootstrap). See `tasks/spec.md` for the full specification and `tasks/todo.md` for the milestone-by-milestone plan.
+> **Status**: M0–M4 delivered (bootstrap → DB schema → auth → RBAC → MITRE ATT&CK reference). See `tasks/spec.md` for the full specification and `tasks/todo.md` for the milestone-by-milestone plan.
 
 ## Stack
 
 - **Backend**: Python 3.12, Flask 3, SQLAlchemy 2 + Alembic (M1+), PostgreSQL 16.
 - **Frontend**: React 18 + TypeScript + Vite + TailwindCSS (RTOps design tokens, see `tasks/design.md`).
 - **Auth (M2+)**: JWT access (1h) + refresh (30d), Argon2id, invite-link enrollment.
+- **RBAC (M3+)**: atomic permissions (31 codes) bundled into custom groups; 3 system groups seeded (`admin` / `redteam` / `blueteam`).
+- **MITRE ATT&CK (M4+)**: Enterprise reference catalogue pinned to v19.0, seedable via `make seed-mitre`.
 - **Delivery**: docker-compose. TLS termination is expected to be handled by an external reverse proxy in production.
 
 ## Quickstart
@@ -43,6 +45,17 @@ Then:
 - Front: 
 - API health:  (proxied) or 
 
+### First-time setup
+
+```bash
+make migrate                  # apply DB schema
+make print-install-token      # prints the bootstrap admin token (logs banner)
+# visit http://localhost:8080/setup, paste the token, create the admin account
+make seed-mitre               # populate the MITRE ATT&CK reference (~50 MB, ~1 s)
+```
+
+The MITRE bundle is cached in the named volume `metamorph_mitre` (`/data/mitre/.json` inside the api container). For air-gapped operators, pre-populate the volume with your own STIX 2.1 file and run `podman compose exec api flask --app app.cli metamorph seed-mitre --source /data/mitre/your-file.json --skip-checksum`.
+
 To stop:
 
 ```bash
@@ -80,7 +93,7 @@ See `.env.example`. The most important ones:
 
 ## Testing
 
-- **Manual + automated checklist for the current milestone**: see [`tasks/testing-m.md`](tasks/testing-m0.md) (currently `testing-m0.md`).
+- **Manual + automated checklist for the current milestone**: see [`tasks/testing-m.md`](tasks/testing-m4.md) (current: `testing-m4.md`).
 - **Backend unit tests**: `make test-api`
 - **End-to-end (Playwright)**: `make e2e-install` (once), then `make up && make e2e`. Reports land in `e2e/playwright-report/` (HTML + JUnit XML); open with `make e2e-report`.
 
@@ -122,7 +135,7 @@ The hooks run `ruff` + `ruff-format` on the backend and `eslint` / `tsc --noEmit
 
 ## Roadmap
 
-See `tasks/todo.md`. Current milestone: **M0 — bootstrap**.
+See `tasks/todo.md`. Current milestone: **M4 — MITRE ATT&CK Enterprise**. Next: M5 (test & scenario templates).
 
 ## License
 
diff --git a/tasks/lessons.md b/tasks/lessons.md
index ad556ca..4386376 100644
--- a/tasks/lessons.md
+++ b/tasks/lessons.md
@@ -41,6 +41,17 @@ project: Metamorph
 - **Le test d'intégration "expected tables/FK/CHECK" est le bon filet de sécurité** pour M1+ : il a immédiatement attrapé les fixes du reviewer (le retrait de `ck_mission_test_mitre_tags_exactly_one_mitre_fk` aurait été un oubli silencieux sinon).
 - **Lancer le DoD avant de dire "M1 done"** : règle gravée à M0, respectée ici. `make clean && make up && make migrate && make test-api && make e2e` est la séquence canonique de fin de milestone.
 
+## 2026-05-12 — M4 MITRE ATT&CK
+
+- **STIX parsing avec stdlib uniquement** (`urllib.request` + `json` + `hashlib`) suffit pour 50 MB de bundle, ~1.1 s parse end-to-end. Pas besoin de `requests`/`httpx`. Toute future ingestion de gros JSON pinné → stdlib first, ne pas inflater l'image pour un cas d'usage one-shot.
+- **Le sous-jacent MITRE évolue** : la spec mentionne "14 tactics" mais la v19 actuelle en ship 15 (Reconnaissance + Resource Development depuis v8). Les assertions de DoD sont à exprimer en `>= X` quand un référentiel externe est en jeu, pas en `== X`. Pattern : décorréler la valeur exacte du contrat (sinon la maintenance casse au prochain bump).
+- **Sub-technique parent resolution** : la source authoritative est la `relationship[subtechnique-of]` STIX, pas la convention dotted-id `T1003.001 → T1003`. La regex en fallback ne sert que si la relation manque (jamais le cas avec MITRE officiel, mais utile pour bundles custom).
+- **`session_scope()` enveloppe tout le seed dans une seule transaction** → les lecteurs externes ne voient jamais un état intermédiaire pendant le DELETE+INSERT de `mitre_technique_tactics`. Postgres READ COMMITTED isole. Pas besoin d'advisory lock sauf si on s'attend à des syncs concurrents.
+- **Checksum bypass silencieux = footgun**. Avant : `source != MITRE_DEFAULT_URL` → `expected_sha256 = None`. Un admin qui type un domaine attaquant dans `mitre_source_url` ingère du JSON arbitraire sans intégrité. Patron correct : `MitreSeedError("custom URL requires an expected_sha256 or allow_unverified=True")`. L'opt-out explicite (`--skip-checksum` côté CLI, `allow_unverified: true` côté API) reste possible mais visible.
+- **`/diag/reset` cohérence** : si on TRUNCATE `settings` mais pas les tables de référence MITRE (gardées car coûteuses à re-seeder), `GET /mitre/status` retourne `last_sync: null` alors que `GET /mitre/tactics` retourne 15 lignes. Discrepancy mensongère. Fix : TRUNCATE aussi les `mitre_*` dans `/diag/reset` (test-only endpoint, on accepte la re-sync via `/mitre/sync` en `beforeAll`).
+- **Volume permissions et chown au build** : `mkdir -p /data/mitre && chown -R metamorph:metamorph /data` dans le Dockerfile suffit POUR le premier `make up` (podman copie l'ownership de l'image lors de l'init du named volume). Mais si un volume préexiste owned root, le chown ne replay pas. À documenter en pré-requis dans `tasks/testing-m.md`, ou ajouter un entrypoint shim qui valide les perms au boot.
+- **Build cache du front silencieux** : `podman build` montre `Using cache aad724...` même quand src/ a changé si le diff entre les arborescences est invisible (mtime). En cas de doute : `podman build --no-cache` une fois pour confirmer que le typecheck passe, puis `make down && make up` pour pousser le bundle. Réflexe à garder en mémoire.
+
 ## 2026-05-11 — M3 RBAC, groupes, users, invitations
 
 - **`logging.LogRecord` réserve `name`** comme attribut interne (en plus de `message`, `levelname`, `pathname`, `filename`, `module`, `funcName`, `lineno`, `asctime`, `process`, `thread`, `args`). Donc `log.info("metamorph.x.created", extra={"name": entity.name})` lève `KeyError: "Attempt to overwrite 'name' in LogRecord"`. Patron : préfixer toute clé risquée par l'entité (`group_name`, `user_name`, `template_name`). À documenter dans le style guide quand on en aura un.
diff --git a/tasks/spec.md b/tasks/spec.md
index 42c8db6..d77d891 100644
--- a/tasks/spec.md
+++ b/tasks/spec.md
@@ -137,7 +137,7 @@ Remplace l'aller-retour Excel actuel par une UI partagée temps réel, multi-rô
 1. Premier boot : token d'install affiché dans les logs, accès `/setup` permet de créer le 1er admin.
 2. Admin crée un groupe custom et lui attribue des permissions atomiques (write_red_fields uniquement, par exemple).
 3. Admin envoie un lien d'invitation, l'utilisateur s'enregistre avec succès et hérite des bons groupes.
-4. Admin importe la matrice MITRE et crée un test unitaire avec Tactic+Technique+Sub-technique.
+4. Admin importe la matrice MITRE et crée un test unitaire avec Tactic+Technique+Sub-technique. *(≥ 14 tactics Enterprise — la v19 du pin actuel en ship 15.)*
 5. Admin compose un scénario de 3 tests ordonnés via drag-and-drop.
 6. Red Teamer crée une mission avec scénario, assigne 1 red + 1 blue.
 7. Red Teamer marque un test « exécuté », saisit commande+output, timestamp auto capturé.
diff --git a/tasks/todo.md b/tasks/todo.md
index 6716d5c..7aba737 100644
--- a/tasks/todo.md
+++ b/tasks/todo.md
@@ -101,7 +101,7 @@ spec: tasks/spec.md
 
 ---
 
-## M4 — MITRE ATT&CK Enterprise ☐
+## M4 — MITRE ATT&CK Enterprise ☑
 
 **But** : le référentiel ATT&CK est interrogeable et tagué sur les tests.