feature/m4-mitre #1

Merged
knacky merged 13 commits from feature/m4-mitre into main 2026-05-12 17:24:14 +00:00
2 changed files with 26 additions and 3 deletions
Showing only changes of commit 54adfee690 - Show all commits

View File

@@ -51,6 +51,10 @@ export interface MitreTag {
name: string;
}
// Query keys. `status` + `matrix` drive the M4 picker; the per-list factories
// (`tactics`/`techniques`/`subtechniques`) are unused today but the M5
// template forms will consume them for the standalone REST endpoints when
// users edit a single test's tags inline.
export const mitreKeys = {
status: ['mitre', 'status'] as const,
matrix: ['mitre', 'matrix'] as const,
@@ -85,3 +89,17 @@ export interface MatrixTactic {
export interface MitreMatrix {
tactics: MatrixTactic[];
}
/** Mirror of backend `SyncResultOut` (`api/mitre.py`). */
export interface MitreSyncResult {
tactics_upserted: number;
techniques_upserted: number;
subtechniques_upserted: number;
subtechniques_skipped_orphan: number;
technique_tactic_links: number;
version: string | null;
source: string;
started_at: string;
finished_at: string;
duration_ms: number;
}

View File

@@ -9,7 +9,12 @@ import { SectionHeader } from '@/components/ui/SectionHeader';
import { Tag } from '@/components/ui/Tag';
import { ApiError, apiGet, apiPost } from '@/lib/api';
import { useAuth } from '@/lib/auth';
import { mitreKeys, type MitreStatus, type MitreTag } from '@/lib/mitre';
import {
mitreKeys,
type MitreStatus,
type MitreSyncResult,
type MitreTag,
} from '@/lib/mitre';
export function MitrePage() {
const { state } = useAuth();
@@ -24,14 +29,14 @@ export function MitrePage() {
});
const sync = useMutation({
mutationFn: () => apiPost<Record<string, unknown>>('/mitre/sync'),
mutationFn: () => apiPost<MitreSyncResult>('/mitre/sync'),
onMutate: () => {
setSyncResult(null);
setSyncError(null);
},
onSuccess: async (res) => {
const counts = `${res.tactics_upserted} tactics, ${res.techniques_upserted} techniques, ${res.subtechniques_upserted} subtechniques`;
setSyncResult(`Sync completed in ${(res as { duration_ms: number }).duration_ms / 1000}s — ${counts}.`);
setSyncResult(`Sync completed in ${(res.duration_ms / 1000).toFixed(1)}s — ${counts}.`);
await qc.invalidateQueries({ queryKey: ['mitre'] });
},
onError: (e) => {