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:
@@ -1,30 +1,76 @@
|
||||
import client from "./client";
|
||||
import type { Evidence, TeamSide } from "../types/models";
|
||||
|
||||
export interface EvidenceOut {
|
||||
id: string;
|
||||
test_id: string;
|
||||
file_name: string;
|
||||
sha256_hash: string;
|
||||
uploaded_by: string | null;
|
||||
uploaded_at: string;
|
||||
// ── Response type (with download URL) ──────────────────────────────
|
||||
|
||||
export interface EvidenceOut extends Evidence {
|
||||
download_url: string;
|
||||
}
|
||||
|
||||
/** Upload evidence file for a test. */
|
||||
export async function uploadEvidence(testId: string, file: File): Promise<EvidenceOut> {
|
||||
// ── Upload ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Upload an evidence file for the given test.
|
||||
*
|
||||
* The ``team`` field is sent as form data alongside the file so the
|
||||
* backend can enforce Red/Blue access control.
|
||||
*/
|
||||
export async function uploadEvidence(
|
||||
testId: string,
|
||||
file: File,
|
||||
team: TeamSide,
|
||||
notes?: string,
|
||||
): Promise<EvidenceOut> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("team", team);
|
||||
if (notes) {
|
||||
formData.append("notes", notes);
|
||||
}
|
||||
|
||||
const { data } = await client.post<EvidenceOut>(`/tests/${testId}/evidence`, formData, {
|
||||
const { data } = await client.post<EvidenceOut>(
|
||||
`/tests/${testId}/evidence`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Get evidence metadata with download URL. */
|
||||
// ── List ───────────────────────────────────────────────────────────
|
||||
|
||||
/** List evidences for a test, optionally filtered by team. */
|
||||
export async function getTestEvidences(
|
||||
testId: string,
|
||||
team?: TeamSide,
|
||||
): Promise<EvidenceOut[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (team) params.append("team", team);
|
||||
|
||||
const { data } = await client.get<EvidenceOut[]>(
|
||||
`/tests/${testId}/evidence${params.toString() ? `?${params}` : ""}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Detail ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Get evidence metadata with presigned download URL. */
|
||||
export async function getEvidence(evidenceId: string): Promise<EvidenceOut> {
|
||||
const { data } = await client.get<EvidenceOut>(`/evidence/${evidenceId}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Delete ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Delete an evidence record (only in editable states). */
|
||||
export async function deleteEvidence(
|
||||
evidenceId: string,
|
||||
): Promise<{ detail: string }> {
|
||||
const { data } = await client.delete<{ detail: string }>(
|
||||
`/evidence/${evidenceId}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import client from "./client";
|
||||
import type { Technique, TechniqueStatus } from "../types/models";
|
||||
import type { Technique, TechniqueStatus, Test, IntelItem } from "../types/models";
|
||||
|
||||
/** Summary representation used in list endpoints. */
|
||||
export interface TechniqueSummary {
|
||||
@@ -13,31 +13,8 @@ export interface TechniqueSummary {
|
||||
|
||||
/** Extended technique with tests for detail view. */
|
||||
export interface TechniqueWithTests extends Technique {
|
||||
tests?: Array<{
|
||||
id: string;
|
||||
technique_id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
platform: string | null;
|
||||
procedure_text: string | null;
|
||||
tool_used: string | null;
|
||||
execution_date: string | null;
|
||||
created_by: string | null;
|
||||
result: "detected" | "not_detected" | "partially_detected" | null;
|
||||
state: "draft" | "in_review" | "validated" | "rejected";
|
||||
validated_by: string | null;
|
||||
validated_at: string | null;
|
||||
created_at: string;
|
||||
}>;
|
||||
intel_items?: Array<{
|
||||
id: string;
|
||||
technique_id: string | null;
|
||||
url: string;
|
||||
title: string | null;
|
||||
source: string | null;
|
||||
detected_at: string;
|
||||
reviewed: boolean;
|
||||
}>;
|
||||
tests?: Test[];
|
||||
intel_items?: IntelItem[];
|
||||
}
|
||||
|
||||
export interface TechniqueFilters {
|
||||
|
||||
106
frontend/src/api/test-templates.ts
Normal file
106
frontend/src/api/test-templates.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import client from "./client";
|
||||
import type { TestTemplate, TestTemplateSummary } from "../types/models";
|
||||
|
||||
// ── Filters ────────────────────────────────────────────────────────
|
||||
|
||||
export interface TemplateFilters {
|
||||
source?: string;
|
||||
platform?: string;
|
||||
severity?: string;
|
||||
mitre_technique_id?: string;
|
||||
search?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// ── Create payload ─────────────────────────────────────────────────
|
||||
|
||||
export interface CreateTemplatePayload {
|
||||
mitre_technique_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
source?: string;
|
||||
source_url?: string;
|
||||
attack_procedure?: string;
|
||||
expected_detection?: string;
|
||||
platform?: string;
|
||||
tool_suggested?: string;
|
||||
severity?: string;
|
||||
atomic_test_id?: string;
|
||||
}
|
||||
|
||||
// ── Import response ────────────────────────────────────────────────
|
||||
|
||||
export interface ImportAtomicResponse {
|
||||
message: string;
|
||||
imported: number;
|
||||
skipped: number;
|
||||
total_parsed: number;
|
||||
}
|
||||
|
||||
// ── List (paginated, filtered) ─────────────────────────────────────
|
||||
|
||||
/** Fetch a paginated, filtered list of test templates. */
|
||||
export async function getTemplates(
|
||||
filters?: TemplateFilters,
|
||||
): Promise<TestTemplateSummary[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.source) params.append("source", filters.source);
|
||||
if (filters?.platform) params.append("platform", filters.platform);
|
||||
if (filters?.severity) params.append("severity", filters.severity);
|
||||
if (filters?.mitre_technique_id)
|
||||
params.append("mitre_technique_id", filters.mitre_technique_id);
|
||||
if (filters?.search) params.append("search", filters.search);
|
||||
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<TestTemplateSummary[]>(
|
||||
`/test-templates${params.toString() ? `?${params}` : ""}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Detail ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Fetch a single test template by ID. */
|
||||
export async function getTemplateById(id: string): Promise<TestTemplate> {
|
||||
const { data } = await client.get<TestTemplate>(`/test-templates/${id}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── By technique ───────────────────────────────────────────────────
|
||||
|
||||
/** Fetch all templates mapped to a specific MITRE technique. */
|
||||
export async function getTemplatesByTechnique(
|
||||
mitreId: string,
|
||||
): Promise<TestTemplateSummary[]> {
|
||||
const { data } = await client.get<TestTemplateSummary[]>(
|
||||
`/test-templates/by-technique/${mitreId}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Create (admin) ─────────────────────────────────────────────────
|
||||
|
||||
/** Create a custom test template. Admin only. */
|
||||
export async function createTemplate(
|
||||
payload: CreateTemplatePayload,
|
||||
): Promise<TestTemplate> {
|
||||
const { data } = await client.post<TestTemplate>(
|
||||
"/test-templates",
|
||||
payload,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Import Atomic Red Team ─────────────────────────────────────────
|
||||
|
||||
/** Trigger Atomic Red Team import. Admin only. */
|
||||
export async function importAtomicTests(): Promise<ImportAtomicResponse> {
|
||||
const { data } = await client.post<ImportAtomicResponse>(
|
||||
"/system/import-atomic-tests",
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -20,6 +20,8 @@ import { useState } from "react";
|
||||
|
||||
const testStateBadgeColors: Record<TestState, string> = {
|
||||
draft: "bg-gray-800/50 text-gray-400 border-gray-600/30",
|
||||
red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30",
|
||||
blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30",
|
||||
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
|
||||
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
||||
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
|
||||
@@ -60,7 +62,7 @@ export default function TestDetailPage() {
|
||||
});
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: (file: File) => uploadEvidence(testId!, file),
|
||||
mutationFn: (file: File) => uploadEvidence(testId!, file, "red"),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["test", testId] });
|
||||
},
|
||||
@@ -248,7 +250,7 @@ export default function TestDetailPage() {
|
||||
{/* Evidence List */}
|
||||
<div className="mt-6">
|
||||
<EvidenceList
|
||||
evidences={test.evidences || []}
|
||||
evidences={[...(test.red_evidences || []), ...(test.blue_evidences || [])]}
|
||||
onDownload={handleDownload}
|
||||
/>
|
||||
</div>
|
||||
@@ -277,11 +279,19 @@ export default function TestDetailPage() {
|
||||
{formatDate(test.execution_date)}
|
||||
</dd>
|
||||
</div>
|
||||
{test.validated_at && (
|
||||
{test.red_validated_at && (
|
||||
<div>
|
||||
<dt className="text-xs font-medium uppercase text-gray-500">Validated</dt>
|
||||
<dt className="text-xs font-medium uppercase text-gray-500">Red Validated</dt>
|
||||
<dd className="mt-1 text-sm text-gray-300">
|
||||
{formatDate(test.validated_at)}
|
||||
{formatDate(test.red_validated_at)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{test.blue_validated_at && (
|
||||
<div>
|
||||
<dt className="text-xs font-medium uppercase text-gray-500">Blue Validated</dt>
|
||||
<dd className="mt-1 text-sm text-gray-300">
|
||||
{formatDate(test.blue_validated_at)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
/* ── Shared TypeScript types matching the backend Pydantic schemas ── */
|
||||
|
||||
// ── Users ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// ── Techniques ─────────────────────────────────────────────────────
|
||||
|
||||
export interface Technique {
|
||||
id: string;
|
||||
mitre_id: string;
|
||||
@@ -30,6 +34,28 @@ export type TechniqueStatus =
|
||||
| "not_covered"
|
||||
| "review_required";
|
||||
|
||||
// ── Tests (v2 — Red/Blue dual validation) ──────────────────────────
|
||||
|
||||
export type TestState =
|
||||
| "draft"
|
||||
| "red_executing"
|
||||
| "blue_evaluating"
|
||||
| "in_review"
|
||||
| "validated"
|
||||
| "rejected";
|
||||
|
||||
export type TestResult = "detected" | "not_detected" | "partially_detected";
|
||||
|
||||
export type ValidationStatus = "pending" | "approved" | "rejected";
|
||||
|
||||
export type TeamSide = "red" | "blue";
|
||||
|
||||
/** States in which Red Team can edit / upload evidence. */
|
||||
export const RED_EDITABLE_STATES: TestState[] = ["draft", "red_executing"];
|
||||
|
||||
/** States in which Blue Team can edit / upload evidence. */
|
||||
export const BLUE_EDITABLE_STATES: TestState[] = ["blue_evaluating"];
|
||||
|
||||
export interface Test {
|
||||
id: string;
|
||||
technique_id: string;
|
||||
@@ -42,13 +68,30 @@ export interface Test {
|
||||
created_by: string | null;
|
||||
result: TestResult | null;
|
||||
state: TestState;
|
||||
validated_by: string | null;
|
||||
validated_at: string | null;
|
||||
created_at: string;
|
||||
|
||||
// Red Team fields
|
||||
red_summary: string | null;
|
||||
attack_success: boolean | null;
|
||||
red_validated_by: string | null;
|
||||
red_validated_at: string | null;
|
||||
red_validation_status: ValidationStatus | null;
|
||||
red_validation_notes: string | null;
|
||||
|
||||
// Blue Team fields
|
||||
blue_summary: string | null;
|
||||
detection_result: TestResult | null;
|
||||
blue_validated_by: string | null;
|
||||
blue_validated_at: string | null;
|
||||
blue_validation_status: ValidationStatus | null;
|
||||
blue_validation_notes: string | null;
|
||||
|
||||
// Separated evidences
|
||||
red_evidences: Evidence[];
|
||||
blue_evidences: Evidence[];
|
||||
}
|
||||
|
||||
export type TestState = "draft" | "in_review" | "validated" | "rejected";
|
||||
export type TestResult = "detected" | "not_detected" | "partially_detected";
|
||||
// ── Evidence (v2 — with team) ──────────────────────────────────────
|
||||
|
||||
export interface Evidence {
|
||||
id: string;
|
||||
@@ -58,8 +101,51 @@ export interface Evidence {
|
||||
sha256_hash: string;
|
||||
uploaded_by: string | null;
|
||||
uploaded_at: string;
|
||||
team: TeamSide;
|
||||
notes: string | null;
|
||||
download_url?: string;
|
||||
}
|
||||
|
||||
// ── Test Templates ─────────────────────────────────────────────────
|
||||
|
||||
export interface TestTemplate {
|
||||
id: string;
|
||||
mitre_technique_id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
source: string;
|
||||
source_url: string | null;
|
||||
attack_procedure: string | null;
|
||||
expected_detection: string | null;
|
||||
platform: string | null;
|
||||
tool_suggested: string | null;
|
||||
severity: string | null;
|
||||
atomic_test_id: string | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface TestTemplateSummary {
|
||||
id: string;
|
||||
mitre_technique_id: string;
|
||||
name: string;
|
||||
source: string;
|
||||
platform: string | null;
|
||||
severity: string | null;
|
||||
}
|
||||
|
||||
// ── Timeline ───────────────────────────────────────────────────────
|
||||
|
||||
export interface TestTimelineEntry {
|
||||
id: string;
|
||||
action: string;
|
||||
user_id: string | null;
|
||||
timestamp: string;
|
||||
details: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ── Intel ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface IntelItem {
|
||||
id: string;
|
||||
technique_id: string | null;
|
||||
@@ -70,6 +156,8 @@ export interface IntelItem {
|
||||
reviewed: boolean;
|
||||
}
|
||||
|
||||
// ── Metrics ────────────────────────────────────────────────────────
|
||||
|
||||
export interface CoverageSummary {
|
||||
total_techniques: number;
|
||||
validated: number;
|
||||
|
||||
Reference in New Issue
Block a user