CSV multiline injection + Markdown stored-XSS regressions caught by
security-guidance@claude-code-plugins on the 7-column refactor.
Backend fix in 3a9d9d3 (257 pytest, ruff/mypy clean). PR #9 body
counter bumped 255 → 257.
36 KiB
Changelog
All notable changes to Mimic are tracked here.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[Unreleased]
Added — Sprint 6 (Engagement export)
Backend (253 pytest passing — 226 sprint-1-to-4 + 28 sprint 5 + 5 sprint 5 post-code-review + 23 sprint 6 + 1 CSV-injection defense-in-depth test)
backend/app/services/export.py(new, 302 lines) — 3 pure render functions (render_engagement_markdown,render_engagement_csv,render_engagement_pdf) + filename slugifier (_export_filename) + HTML helper for the PDF pipeline + CSV formula-injection defense helper (_csv_safe).- New endpoint
GET /api/engagements/<int:eid>/export?format=md|csv|pdfextended on the existingengagements_bp. Decorator@role_required("admin", "redteam")(SOC → 403). 400 on missing/unknown format, 404 on unknown engagement. Returns the rendered file body withContent-Typematching the format andContent-Disposition: attachment; filename="engagement-<id>-<slug>-YYYYMMDD.<ext>". - Filename slugifier uses
unicodedata.normalize('NFKD', ...).encode('ascii', 'ignore')to strip accents (Opération→operation) and falls back to"unnamed"when the slug is empty after stripping. - Markdown rendering uses fenced code blocks with
~~~bash(tildes, not backticks) so backticks in commands don't break the fence. SOC fields are always rendered, even when blank (consistency for handoff)._creator()helper renders the username string only (not the{id, username}dict). - CSV rendering uses stdlib
csv.writer(handles multiline / quotes / commas natively)._csv_safe()prefixes a single apostrophe to any string starting with=,+,-,@,\t, or\r— defuses Excel / LibreOffice / Google Sheets formula injection on the SOC analyst's machine when they open the exported CSV. Applied to all user-controlled string fields; ISO dates and the enum status value are exempted. - PDF rendering via WeasyPrint (Python HTML→PDF). The PDF is generated from the same engagement DATA as the Markdown (not from the Markdown string) via
_render_engagement_html()andweasyprint.HTML(string=html).write_pdf(). CSS inline (≤ 30 lines). All user-controlled fields HTML-escaped via stdlibhtml.escape(). docker/Dockerfilepython stage now installs minimal WeasyPrint deps:libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b libfontconfig1 shared-mime-info.libgdk-pixbuf-2.0-0deliberately excluded (text-only PDF).weasyprint>=60.0added tobackend/requirements.txt.- No DB schema change. No migration.
Frontend (136 vitest passing — 121 sprint-1-to-5 + 12 sprint 6 + 3 sprint 6 coverage-gap fix)
frontend/src/components/ExportEngagementButton.tsx(new) — split-button dropdown[Export ▼]withDownload+ChevronDownlucide icons. Both halves open the dropdown (no default left-click action — different semantic from sprint 5'sNewSimulationDropdownwhere left navigates blank), because there is no obvious default format among MD/CSV/PDF. Loading state per-item, toast on error. Click-outside + Escape close (reuses theuseEffect+pointerdown+keydownpattern fromNewSimulationDropdown).data-testid="export-dropdown"for e2e selection. Visual: sharesbtn-outlineclass with the neighbourEditbutton.frontend/src/api/exports.ts(new) —downloadEngagementExport(engagementId, format)withresponseType: 'blob'. ReadsContent-Disposition: attachment; filename="...", falls back toengagement-<id>.<ext>when the header is absent or malformed. Throws anErroron non-2xx (caller catches and toasts). HelperparseContentDispositionFilename().frontend/src/pages/EngagementDetailPage.tsx(edited) — integrates<ExportEngagementButton engagementId={engagement.id} />in the header next to theEditCTA. Gated bycanEditEngagementsfromuseAuth(admin + redteam).- New test file
frontend/tests/exports.test.tscovers the API client directly viaaxios-mock-adapter(the component test file mocksdownloadEngagementExportentirely, so the fallback logic insideexports.tswasn't reachable from there — new file lets the real function run for 3 dedicated tests).
Acceptance tests (Playwright, 223 passed — baseline sprint 5 = 201, +22 sprint 6)
- 3 new spec files (one per US):
us29-export-formats.spec.ts(8 tests),us30-export-rbac.spec.ts(3 tests),us31-export-robustness.spec.ts(5 tests). - No regression on sprints 1–5: full pre-sprint-6 suite still green.
Security
- CSV formula injection (MEDIUM) flagged by
security-guidance@claude-code-pluginsautomated review during the sprint, fixed mid-sprint (commit57dbd14). 3 dedicated unit tests cover the apostrophe-prefix on=,@triggers and the no-op on safe strings. - Defense-in-depth: a property test (
test_export_filename_never_contains_quote_or_crlf) asserts the slugifier output never contains",\r, or\n— guards against Content-Disposition header injection if someone later weakens the slug regex.
Changed
- 2026-06-07 — SPEC.md § Export d'engagement added (between § Templates de simulations and § Stacks techniques). Committed as the first sprint commit (
7aaa5cc), applying the fix-candidate from sprint 5's recurrent "SPEC.md uncommitted at sprint close" lesson. Four-sprint recurrence finally broken. - 2026-06-08 — Team
mimic(persistent.claude/teams/mimic/config.json) instantiated with the full 7-agent project roster (backend-builder, frontend-builder, spec-reviewer, code-reviewer, design-reviewer, test-verifier, devil-advocate). Agents are spawned with an idle prompt at sprint start and woken via SendMessage per phase — flip vs the old "spawn-with-task-only" policy that hit the "team roster is flat" gotcha when respawning. Persistent across sprints from sprint 7+. - 2026-06-08 (post-review, pre-merge) — Export schema switched to a fixed 7-column handoff layout uniform across MD/CSV/PDF. Columns (FR headers):
Scénario,Test,Source de log,Commentaires SOC,Exécution(multi-line concat without labels —executed_at→commands→execution_result),Logs remontés au SIEM,Cyber incident. Removed from the export (intentional, handoff-focused): simulation status, MITRE techniques/tactics, prerequisites, id, created_at, updated_at. Markdown switched from narrative-per-simulation to a GFM table. PDF switched from sectioned HTML to a single<table>. SPECfdab324, backend refactor7335b9f, e2e adaptationaeb4bdb. Final counters: backend 257 pytest, frontend 136 vitest, e2e 223 Playwright. - 2026-06-08 (post-refactor, pre-merge) — Two MEDIUM security regressions fixed in the 7-column refactor (
3a9d9d3), flagged bysecurity-guidance@claude-code-plugins:- CSV formula injection inside the multi-line
Exécutioncell:_csv_safeonly checkscell[0]. Withexecuted_atnon-null, the cell starts with a safe date digit, but inner lines (commands, execution_result) starting with=/+/-/@evaded defense. Fix:_format_execution_csv()applies_csv_safeper user-controlled component BEFORE the multi-line concat. Outer_csv_safeon the assembled cell retained as belt-and-braces. - Stored XSS in Markdown table cells: the new GFM table allows inline HTML (we use it for
<br/>). Asim.commands = "<script>alert(1)</script>"would be rendered raw by MD viewers that interpret inline HTML (Notion, Obsidian, GitHub preview). Fix:_cell()now callshtml.escape()on each value BEFORE the pipe-escape and\n→<br/>substitution — mirrors the_render_engagement_htmlPDF defense. The<br/>we insert ourselves stays unescaped (it's not user-controlled). 2 dedicated regression tests added.
- CSV formula injection inside the multi-line
[Sprint 5] — Simulation templates + instantiation + nav + dropdown (merged 2026-05-28)
Added — Sprint 5 (Simulation templates)
Backend (226 pytest passing — 193 sprint-1-to-4 + 28 sprint 5 + 5 post-code-review)
SimulationTemplatemodel (tablesimulation_templates) — UNIQUE constraint onname, JSONtechniques+tactic_ids(default[], NOT NULL viaserver_default), Text fieldsdescription/commands/prerequisites, FKcreated_by_idtousers,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 enrichedtechniques: [{id, name, tactics}]andtactics: [{id, name}].POST /api/templates— create.namerequired (400 if empty), unique (409 viaIntegrityErrorcatch, no pre-check race).technique_ids/tactic_idsvalidated upfront — type checkisinstance(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 onnameconflict; no-op rename (name == current) returns 200.DELETE /api/templates/<tid>— 204. No cascade to instantiated simulations (decoupling guarantee).
POST /api/engagements/<eid>/simulationsextended with optionaltemplate_id. When provided:- Template loaded (404 on miss).
- Fields copied directly onto the new
SimulationORM object (techniques,tactic_ids,description,commands,prerequisites, andnameif missing from body). - Explicit non-call to
apply_patch()/_resolve_*helpers — avoids re-hitting the MITRE bundle AND avoids triggering the auto-transitionpending → in_progress. Status stayspending, engagement staysplanned(no_maybe_activate_engagementcall). Decorrelation: notemplate_idFK onSimulation, deep copy of JSON arrays.
- New helpers in
mitre.pyreused / re-exposed; newserialize_template()inserializers.pymirrorsserialize_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),+ NewCTA with Plus icon. - New page
TemplateFormPage(/admin/templates/newand/admin/templates/:id/edit) — single-column FormField stack (sidesteps the multi-column grid trap that broke AC-17.3 on UsersAdminPage). IncludesMitreTechniquePicker+MitreMatrixModalinline (NOTMitreTechniquesField— that one auto-saves; template form needs batched save). Delete viaConfirmDialog. - New component
TemplatePickerModal— modal listing all templates (Name / MITRE count / Created by). Empty state whenuseTemplates()returns[]: "No templates available — Create one from the Templates page." - New nav link "Templates" in
Layout.tsxtopbar — 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-stateSimulationListnow also exposes the same dropdown (so users can instantiate from a template on a fresh engagement without creating a blank first).dark:shadow-floating-darkconsistently applied to the new dropdown andTemplatePickerModal— matches the sprint 4 shadow token model.dark:hover:bg-fogon dropdown items for contrast.- New types:
SimulationTemplate,SimulationTemplateCreateInput,SimulationTemplatePatchInput.SimulationCreateInputextended withtemplate_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 commit90fc5baused/simulation-templatespaths everywhere; caught immediately, fixed in2b70011.)
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.tsandus7-simulation-create.spec.tsnow usegetByTestId('new-simulation-btn')instead ofgetByRole('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>/simulationsAPI contract:nameis now optional whentemplate_idis provided (falls back totemplate.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)
Simulation.tactic_idsJSON column (default[], NOT NULL viaserver_default). Sprint 3'stechniquesarray is joined by a paralleltacticsfield in the serialized response.- Alembic migration
0004_simulation_tactic_ids.py— simple ADD COLUMN (SQLite native); downgrade viabatch_alter_table. PATCH /api/simulations/<sid>accepts{tactic_ids: ["TA0007", ...]}(TA-format, validated against the hardcoded_TACTIC_IDSmap — no MITRE bundle dependency for tactics since TA-ids are a stable MITRE standard). Dedup viadict.fromkeys. SOC sendingtactic_ids→ 403. Auto-transitionpending → in_progressextended to non-emptytactic_ids.- Done is now terminal:
PATCH /api/simulations/<sid>on adonesimulation → 409{error: "simulation is done — reopen first"}(applies to all 3 roles, prioritised over RBAC field-level). - Reopen transition:
POST /api/simulations/<sid>/transition {to: "review_required"}fromdone→ 200, open to admin + redteam + soc. Implemented as a special case before the_ALLOWED_TRANSITIONSdict lookup; other transitions fromdone(→ pending/→ in_progress/→ done) remain forbidden (409 via dict miss). - Engagement auto-status: when any simulation transitions to
in_progress(auto or manual), ifengagement.status == planned→ engagement passes toactivein the same DB transaction. No auto-rollback. The_maybe_activate_engagementhelper modifies anddb.session.add()s only — the caller commits (no double-commit). GET /api/mitre/matrixtactic_idfield now returned in TA-format ("TA0007") instead of the internal slug ("discovery"). Aligns with the PATCH endpoint contract — frontend can round-trip the sametactic_idbetween matrix display and PATCH body. Spec-drift caught by the e2e test-verifier (AC-21.6 defect).- Internal helpers :
_TACTIC_IDS(TA-id → short-name, 12 entries, non-sequential),_SLUG_TO_TA_ID(reverse),lookup_tactic(),get_tactic_name(). - Migration tests now derive paths from
__file__instead of hardcoded worktree absolute paths (recurring sprint-3 issue resolved).
Frontend (92 vitest passing — typecheck + lint clean)
- Dark mode: full Tailwind
darkMode: 'class'plumbing, themed surface tokens via CSS variables under:root/.darkinindex.css. Three-state cycle (light/dark/system) toggle in the topbar with lucide-react icons (Sun / Moon / Monitor). Persisted under localStorage keymimic-theme(defaultsystem, followsprefers-color-scheme). DedicateduseTheme()hook orchestrates the cycle + media-query listener. - Slab token split: a new
slab/slab-text/slab-mutedtoken family stays fixed#111827/#f9fafb/#6b7280regardless of theme. Used for permanently-dark surfaces (utility strip, footer, modal backdrop) that must NOT invert in dark mode. The themedinktoken is now strictly for text..btn-inkuses@apply bg-slab(single source of truth). - Modal backdrop: new
.modal-backdropCSS class (fixedrgba(0,0,0,0.6)) replacesbg-ink/60(which inverted in dark mode). Applies toMitreMatrixModalandConfirmDialog. - Badge contrast in dark mode:
SimulationStatusBadgeandStatusBadgeusetext-white(fixed) on colored backgrounds instead oftext-canvas/text-ink-on(which inverted).Toasterror usesbg-slab text-slab-text. - Dark mode shadows: new
soft-lift-darkandfloating-darktoken variants, applied to cards and modals viadark:shadow-*so the lift remains visible on dark canvas. Hairlines bumped (#4b5563) for better separator visibility. - MITRE matrix modal overhaul: 12-column CSS grid (
repeat(12, minmax(0, 1fr))), no horizontal scroll at 1280×720. Compact technique cells (text-[12px], hairline borders). Sticky tactic headers (uppercase, count badge). Sub-techniques expand/collapse preserved from sprint 3. - Tactic selection in matrix: clicking a tactic header toggles its selection in addition to techniques + sub-techniques. Tactic chips render with
bg-primary text-canvas(filled), distinct from technique chips (bg-primary-soft text-primary-deep). Apply emits one combined PATCH{technique_ids, tactic_ids}— no two sequential calls. - MITRE input redesign: replaces the prior
Add technique+Quick searchbutton pair with an inline autocomplete input + matrix icon button to the right. Chips display the reference only (T1059.001orTA0007); full technique name surfaces ontitle=hover. Empty state minimal. donesimulation UI: form fields are fully disabled,MitreTechniquesFieldis read-only (chips without ×, input + matrix icon hidden), action bar shows ONLY aReopenbutton (visible to all 3 roles per RBAC). Save / Mark for review / Close / Delete are hidden in the done state. A "this simulation is done and read-only" banner replaces them.- UsersAdminPage
Create accountform alignment (3rd attempt — finally pixel-perfect): refactored fromFormField+items-endto an explicit 3-row grid (labels / inputs+button / hints) usinggrid-rows-[auto_auto_auto]. Labels share row 1, inputs + button share row 2, hint sits alone in row 3 — the browser cannot misalign cells of different heights. - EngagementsListPage dedup: single
+ NewCTA (header + empty-state share the same label). - Engagement query invalidation:
useUpdateSimulationanduseTransitionSimulationnow invalidate both the simulation queries AND["engagement", engagement_id]+["engagements"]so the engagement status badge updates without a full reload after the auto-transition.
Process hygiene (US-24)
- New agent definition
.claude/agents/design-reviewer.md— read-only, runs AFTERfrontend-builderand BEFOREcode-reviewer. Audits alignment, DESIGN.md token usage, light/dark consistency, typography, whitespace rhythm, responsive sanity at 1280×720, button convention, V1 a11y, and inter-screen coherence. - Updated
.claude/agents/frontend-builder.mdDefinition of Done — screenshots are now MANDATORY (one per feature/state introduced or modified, light + dark when theming is in scope). A "Dev server not started" line is a hard block.
Infra hygiene (US-25)
scripts/open-pr.sh— wrapsPOST /api/v1/repos/{owner}/{repo}/pullson the Gitea REST API. Reads credentials from~/.git-credentials(same source asgit push— no token in env). Detects host/owner/repo fromgit remote get-url origin. Validates args, prints PR URL.- New Makefile target
open-pr TITLE="..." BODY=path/to/body.md [BASE=main]wrapping the script. Team-lead PR creation is now automated. - README
Make targetstable documents the new target.
Acceptance tests (Playwright, 158 passed)
- 7 new spec files (one per testable US):
us17-ui-polish,us18-done-readonly-reopen,us19-engagement-auto-status,us20-matrix-fits-modal,us21-tactic-selection,us22-mitre-input-redesign,us23-dark-mode. - Coverage gaps from code-reviewer filled:
+Nsuffix when techniques + tactics are mixed in theSimulationListMITRE column ; Tab focus-trap cycle inMitreMatrixModal; dark-modelocalStoragepersistence across reload. - AC-21.6 defect caught by the e2e (matrix returned slug
tactic_id, PATCH expected TA-format) was bounced to backend-builder and resolved within the sprint.
Changed
- 2026-05-27 — SPEC.md § Fonctionnement clarified:
doneis terminal, only Reopen (open to all 3 roles) returns toreview_required. Engagement auto-flipsplanned → activeon first simulationin_progress, never the reverse. - 2026-05-27 — SPEC.md § Référentiel MITRE: added the sprint 4
tactic_ids(separated fromtechnique_ids). - 2026-05-27 — SPEC.md § UI/UX (new section): theming (light/dark/system, default =
system), button convention (icon + ≤8-char label), modal focus trap V1. - 2026-05-27 — SPEC.md § Workflows:
design-revieweragent inserted betweenfrontend-builderandcode-reviewer. PR creation now viamake open-pr. - 2026-05-27 — Carry-over commit: sprint 3
§ Simulationmulti-techniques edit had been left uncommitted at sprint 3 close; applied at sprint 4 start so SPEC.md and the shipped code finally agree (lesson logged in tasks/lessons.md).
[Sprint 3] — Multi-technique simulations + MITRE matrix modal (merged 2026-05-27)
Added — Sprint 3 (Multi-technique simulations + MITRE matrix modal)
Backend (164 pytest passing)
Simulation.techniquesJSON column replaces the scalarmitre_technique_id/mitre_technique_namepair. Stored as[{"id", "name"}]; tactics are derived at serialize time from the MITRE service (snapshot pattern survives bundle updates).- Alembic migration
0003_simulation_techniques_array.py— reversible upgrade (backfill from scalars → drop scalars → enforceNOT NULLviabatch_alter_table) and symmetric downgrade. PATCH /api/simulations/<sid>now accepts{technique_ids: ["T1059", "T1059.001", ...]}(flat list of T-IDs, parents and subs at the same level). Server validates each ID against the bundle (400 on unknown), deduplicates while preserving order, resolves names, and rejects SOC payloads (403). Returns 503 if the bundle isn't loaded.GET /api/mitre/matrix— new endpoint returning the full Enterprise tree[{tactic_id, tactic_name, techniques: [{id, name, subtechniques: [{id, name}]}]}]. Tactics in canonical order (Initial Access → Impact). Techniques sorted alphabetically per tactic; sub-techniques nested under their parent via dot-ID detection.mitre_svcextended withget_tactics(id),lookup_name(id),get_matrix(), and aTACTIC_NAMESconstant fixing the cosmetic"Command And Control"→"Command and Control"(MITRE canonical capitalisation).REDTEAM_FIELDS | {"technique_ids"}SOC gate insimulation_workflow.apply_patchpreserves the sprint 2 field-level RBAC pattern.- Auto-transition
pending → in_progressextended: triggers whentechnique_idsis non-empty (consistent with the "non-empty value" rule from sprint 2). Empty list does not trigger.
Frontend (86 vitest passing)
MitreTechniquesFieldorchestrates multi-technique selection with auto-save — every add (Quick Search / matrix Apply) and every remove (× on tag chip) triggers a PATCH viauseUpdateSimulation. Toast feedback on success/error; UI disabled during the in-flight PATCH; silent dedup if the user re-adds an already-present technique.MitreTechniqueTag— chip component (bg-primary-soft text-primary-deep rounded-full) with an × remove button.MitreMatrixModal— full-width modal, one column per tactic (220px fixed), horizontal scroll. Each technique top-level is clickable (toggle); a chevron expands/collapses sub-techniques rendered in cascade. Search filter (case-insensitive on id + name) auto-expands the parent of a matched sub-technique. Tactic header shows a "N selected" counter (parents + subs). Footer: Cancel + "Apply N technique(s)" (or "Clear all" when N=0 and there's an existing selection). Focus trap V1: search input auto-focus on open, Tab cycles within the modal, Escape and backdrop click both = Cancel.MitreTechniquePicker(sprint 2) clean-rewritten to a one-shotonSelect({id, name})signature; no incoming value props. The picker resets after each selection — the parent (MitreTechniquesField) handles append + dedup.SimulationListMITRE column displaysT1059 +2when 3 techniques are selected (first id + remainder counter) or—when empty.SimulationFormPage—MitreTechniquesFieldreplaces the old standaloneMitreTechniquePicker. The technique state moves out of the RT form (independent auto-save cycle); the Save Red Team button still batches the other RT fields.
Acceptance tests (Playwright)
- 4 new spec files:
us13-multi-techniques.spec.ts,us14-techniques-tags.spec.ts,us15-mitre-matrix-modal.spec.ts,us16-regression-sprint2.spec.ts— all ACs (AC-13.1 → AC-16.3) pass. - Sprint 2 specs
us8-simulation-redteam-fill.spec.tsandus10-mitre-autocomplete.spec.tsadapted to the newtechniques: []array (no more scalar field assertions).
Changed
- 2026-05-27 — SPEC.md § Simulation: "Type d'attaque MITRE correspondant" (singular) → "Types d'attaque MITRE correspondants (multi-techniques) — sélectionnables par autocomplete OU via la matrice ATT&CK affichée en modale. Sub-techniques supportées."
- 2026-05-27 — Breaking API change:
mitre_technique_idandmitre_technique_nameremoved from theSimulationpayload (both directions). Replaced bytechniques: [{id, name, tactics}]in responses andtechnique_ids: string[]in PATCH requests. No backwards-compatibility shim (no external consumer at this stage).
[Sprint 2] — Simulations + MITRE ATT&CK (merged 2026-05-27)
Added — Sprint 2 (Simulations + MITRE ATT&CK)
Backend (Flask + SQLAlchemy, 131 pytest passing)
Simulationmodel 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, plusstatusenum (pending/in_progress/review_required/done), FK toEngagement(cascade delete) andUser(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_workflowservice: 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-transitionpending → in_progresswhen admin/redteam saves any non-empty redteam field.mitreservice: 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-mitreis now a real target — fetches the upstream STIX bundle and restarts the container if running. Bundle is committed atbackend/data/mitre/enterprise-attack.json(~46 MB) somake buildstays self-contained.- Upfront validation of
executed_at(no partial mutation on parse failure).
Frontend (React + TanStack Query, 63 vitest passing)
SimulationListcomponent rendered insideEngagementDetailPage(replaces the Sprint 1 placeholder). Columns: name, MITRE id, status badge, executed_at. Row click → SPA navigation viauseNavigate(no full reload).SimulationFormPage(/engagements/:eid/simulations/newand/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 reachesreview_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 bothmitre_technique_idandmitre_technique_name. AhasHydratedFromPropsref prevents the input from being wiped mid-stroke when the parent emitsonChange(null, null).SimulationStatusBadge: 4 variants mapped to DESIGN.md tokens (bg-fog,bg-primary-soft,bg-bloom-coral,bg-storm-deep). Sibling of the existingStatusBadgerather 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/68 passing)
- 6 new spec files (one per user story US-7 → US-12), 32 tests, all green.
us4-engagements.spec.tsAC-4.9 assertion refreshed: the Sprint 1 placeholder text was correctly replaced by the newSimulationList(the test now asserts the new heading + "New simulation" link).- Sprint 1 docker-hardcoded tests (
us1,us6) now resolve thanks to the podman auto-detect added to those specs in the same sprint — full suite is green on both docker and podman hosts. - E2e assertions translated to match the i18n cleanup (French → English) shipped in the post-QA fix.
Post-QA fixes (2026-05-26)
- All French labels in the frontend translated to English (convention: anglais partout). Affected:
SimulationList,SimulationFormPage,ConfirmDialogstrings. UsersAdminPage"Create account" form: grid alignment fixed — the password field'shint="≥ 8 characters"was pushing labels out of alignment withitems-end. Now usesitems-start+self-endbutton wrapper so labels sit at the same baseline and the Create button stays bottom-aligned.SimulationFormPage"Execution result" field: switched from single-lineTextInputto multilineTextArea(5 rows).SimulationFormPageactions reorganised: single sticky action bar at the bottom of the page replaces the previous split between RT-card footer, SOC-card footer, and workflow div. Layout: Save Red Team · Save SOC · | · Mark for review · Close · (right-aligned) Delete.
Changed
- 2026-05-26 —
make update-mitreupgraded from no-op placeholder to a realcurl+ optional container restart (Sprint 1 marker resolved). - 2026-05-26 —
EngagementDetailPageno longer renders the "Simulations à venir au Sprint 2" placeholder; it embeds<SimulationList>instead. - 2026-05-26 — Makefile now auto-detects the container engine (
CONTAINER_CMD ?= docker || podman) instead of hard-codingdocker. Override withmake <target> CONTAINER_CMD=podmanorexport CONTAINER_CMD=…. The matching e2e tests (us1,us6) were updated to mirror the same detection so they pass on podman-only machines without an explicitMIMIC_CONTAINER_CMDexport.
[Sprint 1] — Auth + CRUD Engagement (merged 2026-05-26)
Added
Backend (Flask + SQLAlchemy + SQLite, 63 pytest passing)
Usermodel withadmin / redteam / socenum, argon2 password hashing.Engagementmodel withplanned / active / closedstatus, FK to creator user.- JWT Bearer auth (
PyJWT, HS256, 60-min TTL),@login_requiredand@role_required(*roles)decorators. - 13 API endpoints:
/api/auth/{login,logout,me},/api/usersCRUD (admin-only with last-admin protection),/api/engagementsCRUD (RBAC per role),/api/health. - Alembic migration applied at container boot by
docker/entrypoint.sh. flask create-adminCLI with duplicate-username and short-password validation.- Engagement serializer returns
created_by={id, username}(not bare User object). - SPA fallback returns JSON 404 for unknown
/api/*paths (no HTML leakage).
Frontend (React + Vite + TailwindCSS + TanStack Query, 20 vitest passing)
- Inter font bundled locally via
@fontsource-variable/inter(no CDN at runtime). - Tailwind config maps the
DESIGN.mdtoken system (palette, typography, spacing, radii). - Pages:
LoginPage,EngagementsListPage,EngagementFormPage(new+edit),EngagementDetailPage(Sprint 2 placeholder),UsersAdminPage. - Components:
Layout,ProtectedRoute(auth + role gate),StatusBadge,FormField,LoadingState/ErrorState/EmptyState,Toast+ provider. - Axios client with Bearer interceptor; 401 → token purge + redirect
/login+ "Session expirée" toast (AC-2.6); 403 → "Accès refusé" toast (AC-3.7). - TanStack Query hooks:
useAuth,useEngagements,useUsers,useToast.
Deployment
- Single-container
docker/Dockerfile(multistage:node:20-alpine→python:3.12-slim). docker/entrypoint.shrunningflask db upgrade && flask run.Makefilewithbuild,start,stop,restart,update,logs,create-admin,update-mitre(no-op placeholder for Sprint 2),test-backend,test-frontend,test-e2e,clean..env.exampledocumentingMIMIC_JWT_SECRET,MIMIC_DB_PATH,MIMIC_PORT.- SQLite persisted at
/data/mimic.sqlite, volumemimic-datasurvivesmake restart.
Acceptance tests (Playwright, 36 specs, all 27 ACs covered)
e2e/scaffold:playwright.config.ts,fixtures/{auth,api}.ts, 6 spec files (one per user story).- Suite is portable via
MIMIC_CONTAINER_CMD/MIMIC_BASE_URLenv vars (works withdockerorpodman).
Docs
README.mdwith quick-start, architecture overview, project layout, make target reference, and dev workflow.pyrightconfig.jsonat repo root pointing the Python LSP tobackend/.venvand adding the worktree root toextraPathsfor absolute imports.
Changed
- 2026-05-26 —
adminrole widened inSPEC.md§ Décisions techniques. The initial draft restricted admin to user-management only; after the Sprint 1 plan review surfaced the operational pain (admin would need a secondredteamaccount just to manage engagements), the user decided to make admin a super-user that cumulates redteam rights on engagements/simulations.
Removed
- none
[Sprint 0] — Bootstrap (merged 2026-05-26)
Added
- Initial
SPEC.mdcovering project scope, simulation model, workflow, stack, and agent team. - Technical decisions section in
SPEC.md: 3-role auth (admin/redteam/soc), JWT Bearer, single-container Flask+React, local MITRE STIX bundle, minimal Engagement model, admin bootstrap via Makefile target. - Sub-agent definitions under
.claude/agents/for backend-builder, frontend-builder, spec-reviewer (project override of the built-in, covers plan-vs-spec and code-vs-spec), code-reviewer, test-verifier, devil-advocate. - Project tracking scaffold:
tasks/todo.md,tasks/lessons.md,CHANGELOG.md,.gitignore.