- 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>
18 KiB
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", ...}.namerequis, 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éecreated_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 ninullni 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êmename) 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 PATCHsoc. - AC-8.3 :
commandsest stocké en colonnetext(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_atvalide ISO 8601 ou null. Si invalide → 400{error: "invalid executed_at"}. - AC-8.5 : page
/engagements/:eid/simulations/:sidaffiche un formulaire avec deux sections visibles ("Red Team" et "SOC"). Pour admin/redteam, les deux sections sont éditables. Validation client :namenon 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 usersocn'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
socne peut PATCH une simulation que si son status estreview_requiredoudone. Avant ça → 403{error: "simulation not ready for SOC review"}. - AC-9.3 : page
/engagements/:eid/simulations/:sidpour un usersoc: 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
pendingouin_progresset 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-mitretélécharge le bundle STIX 2.1 Enterprise depuishttps://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.jsonet l'écrit dansbackend/data/mitre/enterprise-attack.json. Le bundle est COMMITTÉ dans le repo (make buildreste autosuffisant).make update-mitrereste 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 surid(ex: "T1059") OUname(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 documentemake update-mitredans 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), affichageT1059 — Command and Scripting Interpreter (initial-access). La sélection d'une suggestion remplitmitre_technique_idETmitre_technique_namedu form. Pas de fallback free-text : si l'utilisateur tape sans sélectionner, le champ technique reste vide en sortie de form (id et namenull).
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_requiredet role ∈ {admin, redteam, soc}. Autres transitions → 409. - AC-11.3 : aucune transition arrière (ex: done → pending) n'est permise. Pas de transition
→ pendingni→ in_progressvia cet endpoint (le passage àin_progressest 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 rolesPOST /api/engagements/<eid>/simulations— create, admin|redteamGET /api/simulations/<sid>— get, authPATCH /api/simulations/<sid>— update avec RBAC field-level (voir AC-8/9)DELETE /api/simulations/<sid>— admin|redteamPOST /api/simulations/<sid>/transition— state machineGET /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.jsonen mémoire ; si absent ou parse error → flagmitre_loaded = False(logue warning, app démarre quand même). - Indexe les objets STIX
type == "attack-pattern": extractexternal_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 :
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), mutationsuseCreateSimulation,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/newet/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) : seulnamerequis ; 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 (useDebouncedValueou util inline), navigation clavier (↑/↓/Enter/Escape), affichageT1059 — 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,StatusBadgeexistants. 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,mypyclean.npm run typecheck,lint,testclean 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(mentionmake 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)
- Source MITRE :
https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json(default team-lead). - MITRE bundle dans le repo : COMMITTÉ (
backend/data/mitre/enterprise-attack.jsonversionné,make buildautosuffisant). - Commands storage : colonne
textmultiligne, une commande par ligne, transport tel quel. - Workflow auto-transition pending→in_progress : déclenchée par toute PATCH admin/redteam touchant ≥1 champ redteam à valeur non vide (default team-lead).
- Page simulation : UNE page d'édition role-aware (
/engagements/:eid/simulations/:sid/edit), pas de page détail séparée. - Suppression cascade : delete engagement → delete simulations (default team-lead).
- SOC restriction status : soc ne peut PATCH que si status ∈ {review_required, done}.
- Sous-techniques MITRE : incluses dans l'autocomplete (T1059.001 visible) (default team-lead).
6. Plan d'exécution (séquence)
- ✅ User a validé les 8 décisions §5 (2026-05-26).
- ✅ Spec-reviewer : APPROVED WITH NOTES (4 items mineurs corrigés avant dispatch).
- ✅ Backend-builder : commit
006c4c2(67 nouveaux tests, 130 passing). - ✅ Frontend-builder : commit
765bb5a(41 nouveaux tests, 61 passing). - ✅ Code-reviewer : 2 MAJOR + 4 MINOR + 3 NITs → 2 commits de fix (
83bf60fbackend,c9032a9+cf0e8a8frontend). - ✅ Test-verifier : 32/32 sprint 2 verts, commits
da905cc+54e90f7(AC-4.9 refresh). - 🟡 Team-lead : récap + PR en cours.
Branche unique : sprint/2-simulations.