docs: sprint 3 wrap-up — README + CHANGELOG + lessons + plan final
- README: status bump to sprint 3, test counts refreshed (164/86/105), IPv6 note for the e2e runner - CHANGELOG: sprint 3 entry under [Unreleased] (multi-tech model + matrix endpoint + auto-save UI); sprint 2 moved to its own [Sprint 2] section (merged 2026-05-27) - tasks/lessons.md: 6 lessons captured (2-pass spec-review, inline summary scoping, "test in brief means test in commit" discipline, SQLite batch_alter_table, real migration round-trip, modal Apply 0 disambiguation) - tasks/todo.md: status flipped to 🟢 SPRINT COMPLET, execution sequence ticks updated with commit hashes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
31
CHANGELOG.md
31
CHANGELOG.md
@@ -6,6 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added — Sprint 3 (Multi-technique simulations + MITRE matrix modal)
|
||||||
|
|
||||||
|
**Backend** (164 pytest passing)
|
||||||
|
- `Simulation.techniques` JSON column replaces the scalar `mitre_technique_id` / `mitre_technique_name` pair. Stored as `[{"id", "name"}]`; tactics are derived at serialize time from the MITRE service (snapshot pattern survives bundle updates).
|
||||||
|
- Alembic migration `0003_simulation_techniques_array.py` — reversible upgrade (backfill from scalars → drop scalars → enforce `NOT NULL` via `batch_alter_table`) and symmetric downgrade.
|
||||||
|
- `PATCH /api/simulations/<sid>` now accepts `{technique_ids: ["T1059", "T1059.001", ...]}` (flat list of T-IDs, parents and subs at the same level). Server validates each ID against the bundle (400 on unknown), deduplicates while preserving order, resolves names, and rejects SOC payloads (403). Returns 503 if the bundle isn't loaded.
|
||||||
|
- `GET /api/mitre/matrix` — new endpoint returning the full Enterprise tree `[{tactic_id, tactic_name, techniques: [{id, name, subtechniques: [{id, name}]}]}]`. Tactics in canonical order (Initial Access → Impact). Techniques sorted alphabetically per tactic; sub-techniques nested under their parent via dot-ID detection.
|
||||||
|
- `mitre_svc` extended with `get_tactics(id)`, `lookup_name(id)`, `get_matrix()`, and a `TACTIC_NAMES` constant fixing the cosmetic `"Command And Control"` → `"Command and Control"` (MITRE canonical capitalisation).
|
||||||
|
- `REDTEAM_FIELDS | {"technique_ids"}` SOC gate in `simulation_workflow.apply_patch` preserves the sprint 2 field-level RBAC pattern.
|
||||||
|
- Auto-transition `pending → in_progress` extended: triggers when `technique_ids` is non-empty (consistent with the "non-empty value" rule from sprint 2). Empty list does not trigger.
|
||||||
|
|
||||||
|
**Frontend** (86 vitest passing)
|
||||||
|
- `MitreTechniquesField` orchestrates multi-technique selection with **auto-save** — every add (Quick Search / matrix Apply) and every remove (× on tag chip) triggers a PATCH via `useUpdateSimulation`. Toast feedback on success/error; UI disabled during the in-flight PATCH; silent dedup if the user re-adds an already-present technique.
|
||||||
|
- `MitreTechniqueTag` — chip component (`bg-primary-soft text-primary-deep rounded-full`) with an × remove button.
|
||||||
|
- `MitreMatrixModal` — full-width modal, one column per tactic (220px fixed), horizontal scroll. Each technique top-level is clickable (toggle); a chevron expands/collapses sub-techniques rendered in cascade. Search filter (case-insensitive on id + name) auto-expands the parent of a matched sub-technique. Tactic header shows a "N selected" counter (parents + subs). Footer: Cancel + "Apply N technique(s)" (or "Clear all" when N=0 and there's an existing selection). Focus trap V1: search input auto-focus on open, Tab cycles within the modal, Escape and backdrop click both = Cancel.
|
||||||
|
- `MitreTechniquePicker` (sprint 2) clean-rewritten to a one-shot `onSelect({id, name})` signature; no incoming value props. The picker resets after each selection — the parent (`MitreTechniquesField`) handles append + dedup.
|
||||||
|
- `SimulationList` MITRE column displays `T1059 +2` when 3 techniques are selected (first id + remainder counter) or `—` when empty.
|
||||||
|
- `SimulationFormPage` — `MitreTechniquesField` replaces the old standalone `MitreTechniquePicker`. The technique state moves out of the RT form (independent auto-save cycle); the Save Red Team button still batches the other RT fields.
|
||||||
|
|
||||||
|
**Acceptance tests** (Playwright)
|
||||||
|
- 4 new spec files: `us13-multi-techniques.spec.ts`, `us14-techniques-tags.spec.ts`, `us15-mitre-matrix-modal.spec.ts`, `us16-regression-sprint2.spec.ts` — all ACs (AC-13.1 → AC-16.3) pass.
|
||||||
|
- Sprint 2 specs `us8-simulation-redteam-fill.spec.ts` and `us10-mitre-autocomplete.spec.ts` adapted to the new `techniques: []` array (no more scalar field assertions).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- 2026-05-27 — SPEC.md § Simulation: "Type d'attaque MITRE correspondant" (singular) → "Types d'attaque MITRE correspondants (multi-techniques) — sélectionnables par autocomplete OU via la matrice ATT&CK affichée en modale. Sub-techniques supportées."
|
||||||
|
- 2026-05-27 — Breaking API change: `mitre_technique_id` and `mitre_technique_name` removed from the `Simulation` payload (both directions). Replaced by `techniques: [{id, name, tactics}]` in responses and `technique_ids: string[]` in PATCH requests. No backwards-compatibility shim (no external consumer at this stage).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [Sprint 2] — Simulations + MITRE ATT&CK (merged 2026-05-27)
|
||||||
|
|
||||||
### Added — Sprint 2 (Simulations + MITRE ATT&CK)
|
### Added — Sprint 2 (Simulations + MITRE ATT&CK)
|
||||||
|
|
||||||
**Backend** (Flask + SQLAlchemy, 131 pytest passing)
|
**Backend** (Flask + SQLAlchemy, 131 pytest passing)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Mimic** is a Breach and Attack Simulation (BAS) web UI built on the MITRE ATT&CK matrix. It replaces the flat Excel spreadsheets that red-teams and SOC analysts pass around at the end of an engagement, providing a shared workspace for Purple Team handoffs.
|
**Mimic** is a Breach and Attack Simulation (BAS) web UI built on the MITRE ATT&CK matrix. It replaces the flat Excel spreadsheets that red-teams and SOC analysts pass around at the end of an engagement, providing a shared workspace for Purple Team handoffs.
|
||||||
|
|
||||||
> Status: **Sprint 2 — Simulations + MITRE ATT&CK**. The Purple Team workflow (RedTeam fills test → marks for review → SOC documents detection → closes) is now end-to-end testable in the UI, with MITRE technique autocomplete for TTP tagging.
|
> Status: **Sprint 3 — Multi-technique simulations + MITRE matrix modal**. A simulation can now be tagged with multiple MITRE techniques (top-level and sub-techniques) via either autocomplete or a clickable ATT&CK matrix modal. Tags auto-save on add/remove; the rest of the Sprint 2 Purple Team workflow (workflow states, RBAC, etc.) is unchanged.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -138,9 +138,9 @@ npm run dev # http://localhost:5173 with /api proxied to :5000
|
|||||||
Tests:
|
Tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend && pytest -q # 131 tests
|
cd backend && pytest -q # 164 tests
|
||||||
cd frontend && npm run test -- --run # 63 tests
|
cd frontend && npm run test -- --run # 86 tests
|
||||||
cd e2e && npx playwright test # 68 tests (needs container up)
|
cd e2e && npx playwright test # 105 tests (needs container up — use MIMIC_BASE_URL=http://127.0.0.1:5000 if localhost resolves to IPv6)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -4,6 +4,34 @@ Recurring mistakes and the rule we adopted so the same issue doesn't bite twice.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Sprint 3 (closed 2026-05-27)
|
||||||
|
|
||||||
|
### Process — Spec-review 2-pass after team-lead edits (team-lead)
|
||||||
|
**Context** : Sprint 3 spec-reviewer ran a first pass on my drafted plan, flagged 5 items, I edited the plan to address 4 of them. Spec-reviewer ran a 2nd pass and caught 2 critical gaps I'd missed in my edits (REDTEAM_FIELDS update + SQLite batch_alter_table for nullable=False / drop_column). Backend-builder was already mid-implementation when the 2nd pass arrived — I dispatched an urgent addendum SendMessage.
|
||||||
|
**Lesson** : after editing the plan in response to spec-reviewer's notes, always run a 2nd spec-review pass before dispatching builders. The fixes themselves can introduce new gaps. Cheaper than urgent addenda mid-sprint. Cost: one extra read-only pass; benefit: no addenda churn.
|
||||||
|
|
||||||
|
### Process — Avoid embedding builder summaries that look like new dispatches (team-lead)
|
||||||
|
**Context** : The frontend dispatch brief contained a "BACKEND-BUILDER SUMMARY" inline section to give the frontend the API contract. The backend-builder also received this message (team routing) and interpreted the embedded summary as a fresh dispatch — they re-read it, found one ambiguity I'd resolved differently in the frontend brief than in the original backend brief (503 vs 400 on bundle unloaded), and pushed a fix commit `673b25e` independently. Net positive but a coordination cost.
|
||||||
|
**Lesson** : when embedding another builder's summary as inline context, prefix the section with "DO NOT ACT ON THIS — INLINE CONTEXT ONLY" or use a clear visual separator (`---`) plus a header that makes scope obvious. Builders inherit the entire message — they don't know which parts are addressed to them.
|
||||||
|
|
||||||
|
### Process — Explicit "Ajouter un test" in brief means a real test, not just code (team-lead)
|
||||||
|
**Context** : Sprint 3 post-review dispatch to backend-builder explicitly said "Test : ajouter un assert que… NOT NULL après upgrade" and "Test : un assert dans test_mitre.py qui vérifie… 'Command and Control'". Backend-builder fixed the code in commit `4596f09` but added zero new tests (162 → 162). I bounced back with a SendMessage; backend-builder added the tests in `393b6ed` (164/164).
|
||||||
|
**Lesson** : the discipline of "if the brief says 'add a test', the test is non-negotiable" must be enforced. Don't accept a fix-commit that doesn't include the regression tests requested in the brief — bounce back via SendMessage. Builders may otherwise treat tests as "if I have time" while only delivering the production change.
|
||||||
|
|
||||||
|
### Engineering — SQLite Alembic migrations require batch_alter_table for ALTER + DROP COLUMN (backend-builder)
|
||||||
|
**Context** : Spec-reviewer flagged that the migration brief mentioned `alter_column nullable=False` and `drop_column` without specifying `op.batch_alter_table(...)`. SQLite doesn't support either operation natively — without batch mode, the migration crashes at runtime. Backend-builder initially skipped the `nullable=False` step entirely with a comment "model + app logic enforces it"; code-reviewer pushed back ("batch mode rebuilds the table and does support the change — that's its purpose"). Final fix wraps the step in `batch_alter_table`.
|
||||||
|
**Lesson** : on SQLite, ANY operation that mutates a column type, nullability, or schema beyond ADD COLUMN must go through `with op.batch_alter_table(table) as batch_op: batch_op.alter_column(...)`. Don't accept "model enforces it" as a substitute for DDL-level constraint — a fresh DB initialised from migrations alone won't have the constraint.
|
||||||
|
|
||||||
|
### Engineering — Real migration round-trip > pure unit test (backend-builder)
|
||||||
|
**Context** : Backend-builder's initial migration backfill test was tautological — it inlined a `_backfill` Python helper and tested the helper against itself, never invoking the real Alembic `upgrade()`. Code-reviewer flagged it. Fix: load the migration module via `importlib.util.spec_from_file_location`, patch `alembic.op._proxy` with a live `Operations` context, run `upgrade()` against in-memory SQLite, then `sqlalchemy.inspect` the resulting schema.
|
||||||
|
**Lesson** : a migration test that doesn't invoke `command.upgrade()` (or the equivalent `Operations` runner against the real migration module) tests nothing about the actual migration path. Use `alembic.runtime.migration.MigrationContext` + `alembic.operations.Operations` to instantiate a real runner against an in-memory engine.
|
||||||
|
|
||||||
|
### UX — Modal Apply 0 disambiguation (frontend-builder)
|
||||||
|
**Context** : MitreMatrixModal initially labelled the Apply button as "Apply " (trailing space, no count) when 0 techniques were selected, while the button stayed enabled. A click with 0 selected and a non-empty current list would silently clear all techniques. Code-reviewer flagged. Final design: `disabled` when both counts are zero (nothing to do); label switches to "Clear all" when the user wants to wipe a non-empty list (count=0 but initial selection non-empty); standard "Apply N technique(s)" otherwise.
|
||||||
|
**Lesson** : for any "Apply"/"Confirm"/"Save" button whose effect depends on the diff between local and remote state, enumerate the three cases — no-op (disable), destructive intent (relabel to confirm), normal (count + verb) — before shipping. The trailing-space label is a code smell that exposes missing edge-case handling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Sprint 2 (closed 2026-05-26)
|
## Sprint 2 (closed 2026-05-26)
|
||||||
|
|
||||||
### Testing — Vitest module hoisting (frontend-builder)
|
### Testing — Vitest module hoisting (frontend-builder)
|
||||||
|
|||||||
414
tasks/todo.md
414
tasks/todo.md
@@ -1,252 +1,290 @@
|
|||||||
# Sprint 2 — Simulations + MITRE ATT&CK
|
# Sprint 3 — MITRE matrix modal + multi-technique simulations
|
||||||
|
|
||||||
**Branche** : `sprint/2-simulations`
|
**Branche** : `sprint/3-mitre-matrix`
|
||||||
**Statut** : 🟢 SPRINT COMPLET — 32 acceptance tests sprint 2 verts, code-review traité (2 MAJOR + 2 MINOR + 2 NITs fixés), PR prête
|
**Statut** : 🟢 SPRINT COMPLET — 105/105 sprint 3 e2e verts, code-review traité, PR prête
|
||||||
**Base** : `main` (sprint 1 mergé en `7fc79cc`)
|
**Base** : `main` @ `e1d9738`
|
||||||
**Objectif** : livrer les simulations (CRUD + workflow Pending→In progress→Review required→Done) à l'intérieur d'un engagement, avec autocomplete MITRE ATT&CK alimenté par un bundle STIX local. C'est le cœur métier — l'app remplace enfin le fichier Excel partagé redteam/SOC.
|
**Objectif** : remplacer la sélection MITRE mono-technique de sprint 2 par une sélection multi-techniques avec deux modes complémentaires : autocomplete (rapide) et matrice cliquable (exploration). Les techniques choisies s'affichent comme tags sur la simulation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Évolution SPEC.md à acter en début de sprint
|
||||||
|
|
||||||
|
SPEC.md § Simulation dit aujourd'hui "Type d'attaque MITRE correspondant (peut être une liste de référence)" au singulier. Le team-lead met à jour cette ligne en début de sprint (pas de PR séparée) pour refléter le scope multi-techniques. Texte cible :
|
||||||
|
|
||||||
|
> Types d'attaque MITRE correspondants (multi-techniques) — sélectionnables par autocomplete OU via la matrice ATT&CK affichée en modale.
|
||||||
|
|
||||||
|
L'évolution est tracée dans CHANGELOG.md § Changed du sprint 3.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. User stories
|
## 1. User stories
|
||||||
|
|
||||||
### US-7 — En tant que redteam, je crée une simulation dans un engagement
|
### US-13 — En tant que redteam, je sélectionne plusieurs techniques MITRE par simulation
|
||||||
**Pourquoi** : c'est la feature centrale du sprint 2.
|
**Pourquoi** : un test couvre souvent plusieurs TTPs (ex : Initial Access → Discovery → Execution). Mono-technique limite la description réelle d'un test.
|
||||||
|
|
||||||
**Critères d'acceptation**
|
**Critères d'acceptation**
|
||||||
- [ ] AC-7.1 : `POST /api/engagements/<eid>/simulations {name}` (admin|redteam) → 201 + simulation `{id, engagement_id, name, status: "pending", ...}`. `name` requis, non vide.
|
- [ ] AC-13.1 : modèle `Simulation` n'a plus `mitre_technique_id` ni `mitre_technique_name` (scalaires). Remplacés par `techniques` (colonne JSON, liste d'objets `{id: str, name: str}`, défaut `[]`).
|
||||||
- [ ] AC-7.2 : autres rôles (soc) → 403.
|
- [ ] AC-13.2 : migration Alembic `0003_simulation_techniques_array.py` :
|
||||||
- [ ] AC-7.3 : engagement inexistant → 404. Engagement existant mais aucune simulation → liste vide.
|
- ajoute la colonne `techniques` (JSON)
|
||||||
- [ ] AC-7.4 : `GET /api/engagements/<eid>/simulations` (auth) → liste des simulations de l'engagement, ordonnée `created_at desc`.
|
- backfill les simulations existantes : si `mitre_technique_id` non null → `techniques = [{id, name}]`, sinon `techniques = []`
|
||||||
- [ ] AC-7.5 : page `/engagements/:eid` (EngagementDetailPage) remplace le placeholder Sprint 2 par une section "Simulations" : liste (colonnes: name, MITRE id, status badge, executed_at) + bouton "Nouvelle simulation" pour admin/redteam.
|
- drop les deux anciennes colonnes
|
||||||
- [ ] AC-7.6 : depuis cette liste, click sur une ligne → ouvre `/engagements/:eid/simulations/:sid/edit` (page d'édition role-aware, unique URL pour view+edit).
|
- migration réversible (downgrade : prendre le premier élément, ré-injecter dans les scalaires, drop `techniques`)
|
||||||
|
- [ ] AC-13.3 : sérialisation simulation expose `techniques: [{id, name, tactics: [...]}]` — le backend enrichit chaque entrée avec ses `tactics` depuis le service MITRE au moment du serialize (snapshot d'`id`+`name` en DB, tactics dérivées au runtime depuis le bundle).
|
||||||
|
- [ ] AC-13.4 : `PATCH /api/simulations/<sid>` accepte `{technique_ids: ["T1059", "T1078"]}` (liste d'IDs string). Backend valide chaque ID contre le bundle MITRE, résout `name`, écrit `[{id, name}]` en DB. ID inconnu → 400 `{error: "unknown technique id: T9999"}`.
|
||||||
|
- [ ] AC-13.5 : la règle d'auto-transition `pending → in_progress` s'applique aussi à `technique_ids` quand la liste reçue est non vide.
|
||||||
|
|
||||||
### US-8 — En tant que redteam, je renseigne les détails techniques d'une simulation
|
### US-14 — En tant que redteam, je vois et retire les techniques d'une simulation sous forme de tags
|
||||||
**Pourquoi** : c'est la trace de ce que la redteam a exécuté.
|
**Pourquoi** : visualiser rapidement la couverture TTP d'un test.
|
||||||
|
|
||||||
**Critères d'acceptation**
|
**Critères d'acceptation**
|
||||||
- [ ] AC-8.1 : `PATCH /api/simulations/<sid>` (admin|redteam) accepte les champs redteam : `name`, `mitre_technique_id`, `mitre_technique_name`, `description`, `commands` (texte multiligne, une commande par ligne), `prerequisites`, `executed_at` (ISO datetime), `execution_result`. Champs partiels OK.
|
- [ ] AC-14.1 : sur `SimulationFormPage`, à la place du seul `MitreTechniquePicker` du sprint 2, un composant `MitreTechniquesField` affiche :
|
||||||
- [ ] AC-8.2 : règle d'auto-transition pending → in_progress. Trigger PRÉCIS : `PATCH /api/simulations/<sid>` par admin|redteam où **le payload JSON contient au moins une clé parmi les champs redteam** (`name`, `mitre_technique_id`, `mitre_technique_name`, `description`, `commands`, `prerequisites`, `executed_at`, `execution_result`) **dont la valeur n'est ni `null` ni une string vide ni une liste vide**, ET status courant == `pending`. La comparaison se fait sur le payload entrant — pas sur l'état final de la simulation. Un PATCH qui ne ré-envoie qu'un champ inchangé (ex: même `name`) déclenche quand même la transition, car c'est une action explicite "la redteam saisit". L'auto-transition ne se déclenche jamais sur un PATCH `soc`.
|
- Liste des techniques sélectionnées sous forme de chips/tags (id + name, ex : `T1059 — Command and Scripting Interpreter`), avec un `×` cliquable pour retirer chaque technique.
|
||||||
- [ ] AC-8.3 : `commands` est stocké en colonne `text` (chaîne multiligne, une commande par ligne). Sérialisation API = texte brut tel que stocké. Le frontend affiche dans un `<textarea>`.
|
- Bouton "Add technique" qui ouvre la modale matrice (US-15).
|
||||||
- [ ] AC-8.4 : `executed_at` valide ISO 8601 ou null. Si invalide → 400 `{error: "invalid executed_at"}`.
|
- Bouton "Quick search" qui ouvre l'autocomplete existant (réutilisation du `MitreTechniquePicker`) en mode "ajoute à la liste" (sélection = append, pas replace).
|
||||||
- [ ] AC-8.5 : page `/engagements/:eid/simulations/:sid` affiche un formulaire avec deux sections visibles ("Red Team" et "SOC"). Pour admin/redteam, les deux sections sont éditables. Validation client : `name` non vide.
|
- État vide : message "No techniques selected — use the matrix or the quick search to add."
|
||||||
- [ ] AC-8.6 : autocomplete MITRE dans le champ "Technique" — voir US-10.
|
- [ ] AC-14.2 : retirer un tag (× sur le chip) déclenche un PATCH immédiat (auto-save) avec la liste mise à jour. La modale matrice (US-15) auto-save aussi via "Apply". Le picker Quick Search auto-save chaque sélection. Toast `'Techniques updated'` sur succès, toast erreur sinon. Pas de bouton Save manuel pour les techniques.
|
||||||
|
- [ ] AC-14.3 : sur `SimulationList` (table dans EngagementDetailPage), la colonne "MITRE" affiche un compteur + premier tag (ex : `T1059 +2` si 3 techniques sélectionnées). Si la liste est vide, afficher `—`.
|
||||||
|
- [ ] AC-14.4 : ordre des tags dans la simulation préservé entre lecture et écriture (pas de tri imposé côté serveur).
|
||||||
|
- [ ] AC-14.5 : tags affichés avec les couleurs/spacing DESIGN.md (`bg-primary-soft`, `text-primary-deep`, `rounded-full`, `px-md py-xxs`).
|
||||||
|
|
||||||
### US-9 — En tant qu'analyste SOC, je remplis ma partie de la simulation
|
### US-15 — En tant que redteam, j'ouvre la matrice MITRE ATT&CK pour explorer et sélectionner des techniques
|
||||||
**Pourquoi** : le SOC documente la détection sans toucher au scope redteam.
|
**Pourquoi** : l'autocomplete est efficace si on sait ce qu'on cherche ; la matrice est nécessaire pour "voir ce qui existe" et combiner par tactique.
|
||||||
|
|
||||||
**Critères d'acceptation**
|
**Critères d'acceptation**
|
||||||
- [ ] AC-9.1 : `PATCH /api/simulations/<sid>` envoyé par un user `soc` n'accepte QUE les champs SOC : `log_source`, `logs`, `soc_comment`, `incident_number`. Si la requête contient un champ redteam → 403 `{error: "soc cannot edit redteam fields"}`.
|
- [ ] AC-15.1 : nouvel endpoint `GET /api/mitre/matrix` (auth, tous rôles) → tree `[{tactic_id, tactic_name, techniques: [{id, name, subtechniques: [{id, name}]}]}]`. Chaque technique top-level embarque ses sub-techniques (`T1059` → `[T1059.001, T1059.002, ...]`). Ordre des tactiques = ordre canonique MITRE Enterprise (Initial Access → Execution → Persistence → ... → Exfiltration → Impact). 503 si bundle non chargé.
|
||||||
- [ ] AC-9.2 : un user `soc` ne peut PATCH une simulation que si son status est `review_required` ou `done`. Avant ça → 403 `{error: "simulation not ready for SOC review"}`.
|
- [ ] AC-15.2 : composant `MitreMatrixModal` :
|
||||||
- [ ] AC-9.3 : page `/engagements/:eid/simulations/:sid` pour un user `soc` : la section "Red Team" est rendue en read-only (champs grisés) ; la section "SOC" est éditable.
|
- Modal large (≥ 1100px), scroll vertical interne.
|
||||||
- [ ] AC-9.4 : si la simulation est en `pending` ou `in_progress` et qu'un soc visite la page, un bandeau "Simulation pas encore en revue — la redteam doit la marquer comme 'Review required' avant que vous puissiez intervenir" s'affiche, les champs SOC sont désactivés.
|
- Layout horizontal en colonnes : 1 colonne par tactique. Header de colonne = nom de la tactique + compteur de techniques sélectionnées dans cette tactique (sub-techniques incluses).
|
||||||
|
- Chaque technique top-level = bouton/cellule cliquable. État sélectionné visible (`bg-primary` + texte blanc).
|
||||||
|
- Si la technique a des sub-techniques (`subtechniques.length > 0`), un chevron (▸/▾) précède le nom. Click sur le chevron = expand/collapse (n'affecte PAS la sélection). Click sur le label = toggle sélection de la technique top-level.
|
||||||
|
- Sub-techniques affichées en cascade indentée sous leur parent quand expand. Cliquables individuellement (toggle de la sub). État visuel distinct : `bg-primary-soft` quand sélectionnée, indent `pl-md`, font-size légèrement plus petit.
|
||||||
|
- Sélectionner une sub-technique ne sélectionne PAS le parent (les deux sont indépendants côté data). Mais le compteur de tactique somme parent + subs sélectionnées.
|
||||||
|
- Champ de recherche en haut du modal qui filtre les techniques affichées (case-insensitive sur id ET name). Quand le filtre matche une sub-technique, son parent est automatiquement expand pour la rendre visible.
|
||||||
|
- Boutons en footer : "Cancel" (ferme sans appliquer), "Apply N techniques" (compteur = total parents + subs sélectionnés).
|
||||||
|
- [ ] AC-15.3 : la modale est ouverte depuis le bouton "Add technique" de US-14. Elle reçoit en input la liste actuelle de techniques sélectionnées et travaille sur une copie locale ; "Apply" déclenche directement le PATCH (auto-save, cf AC-14.2) et ferme la modale ; "Cancel" jette le diff local.
|
||||||
|
- [ ] AC-15.4 : Escape ferme la modale (= Cancel). Click sur le backdrop = Cancel.
|
||||||
|
- [ ] AC-15.5 : a11y V1 — **scope minimal explicite** : (1) focus initial sur le champ recherche à l'ouverture, (2) Tab cycle entre les éléments focusables de la modale (wrap : dernier élément → premier), (3) Escape ferme = onCancel, (4) ARIA `role="dialog"` + `aria-labelledby` sur le titre. Full WAI-ARIA dialog conformance (live regions, focus restoration au close, screen reader announcements détaillés) **out of scope V1** — c'est une dette assumée à reprendre dans un sprint a11y dédié.
|
||||||
|
|
||||||
### US-10 — En tant que redteam, j'autocomplète une technique MITRE ATT&CK
|
### US-16 — En tant que user (tous rôles), j'utilise les autres fonctionnalités sans régression
|
||||||
**Pourquoi** : éviter de taper l'id à la main, garantir la cohérence.
|
**Critères d'acceptation** (régression)
|
||||||
|
- [ ] AC-16.1 : workflow sprint 2 (auto-transition, transitions manuelles, RBAC SOC) inchangé — tous les ACs sprint 2 (US-7 → US-12) continuent de passer.
|
||||||
**Critères d'acceptation**
|
- [ ] AC-16.2 : l'ancien `MitreTechniquePicker` est conservé dans la base de code MAIS sa signature passe en clean rewrite (`onSelect({id, name})` au lieu de `onChange(id, name)`), wrappé par `MitreTechniquesField` en mode append.
|
||||||
- [ ] AC-10.1 : `make update-mitre` télécharge le bundle STIX 2.1 Enterprise depuis `https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json` et l'écrit dans `backend/data/mitre/enterprise-attack.json`. Le bundle est COMMITTÉ dans le repo (`make build` reste autosuffisant). `make update-mitre` reste l'unique méthode de rafraîchissement et le diff résultant est committé manuellement.
|
- [ ] AC-16.3 : aucune e2e sprint 1/sprint 2 ne casse. Quelques assertions sprint 2 (US-8 et US-10) qui validaient le mono-technique sont mises à jour pour refléter la liste.
|
||||||
- [ ] AC-10.2 : `GET /api/mitre/techniques?q=<query>` (auth, tous rôles) → liste max 20 résultats `[{id, name, tactics: ["initial-access", ...]}]`. Recherche full-text sur `id` (ex: "T1059") OU `name` (ex: "Command and Scripting Interpreter"), case-insensitive, ordonnée : match exact id > match préfixe id > match nom.
|
|
||||||
- [ ] AC-10.3 : si le bundle local est absent → endpoint répond 503 `{error: "mitre bundle not loaded"}`. Le team-lead documente `make update-mitre` dans le README.
|
|
||||||
- [ ] AC-10.4 : sous-techniques (id format `T1059.001`) incluses dans l'index.
|
|
||||||
- [ ] AC-10.5 : composant frontend `MitreTechniquePicker` : input + dropdown des matches (debounce 200ms, navigation clavier ↑↓ + Enter, Escape ferme le dropdown), affichage `T1059 — Command and Scripting Interpreter (initial-access)`. La sélection d'une suggestion remplit `mitre_technique_id` ET `mitre_technique_name` du form. Pas de fallback free-text : si l'utilisateur tape sans sélectionner, le champ technique reste vide en sortie de form (id et name `null`).
|
|
||||||
|
|
||||||
### US-11 — En tant qu'utilisateur, je transitionne le workflow d'une simulation
|
|
||||||
**Pourquoi** : la coordination redteam ↔ soc passe par le statut.
|
|
||||||
|
|
||||||
**Critères d'acceptation**
|
|
||||||
- [ ] AC-11.1 : `POST /api/simulations/<sid>/transition {to: "review_required"}` → 200, requiert status courant ∈ {`pending`, `in_progress`} et role ∈ {admin, redteam}. Refuse les autres transitions → 409 `{error: "invalid transition"}`.
|
|
||||||
- [ ] AC-11.2 : `POST /api/simulations/<sid>/transition {to: "done"}` → 200, requiert status courant == `review_required` et role ∈ {admin, redteam, soc}. Autres transitions → 409.
|
|
||||||
- [ ] AC-11.3 : aucune transition arrière (ex: done → pending) n'est permise. Pas de transition `→ pending` ni `→ in_progress` via cet endpoint (le passage à `in_progress` est strictement automatique cf AC-8.2).
|
|
||||||
- [ ] AC-11.4 : sur la page d'édition simulation, deux boutons contextuels :
|
|
||||||
- Pour admin/redteam, status ∈ {pending, in_progress} : bouton "Marquer en revue".
|
|
||||||
- Pour admin/redteam/soc, status == review_required : bouton "Clôturer".
|
|
||||||
- Sinon : boutons cachés.
|
|
||||||
- [ ] AC-11.5 : après transition réussie, la query simulation et la liste sont invalidées (TanStack Query), le badge se met à jour.
|
|
||||||
|
|
||||||
### US-12 — En tant qu'admin ou redteam, je supprime une simulation
|
|
||||||
**Critères d'acceptation**
|
|
||||||
- [ ] AC-12.1 : `DELETE /api/simulations/<sid>` (admin|redteam) → 204.
|
|
||||||
- [ ] AC-12.2 : `soc` → 403.
|
|
||||||
- [ ] AC-12.3 : suppression d'engagement (cascade) supprime toutes ses simulations.
|
|
||||||
- [ ] AC-12.4 : bouton "Supprimer" sur la page d'édition (admin/redteam uniquement), avec confirmation modal.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Brief technique — Backend Builder
|
## 2. Brief technique — Backend Builder
|
||||||
|
|
||||||
**Scope strict** : `backend/`, `docker/`, `Makefile` (target `update-mitre`).
|
**Scope strict** : `backend/`. Pas de touche au frontend, e2e, docs (team-lead).
|
||||||
|
|
||||||
### Livrables
|
### Livrables
|
||||||
|
|
||||||
**Modèle `Simulation`** (`backend/app/models/simulation.py`)
|
**Modèle `Simulation`** (`backend/app/models/simulation.py`)
|
||||||
| Champ | Type | Notes |
|
- Remplacer `mitre_technique_id`, `mitre_technique_name` (str nullable) par :
|
||||||
|---|---|---|
|
```python
|
||||||
| id | int PK | |
|
techniques: Mapped[list[dict]] = mapped_column(JSON, nullable=False, default=list)
|
||||||
| engagement_id | int FK Engagement, CASCADE | requis |
|
```
|
||||||
| name | str, NOT NULL | redteam-side |
|
- Stockage : `[{"id": "T1059", "name": "Command and Scripting Interpreter"}, ...]`. Pas de `tactics` en DB (dérivé au serialize).
|
||||||
| mitre_technique_id | str, nullable | ex "T1059" / "T1059.001" |
|
|
||||||
| mitre_technique_name | str, nullable | snapshot pour résilience aux maj MITRE |
|
|
||||||
| description | text, nullable | redteam-side |
|
|
||||||
| commands | text, nullable | chaîne multiligne, une commande par ligne — pas de JSON |
|
|
||||||
| prerequisites | text, nullable | redteam-side |
|
|
||||||
| executed_at | datetime, nullable | redteam-side |
|
|
||||||
| execution_result | text, nullable | redteam-side |
|
|
||||||
| log_source | text, nullable | soc-side |
|
|
||||||
| logs | text, nullable | soc-side |
|
|
||||||
| soc_comment | text, nullable | soc-side |
|
|
||||||
| incident_number | str, nullable | soc-side |
|
|
||||||
| status | enum(pending/in_progress/review_required/done), défaut `pending` | |
|
|
||||||
| created_at | datetime | |
|
|
||||||
| updated_at | datetime, nullable | mis à jour à chaque PATCH |
|
|
||||||
| created_by_id | int FK User | |
|
|
||||||
|
|
||||||
**Migration Alembic** `0002_add_simulations.py` — table `simulations` + FK indexes (`engagement_id`, `created_by_id`).
|
**Service workflow** (`backend/app/services/simulation_workflow.py`) — **mise à jour RBAC field-level OBLIGATOIRE**
|
||||||
|
- Dans le `REDTEAM_FIELDS` frozenset existant : **retirer** `"mitre_technique_id"` et `"mitre_technique_name"`, **ajouter** `"technique_ids"`.
|
||||||
|
- Sans ce changement : un user soc qui PATCH avec `{technique_ids: [...]}` reçoit un silent no-op (champ ignoré) au lieu du 403 attendu. La gate field-level RBAC pour `technique_ids` repose intégralement sur ce frozenset.
|
||||||
|
- Le `SOC_FIELDS` frozenset reste inchangé.
|
||||||
|
- Tester explicitement : `test_simulations_techniques.py` doit inclure "SOC PATCH technique_ids → 403" (cf. liste de tests plus bas).
|
||||||
|
|
||||||
**Endpoints** (nouveau blueprint `backend/app/api/simulations.py`)
|
**Migration Alembic `0003_simulation_techniques_array.py`**
|
||||||
- `GET /api/engagements/<eid>/simulations` — list, auth, all roles
|
- Upgrade :
|
||||||
- `POST /api/engagements/<eid>/simulations` — create, admin|redteam
|
1. Ajouter colonne `techniques` (JSON, nullable=True temporaire, default `'[]'`) — `op.add_column` direct OK.
|
||||||
- `GET /api/simulations/<sid>` — get, auth
|
2. Data migration : pour chaque ligne, si `mitre_technique_id` IS NOT NULL → set `techniques = '[{"id":"<id>","name":"<name>"}]'`, sinon `'[]'`.
|
||||||
- `PATCH /api/simulations/<sid>` — update avec RBAC field-level (voir AC-8/9)
|
3. ALTER column `techniques` → nullable=False — **OBLIGATOIRE via `op.batch_alter_table('simulations', ...)`** car SQLite ne supporte pas ALTER COLUMN nativement.
|
||||||
- `DELETE /api/simulations/<sid>` — admin|redteam
|
4. Drop columns `mitre_technique_id`, `mitre_technique_name` — **OBLIGATOIRE via `op.batch_alter_table('simulations', ...)`** (même raison : SQLite ne supporte pas DROP COLUMN hors batch mode).
|
||||||
- `POST /api/simulations/<sid>/transition` — state machine
|
- Downgrade : symétrique avec les MÊMES guards batch_alter_table pour les étapes ALTER/DROP. Recrée les 2 colonnes, prend le premier élément de `techniques` si non vide, drop `techniques`.
|
||||||
- `GET /api/mitre/techniques?q=` — autocomplete (200 OK + array, 503 si bundle absent)
|
- Pattern à suivre : la migration `0002_add_simulations.py` (sprint 2) — vérifier le style batch_alter_table déjà en place.
|
||||||
|
|
||||||
**Serializer** : retourne `created_by={id, username}` (pattern existant). `commands` → string brut (tel que stocké en DB, peut être `null` ou chaîne multiligne).
|
**Serializer** (`backend/app/serializers.py`)
|
||||||
|
- `serialize_simulation(sim)` :
|
||||||
**Service workflow** (`backend/app/services/simulation_workflow.py`)
|
- Avant retour, enrichir chaque tag avec `tactics` depuis `mitre_svc.get_tactics(id)`. Si la technique a été retirée du bundle MITRE entre-temps, `tactics = []` (gracieux).
|
||||||
- `apply_patch(simulation, payload, user)` :
|
- `commands` reste tel quel (text brut, inchangé sprint 2).
|
||||||
- sépare champs redteam vs soc
|
|
||||||
- vérifie RBAC field-level
|
|
||||||
- détecte auto-transition pending → in_progress (AC-8.2)
|
|
||||||
- applique le patch + commit
|
|
||||||
- `transition(simulation, to_status, user)` :
|
|
||||||
- vérifie state machine (transitions autorisées)
|
|
||||||
- vérifie RBAC role
|
|
||||||
- met à jour status + updated_at
|
|
||||||
|
|
||||||
**Service MITRE** (`backend/app/services/mitre.py`)
|
**Service MITRE** (`backend/app/services/mitre.py`)
|
||||||
- Au boot de l'app : tente de charger `backend/data/mitre/enterprise-attack.json` en mémoire ; si absent ou parse error → flag `mitre_loaded = False` (logue warning, app démarre quand même).
|
- Étendre l'index avec un dict `tactics_by_technique: dict[str, list[str]]` pour lookup O(1) au serialize.
|
||||||
- Indexe les objets STIX `type == "attack-pattern"` : extract `external_id` (T-id), `name`, `kill_chain_phases[].phase_name`.
|
- Nouvelle fonction `get_tactics(technique_id: str) -> list[str]`.
|
||||||
- Fonction `search(query, limit=20)` : ranking par exact-id > prefix-id > substring-name.
|
- Nouvelle fonction `lookup_name(technique_id: str) -> str | None` — utilisée par l'endpoint PATCH pour résoudre le name côté serveur (le client n'envoie que les IDs).
|
||||||
|
- Nouvelle fonction `get_matrix() -> list[dict]` :
|
||||||
**`Makefile`** : remplacer le no-op de `update-mitre` par :
|
```json
|
||||||
```makefile
|
[
|
||||||
MITRE_URL ?= https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json
|
{"tactic_id": "TA0001", "tactic_name": "Initial Access",
|
||||||
update-mitre:
|
"techniques": [
|
||||||
@mkdir -p backend/data/mitre
|
{"id": "T1078", "name": "Valid Accounts",
|
||||||
@curl -fsSL "$(MITRE_URL)" -o backend/data/mitre/enterprise-attack.json
|
"subtechniques": [{"id": "T1078.001", "name": "Default Accounts"}, ...]},
|
||||||
@echo "MITRE bundle updated"
|
...
|
||||||
@if docker ps --format '{{.Names}}' | grep -q "^$(CONTAINER)$$"; then \
|
]},
|
||||||
echo "Restarting $(CONTAINER) to reload MITRE bundle..."; \
|
...
|
||||||
docker restart $(CONTAINER); \
|
]
|
||||||
fi
|
|
||||||
```
|
```
|
||||||
|
Sub-techniques embarquées sous chaque parent (relation STIX `subtechnique-of` dans le bundle). Si la technique n'en a pas, `subtechniques: []`.
|
||||||
|
Ordre des tactiques : canonical MITRE Enterprise order (12 tactics). Lecture depuis les objets STIX `x-mitre-tactic` ordonnés par `x_mitre_shortname` natif OU constante module-level hardcodée si plus simple.
|
||||||
|
Ordre des techniques au sein d'une tactique : alphabétique par `name` (déterministe, lisible).
|
||||||
|
|
||||||
**Dockerfile** : copier `backend/data/mitre/` dans l'image (présent dans le repo, donc fonctionne au premier build).
|
**API** (`backend/app/api/simulations.py`)
|
||||||
|
- `GET /api/mitre/matrix` — nouvel endpoint, 200 + tree, 503 si bundle absent.
|
||||||
|
- `PATCH /api/simulations/<sid>` : le payload accepte maintenant `technique_ids: list[str]` à la place de `mitre_technique_id` + `mitre_technique_name`. Validation : tous les IDs doivent exister dans le bundle (400 sinon), `name` snapshot servi par `lookup_name`. Pas de rétrocompat avec les anciens champs scalaires (clean break — pas d'utilisateur externe).
|
||||||
|
- **Dedup serveur** : avant écriture en DB, dédupliquer la liste `technique_ids` en préservant l'ordre (`list(dict.fromkeys(technique_ids))`). Le client peut envoyer accidentellement des doublons (race UI ou bug), le serveur ne doit jamais persister deux fois la même technique.
|
||||||
|
- Auto-transition (AC-13.5) : un `technique_ids` non vide (≥1 élément) compte comme redteam-side filled, déclenche `pending → in_progress`. Liste vide = pas de trigger.
|
||||||
|
|
||||||
**Bundle MITRE** : committé dans le repo à `backend/data/mitre/enterprise-attack.json`. Le backend-builder l'inclut dans son premier commit via `make update-mitre`.
|
**Tests pytest**
|
||||||
|
- `test_simulations_techniques.py` (nouveau) :
|
||||||
|
- Création + PATCH `technique_ids` → simulation a la bonne liste, sérialisation expose `techniques` avec `tactics`.
|
||||||
|
- PATCH avec ID inconnu → 400.
|
||||||
|
- Auto-transition sur `technique_ids` non vide.
|
||||||
|
- Retirer toutes les techniques (`technique_ids: []`) → pas de trigger d'auto-transition (cohérent avec règle "valeur vide").
|
||||||
|
- **Dedup** : PATCH avec `technique_ids: ["T1059", "T1078", "T1059"]` → DB ne stocke que 2 entrées, ordre préservé (`T1059` en premier).
|
||||||
|
- `test_mitre.py` (existant) — ajouter :
|
||||||
|
- `get_matrix()` renvoie les bonnes tactiques dans le bon ordre.
|
||||||
|
- `lookup_name(unknown)` → None.
|
||||||
|
- `get_tactics(known)` → liste correcte (≥1 tactique).
|
||||||
|
- `test_simulations_crud.py` + `test_simulations_patch.py` + `test_simulations_workflow.py` (existants) — adapter toute assertion qui touchait `mitre_technique_id` / `mitre_technique_name`.
|
||||||
|
- Migration : test que les anciennes simulations en `pending` avec un id mono-tech sont upgradées en `techniques: [{id, name}]` (fixture inline ou test direct sur Alembic).
|
||||||
|
|
||||||
**Tests pytest** (`backend/tests/`)
|
**Quality bar** : ruff + mypy clean, tous les tests existants + nouveaux verts.
|
||||||
- `test_simulations_crud.py` : create + list + get + delete + cascade, RBAC create/delete.
|
|
||||||
- `test_simulations_patch.py` : auto-transition pending→in_progress, RBAC field-level soc, blocage soc avant review_required (AC-9.2).
|
|
||||||
- `test_simulations_workflow.py` : transitions valides/invalides, RBAC par transition.
|
|
||||||
- `test_mitre.py` : load bundle (fixture mini), search ranking, endpoint 503 si pas chargé, sous-techniques incluses.
|
|
||||||
|
|
||||||
Tous les tests existants doivent rester verts. Lint ruff + mypy clean.
|
|
||||||
|
|
||||||
### Règles
|
|
||||||
- Pas de touche au frontend.
|
|
||||||
- Pas d'invention de dépendances (pas besoin d'en ajouter).
|
|
||||||
- Renvoyer le summary attendu (cf. `.claude/agents/backend-builder.md`).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Brief technique — Frontend Builder
|
## 3. Brief technique — Frontend Builder
|
||||||
|
|
||||||
**Scope strict** : `frontend/` UNIQUEMENT. Interdiction de toucher `e2e/`.
|
**Scope strict** : `frontend/` uniquement.
|
||||||
|
|
||||||
|
**Note process (lesson learned sprint 2)** : avant de marquer la tâche terminée, lance le dev server et screenshot (a) la matrice modale ouverte avec ≥3 techniques sélectionnées et (b) la simulation form avec ≥2 tags affichés. Joins-les à ton summary final.
|
||||||
|
|
||||||
### Livrables
|
### Livrables
|
||||||
|
|
||||||
**Types** (`frontend/src/api/types.ts`) : ajouter `Simulation`, `SimulationStatus`, `MitreTechnique`, et les payloads PATCH/POST.
|
**Types** (`frontend/src/api/types.ts`)
|
||||||
|
- `MitreTechnique`: `{id: string, name: string, tactics: string[]}` (déjà existant pour le picker — réutiliser, ajouter `tactics` si manquant).
|
||||||
|
- Ajouter `MitreTactic`: `{tactic_id: string, tactic_name: string, techniques: MitreMatrixTechnique[]}` avec `MitreMatrixTechnique = {id: string, name: string, subtechniques: {id: string, name: string}[]}`.
|
||||||
|
- `Simulation.techniques: MitreTechnique[]` à la place de `mitre_technique_id` + `mitre_technique_name`. PATCH payload : `{technique_ids: string[]}`.
|
||||||
|
|
||||||
**Client API** (`frontend/src/api/simulations.ts`, `frontend/src/api/mitre.ts`)
|
**API client** (`frontend/src/api/mitre.ts`)
|
||||||
- `listSimulations(engagementId)`, `createSimulation(engagementId, {name})`, `getSimulation(id)`, `updateSimulation(id, patch)`, `deleteSimulation(id)`, `transitionSimulation(id, to)`.
|
- `searchMitreTechniques(q)` — existant, garder.
|
||||||
- `searchMitreTechniques(query)`.
|
- `getMitreMatrix()` — nouveau, GET `/api/mitre/matrix`.
|
||||||
|
|
||||||
**Hooks TanStack Query** (`frontend/src/hooks/useSimulations.ts`)
|
**Hooks** (`frontend/src/hooks/useMitre.ts`)
|
||||||
- `useEngagementSimulations(engagementId)`, `useSimulation(id)`, mutations `useCreateSimulation`, `useUpdateSimulation`, `useDeleteSimulation`, `useTransitionSimulation`.
|
- `useMitreSearch(q, enabled)` — existant, garder.
|
||||||
- Invalidation : transition + update + delete invalident `["simulations", id]` et `["engagements", eid, "simulations"]`.
|
- `useMitreMatrix(enabled)` — nouveau hook TanStack Query, `staleTime: Infinity` (la matrice ne change qu'avec `make update-mitre` + redémarrage).
|
||||||
|
|
||||||
**Hook `useMitre`** : `useMitreSearch(query, enabled)` (debounce géré côté composant, hook sans staleTime court — cache 5min).
|
**Composants**
|
||||||
|
|
||||||
|
- **`MitreTechniqueTag.tsx`** (nouveau) : chip affichant `{id} — {name}` avec un bouton `×`. Props : `technique: MitreTechnique`, `onRemove: () => void`, `disabled?: boolean`.
|
||||||
|
|
||||||
|
- **`MitreTechniquesField.tsx`** (nouveau, dans `frontend/src/components/`) : conteneur qui orchestre la sélection multi-tech avec **auto-save** (PATCH déclenché par chaque add/remove/Apply).
|
||||||
|
- Props : `value: MitreTechnique[]`, `simulationId: number`, `disabled?: boolean`. (Pas de `onChange` du parent — le composant fait son propre PATCH via `useUpdateSimulation`.)
|
||||||
|
- UI : liste de `<MitreTechniqueTag>` + 2 boutons "Add technique" (ouvre matrix) et "Quick search" (ouvre/toggle picker autocomplete inline).
|
||||||
|
- Dédup : si l'utilisateur essaye d'ajouter une technique déjà présente, no-op silencieux (pas de PATCH non plus).
|
||||||
|
- Auto-save : chaque mutation (× sur tag, Apply matrice, sélection Quick Search) déclenche `useUpdateSimulation` avec `{technique_ids: [...]}`. Toast succès `'Techniques updated'`, toast erreur sinon. Pendant le PATCH : disable l'interaction (les × deviennent grisés, les boutons disabled).
|
||||||
|
|
||||||
|
- **`MitreMatrixModal.tsx`** (nouveau) : modale matrice avec sub-techniques expand/collapse.
|
||||||
|
- Props : `isOpen: boolean`, `initialSelection: MitreTechnique[]`, `onApply: (selection: MitreTechnique[]) => void`, `onCancel: () => void`.
|
||||||
|
- État local : (a) copie de `initialSelection` mutée par les toggles, (b) `expandedTechniques: Set<string>` pour les IDs parents dépliés.
|
||||||
|
- Layout : flex horizontal scrollable, 1 colonne par tactique. Largeur fixe 220px par colonne pour cohérence visuelle.
|
||||||
|
- Chevron `▸/▾` à gauche du nom des techniques qui ont des sub-techniques (`subtechniques.length > 0`). Click chevron = toggle expand (mute le set `expandedTechniques`), ne modifie PAS la sélection.
|
||||||
|
- Click sur le label d'une technique top-level = toggle sa sélection (le chevron ne se déclenche pas dans ce cas — séparer les zones cliquables).
|
||||||
|
- Sub-techniques rendues en cascade indentée sous leur parent quand expand : `pl-md text-[12px] bg-cloud rounded` (vs parent `text-[14px]`). Cliquables individuellement, sélection indépendante du parent.
|
||||||
|
- Compteur header de tactique = nombre de techniques **parents + subs** sélectionnées dans cette tactique.
|
||||||
|
- Champ recherche en haut : filtre case-insensitive sur id ET name. Une sub-technique matchée force l'expand de son parent (modifie automatiquement `expandedTechniques`).
|
||||||
|
- Modale : `position: fixed`, backdrop `bg-ink/60`, container `bg-canvas rounded-xl shadow-elevated max-w-[95vw] max-h-[85vh] overflow-hidden`.
|
||||||
|
- Footer : "Cancel" (jette les changements locaux + ferme), "Apply N techniques" (compteur total ; click → onApply renvoie la liste complète, parent fait le PATCH auto-save US-14.2).
|
||||||
|
- Focus trap (scope minimal V1, cf AC-15.5) :
|
||||||
|
- `useEffect` au mount → `searchInputRef.current?.focus()`.
|
||||||
|
- `onKeyDown` au niveau du container modale :
|
||||||
|
- `Tab` sans shift sur le dernier élément focusable → `preventDefault()` + focus le premier.
|
||||||
|
- `Shift+Tab` sur le premier → `preventDefault()` + focus le dernier.
|
||||||
|
- Récupérer la liste des focusables via `container.querySelectorAll('a, button, input, [tabindex]:not([tabindex="-1"])')`, ignorer ceux `disabled` ou `hidden`.
|
||||||
|
- Pas de focus restoration ni de live region — out of scope V1.
|
||||||
|
- Pas de dépendance npm.
|
||||||
|
- Escape → onCancel. Click backdrop → onCancel.
|
||||||
|
|
||||||
|
- **`MitreTechniquePicker.tsx`** (existant, sprint 2) : clean rewrite de la signature.
|
||||||
|
- Avant : `onChange(id: string | null, name: string | null)` qui remplaçait la valeur.
|
||||||
|
- Après : `onSelect({id, name})` — un seul match sélectionné, le parent (MitreTechniquesField) gère l'append + le dédup.
|
||||||
|
- Plus de prop `techniqueId`/`techniqueName` en entrée (le picker est désormais un sélecteur "one-shot" qui se réinitialise après chaque sélection).
|
||||||
|
|
||||||
**Pages**
|
**Pages**
|
||||||
- `EngagementDetailPage.tsx` : remplacer le placeholder (lignes 74-81) par `<SimulationList engagementId={eng.id} />`. Conserver le reste.
|
|
||||||
- `SimulationFormPage.tsx` (`/engagements/:eid/simulations/new` et `/engagements/:eid/simulations/:sid/edit`) :
|
|
||||||
- Layout en deux cards : "Red Team" et "SOC".
|
|
||||||
- Champs redteam : name, MitreTechniquePicker, description, commands (textarea, une commande par ligne, envoyé tel quel — pas de split), prerequisites, executed_at (datetime-local input), execution_result.
|
|
||||||
- Champs SOC : log_source, logs, soc_comment, incident_number.
|
|
||||||
- Boutons en footer : "Save", "Marquer en revue" (si AC-11.4), "Clôturer" (si AC-11.4), "Supprimer" (modal de confirmation, admin/redteam).
|
|
||||||
- Mode création (`new`) : seul `name` requis ; après création, redirige sur `/engagements/:eid/simulations/:sid/edit`.
|
|
||||||
|
|
||||||
**Composants** (`frontend/src/components/`)
|
- **`SimulationFormPage.tsx`** : remplacer le `<MitreTechniquePicker>` standalone par un `<MitreTechniquesField simulationId={sim.id}>`. Le state `rt.techniques` disparait du form (les techniques ont leur propre cycle de save via le champ lui-même — auto-save). Le bouton "Save Red Team" continue de batcher tous les AUTRES champs (name, description, commands, etc.) mais ne touche pas aux techniques. Affichage read-only (rôle SOC) : afficher les tags sans `×`, boutons Add/Quick Search masqués (`disabled` prop).
|
||||||
- `SimulationList.tsx` : table tri par created_at desc, colonnes (Name, MITRE, Status badge, Executed at), bouton "Nouvelle" si admin/redteam, ligne cliquable → navigate edit page.
|
|
||||||
- `SimulationStatusBadge.tsx` : variant du StatusBadge existant si possible (factoriser), 4 couleurs (pending=fog, in_progress=primary-soft, review_required=bloom-coral, done=storm-deep). Si le StatusBadge existant n'est pas factorisable proprement, créer un nouveau composant — pas d'over-engineering.
|
|
||||||
- `MitreTechniquePicker.tsx` : input + dropdown, debounce 200ms (`useDebouncedValue` ou util inline), navigation clavier (↑/↓/Enter/Escape), affichage `T1059 — Command and Scripting Interpreter (initial-access)`. Loading state inline.
|
|
||||||
- `ConfirmDialog.tsx` : modal générique de confirmation (utilisée pour delete).
|
|
||||||
|
|
||||||
**Routing** (`App.tsx`)
|
- **`SimulationList.tsx`** : colonne MITRE — afficher `techniques[0]?.id + (techniques.length > 1 ? ` +${techniques.length - 1}` : '')`. Si `techniques` est vide, afficher `—`.
|
||||||
- Ajouter `/engagements/:eid/simulations/new` (auth, admin|redteam)
|
|
||||||
- Ajouter `/engagements/:eid/simulations/:sid/edit` (auth, all roles, RBAC champs interne)
|
|
||||||
|
|
||||||
**Tests Vitest** (`frontend/tests/`)
|
**Tests Vitest**
|
||||||
- `SimulationList.test.tsx` : loading/error/empty + bouton "Nouvelle" gated par role.
|
- `MitreTechniqueTag.test.tsx` — render id+name, click × appelle onRemove.
|
||||||
- `MitreTechniquePicker.test.tsx` : autocomplete debounce, sélection met à jour, navigation clavier.
|
- `MitreTechniquesField.test.tsx` — affiche tags, "Add technique" ouvre le modal matrix, "Quick search" ouvre le picker, dédup silencieuse, remove via × appelle onChange avec liste mise à jour.
|
||||||
- `SimulationFormPage.test.tsx` : rôle redteam → tous champs éditables ; rôle soc → champs RT disabled, soc-side enabled si status review_required, bandeau si pending.
|
- `MitreMatrixModal.test.tsx` — render colonnes par tactique, click toggle sélection, Apply renvoie liste, Cancel jette, Escape ferme, search filtre.
|
||||||
- `SimulationStatusBadge.test.tsx` : 4 variants.
|
- Adapter `MitreTechniquePicker.test.tsx` (sprint 2) à la nouvelle signature `onSelect`.
|
||||||
|
- Adapter `SimulationFormPage.test.tsx` (sprint 2) — assertions sur `techniques` array au lieu de scalaire.
|
||||||
|
|
||||||
|
**Quality bar** : typecheck + lint + vitest clean.
|
||||||
|
|
||||||
### Règles
|
### Règles
|
||||||
- Lit le summary du backend EN PREMIER (contrat API).
|
- Lit le summary backend EN PREMIER.
|
||||||
- Pas d'invention d'endpoints. Mismatch → escalade au team-lead.
|
- Pas d'invention d'endpoints — `GET /api/mitre/matrix` est le seul nouveau, déjà spec'd.
|
||||||
- Réutiliser `LoadingState`, `ErrorState`, `EmptyState`, `Toast`, `FormField`, `StatusBadge` existants. NE PAS dupliquer.
|
- Réutiliser `LoadingState`, `ErrorState`, `ConfirmDialog`, `useToast`, action bar pattern (sprint 2) existants.
|
||||||
- Respect DESIGN.md (utiliser tokens Tailwind existants — pas de couleurs hardcodées).
|
- Respect DESIGN.md tokens (palette + spacing). Tags = `bg-primary-soft text-primary-deep rounded-full px-md py-xxs gap-xxs text-[14px]`.
|
||||||
- Pas de CDN remote.
|
- Pas de nouvelle dépendance npm sans escalade au team-lead.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Definition of Done — Sprint 2
|
## 4. Brief — Test verifier
|
||||||
|
|
||||||
- [ ] Tous les critères AC-7 → AC-12 passent.
|
E2e Playwright. Un fichier par US :
|
||||||
- [ ] `pytest` (existing 63 + nouveaux ~25) tous verts. `ruff`, `mypy` clean.
|
- `us13-multi-techniques.spec.ts` — AC-13.1 → AC-13.5 (focus API + données)
|
||||||
- [ ] `npm run typecheck`, `lint`, `test` clean frontend.
|
- `us14-techniques-tags.spec.ts` — AC-14.1 → AC-14.5 (UI tags + remove)
|
||||||
- [ ] Playwright suite (existing 36 + nouveaux ~15) verte.
|
- `us15-mitre-matrix-modal.spec.ts` — AC-15.1 → AC-15.5 (modal interaction + a11y)
|
||||||
- [ ] `make build` + `make start` + `make update-mitre` + workflow simulation complet manuel OK.
|
- `us16-regression-sprint2.spec.ts` — re-exécuter les ACs critiques sprint 2 (auto-transition US-8, workflow US-11, SOC restrictions US-9) avec le nouveau modèle.
|
||||||
- [ ] Code-reviewer (Opus) sans BLOCKER ouvert.
|
|
||||||
- [ ] `SPEC.md` (section Simulation enrichie si besoin), `README.md` (mention `make update-mitre` + workflow), `CHANGELOG.md` à jour.
|
Mettre à jour les e2e sprint 2 qui assertaient `mitre_technique_id` / `mitre_technique_name` scalaires (US-8, US-10 selon le grep).
|
||||||
- [ ] PR ouverte sur `sprint/2-simulations`, récap synthétique team-lead, validation utilisateur.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Décisions arrêtées (utilisateur 2026-05-26)
|
## 5. Definition of Done — Sprint 3
|
||||||
|
|
||||||
1. **Source MITRE** : `https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json` (default team-lead).
|
- [ ] Tous les AC US-13 → US-16 passent.
|
||||||
2. **MITRE bundle dans le repo** : COMMITTÉ (`backend/data/mitre/enterprise-attack.json` versionné, `make build` autosuffisant).
|
- [ ] Backend tests verts (`pytest -q`). Ruff + mypy clean.
|
||||||
3. **Commands storage** : colonne `text` multiligne, une commande par ligne, transport tel quel.
|
- [ ] Frontend tests verts (`npm run test -- --run`). Typecheck + lint clean.
|
||||||
4. **Workflow auto-transition** pending→in_progress : déclenchée par toute PATCH admin/redteam touchant ≥1 champ redteam à valeur non vide (default team-lead).
|
- [ ] E2e Playwright suite verte (sprint 1 + 2 + 3).
|
||||||
5. **Page simulation** : UNE page d'édition role-aware (`/engagements/:eid/simulations/:sid/edit`), pas de page détail séparée.
|
- [ ] Migration Alembic testée upgrade + downgrade.
|
||||||
6. **Suppression cascade** : delete engagement → delete simulations (default team-lead).
|
- [ ] SPEC.md mis à jour (multi-techniques acté).
|
||||||
7. **SOC restriction status** : soc ne peut PATCH que si status ∈ {review_required, done}.
|
- [ ] README.md mis à jour (mention matrice + multi-tech dans la description workflow).
|
||||||
8. **Sous-techniques MITRE** : incluses dans l'autocomplete (T1059.001 visible) (default team-lead).
|
- [ ] CHANGELOG.md sprint 3 entry sous [Unreleased].
|
||||||
|
- [ ] Code-reviewer sans BLOCKER.
|
||||||
|
- [ ] **Frontend-builder a screenshot la matrice modale + la simulation form avec tags AVANT de marquer la tâche terminée (lesson learned sprint 2).**
|
||||||
|
- [ ] PR ouverte + récap synthétique team-lead.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Plan d'exécution (séquence)
|
## 6. Décisions arrêtées (utilisateur 2026-05-27)
|
||||||
|
|
||||||
1. ✅ User a validé les 8 décisions §5 (2026-05-26).
|
1. **Storage multi-tech** : colonne JSON `[{id, name}]` (KISS, pattern `commands` sprint 2).
|
||||||
2. ✅ **Spec-reviewer** : APPROVED WITH NOTES (4 items mineurs corrigés avant dispatch).
|
2. **Sub-techniques dans la matrice** : OUI, affichées avec expand/collapse par technique parent. Sub-techniques sont aussi accessibles via Quick Search en plus.
|
||||||
3. ✅ **Backend-builder** : commit `006c4c2` (67 nouveaux tests, 130 passing).
|
3. **API shape** : `PATCH` reçoit `{technique_ids: ["T1059", "T1059.001", ...]}` — IDs uniquement (parents et subs au même niveau dans la liste). Backend résout names depuis le bundle.
|
||||||
4. ✅ **Frontend-builder** : commit `765bb5a` (41 nouveaux tests, 61 passing).
|
4. **Rétrocompat** : migration backfill `[{id, name}]` depuis les scalaires. Pas de rétrocompat API.
|
||||||
5. ✅ **Code-reviewer** : 2 MAJOR + 4 MINOR + 3 NITs → 2 commits de fix (`83bf60f` backend, `c9032a9`+`cf0e8a8` frontend).
|
5. **MitreTechniquePicker** : clean rewrite de la signature (`onSelect({id, name})`).
|
||||||
6. ✅ **Test-verifier** : 32/32 sprint 2 verts, commits `da905cc` + `54e90f7` (AC-4.9 refresh).
|
6. **Matrix layout** : colonnes par tactique, 220px fixe, scroll horizontal global.
|
||||||
7. 🟡 **Team-lead** : récap + PR en cours.
|
7. **Apply de la modale matrice** : auto-save immédiat (PATCH déclenché par `MitreTechniquesField` quand le modal renvoie sa liste via `onApply`). Add/remove via tag × ou Quick Search aussi auto-save.
|
||||||
|
8. **Sprint 4 framing** (anticipation, NE PAS implémenter dans sprint 3) : Dark mode (toggle + tokens dark + persistence) + Hygiène process UI (`design-reviewer` agent + screenshot mandatory dans brief frontend-builder). Connecteur C2 reporté au-delà. Les builders sprint 3 N'ajoutent PAS de tokens dark, N'invoquent PAS le design-reviewer (qui n'existe pas encore). Seule la lesson `screenshots mandatory` est déjà appliquée en sprint 3 dans le brief frontend (§3).
|
||||||
|
|
||||||
Branche unique : `sprint/2-simulations`.
|
---
|
||||||
|
|
||||||
|
## 7. Plan d'exécution
|
||||||
|
|
||||||
|
1. ✅ User a validé les 8 décisions §6 (2026-05-27).
|
||||||
|
2. ✅ Team-lead a mis à jour SPEC.md (§0).
|
||||||
|
3. ✅ Spec-reviewer : APPROVED WITH NOTES après 2 passes (5 items au total, tous traités).
|
||||||
|
4. ✅ Backend-builder : commits `b5ea292` + `673b25e` (model + migration + matrix endpoint + 503 unloaded, 162 passing).
|
||||||
|
5. ✅ Frontend-builder : commit `771483f` (MitreTechniquesField + MitreMatrixModal + tags + auto-save + screenshots, 84 passing).
|
||||||
|
6. ✅ Code-reviewer : APPROVED WITH NITS (2 MINORs + 4 NITs).
|
||||||
|
7. ✅ Post-review fixes : `4596f09` + `393b6ed` backend (164 passing) + `39f4076` frontend (86 passing).
|
||||||
|
8. ✅ Test-verifier : commit `df8a6b6` (105/106 sprint 3 e2e verts, 1 pré-existant sprint 1 — DB pollution, non-régression).
|
||||||
|
9. 🟡 Team-lead : récap + PR en cours.
|
||||||
|
4. 🔵 Backend-builder : modèle + migration + endpoints + tests.
|
||||||
|
5. 🔵 Frontend-builder : composants + page update + tests Vitest. Screenshots obligatoires avant "done".
|
||||||
|
6. 🔵 Code-reviewer : LSP-first.
|
||||||
|
7. 🔵 Test-verifier : e2e US-13 → US-16 + adaptation sprint 2.
|
||||||
|
8. 🟢 Team-lead : PR + récap.
|
||||||
|
|||||||
Reference in New Issue
Block a user