docs(sprint-11): plan spectrum UX port — 4 primitives + compact density global
This commit is contained in:
227
tasks/todo.md
227
tasks/todo.md
@@ -1,115 +1,164 @@
|
|||||||
# Sprint 10 — C2 TLS verify: redteam-friendly defaults + warning suppression
|
# Sprint 11 — Spectrum UX port : 4 primitives + compact density global
|
||||||
|
|
||||||
**Base**: `origin/main` (PR #11 merged — sprint 8 + 9 are in).
|
**Base** : `origin/main` (PR #12 mergée — sprint 8 + 9 + 10 en).
|
||||||
**Branch**: `sprint/10-c2-tls-default`.
|
**Branch** : `sprint/11-spectrum-ux`.
|
||||||
**Scope**: tiny correctness sprint. No new feature. Flip a security-relevant default to match the actual operator usage of this tool.
|
**Scope** : frontend-only sprint, 5 livrables coordonnés. Pas de backend, pas de schéma, pas de nouvelle API. Pas de modif tokens couleurs/spacing/font dans tailwind.config.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Symptom (user report)
|
## Contexte
|
||||||
|
|
||||||
> "Impossible de se connecter à mon C2 à cause de la vérification du certificat SSL (self signed)"
|
Analyse Spectrum (`/home/user/Documents/01_Projects/spectrum`) faite via workflow. Résultat : Spectrum n'a **pas de design system** (Tailwind raw + violet ramp + recipes inlinées 30× partout). Mimic est structurellement meilleur. On ne porte pas la philosophie Spectrum — on extrait **5 patterns isolés** qui survivent au brutalisme Mimic.
|
||||||
|
|
||||||
Operator opens C2 config card, fills URL + token, hits **Test connection** without noticing the checkbox → SSL VERIFY FAILED against self-signed Mythic.
|
## Constraints absolues
|
||||||
|
|
||||||
## Root cause (workflow diagnosis confirmed)
|
1. **Primary `#024ad8` (Electric Blue) reste**. Pas de violet, pas de spectrum ramp.
|
||||||
|
2. **Brutalisme reste** : `rounded-none` sur containers (sauf status pills + tab count pills + avatars), zero `transition-*`, zero `shadow-*`, hairline 1px borders.
|
||||||
|
3. **Pas de modif `tailwind.config.*`** sauf si une nouvelle classe utility est strictement nécessaire (justifier dans le PR).
|
||||||
|
4. **DESIGN.md amendments additives uniquement** — pas de modif des tokens existants.
|
||||||
|
5. **Mono uniquement pour data** (IDs MITRE, dates ISO, commands, etc.) — rule sprint 7.
|
||||||
|
|
||||||
The `verify_tls` chain is **fully intact** end-to-end :
|
## Décisions binding (lockées par user)
|
||||||
React `verifyTls` state → `C2ConfigInput.verify_tls` → PUT body → `C2Config.verify_tls` column → `cfg.verify_tls` in API loader → `get_adapter(verify_tls=)` → `MythicAdapter._verify` → `requests.post(verify=self._verify)`.
|
|
||||||
|
|
||||||
The bug is the **default** at every layer is `True` :
|
- **Q1 → B** : Tabs primitive **+ consumer EngagementDetailPage** (3 tabs : Schedule / Description / Simulations au lieu du stack vertical actuel).
|
||||||
- `backend/app/models/c2_config.py:22` — `default=True`
|
- **Q2 → B** : **Compact density GLOBAL** — toutes les tables passent à 32px row, pas opt-in. User accepte le tradeoff WCAG (touch target) sur Engagements list.
|
||||||
- `backend/migrations/versions/0006_c2_layer.py:29` — `server_default=sa.true()`
|
- **Q3 → A** : AlertBanner + refactor `SimulationFormPage` (Done banner + SOC-blocked banner).
|
||||||
- `backend/app/api/c2.py:87` — `data.get("verify_tls", True)`
|
|
||||||
- `backend/app/services/c2/mythic.py:116` — `verify_tls: bool = True`
|
|
||||||
- `backend/app/services/c2/factory.py:9` — `verify_tls=True`
|
|
||||||
- `frontend/src/components/C2ConfigCard.tsx:27` — `useState(true)`
|
|
||||||
- `frontend/src/components/C2ConfigCard.tsx:74` — reset on delete `setVerifyTls(true)`
|
|
||||||
|
|
||||||
Mimic is a BAS / red-team lab tool ; the dominant case is a **self-signed Mythic instance**, not a publicly-trusted cert chain. Defaulting `verify=True` is hostile to the actual workflow.
|
|
||||||
|
|
||||||
Secondary defect : `urllib3.exceptions.InsecureRequestWarning` is never suppressed (zero hits for `disable_warnings` / `urllib3` in `backend/`). When operators correctly uncheck verify, stderr gets spammed once per HTTP call.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Decisions (locked)
|
## Livrables
|
||||||
|
|
||||||
1. **Flip default to `False` at every layer** — model, API fallback, adapter, factory, React state, delete reset.
|
### Livrable 1 — `Tabs` primitive + `useHashTab` hook + consumer
|
||||||
2. **New migration 0008** : flip `server_default` to `sa.false()`. Existing rows are NOT mutated (their stored boolean is preserved).
|
|
||||||
3. **Suppress `urllib3.InsecureRequestWarning` ONLY when `verify_tls=False`** — gated inside `MythicAdapter.__init__`. Keeps the warning live for any future code that legitimately verifies.
|
|
||||||
4. **Add helper text under checkbox** : "Leave unchecked for lab Mythic with self-signed certificates." Operators see why the box matters.
|
|
||||||
5. **No API contract change** — `C2ConfigInput.verify_tls: boolean` stays required. The fallback in `data.get("verify_tls", False)` only matters for hand-crafted requests.
|
|
||||||
|
|
||||||
## Out of scope
|
|
||||||
|
|
||||||
- Don't touch existing C2 endpoints behavior (route paths, payload shapes).
|
|
||||||
- Don't change the `verify_tls` field type or remove the column.
|
|
||||||
- Don't change the FakeAdapter (it makes no HTTP calls).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task A — Backend (backend-builder)
|
|
||||||
|
|
||||||
**Files** :
|
**Files** :
|
||||||
- `backend/app/models/c2_config.py` — line 22 : `default=True` → `default=False`
|
- `frontend/src/hooks/useHashTab.ts` (NEW) — ~30 LoC. Reads `window.location.hash`, falls back to default ID, listens to `hashchange`, updates URL without reload. TypeScript pur, zéro style.
|
||||||
- `backend/app/api/c2.py` — line 87 : `data.get("verify_tls", True)` → `data.get("verify_tls", False)`
|
- `frontend/src/components/Tabs.tsx` (NEW) — composant `<Tabs items={[{id, label, count?}]} activeId onChange />`. Underline variant.
|
||||||
- `backend/app/services/c2/factory.py` — line 9 : `verify_tls: bool = True` → `verify_tls: bool = False`
|
- `frontend/src/styles/index.css` — nouvelles recipes :
|
||||||
- `backend/app/services/c2/mythic.py` — line 116 : `verify_tls: bool = True` → `verify_tls: bool = False`, AND add at top of file `import urllib3` + `from urllib3.exceptions import InsecureRequestWarning`, AND inside `__init__` after `self._verify = verify_tls`:
|
- `.tab-underline` — `text-graphite caption-bold cursor-pointer border-b-2 border-transparent hover:text-ink` (instantané, pas de transition)
|
||||||
```python
|
- `.tab-underline-active` — `text-primary border-primary` (override)
|
||||||
if not verify_tls:
|
- `.tab-count-pill` — `rounded-pill bg-cloud text-graphite text-[11px] px-xs py-0 font-mono`
|
||||||
urllib3.disable_warnings(InsecureRequestWarning)
|
- `.tab-count-pill-active` — `bg-primary-soft text-primary` (override)
|
||||||
```
|
- `frontend/src/pages/EngagementDetailPage.tsx` — refactor en 3 tabs :
|
||||||
- **NEW migration** `backend/migrations/versions/0008_c2_verify_tls_default_false.py` — flip `server_default` to `sa.false()`. Use `op.batch_alter_table("c2_config")` for SQLite compatibility (Mimic uses SQLite per sprint 1 SPEC). Down-migration restores `sa.true()`.
|
- Tab 1 "Schedule" (id `schedule`, default) — dates + statut + bouton "Edit engagement"
|
||||||
- **Tests** : grep `backend/tests/` for `verify_tls` and flip every assertion that presupposed the old `True` default (likely in `test_c2_config*.py` PUT-without-verify-tls tests and GET-fresh-row tests). Don't add new tests — adapt existing ones.
|
- Tab 2 "Description" (id `description`) — texte description
|
||||||
|
- Tab 3 "Simulations" (id `simulations`) — la liste actuelle de simulations
|
||||||
|
- Count pills sur tab 3 = nombre de simulations
|
||||||
|
- Tabs branchées sur `useHashTab('schedule')`
|
||||||
|
- `DESIGN.md` — nouvelle subsection `### Navigation › Sub-page tabs` (additive)
|
||||||
|
|
||||||
**Constraints** :
|
**Brutalism check** :
|
||||||
- `pytest` baseline 468/468 must hold (or grow ; never shrink).
|
- Tab : `border-b-2` only, pas de bg, pas de rounded, pas de transition
|
||||||
- `ruff` + `mypy --strict` clean.
|
- Pill : `rounded-pill` autorisé (exception status pill DESIGN.md L125)
|
||||||
- Migration 0008 must be reversible — round-trip `alembic upgrade head` then `alembic downgrade -1` then `alembic upgrade head` must work on a fresh SQLite DB.
|
- Hover : `hover:text-ink` instantané
|
||||||
- Don't restructure or refactor anything else. Minimum surface.
|
|
||||||
|
|
||||||
## Task B — Frontend (frontend-builder)
|
### Livrable 2 — `AlertBanner` component + 4 recipes
|
||||||
|
|
||||||
**Files** :
|
**Files** :
|
||||||
- `frontend/src/components/C2ConfigCard.tsx` :
|
- `frontend/src/components/AlertBanner.tsx` (NEW) — `<AlertBanner variant="error|warn|success|info" title? children />`. Brutalist : border-l-4 strip semantic, fill `bg-paper`, border `border-hairline` partout sauf le strip gauche, `rounded-none`. Icon Lucide à gauche (`AlertCircle` error, `AlertTriangle` warn, `CheckCircle` success, `Info` info) à `size={16}`.
|
||||||
- Line 27 : `useState(true)` → `useState(false)`
|
- `frontend/src/styles/index.css` — 4 nouvelles recipes :
|
||||||
- Line 74 (delete handler) : `setVerifyTls(true)` → `setVerifyTls(false)`
|
- `.alert-error` — `bg-paper border border-hairline border-l-4 border-l-bloom-deep px-md py-sm flex items-start gap-sm`
|
||||||
- Under the checkbox JSX (around lines 169-182) : add a `<p>` with helper text :
|
- `.alert-warn` — same + `border-l-warn`
|
||||||
```tsx
|
- `.alert-success` — same + `border-l-success`
|
||||||
<p className="text-[12px] text-charcoal mt-xxs">
|
- `.alert-info` — same + `border-l-primary`
|
||||||
Leave unchecked for lab Mythic with self-signed certificates.
|
- `frontend/src/pages/SimulationFormPage.tsx` — refactor 2 banners hand-rollés (~L300 Done banner + ~L310 SOC-blocked banner) → `<AlertBanner variant="success">` / `<AlertBanner variant="warn">`.
|
||||||
</p>
|
- `DESIGN.md` — extend `### Toast Notifications` avec subsection `### Inline Banners` (additive, mêmes 4 variants).
|
||||||
```
|
|
||||||
(Use the existing DESIGN.md tokens — `text-[12px] text-charcoal` matches the `hint` style on `FormField`. Confirm token name by reading neighboring components first.)
|
|
||||||
- **Vitest** : if `C2ConfigCard.test.tsx` exists, flip any "starts checked" assertion to "starts unchecked".
|
|
||||||
|
|
||||||
**Constraints** :
|
**Note** : `bloom-deep`, `warn`, `success`, `primary` sont les tokens existants — pas de nouveau token couleur.
|
||||||
- `vitest` baseline 212/212 must hold.
|
|
||||||
|
### Livrable 3 — `BackLink` component (dédup)
|
||||||
|
|
||||||
|
**Files** :
|
||||||
|
- `frontend/src/components/BackLink.tsx` (NEW) — `<BackLink to="/engagements">Back to engagements</BackLink>`. Renders `← {children}` (ArrowLeft Lucide size=14, gap-xxs). Style : `text-graphite hover:text-primary caption-md` instantané.
|
||||||
|
- Refactor consumers (3 instances hand-rollées) :
|
||||||
|
- `frontend/src/pages/EngagementDetailPage.tsx` (~L35)
|
||||||
|
- `frontend/src/pages/SimulationFormPage.tsx` (back-link en edit mode)
|
||||||
|
- `frontend/src/pages/TemplateFormPage.tsx` (back-link en edit mode)
|
||||||
|
|
||||||
|
### Livrable 4 — `.table-compact` recipe + apply GLOBAL
|
||||||
|
|
||||||
|
**Files** :
|
||||||
|
- `frontend/src/styles/index.css` — nouvelle recipe (mais appliquée comme default, pas opt-in cette sprint per user decision Q2-B) :
|
||||||
|
- Soit : modifier la recipe `.card-product table` existante pour passer à 32px row, `caption-md` text, `py-xxs px-xs` cells, divider `border-hairline`.
|
||||||
|
- Soit : ajouter `.table-compact` puis l'appliquer à TOUTES les list pages (Engagements, Templates, Users, etc.).
|
||||||
|
- **Choix builder** : pick the cleaner path. Préférer modifier le default si toutes les tables doivent suivre, sinon `.table-compact` + apply partout.
|
||||||
|
- Apply sur :
|
||||||
|
- `frontend/src/pages/EngagementsListPage.tsx`
|
||||||
|
- `frontend/src/pages/TemplatesListPage.tsx`
|
||||||
|
- `frontend/src/pages/UsersAdminPage.tsx`
|
||||||
|
- `frontend/src/components/SimulationList.tsx` (utilisé dans EngagementDetailPage)
|
||||||
|
- Toute autre table dans le code (grep `<table` dans `frontend/src/`)
|
||||||
|
- `DESIGN.md` — `### Data Tables` section : modifier la ligne "Row min-height 44px" → "Row min-height 32px (compact density, all surfaces)". Documenter le tradeoff WCAG (touch target dérogé en faveur de la densité opérateur — c'est un BAS tool, pas un site grand public).
|
||||||
|
|
||||||
|
**ATTENTION** : WCAG SC 2.5.5 (Target Size: Minimum) recommande 44×44. Tu déroges. Garder les boutons d'action des rows ≥ 24px d'icône avec padding pour préserver le hit area minimal. Tester en clavier + lecteur d'écran nicht regressé.
|
||||||
|
|
||||||
|
### Livrable 5 — DESIGN.md amendments + CHANGELOG
|
||||||
|
|
||||||
|
**Files** :
|
||||||
|
- `DESIGN.md` :
|
||||||
|
- `### Navigation` : nouvelle subsection "Sub-page tabs" (livrable 1)
|
||||||
|
- `### Toast Notifications` : nouvelle subsection "Inline Banners" (livrable 2)
|
||||||
|
- `### Data Tables` : ligne row min-height passe à 32px + paragraphe rationale (livrable 4)
|
||||||
|
- `CHANGELOG.md` : entrée Sprint 11 listant chaque primitive + ses tokens DESIGN.md.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sequencing
|
||||||
|
|
||||||
|
1. **frontend-builder** : livrables 1 + 2 + 3 + 4 + 5 en cascade. Une seule passe. Commits atomiques :
|
||||||
|
```
|
||||||
|
feat(frontend): add Tabs primitive + useHashTab hook
|
||||||
|
feat(frontend): refactor EngagementDetailPage to 3-tab layout
|
||||||
|
feat(frontend): add AlertBanner component + 4 semantic variants
|
||||||
|
refactor(frontend): migrate SimulationFormPage banners to AlertBanner
|
||||||
|
feat(frontend): add BackLink helper + dedup 3 hand-rolled instances
|
||||||
|
feat(frontend): compact table density (32px row) global
|
||||||
|
docs(design): add tabs / inline banners / compact-table-density to DESIGN.md
|
||||||
|
```
|
||||||
|
(collapse OK si plus efficace ; ~5-7 commits attendus)
|
||||||
|
2. **design-reviewer** : visual + token compliance check. Focus :
|
||||||
|
- Underline tabs : pas de transition, pas de bg fill, border-b instantané
|
||||||
|
- Count pill : `rounded-pill` est dans l'exception DESIGN.md, OK
|
||||||
|
- AlertBanner : `border-l-4` semantic OK, pas de shadow
|
||||||
|
- BackLink : caption + icon align baseline
|
||||||
|
- 32px tables : densité lisible ? Hit area boutons d'action OK ?
|
||||||
|
3. **code-reviewer** : sprint diff. Focus :
|
||||||
|
- Aucune transition / shadow / rounded leak
|
||||||
|
- useHashTab : `hashchange` listener cleanup dans useEffect return
|
||||||
|
- Tabs : prop API stable, fallback default ID si hash invalide
|
||||||
|
- AlertBanner : ARIA `role="alert"` ou `role="status"` selon variant
|
||||||
|
- BackLink : `to` est `string` ou objet React Router ? Choisir un seul
|
||||||
|
4. **spec-reviewer** : `SPEC.md` n'a pas vocation à mentionner ces primitives, **skip ce sprint** sauf si tu détectes une feature côté backend qui aurait dérivé.
|
||||||
|
|
||||||
|
## Contraintes tests
|
||||||
|
|
||||||
|
- `vitest --run` baseline **212/212** doit tenir.
|
||||||
|
- **Ajouter 4 specs** (1 par new component) :
|
||||||
|
- `Tabs.test.tsx` — active state, hash sync, count pill render
|
||||||
|
- `AlertBanner.test.tsx` — 4 variants rendent les bonnes classes
|
||||||
|
- `BackLink.test.tsx` — render + click navigation
|
||||||
|
- `useHashTab.test.tsx` — hash read/write
|
||||||
- `tsc --noEmit` + `eslint --max-warnings=0` clean.
|
- `tsc --noEmit` + `eslint --max-warnings=0` clean.
|
||||||
- No type change to `C2ConfigInput` (the field is already required `boolean`).
|
- Brutalism unit assertions : pour chaque new component, un test qui vérifie `expect(el).not.toHaveClass('rounded-md')` (sauf pill counter) + `.not.toHaveClass(/transition-/)` + `.not.toHaveClass(/shadow-/)`.
|
||||||
- Visual: same row, just one extra `<p>` hint below the checkbox-label row. Same brutalist treatment, no transition.
|
|
||||||
|
|
||||||
## Task C — Sequencing
|
## Out of scope (explicite)
|
||||||
|
|
||||||
Both tasks have **zero shared files**. Dispatch backend-builder + frontend-builder **in parallel**. No ordering constraint.
|
- ❌ Side nav / drawer / breadcrumb component — top nav + BackLink suffit
|
||||||
|
- ❌ Global engagement selector top-bar (Spectrum pattern rejeté)
|
||||||
After both report green :
|
- ❌ Editor dynamic-column grid — pas d'éditeur dans Mimic
|
||||||
- **code-reviewer** : sprint diff scan (focus : migration reversibility, urllib3 gating, no leftover hardcoded `True`).
|
- ❌ Dark mode rework
|
||||||
- **design-reviewer** : helper-text placement, token compliance, focus ring still works, no regression on the card.
|
- ❌ Toast queue system (les toasts actuels via `useToast` restent)
|
||||||
- **spec-reviewer** : verify SPEC.md § Intégration C2 still matches the new defaults (may need a one-line note about lab-mode default ; check before editing).
|
- ❌ PDF/document deliverable
|
||||||
|
- ❌ Color palette changes
|
||||||
|
- ❌ Tokens spacing/typo dans tailwind.config
|
||||||
|
|
||||||
## Definition of Done
|
## Definition of Done
|
||||||
|
|
||||||
- Test connection against self-signed Mythic from a freshly-created C2 config works **without unchecking anything**.
|
- ✅ 5 primitives livrées + EngagementDetailPage refactorée en tabs + SimulationFormPage banners migrés + BackLink dédup × 3
|
||||||
- Existing rows are untouched (operators who saved verify_tls=true keep it until they re-save).
|
- ✅ vitest **212+/212+** (4 new specs ajoutées), tsc + lint clean
|
||||||
- `pytest` 468/468 → 468+ (no shrink), `vitest` 212/212.
|
- ✅ pytest **469/469** intact (zéro backend touché)
|
||||||
- `ruff` + `mypy --strict` + `tsc --noEmit` + `eslint` clean.
|
- ✅ Toutes tables passent à 32px row density (validation visuelle)
|
||||||
- Migration 0008 round-trip OK.
|
- ✅ Design-reviewer APPROVED (brutalism check sur les 4 new recipes)
|
||||||
- No `urllib3.InsecureRequestWarning` on stderr when `verify_tls=False`.
|
- ✅ Code-reviewer APPROVED
|
||||||
- Code-reviewer + design-reviewer + spec-reviewer APPROVED.
|
- ✅ DESIGN.md amendé (3 subsections additives)
|
||||||
- PR opened on Gitea ; tasks/pr-body-sprint-10.md drafted by team-lead.
|
- ✅ CHANGELOG.md entrée sprint 11
|
||||||
|
- ✅ PR ouverte sur Gitea avec body
|
||||||
## Operator notes (for PR body)
|
|
||||||
|
|
||||||
- This sprint flips a security-relevant default. Production operators who legitimately use a publicly-trusted cert chain will need to explicitly check the box — but the dominant case in this BAS tool is lab Mythic with self-signed, so the new default matches the actual workflow.
|
|
||||||
- Existing engagements with `verify_tls=true` already stored remain unchanged. They keep their current behaviour. If the operator reported the bug because their existing row has `verify_tls=true`, they will still need to uncheck + save once after this sprint lands.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user