Compare commits

...

3 Commits

Author SHA1 Message Date
Knacky
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
Knacky
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
4 changed files with 140 additions and 154 deletions

View File

@@ -2,7 +2,7 @@
Mimic is a **BAS (Breach and Attack Simulation) purple-team console** — not a product catalog, not a marketing page. The aesthetic is **Bloomberg Terminal / SOC dashboard**: dense, angular, semantic-color-driven, zero ornament. Every surface decision reinforces operational trust: data is primary, chrome is invisible. Mimic is a **BAS (Breach and Attack Simulation) purple-team console** — not a product catalog, not a marketing page. The aesthetic is **Bloomberg Terminal / SOC dashboard**: dense, angular, semantic-color-driven, zero ornament. Every surface decision reinforces operational trust: data is primary, chrome is invisible.
The system sits on a **pure-white canvas** (light) / **dark slab** (dark) with one chromatic action color — **Electric Blue** (`{colors.primary}``#024ad8`) — and semantic status signals (`success`, `warn`, `bloom-deep`). Inter handles body/headers/labels. JetBrains Mono carries data: IDs, ISO dates, commands, execution output, MITRE technique codes, metrics — anything that must be read at a glance without typographic distraction. The system sits on a **pale-tinted canvas** (light: `#f3f5f8`) / **dark slab** (dark) with one chromatic action color — **Electric Blue** (`{colors.primary}``#024ad8`) — and semantic status signals (`success`, `warn`, `bloom-deep`). Inter handles body/headers/labels. JetBrains Mono carries data: IDs, ISO dates, commands, execution output, MITRE technique codes, metrics — anything that must be read at a glance without typographic distraction.
**No rounded corners on containers.** No shadows on interactive surfaces. No transitions. Hover is instantaneous. Focus rings are sharp. This is a tool, not a storefront. **No rounded corners on containers.** No shadows on interactive surfaces. No transitions. Hover is instantaneous. Focus rings are sharp. This is a tool, not a storefront.
@@ -25,7 +25,7 @@ The system sits on a **pure-white canvas** (light) / **dark slab** (dark) with o
- **Soft Blue** (`{colors.primary-soft}``#c9e0fc`): selection highlight, chip background on light surfaces. - **Soft Blue** (`{colors.primary-soft}``#c9e0fc`): selection highlight, chip background on light surfaces.
### Surface ### Surface
- **Canvas** (`{colors.canvas}``#ffffff` light / `#111827` dark): universal page background. - **Canvas** (`{colors.canvas}``#f3f5f8` light / `#111827` dark): universal page background. In light mode, canvas is tinted while paper stays pure white so cards lift without shadow or radius, preserving brutalism.
- **Paper** (`{colors.paper}``#ffffff` light / `#1f2937` dark): card and panel surfaces. - **Paper** (`{colors.paper}``#ffffff` light / `#1f2937` dark): card and panel surfaces.
- **Cloud** (`{colors.cloud}``#f7f7f7` light / `#1f2937` dark): alternating section band, table row zebra. - **Cloud** (`{colors.cloud}``#f7f7f7` light / `#1f2937` dark): alternating section band, table row zebra.
- **Fog** (`{colors.fog}``#e8e8e8` light / `#374151` dark): secondary section band, input background on dense panels. - **Fog** (`{colors.fog}``#e8e8e8` light / `#374151` dark): secondary section band, input background on dense panels.

View File

@@ -124,7 +124,7 @@ export function EngagementFormPage(): JSX.Element {
const submitting = createMutation.isPending || patchMutation.isPending; const submitting = createMutation.isPending || patchMutation.isPending;
return ( return (
<div className="flex flex-col gap-xl max-w-2xl"> <div className="flex flex-col gap-xl">
<header> <header>
<h1 className="text-[32px] font-medium leading-none"> <h1 className="text-[32px] font-medium leading-none">
{editing ? 'Edit engagement' : 'New engagement'} {editing ? 'Edit engagement' : 'New engagement'}
@@ -136,91 +136,99 @@ export function EngagementFormPage(): JSX.Element {
</p> </p>
</header> </header>
<form onSubmit={onSubmit} noValidate className="card-product flex flex-col gap-md"> <div
<FormField label="Name" htmlFor="eng-name" required error={errors.name}> className={
<TextInput editing && canEditEngagements
id="eng-name" ? 'grid grid-cols-1 lg:grid-cols-2 gap-xl items-start'
name="name" : 'max-w-2xl'
value={form.name} }
onChange={(e) => setForm({ ...form, name: e.target.value })} >
required <form onSubmit={onSubmit} noValidate className="card-product flex flex-col gap-md">
/> <FormField label="Name" htmlFor="eng-name" required error={errors.name}>
</FormField>
<FormField label="Description" htmlFor="eng-description">
<TextArea
id="eng-description"
name="description"
value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })}
/>
</FormField>
<div className="grid grid-cols-1 md:grid-cols-2 gap-md">
<FormField
label="Start date"
htmlFor="eng-start"
required
error={errors.start_date}
>
<TextInput <TextInput
id="eng-start" id="eng-name"
type="date" name="name"
name="start_date" value={form.name}
value={form.start_date} onChange={(e) => setForm({ ...form, name: e.target.value })}
onChange={(e) => setForm({ ...form, start_date: e.target.value })}
required required
/> />
</FormField> </FormField>
<FormField <FormField label="Description" htmlFor="eng-description">
label="End date" <TextArea
htmlFor="eng-end" id="eng-description"
hint="Leave empty to clear / leave open-ended" name="description"
error={errors.end_date} value={form.description}
> onChange={(e) => setForm({ ...form, description: e.target.value })}
<TextInput
id="eng-end"
type="date"
name="end_date"
value={form.end_date}
onChange={(e) => setForm({ ...form, end_date: e.target.value })}
/> />
</FormField> </FormField>
</div>
<FormField label="Status" htmlFor="eng-status" required> <div className="grid grid-cols-1 md:grid-cols-2 gap-md">
<Select <FormField
id="eng-status" label="Start date"
name="status" htmlFor="eng-start"
value={form.status} required
onChange={(e) => setForm({ ...form, status: e.target.value as EngagementStatus })} error={errors.start_date}
options={STATUS_OPTIONS} >
/> <TextInput
</FormField> id="eng-start"
type="date"
name="start_date"
value={form.start_date}
onChange={(e) => setForm({ ...form, start_date: e.target.value })}
required
/>
</FormField>
{submitError ? ( <FormField
<div role="alert" className="text-[14px] text-bloom-deep"> label="End date"
{submitError} htmlFor="eng-end"
hint="Leave empty to clear / leave open-ended"
error={errors.end_date}
>
<TextInput
id="eng-end"
type="date"
name="end_date"
value={form.end_date}
onChange={(e) => setForm({ ...form, end_date: e.target.value })}
/>
</FormField>
</div> </div>
) : null}
<div className="flex items-center gap-md pt-sm"> <FormField label="Status" htmlFor="eng-status" required>
<button type="submit" className="btn-primary" disabled={submitting}> <Select
{submitting ? 'Saving…' : editing ? 'Save changes' : 'Create engagement'} id="eng-status"
</button> name="status"
<Link value={form.status}
to={editing && numericId ? `/engagements/${numericId}` : '/engagements'} onChange={(e) => setForm({ ...form, status: e.target.value as EngagementStatus })}
className="btn-outline-ink" options={STATUS_OPTIONS}
> />
Cancel </FormField>
</Link>
</div>
</form>
{editing && numericId && canEditEngagements && ( {submitError ? (
<C2ConfigCard engagementId={numericId} /> <div role="alert" className="text-[14px] text-bloom-deep">
)} {submitError}
</div>
) : null}
<div className="flex items-center gap-md pt-sm">
<button type="submit" className="btn-primary" disabled={submitting}>
{submitting ? 'Saving…' : editing ? 'Save changes' : 'Create engagement'}
</button>
<Link
to={editing && numericId ? `/engagements/${numericId}` : '/engagements'}
className="btn-outline-ink"
>
Cancel
</Link>
</div>
</form>
{editing && numericId && canEditEngagements && (
<C2ConfigCard engagementId={numericId} />
)}
</div>
</div> </div>
); );
} }

View File

@@ -7,7 +7,7 @@
@layer base { @layer base {
/* Light mode — default */ /* Light mode — default */
:root { :root {
--color-canvas: #ffffff; --color-canvas: #f3f5f8;
--color-paper: #ffffff; --color-paper: #ffffff;
--color-cloud: #f7f7f7; --color-cloud: #f7f7f7;
--color-fog: #e8e8e8; --color-fog: #e8e8e8;

View File

@@ -1,95 +1,73 @@
# Sprint 8C2 Layer: Mythic Integration # Sprint 9UI: engagement 2-col + global contrast pass
> Branch : `sprint/8-c2` · Worktree : `.claude/worktrees/sprint-8-c2` · Base : `main` @ `6ca614a` **Base**: `sprint/8-c2` (sprint 8 not yet merged on origin/main, but its `C2ConfigCard` is the right pane).
**Scope**: frontend-only. No backend, no schema. No new features.
SPEC.md phase 2: "après que la V1 soit terminée, nous ajouterons une couche permettant ---
de se connecter à un C2 (mythic ou maison) afin d'exécuter des simulations au travers du C2."
## §0 — Binding decisions (locked with user, 2026-06-10) ## Decisions (locked)
1. **C2 target**: Mythic 3.x, behind a thin `C2Adapter` interface (keeps the door open for a custom C2 later). 1. **Engagement page** : passer en **2 colonnes** sur desktop (`lg:grid-cols-2`), `[engagement form | C2ConfigCard]`. Mobile/tablet : stack vertical (comportement actuel).
2. **Scope**: FULL bidirectional — execute + near-real-time tracking + history import. User explicitly overrode the "one-shot first" recommendation; overrun risk accepted (see R1). 2. **Contraste global** : le problème est que `canvas` (page bg) et `paper` (card bg) sont **tous deux `#ffffff`** en light mode. Les cartes ne ressortent que par leur hairline 1px → fatigue oculaire confirmée par l'utilisateur.
3. **C2 config**: per-engagement (URL + API token). Token is write-only at the API level — never returned in clear. 3. **Fix retenu** : **tinter le canvas light** d'un neutre froid très pâle. `paper` reste blanc pur. Les cartes "lèvent" naturellement sans casser le brutalisme.
4. **UI anchor**: execution lives in the simulation screen (Red Team card). - Proposition : `canvas` light `#f3f5f8` (gris-bleu très pâle, cohérent avec l'electric blue), `paper` light `#ffffff`.
5. **Realtime mechanism**: short polling. Frontend: TanStack Query `refetchInterval` 2 500 ms while any attached task is incomplete. Backend: poll-on-read — refreshes non-completed tasks from Mythic when the task list is read. No scheduler, no new infra. - Dark mode **inchan** (`canvas #111827` / `paper #1f2937` déjà différenciés).
6. **Secret storage**: API token Fernet-encrypted in SQLite. Key from env var `MIMIC_ENCRYPTION_KEY` (mandatory to enable C2 features, never hardcoded — OPSEC rule). 4. **Pas de shadow**, pas de radius. La brutalité reste intacte — seul le contraste de surface change.
7. **History import**: BOTH — auto-attach of tasks launched from Mimic AND manual browse/select of the callback's task history. 5. **Hairline** : à vérifier sur le nouveau canvas. Si nécessaire, passer `hairline` light de la valeur actuelle à un poil plus sombre pour préserver la lisibilité du bord sur tinted canvas. Mais éviter si la lecture est déjà bonne.
8. **Validation**: fully mocked — no dev Mythic instance available. pytest uses a mocked adapter; e2e uses a built-in `FakeAdapter` selected via `MIMIC_C2_ADAPTER=fake`.
9. **RBAC**: C2 is a RedTeam resource — admin + redteam full access, SOC gets 403 on every C2 endpoint (same precedent as Templates and Export).
10. **Workflow tie-in**: launching a C2 execution auto-transitions the simulation `pending → in_progress` (same rule as a manual RT edit).
11. **Output mapping**: when a task completes (or is imported), its output is APPENDED to `execution_result` prefixed by a `$ <command>` header line; `executed_at` is set from the first task's timestamp if empty; the command is appended to `commands` if not already present.
## §1 — Mythic adapter contract (pinned from MythicMeta/Mythic_Scripting client source) ---
- Transport: POST `https://<host>:7443/graphql`, header `apitoken: <token>` (Hasura behind nginx). ## Task A — EngagementFormPage 2-col
- Issue task: mutation `createTask(callback_id: <display_id>, command, params, tasking_location: "command_line")`.
- List callbacks: query on `callback` table — `id, display_id, active, host, user, domain, last_checkin`.
- Task status: query on `task` table — `display_id, status, completed` (client lib uses a subscription; we poll the query instead, per decision 5).
- Task output: query on `response` table ordered by id — `response_text` is **base64-encoded**, decode + concatenate.
- History: query `task` filtered by `callback_display_id`, paginated, with command + status + timestamps.
`C2Adapter` interface (`backend/app/services/c2/adapter.py`): **File** : `frontend/src/pages/EngagementFormPage.tsx`
- `test_connection() -> C2Health`
- `list_callbacks() -> list[C2Callback]`
- `create_task(callback_display_id, command, params) -> int` (task display_id)
- `get_task(task_display_id) -> C2TaskStatus`
- `get_task_output(task_display_id) -> str` (decoded)
- `list_callback_tasks(callback_display_id, page, page_size) -> C2TaskPage`
Implementations: `MythicAdapter` (requests, `verify_tls` flag from config — lab instances run self-signed), `FakeAdapter` (deterministic in-memory data, selected by `MIMIC_C2_ADAPTER=fake`, also powers e2e). - Remplacer le wrapper `<div className="flex flex-col gap-xl max-w-2xl">` par un container plus large + grid 2-col responsive.
- Header reste en haut, full width.
- Body : `grid grid-cols-1 lg:grid-cols-2 gap-xl` avec :
- Col gauche : `<form>` engagement (déjà en `card-product`).
- Col droite : `<C2ConfigCard>` (seulement quand `editing && canEditEngagements`).
- Si pas en edit (création) : col droite vide → garder la grid mais que la col gauche se déploie via `lg:col-span-2` (pour pas avoir un vide à droite). Acceptable alternative : `flex` + `max-w-2xl` quand non-editing.
- Pas de modif sur la logique de form, validation, mutations.
## §2 — Backend (backend-builder) — milestones M1→M4 ## Task B — Contrast pass (tokens)
**Data model (1 Alembic migration):** **Files** :
- `c2_config`: id, engagement_id FK **unique** (`ON DELETE CASCADE`, same precedent as `simulations.engagement_id`), url, api_token_encrypted, verify_tls bool (default true), created_at, updated_at. - `DESIGN.md` § Surface : mettre à jour `canvas` light = `#f3f5f8`, conserver `paper` light = `#ffffff`. Documenter dans la même section que "canvas tints lift paper cards in light mode without violating brutalism".
- `c2_task`: id, simulation_id FK (`ON DELETE CASCADE`), mythic_task_display_id int, callback_display_id int, command text, params text nullable, status text, completed bool, output text nullable, source enum(`mimic`,`import`), created_at, completed_at nullable. - Token source de vérité (Tailwind config ou CSS vars). Localiser et appliquer la même MAJ. Probablement `frontend/tailwind.config.js` ou un `frontend/src/styles/tokens.css` / `index.css`.
- Vérifier qu'aucun composant ne hardcode `#ffffff` pour la page bg (devrait utiliser `bg-canvas`).
- Tests CSS smoke : `bg-canvas` continue de matcher, dark mode inchangé.
**Endpoints (all admin+redteam; SOC → 403):** ## Task C — Visual regression check
- `GET/PUT/DELETE /api/engagements/<id>/c2-config` — GET returns `has_token: bool`, never the token.
- `POST /api/engagements/<id>/c2-config/test` — connectivity check via adapter.
- `GET /api/engagements/<id>/c2/callbacks` — active callbacks.
- `POST /api/simulations/<id>/c2/execute` `{callback_display_id, commands: [str]}` — one Mythic task per command, rows in `c2_task` (source=mimic), auto-transition pending→in_progress.
- `GET /api/simulations/<id>/c2/tasks` — poll-on-read: refresh incomplete tasks from Mythic; on completion fetch output and apply decision 11 mapping (idempotent, applied once).
- `GET /api/engagements/<id>/c2/callbacks/<cid>/history?page=` — paginated callback history for import.
- `POST /api/simulations/<id>/c2/import` `{task_display_ids: [int]}` — import selected tasks (source=import) + decision 11 mapping.
**Milestones:** M1 crypto service (Fernet) + migration + config CRUD + test endpoint → M2 callbacks + execute → M3 poll-on-read status/output + mapping → M4 history + import. - `pnpm vitest run` clean.
**Tests:** pytest with mocked adapter (~35-45 new), zero live HTTP. Crypto round-trip, RBAC 403 matrix, mapping idempotence, migration up/down. - `pnpm tsc --noEmit` clean.
- `pnpm lint` clean.
- Screenshots avant/après (au moins) :
- EngagementsListPage (cards-on-canvas)
- EngagementDetailPage
- EngagementFormPage (edit, avec C2ConfigCard à droite)
- SimulationFormPage (déjà 2-col sprint 7, vérifier que le tinted canvas n'écrase pas)
- LoginPage
- Dark mode : passe rapide pour confirmer aucune régression.
## §3 — Frontend (frontend-builder) ---
- **Engagement detail/form**: "C2 configuration" card — URL, token (password input, write-only placeholder when `has_token`), verify-TLS checkbox, [Test connection] with result feedback. Admin+redteam only. ## Sequencing
- **SimulationFormPage RT card**: [Execute via C2] button (hidden when no config; disabled when done) → modal: callback picker table (display_id, host, user, active, last_checkin — font-mono data cells) → commands textarea prefilled from `rt.commands` → Launch.
- **C2 tasks panel** (under RT card): table of attached tasks — command (mono), status badge (semantic tokens), completed_at (mono). `refetchInterval` 2 500 ms while any incomplete; stops when all done.
- **Import history modal**: callback picker → paginated task list with checkboxes → [Import selected].
- Terminal-SOC compliance: rounded-none, zero transitions, hairline borders, mono for data only. Status colors via `success`/`warn` semantic tokens.
- Vitest ~15-20 new tests. `data-testid` on every new interactive surface for e2e.
## §4 — Docs & hygiene 1. **frontend-builder** : Task A + B + C. Une seule passe, commits atomiques.
2. **design-reviewer** : revue visuelle après merge des commits builder. Focus :
- Lecture confortable cards-on-tinted-canvas.
- Hairline encore visible.
- Dark mode inchangé.
- Pas de régression sur components qui pourraient ré-utiliser `bg-canvas` pour autre chose (dropdowns, modals).
- **SPEC.md C2 section = FIRST commit** of the sprint (sprint 5/6 lesson: never close a sprint with SPEC uncommitted). ---
- README: `MIMIC_ENCRYPTION_KEY`, `MIMIC_C2_ADAPTER` env vars + docker compose example.
- CHANGELOG sprint 8 entry at close.
## §5 — Pipeline & sequencing ## Definition of Done
1. spec-reviewer pre-pass on this plan (before any code). - EngagementFormPage en édition : 2 colonnes desktop, stack mobile.
2. backend-builder M1+M2 → API contract frozen → frontend-builder starts (parallel with backend M3+M4). - Page bg différencié de card bg en light mode (eyes confort).
3. design-reviewer on new UI surfaces (screenshots come from the Playwright run — e2e bootstraps its own users, which sidesteps the sprint-7 credentials wall). - Vitest + typecheck + lint verts.
4. **code-reviewer must be respawned** (killed 2026-06-10 after idle-loop malfunction) before the review phase. - Design-reviewer APPROVED.
5. test-verifier: new C2 Playwright specs (fake adapter) + **full 223-spec re-run** — also clears the sprint 7 e2e debt (suite never re-ran after the design refresh). - Screenshots livrés ou écueil documenté.
- Commits conventional, branche `sprint/9-ui-contrast`.
## §6 — Risks
- **R1 — scope**: full bidirectional in one sprint. Mitigation: M1→M4 are ordered; M4 (history import) is the cut line if we overrun. User accepted explicitly.
- **R2 — schema fidelity**: no live Mythic to validate against; adapter pinned to the official `MythicMeta/Mythic_Scripting` client source on **master @ 2026-06-10** (verified via raw.githubusercontent.com). First real connection may need a small patch. README records the exact source URL alongside the "may need a patch" note. Adapter must defensively handle `response_text` base64 decode failures (binary output): on `binascii.Error` keep raw bytes hex-encoded with a prefix `<binary> ` so `execution_result` never silently corrupts.
- **R3 — secret at rest**: Fernet + env key; key rotation out of scope (documented limitation).
- **R4 — polling load**: poll-on-read touches only incomplete tasks of the open simulation — bounded.
- **R5 — e2e drift**: sprint 7 redesign never re-validated by Playwright; the full re-run in §5.5 surfaces any breakage — budget fix time.
## §7 — Definition of Done
- pytest green (256 baseline on main + new), vitest green (139 baseline + new), Playwright full suite green (223 baseline + new C2 specs).
- spec-reviewer (plan) + design-reviewer (UI) + code-reviewer (diff) all APPROVED.
- SPEC.md, README.md, CHANGELOG.md updated. No secret, key or IP hardcoded anywhere.
- PR #11 opened via `make open-pr`.