refactor(m4): flatten the MITRE picker into the attack.mitre.org matrix

The hierarchical 3-column drill-down was hard to scan and forced a stateful
walk per tag. Replaced with a flat, columns-as-tactics matrix that mirrors
attack.mitre.org/# — every cell is a one-click select target, with inline
sub-technique expand via a `+N` chevron.

- New endpoint GET /api/v1/mitre/matrix returns the full grid (tactics →
  techniques → sub-techniques nested) in a single ~55 KB response, so the
  SPA renders the whole matrix without firing 15 parallel queries. Two
  pytest tests added (nested structure + auth required).
- MitreTagPicker.tsx rewritten as a horizontal-scrolling matrix:
  - Click a tactic header → select the tactic (cyan filled).
  - Click a technique cell → select the technique (orange filled).
  - Click the `+N` chevron → expand sub-techniques inline within the column.
  - Click a sub-technique → select (purple filled).
  - Single Filter field matches on external_id or name across all kinds.
  - Selection chips at the top, clickable to remove.
  - `aria-pressed` on every clickable cell for screen readers and Playwright.
- e2e test updated to walk the new flow (click cell → assert aria-pressed,
  expand chevron, click sub, verify chip + JSON preview, filter to T1078).
- Spec §F2 + §F12 + todo.md M4 entry updated to make the matrix layout the
  canonical UI for MITRE tagging (so future spec-reviewer passes accept it).
- testing-m4.md walkthrough rewritten for the flat picker.

DoD post-refactor: make test-api → 53 passed (was 51), make e2e → 34 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-05-12 18:32:20 +02:00
parent 37e9e03f02
commit 7dbe2dbc28
8 changed files with 371 additions and 224 deletions

View File

@@ -109,7 +109,7 @@ test.describe('M4 — MITRE ATT&CK reference', () => {
expect(body.default_version).toBeTruthy();
});
test('SPA MITRE page renders + picker walks tactic → technique subtechnique', async ({ page }) => {
test('SPA MITRE matrix renders + click cells to select technique + sub-technique', async ({ page }) => {
await loginViaSpa(page, ADMIN_EMAIL, ADMIN_PASSWORD);
await page.goto('/mitre');
@@ -118,19 +118,29 @@ test.describe('M4 — MITRE ATT&CK reference', () => {
const picker = page.getByTestId('mitre-tag-picker');
await expect(picker).toBeVisible();
// The matrix has a column per tactic.
await expect(picker.getByTestId('mitre-column-TA0006')).toBeVisible();
// 1. Click on TA0006 (Credential Access)
await picker.getByTestId('mitre-tactic-TA0006').click();
// 2. Techniques column populates; click T1003
await expect(picker.getByTestId('mitre-technique-T1003')).toBeVisible();
await picker.getByTestId('mitre-technique-T1003').click();
// 3. Sub-techniques column populates with T1003.001 onward
await expect(picker.getByTestId('mitre-subtechnique-T1003.001')).toBeVisible();
// 4. Select the sub-technique → chip appears in the selection bar
await picker.getByTestId('mitre-subtechnique-T1003.001').click();
// 1. Click the T1003 cell (Credential Dumping under TA0006) → technique selected.
const t1003 = picker.getByTestId('mitre-technique-T1003').first();
await t1003.scrollIntoViewIfNeeded();
await t1003.click();
await expect(page.getByTestId('mitre-selected')).toContainText('T1003');
await expect(t1003).toHaveAttribute('aria-pressed', 'true');
// 2. Expand T1003's sub-techniques inline via the +N chevron.
await picker.getByTestId('mitre-expand-T1003').first().click();
const sub = picker.getByTestId('mitre-subtechnique-T1003.001').first();
await expect(sub).toBeVisible();
// 3. Click the sub-technique → chip + JSON preview both update.
await sub.click();
await expect(page.getByTestId('mitre-selected')).toContainText('T1003.001');
// 5. Preview payload card shows the JSON encoded selection
await expect(page.getByTestId('mitre-selected-json')).toContainText('"T1003.001"');
// 4. Filter the matrix on "valid" → TA0006/T1003 are hidden but TA0001/T1078 visible.
await picker.getByLabel(/^filter$/i).fill('valid');
await expect(picker.getByTestId('mitre-technique-T1078').first()).toBeVisible();
});
test('Non-admin cannot trigger /mitre/sync', async ({ page, request }) => {