feat: Phase 8 - Frontend main views (T-026 to T-031)

Implement all main frontend views for the MITRE ATT&CK coverage platform:

- T-026: Dashboard with coverage summary cards and tactic breakdown table

- T-027: Interactive ATT&CK matrix with filtering by status, tactic, platform

- T-028: Technique detail page with tests, intel items, and review actions

- T-029: Test creation form with technique selector and validation

- T-030: Test detail page with drag and drop evidence upload and download

- T-031: System admin panel with MITRE sync and intel scan controls

New components: CoverageSummaryCard, TacticCoverageChart, AttackMatrix, TechniqueCell, TestForm, EvidenceUpload, EvidenceList

New API modules: metrics.ts, techniques.ts, tests.ts, evidence.ts, system.ts

All views use TanStack Query for data fetching with proper loading and error states. Role-based UI controls for admin/lead actions.
This commit is contained in:
2026-02-06 16:21:14 +01:00
parent 591b5df250
commit cb447f3803
22 changed files with 3092 additions and 27 deletions

View File

@@ -0,0 +1,30 @@
import client from "./client";
export interface EvidenceOut {
id: string;
test_id: string;
file_name: string;
sha256_hash: string;
uploaded_by: string | null;
uploaded_at: string;
download_url: string;
}
/** Upload evidence file for a test. */
export async function uploadEvidence(testId: string, file: File): Promise<EvidenceOut> {
const formData = new FormData();
formData.append("file", file);
const { data } = await client.post<EvidenceOut>(`/tests/${testId}/evidence`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return data;
}
/** Get evidence metadata with download URL. */
export async function getEvidence(evidenceId: string): Promise<EvidenceOut> {
const { data } = await client.get<EvidenceOut>(`/evidence/${evidenceId}`);
return data;
}

View File

@@ -0,0 +1,14 @@
import client from "./client";
import type { CoverageSummary, TacticCoverage } from "../types/models";
/** Fetch the global coverage summary. */
export async function getCoverageSummary(): Promise<CoverageSummary> {
const { data } = await client.get<CoverageSummary>("/metrics/summary");
return data;
}
/** Fetch coverage breakdown by tactic. */
export async function getCoverageByTactic(): Promise<TacticCoverage[]> {
const { data } = await client.get<TacticCoverage[]>("/metrics/by-tactic");
return data;
}

View File

@@ -0,0 +1,41 @@
import client from "./client";
export interface SyncMitreResponse {
message: string;
new: number;
updated: number;
}
export interface IntelScanResponse {
message: string;
new_items: number;
}
export interface SchedulerJob {
id: string;
name: string;
next_run_time: string | null;
}
export interface SchedulerStatusResponse {
running: boolean;
jobs: SchedulerJob[];
}
/** Manually trigger MITRE ATT&CK sync. */
export async function triggerMitreSync(): Promise<SyncMitreResponse> {
const { data } = await client.post<SyncMitreResponse>("/system/sync-mitre");
return data;
}
/** Manually trigger threat intelligence scan. */
export async function triggerIntelScan(): Promise<IntelScanResponse> {
const { data } = await client.post<IntelScanResponse>("/system/run-intel-scan");
return data;
}
/** Get scheduler status. */
export async function getSchedulerStatus(): Promise<SchedulerStatusResponse> {
const { data } = await client.get<SchedulerStatusResponse>("/system/scheduler-status");
return data;
}

View File

@@ -0,0 +1,74 @@
import client from "./client";
import type { Technique, TechniqueStatus } from "../types/models";
/** Summary representation used in list endpoints. */
export interface TechniqueSummary {
id: string;
mitre_id: string;
name: string;
tactic: string | null;
status_global: TechniqueStatus;
review_required?: boolean;
}
/** Extended technique with tests for detail view. */
export interface TechniqueWithTests extends Technique {
tests?: Array<{
id: string;
technique_id: string;
name: string;
description: string | null;
platform: string | null;
procedure_text: string | null;
tool_used: string | null;
execution_date: string | null;
created_by: string | null;
result: "detected" | "not_detected" | "partially_detected" | null;
state: "draft" | "in_review" | "validated" | "rejected";
validated_by: string | null;
validated_at: string | null;
created_at: string;
}>;
intel_items?: Array<{
id: string;
technique_id: string | null;
url: string;
title: string | null;
source: string | null;
detected_at: string;
reviewed: boolean;
}>;
}
export interface TechniqueFilters {
tactic?: string;
status?: TechniqueStatus;
review_required?: boolean;
}
/** Fetch all techniques (summary). */
export async function getTechniques(filters?: TechniqueFilters): Promise<TechniqueSummary[]> {
const params = new URLSearchParams();
if (filters?.tactic) params.append("tactic", filters.tactic);
if (filters?.status) params.append("status", filters.status);
if (filters?.review_required !== undefined) {
params.append("review_required", String(filters.review_required));
}
const { data } = await client.get<TechniqueSummary[]>(
`/techniques${params.toString() ? `?${params}` : ""}`
);
return data;
}
/** Fetch a single technique by mitre_id (with tests). */
export async function getTechniqueByMitreId(mitreId: string): Promise<TechniqueWithTests> {
const { data } = await client.get<TechniqueWithTests>(`/techniques/${mitreId}`);
return data;
}
/** Mark a technique as reviewed. */
export async function markTechniqueReviewed(mitreId: string): Promise<TechniqueWithTests> {
const { data } = await client.patch<TechniqueWithTests>(`/techniques/${mitreId}/review`);
return data;
}

67
frontend/src/api/tests.ts Normal file
View File

@@ -0,0 +1,67 @@
import client from "./client";
import type { Test, TestResult } from "../types/models";
export interface TestCreatePayload {
technique_id: string;
name: string;
description?: string;
platform?: string;
procedure_text?: string;
tool_used?: string;
}
export interface TestUpdatePayload {
name?: string;
description?: string;
platform?: string;
procedure_text?: string;
tool_used?: string;
result?: TestResult;
}
export interface TestValidatePayload {
result: TestResult;
comments?: string;
}
export interface TestWithEvidences extends Test {
evidences?: Array<{
id: string;
test_id: string;
file_name: string;
sha256_hash: string;
uploaded_by: string | null;
uploaded_at: string;
download_url?: string;
}>;
}
/** Create a new test. */
export async function createTest(payload: TestCreatePayload): Promise<Test> {
const { data } = await client.post<Test>("/tests", payload);
return data;
}
/** Get test by ID with evidences. */
export async function getTestById(testId: string): Promise<TestWithEvidences> {
const { data } = await client.get<TestWithEvidences>(`/tests/${testId}`);
return data;
}
/** Update a test (only draft/rejected). */
export async function updateTest(testId: string, payload: TestUpdatePayload): Promise<Test> {
const { data } = await client.patch<Test>(`/tests/${testId}`, payload);
return data;
}
/** Validate a test. */
export async function validateTest(testId: string, payload: TestValidatePayload): Promise<Test> {
const { data } = await client.post<Test>(`/tests/${testId}/validate`, payload);
return data;
}
/** Reject a test. */
export async function rejectTest(testId: string): Promise<Test> {
const { data } = await client.post<Test>(`/tests/${testId}/reject`);
return data;
}