feat(ui): add custom attack paths queries

- Add a custom openCypher query option in Attack Paths
- Show Cartography schema guidance for the selected scan
- Format query execution errors and cover the flow with tests
This commit is contained in:
Hugo P.Brito
2026-03-19 15:43:09 +00:00
parent cece2cb87e
commit a1e48e6b84
14 changed files with 593 additions and 63 deletions

View File

@@ -2,6 +2,14 @@
All notable changes to the **Prowler UI** are documented in this file. All notable changes to the **Prowler UI** are documented in this file.
## [1.22.0] (Prowler UNRELEASED)
### 🚀 Added
- Attack Paths custom openCypher queries with Cartography schema guidance and clearer execution errors (PROWLER-1119)
---
## [1.21.0] (Prowler v5.21.0) ## [1.21.0] (Prowler v5.21.0)
### 🚀 Added ### 🚀 Added

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import {
ATTACK_PATH_QUERY_IDS,
type AttackPathCartographySchemaAttributes,
type AttackPathQuery,
} from "@/types/attack-paths";
import { buildAttackPathQueries } from "./queries.adapter";
const presetQuery: AttackPathQuery = {
type: "attack-paths-scans",
id: "preset-query",
attributes: {
name: "Preset Query",
short_description: "Returns privileged attack paths",
description: "Returns privileged attack paths.",
provider: "aws",
attribution: null,
parameters: [],
},
};
describe("buildAttackPathQueries", () => {
it("prepends a custom openCypher query with a schema documentation link", () => {
// Given
const schema: AttackPathCartographySchemaAttributes = {
id: "aws-0.129.0",
provider: "aws",
cartography_version: "0.129.0",
schema_url:
"https://github.com/cartography-cncf/cartography/blob/0.129.0/docs/root/modules/aws/schema.md",
raw_schema_url:
"https://raw.githubusercontent.com/cartography-cncf/cartography/refs/tags/0.129.0/docs/root/modules/aws/schema.md",
};
// When
const result = buildAttackPathQueries([presetQuery], schema);
// Then
expect(result[0]).toMatchObject({
id: ATTACK_PATH_QUERY_IDS.CUSTOM,
attributes: {
name: "Custom openCypher query",
short_description: "Write and run your own read-only openCypher query",
documentation_link: {
text: "Cartography schema used by Prowler for AWS graphs",
link: schema.schema_url,
},
},
});
expect(result[1]).toEqual(presetQuery);
});
});

View File

@@ -1,7 +1,10 @@
import { MetaDataProps } from "@/types"; import { MetaDataProps } from "@/types";
import { import {
ATTACK_PATH_QUERY_IDS,
type AttackPathCartographySchemaAttributes,
AttackPathQueriesResponse, AttackPathQueriesResponse,
AttackPathQuery, AttackPathQuery,
QUERY_PARAMETER_INPUT_TYPES,
} from "@/types/attack-paths"; } from "@/types/attack-paths";
/** /**
@@ -53,3 +56,52 @@ export function adaptAttackPathQueriesResponse(
return { data: enrichedData, metadata }; return { data: enrichedData, metadata };
} }
const CUSTOM_QUERY_PLACEHOLDER = `MATCH (n)
RETURN n
LIMIT 25`;
const formatSchemaDocumentationLinkText = (
schema: AttackPathCartographySchemaAttributes,
): string => {
return `Cartography schema used by Prowler for ${schema.provider.toUpperCase()} graphs`;
};
const createCustomQuery = (
schema?: AttackPathCartographySchemaAttributes,
): AttackPathQuery => ({
type: "attack-paths-scans",
id: ATTACK_PATH_QUERY_IDS.CUSTOM,
attributes: {
name: "Custom openCypher query",
short_description: "Write and run your own read-only openCypher query",
description:
"Run a read-only openCypher query against the selected Attack Paths scan. Results are automatically scoped to the selected provider.",
provider: "custom",
attribution: null,
documentation_link: schema
? {
text: formatSchemaDocumentationLinkText(schema),
link: schema.schema_url,
}
: null,
parameters: [
{
name: "query",
label: "openCypher query",
data_type: "string",
description: "",
placeholder: CUSTOM_QUERY_PLACEHOLDER,
required: true,
input_type: QUERY_PARAMETER_INPUT_TYPES.TEXTAREA,
},
],
},
});
export const buildAttackPathQueries = (
queries: AttackPathQuery[],
schema?: AttackPathCartographySchemaAttributes,
): AttackPathQuery[] => {
return [createCustomQuery(schema), ...queries];
};

View File

@@ -17,7 +17,11 @@ vi.mock("@/lib/server-actions-helper", () => ({
handleApiResponse: handleApiResponseMock, handleApiResponse: handleApiResponseMock,
})); }));
import { executeQuery } from "./queries"; import {
executeCustomQuery,
executeQuery,
getCartographySchema,
} from "./queries";
describe("executeQuery", () => { describe("executeQuery", () => {
beforeEach(() => { beforeEach(() => {
@@ -65,3 +69,91 @@ describe("executeQuery", () => {
expect(handleApiResponseMock).not.toHaveBeenCalled(); expect(handleApiResponseMock).not.toHaveBeenCalled();
}); });
}); });
describe("executeCustomQuery", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("fetch", fetchMock);
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
handleApiResponseMock.mockResolvedValue({
data: {
type: "attack-paths-query-run-requests",
id: null,
attributes: {
nodes: [],
relationships: [],
},
},
});
});
it("posts the custom query to the dedicated endpoint", async () => {
// Given
fetchMock.mockResolvedValue(new Response(null, { status: 200 }));
// When
await executeCustomQuery(
"550e8400-e29b-41d4-a716-446655440000",
"MATCH (n) RETURN n LIMIT 10",
);
// Then
expect(fetchMock).toHaveBeenCalledWith(
"https://api.example.com/api/v1/attack-paths-scans/550e8400-e29b-41d4-a716-446655440000/queries/custom",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
data: {
type: "attack-paths-custom-query-run-requests",
attributes: {
query: "MATCH (n) RETURN n LIMIT 10",
},
},
}),
}),
);
});
});
describe("getCartographySchema", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("fetch", fetchMock);
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
});
it("fetches the schema metadata for the selected scan", async () => {
// Given
const apiResponse = {
data: {
type: "attack-paths-cartography-schemas",
id: "aws-0.129.0",
attributes: {
id: "aws-0.129.0",
provider: "aws",
cartography_version: "0.129.0",
schema_url:
"https://github.com/cartography-cncf/cartography/blob/0.129.0/docs/root/modules/aws/schema.md",
raw_schema_url:
"https://raw.githubusercontent.com/cartography-cncf/cartography/refs/tags/0.129.0/docs/root/modules/aws/schema.md",
},
},
};
fetchMock.mockResolvedValue(new Response(null, { status: 200 }));
handleApiResponseMock.mockResolvedValue(apiResponse);
// When
const result = await getCartographySchema(
"550e8400-e29b-41d4-a716-446655440000",
);
// Then
expect(fetchMock).toHaveBeenCalledWith(
"https://api.example.com/api/v1/attack-paths-scans/550e8400-e29b-41d4-a716-446655440000/schema",
expect.objectContaining({
method: "GET",
}),
);
expect(result).toEqual(apiResponse);
});
});

View File

@@ -5,10 +5,13 @@ import { z } from "zod";
import { apiBaseUrl, getAuthHeaders } from "@/lib"; import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiResponse } from "@/lib/server-actions-helper"; import { handleApiResponse } from "@/lib/server-actions-helper";
import { import {
AttackPathCartographySchema,
AttackPathCartographySchemaResponse,
AttackPathQueriesResponse, AttackPathQueriesResponse,
AttackPathQuery, AttackPathQuery,
AttackPathQueryError, AttackPathQueryError,
AttackPathQueryResult, AttackPathQueryResult,
ExecuteCustomQueryRequest,
ExecuteQueryRequest, ExecuteQueryRequest,
} from "@/types/attack-paths"; } from "@/types/attack-paths";
@@ -102,3 +105,84 @@ export const executeQuery = async (
}; };
} }
}; };
/**
* Execute a custom openCypher query on an attack path scan
*/
export const executeCustomQuery = async (
scanId: string,
query: string,
): Promise<AttackPathQueryResult | AttackPathQueryError | undefined> => {
const validatedScanId = UUIDSchema.safeParse(scanId);
if (!validatedScanId.success) {
console.error("Invalid scan ID format");
return undefined;
}
const headers = await getAuthHeaders({ contentType: true });
const requestBody: ExecuteCustomQueryRequest = {
data: {
type: "attack-paths-custom-query-run-requests",
attributes: {
query,
},
},
};
try {
const response = await fetch(
`${apiBaseUrl}/attack-paths-scans/${validatedScanId.data}/queries/custom`,
{
headers,
method: "POST",
body: JSON.stringify(requestBody),
},
);
return (await handleApiResponse(response)) as
| AttackPathQueryResult
| AttackPathQueryError;
} catch (error) {
console.error("Error executing custom query on scan:", error);
return {
error:
"Server is temporarily unavailable. Please try again in a few minutes.",
status: 503,
};
}
};
/**
* Fetch cartography schema metadata for a specific attack path scan
*/
export const getCartographySchema = async (
scanId: string,
): Promise<{ data: AttackPathCartographySchema } | undefined> => {
const validatedScanId = UUIDSchema.safeParse(scanId);
if (!validatedScanId.success) {
console.error("Invalid scan ID format");
return undefined;
}
const headers = await getAuthHeaders({ contentType: false });
try {
const response = await fetch(
`${apiBaseUrl}/attack-paths-scans/${validatedScanId.data}/schema`,
{
headers,
method: "GET",
},
);
const apiResponse = (await handleApiResponse(
response,
)) as AttackPathCartographySchemaResponse;
return { data: apiResponse.data };
} catch (error) {
console.error("Error fetching cartography schema for scan:", error);
return undefined;
}
};

View File

@@ -1,6 +1,8 @@
export { ExecuteButton } from "./execute-button"; export { ExecuteButton } from "./execute-button";
export * from "./graph"; export * from "./graph";
export * from "./node-detail"; export * from "./node-detail";
export { QueryDescription } from "./query-description";
export { QueryExecutionError } from "./query-execution-error";
export { QueryParametersForm } from "./query-parameters-form"; export { QueryParametersForm } from "./query-parameters-form";
export { QuerySelector } from "./query-selector"; export { QuerySelector } from "./query-selector";
export { ScanListTable } from "./scan-list-table"; export { ScanListTable } from "./scan-list-table";

View File

@@ -0,0 +1,39 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import type { AttackPathQuery } from "@/types/attack-paths";
import { QueryDescription } from "./query-description";
const customQuery: AttackPathQuery = {
type: "attack-paths-scans",
id: "custom-query",
attributes: {
name: "Custom openCypher query",
short_description: "Write your own query",
description:
"Run a read-only openCypher query against the selected Attack Paths scan.",
provider: "aws",
attribution: null,
documentation_link: {
text: "Cartography schema used by Prowler for AWS graphs",
link: "https://example.com/schema",
},
parameters: [],
},
};
describe("QueryDescription", () => {
it("renders the schema documentation link when the selected query provides one", () => {
// Given
render(<QueryDescription query={customQuery} />);
// When
const link = screen.getByRole("link", {
name: /cartography schema used by prowler for aws graphs/i,
});
// Then
expect(link).toHaveAttribute("href", "https://example.com/schema");
});
});

View File

@@ -0,0 +1,50 @@
import { Info } from "lucide-react";
import type { AttackPathQuery } from "@/types/attack-paths";
interface QueryDescriptionProps {
query: AttackPathQuery;
}
export const QueryDescription = ({ query }: QueryDescriptionProps) => {
return (
<div className="bg-bg-neutral-tertiary text-text-neutral-secondary dark:text-text-neutral-secondary rounded-md px-3 py-2 text-sm">
<div className="flex items-start gap-2">
<Info
className="mt-0.5 size-4 shrink-0"
style={{ color: "var(--bg-data-info)" }}
/>
<div className="flex flex-col gap-2">
<p className="whitespace-pre-line">{query.attributes.description}</p>
{query.attributes.documentation_link && (
<p className="text-xs">
<a
href={query.attributes.documentation_link.link}
target="_blank"
rel="noopener noreferrer"
className="font-medium underline"
>
{query.attributes.documentation_link.text}
</a>
</p>
)}
{query.attributes.attribution && (
<p className="text-xs">
Source:{" "}
<a
href={query.attributes.attribution.link}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{query.attributes.attribution.text}
</a>
</p>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,24 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { QueryExecutionError } from "./query-execution-error";
describe("QueryExecutionError", () => {
it("renders a formatted error alert with the raw query error details", () => {
// Given
const error =
"Invalid input 'WHERE': expected 'MATCH' or 'WITH' (line 1, column 1)";
// When
render(<QueryExecutionError error={error} />);
// Then
expect(
screen.getByRole("heading", { name: /query execution failed/i }),
).toBeInTheDocument();
expect(
screen.getByText(/the attack paths query could not be executed/i),
).toBeInTheDocument();
expect(screen.getByText(error)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,24 @@
import { CircleAlert } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/shadcn";
interface QueryExecutionErrorProps {
error: string;
}
export const QueryExecutionError = ({ error }: QueryExecutionErrorProps) => {
return (
<Alert variant="error">
<CircleAlert className="size-4" />
<AlertTitle>Query execution failed</AlertTitle>
<AlertDescription className="w-full gap-3">
<p>The Attack Paths query could not be executed.</p>
<div className="bg-bg-neutral-primary/70 border-border-neutral-secondary w-full rounded-md border px-3 py-2">
<pre className="text-text-error-primary font-mono text-xs break-words whitespace-pre-wrap">
{error}
</pre>
</div>
</AlertDescription>
</Alert>
);
};

View File

@@ -70,4 +70,50 @@ describe("QueryParametersForm", () => {
screen.queryByText("Tag key to filter the S3 bucket."), screen.queryByText("Tag key to filter the S3 bucket."),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it("renders a textarea when the parameter input type is textarea", () => {
// Given
const customQuery: AttackPathQuery = {
type: "attack-paths-scans",
id: "custom-query",
attributes: {
name: "Custom openCypher query",
short_description: "Write your own openCypher query",
description: "Run a custom query against the graph.",
provider: "aws",
attribution: null,
parameters: [
{
name: "query",
label: "openCypher query",
data_type: "string",
input_type: "textarea",
placeholder: "MATCH (n) RETURN n LIMIT 25",
description: "",
required: true,
},
],
},
};
const form = useForm({
defaultValues: {
query: "",
},
});
render(
<FormProvider {...form}>
<QueryParametersForm selectedQuery={customQuery} />
</FormProvider>,
);
// When
const input = screen.getByRole("textbox", { name: /opencypher query/i });
// Then
expect(input.tagName).toBe("TEXTAREA");
expect(input).toHaveAttribute("data-slot", "textarea");
expect(input).toHaveAttribute("placeholder", "MATCH (n) RETURN n LIMIT 25");
});
}); });

View File

@@ -3,7 +3,12 @@
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { Input } from "@/components/shadcn"; import { Input } from "@/components/shadcn";
import type { AttackPathQuery } from "@/types/attack-paths"; import { Textarea } from "@/components/shadcn/textarea/textarea";
import { cn } from "@/lib/utils";
import {
type AttackPathQuery,
QUERY_PARAMETER_INPUT_TYPES,
} from "@/types/attack-paths";
interface QueryParametersFormProps { interface QueryParametersFormProps {
selectedQuery: AttackPathQuery | null | undefined; selectedQuery: AttackPathQuery | null | undefined;
@@ -76,8 +81,21 @@ export const QueryParametersForm = ({
return undefined; return undefined;
})(); })();
const placeholder =
param.description ||
param.placeholder ||
`Enter ${param.label.toLowerCase()}`;
const isTextarea =
param.input_type === QUERY_PARAMETER_INPUT_TYPES.TEXTAREA;
return ( return (
<div className="flex flex-col gap-1.5"> <div
className={cn(
"flex flex-col gap-1.5",
isTextarea && "md:col-span-2",
)}
>
<label <label
htmlFor={param.name} htmlFor={param.name}
className="text-text-neutral-tertiary text-xs font-medium" className="text-text-neutral-tertiary text-xs font-medium"
@@ -87,17 +105,24 @@ export const QueryParametersForm = ({
<span className="text-text-error-primary">*</span> <span className="text-text-error-primary">*</span>
)} )}
</label> </label>
<Input {isTextarea ? (
{...field} <Textarea
id={param.name} {...field}
type={param.data_type === "number" ? "number" : "text"} id={param.name}
placeholder={ textareaSize="lg"
param.description || placeholder={placeholder}
param.placeholder || value={field.value ?? ""}
`Enter ${param.label.toLowerCase()}` className="min-h-40 font-mono text-xs"
} />
value={field.value ?? ""} ) : (
/> <Input
{...field}
id={param.name}
type={param.data_type === "number" ? "number" : "text"}
placeholder={placeholder}
value={field.value ?? ""}
/>
)}
{errorMessage && ( {errorMessage && (
<span className="text-xs text-red-500">{errorMessage}</span> <span className="text-xs text-red-500">{errorMessage}</span>
)} )}

View File

@@ -7,9 +7,12 @@ import { Suspense, useEffect, useRef, useState } from "react";
import { FormProvider } from "react-hook-form"; import { FormProvider } from "react-hook-form";
import { import {
buildAttackPathQueries,
executeCustomQuery,
executeQuery, executeQuery,
getAttackPathScans, getAttackPathScans,
getAvailableQueries, getAvailableQueries,
getCartographySchema,
} from "@/actions/attack-paths"; } from "@/actions/attack-paths";
import { adaptQueryResultToGraphData } from "@/actions/attack-paths/query-result.adapter"; import { adaptQueryResultToGraphData } from "@/actions/attack-paths/query-result.adapter";
import { AutoRefresh } from "@/components/scans"; import { AutoRefresh } from "@/components/scans";
@@ -35,6 +38,7 @@ import type {
AttackPathScan, AttackPathScan,
GraphNode, GraphNode,
} from "@/types/attack-paths"; } from "@/types/attack-paths";
import { ATTACK_PATH_QUERY_IDS } from "@/types/attack-paths";
import { import {
AttackPathGraph, AttackPathGraph,
@@ -43,6 +47,8 @@ import {
GraphLegend, GraphLegend,
GraphLoading, GraphLoading,
NodeDetailContent, NodeDetailContent,
QueryDescription,
QueryExecutionError,
QueryParametersForm, QueryParametersForm,
QuerySelector, QuerySelector,
ScanListTable, ScanListTable,
@@ -138,11 +144,21 @@ export default function AttackPathsPage() {
setQueriesLoading(true); setQueriesLoading(true);
try { try {
const queriesData = await getAvailableQueries(scanId); const [queriesData, schemaData] = await Promise.all([
if (queriesData?.data) { getAvailableQueries(scanId),
setQueries(queriesData.data); getCartographySchema(scanId),
]);
const availableQueries = buildAttackPathQueries(
queriesData?.data ?? [],
schemaData?.data.attributes,
);
if (availableQueries.length > 0) {
setQueries(availableQueries);
setQueriesError(null); setQueriesError(null);
} else { } else {
setQueries([]);
setQueriesError("Failed to load available queries"); setQueriesError("Failed to load available queries");
toast({ toast({
title: "Error", title: "Error",
@@ -199,15 +215,12 @@ export default function AttackPathsPage() {
graphState.setError(null); graphState.setError(null);
try { try {
const parameters = queryBuilder.getQueryParameters() as Record< const parameters = queryBuilder.getQueryParameters();
string, const isCustomQuery =
string | number | boolean queryBuilder.selectedQuery === ATTACK_PATH_QUERY_IDS.CUSTOM;
>; const result = isCustomQuery
const result = await executeQuery( ? await executeCustomQuery(scanId, String(parameters?.query ?? ""))
scanId, : await executeQuery(scanId, queryBuilder.selectedQuery, parameters);
queryBuilder.selectedQuery,
parameters,
);
if (result && "error" in result) { if (result && "error" in result) {
const apiError = result as AttackPathQueryError; const apiError = result as AttackPathQueryError;
@@ -384,40 +397,9 @@ export default function AttackPathsPage() {
/> />
{queryBuilder.selectedQueryData && ( {queryBuilder.selectedQueryData && (
<div className="bg-bg-neutral-tertiary text-text-neutral-secondary dark:text-text-neutral-secondary rounded-md px-3 py-2 text-sm"> <QueryDescription
<div className="flex items-start gap-2"> query={queryBuilder.selectedQueryData}
<Info />
className="mt-0.5 size-4 shrink-0"
style={{ color: "var(--bg-data-info)" }}
/>
<p className="whitespace-pre-line">
{
queryBuilder.selectedQueryData.attributes
.description
}
</p>
</div>
{queryBuilder.selectedQueryData.attributes
.attribution && (
<p className="mt-2 text-xs">
Source:{" "}
<a
href={
queryBuilder.selectedQueryData.attributes
.attribution.link
}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{
queryBuilder.selectedQueryData.attributes
.attribution.text
}
</a>
</p>
)}
</div>
)} )}
{queryBuilder.selectedQuery && ( {queryBuilder.selectedQuery && (
@@ -436,9 +418,7 @@ export default function AttackPathsPage() {
</div> </div>
{graphState.error && ( {graphState.error && (
<div className="bg-bg-danger-secondary text-text-danger dark:bg-bg-danger-secondary dark:text-text-danger rounded p-3 text-sm"> <QueryExecutionError error={graphState.error} />
{graphState.error}
</div>
)} )}
</> </>
)} )}

View File

@@ -81,6 +81,18 @@ export const DATA_TYPES = {
type DataType = (typeof DATA_TYPES)[keyof typeof DATA_TYPES]; type DataType = (typeof DATA_TYPES)[keyof typeof DATA_TYPES];
export const QUERY_PARAMETER_INPUT_TYPES = {
TEXT: "text",
TEXTAREA: "textarea",
} as const;
export type QueryParameterInputType =
(typeof QUERY_PARAMETER_INPUT_TYPES)[keyof typeof QUERY_PARAMETER_INPUT_TYPES];
export const ATTACK_PATH_QUERY_IDS = {
CUSTOM: "__custom-open-cypher__",
} as const;
// Query Types // Query Types
export interface AttackPathQueryParameter { export interface AttackPathQueryParameter {
name: string; name: string;
@@ -89,6 +101,7 @@ export interface AttackPathQueryParameter {
description: string; description: string;
placeholder?: string; placeholder?: string;
required?: boolean; required?: boolean;
input_type?: QueryParameterInputType;
} }
export interface AttackPathQueryAttribution { export interface AttackPathQueryAttribution {
@@ -96,6 +109,11 @@ export interface AttackPathQueryAttribution {
link: string; link: string;
} }
export interface AttackPathQueryDocumentationLink {
text: string;
link: string;
}
export interface AttackPathQueryAttributes { export interface AttackPathQueryAttributes {
name: string; name: string;
short_description: string; short_description: string;
@@ -103,6 +121,7 @@ export interface AttackPathQueryAttributes {
provider: string; provider: string;
parameters: AttackPathQueryParameter[]; parameters: AttackPathQueryParameter[];
attribution: AttackPathQueryAttribution | null; attribution: AttackPathQueryAttribution | null;
documentation_link?: AttackPathQueryDocumentationLink | null;
} }
export interface AttackPathQuery { export interface AttackPathQuery {
@@ -115,6 +134,24 @@ export interface AttackPathQueriesResponse {
data: AttackPathQuery[]; data: AttackPathQuery[];
} }
export interface AttackPathCartographySchemaAttributes {
id: string;
provider: string;
cartography_version: string;
schema_url: string;
raw_schema_url: string;
}
export interface AttackPathCartographySchema {
type: "attack-paths-cartography-schemas";
id: string;
attributes: AttackPathCartographySchemaAttributes;
}
export interface AttackPathCartographySchemaResponse {
data: AttackPathCartographySchema;
}
// Graph Data Types // Graph Data Types
// Property values from graph nodes can be any primitive type or arrays // Property values from graph nodes can be any primitive type or arrays
export type GraphNodePropertyValue = export type GraphNodePropertyValue =
@@ -256,3 +293,16 @@ export interface ExecuteQueryRequestData {
export interface ExecuteQueryRequest { export interface ExecuteQueryRequest {
data: ExecuteQueryRequestData; data: ExecuteQueryRequestData;
} }
export interface ExecuteCustomQueryRequestAttributes {
query: string;
}
export interface ExecuteCustomQueryRequestData {
type: "attack-paths-custom-query-run-requests";
attributes: ExecuteCustomQueryRequestAttributes;
}
export interface ExecuteCustomQueryRequest {
data: ExecuteCustomQueryRequestData;
}