The hierarchical 3-column drill-down was hard to scan and forced a stateful walk per tag. Replaced with a flat, columns-as-tactics matrix that mirrors attack.mitre.org/# — every cell is a one-click select target, with inline sub-technique expand via a `+N` chevron. - New endpoint GET /api/v1/mitre/matrix returns the full grid (tactics → techniques → sub-techniques nested) in a single ~55 KB response, so the SPA renders the whole matrix without firing 15 parallel queries. Two pytest tests added (nested structure + auth required). - MitreTagPicker.tsx rewritten as a horizontal-scrolling matrix: - Click a tactic header → select the tactic (cyan filled). - Click a technique cell → select the technique (orange filled). - Click the `+N` chevron → expand sub-techniques inline within the column. - Click a sub-technique → select (purple filled). - Single Filter field matches on external_id or name across all kinds. - Selection chips at the top, clickable to remove. - `aria-pressed` on every clickable cell for screen readers and Playwright. - e2e test updated to walk the new flow (click cell → assert aria-pressed, expand chevron, click sub, verify chip + JSON preview, filter to T1078). - Spec §F2 + §F12 + todo.md M4 entry updated to make the matrix layout the canonical UI for MITRE tagging (so future spec-reviewer passes accept it). - testing-m4.md walkthrough rewritten for the flat picker. DoD post-refactor: make test-api → 53 passed (was 51), make e2e → 34 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
type, date, tags, status, project, spec
| type | date | tags | status | project | spec | ||
|---|---|---|---|---|---|---|---|
| todo | 2026-05-08 |
|
in_progress | Metamorph | tasks/spec.md |
Metamorph — Plan d'implémentation
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 danstasks/lessons.md.
Convention
- ☐ = à faire · ☑ = fait · ⚠ = bloqué (commenter) · ↻ = en cours
- Branches :
feature/m<N>-<slug>· commits :feat(m<N>): …/fix(m<N>): … - Chaque PR doit : passer lint/typecheck, mettre à jour
CHANGELOG.md, mettre à jourREADME.mdsi surface utilisateur. - Chaque milestone livre un fichier
tasks/testing-m<N>.md(procédure manuelle + automatisée) et au moins un spec Playwrighte2e/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.tomlavec uv ou poetry, structureapp/{api,core,db,models,services,i18n}) - ☐
frontend/(Vite + React 18 + TS strict, Tailwind 3, ESLint + Prettier, alias@/) - ☐ Tokens design
tasks/design.mdtraduits enfrontend/tailwind.config.ts(palette CSS vars, typo JetBrains Mono / IBM Plex Sans, radii 3/4/6/10). - ☐ Composants UI de base :
<Card>,<Tag>,<SectionHeader>(avec//),<FlowNode>,<Button>— fidèles au design. - ☐
docker-compose.yml: servicesapi,db(postgres:16-alpine),front(nginx servant le bundle Vite). - ☐ Dockerfile multi-stage par service ; volumes nommés
metamorph_db,metamorph_evidence. - ☐
Makefile:dev,build,up,down,migrate,seed-mitre,lint,test. - ☐ Pré-commit hook :
ruff(back),eslint+tsc --noEmit(front). - ☐
README.mdminimal (run en dev, run en prod, variables d'env attendues). - ☐
.gitignore:.env,*.exe,*.dll,__pycache__/,node_modules/,dist/,data/. - ☐
.env.exampledocumenté (POSTGRES_*,JWT_SECRET,LOG_LEVEL,FRONT_ORIGIN). - ☐ 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.
- ☐ Configurer SQLAlchemy 2.x + Alembic.
- ☐ Tables auth/RBAC :
users,groups,permissions,user_groups,group_permissions,invitations,refresh_tokens. - ☐ Tables MITRE :
mitre_tactics,mitre_techniques,mitre_subtechniques(avecexternal_id,name,description,url). - ☐ Tables templates :
test_templates,test_template_mitre_tags(jointure many-to-many tactic/technique/subtechnique),scenario_templates,scenario_template_tests(avecposition). - ☐ Tables missions :
missions,mission_members,mission_scenarios(snapshot),mission_tests(snapshot + state),mission_test_mitre_tags,mission_categories(custom). - ☐ Tables exécution :
evidence_files(FKmission_test_id,sha256,mime,size_bytes,storage_path,original_filename). - ☐ Tables paramétrage :
detection_levels(clé, label_fr, label_en, color_token, position, is_default),settings(key/value). - ☐ Table notifications :
notifications(FK user, type, payload JSONB, read_at, created_at). - ☐ Soft delete : colonne
deleted_atpartout sauf tables jointures simples ; index partielWHERE deleted_at IS NULL. - ☐ Audit minimal :
created_at,updated_atpartout. - ☐ Migration initiale Alembic + commande
make migrate.
DoD : make migrate applique le schéma sur une DB vide ; \dt montre toutes les tables ; les contraintes FK et les index sont en place.
M2 — Auth, bootstrap, invitations ☑
But : un humain peut s'inscrire et se connecter.
- ☐ Hash mot de passe :
argon2-cffi(params modérés,time_cost=2, memory_cost=64MB). - ☐ JWT :
pyjwt, HS256, claimssub,iat,exp,type(access|refresh),jti. Access 1h, refresh 30j. - ☐ Stockage refresh tokens en DB (rotation à chaque usage, révocation au logout).
- ☐ Endpoints :
POST /auth/login,POST /auth/refresh,POST /auth/logout,GET /auth/me,POST /auth/change-password. - ☐ Bootstrap : commande
flask metamorph print-install-tokengénère + persiste un token unique au 1er démarrage (si tableusersvide), écrit dans les logs au boot. - ☐ Endpoint
POST /setup: consomme le token d'install, crée le 1er admin (groupeadminseedé). - ☐ Invitations :
POST /invitations(admin, génère token 7j),GET /invitations/{token}(preview),POST /invitations/{token}/accept(création compte avec password choisi). - ☐ Middleware d'auth Flask (
@require_auth,@require_perm("...")). - ☐ Rate-limit
flask-limitersur/auth/*(10/min/IP). - ☐ Front : pages
/login,/setup,/register?token=…,/profile. Stockage access en mémoire, refresh en cookie HTTPOnly Secure SameSite=Strict. - ☐ Hook React
useAuth()+ interceptor TanStack Query (refresh auto sur 401). - ☐ CORS strict (origin
FRONT_ORIGIN).
DoD : flask metamorph print-install-token → /setup → création admin → login → /auth/me OK ; admin crée invitation → user s'inscrit via lien → login OK ; /auth/refresh renouvelle correctement.
M3 — RBAC : groupes, permissions, gestion users ☑
But : admin peut composer des groupes custom et y assigner des users.
- ☐ Seed des permissions atomiques (familles spec §4) :
user.{read,create,update,delete},group.{read,create,update,delete},invitation.{create,revoke,read}test_template.{read,create,update,delete},scenario_template.{read,create,update,delete}mission.{read,create,update,archive,delete},mission.write_red_fields,mission.write_blue_fieldsdetection_level.{read,update},setting.{read,update},mitre.sync
- ☐ Seed des 3 groupes par défaut (
admin= toutes,redteam= templates(read) + missions(read,create,update) + write_red_fields,blueteam= templates(read) + missions(read) + write_blue_fields). - ☐ Endpoints CRUD
groups,permissions(lecture seule),users(admin),users/{id}/groups(assign). - ☐ Décorateur
@require_permqui 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).
M4 — MITRE ATT&CK Enterprise ☑
But : le référentiel ATT&CK est interrogeable et tagué sur les tests.
- ☐ Téléchargement initial du STIX bundle Enterprise depuis
github.com/mitre/cti(vérifier hash, pin une version). - ☐ Parser STIX → tables
mitre_tactics/mitre_techniques/mitre_subtechniques(extraireexternal_idATT&CK,name,description,url, relations technique↔tactic). - ☐ Commande
flask metamorph seed-mitre [--source <path|url>]. - ☐ Endpoint
POST /mitre/sync(permmitre.sync) qui re-pull depuis l'URL configurée (settingmitre_source_url). - ☐ Persister
mitre_last_syncdanssettings. - ☐ Endpoint
GET /mitre/tactics,/mitre/techniques?tactic=…,/mitre/subtechniques?technique=…(pagination + recherche full-text simple surname). - ☐ Front : composant
<MitreTagPicker>— matrice flat typeattack.mitre.org/#(colonnes = tactics, cellules = techniques, chevron+Nqui déplie les sub-techniques inline). Click = (dé)sélection, multi-niveaux cumulatif, chips en haut, recherche parexternal_idouname. Alimenté parGET /mitre/matrix(one-shot, ~55 KB).
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.
M5 — Templates : tests unitaires & scénarios ☐
But : admin peut bâtir le catalogue réutilisable.
- ☐ Modèle
test_template: nom, description, objectif, procédure (markdown), prérequis (markdown), résultat attendu red, détection attendue blue, niveau OPSEC (enum low/med/high), tags libres (array text), IOCs attendus (array text), tags MITRE (multi). - ☐ Endpoints CRUD
/test-templatesavec validation pydantic. - ☐ Modèle
scenario_template: nom, description, liste ordonnée de tests (position). - ☐ Endpoints CRUD
/scenario-templates,PUT /scenario-templates/{id}/tests(réordonnancement). - ☐ Front : page Admin > Tests (liste filtrable par tactic / OPSEC / tag), modale d'édition (form complet avec markdown editor —
@uiw/react-md-editorou é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_modefigé àwhiteboxv1. - ☐
mission_members: (mission_id, user_id, role_hintred|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_templatesettest_templatesdansmission_scenarios/mission_tests(y compris tags MITRE). - ☐
mission_testsajoute :state(enum pending/executed/reviewed_by_blue/skipped/blocked),executed_at(nullable),executed_at_override(bool),red_command,red_output,red_comment,blue_comment,detection_level_id(nullable). - ☐ 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 #Navec 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 deexecuted_atderrière un toggle « override ». - ☐ Côté blue : sélecteur
detection_level, commentaire markdown, zone d'upload multi-fichiers (drag-and-drop). - ☐ Upload preuves :
POST /missions/{id}/tests/{test_id}/evidence(multipart, validation extension+MIME+taille≤25Mo, calcul SHA256, stockage/data/evidence/<mission_id>/<test_id>/<sha256>{ext}). - ☐
GET /evidence/{id}(download, vérif perm) ;DELETE /evidence/{id}(soft). - ☐ Permissions : tout endpoint d'écriture vérifie
mission.write_red_fieldsoumission.write_blue_fieldsselon le champ touché ; les deux peuvent coexister sur un même groupe (pas exclusifs en code). - ☐ Bouton « Statut » avec choix
executed,reviewed_by_blue,skipped,blocked(transitions contrôlées : pending↔skipped/blocked, executed→reviewed_by_blue). - ☐ 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.
- ☐ Seed initial :
detected_blocked(red),detected_alert(orange),logged_only(yellow),not_detected(rose). - ☐ Endpoints
/detection-levels: list, create, update (label_fr, label_en, color_token, position, is_default). - ☐ 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. - ☐ Endpoints
GET /notifications?unread_only=…,POST /notifications/{id}/read,POST /notifications/read-all. - ☐ Front : badge dans le header avec compteur, dropdown listant les 20 dernières + lien vers la mission/test.
- ☐ Polling
GET /notifications?unread_only=truetoutes 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.htmlqui 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-pdfreveal.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
DELETEdu back deviennentUPDATE deleted_at. - ☐ Tous les
GETfiltrentdeleted_at IS NULLpar défaut, paramètre?include_deleted=trueré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 localesfr/en. Messages d'erreur API viagettext. Fichiermessages.potextrait viapybabel extract. - ☐ Frontend :
react-i18next, namespaces par page, fichiersfrontend/src/i18n/{fr,en}/*.json. - ☐ Préférence user : champ
users.locale(defaultfr), endpointPATCH /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.
M14 — Polish, sécu, observabilité, doc ☐
But : prêt pour livraison.
- ☐ Logs JSON :
request_id,user_id,path,method,status,duration_ms,action(libre côté service). - ☐ Audit minimal : logger toute action sensible (
auth.login,mission.create,evidence.delete,admin.purge). - ☐ Rate-limit confirmé sur
/auth/*et/invitations/*. - ☐ Headers sécu :
Strict-Transport-Security(si reverse proxy le pose, sinon doc),Content-Security-Policystrict côté front,X-Frame-Options: DENY,Referrer-Policy: same-origin. - ☐ Validation : tailles max body globale (Flask
MAX_CONTENT_LENGTH), schéma pydantic strict partout. - ☐
README.mdcomplet (déploiement, env, premier admin, sync MITRE, backup volumes). - ☐
CHANGELOG.mdà jour (Conventional changelog). - ☐ Critères §10 de la spec : check 1 par 1 sur une démo end-to-end documentée dans
tasks/lessons.md. - ☐ Tests : pytest pour la logique critique (auth, RBAC, snapshot, upload, exports). Smoke E2E Playwright (non bloquant, but nice).
DoD : démo from-scratch sur Debian 13 — git clone → make up → setup admin → invite users → crée mission → exécute → annote → génère slide → export. Tous les 15 critères §10 spec validés.
Backlog v2+ (rappel pour ne pas oublier)
- Bascule auth Keycloak/OIDC.
- API d'ingestion C2 externe (push automatique des résultats).
- Audit log détaillé + versioning par champ.
- 2FA TOTP self-service.
- Notifications mail.
- Intégration tunnel C2 (binaires fournis).
- Métriques Prometheus.
- Multi-tenancy / workspaces.
- Branding configurable (logos, couleurs).
Hygiène de session
- Au début de chaque session : relire
tasks/lessons.md,CHANGELOG.md, ce fichier. - À la fin : mettre à jour les ☐/☑, ajouter une entrée
CHANGELOG.md, capturer les apprentissages danstasks/lessons.md. - Pour tout doute architectural : repasser par AskUserQuestion avant d'ouvrir un éditeur.