feature/m4-mitre #1
@@ -130,10 +130,13 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro
|
|||||||
data-testid="mitre-matrix-scroll"
|
data-testid="mitre-matrix-scroll"
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="MITRE ATT&CK matrix"
|
aria-label="MITRE ATT&CK matrix"
|
||||||
/* Equal-width columns share the container; no horizontal scroll. */
|
/* `minmax(7rem, 1fr)` ensures every cell is wide enough for the
|
||||||
className="grid gap-px bg-border rounded"
|
* longest single word in MITRE names (no mid-word breaks), and
|
||||||
|
* stretches to fill the container otherwise. Horizontal scroll only
|
||||||
|
* kicks in on narrow viewports below ~1680px. */
|
||||||
|
className="grid gap-px bg-border rounded overflow-x-auto"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `repeat(${matrix.data.tactics.length}, minmax(0, 1fr))`,
|
gridTemplateColumns: `repeat(${matrix.data.tactics.length}, minmax(7rem, 1fr))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{matrix.data.tactics.map((tactic) => {
|
{matrix.data.tactics.map((tactic) => {
|
||||||
@@ -166,7 +169,7 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro
|
|||||||
data-testid={`mitre-tactic-${tactic.external_id}`}
|
data-testid={`mitre-tactic-${tactic.external_id}`}
|
||||||
aria-pressed={tacticSel}
|
aria-pressed={tacticSel}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-semibold leading-tight break-words">
|
<div className="text-xs font-semibold leading-tight break-normal hyphens-none">
|
||||||
{tactic.name}
|
{tactic.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-text-dim mt-0.5">
|
<div className="text-[10px] text-text-dim mt-0.5">
|
||||||
@@ -205,7 +208,7 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro
|
|||||||
name: tech.name,
|
name: tech.name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="flex-1 min-w-0 text-left px-2 py-1.5 font-sans break-words"
|
className="flex-1 min-w-0 text-left px-2 py-1.5 font-sans break-normal hyphens-none"
|
||||||
title={`${tech.external_id} — ${tech.name}`}
|
title={`${tech.external_id} — ${tech.name}`}
|
||||||
data-testid={`mitre-technique-${tech.external_id}`}
|
data-testid={`mitre-technique-${tech.external_id}`}
|
||||||
aria-pressed={techSel}
|
aria-pressed={techSel}
|
||||||
@@ -250,7 +253,7 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
'block w-full text-left pl-5 pr-2 py-1 font-sans text-xs border-l-2 break-words',
|
'block w-full text-left pl-5 pr-2 py-1 font-sans text-xs border-l-2 break-normal hyphens-none',
|
||||||
subSel
|
subSel
|
||||||
? 'accent-fill-purple border-purple text-text-bright'
|
? 'accent-fill-purple border-purple text-text-bright'
|
||||||
: 'border-purple/30 hover:bg-purple/10',
|
: 'border-purple/30 hover:bg-purple/10',
|
||||||
|
|||||||
@@ -92,7 +92,21 @@ export function MitrePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SectionHeader prefix="Tag" highlight="Picker" accent="orange" />
|
<SectionHeader prefix="Tag" highlight="Picker" accent="orange" />
|
||||||
|
{/* Full-bleed the matrix beyond max-w-page so it uses the full viewport
|
||||||
|
* width. `calc(50% - 50vw)` is the canonical CSS recipe: the element's
|
||||||
|
* left edge slides back to viewport x=0 regardless of how big the
|
||||||
|
* outer max-w-page container is. `px-[60px]` mirrors the page padding
|
||||||
|
* so cells don't touch the viewport edge. */}
|
||||||
|
<div
|
||||||
|
className="px-[60px]"
|
||||||
|
style={{
|
||||||
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
|
marginRight: 'calc(50% - 50vw)',
|
||||||
|
width: '100vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<MitreTagPicker value={selected} onChange={setSelected} />
|
<MitreTagPicker value={selected} onChange={setSelected} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{selected.length > 0 && (
|
{selected.length > 0 && (
|
||||||
<Card accent="purple" className="mt-6" title="Selected (preview payload)">
|
<Card accent="purple" className="mt-6" title="Selected (preview payload)">
|
||||||
|
|||||||
@@ -79,7 +79,14 @@ Remplace l'aller-retour Excel actuel par une UI partagée temps réel, multi-rô
|
|||||||
|
|
||||||
## 5. Exigences fonctionnelles
|
## 5. Exigences fonctionnelles
|
||||||
- **F1** — Gestion users/groupes/invitations par admin avec permissions atomiques (familles listées en §4).
|
- **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 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`.
|
- **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/#` —
|
||||||
|
- **Full-bleed** : le picker s'étend sur toute la largeur du viewport (s'échappe du `max-w-page` global du layout) pour exposer un maximum de cellules sans scroll.
|
||||||
|
- **15 colonnes** equal-width via `grid-template-columns: repeat(N, minmax(7rem, 1fr))` ; scroll horizontal seulement en dernier recours sur viewport étroit (<≈1680px).
|
||||||
|
- **Wrap word-only** : `overflow-wrap: normal` + `hyphens: none` — les noms cassent uniquement sur les espaces, jamais au milieu d'un mot.
|
||||||
|
- **Headers** = nom de la tactic seul + compteur de techniques en 10px ; l'`external_id` `TA00xx` n'apparaît qu'au hover (title) et dans les chips de sélection.
|
||||||
|
- **Cellules** = nom de la technique seul (idem pour `T1xxx` au hover) ; chevron `▸ N / ▾ N` qui déplie inline les sub-techniques dans la colonne.
|
||||||
|
- **Police** sans-serif uniforme `text-xs` (12px) pour cells + headers, `10px` pour les sub-counts.
|
||||||
|
- **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` cliquables pour retirer.
|
||||||
- **F3** — CRUD scénarios = liste ordonnée (drag-and-drop) de tests unitaires.
|
- **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.
|
- **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.
|
- **F5** — Saisie côté red : commande lancée, output texte, commentaires markdown, statut, timestamp auto+override.
|
||||||
|
|||||||
Reference in New Issue
Block a user