Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
- 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
338 lines
11 KiB
TypeScript
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;
|
|
}
|