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:
2026-02-09 10:57:48 +01:00
parent 9d7832c571
commit d660bceeb4
6 changed files with 447 additions and 68 deletions

View File

@@ -1,5 +1,12 @@
import client from "./client";
import type { Test, TestResult } from "../types/models";
import type {
Test,
TestResult,
TestState,
TestTimelineEntry,
} from "../types/models";
// ── Payloads ───────────────────────────────────────────────────────
export interface TestCreatePayload {
technique_id: string;
@@ -19,21 +26,57 @@ export interface TestUpdatePayload {
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 {
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;
}>;
export interface TestListFilters {
state?: TestState;
technique_id?: string;
offset?: number;
limit?: number;
}
// ── CRUD ───────────────────────────────────────────────────────────
/** 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. */
@@ -42,25 +85,134 @@ export async function createTest(payload: TestCreatePayload): Promise<Test> {
return data;
}
/** Get test by ID with evidences. */
export async function getTestById(testId: string): Promise<TestWithEvidences> {
const { data } = await client.get<TestWithEvidences>(`/tests/${testId}`);
/** Create a test from an existing template. */
export async function createTestFromTemplate(
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;
}
/** 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);
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);
// ── Red Team ───────────────────────────────────────────────────────
/** 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;
}
/** 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> {
const { data } = await client.post<Test>(`/tests/${testId}/reject`);
return data;