feat(phase-13): update frontend types and API clients for Red/Blue workflow (T-113, T-114)
T-113: Rewrite models.ts with v2 types - TestState now includes red_executing/blue_evaluating, add TeamSide, ValidationStatus, TestTemplate, TestTemplateSummary, TestTimelineEntry types, RED_EDITABLE_STATES/BLUE_EDITABLE_STATES constants, and dual validation fields on Test interface. Remove old validated_by/validated_at references from TestDetailPage and techniques API. T-114: Rewrite tests.ts API client with 16 functions covering full Red/Blue workflow (createTestFromTemplate, updateTestRed/Blue, startExecution, submitRed/Blue, validateAsRedLead/BlueLead, reopenTest, getTestTimeline). Rewrite evidence.ts with team parameter on upload/list and new deleteEvidence. Create test-templates.ts with getTemplates, getTemplateById, getTemplatesByTechnique, createTemplate, importAtomicTests.
This commit is contained in:
@@ -1,30 +1,76 @@
|
|||||||
import client from "./client";
|
import client from "./client";
|
||||||
|
import type { Evidence, TeamSide } from "../types/models";
|
||||||
|
|
||||||
export interface EvidenceOut {
|
// ── Response type (with download URL) ──────────────────────────────
|
||||||
id: string;
|
|
||||||
test_id: string;
|
export interface EvidenceOut extends Evidence {
|
||||||
file_name: string;
|
|
||||||
sha256_hash: string;
|
|
||||||
uploaded_by: string | null;
|
|
||||||
uploaded_at: string;
|
|
||||||
download_url: string;
|
download_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Upload evidence file for a test. */
|
// ── Upload ─────────────────────────────────────────────────────────
|
||||||
export async function uploadEvidence(testId: string, file: File): Promise<EvidenceOut> {
|
|
||||||
|
/** Upload an evidence file for the given test.
|
||||||
|
*
|
||||||
|
* The ``team`` field is sent as form data alongside the file so the
|
||||||
|
* backend can enforce Red/Blue access control.
|
||||||
|
*/
|
||||||
|
export async function uploadEvidence(
|
||||||
|
testId: string,
|
||||||
|
file: File,
|
||||||
|
team: TeamSide,
|
||||||
|
notes?: string,
|
||||||
|
): Promise<EvidenceOut> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
formData.append("team", team);
|
||||||
|
if (notes) {
|
||||||
|
formData.append("notes", notes);
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await client.post<EvidenceOut>(`/tests/${testId}/evidence`, formData, {
|
const { data } = await client.post<EvidenceOut>(
|
||||||
|
`/tests/${testId}/evidence`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get evidence metadata with download URL. */
|
// ── List ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** List evidences for a test, optionally filtered by team. */
|
||||||
|
export async function getTestEvidences(
|
||||||
|
testId: string,
|
||||||
|
team?: TeamSide,
|
||||||
|
): Promise<EvidenceOut[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (team) params.append("team", team);
|
||||||
|
|
||||||
|
const { data } = await client.get<EvidenceOut[]>(
|
||||||
|
`/tests/${testId}/evidence${params.toString() ? `?${params}` : ""}`,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Detail ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Get evidence metadata with presigned download URL. */
|
||||||
export async function getEvidence(evidenceId: string): Promise<EvidenceOut> {
|
export async function getEvidence(evidenceId: string): Promise<EvidenceOut> {
|
||||||
const { data } = await client.get<EvidenceOut>(`/evidence/${evidenceId}`);
|
const { data } = await client.get<EvidenceOut>(`/evidence/${evidenceId}`);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Delete ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Delete an evidence record (only in editable states). */
|
||||||
|
export async function deleteEvidence(
|
||||||
|
evidenceId: string,
|
||||||
|
): Promise<{ detail: string }> {
|
||||||
|
const { data } = await client.delete<{ detail: string }>(
|
||||||
|
`/evidence/${evidenceId}`,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import client from "./client";
|
import client from "./client";
|
||||||
import type { Technique, TechniqueStatus } from "../types/models";
|
import type { Technique, TechniqueStatus, Test, IntelItem } from "../types/models";
|
||||||
|
|
||||||
/** Summary representation used in list endpoints. */
|
/** Summary representation used in list endpoints. */
|
||||||
export interface TechniqueSummary {
|
export interface TechniqueSummary {
|
||||||
@@ -13,31 +13,8 @@ export interface TechniqueSummary {
|
|||||||
|
|
||||||
/** Extended technique with tests for detail view. */
|
/** Extended technique with tests for detail view. */
|
||||||
export interface TechniqueWithTests extends Technique {
|
export interface TechniqueWithTests extends Technique {
|
||||||
tests?: Array<{
|
tests?: Test[];
|
||||||
id: string;
|
intel_items?: IntelItem[];
|
||||||
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 {
|
export interface TechniqueFilters {
|
||||||
|
|||||||
106
frontend/src/api/test-templates.ts
Normal file
106
frontend/src/api/test-templates.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import client from "./client";
|
||||||
|
import type { TestTemplate, TestTemplateSummary } from "../types/models";
|
||||||
|
|
||||||
|
// ── Filters ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TemplateFilters {
|
||||||
|
source?: string;
|
||||||
|
platform?: string;
|
||||||
|
severity?: string;
|
||||||
|
mitre_technique_id?: string;
|
||||||
|
search?: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Create payload ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface CreateTemplatePayload {
|
||||||
|
mitre_technique_id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
source?: string;
|
||||||
|
source_url?: string;
|
||||||
|
attack_procedure?: string;
|
||||||
|
expected_detection?: string;
|
||||||
|
platform?: string;
|
||||||
|
tool_suggested?: string;
|
||||||
|
severity?: string;
|
||||||
|
atomic_test_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Import response ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface ImportAtomicResponse {
|
||||||
|
message: string;
|
||||||
|
imported: number;
|
||||||
|
skipped: number;
|
||||||
|
total_parsed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── List (paginated, filtered) ─────────────────────────────────────
|
||||||
|
|
||||||
|
/** Fetch a paginated, filtered list of test templates. */
|
||||||
|
export async function getTemplates(
|
||||||
|
filters?: TemplateFilters,
|
||||||
|
): Promise<TestTemplateSummary[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters?.source) params.append("source", filters.source);
|
||||||
|
if (filters?.platform) params.append("platform", filters.platform);
|
||||||
|
if (filters?.severity) params.append("severity", filters.severity);
|
||||||
|
if (filters?.mitre_technique_id)
|
||||||
|
params.append("mitre_technique_id", filters.mitre_technique_id);
|
||||||
|
if (filters?.search) params.append("search", filters.search);
|
||||||
|
if (filters?.offset !== undefined)
|
||||||
|
params.append("offset", String(filters.offset));
|
||||||
|
if (filters?.limit !== undefined)
|
||||||
|
params.append("limit", String(filters.limit));
|
||||||
|
|
||||||
|
const { data } = await client.get<TestTemplateSummary[]>(
|
||||||
|
`/test-templates${params.toString() ? `?${params}` : ""}`,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Detail ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Fetch a single test template by ID. */
|
||||||
|
export async function getTemplateById(id: string): Promise<TestTemplate> {
|
||||||
|
const { data } = await client.get<TestTemplate>(`/test-templates/${id}`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── By technique ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Fetch all templates mapped to a specific MITRE technique. */
|
||||||
|
export async function getTemplatesByTechnique(
|
||||||
|
mitreId: string,
|
||||||
|
): Promise<TestTemplateSummary[]> {
|
||||||
|
const { data } = await client.get<TestTemplateSummary[]>(
|
||||||
|
`/test-templates/by-technique/${mitreId}`,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Create (admin) ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Create a custom test template. Admin only. */
|
||||||
|
export async function createTemplate(
|
||||||
|
payload: CreateTemplatePayload,
|
||||||
|
): Promise<TestTemplate> {
|
||||||
|
const { data } = await client.post<TestTemplate>(
|
||||||
|
"/test-templates",
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Import Atomic Red Team ─────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Trigger Atomic Red Team import. Admin only. */
|
||||||
|
export async function importAtomicTests(): Promise<ImportAtomicResponse> {
|
||||||
|
const { data } = await client.post<ImportAtomicResponse>(
|
||||||
|
"/system/import-atomic-tests",
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
import client from "./client";
|
import client from "./client";
|
||||||
import type { Test, TestResult } from "../types/models";
|
import type {
|
||||||
|
Test,
|
||||||
|
TestResult,
|
||||||
|
TestState,
|
||||||
|
TestTimelineEntry,
|
||||||
|
} from "../types/models";
|
||||||
|
|
||||||
|
// ── Payloads ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface TestCreatePayload {
|
export interface TestCreatePayload {
|
||||||
technique_id: string;
|
technique_id: string;
|
||||||
@@ -19,21 +26,57 @@ export interface TestUpdatePayload {
|
|||||||
result?: TestResult;
|
result?: TestResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RedUpdatePayload {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
procedure_text?: string;
|
||||||
|
tool_used?: string;
|
||||||
|
attack_success?: boolean;
|
||||||
|
red_summary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlueUpdatePayload {
|
||||||
|
detection_result?: TestResult;
|
||||||
|
blue_summary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RedValidationPayload {
|
||||||
|
red_validation_status: "approved" | "rejected";
|
||||||
|
red_validation_notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlueValidationPayload {
|
||||||
|
blue_validation_status: "approved" | "rejected";
|
||||||
|
blue_validation_notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Legacy payload — kept for backwards compat. */
|
||||||
export interface TestValidatePayload {
|
export interface TestValidatePayload {
|
||||||
result: TestResult;
|
result: TestResult;
|
||||||
comments?: string;
|
comments?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestWithEvidences extends Test {
|
export interface TestListFilters {
|
||||||
evidences?: Array<{
|
state?: TestState;
|
||||||
id: string;
|
technique_id?: string;
|
||||||
test_id: string;
|
offset?: number;
|
||||||
file_name: string;
|
limit?: number;
|
||||||
sha256_hash: string;
|
}
|
||||||
uploaded_by: string | null;
|
|
||||||
uploaded_at: string;
|
// ── CRUD ───────────────────────────────────────────────────────────
|
||||||
download_url?: string;
|
|
||||||
}>;
|
/** List tests with optional filters. */
|
||||||
|
export async function getTests(filters?: TestListFilters): Promise<Test[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters?.state) params.append("state", filters.state);
|
||||||
|
if (filters?.technique_id) params.append("technique_id", filters.technique_id);
|
||||||
|
if (filters?.offset !== undefined) params.append("offset", String(filters.offset));
|
||||||
|
if (filters?.limit !== undefined) params.append("limit", String(filters.limit));
|
||||||
|
|
||||||
|
const { data } = await client.get<Test[]>(
|
||||||
|
`/tests${params.toString() ? `?${params}` : ""}`,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new test. */
|
/** Create a new test. */
|
||||||
@@ -42,25 +85,134 @@ export async function createTest(payload: TestCreatePayload): Promise<Test> {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get test by ID with evidences. */
|
/** Create a test from an existing template. */
|
||||||
export async function getTestById(testId: string): Promise<TestWithEvidences> {
|
export async function createTestFromTemplate(
|
||||||
const { data } = await client.get<TestWithEvidences>(`/tests/${testId}`);
|
templateId: string,
|
||||||
|
techniqueId: string,
|
||||||
|
): Promise<Test> {
|
||||||
|
const { data } = await client.post<Test>("/tests/from-template", {
|
||||||
|
template_id: templateId,
|
||||||
|
technique_id: techniqueId,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get test by ID (with evidences). */
|
||||||
|
export async function getTestById(testId: string): Promise<Test> {
|
||||||
|
const { data } = await client.get<Test>(`/tests/${testId}`);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update a test (only draft/rejected). */
|
/** Update a test (only draft/rejected). */
|
||||||
export async function updateTest(testId: string, payload: TestUpdatePayload): Promise<Test> {
|
export async function updateTest(
|
||||||
|
testId: string,
|
||||||
|
payload: TestUpdatePayload,
|
||||||
|
): Promise<Test> {
|
||||||
const { data } = await client.patch<Test>(`/tests/${testId}`, payload);
|
const { data } = await client.patch<Test>(`/tests/${testId}`, payload);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Validate a test. */
|
// ── Red Team ───────────────────────────────────────────────────────
|
||||||
export async function validateTest(testId: string, payload: TestValidatePayload): Promise<Test> {
|
|
||||||
const { data } = await client.post<Test>(`/tests/${testId}/validate`, payload);
|
/** Red Team updates their fields (draft, red_executing). */
|
||||||
|
export async function updateTestRed(
|
||||||
|
testId: string,
|
||||||
|
payload: RedUpdatePayload,
|
||||||
|
): Promise<Test> {
|
||||||
|
const { data } = await client.patch<Test>(`/tests/${testId}/red`, payload);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reject a test. */
|
/** Move test from draft → red_executing. */
|
||||||
|
export async function startExecution(testId: string): Promise<Test> {
|
||||||
|
const { data } = await client.post<Test>(`/tests/${testId}/start-execution`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Red Team finalises — red_executing → blue_evaluating. */
|
||||||
|
export async function submitRedEvidence(testId: string): Promise<Test> {
|
||||||
|
const { data } = await client.post<Test>(`/tests/${testId}/submit-red`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Blue Team ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Blue Team updates their fields (blue_evaluating only). */
|
||||||
|
export async function updateTestBlue(
|
||||||
|
testId: string,
|
||||||
|
payload: BlueUpdatePayload,
|
||||||
|
): Promise<Test> {
|
||||||
|
const { data } = await client.patch<Test>(`/tests/${testId}/blue`, payload);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blue Team finalises — blue_evaluating → in_review. */
|
||||||
|
export async function submitBlueEvidence(testId: string): Promise<Test> {
|
||||||
|
const { data } = await client.post<Test>(`/tests/${testId}/submit-blue`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lead Validation ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Red Lead approves/rejects the red side. */
|
||||||
|
export async function validateAsRedLead(
|
||||||
|
testId: string,
|
||||||
|
payload: RedValidationPayload,
|
||||||
|
): Promise<Test> {
|
||||||
|
const { data } = await client.post<Test>(
|
||||||
|
`/tests/${testId}/validate-red`,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blue Lead approves/rejects the blue side. */
|
||||||
|
export async function validateAsBlueLead(
|
||||||
|
testId: string,
|
||||||
|
payload: BlueValidationPayload,
|
||||||
|
): Promise<Test> {
|
||||||
|
const { data } = await client.post<Test>(
|
||||||
|
`/tests/${testId}/validate-blue`,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Reopen ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Reopen a rejected test — moves back to draft. */
|
||||||
|
export async function reopenTest(testId: string): Promise<Test> {
|
||||||
|
const { data } = await client.post<Test>(`/tests/${testId}/reopen`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Timeline ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Get the audit-log timeline for a test. */
|
||||||
|
export async function getTestTimeline(
|
||||||
|
testId: string,
|
||||||
|
): Promise<TestTimelineEntry[]> {
|
||||||
|
const { data } = await client.get<TestTimelineEntry[]>(
|
||||||
|
`/tests/${testId}/timeline`,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Legacy (kept for backwards compat) ─────────────────────────────
|
||||||
|
|
||||||
|
/** Validate a test (legacy endpoint). */
|
||||||
|
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 (legacy endpoint). */
|
||||||
export async function rejectTest(testId: string): Promise<Test> {
|
export async function rejectTest(testId: string): Promise<Test> {
|
||||||
const { data } = await client.post<Test>(`/tests/${testId}/reject`);
|
const { data } = await client.post<Test>(`/tests/${testId}/reject`);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import { useState } from "react";
|
|||||||
|
|
||||||
const testStateBadgeColors: Record<TestState, string> = {
|
const testStateBadgeColors: Record<TestState, string> = {
|
||||||
draft: "bg-gray-800/50 text-gray-400 border-gray-600/30",
|
draft: "bg-gray-800/50 text-gray-400 border-gray-600/30",
|
||||||
|
red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30",
|
||||||
|
blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30",
|
||||||
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
|
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
|
||||||
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
||||||
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
|
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
|
||||||
@@ -60,7 +62,7 @@ export default function TestDetailPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uploadMutation = useMutation({
|
const uploadMutation = useMutation({
|
||||||
mutationFn: (file: File) => uploadEvidence(testId!, file),
|
mutationFn: (file: File) => uploadEvidence(testId!, file, "red"),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["test", testId] });
|
queryClient.invalidateQueries({ queryKey: ["test", testId] });
|
||||||
},
|
},
|
||||||
@@ -248,7 +250,7 @@ export default function TestDetailPage() {
|
|||||||
{/* Evidence List */}
|
{/* Evidence List */}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<EvidenceList
|
<EvidenceList
|
||||||
evidences={test.evidences || []}
|
evidences={[...(test.red_evidences || []), ...(test.blue_evidences || [])]}
|
||||||
onDownload={handleDownload}
|
onDownload={handleDownload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,11 +279,19 @@ export default function TestDetailPage() {
|
|||||||
{formatDate(test.execution_date)}
|
{formatDate(test.execution_date)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
{test.validated_at && (
|
{test.red_validated_at && (
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-medium uppercase text-gray-500">Validated</dt>
|
<dt className="text-xs font-medium uppercase text-gray-500">Red Validated</dt>
|
||||||
<dd className="mt-1 text-sm text-gray-300">
|
<dd className="mt-1 text-sm text-gray-300">
|
||||||
{formatDate(test.validated_at)}
|
{formatDate(test.red_validated_at)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{test.blue_validated_at && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-xs font-medium uppercase text-gray-500">Blue Validated</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-300">
|
||||||
|
{formatDate(test.blue_validated_at)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
/* ── Shared TypeScript types matching the backend Pydantic schemas ── */
|
/* ── Shared TypeScript types matching the backend Pydantic schemas ── */
|
||||||
|
|
||||||
|
// ── Users ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
role: string;
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Techniques ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface Technique {
|
export interface Technique {
|
||||||
id: string;
|
id: string;
|
||||||
mitre_id: string;
|
mitre_id: string;
|
||||||
@@ -30,6 +34,28 @@ export type TechniqueStatus =
|
|||||||
| "not_covered"
|
| "not_covered"
|
||||||
| "review_required";
|
| "review_required";
|
||||||
|
|
||||||
|
// ── Tests (v2 — Red/Blue dual validation) ──────────────────────────
|
||||||
|
|
||||||
|
export type TestState =
|
||||||
|
| "draft"
|
||||||
|
| "red_executing"
|
||||||
|
| "blue_evaluating"
|
||||||
|
| "in_review"
|
||||||
|
| "validated"
|
||||||
|
| "rejected";
|
||||||
|
|
||||||
|
export type TestResult = "detected" | "not_detected" | "partially_detected";
|
||||||
|
|
||||||
|
export type ValidationStatus = "pending" | "approved" | "rejected";
|
||||||
|
|
||||||
|
export type TeamSide = "red" | "blue";
|
||||||
|
|
||||||
|
/** States in which Red Team can edit / upload evidence. */
|
||||||
|
export const RED_EDITABLE_STATES: TestState[] = ["draft", "red_executing"];
|
||||||
|
|
||||||
|
/** States in which Blue Team can edit / upload evidence. */
|
||||||
|
export const BLUE_EDITABLE_STATES: TestState[] = ["blue_evaluating"];
|
||||||
|
|
||||||
export interface Test {
|
export interface Test {
|
||||||
id: string;
|
id: string;
|
||||||
technique_id: string;
|
technique_id: string;
|
||||||
@@ -42,13 +68,30 @@ export interface Test {
|
|||||||
created_by: string | null;
|
created_by: string | null;
|
||||||
result: TestResult | null;
|
result: TestResult | null;
|
||||||
state: TestState;
|
state: TestState;
|
||||||
validated_by: string | null;
|
|
||||||
validated_at: string | null;
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|
||||||
|
// Red Team fields
|
||||||
|
red_summary: string | null;
|
||||||
|
attack_success: boolean | null;
|
||||||
|
red_validated_by: string | null;
|
||||||
|
red_validated_at: string | null;
|
||||||
|
red_validation_status: ValidationStatus | null;
|
||||||
|
red_validation_notes: string | null;
|
||||||
|
|
||||||
|
// Blue Team fields
|
||||||
|
blue_summary: string | null;
|
||||||
|
detection_result: TestResult | null;
|
||||||
|
blue_validated_by: string | null;
|
||||||
|
blue_validated_at: string | null;
|
||||||
|
blue_validation_status: ValidationStatus | null;
|
||||||
|
blue_validation_notes: string | null;
|
||||||
|
|
||||||
|
// Separated evidences
|
||||||
|
red_evidences: Evidence[];
|
||||||
|
blue_evidences: Evidence[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TestState = "draft" | "in_review" | "validated" | "rejected";
|
// ── Evidence (v2 — with team) ──────────────────────────────────────
|
||||||
export type TestResult = "detected" | "not_detected" | "partially_detected";
|
|
||||||
|
|
||||||
export interface Evidence {
|
export interface Evidence {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -58,8 +101,51 @@ export interface Evidence {
|
|||||||
sha256_hash: string;
|
sha256_hash: string;
|
||||||
uploaded_by: string | null;
|
uploaded_by: string | null;
|
||||||
uploaded_at: string;
|
uploaded_at: string;
|
||||||
|
team: TeamSide;
|
||||||
|
notes: string | null;
|
||||||
|
download_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Test Templates ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TestTemplate {
|
||||||
|
id: string;
|
||||||
|
mitre_technique_id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
source: string;
|
||||||
|
source_url: string | null;
|
||||||
|
attack_procedure: string | null;
|
||||||
|
expected_detection: string | null;
|
||||||
|
platform: string | null;
|
||||||
|
tool_suggested: string | null;
|
||||||
|
severity: string | null;
|
||||||
|
atomic_test_id: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestTemplateSummary {
|
||||||
|
id: string;
|
||||||
|
mitre_technique_id: string;
|
||||||
|
name: string;
|
||||||
|
source: string;
|
||||||
|
platform: string | null;
|
||||||
|
severity: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Timeline ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TestTimelineEntry {
|
||||||
|
id: string;
|
||||||
|
action: string;
|
||||||
|
user_id: string | null;
|
||||||
|
timestamp: string;
|
||||||
|
details: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Intel ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface IntelItem {
|
export interface IntelItem {
|
||||||
id: string;
|
id: string;
|
||||||
technique_id: string | null;
|
technique_id: string | null;
|
||||||
@@ -70,6 +156,8 @@ export interface IntelItem {
|
|||||||
reviewed: boolean;
|
reviewed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Metrics ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface CoverageSummary {
|
export interface CoverageSummary {
|
||||||
total_techniques: number;
|
total_techniques: number;
|
||||||
validated: number;
|
validated: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user