diff --git a/CHANGELOG.md b/CHANGELOG.md index 7139d14..90f5d30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ## [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//simulations`, `GET/PATCH/DELETE /api/simulations/`, `POST /api/simulations//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 `` instead. + +--- + +## [Sprint 1] — Auth + CRUD Engagement (merged 2026-05-26) + +### Added **Backend** (Flask + SQLAlchemy + SQLite, 63 pytest passing) - `User` model with `admin / redteam / soc` enum, argon2 password hashing. diff --git a/README.md b/README.md index c1c1d36..62ea2c7 100644 --- a/README.md +++ b/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. -> 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) │ │ ├── /api/* ── blueprints (auth, users, │ -│ │ engagements) │ +│ │ engagements, simulations,│ +│ │ mitre) │ │ └── / ── SPA fallback → React build │ │ │ │ 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. -- **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`. - **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. @@ -102,7 +105,7 @@ mimic/ | `make update` | `git pull && make build && make restart` | | `make logs` | `docker logs -f mimic` | | `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-frontend` | `npm run test -- --run` in `frontend/` | | `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: ```bash -cd backend && pytest -q # 63 tests -cd frontend && npm run test -- --run # 20 tests -cd e2e && npx playwright test # 36 tests (needs container up) +cd backend && pytest -q # 131 tests +cd frontend && npm run test -- --run # 63 tests +cd e2e && npx playwright test # 68 tests (needs container up) ``` --- diff --git a/tasks/lessons.md b/tasks/lessons.md index 77c7fcf..292a1f3 100644 --- a/tasks/lessons.md +++ b/tasks/lessons.md @@ -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 `` — no params are extracted unless the component is mounted under a matching ``. +**Lesson** : page tests that depend on params must wrap the component in `} />` 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//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 ``, 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. diff --git a/tasks/todo.md b/tasks/todo.md index e6905f0..851dda3 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -1,315 +1,252 @@ -# Sprint 1 — Auth + CRUD Engagement +# Sprint 2 — Simulations + MITRE ATT&CK -**Branche** : `sprint/1-auth-engagements` -**Statut** : 🟢 PLAN APPROUVÉ (spec-reviewer 2026-05-26) — prêt pour dispatch backend-builder -**Base** : `main` -**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. +**Branche** : `sprint/2-simulations` +**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` (sprint 1 mergé en `7fc79cc`) +**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 -### US-1 — En tant qu'admin, je bootstrap le premier compte admin -**Pourquoi** : sinon impossible d'utiliser l'application au premier démarrage. +### US-7 — En tant que redteam, je crée une simulation dans un engagement +**Pourquoi** : c'est la feature centrale du sprint 2. **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-1.2 : la commande échoue proprement (exit ≠ 0, message clair) si le username existe déjà. -- [ ] AC-1.3 : la commande échoue si le password fait moins de 8 caractères. -- [ ] AC-1.4 : la commande s'exécute via `docker exec mimic flask create-admin …` (le Makefile encapsule cet appel). +- [ ] AC-7.1 : `POST /api/engagements//simulations {name}` (admin|redteam) → 201 + simulation `{id, engagement_id, name, status: "pending", ...}`. `name` requis, non vide. +- [ ] AC-7.2 : autres rôles (soc) → 403. +- [ ] AC-7.3 : engagement inexistant → 404. Engagement existant mais aucune simulation → liste vide. +- [ ] AC-7.4 : `GET /api/engagements//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 -**Pourquoi** : porte d'entrée de l'application. +### US-8 — En tant que redteam, je renseigne les détails techniques d'une simulation +**Pourquoi** : c'est la trace de ce que la redteam a exécuté. **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-2.2 : 401 si credentials invalides, avec un message générique ("Invalid credentials") — pas de fuite username vs password. -- [ ] 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-2.4 : page `/login` affiche le formulaire ; soumission OK → redirection `/engagements`. Soumission KO → message d'erreur visible. -- [ ] AC-2.5 : navigation vers `/engagements` sans token → redirection `/login`. -- [ ] 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.1 : `PATCH /api/simulations/` (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-8.2 : règle d'auto-transition pending → in_progress. Trigger PRÉCIS : `PATCH /api/simulations/` 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-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 `