> Découpage en 14 milestones livrables indépendamment. Chaque milestone a une **DoD** vérifiable. Cocher au fil de l'eau, documenter les écarts dans `CHANGELOG.md`, retours d'expérience dans `tasks/lessons.md`.
## Convention
- ☐ = à faire · ☑ = fait · ⚠ = bloqué (commenter) · ↻ = en cours
- Chaque PR doit : passer lint/typecheck, mettre à jour `CHANGELOG.md`, mettre à jour `README.md` si surface utilisateur.
- **Chaque milestone livre un fichier `tasks/testing-m<N>.md`** (procédure manuelle + automatisée) **et au moins un spec Playwright `e2e/tests/m<N>-*.spec.ts`**.
- À la fin de chaque milestone : lancer le subagent `spec-reviewer` (HARD RULE 4 du CLAUDE.md global) avant de marquer le milestone done.
---
## M0 — Bootstrap repo & infra ☐
**But** : squelette buildable de bout en bout sans aucune feature métier.
- ☐ `backend/` (Flask 3, Python 3.12, `pyproject.toml` avec uv ou poetry, structure `app/{api,core,db,models,services,i18n}`)
- ☐ Logs JSON structurés sur stdout (`python-json-logger`).
**DoD** : `make up` démarre les 3 conteneurs ; `curl http://localhost:${HOST_FRONT_PORT:-8080}/api/v1/health` renvoie `{ "status": "ok", "version": "..." }` (proxifié par nginx via `api:8000`) ; le front sur `:8080` affiche une page d'accueil au design RTOps ; **`make e2e` passe les 8 tests Playwright** ; rapport HTML dans `e2e/playwright-report/`. Procédure complète : `tasks/testing-m0.md`. *En prod, la TLS est terminée par un reverse proxy externe (cf. spec §6 NF-network) — la stack compose ne sert que du HTTP.*
---
## M1 — Schéma DB & migrations Alembic ☐
**But** : modèle de données complet versionné, sans logique métier.
- ☐ Bootstrap : commande `flask metamorph print-install-token` génère + persiste un token unique au 1er démarrage (si table `users` vide), écrit dans les logs au boot.
- ☐ Endpoint `POST /setup` : consomme le token d'install, crée le 1er admin (groupe `admin` seedé).
- ☐ Invitations : `POST /invitations` (admin, génère token 7j), `GET /invitations/{token}` (preview), `POST /invitations/{token}/accept` (création compte avec password choisi).
- ☐ Décorateur `@require_perm` qui vérifie l'union des perms via tous les groupes du user.
- ☐ Front : page Admin > Users (liste, recherche, modale d'édition des groupes), Admin > Groups (CRUD + multi-select des perms), Admin > Invitations (liste, créer, révoquer).
- ☐ UI : on n'affiche pas les actions interdites (mais le serveur reste l'arbitre).
**DoD** : un admin peut créer un groupe `pentest-2026-Q2` avec uniquement `mission.read` + `mission.write_red_fields`, l'attribuer à Bob ; Bob voit les missions auxquelles il est membre mais ne peut pas écrire dans les champs blue (HTTP 403 au niveau API).
**DoD** : après `make seed-mitre`, `GET /mitre/tactics` retourne 14 tactics Enterprise ; le picker permet de tagger un test avec « TA0002 / T1059.001 » et l'enregistrement est persistant.
- ☐ Front : page Admin > Tests (liste filtrable par tactic / OPSEC / tag), modale d'édition (form complet avec markdown editor — `@uiw/react-md-editor` ou équivalent léger).
- ☐ Front : page Admin > Scénarios, drag-and-drop avec `@dnd-kit/sortable`.
- ☐ Filtres : recherche full-text sur nom/desc, facettes MITRE/OPSEC/tags.
**DoD** : admin crée 5 tests + 1 scénario de 3 tests réordonnés ; recharge la page → ordre persistant ; suppression soft-delete d'un template n'efface pas les scénarios.
---
## M6 — Missions & snapshot ☐
**But** : transformer les templates en missions vivantes.
- ☐ Modèle `mission` : nom, client/cible (texte), date_start, date_end, status (`enum draft/in_progress/completed/archived`), description (markdown), `visibility_mode` figé à `whitebox` v1.
- ☐ `mission_members` : (mission_id, user_id, role_hint `red|blue`) — rôle hint informatif, l'autorisation reste portée par les permissions.
- ☐ Lors de la création/modification d'une mission, sélection de scénarios → **snapshot** : copie complète des `scenario_templates` et `test_templates` dans `mission_scenarios` / `mission_tests` (y compris tags MITRE).
- ☐ Endpoints : `POST /missions`, `GET /missions` (filtré par perms + membership pour les non-admin), `GET /missions/{id}` (avec scénarios+tests), `PUT /missions/{id}` (métadonnées + ajout de scénarios → snapshot), `POST /missions/{id}/transition` (drift de status), `DELETE /missions/{id}` (soft).
- ☐ Front : page Missions (liste + filtres status/client/dates), création (wizard 3 étapes : meta → scénarios → membres), vue mission (header + onglets Tests / Membres / Synthèse / Export).
- ☐ Vue mission : tableau des tests avec colonnes Tactic | Test | Statut | Niveau de détection | Last update, actions selon perms.
**DoD** : red crée une mission avec 1 scénario de 3 tests, ajoute Alice (red) et Bob (blue) ; modification ultérieure d'un test_template ne change rien dans la mission (snapshot préservé).
---
## M7 — Saisie red & blue sur un test ☐
**But** : exécution de la mission, le cœur du produit.
- ☐ Modale ou page dédiée `Mission > Test #N` avec deux zones distinctes (red / blue), bordures accentuées par couleur (rouge / cyan).
- ☐ Côté red : champ commande (mono), output (mono multiline), commentaire markdown, bouton « Marquer exécuté » qui set `state=executed` + `executed_at=now()` ; édition de `executed_at` derrière un toggle « override ».
- ☐ Côté blue : sélecteur `detection_level`, commentaire markdown, zone d'upload multi-fichiers (drag-and-drop).
- ☐ Permissions : tout endpoint d'écriture vérifie `mission.write_red_fields` ou `mission.write_blue_fields` selon le champ touché ; les deux peuvent coexister sur un même groupe (pas exclusifs en code).
- ☐ Indicateur « modifié par X il y a Ns » : polling `GET /missions/{id}/activity?since=…` toutes les 15 s tant que la page est active.
**DoD** : red et blue saisissent en parallèle sans conflit ; un user sans `write_blue_fields` reçoit 403 sur les champs blue ; un fichier .evtx de 24 Mo est uploadé, un de 26 Mo est rejeté ; le hash SHA256 est correct.
---
## M8 — Niveaux de détection custom ☐
**But** : la taxonomie d'icônes du slide est paramétrable.
- ☐ Garde-fou : empêcher la suppression si utilisé dans des `mission_tests` (proposer désactivation).
- ☐ Front : page Admin > Settings > Detection Levels (table + modale, picker de color_token parmi les 10 accents du design).
**DoD** : admin renomme `not_detected` → `missed`, ajoute `false_positive` avec accent purple ; les missions existantes affichent les nouveaux libellés ; un blueteamer voit la nouvelle option dans le sélecteur.
---
## M9 — Notifications in-app ☐
**But** : red et blue savent quand l'autre a agi.
- ☐ Service `notify(user_id, type, payload)` appelé sur transitions clés : `test_executed`, `test_reviewed_by_blue`, `evidence_added`, `mission_status_changed`.
- ☐ Front : badge dans le header avec compteur, dropdown listant les 20 dernières + lien vers la mission/test.
- ☐ Polling `GET /notifications?unread_only=true` toutes les 30 s (ou WebSocket plus tard, hors scope).
**DoD** : Bob (blue) reçoit un badge « Test #4 prêt à review » 30 s max après qu'Alice (red) clique « Marquer exécuté ».
---
## M10 — Génération du slide reveal.js ☐
**But** : livrable client de fin de mission.
- ☐ Backend : endpoint `GET /missions/{id}/slide.html` qui calcule l'agrégat (tests groupés par MITRE Tactic, comptages par detection_level, plus regroupements custom si configurés).
- ☐ Côté serveur, on émet **un seul fichier HTML standalone** : reveal.js inliné (CSS + JS), tokens design.md inlinés, données JSON inlinées, **aucune ressource externe**.
- ☐ Layout : slide titre, slide « Méthodologie », une slide par Tactic avec liste des techniques/tests + icône colorée par detection_level, slide synthèse (matrice tactic × detection_level), slide annexes (preuves référencées par titre, sans binaires).
- ☐ Bouton « Export PDF » dans le slide → `print-pdf` reveal.js (`window.print()` + media query reveal).
- ☐ Front : page Mission > Synthèse avec preview iframe + bouton « Télécharger HTML ».
- ☐ Conformité design : `// ` headings en cyan, accents par detection_level, JetBrains Mono partout.
**DoD** : on télécharge `mission-X.html`, on l'ouvre offline dans Firefox/Chrome, navigation reveal OK, export PDF côté navigateur produit un PDF lisible.
---
## M11 — Exports JSON & CSV ☐
**But** : sortie des données brutes pour archivage.
- ☐ `GET /missions/{id}/export.json` : mission + scénarios + tests + niveaux de détection + métadonnées preuves (sans binaires, mais avec hash + filename).
- ☐ `GET /missions/{id}/export.csv` : une ligne par test (cols : test_name, mitre_tactic, mitre_technique, mitre_subtechnique, executed_at, status, red_command, detection_level, blue_comment_excerpt).
- ☐ Front : boutons d'export sur la page mission, headers `Content-Disposition: attachment`.
**DoD** : `curl -OJ` sur les deux endpoints donne deux fichiers cohérents et complets ; le JSON peut être réimporté dans le futur (laisser cet import en backlog).
---
## M12 — Soft delete & purge admin ☐
**But** : aucune perte de donnée par accident, ménage explicite.
- ☐ Toutes les `DELETE` du back deviennent `UPDATE deleted_at`.
- ☐ Tous les `GET` filtrent `deleted_at IS NULL` par défaut, paramètre `?include_deleted=true` réservé aux admins.
- ☐ Endpoint `POST /admin/purge` (perm admin) avec body `{entity, ids}` qui DELETE physiquement (suppression fichiers preuves incluse).
- ☐ Commande `flask metamorph purge-soft-deleted --older-than 30d` (manuelle, pas de cron auto).
- ☐ Front : page Admin > Trash (filtrée par entity), bouton « Restaurer » + bouton « Purger ».
**DoD** : suppression d'un test depuis l'UI → disparait des listes mais reste en DB ; admin peut le restaurer ; admin peut le purger définitivement, le fichier evidence associé disparait du disque.
---
## M13 — i18n FR / EN ☐
**But** : commutation de langue par utilisateur.
- ☐ Backend : `flask-babel`, deux locales `fr` / `en`. Messages d'erreur API via `gettext`. Fichier `messages.pot` extrait via `pybabel extract`.
- ☐ Frontend : `react-i18next`, namespaces par page, fichiers `frontend/src/i18n/{fr,en}/*.json`.
- ☐ Préférence user : champ `users.locale` (default `fr`), endpoint `PATCH /auth/me {locale}`, switch dans le header.
- ☐ Données MITRE conservées en EN (officielles, non traduites).
- ☐ Tous les libellés UI passent par `t('…')` — interdit le texte en dur.
**DoD** : Bob change sa langue en EN, recharge → toute l'UI en EN sauf les noms ATT&CK ; un message d'erreur API arrive aussi en EN.