Files
Aegis/frontend/src/api/tests.ts
kitos 02ff89401c
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
fix(disputed): add admin role + contact info in discussion modal
- request-discussion endpoint: add 'admin' to allowed roles
- Return rejector_email and rejector_role in the response
- Modal success state shows contact card with username, role, email link
  so the approving lead can immediately reach out to the rejecting lead
2026-06-03 13:02:57 +02:00

338 lines
11 KiB
TypeScript

import client from "./client";
import type {
Test,
TestResult,
TestState,
TestTimelineEntry,
} from "../types/models";
// ── Payloads ───────────────────────────────────────────────────────
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 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 TestListFilters {
state?: TestState;
technique_id?: string;
platform?: string;
created_by?: string;
pending_validation_side?: "red" | "blue";
not_in_any_campaign?: boolean;
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?.platform) params.append("platform", filters.platform);
if (filters?.created_by) params.append("created_by", filters.created_by);
if (filters?.pending_validation_side) params.append("pending_validation_side", filters.pending_validation_side);
if (filters?.not_in_any_campaign) params.append("not_in_any_campaign", "true");
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. */
export async function createTest(payload: TestCreatePayload): Promise<Test> {
const { data } = await client.post<Test>("/tests", payload);
return data;
}
/** Create a test from an existing template, with optional field overrides. */
export async function createTestFromTemplate(
templateId: string,
techniqueId: string,
overrides?: {
name?: string;
description?: string;
platform?: string;
procedure_text?: string;
tool_used?: string;
},
): Promise<Test> {
const { data } = await client.post<Test>("/tests/from-template", {
template_id: templateId,
technique_id: techniqueId,
...overrides,
});
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> {
const { data } = await client.patch<Test>(`/tests/${testId}`, payload);
return data;
}
// ── 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;
}
/** 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;
}
// ── Timer Controls ─────────────────────────────────────────────────
/** Pause the active phase timer. */
export async function pauseTimer(testId: string): Promise<Test> {
const { data } = await client.post<Test>(`/tests/${testId}/pause-timer`);
return data;
}
/** Resume a paused phase timer. */
export async function resumeTimer(testId: string): Promise<Test> {
const { data } = await client.post<Test>(`/tests/${testId}/resume-timer`);
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;
}
/** Blue tech picks up the test to start evaluating — sets the Tempo timer start. */
export async function startBlueWork(testId: string): Promise<Test> {
const { data } = await client.post<Test>(`/tests/${testId}/start-blue-work`);
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;
}
// ── Retest Chain ────────────────────────────────────────────────────
export interface RetestChainEntry {
id: string;
name: string;
state: string | null;
retest_of: string | null;
retest_count: number;
result: string | null;
detection_result: string | null;
remediation_status: string | null;
created_at: string | null;
}
/** Get the full retest chain for a test. */
export async function getRetestChain(testId: string): Promise<RetestChainEntry[]> {
const { data } = await client.get<RetestChainEntry[]>(`/tests/${testId}/retest-chain`);
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;
}
// ── Tempo sync ─────────────────────────────────────────────────────
export interface TempoSyncResult {
worklog_id: string;
status: "synced" | "already_synced" | "skipped" | "error";
detail?: string;
}
// ── RT Import ──────────────────────────────────────────────────────
export interface RTTechniqueEntry {
mitre_id: string;
result: "detected" | "not_detected" | "partially_detected";
attack_success: boolean;
platform?: string;
notes?: string;
}
export interface RTImportPayload {
name: string;
date?: string;
description?: string;
operator?: string;
techniques: RTTechniqueEntry[];
}
export interface RTImportResult {
created: number;
skipped: number;
items: { mitre_id: string; test_name: string; result: string; attack_success: boolean }[];
warnings: { mitre_id: string; reason: string }[];
engagement: string;
}
/** Confirm approval in a disputed test and notify the rejecting lead to discuss. */
export async function requestDiscussion(testId: string): Promise<{
status: string;
message: string;
rejector_username: string;
rejector_email: string | null;
rejector_role: string;
}> {
const { data } = await client.post(`/tests/${testId}/request-discussion`);
return data;
}
/** Import results from a real Red Team engagement. */
export async function importRT(payload: RTImportPayload): Promise<RTImportResult> {
const { data } = await client.post<RTImportResult>("/tests/import-rt", payload);
return data;
}
/** Manually push this test's red team execution worklog to Tempo. */
export async function syncTestToTempo(
testId: string,
): Promise<{ results: TempoSyncResult[] }> {
const { data } = await client.post<{ results: TempoSyncResult[] }>(
`/tests/${testId}/sync-tempo`,
);
return data;
}