From 8742fb2b6e32616a757326b714b6739166690799 Mon Sep 17 00:00:00 2001 From: Knacky Date: Tue, 12 May 2026 18:41:11 +0200 Subject: [PATCH] =?UTF-8?q?refactor(m4):=20match=20attack.mitre.org=20sizi?= =?UTF-8?q?ng=20=E2=80=94=20equal-width=20cols,=20name-only=20cells?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual parity pass against attack.mitre.org/# per user feedback ("trop dense, illisible, je veux la même représentation"): - Layout switched from flex+fixed-width 224px columns to a CSS grid of `repeat(N, minmax(0, 1fr))` so the 15 tactic columns share the container width equally. No more horizontal scroll on a standard desktop. - Cells now show NAME ONLY (matches mitre.org). The external_id (TA00xx / T1xxx / T1xxx.xxx) is preserved in the chip selection bar at the top and in the `title` hover tooltip on every cell — surfaces on demand, doesn't consume cell real estate. - Font: switched to `font-sans` (IBM Plex Sans) at `text-xs` (12px) across cells, matching the mitre.org typography. Headers use the same family at the same size with a 10px sub-line for the technique count. - Chevron icons: ▸ (collapsed) / ▾ (expanded) — small, sub-technique count rendered inline beside the chevron. - Helper line below the matrix tells the user where the IDs went. Spec §F2 + testing-m4.md walkthrough rewritten to lock the new sizing rules in (font-xs, no external_id in cells, hover/chip for the ID, no horizontal scroll). spec-reviewer will see the matching contract. DoD: make e2e → 34 passed. Selectors (data-testid + aria-pressed) unchanged so the existing M4 e2e test still walks the new layout end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/components/MitreTagPicker.tsx | 289 ++++++++++----------- tasks/spec.md | 2 +- tasks/testing-m4.md | 16 +- 3 files changed, 150 insertions(+), 157 deletions(-) diff --git a/frontend/src/components/MitreTagPicker.tsx b/frontend/src/components/MitreTagPicker.tsx index a4f4afa..04f294d 100644 --- a/frontend/src/components/MitreTagPicker.tsx +++ b/frontend/src/components/MitreTagPicker.tsx @@ -30,12 +30,11 @@ function useMatrix() { } /** - * Flat ATT&CK matrix in the attack.mitre.org style — columns = tactics, each - * cell lists its techniques with an inline chevron to expand sub-techniques. - * - * Click a technique row → toggle selection. Click a sub-technique row → toggle. - * Click the tactic header → toggle the whole tactic as a single tag. - * Selected items are filled with their accent (cyan/orange/purple). + * Flat ATT&CK matrix in the attack.mitre.org/# style — 15 columns share the + * available width (no horizontal scroll on standard desktop). Cells show the + * technique NAME only; the external_id surfaces in the chips at the top and in + * the hover tooltip. A `▸/▾` chevron beside a technique expands its + * sub-techniques inline within the column. */ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerProps) { const matrix = useMatrix(); @@ -44,7 +43,7 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro const filterNorm = filter.trim().toLowerCase(); - // Set of `${kind}:${external_id}` for O(1) lookup. + // O(1) selection lookup. const selectedKeys = useMemo( () => new Set(value.map((t) => `${t.kind}:${t.external_id}`)), [value], @@ -114,7 +113,7 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro className="max-w-md" /> {matrix.data && ( - + {matrix.data.tactics.length} tactics ·{' '} {matrix.data.tactics.reduce((sum, t) => sum + t.techniques.length, 0)} mappings @@ -128,164 +127,156 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro {matrix.data && (
-
- {matrix.data.tactics.map((tactic) => { - const visible = tactic.techniques.filter(matches); - const tacticSel = isSelected('tactic', tactic.external_id); - return ( -
{ + const visible = tactic.techniques.filter(matches); + const tacticSel = isSelected('tactic', tactic.external_id); + return ( +
+ {/* Tactic header — name only (attack.mitre.org style) */} + +
+ {tactic.name} +
+
+ {tactic.techniques.length} technique{tactic.techniques.length === 1 ? '' : 's'} +
+ - {/* Techniques cells */} -
- {visible.length === 0 && filterNorm && ( -
- (filtered out) -
- )} - {visible.map((tech) => { - const techSel = isSelected('technique', tech.external_id); - const isExpanded = expanded.has(tech.external_id); - const hasSubs = tech.subtechniques.length > 0; - return ( -
-
+ {visible.length === 0 && filterNorm && ( +
+ (filtered out) +
+ )} + {visible.map((tech) => { + const techSel = isSelected('technique', tech.external_id); + const isExpanded = expanded.has(tech.external_id); + const hasSubs = tech.subtechniques.length > 0; + return ( +
+
+ + {hasSubs && ( - {hasSubs && ( - - )} -
- - {isExpanded && hasSubs && ( -
- {tech.subtechniques.map((sb) => { - const subSel = isSelected('subtechnique', sb.external_id); - return ( - - ); - })} -
)}
- ); - })} -
+ + {isExpanded && hasSubs && ( +
+ {tech.subtechniques.map((sb) => { + const subSel = isSelected('subtechnique', sb.external_id); + return ( + + ); + })} +
+ )} +
+ ); + })}
- ); - })} -
+
+ ); + })}
)} -

- Click any cell to select · click +N to reveal sub-techniques · click a chip above to remove. +

+ Hover a cell for its external_id. Click a cell to toggle selection. Use to reveal sub-techniques inline. Click a chip above to remove.

); diff --git a/tasks/spec.md b/tasks/spec.md index 7f8b369..1a35a4a 100644 --- a/tasks/spec.md +++ b/tasks/spec.md @@ -79,7 +79,7 @@ Remplace l'aller-retour Excel actuel par une UI partagée temps réel, multi-rô ## 5. Exigences fonctionnelles - **F1** — Gestion users/groupes/invitations par admin avec permissions atomiques (familles listées en §4). -- **F2** — CRUD tests unitaires templates avec MITRE ATT&CK (Tactic+Technique+Sub-technique multi), procédure markdown/code, prérequis, résultat attendu red, détection attendue blue, niveau OPSEC (low/med/high), tags libres, IOCs attendus. **Représentation UI du picker MITRE** : matrice flat type `attack.mitre.org/#` — colonnes = tactics (ordonnées TA00xx), cellules = techniques, chevron `+N` qui déplie inline les sub-techniques. Click sur une cellule = (dé)sélection. Selection multi-niveaux (tactic / technique / sub-technique) cumulative, chips de sélection en haut. +- **F2** — CRUD tests unitaires templates avec MITRE ATT&CK (Tactic+Technique+Sub-technique multi), procédure markdown/code, prérequis, résultat attendu red, détection attendue blue, niveau OPSEC (low/med/high), tags libres, IOCs attendus. **Représentation UI du picker MITRE** : matrice flat fidèle à `attack.mitre.org/#` — 15 colonnes equal-width partageant la largeur disponible (pas de scroll horizontal sur desktop standard), headers = nom de la tactic seul (l'`external_id` `TA00xx` n'apparaît qu'au hover et dans les chips de sélection), cellules = nom de la technique seul (même règle pour `T1xxx`), chevron `▸ N / ▾ N` qui déplie inline les sub-techniques dans la colonne. Police sans-serif uniforme, taille `text-xs` (12px). Click sur une cellule = (dé)sélection. Selection multi-niveaux (tactic / technique / sub-technique) cumulative, chips de sélection en haut avec `external_id · name`. - **F3** — CRUD scénarios = liste ordonnée (drag-and-drop) de tests unitaires. - **F4** — CRUD missions (métadonnées §4) composées d'un ou plusieurs scénarios, snapshot des templates à l'instanciation. - **F5** — Saisie côté red : commande lancée, output texte, commentaires markdown, statut, timestamp auto+override. diff --git a/tasks/testing-m4.md b/tasks/testing-m4.md index edc3322..7df8996 100644 --- a/tasks/testing-m4.md +++ b/tasks/testing-m4.md @@ -39,14 +39,16 @@ Le rapport HTML est dans `e2e/playwright-report/`, le JUnit dans `e2e/playwright 3. Carte **Source** : vérifier `version 19.0` + URL pinnée + `Last sync` non vide. 4. Carte **Sync** (admin uniquement) : cliquer **Trigger MITRE sync** → bannière verte avec counts (15 tactics / 222 techniques / 475 subtechniques). 5. **Picker — matrice flat type attack.mitre.org** : - - La matrice affiche 15 colonnes (tactics TA0001 → TA0040) en horizontal scroll. Chaque header montre l'`external_id`, le nom complet, et le compte de techniques. - - Click sur le header **TA0006 — Credential Access** → la colonne entière est sélectionnée (chip cyan en haut). + - La matrice tient sur la largeur de la page sans scroll horizontal (15 colonnes de largeur égale, partagent l'espace dispo). + - Chaque header de colonne montre **seulement le nom de la tactic** (ex. `Credential Access`) + `17 techniques` en petit dessous. L'`external_id` (TA0006) apparaît au hover (title). + - Click sur le header **Credential Access** → toute la colonne est sélectionnée (chip cyan en haut, header en cyan filled). - Re-click pour désélectionner. - - Cliquer la cellule **T1003** dans TA0006 → la cellule passe en orange filled, un chip orange apparaît. - - Cliquer le chevron **+8** à droite de T1003 → la liste de ses sub-techniques se déploie inline dans la même colonne. - - Cliquer **T1003.001 LSASS Memory** → cell purple filled, chip purple ajouté en haut. + - Les cellules affichent **uniquement le nom de la technique** (ex. `OS Credential Dumping`). L'`external_id` (T1003) apparaît au hover (title) et dans le chip de sélection. + - Cliquer la cellule **OS Credential Dumping** → cellule en orange filled, chip `T1003 · OS Credential Dumping` en haut. + - Cliquer le chevron `▸ 8` à droite de la cellule → la liste des sub-techniques se déploie inline dans la même colonne, chevron passe à `▾ 8`. + - Cliquer **LSASS Memory** (sub-technique) → cell purple filled, chip `T1003.001 · LSASS Memory`. - Click le chip pour le retirer. - - La carte « Selected (preview payload) » sous la matrice montre le JSON cumulatif. + - La carte « Selected (preview payload) » sous la matrice montre le JSON cumulatif avec les `external_id`. ### 3.2 Filtre 1. Taper `dump` dans le champ **Filter** → seules T1003 + sub-techniques restent visibles, les autres techniques sont cachées (mais leurs colonnes restent visibles pour préserver la grille). @@ -109,6 +111,6 @@ curl -sX POST http://localhost:8080/api/v1/mitre/sync \ - [x] `/mitre/sync` exige la perm `mitre.sync` (admin via bypass `is_admin`). - [x] Sha256 mismatch sur la pinned URL → 502 `checksum_mismatch`, DB intacte. - [x] Bundle local (`--source `) bypasse la vérif checksum. -- [x] Picker SPA : matrice flat attack.mitre.org-style (15 colonnes), click cellule → sélection, chevron `+N` → sub-techniques inline, chips multi-niveaux en haut. +- [x] Picker SPA : matrice flat attack.mitre.org-style — 15 colonnes equal-width sans scroll horizontal, cellules avec **name only** (external_id au hover + dans chips), chevron `▸ N / ▾ N` → sub-techniques inline, chips multi-niveaux en haut. - [x] `GET /mitre/matrix` retourne tous les tactics + leurs techniques + sub-techniques nestées en un seul appel (~55 KB pour v19). - [x] Non-admin : voit la page `/mitre` mais pas la carte Sync ; `POST /mitre/sync` → 403.