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>
This commit is contained in:
43
CHANGELOG.md
43
CHANGELOG.md
@@ -6,6 +6,49 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added — Sprint 5 (Simulation templates)
|
||||
|
||||
**Backend** (226 pytest passing — 193 sprint-1-to-4 + 28 sprint 5 + 5 post-code-review)
|
||||
- `SimulationTemplate` model (table `simulation_templates`) — UNIQUE constraint on `name`, JSON `techniques` + `tactic_ids` (default `[]`, NOT NULL via `server_default`), Text fields `description` / `commands` / `prerequisites`, FK `created_by_id` to `users`, `created_at` / `updated_at`.
|
||||
- Alembic migration `0005_simulation_templates.py` — CREATE TABLE (SQLite native, no batch); downgrade via DROP TABLE.
|
||||
- 5 new endpoints under `/api/templates`, all gated `@role_required("admin", "redteam")` (SOC → 403):
|
||||
- `GET /api/templates` — list, sorted name ASC, serialized with enriched `techniques: [{id, name, tactics}]` and `tactics: [{id, name}]`.
|
||||
- `POST /api/templates` — create. `name` required (400 if empty), unique (409 via `IntegrityError` catch, no pre-check race). `technique_ids` / `tactic_ids` validated upfront — type check `isinstance(list)` (400 with friendly message) THEN resolved against the bundle / `_TACTIC_IDS` (400 with id on unknown).
|
||||
- `GET /api/templates/<tid>` — single, 404 on miss.
|
||||
- `PATCH /api/templates/<tid>` — partial update. Same validations. 409 on `name` conflict; no-op rename (`name == current`) returns 200.
|
||||
- `DELETE /api/templates/<tid>` — 204. **No cascade** to instantiated simulations (decoupling guarantee).
|
||||
- `POST /api/engagements/<eid>/simulations` extended with optional `template_id`. When provided:
|
||||
- Template loaded (404 on miss).
|
||||
- Fields copied directly onto the new `Simulation` ORM object (`techniques`, `tactic_ids`, `description`, `commands`, `prerequisites`, and `name` if missing from body).
|
||||
- **Explicit non-call to `apply_patch()` / `_resolve_*` helpers** — avoids re-hitting the MITRE bundle AND avoids triggering the auto-transition `pending → in_progress`. Status stays `pending`, engagement stays `planned` (no `_maybe_activate_engagement` call). Decorrelation: no `template_id` FK on `Simulation`, deep copy of JSON arrays.
|
||||
- New helpers in `mitre.py` reused / re-exposed; new `serialize_template()` in `serializers.py` mirrors `serialize_simulation` (minus SOC fields, status, executed_at) and uses the shared `_enrich_techniques` + `_enrich_tactics` (no duplication).
|
||||
- All migration tests (0003, 0004, 0005) now use `Path(__file__).resolve().parent.parent / "migrations" / "versions" / "..."` — sprint 4's hardcoded-path MAJOR is closed for the third sprint running.
|
||||
|
||||
**Frontend** (121 vitest passing — 92 sprint-1-to-4 + 26 sprint 5 + 3 post-code-review)
|
||||
- New page `TemplatesListPage` (`/admin/templates`, admin+redteam only) — table (Name / MITRE count / Created by / Updated / Actions), `+ New` CTA with Plus icon.
|
||||
- New page `TemplateFormPage` (`/admin/templates/new` and `/admin/templates/:id/edit`) — single-column FormField stack (sidesteps the multi-column grid trap that broke AC-17.3 on UsersAdminPage). Includes `MitreTechniquePicker` + `MitreMatrixModal` inline (NOT `MitreTechniquesField` — that one auto-saves; template form needs batched save). Delete via `ConfirmDialog`.
|
||||
- New component `TemplatePickerModal` — modal listing all templates (Name / MITRE count / Created by). Empty state when `useTemplates()` returns `[]`: "No templates available — Create one from the Templates page."
|
||||
- New nav link "Templates" in `Layout.tsx` topbar — visible to admin + redteam only, masked for SOC. Mirrors the pattern used by the "Users" link.
|
||||
- `SimulationList` "New" button refactored into a **split-button dropdown**: `[+ New] [▼]`. Primary half → `/.../simulations/new` (blank). Dropdown → "Blank" + "From template…". Open dropdown closes on click-outside or Escape (sprint 3 picker pattern). Empty-state `SimulationList` now also exposes the same dropdown (so users can instantiate from a template on a fresh engagement without creating a blank first).
|
||||
- `dark:shadow-floating-dark` consistently applied to the new dropdown and `TemplatePickerModal` — matches the sprint 4 shadow token model. `dark:hover:bg-fog` on dropdown items for contrast.
|
||||
- New types: `SimulationTemplate`, `SimulationTemplateCreateInput`, `SimulationTemplatePatchInput`. `SimulationCreateInput` extended with `template_id?: number`.
|
||||
- New TanStack Query hooks (`useTemplates`, `useTemplate`, `useCreateTemplate`, `useUpdateTemplate`, `useDeleteTemplate`) with cache invalidation on mutations.
|
||||
- API client `frontend/src/api/templates.ts` — 5 calls to `/api/templates*`. (Sprint-5 in-flight bug : initial commit `90fc5ba` used `/simulation-templates` paths everywhere; caught immediately, fixed in `2b70011`.)
|
||||
|
||||
**Acceptance tests** (Playwright, **201 passed**)
|
||||
- 3 new spec files (one per US): `us26-templates-crud.spec.ts` (22 tests), `us27-instantiate-from-template.spec.ts` (14 tests), `us28-templates-nav.spec.ts` (8 tests).
|
||||
- Coverage gaps from code-reviewer filled: bidirectional template↔instance decorrelation, dropdown click-outside + Escape, SOC + template_id 403.
|
||||
- Sprint 2/3 spec adapts: `us4-engagements.spec.ts` and `us7-simulation-create.spec.ts` now use `getByTestId('new-simulation-btn')` instead of `getByRole('link', /new simulation/)` — the link became a split-button dropdown.
|
||||
- 1 pre-existing flaky in `us3-users-admin AC-3.4` (DB contamination across runs) — predates sprint 5, unrelated.
|
||||
|
||||
### Changed
|
||||
- 2026-05-28 — SPEC.md § Templates de simulations added (between § Fonctionnement and § Authentification & rôles). Spells out the decoupling rule and the SOC-zero-access RBAC.
|
||||
- 2026-05-28 — `POST /api/engagements/<eid>/simulations` API contract: `name` is now optional when `template_id` is provided (falls back to `template.name`).
|
||||
|
||||
---
|
||||
|
||||
## [Sprint 4] — UI polish + workflow tightening + dark mode + process hygiene (merged 2026-05-28)
|
||||
|
||||
### Added — Sprint 4 (UI polish + workflow tightening + dark mode + process hygiene)
|
||||
|
||||
**Backend** (193 pytest passing — 192 sprint-1-to-3 + 1 sprint-4)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Mimic** is a Breach and Attack Simulation (BAS) web UI built on the MITRE ATT&CK matrix. It replaces the flat Excel spreadsheets that red-teams and SOC analysts pass around at the end of an engagement, providing a shared workspace for Purple Team handoffs.
|
||||
|
||||
> Status: **Sprint 4 — UI polish + workflow tightening + dark mode + process hygiene**. The Purple Team workflow is now tighter (Done is terminal, Reopen returns to Review required, engagements auto-flip Planned → Active on first in-progress simulation), simulations can be tagged with both techniques AND tactics (TA-ids), the MITRE matrix modal fits the viewport without horizontal scroll, the app supports light / dark / system theming, and PR creation is one Make target away.
|
||||
> Status: **Sprint 5 — Simulation templates**. Admin/redteam can now create reusable simulation templates (name + description + commands + prerequisites + MITRE techniques + tactics) and instantiate them inside an engagement in one click. Template and instance are fully decoupled — editing one never affects the other. SOC has no access to templates.
|
||||
|
||||
---
|
||||
|
||||
@@ -139,9 +139,9 @@ npm run dev # http://localhost:5173 with /api proxied to :5000
|
||||
Tests:
|
||||
|
||||
```bash
|
||||
cd backend && pytest -q # 193 tests
|
||||
cd frontend && npm run test -- --run # 92 tests
|
||||
cd e2e && npx playwright test # 158 tests (needs container up — use MIMIC_BASE_URL=http://127.0.0.1:5000 if localhost resolves to IPv6)
|
||||
cd backend && pytest -q # 226 tests
|
||||
cd frontend && npm run test -- --run # 121 tests
|
||||
cd e2e && npx playwright test # 201 tests (needs container up — use MIMIC_BASE_URL=http://127.0.0.1:5000 if localhost resolves to IPv6)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -4,6 +4,38 @@ Recurring mistakes and the rule we adopted so the same issue doesn't bite twice.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 5 (closed 2026-05-28)
|
||||
|
||||
### Process — Spec-reviewer 2-pass BEFORE backend dispatch eliminated mid-implementation addenda (team-lead)
|
||||
**Context** : Sprint 3 and 4 both required urgent addenda to the backend-builder mid-implementation because the spec-reviewer's 2nd pass arrived after the backend-builder had already started. Sprint 5 explicitly waited for spec-reviewer Pass 2 APPROVED before dispatching backend — and the backend ran straight through with 0 addenda churn. The 2-pass model finally clicked.
|
||||
**Lesson** : ALWAYS wait for the spec-reviewer's verdict on the post-edit pass before dispatching the first builder. If Pass 1 returns NEEDS-CHANGES, apply the fixes, request Pass 2, wait for APPROVED, THEN dispatch. The "let's send to builders in parallel to save time" instinct costs more than it saves.
|
||||
|
||||
### Process — Endpoint path drift caught by visual inspection, not by spec-reviewer (team-lead)
|
||||
**Context** : Backend implemented `/api/templates`. Plan §1 § 2 said `/api/simulation-templates`. Spec-reviewer 2-pass didn't catch the path drift. Frontend used the plan path → mismatch → first frontend commit (`90fc5ba`) was effectively non-functional against the real backend. Caught immediately by team-lead grep + diagnostic.
|
||||
**Lesson** : when backend "improves" a URL without flagging it as a deviation, it's still a deviation. Add to team-lead PR-merge mental checklist: `git diff main...HEAD | grep "@.*\.route\|@.*_bp\.(get|post|put|patch|delete)"` → does this match the plan §1/§2 path strings exactly? If no, dispatch a 1-line frontend fix BEFORE the post-design-review cycle.
|
||||
|
||||
### Process — Frontend-builder's screenshot script reuses stale mocks after path changes (frontend-builder)
|
||||
**Context** : Sprint 5 path fix `2b70011` corrected the API client to `/api/templates`. But the Playwright screenshot script still mocked `/api/simulation-templates/<id>` — for the GET single endpoint specifically. Result: edit-form screenshot showed a 500 ErrorState instead of the actual form. Design-reviewer caught this as a critical coverage gap.
|
||||
**Lesson** : when a path / contract changes mid-sprint, the screenshot script's route handlers must be updated in lockstep with the API client. A grep on the script for the old path is mandatory after every path-rename commit. Add to frontend-builder pre-screenshot checklist: `grep -E "<old-path>" $screenshot_script` must return empty.
|
||||
|
||||
### Engineering — Backend-builder's silent URL "improvement" (backend-builder)
|
||||
**Context** : The team-lead's plan §1/§2 explicitly named the endpoints `/api/simulation-templates`. Backend-builder chose `/api/templates` (shorter, cleaner) but did NOT flag this as a deviation in the summary's "Open questions / deviations" section. The frontend-builder followed the plan and broke. The path change was defensible but the lack of escalation was not.
|
||||
**Lesson** : when a builder chooses a different identifier than the plan (URL path, table name, column name, function name, etc.), even if "better", they MUST flag it in their summary under "Deviations from plan". The team-lead can then propagate the change to dependent briefs. Silent deviations break cross-team contracts.
|
||||
|
||||
### Engineering — Avoid calling `apply_patch()` on creation paths (backend-builder + spec-reviewer)
|
||||
**Context** : Sprint 5 template instantiation copies fields from a template to a new Simulation. Spec-reviewer Pass 1 flagged that a builder unaware of the auto-transition trigger might "reuse the validation" by calling `apply_patch()` — which would trigger `pending → in_progress` on a non-empty technique_ids payload, violating AC-27.4. The plan was explicitly updated to forbid this call. Backend-builder set ORM fields directly, which sidesteps both the bundle lookup AND the auto-transition logic.
|
||||
**Lesson** : `apply_patch()` is the wrong primitive for creation paths that copy already-validated data. Reach for direct ORM assignment (`sim.field = value`) when the source data is pre-validated (template → instance, replica → primary, etc.). Reserve `apply_patch()` for user-input paths that need full validation + workflow side effects.
|
||||
|
||||
### Engineering — Use `IntegrityError` catch for UNIQUE conflict 409, not pre-check SELECT (backend-builder + spec-reviewer)
|
||||
**Context** : Sprint 5's first plan draft listed BOTH a pre-insert SELECT to check name uniqueness AND an `IntegrityError` catch as fallback. Spec-reviewer Pass 1 flagged this as dual strategy — the SELECT still races under concurrent inserts and adds dead code. Plan was simplified to "catch IntegrityError only, rollback, return 409". Backend implementation matches.
|
||||
**Lesson** : for UNIQUE constraint violations, the DB is the authoritative source of truth. Catch the `IntegrityError`, roll back, return 409. Don't pre-check with SELECT — it races, and the IntegrityError catch is still required as a safety net (so the pre-check is just dead code). Same applies to other DB-enforced constraints (FK, CHECK).
|
||||
|
||||
### Process — Designer-reviewer accidental duplicate (`-2`) reminded me to use SendMessage (team-lead)
|
||||
**Context** : Sprint 4 introduced the design-reviewer agent. Sprint 5 first design-review I called `Agent({name: "design-reviewer"})` — the system spawned `design-reviewer-2`. Same mistake as sprint 2/3 with backend/frontend builders. Cleaned up via shutdown_request, but the second design-review pass I correctly used SendMessage on the original `design-reviewer` — and got the verdict cleanly without duplicate noise.
|
||||
**Lesson** : the "SendMessage to existing idle agent, not Agent" rule covers ALL agents in the team, not just builders. Includes design-reviewer, code-reviewer, spec-reviewer, test-verifier. Save: same `feedback-agent-reuse` memory note now applies broadly.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 4 (closed 2026-05-27)
|
||||
|
||||
### Process — git status before declaring sprint complete (team-lead)
|
||||
|
||||
481
tasks/todo.md
481
tasks/todo.md
@@ -1,339 +1,300 @@
|
||||
# Sprint 4 — UI polish + workflow tightening + dark mode + process hygiene
|
||||
# Sprint 5 — Simulation templates
|
||||
|
||||
**Branche** : `sprint/4-ui-polish`
|
||||
**Statut** : 🟢 SPRINT COMPLET — backend 193/193 + frontend 92/92 + e2e 158/158, PR prête
|
||||
**Base** : `main` @ `27573f5` (sprint 3 mergé via PR #6) + `ba313a3` (carry-over SPEC sprint 3)
|
||||
**Objectif** : absorber les 7 retours QA sprint 3 (UI/UX, workflow, alignement) + livrer le dark mode + durcir le process UI (design-reviewer agent + screenshots mandatory) + automatiser l'ouverture de PR. Pas de hotfix sprint 3 séparé — tout dans sprint 4 (décision user 2026-05-27).
|
||||
**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 updates
|
||||
## 0. SPEC.md à enrichir en début de sprint
|
||||
|
||||
- ✅ `ba313a3` — § Simulation : "Type d'attaque MITRE correspondant (peut être une liste de référence)" → "Types d'attaque MITRE correspondants (multi-techniques) ..." (carry-over manquant de sprint 3 §0).
|
||||
- 🟡 § Fonctionnement à enrichir en début de sprint 4 :
|
||||
- Préciser que "Done" est terminal : aucune édition possible sans Reopen explicite.
|
||||
- Préciser que la transition Reopen `Done → Review required` est ouverte à admin/redteam/soc.
|
||||
- Préciser que la création/avancement d'une simu fait avancer l'engagement de `planned` à `active` automatiquement (jamais l'inverse).
|
||||
- 🟡 § Décisions techniques à enrichir :
|
||||
- Section "UI/UX" : convention boutons (icônes / symboles préférés aux longs libellés).
|
||||
- Section "Theming" : dark mode supporté, toggle topbar, défaut = `prefers-color-scheme` du système, persistance `localStorage`.
|
||||
Ajouter une section `## Templates de simulations` (entre § Fonctionnement et § Authentification & rôles) :
|
||||
|
||||
L'évolution est tracée dans CHANGELOG.md § Changed sprint 4.
|
||||
> 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-17 — UI polish : dédoublonnage boutons + alignement + icônes
|
||||
**Pourquoi** : QA sprint 3 — `EngagementsListPage` montre 2 boutons "New engagement" + "Create engagement" qui font la même chose ; le bouton Create de `UsersAdminPage` reste mal aligné malgré le fix sprint 2.
|
||||
### 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-17.1 : `EngagementsListPage` n'affiche qu'UN SEUL bouton "New engagement". Le doublon "Create engagement" est supprimé.
|
||||
- [ ] AC-17.2 : convention nouveaux boutons d'action (Create / Add / Save / Delete) : icône lucide-react ou unicode + label court (≤ 8 chars), pas de phrases. Audit des boutons existants : ne refactoriser que ceux qui dépassent ce seuil, garder les "Mark for review" / "Clear all" qui sont déjà courts ou ont une sémantique sans icône évidente. Boutons à passer en icône+label : "Save Red Team" → "Save" + icône, "Save SOC" → "Save SOC" + icône, "ADD TECHNIQUE" → "+" + "Add", "QUICK SEARCH" → "🔍" + "Search".
|
||||
- [ ] AC-17.3 : `UsersAdminPage` formulaire "Create account" — les 3 FormField (Username, Password, Role) ont leurs labels alignés sur la même baseline ET leurs inputs alignés sur la même baseline. Le bouton Create est aligné horizontalement avec la rangée des inputs. Pixel-perfect au niveau visuel à 1280×720.
|
||||
- [ ] 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-18 — Simulation `done` = read-only + Reopen
|
||||
**Pourquoi** : QA sprint 3 — actuellement une simu `done` peut toujours être PATCHée, ce qui contredit le statut terminal.
|
||||
### 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-18.1 : `PATCH /api/simulations/<sid>` avec status courant `done` retourne **409** `{error: "simulation is done — reopen first"}` quel que soit le rôle.
|
||||
- [ ] AC-18.2 : nouvelle transition `POST /api/simulations/<sid>/transition {to: "review_required"}` quand status courant == `done` → 200, autorisée admin + redteam + soc. Met à jour `updated_at`.
|
||||
- [ ] AC-18.3 : la transition `→ review_required` depuis `pending`/`in_progress` garde le comportement sprint 2 (admin/redteam only). La nouvelle règle s'ajoute SEULEMENT pour le cas `done`.
|
||||
- [ ] AC-18.4 : sur `SimulationFormPage`, quand status == `done` :
|
||||
- Tous les champs (RT + SOC) sont disabled.
|
||||
- `MitreTechniquesField` en read-only (chips sans ×, input + icône matrice masqués).
|
||||
- L'action bar affiche UNIQUEMENT un bouton "Reopen" (visible admin/redteam/soc).
|
||||
- Save RT, Save SOC, Mark for review, Close, Delete sont masqués.
|
||||
- [ ] AC-18.5 : click Reopen → POST transition, toast `'Simulation reopened'`, badge se met à jour, les champs redeviennent éditables selon le rôle.
|
||||
|
||||
### US-19 — Engagement auto-status `planned → active`
|
||||
**Pourquoi** : QA sprint 3 — un engagement reste `planned` même quand ses simulations sont in_progress.
|
||||
- [ ] 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-19.1 : quand une simulation transitionne vers `in_progress` (auto-transition via PATCH RT-field non vide), si son engagement parent est `planned`, l'engagement passe à `active` dans la même unité de travail DB.
|
||||
- [ ] AC-19.2 : si l'engagement est déjà `active` ou `closed`, pas de changement.
|
||||
- [ ] AC-19.3 : aucun retour arrière auto. La transition `closed` reste manuelle.
|
||||
- [ ] AC-19.4 : le frontend invalide `["engagement", eid]` et `["engagements"]` après chaque PATCH/transition simulation pour récupérer le statut à jour.
|
||||
|
||||
### US-20 — Matrice MITRE : look attack.mitre.org + pas de scroll horizontal
|
||||
**Pourquoi** : QA sprint 3 — la matrice actuelle a un scroll horizontal et un layout maison.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- [ ] AC-20.1 : `MitreMatrixModal` est élargi à `max-w-[98vw]`.
|
||||
- [ ] AC-20.2 : layout 12 colonnes (12 tactiques Enterprise) qui tiennent SANS scroll horizontal à 1280×720 min. Largeur cellule technique ~95-110px (vs 220px actuel), font `text-[12px]`.
|
||||
- [ ] AC-20.3 : couleurs cohérentes DESIGN.md ET visuellement proches de attack.mitre.org : header tactic avec fond contrasté + label uppercase tracking, techniques en cellules `bg-canvas` avec hairline border, hover `bg-fog`, sélectionnée `bg-primary` texte blanc.
|
||||
- [ ] AC-20.4 : scroll vertical autorisé (`max-h-[80vh] overflow-y-auto`). Jamais de scroll horizontal.
|
||||
- [ ] AC-20.5 : sub-techniques expand/collapse PRÉSERVÉ — pas de régression sprint 3 AC-15.2. Compteur "N selected" par tactique reste lisible.
|
||||
- [ ] AC-20.6 : screenshot comparaison Mimic matrix vs attack.mitre.org joint au summary frontend-builder.
|
||||
|
||||
### US-21 — Sélection de tactique en plus des techniques
|
||||
**Pourquoi** : QA sprint 3 — l'utilisateur veut tagger une simulation par TACTIQUE (ex : `TA0007 Discovery`) sans devoir choisir une technique précise.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- [ ] AC-21.1 : modèle `Simulation` gagne un champ `tactic_ids` (colonne JSON, liste de strings TA-id, défaut `[]`). Séparé de `techniques`.
|
||||
- [ ] AC-21.2 : migration Alembic `0004_simulation_tactic_ids.py` — ADD COLUMN `tactic_ids` (JSON, NOT NULL, default `[]`). Pas besoin de batch pour ADD COLUMN (SQLite natif). Aucun backfill (default suffit).
|
||||
- [ ] AC-21.3 : sérialisation Simulation expose `tactics: [{id, name}]` enrichi à partir de `tactic_ids` (id snapshot + name dérivé du bundle MITRE au runtime, comme pour `techniques`).
|
||||
- [ ] AC-21.4 : `PATCH /api/simulations/<sid>` accepte `{tactic_ids: ["TA0007", ...]}`. Validation : chaque ID doit exister dans `_TACTIC_IDS` (mapping TA-id → short-name, cf §2 Service MITRE). Dedup serveur. ID inconnu → 400. **Pas de check `mitre_loaded`** : les TA-ids sont une constante MITRE standard stable hardcodée dans `_TACTIC_IDS` — la validation ne dépend pas du bundle STIX runtime (contrairement aux `technique_ids` qui requièrent le bundle). Donc PATCH `tactic_ids` reste OK même si le bundle est absent (alors que `technique_ids` retourne 503). Spec-aligné avec l'implémentation et les tests post-code-review.
|
||||
- [ ] AC-21.5 : `tactic_ids` est ajouté au gate SOC : `(REDTEAM_FIELDS | {"technique_ids", "tactic_ids"}) & payload.keys()`. SOC envoie → 403. Auto-transition se déclenche aussi si `tactic_ids` non vide.
|
||||
- [ ] AC-21.6 : `MitreMatrixModal` — le header de chaque colonne tactique devient cliquable (toggle de la tactique elle-même). État visuel distinct des techniques sélectionnées. Compteur passe à `N+M selected` (techniques + tactique).
|
||||
- [ ] AC-21.7 : `MitreTechniquesField` — tactiques sélectionnées affichées comme chips distincts (style différencié : `bg-primary text-canvas` au lieu de `bg-primary-soft text-primary-deep`). × pour retirer. Auto-save sur add/remove.
|
||||
|
||||
### US-22 — Refonte input MITRE dans le form
|
||||
**Pourquoi** : QA sprint 3 — pattern actuel (2 boutons textuels) trop verbeux.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- [ ] AC-22.1 : sous le label "MITRE Techniques", le composant affiche :
|
||||
- Une rangée de chips (techniques + tactiques sélectionnées).
|
||||
- En dessous, une rangée `[input texte autocomplete] [icône matrice]`.
|
||||
- L'input fait l'autocomplete inline (debounce 200ms, dropdown ↑↓Enter, comme sprint 2 mais EMBARQUÉ).
|
||||
- L'icône matrice à droite ouvre `MitreMatrixModal`.
|
||||
- Aucun bouton textuel "Add Technique" ni "Quick Search".
|
||||
- [ ] AC-22.2 : les chips affichent UNIQUEMENT la référence (T-id ou TA-id, ex : `T1059.001` ou `TA0007`). Le nom apparaît au survol via `title=` attribute.
|
||||
- [ ] AC-22.3 : `MitreTechniquePicker` existant est intégré dans le nouveau layout comme l'autocomplete inline. Garde la signature `onSelect`.
|
||||
- [ ] AC-22.4 : empty state : message court ("No techniques selected") dans la zone des chips. L'input et l'icône matrice restent visibles.
|
||||
- [ ] AC-22.5 : mode read-only (SOC sur simu non-done, ou tous sur simu done) : chips sans ×, input + icône cachés.
|
||||
|
||||
### US-23 — Dark mode
|
||||
**Pourquoi** : ergonomie demandée. Sprint 4 framing acté.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- [ ] AC-23.1 : un toggle theme dans la topbar (`Layout.tsx`), à droite du nom user. Icône lucide-react `Sun` / `Moon` / `Monitor`.
|
||||
- [ ] AC-23.2 : 3 états : `light`, `dark`, `system` (auto = suit `prefers-color-scheme`). Toggle cycle entre les 3.
|
||||
- [ ] AC-23.3 : persistance via `localStorage` (clé `mimic-theme`, valeur `'light'|'dark'|'system'`, défaut `'system'`).
|
||||
- [ ] AC-23.4 : Tailwind `darkMode: 'class'` activé. Classe `dark` appliquée sur `<html>` selon le résolu. Tokens DESIGN.md étendus avec variantes dark (canvas, paper, ink, graphite, charcoal, etc.). Primary HP Electric Blue garde sa teinte.
|
||||
- [ ] AC-23.5 : tous les composants principaux audités et utilisent les classes Tailwind `dark:bg-...` / `dark:text-...`. Pas de couleur hardcodée.
|
||||
- [ ] AC-23.6 : screenshots light + dark de `EngagementsListPage`, `SimulationFormPage`, `MitreMatrixModal` ouverte. Joints au summary.
|
||||
|
||||
### US-24 — Process hygiene : design-reviewer agent + screenshots mandatory
|
||||
**Pourquoi** : sprint 4 framing acté. Sprint 2/3 avait laissé passer des bugs visuels faute de pass design dédié.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- [ ] AC-24.1 : nouveau fichier `.claude/agents/design-reviewer.md`. Brief : revoit le diff frontend + les screenshots fournis par le frontend-builder, audit alignement / hiérarchie typo / DESIGN.md token usage / responsive sanity / cohérence visuelle. Read-only. Lance après frontend-builder, avant code-reviewer.
|
||||
- [ ] AC-24.2 : `.claude/agents/frontend-builder.md` mis à jour pour rendre EXPLICITE que screenshots sont MANDATORY avant de marquer la tâche terminée (au moins 1 par feature visible / état modifié). Liste explicite des screenshots attendus dans le summary.
|
||||
- [ ] AC-24.3 : workflow sprint mis à jour dans SPEC.md § Workflows : ajouter design-reviewer entre frontend-builder et code-reviewer.
|
||||
|
||||
### US-25 — Infra : PR helper script + Makefile target
|
||||
**Pourquoi** : capitaliser le pattern Gitea API curl utilisé en sprint 3 pour automatiser les PRs.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- [ ] AC-25.1 : `scripts/open-pr.sh` (executable, `set -euo pipefail`). Lit `~/.git-credentials`. Args : `--sprint=N`, `--title="..."`, `--body=path`. Détecte la branche courante + owner/repo depuis `git remote get-url origin`. POST `/api/v1/repos/{owner}/{repo}/pulls`. Imprime PR URL.
|
||||
- [ ] AC-25.2 : target Makefile `open-pr SPRINT=N TITLE="..." BODY=path` wrap le script.
|
||||
- [ ] AC-25.3 : documenté dans README.md (1 paragraphe).
|
||||
- [ ] AC-25.4 : team-lead utilise ce target pour ouvrir la PR sprint 4 (dogfooding).
|
||||
- [ ] 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, `.claude/agents/`, `scripts/`, `Makefile`, docs.
|
||||
**Scope strict** : `backend/`. Pas de touche au frontend, e2e, docs, agents, scripts.
|
||||
|
||||
### Livrables
|
||||
|
||||
**Modèle `Simulation`** — ajout uniquement :
|
||||
**Modèle `SimulationTemplate`** (`backend/app/models/simulation_template.py` — nouveau fichier)
|
||||
```python
|
||||
tactic_ids: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list)
|
||||
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")
|
||||
```
|
||||
|
||||
**Migration Alembic `0004_simulation_tactic_ids.py`** :
|
||||
- Upgrade : `op.add_column('simulations', sa.Column('tactic_ids', sa.JSON(), nullable=False, server_default=sa.text("'[]'")))`. ADD COLUMN OK sans batch sur SQLite. `server_default` règle le NOT NULL pour les lignes existantes.
|
||||
- Downgrade : `with op.batch_alter_table('simulations') as batch_op: batch_op.drop_column('tactic_ids')`.
|
||||
- Test : schéma post-upgrade a `tactic_ids` NOT NULL avec default `[]`.
|
||||
Ajouter à `backend/app/models/__init__.py`.
|
||||
|
||||
**Serializer** : `serialize_simulation(sim)` ajoute `tactics: [{id, name}]` enrichi runtime.
|
||||
**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).
|
||||
|
||||
**Service MITRE** :
|
||||
- Sprint 3 a indexé les tactiques par **short-name** (`"initial-access"`, `"execution"`, `...`) dans `_TACTIC_ORDER` et `TACTIC_NAMES`. La SPEC et le plan sprint 4 utilisent la notation **TA-id** (`"TA0001"`, `"TA0007"`, etc.). Il faut un mapping TA-id → short-name pour valider/résoudre les `tactic_ids` reçus.
|
||||
- Ajouter une constante module-level (12 entrées hardcodées, MITRE standard stable — attention, les TA-ids ne sont PAS séquentiels) :
|
||||
**Serializer** (`backend/app/serializers.py`)
|
||||
- Nouvelle fonction `serialize_template(t)` :
|
||||
```python
|
||||
_TACTIC_IDS: dict[str, str] = {
|
||||
"TA0001": "initial-access",
|
||||
"TA0002": "execution",
|
||||
"TA0003": "persistence",
|
||||
"TA0004": "privilege-escalation",
|
||||
"TA0005": "defense-evasion",
|
||||
"TA0006": "credential-access",
|
||||
"TA0007": "discovery",
|
||||
"TA0008": "lateral-movement",
|
||||
"TA0009": "collection",
|
||||
"TA0011": "command-and-control",
|
||||
"TA0010": "exfiltration",
|
||||
"TA0040": "impact",
|
||||
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,
|
||||
}
|
||||
```
|
||||
- Nouvelle fonction `lookup_tactic(tactic_id: str) -> dict | None` :
|
||||
```python
|
||||
short = _TACTIC_IDS.get(tactic_id)
|
||||
if short is None:
|
||||
return None
|
||||
return {"id": tactic_id, "name": TACTIC_NAMES[short]}
|
||||
```
|
||||
- Nouvelle fonction `get_tactic_name(tactic_id: str) -> str | None` : pareil mais retourne juste le name.
|
||||
- Validation `tactic_ids` dans `simulation_workflow.py` : un id absent de `_TACTIC_IDS` → 400 `{"error": "unknown tactic id: <id>"}`.
|
||||
- Pattern parallèle à `serialize_simulation` mais SANS les champs SOC / status / executed_at.
|
||||
|
||||
**Service workflow `simulation_workflow.py`** — modifications :
|
||||
1. **Guard `done` (AC-18.1)** : tout en haut de `apply_patch`, AVANT le check RBAC, si `simulation.status == "done"` → 409 `{error: "simulation is done — reopen first"}`. Vaut pour TOUS les rôles, admin compris.
|
||||
2. **SOC gate étendu** : `(REDTEAM_FIELDS | {"technique_ids", "tactic_ids"}) & payload.keys()`.
|
||||
3. **Validation `tactic_ids`** upfront (similaire à `technique_ids`) : tous les IDs validés contre le bundle, dedup `dict.fromkeys`. Bundle non chargé → 503.
|
||||
4. **Auto-transition** : ajouter le check `len(payload["tactic_ids"]) > 0` au calcul `auto_trigger`.
|
||||
5. **Transition `done → review_required` (AC-18.2)** — **implémentation précise** : le dict `_ALLOWED_TRANSITIONS` actuel est keyé par target status et a déjà une entrée `"review_required"` avec from={pending, in_progress} et roles={admin, redteam}. On NE peut PAS ajouter une 2e entrée avec la même clé. À la place, dans `transition()`, AVANT le lookup dict, ajoute un cas spécial qui suit les patterns existants du fichier :
|
||||
```python
|
||||
# transition() returns tuple[Any, int] | None — None on success, error tuple otherwise.
|
||||
# Existing functions use datetime.now(UTC) (timezone-aware, not deprecated utcnow).
|
||||
# Enum values are UPPERCASE: SimulationStatus.DONE, SimulationStatus.REVIEW_REQUIRED.
|
||||
if to_status == "review_required" and simulation.status == SimulationStatus.DONE:
|
||||
simulation.status = SimulationStatus.REVIEW_REQUIRED
|
||||
simulation.updated_at = datetime.now(UTC)
|
||||
db.session.commit()
|
||||
return None
|
||||
# ... reste de la fonction inchangée (dict lookup pour les autres cas)
|
||||
```
|
||||
Pas de check explicite du rôle ici — `@login_required` upstream + l'enum User limité à admin/redteam/soc rendent la défense superflue (KISS). Autres transitions depuis `done` (vers `pending`, `in_progress`, `done` lui-même) → 409 via le dict lookup qui ne les couvre pas.
|
||||
6. **Hook engagement auto-status (AC-19.1)** : après une transition de simu vers `in_progress` (auto OU manual), appeler une fonction `_maybe_activate_engagement(simulation)` qui, si `simulation.engagement.status == "planned"`, set `engagement.status = "active"` et `db.session.add(engagement)`. **NE PAS appeler `db.session.commit()` dans le helper** — le caller (`api/simulations.py:update_simulation`) gère le commit final, sinon double-commit.
|
||||
**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).
|
||||
|
||||
**API `simulations.py`** :
|
||||
- PATCH : le check status==done est fait dans `apply_patch` (voir au-dessus).
|
||||
- Transition : accepter le nouveau cas done → review_required pour admin/redteam/soc.
|
||||
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_simulations_tactics.py` (nouveau) : PATCH valide, ID inconnu → 400, bundle absent → 503, dedup, auto-transition, SOC → 403.
|
||||
- `test_simulations_done_readonly.py` (nouveau) : PATCH simu done → 409 (admin/redteam/soc). Reopen via transition → 200. Autres transitions depuis done → 409. Après reopen, PATCH OK.
|
||||
- `test_engagement_lifecycle.py` (nouveau) : création simu → engagement reste `planned`. PATCH simu → simu in_progress + engagement active. Engagement déjà active → pas de changement. Engagement closed → pas de changement.
|
||||
- Migration test : `tactic_ids` column NOT NULL après upgrade 0004 (similaire au pattern Alembic round-trip sprint 3).
|
||||
- Adapter `test_simulations_crud.py`, `test_simulations_patch.py`, `test_simulations_workflow.py` si nécessaire pour les assertions sur `tactics` et la garde done.
|
||||
- `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 les tests existants + nouveaux verts.
|
||||
**Quality bar** : ruff + mypy clean, tous tests existants + nouveaux verts.
|
||||
|
||||
### Règles
|
||||
- Pas de touche au frontend, `.claude/agents/`, `scripts/`, `Makefile`.
|
||||
- Renvoyer le summary attendu (cf `.claude/agents/backend-builder.md`).
|
||||
- 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.
|
||||
**Scope strict** : `frontend/` uniquement.
|
||||
|
||||
**SCREENSHOTS MANDATORY** (lesson sprint 2/3) : à la fin de ton travail, lance le dev server et fournis ≥ 5 screenshots :
|
||||
1. `EngagementsListPage` light + dark
|
||||
2. `SimulationFormPage` avec ≥ 2 chips technique + ≥ 1 chip tactique light + dark
|
||||
3. `MitreMatrixModal` ouverte avec sélections light + dark
|
||||
4. `UsersAdminPage` form "Create account" (alignement vérifié) light + dark
|
||||
5. `SimulationFormPage` status `done` (read-only + Reopen visible) light
|
||||
**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 final. Si le dev server n'a pas pu tourner, dis-le EXPLICITEMENT avec les raisons techniques précises.
|
||||
Paths absolus dans le summary. Si auth flow ne marche pas → escalade.
|
||||
|
||||
### Livrables
|
||||
|
||||
**US-17 — UI polish**
|
||||
- `EngagementsListPage.tsx` : supprimer le doublon "Create engagement". Garder un seul CTA "New" + icône `+` (selon convention AC-17.2).
|
||||
- `UsersAdminPage.tsx` : retravailler la grille pour pixel-perfect alignment. Choix laissé au builder (align-items: stretch + align-self, ou restructurer en 2 rangées).
|
||||
- Audit boutons : refactoriser ceux qui dépassent ≤ 8 chars. Garder "Mark for review" / "Clear all" / "Reopen" sans icône si pas d'icône évidente. Boutons à passer en icône+label : "Save Red Team" → icône + "Save", "Save SOC" → icône + "Save SOC", "ADD TECHNIQUE" → "+" + "Add" (rendu obsolète par US-22), "QUICK SEARCH" → "🔍" + "Search" (rendu obsolète par US-22).
|
||||
**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`.
|
||||
|
||||
**US-18 — Done read-only + Reopen**
|
||||
- `SimulationFormPage.tsx` :
|
||||
- Quand `simulation.status === 'done'` : tous champs disabled, `MitreTechniquesField disabled`, action bar montre UNIQUEMENT "Reopen" + icône (`↻`).
|
||||
- Bouton Reopen : visible admin/redteam/soc, click → `useTransitionSimulation` to `review_required`, toast.
|
||||
**API client** (`frontend/src/api/templates.ts` — nouveau)
|
||||
- `listTemplates()`, `getTemplate(id)`, `createTemplate(input)`, `updateTemplate(id, patch)`, `deleteTemplate(id)`.
|
||||
|
||||
**US-19 — Engagement auto-status (côté UI)**
|
||||
- `useUpdateSimulation` et `useTransitionSimulation` : ajouter `["engagement", eid]` et `["engagements"]` aux invalidations après mutation réussie. Pas d'autre changement visuel.
|
||||
- **Note (spec-reviewer Pass 3)** : `eid` n'est pas directement disponible dans la signature des hooks (qui prennent `sid`). Solution : lire `engagement_id` depuis la response simulation (le backend l'expose toujours, cf serialize_simulation sprint 2) OU le passer en arg supplémentaire au hook si plus propre. Pas un trou plan, juste à anticiper.
|
||||
**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]`.
|
||||
|
||||
**US-20 — Matrice MITRE attack.mitre.org look**
|
||||
- `MitreMatrixModal.tsx` overhaul :
|
||||
- `max-w-[98vw]`, `max-h-[80vh] overflow-y-auto`, JAMAIS de scroll horizontal.
|
||||
- `display: grid; grid-template-columns: repeat(12, minmax(0, 1fr))` pour répartir équitablement.
|
||||
- Cellule technique : `text-[12px]`, padding minimal, hairline border.
|
||||
- Header tactique : sticky top, fond contrasté, uppercase tracking, badge compteur à droite.
|
||||
- Sub-techniques indent `pl-[8px]`, fond `bg-cloud`.
|
||||
- Search input top inchangé.
|
||||
**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.
|
||||
|
||||
**US-21 — Tactic selection**
|
||||
- `MitreMatrixModal.tsx` : header de tactique cliquable (toggle). État visuel distinct.
|
||||
- Apply renvoie `{techniques, tactics}` au parent.
|
||||
- `MitreTechniquesField.tsx` : tactic chips style différencié `bg-primary text-canvas`. Auto-save.
|
||||
- **PATCH combiné (spec-reviewer fix #4)** : Apply depuis la matrice → UN SEUL PATCH `{technique_ids: [...], tactic_ids: [...]}` (les 2 listes ensemble). Pas 2 PATCH séquentiels (risque de race + risque que le 2nd appel hit le guard done). Pour les × remove ET les Quick Search adds, l'implémentation finale envoie aussi les 2 listes ensemble (`save({techniques, tactics})`) — fonctionnellement équivalent à un PATCH dimensionnel et plus simple à raisonner (single source of truth = local state). Spec-aligné post-code-review : "always send both dimensions" est la règle, le brief initial "dimension qui change" était over-spec. Toutes les mutations passent par `useUpdateSimulation` en un appel atomique.
|
||||
**Composants** (`frontend/src/components/`)
|
||||
- **`TemplatePickerModal.tsx`** (nouveau) : modale qui liste les templates, permet de cliquer pour instancier. Props : `engagementId`, `onClose`, `onInstantiated(simId)`.
|
||||
|
||||
**US-22 — Refonte input MITRE**
|
||||
- `MitreTechniquesField.tsx` :
|
||||
- Layout : chips area | input autocomplete inline + icône matrice button.
|
||||
- Plus de boutons textuels "Add Technique" / "Quick Search".
|
||||
- Chips compacts (T-id ou TA-id seul, name en `title=`).
|
||||
- Empty state minimal.
|
||||
- **`SimulationFormPage.tsx` — call site update (spec-reviewer fix #4)** : la signature de `MitreTechniquesField` change de `value: MitreTechnique[]` (sprint 3) à `value: {techniques: MitreTechnique[], tactics: MitreTactic[]}`. La page doit passer `value={{techniques: sim.techniques, tactics: sim.tactics}}` (le champ `sim.tactics` vient du nouveau serializer backend). TypeScript catch le miss mais flag-le explicitement pour ne pas l'oublier.
|
||||
**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)
|
||||
|
||||
**US-23 — Dark mode**
|
||||
- `Layout.tsx` : toggle theme dans la topbar. Hook `useTheme()` (localStorage + media query). 3 états avec cycle.
|
||||
- `tailwind.config.ts` : `darkMode: 'class'`. Tokens étendus avec variantes dark (recommandé via CSS variables sous `.dark { ... }` dans `index.css`, comme ça les composants n'ont pas à dupliquer leurs classes).
|
||||
- Audit tous les composants : aucune couleur hardcodée (pas de `bg-white`, `text-black`, `#xxxxxx` inline). Tous passent un check visuel light + dark.
|
||||
**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éutiliser les patterns sprint 1/2/3.
|
||||
- Respect DESIGN.md tokens.
|
||||
- Pas de dépendance npm sans escalade (sauf `lucide-react` autorisé).
|
||||
- **Interdiction absolue de toucher `e2e/`, `backend/`, `.claude/agents/`, `scripts/`, `Makefile`.**
|
||||
- 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 — Team-lead infra (US-24 + US-25, en parallèle des builders)
|
||||
## 4. Brief — Test verifier
|
||||
|
||||
**US-24 — Process hygiene**
|
||||
- Créer `.claude/agents/design-reviewer.md` avec frontmatter agent (model `opus`, tools : `Read`, `Glob`, `Grep`, `Bash` lecture seule). Brief : revoit diff frontend + screenshots, audit alignement / DESIGN.md tokens / cohérence visuelle / responsive.
|
||||
- Mettre à jour `.claude/agents/frontend-builder.md` : DoD strict sur les screenshots.
|
||||
- Mettre à jour SPEC.md § Workflows : insérer design-reviewer entre frontend-builder et code-reviewer.
|
||||
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)
|
||||
|
||||
**US-25 — PR helper**
|
||||
- Écrire `scripts/open-pr.sh` (cf AC-25.1).
|
||||
- Target Makefile `open-pr`.
|
||||
- Documenter README.md.
|
||||
- Dogfood en fin de sprint.
|
||||
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. Brief — Test verifier
|
||||
## 5. Definition of Done — Sprint 5
|
||||
|
||||
E2e Playwright :
|
||||
- `us17-ui-polish.spec.ts` — AC-17.1 (single button), AC-17.3 (alignment via locator boundingBox).
|
||||
- `us18-done-readonly-reopen.spec.ts` — AC-18.1 → AC-18.5.
|
||||
- `us19-engagement-auto-status.spec.ts` — AC-19.1 → AC-19.4.
|
||||
- `us20-matrix-fits-modal.spec.ts` — AC-20.1, AC-20.4 (no horizontal scroll via `boundingBox`).
|
||||
- `us21-tactic-selection.spec.ts` — AC-21.4 → AC-21.7.
|
||||
- `us22-mitre-input-redesign.spec.ts` — AC-22.1 → AC-22.5.
|
||||
- `us23-dark-mode.spec.ts` — AC-23.1 → AC-23.3.
|
||||
|
||||
US-24/25 non e2e (process / repo files). Couverture par dogfood (la PR sprint 4 elle-même est ouverte via `make open-pr`).
|
||||
|
||||
Adapter les sprint 2/3 e2e si l'audit boutons (AC-17.2) renomme certains labels.
|
||||
|
||||
**Spec-reviewer INFO B** : AC-22.2 change le format des chips de "T1059 — Command and Scripting Interpreter" (sprint 3) à juste "T1059" (avec name dans `title=`). Les e2e sprint 3 (notamment `us14-techniques-tags.spec.ts`) qui assertent le format complet doivent être mis à jour. Pas seulement les labels boutons.
|
||||
- [ ] 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
|
||||
## 6. Décisions arrêtées (utilisateur 2026-05-28)
|
||||
|
||||
1. **Tactic storage** : colonne JSON `tactic_ids` séparée. ✓ 2026-05-27
|
||||
2. **Dark mode default** : `system` (suit `prefers-color-scheme`, fallback `light` si non détecté). ✓ 2026-05-27
|
||||
3. **Matrix CSS fidelity** : look similaire qualitatif (frontend-builder itère, pas pixel-perfect). ✓ 2026-05-27
|
||||
4. **Reopen target** : `done → review_required`. ✓ mémoire (sprint 4 scope)
|
||||
5. **Reopen RBAC** : admin + redteam + soc. ✓ mémoire
|
||||
6. **Engagement auto trigger** : `planned → active` sur 1ère simu in_progress (auto-transition ou manual). Pas de retour arrière auto. ✓ mémoire
|
||||
7. **PR helper token source** : `~/.git-credentials` (parse user + token via sed, cf [[reference-gitea-pr-api]]). ✓ 2026-05-27
|
||||
8. **Workflow design-reviewer** : insérée entre frontend-builder et code-reviewer, read-only. Diff frontend + screenshots. Format rapport à la code-reviewer mais focus visuel/design. ✓ mémoire
|
||||
9. **Screenshots frontend-builder** : MANDATORY au sprint 4, en sortie du frontend-builder, paths absolus dans summary, refus de marquer la tâche done sans. ✓ mémoire
|
||||
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. ✅ Team-lead a re-appliqué le SPEC sprint 3 oublié (`ba313a3`).
|
||||
2. ✅ User a validé les 4 décisions ouvertes (tactic separated, theme system, matrix qualitative, token from ~/.git-credentials). Avec les 5 acquises en mémoire (sprint 4 scope), ça fait 9 décisions arrêtées.
|
||||
3. 🟡 Team-lead met à jour SPEC.md § Workflows + § Décisions techniques (§0).
|
||||
4. 🟡 Spec-reviewer valide le plan vs SPEC.md (anti-trous comme à sprint 3 — RBAC field-level, batch SQLite, scope ambigu).
|
||||
5. 🔵 Backend-builder : modèle + migration 0004 + workflow done-readonly/reopen + engagement auto-lifecycle + tactic_ids + tests.
|
||||
6. 🔵 Frontend-builder : UI polish + done read-only + matrix overhaul + tactic selection + input redesign + dark mode + screenshots.
|
||||
7. 🔵 Team-lead (US-24 + US-25 en parallèle de frontend) : design-reviewer agent + frontend-builder.md update + scripts/open-pr.sh + Makefile target.
|
||||
8. 🔵 Design-reviewer (NEW STEP) : revoit diff frontend + screenshots.
|
||||
9. 🔵 Code-reviewer : revoit le diff complet (LSP-first).
|
||||
10. 🔵 Test-verifier : e2e US-17 → US-23.
|
||||
11. 🟢 Team-lead : PR via `make open-pr` (dogfood AC-25.4) + récap.
|
||||
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`.
|
||||
|
||||
Reference in New Issue
Block a user