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:
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user