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>
.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>
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>
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>
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>
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>
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>
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).
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>
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>
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>
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>
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>
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>
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).
- 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.
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.
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>
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>
- 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>
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.
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.
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."