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.
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).
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.
Tous renvoient 503 si MIMIC_ENCRYPTION_KEY est absent et 403 pour le rôle SOC.
Comment tester en local
exportMIMIC_ENCRYPTION_KEY=$(python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')exportMIMIC_C2_ADAPTER=fake
make build && make start
make create-admin USER=alice PASS=changeme8 # premier setup# http://127.0.0.1:5000
Scénarios :
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).
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.
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é.
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.
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).
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.
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é.
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.
## 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)
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).
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>
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>
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 main2026-06-11 10:29:19 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
C2Adaptermince (Mythic 3.x livré, porte ouverte pour un C2 maison).execution_result,executed_at,commands).MIMIC_ENCRYPTION_KEYobligatoire — guard 503 sinon) ; token jamais retourné en clair (GET /c2-configrenvoiehas_token: bool) ; URLhttps://only + hostname non-vide ;allow_redirects=Falsesur toutes les requêtes Mythic ; erreurs adapter sanitizées (C2 transport error: ConnectionErrorau lieu de fuiter le pool URL).FakeAdapterdéterministe activé parMIMIC_C2_ADAPTER=fakepour le dev local et Playwright..badge-pill-*recipes ; a11y traitée (rows cliquables clavier-accessibles avecrole="button"+aria-expanded+ focus ring).lg:grid-cols-2du sprint 7), expandable par tâche, polling visible via micro-indicateurRefreshing….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 :lg+:[engagement form | C2ConfigCard]côte-à-côte, stack vertical sur mobile. La carte C2 livrée au sprint 8 prend sa vraie place.--color-canvaspasse de#ffffffà#f3f5f8(gris-bleu très pâle),--color-paperreste#ffffff. Les cartes "lèvent" sans shadow ni radius. Brutalisme intact (DESIGN.md L28 + L134 alignées).bg-canvas→bg-papercorrigé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 matriceMitreMatrixModal:273est volontairement gardée enbg-canvas(sémantique "cellule vide sur canvas" intentionnel).#111827/ paper#1f2937déjà différenciés).Design-reviewer 2 passes APPROVED. Aucun changement backend.
Test plan
ruff+mypyclean).root/rootpass8.Endpoints livrés
/api/engagements/<id>/c2-config/api/engagements/<id>/c2-config/test/api/engagements/<id>/c2/callbacks/api/engagements/<id>/c2/callbacks/<cid>/history?page&page_size/api/simulations/<id>/c2/execute/api/simulations/<id>/c2/tasks/api/simulations/<id>/c2/importTous renvoient 503 si
MIMIC_ENCRYPTION_KEYest absent et 403 pour le rôle SOC.Comment tester en local
Scénarios :
https://lab.mythic:7443+ un token quelconque (l'adapterfakene valide pas),Save→Test connection(vert avec FakeAdapter).[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.submitteddeviennentcompletedau 2e refetch (FakeAdapter). Le micro-labelRefreshing…est présent pendant le polling, disparaît quand tout est complété.execution_resultau format$ <command>\n<output>\n,executed_atreç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.[Import C2 history]→ picker callback → modale step 2 avec liste paginée (25 par page) → cocher 2 lignes →Import selected. ToastImported 2 task(s)(ouImported 1 task(s), 1 already attachedsi une était déjà rattachée).curl -H "Authorization: Bearer <SOC_TOKEN>" http://127.0.0.1:5000/api/engagements/1/c2/callbacks→403.MIMIC_ENCRYPTION_KEY, l'admin reçoit503 {"error":"C2 disabled: MIMIC_ENCRYPTION_KEY not set"}sur tous les endpoints C2 et la carte de config affiche son banner désactivé./c2/tasksplusieurs fois ne réapplique JAMAIS le mapping (la colonnemapping_appliedsert d'ancre, vérifiée par les 19 teststest_c2_mapping.py).Notes
c2_config,c2_task) en 1 migration atomique (0006), + ajoutmapping_applieden 0007 séparée pour le suivi d'idempotence. Cascade-delete depuisengagementsetsimulations(cohérent avec le pattern sprint 2).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é).MIMIC_C2_ADAPTER=mythic|fake.fakeest déterministe : 3 callbacks (display_id 1/2/3), 12 / 0 / 5 tasks historiques respectivement, transitions submitted→completed au 2e appel.🤖 Generated with Claude Code
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>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>