31e116b4ba
Aegis CI / lint-and-test (push) Has been cancelled
Pause/Resume timer:
- Add paused_at, red_paused_seconds, blue_paused_seconds fields to Test model
- Add pause_timer/resume_timer workflow functions with accumulated pause tracking
- Auto-resume on phase submit; subtract paused time from worklog duration
- Add POST /tests/{id}/pause-timer and resume-timer endpoints
- Update LiveTimer component with pause/resume button and paused visual state
- Wire pause/resume mutations through TestDetailPage and TestDetailHeader
Professional Reporting Engine - Fase 2:
- Add ReportEngine service with Jinja2 HTML rendering, WeasyPrint PDF, and docxtpl DOCX
- Add corporate CSS stylesheet with cover page, data tables, stats grid, findings
- Create purple_campaign, coverage_report, and executive_summary HTML templates
- Add report_generation_service collecting domain data for each report type
- Add professional_reports router: GET /reports/generate/purple-campaign/{id}, coverage-summary, executive-summary
- Add analytics router with flat JSON endpoints for PowerBI: /coverage, /tests, /trends, /operators
- Add advanced_metrics router: /coverage-by-tactic, /never-tested, /avg-validation-time, /detection-rate-trend
- Add weasyprint and docxtpl to requirements.txt
- Add REPORT_TEMPLATES_DIR, REPORT_OUTPUT_DIR, COMPANY_NAME, COMPANY_LOGO_PATH to config
260 lines
8.3 KiB
TypeScript
260 lines
8.3 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";
|
|
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?.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. */
|
|
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> {
|
|
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;
|
|
}
|
|
|
|
// ── 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;
|
|
}
|