feat(frontend): sprint 3 — multi-technique MITRE selection + matrix modal

- types: replace mitre_technique_id/name scalars with techniques:MitreTechnique[]
  on Simulation; add MitreTactic/MitreMatrixTechnique/MitreMatrixSubtechnique;
  SimulationPatchInput now uses technique_ids:string[]
- api/mitre.ts: add getMitreMatrix() → GET /api/mitre/matrix
- hooks/useMitre: add useMitreMatrix(enabled) with staleTime:Infinity
- MitreTechniquePicker: clean rewrite — onSelect(technique) one-shot, resets
  input after selection, no incoming value props
- MitreTechniqueTag: chip component with id+name and × remove button
- MitreMatrixModal: tactic columns (220px fixed), expand/collapse subtechniques,
  search filter (auto-expands parent on sub match), selection state, focus trap
  (Tab wrap, Escape, search autofocus), backdrop click cancel, Apply N techniques
- MitreTechniquesField: orchestrates tags+picker+matrix with auto-save PATCH on
  every add/remove/Apply, dedup guard, disabled read-only mode for SOC
- SimulationFormPage: swap MitreTechniquePicker for MitreTechniquesField; remove
  technique state from RT form (techniques have independent auto-save cycle)
- SimulationList: MITRE column → T1059 +2 counter format, — when empty
- Tests: 84 passing (13 test files); new suites for Tag, Field, Modal;
  MitreTechniquePicker + SimulationFormPage + SimulationList adapted to new API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-05-27 04:04:23 +02:00
parent 673b25e0b0
commit 771483f3b0
15 changed files with 973 additions and 181 deletions

View File

@@ -1,5 +1,5 @@
import { apiClient } from './client';
import type { MitreTechnique } from './types';
import type { MitreTactic, MitreTechnique } from './types';
export async function searchMitreTechniques(query: string): Promise<MitreTechnique[]> {
const { data } = await apiClient.get<MitreTechnique[]>('/mitre/techniques', {
@@ -7,3 +7,8 @@ export async function searchMitreTechniques(query: string): Promise<MitreTechniq
});
return data;
}
export async function getMitreMatrix(): Promise<MitreTactic[]> {
const { data } = await apiClient.get<MitreTactic[]>('/mitre/matrix');
return data;
}

View File

@@ -61,12 +61,28 @@ export interface MitreTechnique {
tactics: string[];
}
export interface MitreMatrixSubtechnique {
id: string;
name: string;
}
export interface MitreMatrixTechnique {
id: string;
name: string;
subtechniques: MitreMatrixSubtechnique[];
}
export interface MitreTactic {
tactic_id: string;
tactic_name: string;
techniques: MitreMatrixTechnique[];
}
export interface Simulation {
id: number;
engagement_id: number;
name: string;
mitre_technique_id: string | null;
mitre_technique_name: string | null;
techniques: MitreTechnique[];
description: string | null;
commands: string | null;
prerequisites: string | null;
@@ -88,8 +104,7 @@ export interface SimulationCreateInput {
export interface SimulationPatchInput {
name?: string;
mitre_technique_id?: string | null;
mitre_technique_name?: string | null;
technique_ids?: string[];
description?: string | null;
commands?: string | null;
prerequisites?: string | null;