sprint/2-simulations #3
34
CHANGELOG.md
34
CHANGELOG.md
@@ -6,7 +6,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added — Sprint 1 (Auth + CRUD Engagement)
|
### Added — Sprint 2 (Simulations + MITRE ATT&CK)
|
||||||
|
|
||||||
|
**Backend** (Flask + SQLAlchemy, 131 pytest passing)
|
||||||
|
- `Simulation` model with redteam-side (`name`, `mitre_technique_id`, `mitre_technique_name`, `description`, `commands`, `prerequisites`, `executed_at`, `execution_result`) and SOC-side (`log_source`, `logs`, `soc_comment`, `incident_number`) fields, plus `status` enum (`pending` / `in_progress` / `review_required` / `done`), FK to `Engagement` (cascade delete) and `User` (creator).
|
||||||
|
- Alembic migration `0002_add_simulations.py`.
|
||||||
|
- 7 new endpoints: `GET/POST /api/engagements/<eid>/simulations`, `GET/PATCH/DELETE /api/simulations/<sid>`, `POST /api/simulations/<sid>/transition`, `GET /api/mitre/techniques?q=`.
|
||||||
|
- `simulation_workflow` service: field-level RBAC (SOC blocked when status ∈ {pending, in_progress}; SOC rejected if payload contains a redteam field), state machine (only forward transitions, validated by role), and auto-transition `pending → in_progress` when admin/redteam saves any non-empty redteam field.
|
||||||
|
- `mitre` service: STIX 2.1 Enterprise bundle loaded at boot, indexed by T-id + name + tactic. Ranked search (`exact-id > prefix-id > substring-name`), max 20 results. Includes sub-techniques (`T1059.001`). Boot-safe: missing/corrupt bundle logs a warning and the endpoint returns 503 instead of crashing the app.
|
||||||
|
- `make update-mitre` is now a real target — fetches the upstream STIX bundle and restarts the container if running. Bundle is committed at `backend/data/mitre/enterprise-attack.json` (~46 MB) so `make build` stays self-contained.
|
||||||
|
- Upfront validation of `executed_at` (no partial mutation on parse failure).
|
||||||
|
|
||||||
|
**Frontend** (React + TanStack Query, 63 vitest passing)
|
||||||
|
- `SimulationList` component rendered inside `EngagementDetailPage` (replaces the Sprint 1 placeholder). Columns: name, MITRE id, status badge, executed_at. Row click → SPA navigation via `useNavigate` (no full reload).
|
||||||
|
- `SimulationFormPage` (`/engagements/:eid/simulations/new` and `/engagements/:eid/simulations/:sid/edit`): single role-aware page with two cards ("Red Team" / "SOC"). Redteam/admin can edit all fields; SOC sees the redteam card as read-only and the SOC card disabled (with an explanatory banner) until status reaches `review_required`. Footer surfaces context-appropriate transition buttons ("Marquer en revue" / "Clôturer") and a confirmation modal for delete.
|
||||||
|
- `MitreTechniquePicker`: debounced (200 ms) autocomplete input with keyboard navigation (↑↓ / Enter / Escape), listbox accessibility, and an inline 503 error path. Selection populates both `mitre_technique_id` and `mitre_technique_name`. A `hasHydratedFromProps` ref prevents the input from being wiped mid-stroke when the parent emits `onChange(null, null)`.
|
||||||
|
- `SimulationStatusBadge`: 4 variants mapped to DESIGN.md tokens (`bg-fog`, `bg-primary-soft`, `bg-bloom-coral`, `bg-storm-deep`). Sibling of the existing `StatusBadge` rather than a forked generic — the two badges share visual scaffolding but their enums diverge.
|
||||||
|
- `ConfirmDialog`: generic modal used by the delete flow.
|
||||||
|
- TanStack Query hooks: `useEngagementSimulations`, `useSimulation`, `useCreateSimulation`, `useUpdateSimulation`, `useDeleteSimulation`, `useTransitionSimulation`, `useMitreSearch`. Mutations invalidate both the simulation detail key and the engagement-scoped list key.
|
||||||
|
|
||||||
|
**Acceptance tests** (Playwright, 68 specs)
|
||||||
|
- 6 new spec files (one per user story US-7 → US-12), 32 tests, all green.
|
||||||
|
- `us4-engagements.spec.ts` AC-4.9 assertion refreshed: the Sprint 1 placeholder text was correctly replaced by the new `SimulationList` (the test now asserts the new heading + "Nouvelle simulation" link).
|
||||||
|
- 5 pre-existing failures in `us1-bootstrap-admin.spec.ts` and `us6-deployment.spec.ts` remain — they hard-code `docker` in the test body and fail in dev environments that only have `podman`. The fixtures already support `MIMIC_CONTAINER_CMD`; the test bodies don't yet. Out of scope for Sprint 2 — to be picked up later.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- 2026-05-26 — `make update-mitre` upgraded from no-op placeholder to a real `curl` + optional container restart (Sprint 1 marker resolved).
|
||||||
|
- 2026-05-26 — `EngagementDetailPage` no longer renders the "Simulations à venir au Sprint 2" placeholder; it embeds `<SimulationList>` instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [Sprint 1] — Auth + CRUD Engagement (merged 2026-05-26)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
**Backend** (Flask + SQLAlchemy + SQLite, 63 pytest passing)
|
**Backend** (Flask + SQLAlchemy + SQLite, 63 pytest passing)
|
||||||
- `User` model with `admin / redteam / soc` enum, argon2 password hashing.
|
- `User` model with `admin / redteam / soc` enum, argon2 password hashing.
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -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.
|
**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 1 — Auth + CRUD Engagement**. Simulation workflow and MITRE TTP autocomplete arrive in Sprint 2+.
|
> Status: **Sprint 2 — Simulations + MITRE ATT&CK**. The Purple Team workflow (RedTeam fills test → marks for review → SOC documents detection → closes) is now end-to-end testable in the UI, with MITRE technique autocomplete for TTP tagging.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -56,7 +56,8 @@ Single-container deployment. A multistage Dockerfile builds the Vite frontend, t
|
|||||||
│ │
|
│ │
|
||||||
│ Flask (Python 3.12) │
|
│ Flask (Python 3.12) │
|
||||||
│ ├── /api/* ── blueprints (auth, users, │
|
│ ├── /api/* ── blueprints (auth, users, │
|
||||||
│ │ engagements) │
|
│ │ engagements, simulations,│
|
||||||
|
│ │ mitre) │
|
||||||
│ └── / ── SPA fallback → React build │
|
│ └── / ── SPA fallback → React build │
|
||||||
│ │
|
│ │
|
||||||
│ SQLAlchemy ── SQLite at /data/mimic.sqlite │
|
│ SQLAlchemy ── SQLite at /data/mimic.sqlite │
|
||||||
@@ -65,9 +66,11 @@ Single-container deployment. A multistage Dockerfile builds the Vite frontend, t
|
|||||||
```
|
```
|
||||||
|
|
||||||
- **Auth**: JWT Bearer tokens (HS256, 60-min TTL). Stateless — no refresh tokens, no server-side session.
|
- **Auth**: JWT Bearer tokens (HS256, 60-min TTL). Stateless — no refresh tokens, no server-side session.
|
||||||
- **Roles**: `admin` (super-user, manages users + engagements), `redteam` (CRUD engagements + simulations), `soc` (read engagements; will write the SOC half of simulations in Sprint 2).
|
- **Roles**: `admin` (super-user — cumulates redteam rights on engagements/simulations), `redteam` (CRUD engagements + simulations, full field access), `soc` (read everything, write-only on the SOC half of simulations once the redteam marks them `review_required`).
|
||||||
- **Password hashing**: argon2 via `argon2-cffi`.
|
- **Password hashing**: argon2 via `argon2-cffi`.
|
||||||
- **Migrations**: Alembic, applied automatically by the container entrypoint (`flask db upgrade && flask run`).
|
- **Migrations**: Alembic, applied automatically by the container entrypoint (`flask db upgrade && flask run`).
|
||||||
|
- **MITRE ATT&CK**: STIX 2.1 Enterprise bundle committed at `backend/data/mitre/enterprise-attack.json` and indexed at app boot. `make update-mitre` re-fetches the latest bundle and (if the container is running) restarts it to reload the index. The endpoint `GET /api/mitre/techniques?q=` powers the autocomplete on simulations.
|
||||||
|
- **Simulation workflow**: Pending → In progress (auto-transition when redteam saves any non-empty field) → Review required (manual, redteam) → Done (manual, redteam or SOC). The state machine is enforced server-side; the UI surfaces the appropriate transition button per role + current state.
|
||||||
|
|
||||||
See [`SPEC.md`](SPEC.md) § "Décisions techniques" for the full architecture rationale and [`DESIGN.md`](DESIGN.md) for the UI design system.
|
See [`SPEC.md`](SPEC.md) § "Décisions techniques" for the full architecture rationale and [`DESIGN.md`](DESIGN.md) for the UI design system.
|
||||||
|
|
||||||
@@ -102,7 +105,7 @@ mimic/
|
|||||||
| `make update` | `git pull && make build && make restart` |
|
| `make update` | `git pull && make build && make restart` |
|
||||||
| `make logs` | `docker logs -f mimic` |
|
| `make logs` | `docker logs -f mimic` |
|
||||||
| `make create-admin USER=… PASS=…` | Run `flask create-admin` inside the container |
|
| `make create-admin USER=… PASS=…` | Run `flask create-admin` inside the container |
|
||||||
| `make update-mitre` | No-op placeholder — Sprint 2+ will fetch the MITRE STIX bundle |
|
| `make update-mitre` | Fetch the latest MITRE STIX 2.1 Enterprise bundle into `backend/data/mitre/`; auto-restart the container if running. Commit the resulting file change manually. |
|
||||||
| `make test-backend` | `pytest -q` inside the container |
|
| `make test-backend` | `pytest -q` inside the container |
|
||||||
| `make test-frontend` | `npm run test -- --run` in `frontend/` |
|
| `make test-frontend` | `npm run test -- --run` in `frontend/` |
|
||||||
| `make test-e2e` | Playwright acceptance suite (container must be running) |
|
| `make test-e2e` | Playwright acceptance suite (container must be running) |
|
||||||
@@ -135,9 +138,9 @@ npm run dev # http://localhost:5173 with /api proxied to :5000
|
|||||||
Tests:
|
Tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend && pytest -q # 63 tests
|
cd backend && pytest -q # 131 tests
|
||||||
cd frontend && npm run test -- --run # 20 tests
|
cd frontend && npm run test -- --run # 63 tests
|
||||||
cd e2e && npx playwright test # 36 tests (needs container up)
|
cd e2e && npx playwright test # 68 tests (needs container up)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -4,4 +4,24 @@ Recurring mistakes and the rule we adopted so the same issue doesn't bite twice.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
_(empty — to be filled by the team-lead at the end of each sprint, with input from builders and reviewers)_
|
## Sprint 2 (closed 2026-05-26)
|
||||||
|
|
||||||
|
### Testing — Vitest module hoisting (frontend-builder)
|
||||||
|
**Context** : `vi.mock(path, factory)` is hoisted to module scope before any other statement runs. A `mockAuth(role)` helper that captures `role` in a closure crashes at runtime with `"role is not defined"` because the factory executes before the closure is set up.
|
||||||
|
**Lesson** : when a Vitest mock needs runtime-mutable state, declare the mutable in module scope (`let mockRole = 'redteam'`) and mutate it inside `beforeEach`. Closures over test-local variables don't survive the hoist.
|
||||||
|
|
||||||
|
### Testing — `useParams()` in MemoryRouter (frontend-builder)
|
||||||
|
**Context** : a page component that reads `useParams()` returns an empty object when rendered directly inside `<MemoryRouter>` — no params are extracted unless the component is mounted under a matching `<Route path="...">`.
|
||||||
|
**Lesson** : page tests that depend on params must wrap the component in `<MemoryRouter><Routes><Route path="/foo/:id" element={<Page />} /></Routes></MemoryRouter>` and set `initialEntries={['/foo/42']}`.
|
||||||
|
|
||||||
|
### Testing — jsdom missing browser APIs (frontend-builder)
|
||||||
|
**Context** : jsdom doesn't implement `Element.scrollIntoView`. Calling it in a component (e.g., scrolling the active autocomplete option into view) throws inside Vitest unless guarded.
|
||||||
|
**Lesson** : in components meant to run in both browser and jsdom, guard browser-only DOM APIs with optional chaining (`el?.scrollIntoView?.({ block: 'nearest' })`) or feature-detect before calling.
|
||||||
|
|
||||||
|
### Process — Reuse idle team agents via SendMessage, not Agent (team-lead)
|
||||||
|
**Context** : during post-review fixes, I re-spawned `backend-builder` and `frontend-builder` via `Agent({name: "..."})` even though the original instances were still alive (just idle). The system auto-suffixed `-2` and BOTH instances received the same brief, producing duplicate parallel commits on the branch. Frontend got two fix commits (`c9032a9` + `cf0e8a8`) where one would have sufficed; the second commit happened to layer cleanly on top, but only by luck.
|
||||||
|
**Lesson** : to redispatch work to an existing team agent, use `SendMessage({to: "backend-builder", ...})`. `Agent({name: ...})` creates a new instance when the name is taken. The team config at `~/.claude/teams/<team>/config.json` is the source of truth for who's already present.
|
||||||
|
|
||||||
|
### Workflow — Update e2e assertions when later sprints supersede placeholders (team-lead)
|
||||||
|
**Context** : Sprint 1 AC-4.9 asserted the literal text "Simulations à venir au Sprint 2" on `EngagementDetailPage`. Sprint 2 correctly replaced that placeholder with `<SimulationList>`, breaking the assertion. The test-verifier initially classified this as "pre-existing failure".
|
||||||
|
**Lesson** : whenever a later sprint replaces a placeholder asserted by an earlier sprint's e2e test, the earlier test must be refreshed in the same sprint (not deferred). A failing test that's "expected" is still a failing test — and it muddies the signal of the PR.
|
||||||
|
|||||||
445
tasks/todo.md
445
tasks/todo.md
@@ -1,315 +1,252 @@
|
|||||||
# Sprint 1 — Auth + CRUD Engagement
|
# Sprint 2 — Simulations + MITRE ATT&CK
|
||||||
|
|
||||||
**Branche** : `sprint/1-auth-engagements`
|
**Branche** : `sprint/2-simulations`
|
||||||
**Statut** : 🟢 PLAN APPROUVÉ (spec-reviewer 2026-05-26) — prêt pour dispatch backend-builder
|
**Statut** : 🟢 SPRINT COMPLET — 32 acceptance tests sprint 2 verts, code-review traité (2 MAJOR + 2 MINOR + 2 NITs fixés), PR prête
|
||||||
**Base** : `main`
|
**Base** : `main` (sprint 1 mergé en `7fc79cc`)
|
||||||
**Objectif** : poser l'infrastructure (Flask + SQLite + React + Docker + Makefile + tests) ET livrer une première feature de bout en bout testable sur l'UI — login + admin gère les comptes + tout utilisateur authentifié peut créer/lister/éditer/supprimer des engagements.
|
**Objectif** : livrer les simulations (CRUD + workflow Pending→In progress→Review required→Done) à l'intérieur d'un engagement, avec autocomplete MITRE ATT&CK alimenté par un bundle STIX local. C'est le cœur métier — l'app remplace enfin le fichier Excel partagé redteam/SOC.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. User stories
|
## 1. User stories
|
||||||
|
|
||||||
### US-1 — En tant qu'admin, je bootstrap le premier compte admin
|
### US-7 — En tant que redteam, je crée une simulation dans un engagement
|
||||||
**Pourquoi** : sinon impossible d'utiliser l'application au premier démarrage.
|
**Pourquoi** : c'est la feature centrale du sprint 2.
|
||||||
|
|
||||||
**Critères d'acceptation**
|
**Critères d'acceptation**
|
||||||
- [ ] AC-1.1 : la commande `make create-admin USER=alice PASS=p4ssw0rd` crée un user `alice` avec le rôle `admin` et le password hashé (argon2).
|
- [ ] AC-7.1 : `POST /api/engagements/<eid>/simulations {name}` (admin|redteam) → 201 + simulation `{id, engagement_id, name, status: "pending", ...}`. `name` requis, non vide.
|
||||||
- [ ] AC-1.2 : la commande échoue proprement (exit ≠ 0, message clair) si le username existe déjà.
|
- [ ] AC-7.2 : autres rôles (soc) → 403.
|
||||||
- [ ] AC-1.3 : la commande échoue si le password fait moins de 8 caractères.
|
- [ ] AC-7.3 : engagement inexistant → 404. Engagement existant mais aucune simulation → liste vide.
|
||||||
- [ ] AC-1.4 : la commande s'exécute via `docker exec mimic flask create-admin …` (le Makefile encapsule cet appel).
|
- [ ] AC-7.4 : `GET /api/engagements/<eid>/simulations` (auth) → liste des simulations de l'engagement, ordonnée `created_at desc`.
|
||||||
|
- [ ] AC-7.5 : page `/engagements/:eid` (EngagementDetailPage) remplace le placeholder Sprint 2 par une section "Simulations" : liste (colonnes: name, MITRE id, status badge, executed_at) + bouton "Nouvelle simulation" pour admin/redteam.
|
||||||
|
- [ ] AC-7.6 : depuis cette liste, click sur une ligne → ouvre `/engagements/:eid/simulations/:sid/edit` (page d'édition role-aware, unique URL pour view+edit).
|
||||||
|
|
||||||
### US-2 — En tant qu'utilisateur, je me connecte et me déconnecte
|
### US-8 — En tant que redteam, je renseigne les détails techniques d'une simulation
|
||||||
**Pourquoi** : porte d'entrée de l'application.
|
**Pourquoi** : c'est la trace de ce que la redteam a exécuté.
|
||||||
|
|
||||||
**Critères d'acceptation**
|
**Critères d'acceptation**
|
||||||
- [ ] AC-2.1 : `POST /api/auth/login {username, password}` retourne `{access_token, user: {id, username, role}}` (200) si credentials valides.
|
- [ ] AC-8.1 : `PATCH /api/simulations/<sid>` (admin|redteam) accepte les champs redteam : `name`, `mitre_technique_id`, `mitre_technique_name`, `description`, `commands` (texte multiligne, une commande par ligne), `prerequisites`, `executed_at` (ISO datetime), `execution_result`. Champs partiels OK.
|
||||||
- [ ] AC-2.2 : 401 si credentials invalides, avec un message générique ("Invalid credentials") — pas de fuite username vs password.
|
- [ ] AC-8.2 : règle d'auto-transition pending → in_progress. Trigger PRÉCIS : `PATCH /api/simulations/<sid>` par admin|redteam où **le payload JSON contient au moins une clé parmi les champs redteam** (`name`, `mitre_technique_id`, `mitre_technique_name`, `description`, `commands`, `prerequisites`, `executed_at`, `execution_result`) **dont la valeur n'est ni `null` ni une string vide ni une liste vide**, ET status courant == `pending`. La comparaison se fait sur le payload entrant — pas sur l'état final de la simulation. Un PATCH qui ne ré-envoie qu'un champ inchangé (ex: même `name`) déclenche quand même la transition, car c'est une action explicite "la redteam saisit". L'auto-transition ne se déclenche jamais sur un PATCH `soc`.
|
||||||
- [ ] AC-2.3 : `POST /api/auth/logout` invalide le token côté client (UI supprime le token). Côté serveur : optionnel V1, on accepte un logout client-side.
|
- [ ] AC-8.3 : `commands` est stocké en colonne `text` (chaîne multiligne, une commande par ligne). Sérialisation API = texte brut tel que stocké. Le frontend affiche dans un `<textarea>`.
|
||||||
- [ ] AC-2.4 : page `/login` affiche le formulaire ; soumission OK → redirection `/engagements`. Soumission KO → message d'erreur visible.
|
- [ ] AC-8.4 : `executed_at` valide ISO 8601 ou null. Si invalide → 400 `{error: "invalid executed_at"}`.
|
||||||
- [ ] AC-2.5 : navigation vers `/engagements` sans token → redirection `/login`.
|
- [ ] AC-8.5 : page `/engagements/:eid/simulations/:sid` affiche un formulaire avec deux sections visibles ("Red Team" et "SOC"). Pour admin/redteam, les deux sections sont éditables. Validation client : `name` non vide.
|
||||||
- [ ] AC-2.6 : si une requête API retourne 401 (token expiré ou invalide), l'intercepteur axios purge le token et redirige vers `/login` avec un toast "Session expirée".
|
- [ ] AC-8.6 : autocomplete MITRE dans le champ "Technique" — voir US-10.
|
||||||
|
|
||||||
### US-3 — En tant qu'admin, je gère les comptes utilisateurs
|
### US-9 — En tant qu'analyste SOC, je remplis ma partie de la simulation
|
||||||
**Pourquoi** : créer redteam/soc accounts depuis l'UI.
|
**Pourquoi** : le SOC documente la détection sans toucher au scope redteam.
|
||||||
|
|
||||||
**Critères d'acceptation**
|
**Critères d'acceptation**
|
||||||
- [ ] AC-3.1 : `GET /api/users` (admin only) → liste `[{id, username, role, created_at}]`.
|
- [ ] AC-9.1 : `PATCH /api/simulations/<sid>` envoyé par un user `soc` n'accepte QUE les champs SOC : `log_source`, `logs`, `soc_comment`, `incident_number`. Si la requête contient un champ redteam → 403 `{error: "soc cannot edit redteam fields"}`.
|
||||||
- [ ] AC-3.2 : `POST /api/users {username, password, role}` (admin only) → 201 + objet user (sans password_hash). 400 si username existe ou password < 8 chars.
|
- [ ] AC-9.2 : un user `soc` ne peut PATCH une simulation que si son status est `review_required` ou `done`. Avant ça → 403 `{error: "simulation not ready for SOC review"}`.
|
||||||
- [ ] AC-3.3 : `PATCH /api/users/<id> {role?, password?}` (admin only) → 200, modifie role et/ou password.
|
- [ ] AC-9.3 : page `/engagements/:eid/simulations/:sid` pour un user `soc` : la section "Red Team" est rendue en read-only (champs grisés) ; la section "SOC" est éditable.
|
||||||
- [ ] AC-3.4 : `DELETE /api/users/<id>` (admin only) → 204. Refuse de supprimer le dernier admin (409).
|
- [ ] AC-9.4 : si la simulation est en `pending` ou `in_progress` et qu'un soc visite la page, un bandeau "Simulation pas encore en revue — la redteam doit la marquer comme 'Review required' avant que vous puissiez intervenir" s'affiche, les champs SOC sont désactivés.
|
||||||
- [ ] AC-3.5 : tout autre rôle (redteam/soc) appelant ces endpoints reçoit 403.
|
|
||||||
- [ ] AC-3.6 : page `/admin/users` (admin only) liste les users avec actions "Créer", "Modifier rôle", "Reset password", "Supprimer".
|
|
||||||
- [ ] AC-3.7 : un user redteam/soc qui visite `/admin/users` est redirigé vers `/engagements` avec un toast "Accès refusé".
|
|
||||||
|
|
||||||
### US-4 — En tant qu'utilisateur authentifié, je gère les engagements
|
### US-10 — En tant que redteam, j'autocomplète une technique MITRE ATT&CK
|
||||||
**Pourquoi** : la feature métier centrale du Sprint 1.
|
**Pourquoi** : éviter de taper l'id à la main, garantir la cohérence.
|
||||||
|
|
||||||
**Critères d'acceptation**
|
**Critères d'acceptation**
|
||||||
- [ ] AC-4.1 : `GET /api/engagements` (auth) → `[{id, name, description, start_date, end_date, status, created_at, created_by}]`.
|
- [ ] AC-10.1 : `make update-mitre` télécharge le bundle STIX 2.1 Enterprise depuis `https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json` et l'écrit dans `backend/data/mitre/enterprise-attack.json`. Le bundle est COMMITTÉ dans le repo (`make build` reste autosuffisant). `make update-mitre` reste l'unique méthode de rafraîchissement et le diff résultant est committé manuellement.
|
||||||
- [ ] AC-4.2 : `POST /api/engagements {name, description?, start_date, end_date?, status?}` (auth) → 201. Valide : `name` non vide, `start_date` parseable, `end_date >= start_date` si fournie, `status ∈ {planned, active, closed}` (défaut `planned`).
|
- [ ] AC-10.2 : `GET /api/mitre/techniques?q=<query>` (auth, tous rôles) → liste max 20 résultats `[{id, name, tactics: ["initial-access", ...]}]`. Recherche full-text sur `id` (ex: "T1059") OU `name` (ex: "Command and Scripting Interpreter"), case-insensitive, ordonnée : match exact id > match préfixe id > match nom.
|
||||||
- [ ] AC-4.3 : `GET /api/engagements/<id>` (auth) → 200 + objet, 404 si inconnu.
|
- [ ] AC-10.3 : si le bundle local est absent → endpoint répond 503 `{error: "mitre bundle not loaded"}`. Le team-lead documente `make update-mitre` dans le README.
|
||||||
- [ ] AC-4.4 : `PATCH /api/engagements/<id>` (auth, redteam ou admin) → 200, modifie les champs fournis.
|
- [ ] AC-10.4 : sous-techniques (id format `T1059.001`) incluses dans l'index.
|
||||||
- [ ] AC-4.5 : `DELETE /api/engagements/<id>` (admin ou redteam) → 204.
|
- [ ] AC-10.5 : composant frontend `MitreTechniquePicker` : input + dropdown des matches (debounce 200ms, navigation clavier ↑↓ + Enter, Escape ferme le dropdown), affichage `T1059 — Command and Scripting Interpreter (initial-access)`. La sélection d'une suggestion remplit `mitre_technique_id` ET `mitre_technique_name` du form. Pas de fallback free-text : si l'utilisateur tape sans sélectionner, le champ technique reste vide en sortie de form (id et name `null`).
|
||||||
- [ ] AC-4.6 : un user `soc` peut lire (GET) mais pas créer/modifier/supprimer (403).
|
|
||||||
- [ ] AC-4.7 : page `/engagements` liste les engagements avec colonnes (name, status badge, dates, created_by). Boutons "Nouveau", "Voir", "Éditer", "Supprimer" selon rôle.
|
|
||||||
- [ ] AC-4.8 : page `/engagements/new` et `/engagements/<id>/edit` (formulaire avec validation côté client + erreurs API affichées).
|
|
||||||
- [ ] AC-4.9 : page `/engagements/<id>` (détail), placeholder "Simulations à venir au Sprint 2".
|
|
||||||
|
|
||||||
### US-5 — En tant qu'utilisateur, l'UI respecte DESIGN.md
|
### US-11 — En tant qu'utilisateur, je transitionne le workflow d'une simulation
|
||||||
**Pourquoi** : non négociable selon SPEC.md.
|
**Pourquoi** : la coordination redteam ↔ soc passe par le statut.
|
||||||
|
|
||||||
**Critères d'acceptation**
|
**Critères d'acceptation**
|
||||||
- [ ] AC-5.1 : la palette, typographie, espacements, composants (boutons, inputs, badges) suivent strictement `DESIGN.md`.
|
- [ ] AC-11.1 : `POST /api/simulations/<sid>/transition {to: "review_required"}` → 200, requiert status courant ∈ {`pending`, `in_progress`} et role ∈ {admin, redteam}. Refuse les autres transitions → 409 `{error: "invalid transition"}`.
|
||||||
- [ ] AC-5.2 : layout responsive desktop-first (≥ 1024px), pas de breakage visible jusqu'à 1280×720 minimum.
|
- [ ] AC-11.2 : `POST /api/simulations/<sid>/transition {to: "done"}` → 200, requiert status courant == `review_required` et role ∈ {admin, redteam, soc}. Autres transitions → 409.
|
||||||
- [ ] AC-5.3 : états loading / error / empty implémentés pour la liste d'engagements et la liste d'users.
|
- [ ] AC-11.3 : aucune transition arrière (ex: done → pending) n'est permise. Pas de transition `→ pending` ni `→ in_progress` via cet endpoint (le passage à `in_progress` est strictement automatique cf AC-8.2).
|
||||||
|
- [ ] AC-11.4 : sur la page d'édition simulation, deux boutons contextuels :
|
||||||
### US-6 — Le livrable se déploie via Docker + Makefile
|
- Pour admin/redteam, status ∈ {pending, in_progress} : bouton "Marquer en revue".
|
||||||
**Pourquoi** : exigence SPEC.md.
|
- Pour admin/redteam/soc, status == review_required : bouton "Clôturer".
|
||||||
|
- Sinon : boutons cachés.
|
||||||
|
- [ ] AC-11.5 : après transition réussie, la query simulation et la liste sont invalidées (TanStack Query), le badge se met à jour.
|
||||||
|
|
||||||
|
### US-12 — En tant qu'admin ou redteam, je supprime une simulation
|
||||||
**Critères d'acceptation**
|
**Critères d'acceptation**
|
||||||
- [ ] AC-6.1 : `make build` produit l'image docker `mimic:latest` (Dockerfile multistage : Node build → Python runtime).
|
- [ ] AC-12.1 : `DELETE /api/simulations/<sid>` (admin|redteam) → 204.
|
||||||
- [ ] AC-6.2 : `make start` lance le container, l'app est accessible sur `http://localhost:5000` (front + API).
|
- [ ] AC-12.2 : `soc` → 403.
|
||||||
- [ ] AC-6.3 : `make stop`, `make restart`, `make logs` fonctionnent.
|
- [ ] AC-12.3 : suppression d'engagement (cascade) supprime toutes ses simulations.
|
||||||
- [ ] AC-6.4 : SQLite persisté via volume nommé `mimic-data` (la DB survit à `make restart`).
|
- [ ] AC-12.4 : bouton "Supprimer" sur la page d'édition (admin/redteam uniquement), avec confirmation modal.
|
||||||
- [ ] AC-6.5 : `make test-backend`, `make test-frontend`, `make test-e2e` exécutent les suites respectives.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Brief technique — Backend Builder
|
## 2. Brief technique — Backend Builder
|
||||||
|
|
||||||
**Scope strict** : `backend/`, `Dockerfile`, `Makefile` (en collab avec ce sprint uniquement).
|
**Scope strict** : `backend/`, `docker/`, `Makefile` (target `update-mitre`).
|
||||||
|
|
||||||
### Livrables
|
### Livrables
|
||||||
1. **Structure** :
|
|
||||||
```
|
|
||||||
backend/
|
|
||||||
__init__.py # ⚠️ requis pour que `backend.app:create_app` soit importable depuis /app dans le Dockerfile
|
|
||||||
app/
|
|
||||||
__init__.py # create_app() factory
|
|
||||||
config.py # DevConfig / ProdConfig (SECRET_KEY, JWT_SECRET, SQLALCHEMY_DATABASE_URI)
|
|
||||||
extensions.py # db = SQLAlchemy(), migrate = Migrate()
|
|
||||||
cli.py # @app.cli.command("create-admin")
|
|
||||||
models/
|
|
||||||
__init__.py
|
|
||||||
user.py # User(id, username, password_hash, role, created_at)
|
|
||||||
engagement.py # Engagement(id, name, description, start_date, end_date, status, created_at, created_by)
|
|
||||||
auth/
|
|
||||||
__init__.py
|
|
||||||
jwt.py # encode_token / decode_token
|
|
||||||
hashing.py # argon2 hash & verify
|
|
||||||
decorators.py # @login_required, @role_required("admin")
|
|
||||||
api/
|
|
||||||
__init__.py
|
|
||||||
auth.py # /login, /logout, /me
|
|
||||||
users.py # CRUD users (admin)
|
|
||||||
engagements.py # CRUD engagements
|
|
||||||
serializers.py # to_dict() helpers — engagement renvoie created_by={id, username}, jamais l'objet User brut
|
|
||||||
errors.py # uniform JSON error handler
|
|
||||||
migrations/ # alembic init + 0001 initial schema
|
|
||||||
tests/
|
|
||||||
conftest.py # app fixture, db fixture, auth helpers
|
|
||||||
test_auth.py # login OK + 401 invalid + 401 token expiré
|
|
||||||
test_users.py # CRUD admin only, 403 redteam/soc, last-admin protection (AC-3.4)
|
|
||||||
test_engagements.py # CRUD redteam/admin, 403 soc en write, serializer created_by
|
|
||||||
test_cli_create_admin.py # success + duplicate username (AC-1.2) + password < 8 chars (AC-1.3)
|
|
||||||
pyproject.toml
|
|
||||||
requirements.txt # flask, flask-sqlalchemy, flask-migrate, pyjwt, argon2-cffi, ruff, mypy, pytest
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Endpoints** : voir critères AC-2 / AC-3 / AC-4.
|
**Modèle `Simulation`** (`backend/app/models/simulation.py`)
|
||||||
3. **Modèles** : voir SPEC.md § Modèle de données.
|
| Champ | Type | Notes |
|
||||||
4. **Config** :
|
|---|---|---|
|
||||||
- `SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.environ.get('MIMIC_DB_PATH', '/data/mimic.sqlite')}"` — l'env var `MIMIC_DB_PATH` du Dockerfile surcharge le chemin si présent (utile pour tests ou changement de mount).
|
| id | int PK | |
|
||||||
- `JWT_SECRET` lu depuis env var `MIMIC_JWT_SECRET`, requis (raise si absent en Prod).
|
| engagement_id | int FK Engagement, CASCADE | requis |
|
||||||
- `JWT_EXP_MINUTES = 60`.
|
| name | str, NOT NULL | redteam-side |
|
||||||
5. **CLI** : `flask create-admin <user> <pass>` (avec validations AC-1.2/1.3).
|
| mitre_technique_id | str, nullable | ex "T1059" / "T1059.001" |
|
||||||
6. **Serializer engagement** : la réponse JSON expose `created_by` sous forme `{"id": <int>, "username": <str>}` — pas l'objet User complet, pas seulement l'id.
|
| mitre_technique_name | str, nullable | snapshot pour résilience aux maj MITRE |
|
||||||
7. **Tests** : couverture success / failure / edge sur chaque endpoint et CLI. Les AC-1.2 et AC-1.3 doivent avoir leur propre test dans `test_cli_create_admin.py`.
|
| description | text, nullable | redteam-side |
|
||||||
8. **Lint / typing** : ruff clean, mypy clean sur `app/`.
|
| commands | text, nullable | chaîne multiligne, une commande par ligne — pas de JSON |
|
||||||
|
| prerequisites | text, nullable | redteam-side |
|
||||||
|
| executed_at | datetime, nullable | redteam-side |
|
||||||
|
| execution_result | text, nullable | redteam-side |
|
||||||
|
| log_source | text, nullable | soc-side |
|
||||||
|
| logs | text, nullable | soc-side |
|
||||||
|
| soc_comment | text, nullable | soc-side |
|
||||||
|
| incident_number | str, nullable | soc-side |
|
||||||
|
| status | enum(pending/in_progress/review_required/done), défaut `pending` | |
|
||||||
|
| created_at | datetime | |
|
||||||
|
| updated_at | datetime, nullable | mis à jour à chaque PATCH |
|
||||||
|
| created_by_id | int FK User | |
|
||||||
|
|
||||||
|
**Migration Alembic** `0002_add_simulations.py` — table `simulations` + FK indexes (`engagement_id`, `created_by_id`).
|
||||||
|
|
||||||
|
**Endpoints** (nouveau blueprint `backend/app/api/simulations.py`)
|
||||||
|
- `GET /api/engagements/<eid>/simulations` — list, auth, all roles
|
||||||
|
- `POST /api/engagements/<eid>/simulations` — create, admin|redteam
|
||||||
|
- `GET /api/simulations/<sid>` — get, auth
|
||||||
|
- `PATCH /api/simulations/<sid>` — update avec RBAC field-level (voir AC-8/9)
|
||||||
|
- `DELETE /api/simulations/<sid>` — admin|redteam
|
||||||
|
- `POST /api/simulations/<sid>/transition` — state machine
|
||||||
|
- `GET /api/mitre/techniques?q=` — autocomplete (200 OK + array, 503 si bundle absent)
|
||||||
|
|
||||||
|
**Serializer** : retourne `created_by={id, username}` (pattern existant). `commands` → string brut (tel que stocké en DB, peut être `null` ou chaîne multiligne).
|
||||||
|
|
||||||
|
**Service workflow** (`backend/app/services/simulation_workflow.py`)
|
||||||
|
- `apply_patch(simulation, payload, user)` :
|
||||||
|
- sépare champs redteam vs soc
|
||||||
|
- vérifie RBAC field-level
|
||||||
|
- détecte auto-transition pending → in_progress (AC-8.2)
|
||||||
|
- applique le patch + commit
|
||||||
|
- `transition(simulation, to_status, user)` :
|
||||||
|
- vérifie state machine (transitions autorisées)
|
||||||
|
- vérifie RBAC role
|
||||||
|
- met à jour status + updated_at
|
||||||
|
|
||||||
|
**Service MITRE** (`backend/app/services/mitre.py`)
|
||||||
|
- Au boot de l'app : tente de charger `backend/data/mitre/enterprise-attack.json` en mémoire ; si absent ou parse error → flag `mitre_loaded = False` (logue warning, app démarre quand même).
|
||||||
|
- Indexe les objets STIX `type == "attack-pattern"` : extract `external_id` (T-id), `name`, `kill_chain_phases[].phase_name`.
|
||||||
|
- Fonction `search(query, limit=20)` : ranking par exact-id > prefix-id > substring-name.
|
||||||
|
|
||||||
|
**`Makefile`** : remplacer le no-op de `update-mitre` par :
|
||||||
|
```makefile
|
||||||
|
MITRE_URL ?= https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json
|
||||||
|
update-mitre:
|
||||||
|
@mkdir -p backend/data/mitre
|
||||||
|
@curl -fsSL "$(MITRE_URL)" -o backend/data/mitre/enterprise-attack.json
|
||||||
|
@echo "MITRE bundle updated"
|
||||||
|
@if docker ps --format '{{.Names}}' | grep -q "^$(CONTAINER)$$"; then \
|
||||||
|
echo "Restarting $(CONTAINER) to reload MITRE bundle..."; \
|
||||||
|
docker restart $(CONTAINER); \
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dockerfile** : copier `backend/data/mitre/` dans l'image (présent dans le repo, donc fonctionne au premier build).
|
||||||
|
|
||||||
|
**Bundle MITRE** : committé dans le repo à `backend/data/mitre/enterprise-attack.json`. Le backend-builder l'inclut dans son premier commit via `make update-mitre`.
|
||||||
|
|
||||||
|
**Tests pytest** (`backend/tests/`)
|
||||||
|
- `test_simulations_crud.py` : create + list + get + delete + cascade, RBAC create/delete.
|
||||||
|
- `test_simulations_patch.py` : auto-transition pending→in_progress, RBAC field-level soc, blocage soc avant review_required (AC-9.2).
|
||||||
|
- `test_simulations_workflow.py` : transitions valides/invalides, RBAC par transition.
|
||||||
|
- `test_mitre.py` : load bundle (fixture mini), search ranking, endpoint 503 si pas chargé, sous-techniques incluses.
|
||||||
|
|
||||||
|
Tous les tests existants doivent rester verts. Lint ruff + mypy clean.
|
||||||
|
|
||||||
### Règles
|
### Règles
|
||||||
- Pas de touche au frontend.
|
- Pas de touche au frontend.
|
||||||
- Pas d'invention de dépendances hors de la liste ci-dessus sans escalade au team-lead.
|
- Pas d'invention de dépendances (pas besoin d'en ajouter).
|
||||||
- Renvoyer le summary attendu (cf. `.claude/agents/backend-builder.md`).
|
- Renvoyer le summary attendu (cf. `.claude/agents/backend-builder.md`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Brief technique — Frontend Builder
|
## 3. Brief technique — Frontend Builder
|
||||||
|
|
||||||
**Scope strict** : `frontend/` UNIQUEMENT. Le dossier `e2e/` est **interdit** au frontend-builder — il est sous la responsabilité exclusive du test-verifier (scaffolding Playwright + tests).
|
**Scope strict** : `frontend/` UNIQUEMENT. Interdiction de toucher `e2e/`.
|
||||||
|
|
||||||
### Livrables
|
### Livrables
|
||||||
1. **Structure** :
|
|
||||||
```
|
|
||||||
frontend/
|
|
||||||
package.json # react, react-dom, react-router-dom, @tanstack/react-query, axios, tailwindcss, vite, typescript, vitest, @testing-library/react
|
|
||||||
vite.config.ts # proxy /api -> http://localhost:5000 en dev
|
|
||||||
tailwind.config.ts # tokens issus de DESIGN.md, font-family principale = "Inter"
|
|
||||||
tsconfig.json
|
|
||||||
index.html
|
|
||||||
src/
|
|
||||||
main.tsx
|
|
||||||
App.tsx # router, QueryClientProvider
|
|
||||||
api/
|
|
||||||
client.ts # axios + interceptor (Bearer + 401 purge → /login)
|
|
||||||
auth.ts # login, me
|
|
||||||
users.ts # CRUD users
|
|
||||||
engagements.ts # CRUD engagements
|
|
||||||
types.ts
|
|
||||||
hooks/
|
|
||||||
useAuth.ts # token in memory + localStorage, role helpers (isAdmin, isRedteam, isSoc)
|
|
||||||
useEngagements.ts # TanStack Query hooks
|
|
||||||
useUsers.ts
|
|
||||||
useToast.ts # provider + hook pour notifications éphémères
|
|
||||||
pages/
|
|
||||||
LoginPage.tsx
|
|
||||||
EngagementsListPage.tsx
|
|
||||||
EngagementFormPage.tsx # new + edit
|
|
||||||
EngagementDetailPage.tsx
|
|
||||||
UsersAdminPage.tsx
|
|
||||||
components/
|
|
||||||
Layout.tsx # nav, topbar, role-aware menu
|
|
||||||
ProtectedRoute.tsx # redirect to /login if no token, role gate
|
|
||||||
StatusBadge.tsx
|
|
||||||
FormField.tsx
|
|
||||||
EmptyState.tsx
|
|
||||||
ErrorState.tsx
|
|
||||||
LoadingState.tsx
|
|
||||||
Toast.tsx # composant + ToastProvider (utilisé par AC-2.6 + AC-3.7)
|
|
||||||
styles/
|
|
||||||
index.css # tailwind base + DESIGN.md tokens
|
|
||||||
fonts.css # @font-face Inter (bundlée localement dans public/fonts/, AUCUN CDN)
|
|
||||||
public/
|
|
||||||
fonts/ # fichiers Inter .woff2 (déposés via npm install + copie post-install, ou commit direct)
|
|
||||||
tests/
|
|
||||||
components/*.test.tsx # Vitest
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Police** : `Inter` (substitut Forma DJR Micro choisi par défaut parmi les 3 options DESIGN.md §86-89). Bundlée localement en `.woff2`, jamais via Google Fonts/CDN. Configurée dans `tailwind.config.ts` comme `font-sans` et chargée via `@font-face` dans `styles/fonts.css`.
|
**Types** (`frontend/src/api/types.ts`) : ajouter `Simulation`, `SimulationStatus`, `MitreTechnique`, et les payloads PATCH/POST.
|
||||||
|
|
||||||
3. **Routing** :
|
**Client API** (`frontend/src/api/simulations.ts`, `frontend/src/api/mitre.ts`)
|
||||||
- `/login`
|
- `listSimulations(engagementId)`, `createSimulation(engagementId, {name})`, `getSimulation(id)`, `updateSimulation(id, patch)`, `deleteSimulation(id)`, `transitionSimulation(id, to)`.
|
||||||
- `/engagements` (auth, all roles)
|
- `searchMitreTechniques(query)`.
|
||||||
- `/engagements/new` (auth, redteam|admin)
|
|
||||||
- `/engagements/:id` (auth, all roles)
|
|
||||||
- `/engagements/:id/edit` (auth, redteam|admin)
|
|
||||||
- `/admin/users` (auth, admin only)
|
|
||||||
- `/` → redirige vers `/engagements` ou `/login`
|
|
||||||
|
|
||||||
4. **Auth** : token JWT en mémoire + `localStorage` ; intercepteur axios ajoute `Authorization: Bearer <token>` ; 401 → purge token + redirect `/login` + toast "Session expirée" (AC-2.6).
|
**Hooks TanStack Query** (`frontend/src/hooks/useSimulations.ts`)
|
||||||
|
- `useEngagementSimulations(engagementId)`, `useSimulation(id)`, mutations `useCreateSimulation`, `useUpdateSimulation`, `useDeleteSimulation`, `useTransitionSimulation`.
|
||||||
|
- Invalidation : transition + update + delete invalident `["simulations", id]` et `["engagements", eid, "simulations"]`.
|
||||||
|
|
||||||
5. **Tests Vitest** : 1 test par composant non trivial (états loading/error/empty, comportement des rôles dans `ProtectedRoute`, Toast déclenché par 401).
|
**Hook `useMitre`** : `useMitreSearch(query, enabled)` (debounce géré côté composant, hook sans staleTime court — cache 5min).
|
||||||
|
|
||||||
|
**Pages**
|
||||||
|
- `EngagementDetailPage.tsx` : remplacer le placeholder (lignes 74-81) par `<SimulationList engagementId={eng.id} />`. Conserver le reste.
|
||||||
|
- `SimulationFormPage.tsx` (`/engagements/:eid/simulations/new` et `/engagements/:eid/simulations/:sid/edit`) :
|
||||||
|
- Layout en deux cards : "Red Team" et "SOC".
|
||||||
|
- Champs redteam : name, MitreTechniquePicker, description, commands (textarea, une commande par ligne, envoyé tel quel — pas de split), prerequisites, executed_at (datetime-local input), execution_result.
|
||||||
|
- Champs SOC : log_source, logs, soc_comment, incident_number.
|
||||||
|
- Boutons en footer : "Save", "Marquer en revue" (si AC-11.4), "Clôturer" (si AC-11.4), "Supprimer" (modal de confirmation, admin/redteam).
|
||||||
|
- Mode création (`new`) : seul `name` requis ; après création, redirige sur `/engagements/:eid/simulations/:sid/edit`.
|
||||||
|
|
||||||
|
**Composants** (`frontend/src/components/`)
|
||||||
|
- `SimulationList.tsx` : table tri par created_at desc, colonnes (Name, MITRE, Status badge, Executed at), bouton "Nouvelle" si admin/redteam, ligne cliquable → navigate edit page.
|
||||||
|
- `SimulationStatusBadge.tsx` : variant du StatusBadge existant si possible (factoriser), 4 couleurs (pending=fog, in_progress=primary-soft, review_required=bloom-coral, done=storm-deep). Si le StatusBadge existant n'est pas factorisable proprement, créer un nouveau composant — pas d'over-engineering.
|
||||||
|
- `MitreTechniquePicker.tsx` : input + dropdown, debounce 200ms (`useDebouncedValue` ou util inline), navigation clavier (↑/↓/Enter/Escape), affichage `T1059 — Command and Scripting Interpreter (initial-access)`. Loading state inline.
|
||||||
|
- `ConfirmDialog.tsx` : modal générique de confirmation (utilisée pour delete).
|
||||||
|
|
||||||
|
**Routing** (`App.tsx`)
|
||||||
|
- Ajouter `/engagements/:eid/simulations/new` (auth, admin|redteam)
|
||||||
|
- Ajouter `/engagements/:eid/simulations/:sid/edit` (auth, all roles, RBAC champs interne)
|
||||||
|
|
||||||
|
**Tests Vitest** (`frontend/tests/`)
|
||||||
|
- `SimulationList.test.tsx` : loading/error/empty + bouton "Nouvelle" gated par role.
|
||||||
|
- `MitreTechniquePicker.test.tsx` : autocomplete debounce, sélection met à jour, navigation clavier.
|
||||||
|
- `SimulationFormPage.test.tsx` : rôle redteam → tous champs éditables ; rôle soc → champs RT disabled, soc-side enabled si status review_required, bandeau si pending.
|
||||||
|
- `SimulationStatusBadge.test.tsx` : 4 variants.
|
||||||
|
|
||||||
### Règles
|
### Règles
|
||||||
- Lit le summary du backend-builder EN PREMIER.
|
- Lit le summary du backend EN PREMIER (contrat API).
|
||||||
- Pas d'invention d'endpoints. Mismatch → escalade au team-lead.
|
- Pas d'invention d'endpoints. Mismatch → escalade au team-lead.
|
||||||
- Respect strict de `DESIGN.md` pour palette/typo/composants.
|
- Réutiliser `LoadingState`, `ErrorState`, `EmptyState`, `Toast`, `FormField`, `StatusBadge` existants. NE PAS dupliquer.
|
||||||
- Pas de CDN remote au runtime — bundle local (police Inter incluse).
|
- Respect DESIGN.md (utiliser tokens Tailwind existants — pas de couleurs hardcodées).
|
||||||
- **Interdiction absolue de toucher `e2e/`** (responsabilité test-verifier).
|
- Pas de CDN remote.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Brief — Docker / Makefile (réalisé par le backend-builder)
|
## 4. Definition of Done — Sprint 2
|
||||||
|
|
||||||
### `docker/Dockerfile` (multistage)
|
- [ ] Tous les critères AC-7 → AC-12 passent.
|
||||||
```dockerfile
|
- [ ] `pytest` (existing 63 + nouveaux ~25) tous verts. `ruff`, `mypy` clean.
|
||||||
# Stage 1: build front
|
- [ ] `npm run typecheck`, `lint`, `test` clean frontend.
|
||||||
FROM node:20-alpine AS frontend-build
|
- [ ] Playwright suite (existing 36 + nouveaux ~15) verte.
|
||||||
WORKDIR /app/frontend
|
- [ ] `make build` + `make start` + `make update-mitre` + workflow simulation complet manuel OK.
|
||||||
COPY frontend/package*.json ./
|
- [ ] Code-reviewer (Opus) sans BLOCKER ouvert.
|
||||||
RUN npm ci
|
- [ ] `SPEC.md` (section Simulation enrichie si besoin), `README.md` (mention `make update-mitre` + workflow), `CHANGELOG.md` à jour.
|
||||||
COPY frontend/ ./
|
- [ ] PR ouverte sur `sprint/2-simulations`, récap synthétique team-lead, validation utilisateur.
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage 2: python runtime
|
|
||||||
FROM python:3.12-slim
|
|
||||||
WORKDIR /app
|
|
||||||
COPY backend/requirements.txt ./backend/
|
|
||||||
RUN pip install --no-cache-dir -r backend/requirements.txt
|
|
||||||
COPY backend/ ./backend/
|
|
||||||
COPY --from=frontend-build /app/frontend/dist ./backend/app/static
|
|
||||||
|
|
||||||
ENV FLASK_APP=backend.app:create_app
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
ENV PYTHONPATH=/app
|
|
||||||
# Variables surchargeables au `docker run` :
|
|
||||||
ENV MIMIC_PORT=5000
|
|
||||||
ENV MIMIC_DB_PATH=/data/mimic.sqlite
|
|
||||||
|
|
||||||
VOLUME ["/data"]
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Entrypoint : applique les migrations Alembic puis lance Flask
|
|
||||||
COPY docker/entrypoint.sh /entrypoint.sh
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
```
|
|
||||||
|
|
||||||
**`docker/entrypoint.sh`** :
|
|
||||||
```bash
|
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
flask db upgrade
|
|
||||||
exec flask run --host=0.0.0.0 --port="${MIMIC_PORT:-5000}"
|
|
||||||
```
|
|
||||||
|
|
||||||
Flask sert `backend/app/static` en racine `/` ET expose les blueprints sous `/api/*`. La DB SQLite vit dans `/data/mimic.sqlite` (volume nommé `mimic-data`) — survit à `make restart` (AC-6.4).
|
|
||||||
|
|
||||||
### `Makefile`
|
|
||||||
Targets requis et leur sémantique :
|
|
||||||
|
|
||||||
| Target | Action |
|
|
||||||
|---|---|
|
|
||||||
| `build` | `docker build -f docker/Dockerfile -t mimic:latest .` |
|
|
||||||
| `start` | `docker run -d --name mimic -p $(PORT):5000 -v mimic-data:/data --env-file .env mimic:latest` (`PORT ?= 5000`) |
|
|
||||||
| `stop` | `docker stop mimic && docker rm mimic` |
|
|
||||||
| `restart` | `$(MAKE) stop && $(MAKE) start` |
|
|
||||||
| `update` | `git pull && $(MAKE) build && $(MAKE) restart` |
|
|
||||||
| `logs` | `docker logs -f mimic` |
|
|
||||||
| `create-admin` | `docker exec mimic flask create-admin $(USER) $(PASS)` (requiert `USER=` et `PASS=`) |
|
|
||||||
| `update-mitre` | placeholder no-op Sprint 1 (`@echo "MITRE update: Sprint 2+"`) |
|
|
||||||
| `test-backend` | `docker exec mimic pytest -q backend/tests/` (ou run local en venv) |
|
|
||||||
| `test-frontend` | `cd frontend && npm run test -- --run` |
|
|
||||||
| `test-e2e` | `cd e2e && npx playwright test` (container doit être up) |
|
|
||||||
| `clean` | `docker rm -f mimic 2>/dev/null; docker volume rm mimic-data 2>/dev/null; rm -rf backend/__pycache__ frontend/node_modules frontend/dist` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Definition of Done — Sprint 1
|
## 5. Décisions arrêtées (utilisateur 2026-05-26)
|
||||||
|
|
||||||
- [ ] Tous les critères d'acceptation des US 1→6 passent.
|
1. **Source MITRE** : `https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json` (default team-lead).
|
||||||
- [ ] `pytest`, `ruff`, `mypy` clean côté backend.
|
2. **MITRE bundle dans le repo** : COMMITTÉ (`backend/data/mitre/enterprise-attack.json` versionné, `make build` autosuffisant).
|
||||||
- [ ] `npm run typecheck`, `lint`, `test` clean côté frontend.
|
3. **Commands storage** : colonne `text` multiligne, une commande par ligne, transport tel quel.
|
||||||
- [ ] Playwright suite verte côté `e2e/`.
|
4. **Workflow auto-transition** pending→in_progress : déclenchée par toute PATCH admin/redteam touchant ≥1 champ redteam à valeur non vide (default team-lead).
|
||||||
- [ ] Image docker se build (`make build`), démarre (`make start`), répond sur `:5000`.
|
5. **Page simulation** : UNE page d'édition role-aware (`/engagements/:eid/simulations/:sid/edit`), pas de page détail séparée.
|
||||||
- [ ] Code review (Opus) sans BLOCKER ouvert.
|
6. **Suppression cascade** : delete engagement → delete simulations (default team-lead).
|
||||||
- [ ] `SPEC.md`, `README.md`, `CHANGELOG.md` à jour.
|
7. **SOC restriction status** : soc ne peut PATCH que si status ∈ {review_required, done}.
|
||||||
- [ ] PR ouverte sur la branche sprint, validée par l'utilisateur après récap synthétique.
|
8. **Sous-techniques MITRE** : incluses dans l'autocomplete (T1059.001 visible) (default team-lead).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Risques & questions à clarifier avec l'utilisateur AVANT de coder
|
## 6. Plan d'exécution (séquence)
|
||||||
|
|
||||||
1. **DESIGN.md** : faut-il que je relise DESIGN.md (27 Ko, déjà présent dans le repo) avant que le frontend-builder l'utilise ? OUI
|
1. ✅ User a validé les 8 décisions §5 (2026-05-26).
|
||||||
2. **Persistance SQLite** : DB stockée dans `/data/mimic.sqlite` montée comme volume nommé `mimic-data`. OK ? OK
|
2. ✅ **Spec-reviewer** : APPROVED WITH NOTES (4 items mineurs corrigés avant dispatch).
|
||||||
3. **Port** : `5000` (Flask défaut). Conflit possible avec macOS AirPlay si jamais — on s'en fiche en Linux. Surcharge du port possible via dockerfile et container.
|
3. ✅ **Backend-builder** : commit `006c4c2` (67 nouveaux tests, 130 passing).
|
||||||
4. **CSRF** : on est en API JWT pure, pas de cookie session, donc pas de CSRF protection nécessaire côté serveur. OK ? OK
|
4. ✅ **Frontend-builder** : commit `765bb5a` (41 nouveaux tests, 61 passing).
|
||||||
5. **Refresh token** : exclu V1. JWT court (60 min) ; user devra se reconnecter. OK ? OK
|
5. ✅ **Code-reviewer** : 2 MAJOR + 4 MINOR + 3 NITs → 2 commits de fix (`83bf60f` backend, `c9032a9`+`cf0e8a8` frontend).
|
||||||
6. **Logout** : V1 = client-side uniquement (purge du token). Pas de blacklist. OK ? OK
|
6. ✅ **Test-verifier** : 32/32 sprint 2 verts, commits `da905cc` + `54e90f7` (AC-4.9 refresh).
|
||||||
7. **CI** : pas mentionné dans la spec. Skip pour Sprint 1 ? (à confirmer) On verra plus tard.
|
7. 🟡 **Team-lead** : récap + PR en cours.
|
||||||
8. **README.md** : actuellement absent. Le team-lead le crée en fin de Sprint 1 avec instructions `make build / start / create-admin`. OK.
|
|
||||||
|
|
||||||
---
|
Branche unique : `sprint/2-simulations`.
|
||||||
|
|
||||||
## 7. Plan d'exécution (séquence)
|
|
||||||
|
|
||||||
**Branche unique pour ce sprint** : `sprint/1-auth-engagements` (pas de sous-branches builders, le sprint est séquentiel).
|
|
||||||
|
|
||||||
1. ✅ **Spec-reviewer** (`.claude/agents/spec-reviewer.md`, override projet du built-in) valide ce plan vs SPEC.md.
|
|
||||||
2. 🔵 **Backend-builder** implémente `backend/` + `docker/Dockerfile` + `docker/entrypoint.sh` + `Makefile`, livre son summary (incluant le contrat API).
|
|
||||||
3. 🔵 **Frontend-builder** lit le summary backend puis implémente le front (`frontend/` UNIQUEMENT, jamais `e2e/`).
|
|
||||||
4. 🔵 **Code-reviewer** relit le diff du sprint (LSP-first, focus bugs/qualité/scope).
|
|
||||||
5. 🔵 **Test-verifier** **scaffolds** `e2e/` (Playwright config, package.json, fixtures auth) **puis écrit** les acceptance tests Playwright et exécute la suite contre le container démarré via `make start`.
|
|
||||||
6. 🟢 **Team-lead** (moi) prépare la PR + récap synthétique → user valide.
|
|
||||||
|
|
||||||
**Responsabilité `e2e/`** : exclusivement le test-verifier. Backend-builder et frontend-builder n'y touchent jamais.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user