feat(c2): integrate Mythic command and control (sprint 8) #11

Merged
knacky merged 16 commits from sprint/8-c2 into main 2026-06-11 10:29:19 +00:00
Owner

Summary

  • Intégration Mythic C2 — phase 2 du SPEC.md : exécution des simulations à travers un Command & Control distant. Toute la couche derrière une interface C2Adapter mince (Mythic 3.x livré, porte ouverte pour un C2 maison).
  • Périmètre full bidirectionnel : config par engagement (chiffrée), exécution depuis la simulation, suivi quasi-temps réel (polling court 2 500 ms qui s'arrête tout seul), import historique (auto + manuel), mapping de sortie vers la simulation (execution_result, executed_at, commands).
  • OPSEC : token Mythic chiffré Fernet (env var MIMIC_ENCRYPTION_KEY obligatoire — guard 503 sinon) ; token jamais retourné en clair (GET /c2-config renvoie has_token: bool) ; URL https:// only + hostname non-vide ; allow_redirects=False sur toutes les requêtes Mythic ; erreurs adapter sanitizées (C2 transport error: ConnectionError au lieu de fuiter le pool URL).
  • RBAC RT-only : admin + redteam ont tout l'accès, SOC = 403 sur tous les endpoints C2 (précédent Templates / Export).
  • Validation 100 % mockée : pytest avec adapter mocké (aucune requête HTTP live) + FakeAdapter déterministe activé par MIMIC_C2_ADAPTER=fake pour le dev local et Playwright.
  • Design terminal-SOC appliqué aux nouvelles surfaces UI : tokens semantic, mono data-only, rounded-none, hairline borders, badges status reusing .badge-pill-* recipes ; a11y traitée (rows cliquables clavier-accessibles avec role="button" + aria-expanded + focus ring).
  • Layout simulation : nouveau panneau C2TasksPanel sous la carte Red Team (colonne gauche de la grille lg:grid-cols-2 du sprint 7), expandable par tâche, polling visible via micro-indicateur Refreshing….

Sprint 9 — UI polish bundlé dans cette PR

5 commits additionnels (a9fe2fc8b5b5d9) qui ajustent le confort visuel mis en évidence par l'intégration C2 :

  • EngagementFormPage en 2 colonnes sur lg+ : [engagement form | C2ConfigCard] côte-à-côte, stack vertical sur mobile. La carte C2 livrée au sprint 8 prend sa vraie place.
  • Canvas teinté en light mode : --color-canvas passe de #ffffff à #f3f5f8 (gris-bleu très pâle), --color-paper reste #ffffff. Les cartes "lèvent" sans shadow ni radius. Brutalisme intact (DESIGN.md L28 + L134 alignées).
  • Dette bg-canvasbg-paper corrigée sur 7 sites où le canvas servait de proxy pour paper avant la différenciation : .text-input, .btn-outline, .btn-outline-ink, et 4 surfaces flottantes (MitreTechniquePicker:110, SimulationList:96, ExportEngagementButton:77, MitreMatrixModal:173). La cellule de matrice MitreMatrixModal:273 est volontairement gardée en bg-canvas (sémantique "cellule vide sur canvas" intentionnel).
  • Dark mode strictement intouché (canvas #111827 / paper #1f2937 déjà différenciés).

Design-reviewer 2 passes APPROVED. Aucun changement backend.

Test plan

  • Backend : 468 / 468 pytest (ruff + mypy clean).
  • Frontend : 212 / 212 vitest (typecheck + lint clean).
  • E2E Playwright : à valider en local — la team Playwright n'a pas pu être run en sandbox (mur de credentials Vite proxy ↔ port). Les fixtures existantes utilisent l'admin bootstrap root / rootpass8.

Endpoints livrés

Méthode Path Rôle
GET/PUT/DELETE /api/engagements/<id>/c2-config admin, redteam
POST /api/engagements/<id>/c2-config/test admin, redteam
GET /api/engagements/<id>/c2/callbacks admin, redteam
GET /api/engagements/<id>/c2/callbacks/<cid>/history?page&page_size admin, redteam
POST /api/simulations/<id>/c2/execute admin, redteam
GET /api/simulations/<id>/c2/tasks admin, redteam
POST /api/simulations/<id>/c2/import admin, redteam

Tous renvoient 503 si MIMIC_ENCRYPTION_KEY est absent et 403 pour le rôle SOC.

Comment tester en local

export MIMIC_ENCRYPTION_KEY=$(python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')
export MIMIC_C2_ADAPTER=fake
make build && make start
make create-admin USER=alice PASS=changeme8         # premier setup
# http://127.0.0.1:5000

Scénarios :

  1. Config C2 (admin/redteam) — sur un engagement en édition, panneau "C2 configuration" → renseigner https://lab.mythic:7443 + un token quelconque (l'adapter fake ne valide pas), SaveTest connection (vert avec FakeAdapter).
  2. Exécution depuis simulation — sur une simulation pending, [Execute via C2] → modale picker de 3 callbacks fake (1, 2, 3) → sélection ligne → 1-3 commandes en textarea → Launch. La simulation passe automatiquement à in_progress. Les tâches apparaissent dans le panneau C2 tasks dessous.
  3. Polling visible — sur la même simulation, les tâches submitted deviennent completed au 2e refetch (FakeAdapter). Le micro-label Refreshing… est présent pendant le polling, disparaît quand tout est complété.
  4. Output mapping — quand une tâche se complete, son output remplit execution_result au format $ <command>\n<output>\n, executed_at reçoit le timestamp si vide, et la commande est appendée à commands (dédupliquée). Vérifier dans l'export Markdown sprint 6 que ces champs sortent proprement.
  5. Import historique[Import C2 history] → picker callback → modale step 2 avec liste paginée (25 par page) → cocher 2 lignes → Import selected. Toast Imported 2 task(s) (ou Imported 1 task(s), 1 already attached si une était déjà rattachée).
  6. RBAC SOC — login en SOC sur une simulation : aucun bouton C2 visible, et curl -H "Authorization: Bearer <SOC_TOKEN>" http://127.0.0.1:5000/api/engagements/1/c2/callbacks403.
  7. 503 guard — sans MIMIC_ENCRYPTION_KEY, l'admin reçoit 503 {"error":"C2 disabled: MIMIC_ENCRYPTION_KEY not set"} sur tous les endpoints C2 et la carte de config affiche son banner désactivé.
  8. Idempotence — re-poller /c2/tasks plusieurs fois ne réapplique JAMAIS le mapping (la colonne mapping_applied sert d'ancre, vérifiée par les 19 tests test_c2_mapping.py).

Notes

  • Modèle de données : 2 nouvelles tables (c2_config, c2_task) en 1 migration atomique (0006), + ajout mapping_applied en 0007 séparée pour le suivi d'idempotence. Cascade-delete depuis engagements et simulations (cohérent avec le pattern sprint 2).
  • Contract Mythic pinné : MythicMeta/Mythic_Scripting master @ 2026-06-10 — pas d'instance live disponible, premier branchement réel pourra nécessiter un petit patch (R2 du plan, documenté).
  • Adapter switch via MIMIC_C2_ADAPTER=mythic|fake. fake est déterministe : 3 callbacks (display_id 1/2/3), 12 / 0 / 5 tasks historiques respectivement, transitions submitted→completed au 2e appel.
  • Process : SPEC.md committée en commit #1 du sprint (la recurrence "SPEC.md uncommitted at close" tient depuis sprint 6). spec-reviewer pre-pass APPROVED. design-reviewer 2-pass APPROVED. code-reviewer agent a échoué (boucle idle, même pattern que la précédente instance), code-review faite directement par team-lead — 1 MAJOR (mapping.py §0.11 incomplet) + 2 MINOR (URL leak, assignment redondant) corrigés.
  • Mur de credentials : Playwright et screenshots dev server bloqués par le port Vite proxy 5000 vs backend container 5001. Validation visuelle déléguée à toi avant merge.

🤖 Generated with Claude Code

## Summary - **Intégration Mythic C2** — phase 2 du SPEC.md : exécution des simulations à travers un Command & Control distant. Toute la couche derrière une interface `C2Adapter` mince (Mythic 3.x livré, porte ouverte pour un C2 maison). - **Périmètre full bidirectionnel** : config par engagement (chiffrée), exécution depuis la simulation, suivi quasi-temps réel (polling court 2 500 ms qui s'arrête tout seul), import historique (auto + manuel), mapping de sortie vers la simulation (`execution_result`, `executed_at`, `commands`). - **OPSEC** : token Mythic chiffré Fernet (env var `MIMIC_ENCRYPTION_KEY` obligatoire — guard 503 sinon) ; token jamais retourné en clair (`GET /c2-config` renvoie `has_token: bool`) ; URL `https://` only + hostname non-vide ; `allow_redirects=False` sur toutes les requêtes Mythic ; erreurs adapter sanitizées (`C2 transport error: ConnectionError` au lieu de fuiter le pool URL). - **RBAC RT-only** : admin + redteam ont tout l'accès, SOC = 403 sur tous les endpoints C2 (précédent Templates / Export). - **Validation 100 % mockée** : pytest avec adapter mocké (aucune requête HTTP live) + `FakeAdapter` déterministe activé par `MIMIC_C2_ADAPTER=fake` pour le dev local et Playwright. - **Design terminal-SOC** appliqué aux nouvelles surfaces UI : tokens semantic, mono data-only, rounded-none, hairline borders, badges status reusing `.badge-pill-*` recipes ; a11y traitée (rows cliquables clavier-accessibles avec `role="button"` + `aria-expanded` + focus ring). - **Layout simulation** : nouveau panneau C2TasksPanel sous la carte Red Team (colonne gauche de la grille `lg:grid-cols-2` du sprint 7), expandable par tâche, polling visible via micro-indicateur `Refreshing…`. ## Sprint 9 — UI polish bundlé dans cette PR 5 commits additionnels (`a9fe2fc` → `8b5b5d9`) qui ajustent le confort visuel mis en évidence par l'intégration C2 : - **EngagementFormPage en 2 colonnes** sur `lg+` : `[engagement form | C2ConfigCard]` côte-à-côte, stack vertical sur mobile. La carte C2 livrée au sprint 8 prend sa vraie place. - **Canvas teinté en light mode** : `--color-canvas` passe de `#ffffff` à `#f3f5f8` (gris-bleu très pâle), `--color-paper` reste `#ffffff`. Les cartes "lèvent" sans shadow ni radius. Brutalisme intact (DESIGN.md L28 + L134 alignées). - **Dette `bg-canvas` → `bg-paper` corrigée** sur 7 sites où le canvas servait de proxy pour paper avant la différenciation : `.text-input`, `.btn-outline`, `.btn-outline-ink`, et 4 surfaces flottantes (`MitreTechniquePicker:110`, `SimulationList:96`, `ExportEngagementButton:77`, `MitreMatrixModal:173`). La cellule de matrice `MitreMatrixModal:273` est volontairement gardée en `bg-canvas` (sémantique "cellule vide sur canvas" intentionnel). - **Dark mode strictement intouché** (canvas `#111827` / paper `#1f2937` déjà différenciés). Design-reviewer 2 passes APPROVED. Aucun changement backend. ## Test plan - **Backend** : **468 / 468** pytest (`ruff` + `mypy` clean). - **Frontend** : **212 / 212** vitest (typecheck + lint clean). - **E2E Playwright** : à valider en local — la team Playwright n'a pas pu être run en sandbox (mur de credentials Vite proxy ↔ port). Les fixtures existantes utilisent l'admin bootstrap `root` / `rootpass8`. ## Endpoints livrés | Méthode | Path | Rôle | |---|---|---| | GET/PUT/DELETE | `/api/engagements/<id>/c2-config` | admin, redteam | | POST | `/api/engagements/<id>/c2-config/test` | admin, redteam | | GET | `/api/engagements/<id>/c2/callbacks` | admin, redteam | | GET | `/api/engagements/<id>/c2/callbacks/<cid>/history?page&page_size` | admin, redteam | | POST | `/api/simulations/<id>/c2/execute` | admin, redteam | | GET | `/api/simulations/<id>/c2/tasks` | admin, redteam | | POST | `/api/simulations/<id>/c2/import` | admin, redteam | Tous renvoient **503** si `MIMIC_ENCRYPTION_KEY` est absent et **403** pour le rôle SOC. ## Comment tester en local ```bash export MIMIC_ENCRYPTION_KEY=$(python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())') export MIMIC_C2_ADAPTER=fake make build && make start make create-admin USER=alice PASS=changeme8 # premier setup # http://127.0.0.1:5000 ``` Scénarios : 1. **Config C2 (admin/redteam)** — sur un engagement en édition, panneau "C2 configuration" → renseigner `https://lab.mythic:7443` + un token quelconque (l'adapter `fake` ne valide pas), `Save` → `Test connection` (vert avec FakeAdapter). 2. **Exécution depuis simulation** — sur une simulation pending, `[Execute via C2]` → modale picker de 3 callbacks fake (1, 2, 3) → sélection ligne → 1-3 commandes en textarea → `Launch`. La simulation passe automatiquement à `in_progress`. Les tâches apparaissent dans le panneau C2 tasks dessous. 3. **Polling visible** — sur la même simulation, les tâches `submitted` deviennent `completed` au 2e refetch (FakeAdapter). Le micro-label `Refreshing…` est présent pendant le polling, disparaît quand tout est complété. 4. **Output mapping** — quand une tâche se complete, son output remplit `execution_result` au format `$ <command>\n<output>\n`, `executed_at` reçoit le timestamp si vide, et la commande est appendée à `commands` (dédupliquée). Vérifier dans l'export Markdown sprint 6 que ces champs sortent proprement. 5. **Import historique** — `[Import C2 history]` → picker callback → modale step 2 avec liste paginée (25 par page) → cocher 2 lignes → `Import selected`. Toast `Imported 2 task(s)` (ou `Imported 1 task(s), 1 already attached` si une était déjà rattachée). 6. **RBAC SOC** — login en SOC sur une simulation : aucun bouton C2 visible, et `curl -H "Authorization: Bearer <SOC_TOKEN>" http://127.0.0.1:5000/api/engagements/1/c2/callbacks` → `403`. 7. **503 guard** — sans `MIMIC_ENCRYPTION_KEY`, l'admin reçoit `503 {"error":"C2 disabled: MIMIC_ENCRYPTION_KEY not set"}` sur tous les endpoints C2 et la carte de config affiche son banner désactivé. 8. **Idempotence** — re-poller `/c2/tasks` plusieurs fois ne réapplique JAMAIS le mapping (la colonne `mapping_applied` sert d'ancre, vérifiée par les 19 tests `test_c2_mapping.py`). ## Notes - **Modèle de données** : 2 nouvelles tables (`c2_config`, `c2_task`) en 1 migration atomique (0006), + ajout `mapping_applied` en 0007 séparée pour le suivi d'idempotence. Cascade-delete depuis `engagements` et `simulations` (cohérent avec le pattern sprint 2). - **Contract Mythic pinné** : `MythicMeta/Mythic_Scripting master @ 2026-06-10` — pas d'instance live disponible, premier branchement réel pourra nécessiter un petit patch (R2 du plan, documenté). - **Adapter switch** via `MIMIC_C2_ADAPTER=mythic|fake`. `fake` est déterministe : 3 callbacks (display_id 1/2/3), 12 / 0 / 5 tasks historiques respectivement, transitions submitted→completed au 2e appel. - **Process** : SPEC.md committée en commit #1 du sprint (la recurrence "SPEC.md uncommitted at close" tient depuis sprint 6). spec-reviewer pre-pass APPROVED. design-reviewer 2-pass APPROVED. code-reviewer agent a échoué (boucle idle, même pattern que la précédente instance), code-review faite directement par team-lead — 1 MAJOR (mapping.py §0.11 incomplet) + 2 MINOR (URL leak, assignment redondant) corrigés. - **Mur de credentials** : Playwright et screenshots dev server bloqués par le port Vite proxy 5000 vs backend container 5001. Validation visuelle déléguée à toi avant merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
knacky added 11 commits 2026-06-10 18:33:05 +00:00
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).
- 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>
- 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>
- 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>
- 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>
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>
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>
- 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>
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>
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>
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>
knacky added 5 commits 2026-06-11 09:56:53 +00:00
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>
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>
.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>
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>
knacky merged commit 24d4a3b146 into main 2026-06-11 10:29:19 +00:00
knacky deleted branch sprint/8-c2 2026-06-11 10:29:19 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: knacky/mimic#11