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:
30
frontend/src/api/evidence.ts
Normal file
30
frontend/src/api/evidence.ts
Normal 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;
|
||||
}
|
||||
14
frontend/src/api/metrics.ts
Normal file
14
frontend/src/api/metrics.ts
Normal 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;
|
||||
}
|
||||
41
frontend/src/api/system.ts
Normal file
41
frontend/src/api/system.ts
Normal 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;
|
||||
}
|
||||
74
frontend/src/api/techniques.ts
Normal file
74
frontend/src/api/techniques.ts
Normal 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
67
frontend/src/api/tests.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user