import client from "./client"; import type { Test, TestResult, ContainmentResult, AttackSuccessResult, DataClassification, 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?: AttackSuccessResult; red_summary?: string; execution_start_time?: string; execution_end_time?: string; } export interface BlueUpdatePayload { detection_result?: TestResult; containment_result?: ContainmentResult; detection_time?: string; containment_time?: string; blue_summary?: string; detect_procedure?: 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; /** Free-text filter on technique MITRE ID or name — doesn't require knowing the technique's UUID. */ technique_search?: string; platform?: string; created_by?: string; pending_validation_side?: "red" | "blue"; /** "My reviews" queue — tests assigned to this reviewer at the red_review/blue_review gate. */ reviewer_id?: string; not_in_any_campaign?: boolean; attack_success?: AttackSuccessResult; detection_result?: TestResult; /** ISO date/datetime — filters on whichever lead validated last. */ validated_from?: string; validated_to?: string; assigned_to_me?: boolean; unassigned_red?: boolean; unassigned_blue?: boolean; offset?: number; limit?: number; } function buildTestListParams(filters?: TestListFilters): URLSearchParams { 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?.technique_search) params.append("technique_search", filters.technique_search); 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?.reviewer_id) params.append("reviewer_id", filters.reviewer_id); if (filters?.not_in_any_campaign) params.append("not_in_any_campaign", "true"); if (filters?.attack_success) params.append("attack_success", filters.attack_success); if (filters?.detection_result) params.append("detection_result", filters.detection_result); if (filters?.validated_from) params.append("validated_from", filters.validated_from); if (filters?.validated_to) params.append("validated_to", filters.validated_to); return params; } // ── CRUD ─────────────────────────────────────────────────────────── /** List tests with optional filters. */ export async function getTests(filters?: TestListFilters): Promise { const params = buildTestListParams(filters); 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( `/tests${params.toString() ? `?${params}` : ""}`, ); return data; } /** Return the true total of tests matching the given filters (unaffected by pagination limits). */ export async function getTestsCount(filters?: TestListFilters): Promise { const params = buildTestListParams(filters); const { data } = await client.get<{ total: number }>( `/tests/count${params.toString() ? `?${params}` : ""}`, ); return data.total; } /** Create a new test. */ export async function createTest(payload: TestCreatePayload): Promise { const { data } = await client.post("/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; detect_procedure?: string; }, ): Promise { const { data } = await client.post("/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 { const { data } = await client.get(`/tests/${testId}`); return data; } /** Update a test (only draft/rejected). */ export async function updateTest( testId: string, payload: TestUpdatePayload, ): Promise { const { data } = await client.patch(`/tests/${testId}`, payload); return data; } /** Update the data classification label for a test (admin only). */ export async function updateTestClassification( testId: string, dataClassification: DataClassification, ): Promise { const { data } = await client.patch(`/tests/${testId}/classification`, { data_classification: dataClassification, }); return data; } /** Assign operators or hand off a review to a peer lead (leads + managers only). */ export async function assignTestOperators( testId: string, payload: { red_tech_assignee?: string | null; blue_tech_assignee?: string | null; red_reviewer_assignee?: string | null; blue_reviewer_assignee?: string | null; }, ): Promise { const { data } = await client.post(`/tests/${testId}/assign`, payload); return data; } // ── Red Team ─────────────────────────────────────────────────────── /** Red Team updates their fields (draft, red_executing). */ export async function updateTestRed( testId: string, payload: RedUpdatePayload, ): Promise { const { data } = await client.patch(`/tests/${testId}/red`, payload); return data; } /** Move test from draft → red_executing. */ export async function startExecution(testId: string): Promise { const { data } = await client.post(`/tests/${testId}/start-execution`); return data; } /** Red Team finalises — red_executing → blue_evaluating. */ export async function submitRedEvidence(testId: string): Promise { const { data } = await client.post(`/tests/${testId}/submit-red`); return data; } // ── Timer Controls ───────────────────────────────────────────────── /** Pause the active phase timer. */ export async function pauseTimer(testId: string): Promise { const { data } = await client.post(`/tests/${testId}/pause-timer`); return data; } /** Resume a paused phase timer. */ export async function resumeTimer(testId: string): Promise { const { data } = await client.post(`/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 { const { data } = await client.patch(`/tests/${testId}/blue`, payload); return data; } /** Blue Team finalises — blue_evaluating → in_review. */ export async function submitBlueEvidence(testId: string): Promise { const { data } = await client.post(`/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 { const { data } = await client.post(`/tests/${testId}/start-blue-work`); return data; } // ── Lead Review Gates (red_review / blue_review) ──────────────────── export interface RedReviewPayload { decision: "approve" | "reopen"; notes?: string; } export interface BlueReviewPayload { decision: "approve" | "reopen" | "gap"; notes?: string; system_gaps?: string; } /** Assigned Red Lead approves or reopens a test sitting in red_review. */ export async function reviewAsRedLead(testId: string, payload: RedReviewPayload): Promise { const { data } = await client.post(`/tests/${testId}/review-red`, payload); return data; } /** Assigned Blue Lead approves, reopens, or flags a capability gap on a test in blue_review. */ export async function reviewAsBlueLead(testId: string, payload: BlueReviewPayload): Promise { const { data } = await client.post(`/tests/${testId}/review-blue`, payload); return data; } // ── Lead Validation ──────────────────────────────────────────────── /** Red Lead approves/rejects the red side. */ export async function validateAsRedLead( testId: string, payload: RedValidationPayload, ): Promise { const { data } = await client.post( `/tests/${testId}/validate-red`, payload, ); return data; } /** Blue Lead approves/rejects the blue side. */ export async function validateAsBlueLead( testId: string, payload: BlueValidationPayload, ): Promise { const { data } = await client.post( `/tests/${testId}/validate-blue`, payload, ); return data; } // ── Dispute resolution ─────────────────────────────────────────────── export interface ResolveDisputePayload { target_team: "red" | "blue"; notes?: string; } /** The approving lead flips to reject, choosing which team must redo the work. */ export async function resolveDispute(testId: string, payload: ResolveDisputePayload): Promise { const { data } = await client.post(`/tests/${testId}/resolve-dispute`, payload); return data; } /** Either lead on a disputed test asks a manager to step in and decide. */ export async function escalateToManager(testId: string): Promise<{ status: string; message: string }> { const { data } = await client.post(`/tests/${testId}/escalate-to-manager`); return data; } export interface ManagerResolveDisputePayload { outcome: "validated" | "rejected"; notes?: string; } /** A manager decides the final outcome of a disputed test directly. */ export async function managerResolveDispute( testId: string, payload: ManagerResolveDisputePayload, ): Promise { const { data } = await client.post(`/tests/${testId}/manager-resolve-dispute`, payload); return data; } // ── Reopen ───────────────────────────────────────────────────────── /** Reopen a rejected test — moves back to draft. */ export async function reopenTest(testId: string): Promise { const { data } = await client.post(`/tests/${testId}/reopen`); return data; } // ── Assignment ───────────────────────────────────────────────────── /** Assign red_tech and/or blue_tech operators to a test. Admin/leads only. */ export async function assignTest( testId: string, payload: { red_tech_assignee?: string | null; blue_tech_assignee?: string | null }, ): Promise { const { data } = await client.post(`/tests/${testId}/assign`, payload); return data; } // ── On Hold ──────────────────────────────────────────────────────── /** Place a test on hold with a mandatory reason. */ export async function holdTest(testId: string, reason: string): Promise { const { data } = await client.post(`/tests/${testId}/hold`, { reason }); return data; } /** Resume a test that was placed on hold. */ export async function resumeTest(testId: string): Promise { const { data } = await client.post(`/tests/${testId}/resume`); return data; } /** Delete a standalone test that hasn't started yet. Manager only. */ export async function deleteTest(testId: string): Promise { await client.delete(`/tests/${testId}`); } // ── Timeline ─────────────────────────────────────────────────────── /** Get the audit-log timeline for a test. */ export async function getTestTimeline( testId: string, ): Promise { const { data } = await client.get( `/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 { const { data } = await client.get(`/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 { const { data } = await client.post( `/tests/${testId}/validate`, payload, ); return data; } /** Reject a test (legacy endpoint). */ export async function rejectTest(testId: string): Promise { const { data } = await client.post(`/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 RTEvidenceEntry { filename: string; // e.g. "screenshot_edr.png" data: string; // base64-encoded image (PNG / JPG / GIF / WebP / BMP) caption?: string; // optional description } export interface RTTechniqueEntry { mitre_id: string; result: "detected" | "not_detected" | "partially_detected"; attack_success: AttackSuccessResult; platform?: string; notes?: string; evidence: RTEvidenceEntry[]; // required — at least 1 image } 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: AttackSuccessResult; evidence_attached: number }[]; 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_full_name: string | null; 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 { const { data } = await client.post("/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; }