- README: status bump to sprint 2, blueprints + workflow + MITRE section, test counts refreshed (131/63/68) - CHANGELOG: sprint 2 entry under [Unreleased]; sprint 1 moved to its own [Sprint 1] section - tasks/lessons.md: 5 lessons captured (3 frontend testing gotchas, agent-reuse via SendMessage, e2e refresh on placeholder supersession) - 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>
253 lines
18 KiB
Markdown
253 lines
18 KiB
Markdown
# Sprint 2 — Simulations + MITRE ATT&CK
|
|
|
|
**Branche** : `sprint/2-simulations`
|
|
**Statut** : 🟢 SPRINT COMPLET — 32 acceptance tests sprint 2 verts, code-review traité (2 MAJOR + 2 MINOR + 2 NITs fixés), PR prête
|
|
**Base** : `main` (sprint 1 mergé en `7fc79cc`)
|
|
**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.
|
|
|
|
---
|
|
|
|
## 1. User stories
|
|
|
|
### US-7 — En tant que redteam, je crée une simulation dans un engagement
|
|
**Pourquoi** : c'est la feature centrale du sprint 2.
|
|
|
|
**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-7.2 : autres rôles (soc) → 403.
|
|
- [ ] AC-7.3 : engagement inexistant → 404. Engagement existant mais aucune simulation → liste vide.
|
|
- [ ] AC-7.4 : `GET /api/engagements/<eid>/simulations` (auth) → liste des simulations de l'engagement, ordonnée `created_at desc`.
|
|
- [ ] 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.
|
|
- [ ] 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).
|
|
|
|
### US-8 — En tant que redteam, je renseigne les détails techniques d'une simulation
|
|
**Pourquoi** : c'est la trace de ce que la redteam a exécuté.
|
|
|
|
**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-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`.
|
|
- [ ] 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>`.
|
|
- [ ] AC-8.4 : `executed_at` valide ISO 8601 ou null. Si invalide → 400 `{error: "invalid executed_at"}`.
|
|
- [ ] 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.
|
|
- [ ] AC-8.6 : autocomplete MITRE dans le champ "Technique" — voir US-10.
|
|
|
|
### US-9 — En tant qu'analyste SOC, je remplis ma partie de la simulation
|
|
**Pourquoi** : le SOC documente la détection sans toucher au scope redteam.
|
|
|
|
**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-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-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.
|
|
- [ ] 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.
|
|
|
|
### US-10 — En tant que redteam, j'autocomplète une technique MITRE ATT&CK
|
|
**Pourquoi** : éviter de taper l'id à la main, garantir la cohérence.
|
|
|
|
**Critères d'acceptation**
|
|
- [ ] 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-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
|
|
|
|
**Scope strict** : `backend/`, `docker/`, `Makefile` (target `update-mitre`).
|
|
|
|
### Livrables
|
|
|
|
**Modèle `Simulation`** (`backend/app/models/simulation.py`)
|
|
| Champ | Type | Notes |
|
|
|---|---|---|
|
|
| id | int PK | |
|
|
| engagement_id | int FK Engagement, CASCADE | requis |
|
|
| name | str, NOT NULL | redteam-side |
|
|
| 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`).
|
|
|
|
**Endpoints** (nouveau blueprint `backend/app/api/simulations.py`)
|
|
- `GET /api/engagements/<eid>/simulations` — list, auth, all roles
|
|
- `POST /api/engagements/<eid>/simulations` — create, admin|redteam
|
|
- `GET /api/simulations/<sid>` — get, auth
|
|
- `PATCH /api/simulations/<sid>` — update avec RBAC field-level (voir AC-8/9)
|
|
- `DELETE /api/simulations/<sid>` — admin|redteam
|
|
- `POST /api/simulations/<sid>/transition` — state machine
|
|
- `GET /api/mitre/techniques?q=` — autocomplete (200 OK + array, 503 si bundle absent)
|
|
|
|
**Serializer** : retourne `created_by={id, username}` (pattern existant). `commands` → string brut (tel que stocké en DB, peut être `null` ou chaîne multiligne).
|
|
|
|
**Service workflow** (`backend/app/services/simulation_workflow.py`)
|
|
- `apply_patch(simulation, payload, user)` :
|
|
- 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`)
|
|
- 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).
|
|
- Indexe les objets STIX `type == "attack-pattern"` : extract `external_id` (T-id), `name`, `kill_chain_phases[].phase_name`.
|
|
- Fonction `search(query, limit=20)` : ranking par exact-id > prefix-id > substring-name.
|
|
|
|
**`Makefile`** : remplacer le no-op de `update-mitre` par :
|
|
```makefile
|
|
MITRE_URL ?= https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json
|
|
update-mitre:
|
|
@mkdir -p backend/data/mitre
|
|
@curl -fsSL "$(MITRE_URL)" -o backend/data/mitre/enterprise-attack.json
|
|
@echo "MITRE bundle updated"
|
|
@if docker ps --format '{{.Names}}' | grep -q "^$(CONTAINER)$$"; then \
|
|
echo "Restarting $(CONTAINER) to reload MITRE bundle..."; \
|
|
docker restart $(CONTAINER); \
|
|
fi
|
|
```
|
|
|
|
**Dockerfile** : copier `backend/data/mitre/` dans l'image (présent dans le repo, donc fonctionne au premier build).
|
|
|
|
**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** (`backend/tests/`)
|
|
- `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
|
|
|
|
**Scope strict** : `frontend/` UNIQUEMENT. Interdiction de toucher `e2e/`.
|
|
|
|
### Livrables
|
|
|
|
**Types** (`frontend/src/api/types.ts`) : ajouter `Simulation`, `SimulationStatus`, `MitreTechnique`, et les payloads PATCH/POST.
|
|
|
|
**Client API** (`frontend/src/api/simulations.ts`, `frontend/src/api/mitre.ts`)
|
|
- `listSimulations(engagementId)`, `createSimulation(engagementId, {name})`, `getSimulation(id)`, `updateSimulation(id, patch)`, `deleteSimulation(id)`, `transitionSimulation(id, to)`.
|
|
- `searchMitreTechniques(query)`.
|
|
|
|
**Hooks TanStack Query** (`frontend/src/hooks/useSimulations.ts`)
|
|
- `useEngagementSimulations(engagementId)`, `useSimulation(id)`, mutations `useCreateSimulation`, `useUpdateSimulation`, `useDeleteSimulation`, `useTransitionSimulation`.
|
|
- Invalidation : transition + update + delete invalident `["simulations", id]` et `["engagements", eid, "simulations"]`.
|
|
|
|
**Hook `useMitre`** : `useMitreSearch(query, enabled)` (debounce géré côté composant, hook sans staleTime court — cache 5min).
|
|
|
|
**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/`)
|
|
- `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`)
|
|
- Ajouter `/engagements/:eid/simulations/new` (auth, admin|redteam)
|
|
- Ajouter `/engagements/:eid/simulations/:sid/edit` (auth, all roles, RBAC champs interne)
|
|
|
|
**Tests Vitest** (`frontend/tests/`)
|
|
- `SimulationList.test.tsx` : loading/error/empty + bouton "Nouvelle" gated par role.
|
|
- `MitreTechniquePicker.test.tsx` : autocomplete debounce, sélection met à jour, navigation clavier.
|
|
- `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.
|
|
- `SimulationStatusBadge.test.tsx` : 4 variants.
|
|
|
|
### Règles
|
|
- Lit le summary du backend EN PREMIER (contrat API).
|
|
- Pas d'invention d'endpoints. Mismatch → escalade au team-lead.
|
|
- Réutiliser `LoadingState`, `ErrorState`, `EmptyState`, `Toast`, `FormField`, `StatusBadge` existants. NE PAS dupliquer.
|
|
- Respect DESIGN.md (utiliser tokens Tailwind existants — pas de couleurs hardcodées).
|
|
- Pas de CDN remote.
|
|
|
|
---
|
|
|
|
## 4. Definition of Done — Sprint 2
|
|
|
|
- [ ] Tous les critères AC-7 → AC-12 passent.
|
|
- [ ] `pytest` (existing 63 + nouveaux ~25) tous verts. `ruff`, `mypy` clean.
|
|
- [ ] `npm run typecheck`, `lint`, `test` clean frontend.
|
|
- [ ] Playwright suite (existing 36 + nouveaux ~15) verte.
|
|
- [ ] `make build` + `make start` + `make update-mitre` + workflow simulation complet manuel OK.
|
|
- [ ] Code-reviewer (Opus) sans BLOCKER ouvert.
|
|
- [ ] `SPEC.md` (section Simulation enrichie si besoin), `README.md` (mention `make update-mitre` + workflow), `CHANGELOG.md` à jour.
|
|
- [ ] PR ouverte sur `sprint/2-simulations`, récap synthétique team-lead, validation utilisateur.
|
|
|
|
---
|
|
|
|
## 5. Décisions arrêtées (utilisateur 2026-05-26)
|
|
|
|
1. **Source MITRE** : `https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json` (default team-lead).
|
|
2. **MITRE bundle dans le repo** : COMMITTÉ (`backend/data/mitre/enterprise-attack.json` versionné, `make build` autosuffisant).
|
|
3. **Commands storage** : colonne `text` multiligne, une commande par ligne, transport tel quel.
|
|
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).
|
|
5. **Page simulation** : UNE page d'édition role-aware (`/engagements/:eid/simulations/:sid/edit`), pas de page détail séparée.
|
|
6. **Suppression cascade** : delete engagement → delete simulations (default team-lead).
|
|
7. **SOC restriction status** : soc ne peut PATCH que si status ∈ {review_required, done}.
|
|
8. **Sous-techniques MITRE** : incluses dans l'autocomplete (T1059.001 visible) (default team-lead).
|
|
|
|
---
|
|
|
|
## 6. Plan d'exécution (séquence)
|
|
|
|
1. ✅ User a validé les 8 décisions §5 (2026-05-26).
|
|
2. ✅ **Spec-reviewer** : APPROVED WITH NOTES (4 items mineurs corrigés avant dispatch).
|
|
3. ✅ **Backend-builder** : commit `006c4c2` (67 nouveaux tests, 130 passing).
|
|
4. ✅ **Frontend-builder** : commit `765bb5a` (41 nouveaux tests, 61 passing).
|
|
5. ✅ **Code-reviewer** : 2 MAJOR + 4 MINOR + 3 NITs → 2 commits de fix (`83bf60f` backend, `c9032a9`+`cf0e8a8` frontend).
|
|
6. ✅ **Test-verifier** : 32/32 sprint 2 verts, commits `da905cc` + `54e90f7` (AC-4.9 refresh).
|
|
7. 🟡 **Team-lead** : récap + PR en cours.
|
|
|
|
Branche unique : `sprint/2-simulations`.
|