Commit Graph
100 Commits
Author SHA1 Message Date
KnackyandClaude Sonnet 4.6 1819089925 feat(frontend): compact table density (32px row) global
Add table-compact class to all 4 list surfaces: EngagementsListPage,
TemplatesListPage, UsersAdminPage, SimulationList. Remove inline
px-xl py-md from th/td — recipe handles padding. Row border-b moved
to recipe. WCAG SC 2.5.5 tradeoff accepted (BAS operator tool).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:11:24 +02:00
KnackyandClaude Sonnet 4.6 9a8c5f52ab feat(frontend): add BackLink helper + dedup 3 hand-rolled instances
BackLink: ArrowLeft (14px) + caption-md text-graphite hover:text-primary,
instant. Replaces inline Link patterns in EngagementDetailPage (already
committed), SimulationFormPage, and TemplateFormPage. Also migrates
SimulationFormPage Done/SOC banners to AlertBanner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:11:17 +02:00
KnackyandClaude Sonnet 4.6 59eaa342a9 feat(frontend): add AlertBanner component + 4 semantic variants
border-l-4 semantic strip, bg-paper, Lucide icons at size=16.
ARIA role="alert" for error/warn, role="status" for success/info.
No shadow, no radius, no transition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:11:10 +02:00
KnackyandClaude Sonnet 4.6 004d075cad feat(frontend): refactor EngagementDetailPage to 3-tab layout
Tabs: Schedule (default) / Description / Simulations, wired via
useHashTab('schedule'). Count pill on Simulations tab. BackLink
replaces hand-rolled Link.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:11:04 +02:00
KnackyandClaude Sonnet 4.6 bca39dcca7 feat(frontend): add Tabs primitive + useHashTab hook
- useHashTab: pure-TS hook reading window.location.hash, falls back to
  defaultId, hashchange listener cleaned up on unmount
- Tabs: <Tabs items activeId onChange> with tab-underline / tab-underline-active
  recipes and count-pill (rounded-pill exception per DESIGN.md)
- index.css: tab-underline*, tab-count-pill*, alert-*, table-compact recipes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:10:59 +02:00
Knacky 89fb38b107 docs(sprint-11): plan spectrum UX port — 4 primitives + compact density global 2026-06-21 22:03:15 +02:00
KnackyandClaude Sonnet 4.6 208c6e6428 fix(c2): append trailing slash to mythic /graphql URL to avoid 301 redirect
Nginx fronting Mythic 3.x redirects /graphql → /graphql/ (HTTP 301).
The sprint 8 SSRF defense (allow_redirects=False) is correct and intentional;
the fix is the URL, not the redirect policy. Upstream reference:
mythic_utilities.get_http_transport hardcodes /graphql/ with trailing slash.

Updated _GQL_URL in all three adapter test files to match.
Added TestMythicAdapterUrlConstruction regression test to prevent silent
regression on future refactors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 21:25:06 +02:00
Knacky 91533c214c docs(spec): align c2_config.verify_tls default with sprint 10 (false) 2026-06-21 21:06:10 +02:00
KnackyandClaude Sonnet 4.6 0fab40b486 feat(backend): default verify_tls to false for lab/redteam Mythic
flip default to False at model, API fallback, adapter, and factory layers.
add migration 0008 flipping c2_config.verify_tls server_default to false.
suppress urllib3 InsecureRequestWarning when verify_tls is off.
adapt c2 tests to new verify_tls default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 21:03:05 +02:00
KnackyandClaude Sonnet 4.6 ee5fda6059 fix(frontend): make verify_tls helper text spell out the MITM tradeoff
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 21:01:49 +02:00
KnackyandClaude Sonnet 4.6 774ae05748 fix(frontend): default verify_tls checkbox to unchecked + add helper text
Flip useState(true) → useState(false) and delete-reset from true → false
so a freshly-created C2 config does not block self-signed Mythic instances.
Add hint paragraph below the checkbox matching FormField hint style
(text-[12px] text-graphite): "Leave unchecked for lab Mythic with
self-signed certificates."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 20:57:41 +02:00
Knacky c31f985e7a docs(sprint-10): plan flip verify_tls default to false (redteam-friendly) 2026-06-21 20:55:32 +02:00
KnackyandClaude Sonnet 4.6 8b5b5d94d8 fix(frontend): floating dropdowns and matrix modal frame use bg-paper
MitreTechniquePicker dropdown, SimulationList overflow menu,
ExportEngagementButton format menu, and MitreMatrixModal dialog frame
all used bg-canvas as their surface color. With the tinted canvas
(#f3f5f8), these floating surfaces appeared slightly grey instead of
clean white. Switched to bg-paper (#ffffff light / #1f2937 dark).
MitreMatrixModal cell hover (bg-canvas) intentionally preserved —
matrix cells sit on canvas, not on paper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 11:12:07 +02:00
KnackyandClaude Sonnet 4.6 76bcb04c8f style(frontend): inputs + outline buttons inside cards use bg-paper
.text-input, .btn-outline, .btn-outline-ink were using bg-canvas which
now resolves to the tinted #f3f5f8 instead of white, making inputs and
Cancel/outlined buttons visually gritty on white paper cards. Switching
all three to bg-paper restores white surfaces inside cards in light mode.
Dark mode unaffected (canvas/paper both resolve correctly there).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 11:11:59 +02:00
KnackyandClaude Sonnet 4.6 88b97cef2e feat(frontend): 2-column layout for EngagementFormPage in edit mode
In edit mode with canEditEngagements, wraps [form | C2ConfigCard] in a
lg:grid-cols-2 responsive grid with items-start alignment. Stacks to
single column on screens narrower than lg. In create mode, retains the
existing max-w-2xl single-column layout. No logic changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 11:01:09 +02:00
KnackyandClaude Sonnet 4.6 e4b1d6cb57 style(frontend): tint canvas light to #f3f5f8 for cards-on-canvas contrast
Canvas and paper were both #ffffff in light mode — cards only separated
by a 1px hairline, causing eye fatigue. Tints the canvas token to a
very pale cool neutral (#f3f5f8) so paper cards lift naturally without
shadow or radius, preserving brutalism. Dark mode tokens unchanged.
Updates DESIGN.md Surface section with rationale.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 11:01:00 +02:00
Knacky a9fe2fc528 docs(sprint-9): plan UI 2-col engagement + global contrast pass 2026-06-11 10:53:16 +02:00
KnackyandClaude Sonnet 4.6 38e282a126 fix(backend): complete c2 task→simulation mapping per spec + sanitize adapter errors (sprint 8 code-review)
mapping.py — full §0.11 contract:
1. execution_result: append '$ <command>\n<output>\n' block (previously
   wrote raw output without command header, making multi-task blobs
   unreadable in exports)
2. executed_at: set from task.completed_at when currently null (was
   completely missing — simulation.executed_at stayed null forever)
3. commands: append task.command deduplicated line-by-line (was
   completely missing — simulation.commands stayed empty)

mythic.py — sanitize transport errors:
Replace 'raise C2Error(str(exc))' (which leaks the Mythic URL via
requests exception repr) with 'raise C2Error(f"C2 transport error:
{type(exc).__name__}")'. Original exc stays chained for backend logs.

api/c2.py — remove redundant 'task.mapping_applied = True' in import
endpoint (apply_task_to_simulation() already sets it).

test_c2_mapping.py — full rewrite: 19 tests covering command blocks,
executed_at set/preserve, commands dedup, idempotency.

test_c2_adapter_mythic.py — add URL-leak sanitization assertion.

468 passed; ruff + mypy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 20:28:49 +02:00
KnackyandClaude Sonnet 4.6 7d3d39639e fix(backend): expose c2_task.source in GET /c2/tasks response
Serialize source as t.source.value (string) in list_simulation_tasks.
Updated test_c2_tasks_list shape assertion to include 'source' and
assert value is 'mimic' for execute-created tasks. Added test in
test_c2_import to assert source='import' in GET /c2/tasks after import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 20:24:22 +02:00
KnackyandClaude Sonnet 4.6 184a2a16c9 fix(frontend): a11y on clickable rows + correct c2 source field + pill metric alignment (sprint 8 design-review)
F1: add tabIndex/role/onKeyDown/aria-expanded to C2TasksPanel expander rows and
    C2CallbackPicker callback rows; focus-visible ring via Tailwind utilities
F2: add source:'mimic'|'import' to C2TaskListItem; C2TasksPanel reads task.source
    instead of mapping_applied for the Source badge label
F3: align C2TaskStatusBadge and C2CallbackPicker Active/Inactive pill metrics to
    py-[6px] text-[14px] font-medium (matches SimulationStatusBadge / StatusBadge)
F4: replace hand-rolled Source pill class string with badge-pill-outline recipe
Tests: 212/212 passing (+3 new: Enter/Space key on expander, Enter key on callback row)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 20:22:45 +02:00
KnackyandClaude Sonnet 4.6 7ff153905b feat(frontend): c2 tasks panel + history import (sprint 8 phase 2)
- Add getC2Tasks / listCallbackHistory / importC2 API functions + types
- useC2Tasks with 2500ms polling (stops when all tasks completed)
- useC2CallbackHistory, useImportC2 hooks
- C2TaskStatusBadge, C2TasksPanel (expandable output rows, polling indicator)
- C2CallbackPicker extracted as shared component (reused in both modals)
- ImportC2HistoryModal: 2-step callback picker → paginated history table
- SimulationFormPage: RT card + tasks panel share left grid column; Import C2 history button
- 37 new tests (api/c2, C2TasksPanel, ImportC2HistoryModal, SimulationFormPage panel visibility)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 20:11:12 +02:00
KnackyandClaude Sonnet 4.6 8f23f59601 feat(backend): c2 callback history + task import (sprint 8 M4)
Command source decision: extended C2TaskStatus with command: str | None
(default None). Added command_name to _GET_TASK_QUERY so get_task() returns
command in a single round-trip — no separate history fetch needed on import.
4-line change, zero cascading test impact.

adapter.py:
- C2TaskStatus: add command: str | None = None field
- C2HistoricalTask: new dataclass (display_id, command, params, status,
  completed, timestamp) for history rows
- C2TaskPage.items: typed as list[C2HistoricalTask] (was list[dict])

mythic.py:
- _GET_TASK_QUERY: add command_name field
- _LIST_CALLBACK_TASKS_QUERY: new query (order_by id desc, limit/offset)
- _COUNT_CALLBACK_TASKS_QUERY: new aggregate query for total
- get_task(): surfaces command_name as status.command
- list_callback_tasks(): two _post() calls (tasks + count), allow_redirects=False

fake.py:
- _FAKE_HISTORY: frozen deterministic history (cb1=12, cb2=0, cb3=5 tasks)
- list_callback_tasks(): serves from _FAKE_HISTORY, pagination applied
- get_task(): returns command from _tasks dict

api/c2.py:
- GET /api/engagements/<eid>/c2/callbacks/<cid>/history: page+page_size
  defaults 1/25, cap 100, reject <1, 502 on adapter error
- POST /api/simulations/<sid>/c2/import: idempotent per (sim,mythic_id) pair,
  source=import, completed tasks get output+mapping_applied, incomplete tasks
  stored for poll-on-read pickup, auto-transition pending→in_progress

60 new tests (456 total); pytest/ruff/mypy all green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 20:09:29 +02:00
KnackyandClaude Sonnet 4.6 b83316f715 fix(backend): repair 0006 migration tests for alembic 1.18.x proxy api
Replace deprecated alembic.op._proxy assignment with
ops._install_proxy() / ops._remove_proxy() pattern required
by Alembic >= 1.13. Consistent with test_migration_0007_c2.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 20:02:46 +02:00
KnackyandClaude Sonnet 4.6 873e52a2a1 feat(backend): c2 poll-on-read + output mapping (sprint 8 M3)
- adapter.py: add completed_at field to C2TaskStatus dataclass
- mythic.py: implement get_task() (GraphQL task query) and
  get_task_output() (response query + decode_response_text concat)
- fake.py: deterministic state progression via per-instance call counter;
  get_task_output raises C2Error until completed
- mapping.py: apply_task_to_simulation() idempotent output mapper
  (mapping_applied anchor prevents double-writes)
- migration 0007: add mapping_applied BOOLEAN NOT NULL DEFAULT false to c2_task
- c2_task model: mapping_applied column added
- api/c2.py: GET /api/simulations/<id>/c2/tasks poll-on-read endpoint;
  refreshes incomplete tasks from C2, fetches output on completion,
  applies mapping, skips re-polling for completed tasks; best-effort
  (C2Error on individual task skipped, returns 200 with stale status)
- 51 new tests (396 total); pytest/ruff/mypy all green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 19:56:06 +02:00
KnackyandClaude Sonnet 4.6 5ff6ae8940 feat(frontend): c2 config card + execute modal (sprint 8 phase 1)
- frontend/src/api/c2.ts: 6 typed API calls (getC2Config, putC2Config,
  deleteC2Config, testC2Config, listCallbacks, executeC2) following the
  frozen M1+M2 backend contracts
- frontend/src/api/types.ts: C2Config, C2ConfigInput, C2TestResult,
  C2Callback, C2CallbacksResponse, C2Task, C2ExecuteInput, C2ExecuteResponse
- frontend/src/hooks/useC2.ts: useC2Config, useUpdateC2Config,
  useDeleteC2Config, useTestC2Config, useC2Callbacks, useExecuteC2
- frontend/src/components/C2ConfigCard.tsx: engagement-scoped C2 config
  card (url + write-only token + verify-tls + save/delete/test-connection),
  503 disabled state, ConfirmDialog on delete
- frontend/src/components/ExecuteViaC2Modal.tsx: callback picker table
  (mono data cells), commands textarea pre-filled from rt.commands,
  Launch disabled until row selected + non-empty commands
- frontend/src/pages/EngagementFormPage.tsx: embed C2ConfigCard in edit
  mode only, admin+redteam only (canEditEngagements gate)
- frontend/src/pages/SimulationFormPage.tsx: Execute via C2 button in RT
  card, visible only when !isDone && canEditRT && hasC2Config; opens modal
- Tests: 33 new tests across api/c2, components/C2ConfigCard,
  components/ExecuteViaC2Modal, EngagementFormPage, SimulationFormPage
  (172 total, 139 baseline + 33 new, all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 19:50:11 +02:00
KnackyandClaude Sonnet 4.6 53755a31d6 feat(backend): c2 callbacks + execute endpoints (sprint 8 M2)
- Add C2Error exception to adapter ABC
- Add promote_to_in_progress() helper to simulation_workflow (pending→in_progress)
- Flesh out MythicAdapter: list_callbacks() (GraphQL query) + create_task() (mutation)
- Expand FakeAdapter to 3 deterministic callbacks; switch task store to per-instance
- Add GET /api/engagements/<id>/c2/callbacks — lists active callbacks via adapter
- Add POST /api/simulations/<id>/c2/execute — issues tasks, stores C2Task rows,
  auto-transitions pending→in_progress, blocks on done (409)
- Both endpoints: SOC=403, 503 no-key, 502 adapter error, sanitized error messages
- Add requests-mock==1.12.1 to requirements.txt
- 42 new tests (342 total, 300 M1 baseline preserved green)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 19:38:07 +02:00
KnackyandClaude Sonnet 4.6 9a9c98beab feat(backend): c2 crypto + config CRUD + adapter scaffolding (sprint 8 M1)
- Add Fernet crypto service (MIMIC_ENCRYPTION_KEY env, C2Disabled on absent key)
- Add Alembic migration 0006: c2_config + c2_task tables with cascade FKs
- Add C2Config and C2Task SQLAlchemy models
- Add C2Adapter ABC with dataclasses (C2Health, C2Callback, C2TaskStatus, C2TaskPage)
- Add FakeAdapter (deterministic in-memory, MIMIC_C2_ADAPTER=fake)
- Add MythicAdapter scaffold: test_connection() live, M2+ raise NotImplementedError
- Add decode_response_text() helper for base64/binary Mythic responses
- Add GET/PUT/DELETE/POST-test /api/engagements/<id>/c2-config endpoints
- RBAC: admin+redteam OK, SOC 403; 503 guard when encryption key absent
- Token never returned in API responses; stored Fernet-encrypted only
- 42 new tests (300 total, 258 baseline preserved green)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 19:20:52 +02:00
Knacky 813e69ee01 docs(spec): add C2 integration section (sprint 8 commit #1)
Introduce the SPEC section for the Mythic C2 integration layer.
Covers RBAC (RT-only, SOC=403), per-engagement Fernet-encrypted config,
c2_config + c2_task data model with ON DELETE CASCADE, full endpoint
list, output mapping rules (append-only, idempotent), 2500 ms polling
and the fake/real adapter selection via MIMIC_C2_ADAPTER.

Also patch tasks/todo.md: fix pytest baseline (256 from main, not 253),
make cascade-delete explicit, pin the MythicMeta/Mythic_Scripting source
version and document defensive base64 handling.

Closes spec-reviewer WARN-1 (SPEC ↔ plan parity), WARN-2 (cascade),
INFO-1 (pinned source), INFO-3 (baseline).
2026-06-10 19:07:35 +02:00
KnackyandClaude Sonnet 4.6 1997a8c621 feat(frontend): side-by-side red-team/SOC columns on simulation screen
Wrap RT and SOC form cards in a responsive 2-column grid
(grid gap-xl lg:grid-cols-2 items-start) on the simulation edit view.
Drop the max-w-3xl constraint from the outer container so the grid
can use full page width (matching EngagementDetailPage). Header,
banners, submitError, and sticky action bar remain full-width above
the grid. The isNew create form keeps its current narrow layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 18:17:54 +02:00
KnackyandClaude Sonnet 4.6 573281f454 fix(design): code-reviewer polish — dedupe tag-mitre recipe, drop dead boxShadow tokens, cover StatusBadge classes
F1: MitreTechniqueTag inline classes replaced with className="tag-mitre gap-xxs"
    (index.css .tag-mitre is now the single source of truth for technique tags).
F2: boxShadow block removed from tailwind.config.ts — no component referenced
    shadow-soft-lift / shadow-floating / *-dark; DESIGN.md §Don'ts prohibits shadows.
F4: StatusBadge.test.tsx gains 3 class assertions (bg-warn-soft / bg-primary-soft / bg-cloud)
    mirroring SimulationStatusBadge.test.tsx pattern. Test count: 136 → 139.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 05:53:53 +02:00
KnackyandClaude Sonnet 4.6 6995c4c860 fix(design): address design-reviewer findings F1-F6 — nav slab, spinner, badge coverage, mono discipline
F3: nav-bar-top bg-paper → bg-slab text-slab-text (3-band slab anchoring restored).
    NavLinks: text-slab-muted default, text-slab-text + border-primary active.
    Logo span: text-ink → text-slab-text.
F2: (you) label extracted from font-mono td into adjacent font-sans span.
F1: Loader2 circular spinner → EXPORTING… text with animate-pulse (terminal-SOC compatible).

Screenshots regenerated:
- All 8 pages light+dark (01→10)
- 05-simulation-form-edit light+dark (F6)
- 11-mitre-matrix-modal light+dark (F6)
- 12-toast-success light+dark (F6)
- 13-confirm-dialog light+dark (F6)
- admin-light/dark-open/closed regenerated from HEAD (F4)

F4: StatusBadge.tsx confirmed single code path — planned → bg-warn-soft (no divergence in code).
    Divergence in prior captures was stale cache; regenerated admin-* confirm consistency.
F5: Simulations seeded (pending/in_progress/review_required/done) via API;
    10-sim-list-badges shows all 4 semantic badge colors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 19:43:08 +02:00
KnackyandClaude Sonnet 4.6 7ea2fe490c docs: add sprint 7 terminal-SOC design refresh to CHANGELOG
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 19:01:29 +02:00
Knacky 0e69eb901c test(frontend): update vitest assertions for new design tokens 2026-06-09 18:46:03 +02:00
Knacky 5cc830554c refactor(pages): apply terminal-SOC aesthetic to all 8 pages 2026-06-09 18:44:47 +02:00
Knacky ec7800ae38 refactor(components): squared shapes + mono data cells across all components 2026-06-09 18:42:26 +02:00
Knacky c791e50108 feat(design): bundle JetBrains Mono for data cells 2026-06-09 18:39:13 +02:00
Knacky 64da94f10d feat(design): refresh tailwind tokens for brutalist terminal aesthetic 2026-06-09 18:38:43 +02:00
Knacky 6c05cc2e11 docs(design): rewrite DESIGN.md for terminal-SOC aesthetic 2026-06-09 18:37:44 +02:00
KnackyandClaude Opus 4.7 5627d7dcfa docs(sprint 7): plan — terminal-SOC design refresh
9 binding decisions locked with user 2026-06-09 (4-question + 4-question
+ 3-question rounds). Visual direction Bloomberg / terminal SOC. Border
radius 0 except status pills and avatars. Palette kept (primary blue +
slab + canvas/paper/cloud/fog/ink), ADD success/warn semantic tokens.
Scope: 8 pages + 17 components + tokens + DESIGN.md rewrite, all in one
sprint. JetBrains Mono for data only (Inter stays for body/headers).
Light + dark both kept. Zero transitions (brutalist).

Plan validated by spec-reviewer pre-pass: APPROVED with 3 findings
addressed inline (D9 added, R2 reworded, semantic tokens promoted from
optional to locked).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 18:34:45 +02:00
KnackyandClaude Opus 4.7 c85ece46b9 chore(agents): frontend-builder must invoke Skill frontend-design before UI work
Adds a mandatory pre-step to the frontend-builder agent prompt: invoke the
frontend-design skill at sprint start before creating or modifying any
visible UI component. DESIGN.md rules project-specific tokens; the skill
covers universal principles (typographic hierarchy, alignment, contrast,
focus, density, motion). Skip allowed only for pure logic/data-layer work
with no visual change.

Authored locally during sprint 6 (uncommitted in worktree), bundled into
sprint 7 hygiene as the first commit so it takes effect immediately for
the design refresh work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 18:34:36 +02:00
KnackyandClaude Sonnet 4.6 e41679b331 fix(export): render PDF in A4 landscape for 7-column readability
Add @page { size: A4 landscape } to _CSS, reduce font-size to 11px,
and set table-layout: fixed + word-break: break-word so 7 columns
fit without overflow. Unit test asserts the landscape rule is present
in the rendered HTML.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 18:13:46 +02:00
Knacky 2d1c113f0c docs: log 2 MEDIUM security fixes in CHANGELOG (post-refactor)
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.
2026-06-08 19:29:59 +02:00
KnackyandClaude Sonnet 4.6 3a9d9d3203 fix(security): defuse CSV formula injection in multiline exécution cell + HTML-escape Markdown table cells
Finding 1 — CSV multiline formula injection:
- Split _format_execution into _format_execution_text (MD/PDF, no sanitization) and
  _format_execution_csv (CSV, applies _csv_safe to each user-controlled component before join)
- Moved _CSV_FORMULA_TRIGGERS + _csv_safe above the format helpers (required by _format_execution_csv)
- Outer _csv_safe on the Exécution cell retained as belt-and-braces for the empty-date case
- New test: test_render_engagement_csv_defuses_formula_in_inner_execution_lines

Finding 2 — Stored XSS in Markdown table:
- _cell() in render_engagement_markdown now calls _html_escape() (quote=True, default)
  before pipe-escaping and \n→<br/> substitution — correct order preserved
- New test: test_render_engagement_markdown_escapes_html_in_table_cells

255 → 257 passed, ruff clean, mypy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:29:10 +02:00
Knacky 4d9447082f docs: sprint 6 amendment — 7-column schema in CHANGELOG + PR body
Post-review user decision (2026-06-08) switched the export payload to a
fixed 7-column FR handoff schema (Scénario / Test / Source de log /
Commentaires SOC / Exécution / Logs remontés au SIEM / Cyber incident).

Logged in CHANGELOG [Unreleased] Changed section with commit refs
(SPEC fdab324, backend 7335b9f, e2e aeb4bdb) and updated PR #9 body
counters: 255 pytest (was 253), 136 vitest unchanged, 223 e2e
unchanged.
2026-06-08 19:23:02 +02:00
KnackyandClaude Sonnet 4.6 aeb4bdb025 test(e2e): adapt export specs to 7-column schema (Scénario/Test/...)
Update AC-29.2 (Markdown) to assert | Scénario | GFM table header.
Update AC-29.3 (CSV) to assert exact 7 FR column names instead of 'name'.
Update AC-31.4 (empty engagement) MD to assert table absent, CSV header
to assert exact 7 FR columns.
Drop unused sim1/sim2 vars and makeClient import (NIT cleanup).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:21:51 +02:00
KnackyandClaude Sonnet 4.6 7335b9f2c6 refactor(export): switch render output to fixed 7-column schema (Scénario, Test, ...)
All three renderers (MD, CSV, PDF) now emit a uniform 7-column table with
French headers matching the RT↔SOC handoff contract locked in SPEC.md fdab324.

Helpers added:
- _format_execution(sim): canonical 3-part concat (executed_at / commands / execution_result)
- _MD_HEADERS / _HTML_HEADERS / _CSV_HEADERS unified to the same 7 FR strings

Helpers removed (no longer called):
- _tactic_names() — MITRE tactics dropped from export
- _enrich_sim_techniques() — MITRE techniques dropped from export

Fields dropped from export: status, techniques, tactic_ids, prerequisites, id,
created_at, updated_at (intentional — focused RT↔SOC handoff, see SPEC §Export).

_csv_safe() still applied to all 7 user-controlled cells including Exécution concat.

Tests updated: 255 passed, ruff clean, mypy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:15:49 +02:00
Knacky fdab324217 docs(spec): export — switch to fixed 7-column handoff schema
User decision 2026-06-08 (post-PR-9, pre-merge): the export schema is
now a fixed 7-column layout focused on the RT↔SOC handoff, applied
uniformly across Markdown / CSV / PDF.

Columns (French headers): Scénario, Test, Source de log,
Commentaires SOC, Exécution (multiline concat of executed_at +
commands + execution_result, no labels), Logs remontés au SIEM,
Cyber incident.

Removed from the export (intentional): simulation status, MITRE
techniques and tactics, prerequisites, id, created_at, updated_at.
The export is a handoff product, not a full data dump.

This is the spec change that drives the upcoming render refactor
in services/export.py. SPEC committed first per the sprint-6
positional fix (FIRST commit, not at sprint close).
2026-06-08 19:10:42 +02:00
Knacky e4a672c443 docs: sprint 6 wrap-up — README + CHANGELOG + 6 lessons
- README "Status" bumped to sprint 6 + test counts (253 backend, 136
  frontend, 223 e2e).
- CHANGELOG [Unreleased] section for sprint 6: backend, frontend, e2e,
  security, and changed-section notes (SPEC commit-first + mimic team).
- 6 sprint-6 lessons in tasks/lessons.md:
  1. SPEC.md commit-first tamed the 4-sprint recurrence
  2. Persistent team mimic + idle members > "never idle"
  3. Security plugin caught CSV formula injection mid-sprint
  4. Stdlib first before custom helpers
  5. Tests that mock at module level can't exercise the target's branches
  6. _engagement param for signature symmetry across render trio

This is the team-lead wrap-up commit. PR body in tasks/pr-body-sprint-6.md
will be ingested by make open-pr.
2026-06-08 18:35:21 +02:00
KnackyandClaude Sonnet 4.6 b572a67066 test(e2e): sprint 6 acceptance — US-29 / US-30 / US-31
Adds 3 Playwright spec files covering all 13 ACs for the engagement
export feature:
- us29-export-formats.spec.ts (8 tests): dropdown, md/csv/pdf downloads,
  admin + redteam, filename convention
- us30-export-rbac.spec.ts (3 tests): SOC button absent, SOC 403, no-token 401
- us31-export-robustness.spec.ts (4 tests): missing format 400, bad format 400,
  unknown engagement 404, zero-sim export OK

Total: 201 → 223 Playwright tests. No regressions on sprints 1–5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:31:32 +02:00
KnackyandClaude Sonnet 4.6 3725d4415e chore: code-review cleanups (NITs + filename defense-in-depth test)
- NIT-1: remove dead _technique_names() and _technique_ids() helpers (no callers)
- NIT-2: rename engagement → _engagement in render_engagement_csv signature
- NIT-4: remove duplicate inline User import in test_export_csv_escapes_special_characters
- NIT-5: add comment on _CSV_FORMULA_TRIGGERS explaining \t and \r inclusion
- REUSE: replace custom _html_escape with stdlib html.escape (quote=True default)
- Remove now-unnecessary type: ignore comments on weasyprint (stubs resolve cleanly)
- Add test_export_filename_never_contains_quote_or_crlf defense-in-depth test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:23:39 +02:00
Knacky 123d9812bc test: cover Content-Disposition fallback in ExportEngagementButton 2026-06-08 18:20:29 +02:00
Knacky 57dbd14347 fix(security): defuse CSV formula injection in engagement export (MEDIUM)
Authenticated red-team users could craft any user-controlled string field
(name, description, commands, prerequisites, execution_result, log_source,
logs, soc_comment, incident_number, MITRE technique IDs) starting with =,
+, -, @, \t or \r. When the SOC analyst opens the exported CSV in Excel /
LibreOffice / Google Sheets — explicitly the consumption flow this sprint
optimizes for — the spreadsheet executes the field as a formula on the
SOC's machine.

Fix: new helper _csv_safe() prefixes a single apostrophe to any string
starting with a formula-trigger character, forcing the spreadsheet to
render the cell as text. Applied to every user-controlled field in
render_engagement_csv. Numeric and ISO-date fields are not wrapped.

Tests:
- test_render_engagement_csv_escapes_formula_injection_in_name
- test_render_engagement_csv_escapes_formula_injection_in_commands
- test_render_engagement_csv_does_not_alter_safe_strings

Result: 249 → 252 passing (the 1 remaining failure is pre-existing
test_index_without_built_frontend_returns_json, unrelated to this fix).

Flagged by security-guidance@claude-code-plugins automated review.
2026-06-08 18:13:16 +02:00
KnackyandClaude Sonnet 4.6 25877c4092 test: ExportEngagementButton + EngagementDetailPage RBAC tests
9 tests for ExportEngagementButton (render, open, close-outside,
Escape, per-format trigger, loading state, error toast).
3 RBAC tests for EngagementDetailPage (admin/redteam see Export,
soc does not). Total: 121 → 133 vitest passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:04:56 +02:00
KnackyandClaude Sonnet 4.6 100441bdeb feat: ExportEngagementButton + exports API client
Add split-button dropdown [Export ▼] on EngagementDetailPage that
downloads engagement as Markdown, CSV, or PDF via
GET /api/engagements/<id>/export?format=md|csv|pdf.

Both halves open the dropdown (no default left-click action).
RBAC-gated with canEditEngagements (admin + redteam only).
Loading state per item, toast on error, click-outside + Escape close.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:04:49 +02:00
KnackyandClaude Sonnet 4.6 5471c8fd89 test: add export endpoint + render unit tests (226 → 249 passing)
- test_export_engagement.py: 13 endpoint tests — RBAC (admin/redteam ok, SOC 403,
  401 unauthenticated), CSV column contract, CSV special char escaping, PDF magic bytes,
  400 on missing/unknown format, 404 on missing engagement, zero-simulations edge case,
  filename slugification.
- test_export_render.py: 10 unit tests on pure render functions — header fields,
  simulation order, techniques/tactics enrichment, SOC fields always rendered,
  backtick safety in commands, CSV header row, multi-technique pipe join, PDF magic
  bytes, MITRE bundle not loaded does not crash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:57:40 +02:00
KnackyandClaude Sonnet 4.6 f1a7965ab9 chore: add WeasyPrint system deps to Dockerfile python stage
apt-get install libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b
libfontconfig1 shared-mime-info — minimal set for text-only PDF rendering.
libgdk-pixbuf-2.0-0 excluded (no images in PDF, verified via weasyprint --info).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:57:31 +02:00
KnackyandClaude Sonnet 4.6 87e4409530 feat: add engagement export service and endpoint (md/csv/pdf)
- New module backend/app/services/export.py with render_engagement_markdown,
  render_engagement_csv, render_engagement_pdf, _render_engagement_html helper,
  and _export_filename slugifier (NFKD + fallback "unnamed").
- Extend engagements_bp with GET /api/engagements/<int:eid>/export?format=md|csv|pdf,
  gated @role_required("admin","redteam"). Returns 400 on missing/unknown format,
  404 on unknown engagement, correct Content-Type + Content-Disposition headers.
- Reuses _enrich_techniques and _enrich_tactics from serializers.py; resilient
  to MITRE bundle not loaded (returns empty tactics, no crash).
- Adds weasyprint>=60.0 to backend/requirements.txt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:57:22 +02:00
Knacky cf006a2ba8 docs(plan): sprint 6 — apply spec-reviewer Pass 1 fixes (1 BLOCKER + 6 WARN)
Fixes applied:
- BLOCKER §2 : EngagementDetailPage.test.tsx → "nouveau" (n'existe pas
  encore), pas "existant — adapter".
- WARN §1 : "Première ligne du summary" obligatoire pour backend-builder
  avec le path final EXACT (anti-URL-drift, lesson sprint 5).
- WARN §0/§1 : slug avec NFKD-strip pour accents + fallback "unnamed"
  pour edge case nom 100% non-alphanum.
- WARN §2 : ExportEngagementButton les DEUX moitiés ouvrent le dropdown
  (pas d'action par défaut — différence vs NewSimulationDropdown).
- WARN §2 : exports.ts throw Error sur non-2xx pour pipeline toast.
- WARN §1 : created_by rendu username-only en MD/CSV (pas la dict).
- WARN §1 : PDF généré depuis les DONNÉES (pas depuis le string Markdown).

NITs incorporés :
- gdk-pixbuf-2.0-0 retiré du set minimal (text-only PDF), avec note
  pour confirmer via weasyprint --info.
- data-testid="export-dropdown" sur le wrapper pour AC-30.1.
- AC-29.3 : compter rows via csv.reader, pas file.split.
- §0 point 14 : style explicite btn-outline (cohérence header).
- Test MITRE-bundle-not-loaded ajouté à test_export_render.py.

Plan prêt pour spec-reviewer Pass 2.
2026-06-07 18:38:41 +02:00
Knacky 01434c04a7 docs(plan): sprint 6 — engagement export (md/csv/pdf) plan
3 user stories scoped (US-29 export formats, US-30 SOC zero access,
US-31 format/engagement robustness). Backend extends engagements_bp
with GET /api/engagements/<id>/export?format=md|csv|pdf returning the
rendered file, no DB schema change. Frontend adds an
ExportEngagementButton split-button dropdown on EngagementDetailPage,
gated to admin+redteam.

Binding decisions locked with the user: 3 formats Markdown/CSV/PDF,
RBAC admin+redteam, engagement + all simulations RT+SOC, single
endpoint with format query param. WeasyPrint chosen for PDF (Python
HTML→PDF, ~50MB cairo/pango deps to add to Dockerfile, accepted).

Plan ready for spec-reviewer Pass 1.
2026-06-07 18:29:59 +02:00
Knacky 7aaa5ccc6d docs(spec): add § Export d'engagement section (sprint 6)
Specifies the new export feature contract:
- 3 formats : Markdown, CSV, PDF
- Engagement header + all simulations RT + SOC
- Endpoint unique GET /api/engagements/<id>/export?format=md|csv|pdf
- RBAC admin + redteam (SOC zero access, cohérent avec Templates)
- Filename normalisé engagement-<id>-<slug>-YYYYMMDD.<ext>

Committed as commit #1 of sprint 6 — applies lesson learned in sprints 3/4/5
where the SPEC section sat as uncommitted M SPEC.md until sprint-close
discovery. Per lessons.md §sprint-5 fix candidate "Stage SPEC.md as part
of the FIRST sprint commit, not as a separate later commit."
2026-06-07 18:29:49 +02:00
Knacky e18ec2bf79 docs(lessons): sprint 5 — fold in the recurrent SPEC-uncommitted lesson with concrete fix candidates 2026-05-28 07:25:09 +02:00
KnackyandClaude Opus 4.7 cbc176ab82 docs(spec): carry over sprint 5 SPEC update missed in sprint 5 commits
Sprint 5 plan §0 added a new ## Templates de simulations section to SPEC.md
(between § Fonctionnement and § Authentification & rôles). The edit sat in
the sprint 5 worktree but was never committed across the 9 sprint commits,
so PR #8 currently does not include the corresponding spec text.

This is the THIRD sprint running this happens (sprint 3 → fixed at sprint 4
start; sprint 4 → fixed at sprint 5 start; sprint 5 → fixed here mid-PR
because I caught the M SPEC.md before merge).

Lesson updated in tasks/lessons.md to make the "git status pre-sprint-close"
discipline harder to forget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 07:24:43 +02:00
KnackyandClaude Sonnet 4.6 54959c7d5b test(e2e): sprint 5 acceptance — US-26 / US-27 / US-28 + adaptations dropdown sprint 2-4
- us26: add AC-26.4 isinstance guard (technique_ids string→400) + AC-26.7 cascade test (DELETE template does not affect instantiated sim)
- us27: add NIT-1 dropdown Escape/click-outside close, NIT-2 empty-engagement dropdown visibility
- 49 sprint 5 tests passing, 206/207 full suite passing (us1 pre-existing isolation issue)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:23:33 +02:00
KnackyandClaude Opus 4.7 2e59743af5 docs: sprint 5 wrap-up — CHANGELOG + README + 6 lessons + plan final
- CHANGELOG: sprint 5 entry under [Unreleased] (templates CRUD + instantiation + nav + dropdown + decorrelation). Sprint 4 moved to its own [Sprint 4] section.
- README: status bump to sprint 5, test counts refreshed (226/121/201).
- tasks/lessons.md: 6 sprint-5 lessons captured (spec-reviewer 2-pass before dispatch finally clicked, endpoint path drift caught visually not by spec-review, screenshot script mocks lag path changes, silent URL "improvements" by backend, apply_patch wrong primitive for creation copy paths, IntegrityError catch beats pre-check SELECT, SendMessage rule applies to all team agents).
- tasks/todo.md: status flipped to 🟢 SPRINT COMPLET.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 07:18:21 +02:00
KnackyandClaude Sonnet 4.6 7c011db6d9 test(e2e): sprint 5 acceptance tests — US-26 → US-28 + dropdown adaptations
Add three new spec files:
- us26-templates-crud: API CRUD (AC-26.3–26.7) + UI list/form/delete/redirect (AC-26.8)
- us27-instantiate-from-template: template_id copy + name override + 404 + decoupling
  (AC-27.1–27.3) + no auto-transition/engagement-activate (AC-27.4–27.5) + dropdown
  UI + picker modal + empty state + SOC gate (AC-27.6–27.7)
- us28-templates-nav: Templates link admin+redteam only, SOC redirect, form editable (AC-28.1–28.3)

Adapt sprint 2/3 e2e for sprint 5 dropdown:
- us4-engagements: getByRole link "New simulation" → getByTestId "new-simulation-btn"
- us7-simulation-create: same — split-button dropdown replaced the link

Suite: 201 passed (1 pre-existing flaky in us3 re DB state, unrelated to sprint 5).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:15:04 +02:00
KnackyandClaude Sonnet 4.6 55f993fa24 fix(backend): sprint 5 post-review — name fallback, isinstance guards, 400 tests
- create_simulation: name falls back to template.name when template_id provided
  and name is absent/empty (AC-27.1)
- templates POST/PATCH: isinstance(list) check on technique_ids/tactic_ids
  before resolving, returns 400 with clear message
- 5 new tests: unknown technique_id → 400 (POST+PATCH), unknown tactic_id → 400
  (POST+PATCH), name fallback to template.name
- mypy: merged template branch into if/else to eliminate union-attr false positives

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:04:25 +02:00
KnackyandClaude Sonnet 4.6 33a0ca30bb fix(frontend): sprint 5 post-code-review — dropdown close-on-outside + empty-state dropdown
- useEffect pointerdown + Escape listeners when dropdown open (NIT 1)
- empty state now renders NewSimulationDropdown instead of plain Link (NIT 2)
- 3 new Vitest: close-on-outside, close-on-Escape, empty-state has dropdown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:02:34 +02:00
KnackyandClaude Sonnet 4.6 20783118ee fix(frontend): sprint 5 design-review — dropdown dark + Plus icon + re-shoots
- shadow-floating dark:shadow-floating-dark on dropdown menu (Fix 1)
- hover:bg-cloud dark:hover:bg-fog on dropdown items (Fix 2)
- Plus icon + "New" label on split-button primary half (Fix 3)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 06:50:19 +02:00
KnackyandClaude Sonnet 4.6 2b700115e8 fix(frontend): sprint 5 — correct API path /simulation-templates → /templates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 06:43:33 +02:00
KnackyandClaude Sonnet 4.6 90fc5bab6c feat(frontend): sprint 5 — templates CRUD pages + nav + picker modal + dropdown
- types.ts: SimulationTemplate, SimulationTemplateCreateInput, SimulationTemplatePatchInput,
  extend SimulationCreateInput with template_id
- api/templates.ts: listTemplates, getTemplate, createTemplate, updateTemplate, deleteTemplate
- hooks/useTemplates.ts: useTemplates, useTemplate, useCreateTemplate, useUpdateTemplate,
  useDeleteTemplate (TanStack Query, invalidates ["templates"])
- TemplatesListPage: /admin/templates — table (name, MITRE count, created by, updated),
  New/Edit/Delete actions, loading/error/empty states
- TemplateFormPage: /admin/templates/new + /admin/templates/:id/edit — controlled form
  with inline MITRE field (picker + matrix modal), ConfirmDialog for delete
- TemplatePickerModal: reusable modal listing templates with empty state (AC-27.6)
- SimulationList: replace "New simulation" link with split-button dropdown
  (Blank → /simulations/new | From template… → TemplatePickerModal + POST template_id)
- Layout: "Templates" nav link (admin | redteam, before "Users")
- App.tsx: /admin/templates routes gated roles=["admin","redteam"]
- 26 new Vitest tests (118 total, 92 original preserved)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 06:36:10 +02:00
KnackyandClaude Sonnet 4.6 1f327e9aa8 feat(backend): sprint 5 — SimulationTemplate CRUD + instantiation
- SimulationTemplate model + migration 0005 (CREATE TABLE + name index)
- 5 CRUD endpoints under /api/templates (admin|redteam only, SOC 403)
- POST /api/engagements/<eid>/simulations extended with optional template_id
- serialize_template() reusing _enrich_techniques/_enrich_tactics helpers
- IntegrityError → 409 for duplicate name on both POST and PATCH
- 28 new tests (CRUD, RBAC, dedup, instantiation, migration round-trip)
- 221 tests pass; ruff clean; mypy clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 06:25:19 +02:00
KnackyandClaude Opus 4.7 6d2bb091e2 docs: sprint 4 wrap-up — CHANGELOG + README + 7 lessons + plan final
- CHANGELOG: sprint 4 entry under [Unreleased] (covers all 9 US: dark mode, MITRE matrix overhaul, tactic_ids, done read-only + Reopen, engagement auto-status, UI polish, design-reviewer agent, PR helper, screenshots mandatory). Sprint 3 moved to its own [Sprint 3] section.
- README: status bump, test counts refreshed (193/92/158).
- tasks/lessons.md: 7 sprint-4 lessons captured (git status before sprint close, endpoint round-trip mismatch caught only by e2e, ink vs slab token split, structural row layout > class tweaks, hardcoded paths in migration tests, screenshots with auth, builder cross-context summaries as accidental re-dispatch).
- tasks/todo.md: status flipped to 🟢 SPRINT COMPLET, execution sequence ticks updated with commit hashes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:41:47 +02:00
KnackyandClaude Sonnet 4.6 43ab7073f1 test(e2e): un-skip AC-21.6 — backend matrix fix landed
Remove test.fail annotation from AC-21.6 "Apply from modal includes
tactic in result". GET /api/mitre/matrix now returns tactic_id in TA-format
("TA0007") so the PATCH succeeds and the tactic chip appears.

Update button selector in both AC-21.6 tests from title*="discovery"
to title*="TA0007" to match the fixed matrix response format.

Suite: 158 passed, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 21:38:17 +02:00
KnackyandClaude Sonnet 4.6 7d81ce9785 test(e2e): fill coverage gaps — +N suffix + focus-trap cycle
Add two tests omitted from the initial sprint 4 run:
- us21: SimulationList MITRE column shows "TA0007 +2" for 1 tactic + 2 techniques
- us20: MitreMatrixModal Tab wraps to first focusable, Shift+Tab wraps to last

Suite: 158 passed, 0 failed (1 expected test.fail for AC-21.6 slug defect).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 21:33:18 +02:00
KnackyandClaude Sonnet 4.6 a824df06b2 fix(backend): AC-21.6 — matrix tactic_id returns TA-format (TA0007 not slug)
- mitre.py: add _SLUG_TO_TA_ID reverse map; _build_matrix() now emits tactic_id
  as TA-id (e.g. "TA0007") so frontend can send it back verbatim in PATCH tactic_ids
- test_mitre.py: update all matrix assertions to use TA-ids; add
  test_get_matrix_tactic_id_is_ta_format regression guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 21:30:48 +02:00
KnackyandClaude Sonnet 4.6 5aa839d105 test(e2e): sprint 4 acceptance tests — US-17 to US-23
Add new spec files for US-17 (UI polish), US-18 (done read-only + reopen),
US-19 (engagement auto-status), US-20 (matrix fits modal), US-21 (tactic
selection), US-22 (MITRE input redesign), US-23 (dark mode).

Adapt sprint 2/3 specs for sprint 4 UI renames: matrix icon button replaces
text buttons, inline search replaces Quick Search, Save replaces Save Red Team,
New replaces New Engagement, topbar uses bg-slab tokens, Apply N item(s) replaces
Apply N technique(s), done→review_required transition now valid (Reopen flow).

Mark AC-21.6 Apply-from-modal as test.fail: known defect where /api/mitre/matrix
returns slug tactic IDs but PATCH /simulations/:id expects TA-format IDs.

Final result: 156 passed, 0 failed (1 expected failure via test.fail).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 21:27:12 +02:00
KnackyandClaude Sonnet 4.6 e99286ef8e fix(frontend): sprint 4 post-code-review — btn-ink uses slab token + unify New CTA label
- btn-ink hover: bg-slab-hover (unnecessary new token) → bg-paper (existing token,
  same #1f2937 value in dark, avoids token sprawl)
- tailwind.config.ts: remove slab-hover token added in fc530af
- EngagementsListPage: both CTAs unified to "+ New" (≤8 chars convention, AC-17.2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:40:06 +02:00
KnackyandClaude Sonnet 4.6 988de841e5 fix(backend): sprint 4 post-review — relative paths + dead branch removal
- test_engagement_lifecycle.py, test_simulations_techniques.py: replace hardcoded
  absolute paths with Path(__file__).parent.parent / migrations/... (portable)
- simulation_workflow.py: remove dead branch in transition() — the IN_PROGRESS
  hook was unreachable since _ALLOWED_TRANSITIONS only targets review_required/done

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:39:37 +02:00
KnackyandClaude Sonnet 4.6 fc530af78b fix(frontend): post-code-review NITs — btn-ink uses @apply bg-slab, unify New engagement label
- btn-ink: replace inline background-color #111827 with @apply bg-slab (and add
  slab-hover token #1f2937 for the hover state) so the token system is consistent
- EngagementsListPage: header button "+ New" → "+ New engagement" to match
  empty-state CTA label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:39:01 +02:00
KnackyandClaude Sonnet 4.6 9964d058f4 fix(frontend): sprint 4 design-review — slab token + UsersAdmin alignment + dark hairlines + badge contrast
- bump dark hairline from #374151 → #4b5563 for visible table borders
- topbar header bg-canvas → bg-paper for dark-mode lift vs canvas body
- UsersAdminPage create-form: Option A structural 3-row grid (labels / inputs / hints)
  to fix AC-17.3 alignment; removes FormField wrapper that caused row-height misalignment
- EngagementsListPage: replace text "+ New" with lucide Plus icon per design spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:28:32 +02:00
KnackyandClaude Sonnet 4.6 892692f3b8 fix(frontend): post-design-review — slab token split + badge contrast + modal backdrop + dark shadows
- Add fixed slab/slab-text/slab-muted tokens so utility strip and footer never
  invert to near-white in dark mode (root token split: ink is themed text,
  slab is fixed dark surface)
- btn-ink uses fixed #111827 so confirm dialogs stay dark-on-dark readable
- Toast error surface switched to slab; success uses text-white (not text-ink-on)
- StatusBadge active and SimulationStatusBadge review_required/done use text-white
  instead of text-canvas/text-ink-on (prevents near-black text on colored pill
  in dark mode)
- Modal backdrops (MitreMatrixModal, ConfirmDialog) switched to .modal-backdrop
  class (fixed rgba(0,0,0,0.6)) instead of bg-ink/60 which turned near-white
- Card shadow lifted in dark mode via .dark .card-product override
- MitreMatrixModal panel uses shadow-floating-dark in dark mode
- UsersAdminPage form: items-start + explicit label-height spacer on button
  column for pixel-perfect baseline alignment (AC-17.3 structural fix)

92/92 tests passing, typecheck and lint clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:19:16 +02:00
KnackyandClaude Sonnet 4.6 f5ea9d16af feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence

92/92 tests passing, typecheck and lint clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
KnackyandClaude Sonnet 4.6 d5ab1fd26f feat(backend): sprint 4 — tactic_ids + done guard + engagement auto-status
- Simulation model: add tactic_ids JSON column (nullable=False, default=[])
- Migration 0004: ADD COLUMN tactic_ids (server_default='[]', no batch needed)
- mitre.py: add _TACTIC_IDS map, lookup_tactic(), get_tactic_name()
- simulation_workflow.py: done guard (409) before RBAC; SOC gate += tactic_ids;
  _resolve_tactic_ids() validates against hardcoded map; auto-transition += tactic_ids;
  transition done→review_required is Reopen (all 3 roles); _maybe_activate_engagement hook
- serializers.py: _enrich_tactics() → serialize_simulation adds tactics:[{id,name}]
- test_simulations_tactics.py: valid/invalid/dedup/SOC gate/auto-transition/no-bundle
- test_simulations_done_readonly.py: 409 all roles, Reopen all roles, invalid transitions, after-reopen ok
- test_engagement_lifecycle.py: planned→active on auto-transition, already active/closed unchanged, migration 0004 round-trip
- Updated test_simulations_patch.py + test_simulations_workflow.py for AC-18 behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 19:52:02 +02:00
KnackyandClaude Opus 4.7 0f6ae857b3 feat(infra): design-reviewer agent + PR helper (US-24 + US-25)
US-24 — Process hygiene UI:
- New .claude/agents/design-reviewer.md (model: opus, read-only) — visual + design-system reviewer that runs after frontend-builder and before code-reviewer. Audits alignment, DESIGN.md tokens, light/dark consistency, typo hierarchy, whitespace rhythm, responsive sanity at 1280x720, button convention, V1 a11y. Output format mirrors code-reviewer.
- Updated .claude/agents/frontend-builder.md DoD: screenshots are MANDATORY (one per feature/state introduced or modified, light+dark when theming is in scope). Hard block on "Dev server not started" — must be flagged explicitly. Screenshots feed the design-reviewer step.

US-25 — PR helper:
- scripts/open-pr.sh wraps `POST /api/v1/repos/{owner}/{repo}/pulls`. Detects host/owner/repo from `git remote get-url origin`, reads basic-auth credentials from `~/.git-credentials` (same source as `git push`, no token in env), uses jq to compose the multiline-safe payload. Validates args, prints PR URL on success, exits non-zero with the server message on failure.
- Makefile target `open-pr TITLE="..." BODY=path/to/body.md [BASE=main]` wraps the script with the same arg validation.
- README.md "Make targets" table extended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:41:34 +02:00
KnackyandClaude Opus 4.7 89eccad1eb docs(sprint-4): plan + SPEC updates (Done terminal, engagement auto, UI/UX, workflows)
- tasks/todo.md: sprint 4 plan with 9 user stories (US-17 → US-25), 9 décisions arrêtées
- SPEC.md § Fonctionnement: Done is terminal, Reopen returns to review_required (open to all roles); engagement auto-flips planned → active when any simulation hits in_progress, no auto-rollback
- SPEC.md § Référentiel MITRE: sprint 3 multi-tech + sprint 4 tactic_ids separated field
- SPEC.md § UI/UX (new): theming light/dark/system with system default, button convention (icon + ≤8-char label), modal focus trap V1
- SPEC.md § Workflows: design-reviewer inserted between frontend-builder and code-reviewer; PR via make open-pr

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:41:16 +02:00
KnackyandClaude Opus 4.7 ba313a3880 docs(spec): carry over sprint 3 SPEC update missed in PR #6
The sprint 3 plan §0 updated SPEC.md § Simulation to reflect multi-techniques
(plural + autocomplete + matrix modal + sub-techniques). That edit sat in the
sprint 3 worktree but was never committed, so PR #6 merged the multi-tech
code without the corresponding spec text. Applying it here at the start of
sprint 4 so SPEC and main are aligned again.

Lesson captured in tasks/lessons.md for sprint 4 wrap-up: always
git status before declaring sprint complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:14:25 +02:00
KnackyandClaude Opus 4.7 b001f57774 docs: sprint 3 wrap-up — README + CHANGELOG + lessons + plan final
- README: status bump to sprint 3, test counts refreshed (164/86/105), IPv6 note for the e2e runner
- CHANGELOG: sprint 3 entry under [Unreleased] (multi-tech model + matrix endpoint + auto-save UI); sprint 2 moved to its own [Sprint 2] section (merged 2026-05-27)
- tasks/lessons.md: 6 lessons captured (2-pass spec-review, inline summary scoping, "test in brief means test in commit" discipline, SQLite batch_alter_table, real migration round-trip, modal Apply 0 disambiguation)
- tasks/todo.md: status flipped to 🟢 SPRINT COMPLET, execution sequence ticks updated with commit hashes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 04:55:12 +02:00
KnackyandClaude Sonnet 4.6 df8a6b605b test(e2e): sprint 3 acceptance tests — US-13 to US-16
Four new spec files covering the MITRE multi-technique feature:
- us13: API contract (techniques array, dedup, unknown ID → 400, SOC 403, auto-transition)
- us14: tag UI (empty state, add/remove auto-save, SimulationList column, order, styling)
- us15: matrix modal (tactic tree, layout, select/expand/search, Apply/Cancel/Escape/backdrop, a11y)
- us16: sprint 2 regression (workflow, badge, SOC RBAC, picker still works)

Updated sprint 2 specs (us8, us10) to use technique_ids array and Quick search button
instead of deprecated scalar mitre_technique_id/name fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:51:34 +02:00
Knacky 393b6ed416 fix(backend): sprint 3 post-review — migration nullable + dead code + tactic names + tests
- Migration 0003: enforce techniques NOT NULL via batch_alter_table (AC-13.1 DDL spec)
- Migration 0003: remove unused _sims table proxy and orphaned column/table imports
- mitre.py: rename _TACTIC_NAMES → TACTIC_NAMES (public); add all 12 correct display names
- mitre.py: use TACTIC_NAMES dict in _build_matrix() to fix "Command And Control" → "Command and Control"
- test_mitre.py: add T1071 fixture entry under command-and-control; assert tactic_name lowercase "and"
- test_simulations_techniques.py: real Alembic round-trip test asserting techniques NOT NULL after upgrade
2026-05-27 04:31:10 +02:00
Knacky 4596f09e71 fix(backend): sprint 3 post-review — nullable migration + dead code + tactic names
- Migration 0003: enforce techniques NOT NULL via batch_alter_table
- Migration 0003: remove unused _sims table proxy and dead column/table imports
- mitre.py: add _TACTIC_NAMES dict to fix 'Command And Control' → 'Command and Control'
2026-05-27 04:25:20 +02:00
KnackyandClaude Sonnet 4.6 39f4076a81 fix(frontend): sprint 3 post-review — real dedup test + Apply 0 guard + Link stopPropagation
- MitreTechniquesField test: rewrite dedup test to actually exercise picker
  selection path — types query, waits for option, fires pointerDown,
  asserts no PATCH sent (dedup guard in handleSelect now truly covered)
- MitreMatrixModal: Apply button disabled only when totalSelected === 0
  AND initialSelection.length === 0 (no-op case); when totalSelected === 0
  but initialSelection was non-empty, shows "Clear all" and stays enabled
  so user can explicitly wipe the list
- MitreMatrixModal tests: update disabled test to match "Clear all" label,
  add "Clear all" enabled + onApply([]) path test
- SimulationList: stopPropagation on Name <Link> to prevent double-navigate
  with row onClick handler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:23:46 +02:00
KnackyandClaude Sonnet 4.6 771483f3b0 feat(frontend): sprint 3 — multi-technique MITRE selection + matrix modal
- types: replace mitre_technique_id/name scalars with techniques:MitreTechnique[]
  on Simulation; add MitreTactic/MitreMatrixTechnique/MitreMatrixSubtechnique;
  SimulationPatchInput now uses technique_ids:string[]
- api/mitre.ts: add getMitreMatrix() → GET /api/mitre/matrix
- hooks/useMitre: add useMitreMatrix(enabled) with staleTime:Infinity
- MitreTechniquePicker: clean rewrite — onSelect(technique) one-shot, resets
  input after selection, no incoming value props
- MitreTechniqueTag: chip component with id+name and × remove button
- MitreMatrixModal: tactic columns (220px fixed), expand/collapse subtechniques,
  search filter (auto-expands parent on sub match), selection state, focus trap
  (Tab wrap, Escape, search autofocus), backdrop click cancel, Apply N techniques
- MitreTechniquesField: orchestrates tags+picker+matrix with auto-save PATCH on
  every add/remove/Apply, dedup guard, disabled read-only mode for SOC
- SimulationFormPage: swap MitreTechniquePicker for MitreTechniquesField; remove
  technique state from RT form (techniques have independent auto-save cycle)
- SimulationList: MITRE column → T1059 +2 counter format, — when empty
- Tests: 84 passing (13 test files); new suites for Tag, Field, Modal;
  MitreTechniquePicker + SimulationFormPage + SimulationList adapted to new API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:04:23 +02:00
KnackyandClaude Sonnet 4.6 673b25e0b0 fix(backend): PATCH technique_ids returns 503 when MITRE bundle not loaded
Added bundle-loaded guard in _resolve_technique_ids() before attempting any
lookup; matches behavior of GET /api/mitre/matrix and GET /api/mitre/techniques.
Added corresponding test case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 03:58:30 +02:00
KnackyandClaude Sonnet 4.6 b5ea2929de feat(backend): sprint 3 — multi-technique simulations + MITRE matrix
- Simulation model: replace mitre_technique_id/name scalars with techniques JSON column [{id, name}]
- Alembic migration 0003: add techniques, backfill from scalars, drop old columns (reversible)
- MITRE service: add get_tactics(), lookup_name(), get_matrix() with canonical tactic order and sub-technique nesting
- serializer: enrich techniques with tactics from service at serialize time (graceful empty tactics if bundle outdated)
- simulation_workflow: PATCH now accepts technique_ids list, validates against bundle, deduplicates preserving order, auto-transitions on non-empty list
- simulations API: add GET /api/mitre/matrix endpoint (503 if bundle absent)
- test_mitre.py: updated _reset_mitre fixture, added T1059.006 sub-technique, 14 new tests for get_tactics/lookup_name/get_matrix/matrix endpoint
- test_simulations_techniques.py: 20 new tests covering AC-13.1 to AC-13.5 (create, PATCH, dedup, auto-transition, SOC blocked, migration backfill logic)

Total: 161 tests passing. ruff clean. mypy: no new errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 03:56:02 +02:00
KnackyandClaude Opus 4.7 ddf48dd1d1 docs(changelog): post-QA fixes summary + e2e suite now fully green
- Document the 4 post-QA fixes (i18n FR→EN, password field alignment,
  execution_result TextArea, unified sticky action bar)
- Update the e2e suite status: 68/68 passing on both docker and podman
  (sprint 1 us1/us6 failures resolved by b3124ba's auto-detect that also
  landed in those specs)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:14:37 +02:00
KnackyandClaude Sonnet 4.6 da2ce68660 test(e2e): align assertions with i18n fix — French→English labels
- us7: "Nouvelle simulation" → "New simulation" (3 assertions)
- us4: "Nouvelle simulation" → "New simulation" (1 assertion)
- us9: "Simulation pas encore en revue" → "Simulation not yet ready for review" (1 assertion)
- us11: "Marquer en revue" → "Mark for review" (6 assertions), "Clôturer" → /^close$/i (7 assertions)
- us12: "Supprimer" → /^delete$/i (4 assertions), "Supprimer la simulation" → "Delete simulation" (1 assertion)

No other French strings found in e2e/tests/. Suite: 68/68 pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:13:33 +02:00
KnackyandClaude Sonnet 4.6 2a7d27bf02 fix(frontend): post-QA sprint 2 — i18n + alignment + textarea + action bar layout
- Translate all remaining French strings to English (toasts, buttons, banner)
- Fix UsersAdminPage create-form grid alignment: items-start + self-end on button wrapper
- Change execution_result from TextInput to TextArea (5 rows, multiline)
- Replace split Save RT / Save SOC footers + workflow div with a single sticky
  action bar (Save Red Team | Save SOC | Mark for review | Close | Delete)
- Update Vitest assertions to use English button labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:08:46 +02:00
KnackyandClaude Opus 4.7 b3124ba4dd fix(make): auto-detect docker/podman so Makefile works on either engine
- Makefile: introduce CONTAINER_CMD ?= $(shell command -v docker || echo podman),
  replace all 12 hardcoded `docker` invocations with $(CONTAINER_CMD). User can
  override with `make <target> CONTAINER_CMD=podman` or env export.
- e2e/tests/us1-bootstrap-admin.spec.ts: AC-1.4 regex updated to match the new
  variable form `$(CONTAINER_CMD) exec … flask create-admin` (was hardcoded
  `docker exec`). RUNTIME default also auto-detects (same logic as Makefile)
  so the test exec'es the right engine without a MIMIC_CONTAINER_CMD export.
- e2e/tests/us6-deployment.spec.ts: same RUNTIME auto-detect so the make-dry-run
  regex assertions on lines 75 + 77 match what the Makefile actually emits on
  a podman-only host.
- README + CHANGELOG document the new behavior.

Fixes the user-reported issue: "Le makefile ne fonctionne pas sur ma machine
qui n'a que podman."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:20:29 +02:00
KnackyandClaude Opus 4.7 9ace9ac0d8 docs: sprint 2 wrap-up — README + CHANGELOG + lessons + plan final
- README: status bump to sprint 2, blueprints + workflow + MITRE section, test counts refreshed (131/63/68)
- CHANGELOG: sprint 2 entry under [Unreleased]; sprint 1 moved to its own [Sprint 1] section
- tasks/lessons.md: 5 lessons captured (3 frontend testing gotchas, agent-reuse via SendMessage, e2e refresh on placeholder supersession)
- tasks/todo.md: status flipped to 🟢 SPRINT COMPLET, execution sequence ticks updated with commit hashes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:41:23 +02:00
KnackyandClaude Sonnet 4.6 54e90f78bb test(e2e): refresh us4 AC-4.9 — placeholder replaced by SimulationList (sprint 2)
The sprint 2 SimulationList component replaced the "Simulations à venir au
Sprint 2" placeholder. AC-4.9 now asserts the Simulations heading and the
"Nouvelle simulation" button are visible for redteam, in line with AC-7.5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:37:51 +02:00