Files
mimic/tasks/todo.md
Knacky 2e59743af5 docs: sprint 5 wrap-up — CHANGELOG + README + 6 lessons + plan final
- CHANGELOG: sprint 5 entry under [Unreleased] (templates CRUD + instantiation + nav + dropdown + decorrelation). Sprint 4 moved to its own [Sprint 4] section.
- README: status bump to sprint 5, test counts refreshed (226/121/201).
- tasks/lessons.md: 6 sprint-5 lessons captured (spec-reviewer 2-pass before dispatch finally clicked, endpoint path drift caught visually not by spec-review, screenshot script mocks lag path changes, silent URL "improvements" by backend, apply_patch wrong primitive for creation copy paths, IntegrityError catch beats pre-check SELECT, SendMessage rule applies to all team agents).
- tasks/todo.md: status flipped to 🟢 SPRINT COMPLET.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 07:18:21 +02:00

21 KiB

Sprint 5 — Simulation templates

Branche : sprint/5-templates Statut : 🟢 SPRINT COMPLET — backend 226/226 + frontend 121/121 + e2e 201/201, PR prête Base : main @ 9873c53 (PR #7 sprint 4 mergé) Objectif : permettre à un admin/redteam de créer des templates de simulations pré-remplies (RT-side : name, description, commands, prerequisites, techniques, tactics). Instancier un template dans un engagement crée une nouvelle simulation décorrélée (copie indépendante — éditer l'instance ne touche pas le template et vice-versa). User QA item 8 sprint 3.


0. SPEC.md à enrichir en début de sprint

Ajouter une section ## Templates de simulations (entre § Fonctionnement et § Authentification & rôles) :

Un template de simulation est une simulation pré-remplie côté redteam (name + description + commandes + pré-requis + techniques MITRE + tactiques MITRE) qui sert de point de départ pour instancier rapidement des simulations dans un engagement. Le template ne contient PAS de partie SOC, ni de date d'exécution, ni de résultat d'exécution — ces champs restent par-instance. L'instanciation d'un template dans un engagement crée une nouvelle simulation indépendante : le template et l'instance sont décorrélés, l'édition de l'un n'affecte pas l'autre. Templates = ressource red team : admin et redteam les gèrent (CRUD). SOC n'y a aucun accès (ni lecture, ni écriture, pas de nav link).

L'évolution est tracée dans CHANGELOG.md § Changed sprint 5.


1. User stories

US-26 — En tant qu'admin/redteam, je crée et gère des templates de simulations

Pourquoi : standardiser des simulations récurrentes (ex: "Mimikatz LSASS dump", "PowerShell empire stager") et éviter de retaper les mêmes commandes/MITRE à chaque engagement.

Critères d'acceptation

  • AC-26.1 : modèle SimulationTemplate (table simulation_templates) :
    • id int PK
    • name str NOT NULL UNIQUE (limite UX : un template unique par nom pour éviter les doublons dans le dropdown d'instanciation)
    • description text nullable
    • commands text nullable (chaîne multiligne, une commande par ligne, pattern sprint 2)
    • prerequisites text nullable
    • techniques JSON NOT NULL default [] (liste [{id, name}], snapshot des techniques MITRE)
    • tactic_ids JSON NOT NULL default [] (liste de TA-id strings)
    • created_at datetime NOT NULL
    • updated_at datetime nullable
    • created_by_id int FK User
  • AC-26.2 : migration Alembic 0005_simulation_templates.py — CREATE TABLE simulation_templates + index sur name. Downgrade : DROP TABLE.
  • AC-26.3 : GET /api/templates (admin|redteam) → liste [{id, name, description, commands, prerequisites, techniques: [{id, name, tactics}], tactics: [{id, name}], created_at, created_by}], ordre name ASC. SOC → 403.
  • AC-26.4 : POST /api/templates (admin|redteam) → 201 + template créé. Body : {name, description?, commands?, prerequisites?, technique_ids?: [...], tactic_ids?: [...]}. Valide : name non vide, name unique (409 si doublon), technique_ids / tactic_ids validés contre bundle MITRE / _TACTIC_IDS (réutilise les helpers _resolve_technique_ids / _resolve_tactic_ids sprint 3/4). SOC → 403.
  • AC-26.5 : GET /api/templates/<tid> (admin|redteam) → 200 ou 404. SOC → 403.
  • AC-26.6 : PATCH /api/templates/<tid> (admin|redteam) → 200, accepte les mêmes champs que POST en partial. Si name est modifié et entre en conflit avec un autre template → 409. SOC → 403.
  • AC-26.7 : DELETE /api/templates/<tid> (admin|redteam) → 204. Pas de cascade vers les simulations déjà instanciées — celles-ci sont décorrélées et survivent. SOC → 403.
  • AC-26.8 : page /admin/templates (admin|redteam uniquement) liste les templates en table (Name, MITRE count, Created by, Updated at, Actions). Boutons "New template" + "Edit" + "Delete". Tous les endpoints templates sont gated @role_required("admin", "redteam") côté backend, et ProtectedRoute frontend impose le même filtre.

US-27 — En tant que redteam, j'instancie un template dans un engagement

Pourquoi : c'est le use-case principal des templates.

Critères d'acceptation

  • AC-27.1 : POST /api/engagements/<eid>/simulations (admin|redteam) accepte un nouveau paramètre optionnel template_id. Si présent, le serveur valide que le template existe (404 sinon), puis crée une nouvelle simulation en copiant :
    • name ← template.name (peut être override par name du body si fourni)
    • description ← template.description
    • commands ← template.commands
    • prerequisites ← template.prerequisites
    • techniques ← template.techniques (deep copy)
    • tactic_ids ← template.tactic_ids (deep copy)
    • Autres champs : status=pending, executed_at=null, execution_result=null, SOC fields=null
  • AC-27.2 : POST sans template_id garde le comportement sprint 2 (création vierge avec juste name).
  • AC-27.3 : la simulation créée depuis un template est complètement décorrélée : modifier l'instance ne touche pas le template, modifier le template ne touche pas les instances existantes. Pas de FK template_id stockée sur la simulation (clean decoupling).
  • AC-27.4 : auto-transition pending → in_progress NE se déclenche PAS lors de la création depuis un template (même si le template a un name + description + techniques non vides). La création reste status=pending — la transition se fera au prochain PATCH explicite de la redteam. Cohérent avec règle sprint 2 "trigger sur PATCH" pas "trigger sur création".
  • AC-27.5 : engagement auto-status n'est PAS déclenché par l'instanciation (status reste planned). Coherent avec AC-27.4.
  • AC-27.6 : sur EngagementDetailPage (sprint 2/3/4), le bouton "+ New" (ou équivalent UI) ouvre désormais un menu / dropdown / modal avec 2 options : "Blank" et "From template…". L'option "From template…" affiche la liste des templates disponibles avec leur nom + un aperçu (count techniques/tactics). Click sur un template → POST avec template_id + redirection sur la simu créée. Si useTemplates() retourne une liste vide → la modale affiche un <EmptyState title="No templates available" description="Create one from the Templates page" />. NE PAS désactiver l'option "From template…" dans le dropdown (l'utilisateur doit pouvoir l'ouvrir pour comprendre qu'il n'y a rien — un disabled item silencieux serait confus).
  • AC-27.7 : SOC n'a PAS accès au bouton d'instanciation (cohérent avec RBAC simulation creation sprint 2).

US-28 — En tant qu'admin/redteam, j'accède aux templates depuis la nav

Critères d'acceptation

  • AC-28.1 : Layout.tsx topbar nav contient un nouveau lien "Templates" (visible uniquement à admin + redteam). Pour SOC : le lien n'apparaît pas (cohérent avec "Users" qui est admin-only et masqué côté SOC).
  • AC-28.2 : ProtectedRoute pour /admin/templates impose roles=["admin", "redteam"]. SOC qui tente d'y accéder en tapant l'URL → redirigé vers /engagements + toast "Accès refusé" (pattern existant ProtectedRoute sprint 1).
  • AC-28.3 : la page /admin/templates n'inclut PAS de mode "read-only SOC" — elle est strictement admin+redteam. Les composants peuvent assumer canEditTemplates = isAdmin || isRedteam = true (toujours vrai à ce niveau).

2. Brief technique — Backend Builder

Scope strict : backend/. Pas de touche au frontend, e2e, docs, agents, scripts.

Livrables

Modèle SimulationTemplate (backend/app/models/simulation_template.py — nouveau fichier)

from datetime import UTC, datetime
from sqlalchemy.orm import Mapped, mapped_column

class SimulationTemplate(db.Model):
    __tablename__ = "simulation_templates"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False, unique=True)
    description = db.Column(db.Text, nullable=True)
    commands = db.Column(db.Text, nullable=True)
    prerequisites = db.Column(db.Text, nullable=True)
    techniques = db.Column(db.JSON, nullable=False, default=list)
    tactic_ids = db.Column(db.JSON, nullable=False, default=list)
    created_at = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(UTC))
    updated_at = db.Column(db.DateTime, nullable=True)
    created_by_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
    created_by = db.relationship("User")

Ajouter à backend/app/models/__init__.py.

Migration Alembic 0005_simulation_templates.py

  • Upgrade : op.create_table("simulation_templates", ...) avec tous les champs et la contrainte UNIQUE sur name. Pas besoin de batch (CREATE TABLE est natif SQLite).
  • Downgrade : op.drop_table("simulation_templates"). SQLite OK natif.
  • Pas de backfill (table vide à la création).

Serializer (backend/app/serializers.py)

  • Nouvelle fonction serialize_template(t) :
    return {
        "id": t.id,
        "name": t.name,
        "description": t.description,
        "commands": t.commands,
        "prerequisites": t.prerequisites,
        "techniques": _enrich_techniques(t.techniques),  # réutilise sprint 3
        "tactics": _enrich_tactics(t.tactic_ids),         # réutilise sprint 4
        "created_at": t.created_at.isoformat() if t.created_at else None,
        "updated_at": t.updated_at.isoformat() if t.updated_at else None,
        "created_by": serialize_user_brief(t.created_by) if t.created_by else None,
    }
    
  • Pattern parallèle à serialize_simulation mais SANS les champs SOC / status / executed_at.

API (backend/app/api/templates.py — nouveau blueprint) Tous les endpoints templates sont gated @role_required("admin", "redteam") — SOC reçoit 403 partout.

  • GET /api/templates (admin|redteam) → liste serializée, tri name ASC.
  • POST /api/templates (admin|redteam) → création. Validation : name non vide (400), technique_ids / tactic_ids valides (réutilise _resolve_technique_ids / _resolve_tactic_ids de simulation_workflow.py — import direct, KISS). Pour le name UNIQUE conflict : catch sqlalchemy.exc.IntegrityError sur INSERT → 409 {"error": "template name already exists"}. Pas de pre-check SELECT (race condition + code mort, la contrainte UNIQUE en DB est l'autorité).
  • GET /api/templates/<tid> (admin|redteam) → 200 ou 404.
  • PATCH /api/templates/<tid> (admin|redteam) → update partial. Pour le name conflict : même pattern, catch IntegrityError sur UPDATE → 409 {"error": "template name already exists"}. Cas edge : PATCH avec name == current_name (no-op rename) → 200 (l'UPDATE sur sa propre row ne viole pas UNIQUE). Mettre à jour updated_at.
  • DELETE /api/templates/<tid> (admin|redteam) → 204. Pas de cascade FK simulations (les simulations n'ont pas de template_id FK).

Enregistrer le blueprint dans backend/app/__init__.py.

Modification POST /api/engagements/<eid>/simulations (backend/app/api/simulations.py)

  • Le payload accepte maintenant un template_id optionnel.
  • Si présent :
    • Charger le template (404 si non trouvé).
    • Créer la simulation en setant DIRECTEMENT les champs RT sur l'objet ORM Simulation à partir des champs du template (sim.techniques = template.techniques, sim.tactic_ids = template.tactic_ids, sim.description = template.description, sim.commands = template.commands, sim.prerequisites = template.prerequisites).
    • name du body override si fourni, sinon template.name.
    • NE PAS appeler apply_patch(), _resolve_technique_ids(), ni _resolve_tactic_ids() — les données viennent du template déjà persisté+validé, re-résoudre frapperait inutilement le bundle MITRE ET déclencherait l'auto-transition pending→in_progress via la logique auto-trigger de apply_patch, ce qui violerait AC-27.4. Le set direct ORM est intentionnellement court-circuité.
  • Si absent : comportement actuel inchangé (création vierge avec name).
  • Auto-transition NE PAS déclencher (status reste pending — règle sprint 2 "trigger sur PATCH", la création ne compte pas). Le fait de ne pas appeler apply_patch est ce qui garantit ça structurellement.
  • Engagement auto-status NE PAS déclencher (status engagement reste planned). Idem — _maybe_activate_engagement n'est appelé que depuis apply_patch.

Tests pytest

  • test_simulation_templates_crud.py (nouveau) :
    • GET liste vide, GET liste après création, GET liste tri name ASC.
    • POST valide → 201, fields persisted.
    • POST name vide → 400.
    • POST name dupliqué → 409.
    • POST technique_id inconnu → 400.
    • POST tactic_id inconnu → 400.
    • POST par SOC → 403.
    • GET inexistant → 404.
    • PATCH valide → 200, updated_at set.
    • PATCH name → conflit → 409.
    • PATCH par SOC → 403.
    • DELETE valide → 204, GET ensuite → 404.
    • DELETE par SOC → 403.
  • test_simulations_from_template.py (nouveau) :
    • POST simulation avec template_id valide → copie tous les RT fields, status=pending, executed_at=null, SOC fields=null.
    • POST avec template_id valide + name override → name override gagne.
    • POST avec template_id inexistant → 404.
    • POST avec template_id par SOC → 403 (cohérent avec création).
    • Vérifier décorrélation : créer template → instancier → modifier l'instance → assert template inchangé. Symétrique : modifier le template → instance inchangée.
    • Auto-transition NE PAS déclenchée (sim reste pending même si template avait des techniques).
    • Engagement reste planned (auto-status NOT triggered).
  • Migration test : 0005 create/drop round-trip propre (réutilise pattern Alembic round-trip sprint 3/4 avec Path(__file__)).

Quality bar : ruff + mypy clean, tous tests existants + nouveaux verts.

Règles

  • Pas de touche au frontend, e2e, agents, scripts, Makefile.
  • Renvoyer le summary attendu (cf. .claude/agents/backend-builder.md).

3. Brief technique — Frontend Builder

Scope strict : frontend/ uniquement.

Screenshots MANDATORY (lesson sprint 4) : à la fin de ton travail, dev server + auth flow (page.goto('/login') + fill creds + submit + wait) pour fournir MIN 6 screenshots :

  1. /admin/templates liste (admin OU redteam vue, ≥ 2 templates) — light + dark
  2. Template create/edit form (mode edit avec techniques + tactic chips) — light + dark
  3. EngagementDetail avec dropdown "Blank | From template…" ouvert — light
  4. TemplatePickerModal ouverte (au moins 2 templates listés) — light
  5. TemplatePickerModal ouverte avec aucun template — empty state visible — light
  6. Simulation créée depuis un template (champs pré-remplis avec le nom, MITRE chips) — light

Paths absolus dans le summary. Si auth flow ne marche pas → escalade.

Livrables

Types (frontend/src/api/types.ts)

  • SimulationTemplate: {id, name, description, commands, prerequisites, techniques: MitreTechnique[], tactics: MitreTactic[], created_at, updated_at, created_by}.
  • SimulationTemplateCreateInput: payload POST.
  • SimulationTemplatePatchInput: payload PATCH.
  • Étendre SimulationCreateInput avec template_id?: number.

API client (frontend/src/api/templates.ts — nouveau)

  • listTemplates(), getTemplate(id), createTemplate(input), updateTemplate(id, patch), deleteTemplate(id).

Hooks TanStack Query (frontend/src/hooks/useTemplates.ts — nouveau)

  • useTemplates(), useTemplate(id), mutations useCreateTemplate, useUpdateTemplate, useDeleteTemplate.
  • Invalidation : create/update/delete invalide ["templates"] et ["templates", id].

Pages

  • TemplatesListPage.tsx (nouveau, /admin/templates) — admin+redteam only :
    • Table : Name, MITRE count (techniques + tactics), Created by, Updated at, Actions.
    • Bouton "+ New template" en header.
    • Actions par ligne : "Edit" + "Delete".
    • Click sur une ligne → /admin/templates/:id/edit.
    • States : loading / error / empty.
  • TemplateFormPage.tsx (nouveau, /admin/templates/new et /admin/templates/:id/edit) — admin+redteam only :
    • Form pour name + description + commands (textarea) + prerequisites + MitreTechniquesField.
    • Mode new : seul name requis ; après création, redirige sur /admin/templates/:id/edit.
    • Mode edit : load existing template, allow update.
    • Bouton Delete (confirmation modal).
    • Pas de mode read-only (SOC n'a pas accès aux routes).
  • EngagementDetailPage.tsx (modification) :
    • Remplacer le bouton simple "+ New simulation" par un dropdown OU un menu :
      • "Blank" (action default)
      • "From template…" → ouvre une modale avec la liste des templates.
    • Modale "From template…" : useTemplates(), table simple Name + MITRE count, click sur un template → useCreateSimulation avec template_id: t.id → redirect sur la simu créée.

Composants (frontend/src/components/)

  • TemplatePickerModal.tsx (nouveau) : modale qui liste les templates, permet de cliquer pour instancier. Props : engagementId, onClose, onInstantiated(simId).

Routing (App.tsx) — toutes routes templates gated roles=["admin", "redteam"] :

  • /admin/templates (admin|redteam)
  • /admin/templates/new (admin|redteam)
  • /admin/templates/:id/edit (admin|redteam)

Layout (Layout.tsx)

  • Nouveau lien "Templates" dans la topbar, à droite de "Users" — visible UNIQUEMENT à admin + redteam (masqué pour SOC, pattern identique à "Users" qui est admin-only).

Tests Vitest

  • TemplatesListPage.test.tsx — loading/error/empty + boutons New/Edit/Delete présents (admin|redteam — pas de variante soc puisque route inaccessible).
  • TemplateFormPage.test.tsx — mode new + mode edit (pas de mode read-only).
  • TemplatePickerModal.test.tsx — liste templates, click instantiate, gestion erreur, close.
  • EngagementDetailPage.test.tsx — adapter pour le nouveau dropdown "+ New simulation".

Règles

  • Lit le summary backend EN PREMIER.
  • Pas d'invention d'endpoints.
  • Réutilise LoadingState, ErrorState, EmptyState, Toast, ConfirmDialog, MitreTechniquesField, StatusBadge etc.
  • Respect DESIGN.md tokens. Dark mode déjà en place — applique les patterns sprint 4 (bg-canvas dark:bg-canvas, etc.).
  • Pas de dépendance npm sans escalade.

4. Brief — Test verifier

E2e Playwright. Un fichier par US :

  • us26-templates-crud.spec.ts — AC-26.1 → AC-26.8 (focus API + UI gérance templates)
  • us27-instantiate-from-template.spec.ts — AC-27.1 → AC-27.7 (création simu depuis template, décorrélation)
  • us28-templates-nav.spec.ts — AC-28.1 → AC-28.3 (nav link, accès SOC read-only)

Adapter les sprint 2/3/4 e2e si l'ajout du dropdown "+ New simulation" casse des sélecteurs (les tests sprint 2/3 cliquent directement sur "+ New" — désormais ça ouvre un menu avant d'aller au form blanc).


5. Definition of Done — Sprint 5

  • Tous les AC US-26 → US-28 passent.
  • Backend pytest verts (~193 existants + ~25 nouveaux). Ruff + mypy clean.
  • Frontend vitest verts (92 existants + nouveaux). Typecheck + lint clean.
  • E2e Playwright suite verte (sprint 1-4 + sprint 5).
  • Migration 0005 testée via Alembic round-trip.
  • SPEC.md § Templates de simulations ajoutée.
  • README.md mis à jour si nouveau bullet "Templates" pertinent.
  • CHANGELOG.md sprint 5 entry sous [Unreleased].
  • Design-reviewer pass sur les nouveaux écrans (lesson sprint 4 — design-reviewer = part of workflow depuis sprint 4).
  • Code-reviewer sans BLOCKER.
  • PR via make open-pr (sprint 4 dogfood validé).

6. Décisions arrêtées (utilisateur 2026-05-28)

  1. Table : simulation_templates séparée (clean schema, pas de colonnes nullable confuses).
  2. Instantiation API : extension de POST /api/engagements/<eid>/simulations avec template_id optionnel.
  3. Name uniqueness : UNIQUE (1 template par nom, UX dropdown clean).
  4. Template RBAC : admin + redteam writable. SOC pas d'accès du tout — pas de nav link, pas de page, tous endpoints templates → 403. Templates sont une ressource Red Team uniquement.
  5. UI instanciation : dropdown sur le bouton "+ New simulation" (Blank | From template…).

7. Plan d'exécution

  1. User a validé les 5 décisions §6 (2026-05-28). SOC zero access acté.
  2. 🟡 Team-lead met à jour SPEC.md (§0).
  3. 🟡 Spec-reviewer valide le plan (2-pass — lesson sprint 3/4 — RBAC SOC blocked, name unique conflict 409 handling, template_id passing through auto-transition, design-reviewer scope new pages).
  4. 🔵 Backend-builder : modèle + migration 0005 + endpoints + tests.
  5. 🔵 Frontend-builder : pages Templates list/form + TemplatePickerModal + nav link + dropdown engagement + tests Vitest. Screenshots mandatory.
  6. 🔵 Design-reviewer : revoit diff frontend + screenshots.
  7. 🔵 Code-reviewer : LSP-first review du diff complet.
  8. 🔵 Test-verifier : e2e US-26 → US-28.
  9. 🟢 Team-lead : make open-pr + récap.

Branche : sprint/5-templates.