diff --git a/docs/user-guide/tutorials/prowler-app-attack-paths.mdx b/docs/user-guide/tutorials/prowler-app-attack-paths.mdx
index 646ee557de..7b3e382f20 100644
--- a/docs/user-guide/tutorials/prowler-app-attack-paths.mdx
+++ b/docs/user-guide/tutorials/prowler-app-attack-paths.mdx
@@ -105,6 +105,87 @@ If a query requires no parameters, the form displays a message confirming that t
width="700"
/>
+## Writing Custom openCypher Queries
+
+In addition to the built-in queries, Attack Paths supports custom read-only [openCypher](https://opencypher.org/) queries. Custom queries provide direct access to the underlying graph so security teams can answer ad-hoc questions, prototype detections, or extend coverage beyond the built-in catalogue.
+
+To write a custom query, select **Custom openCypher query** from the query dropdown. A code editor with syntax highlighting and line numbers appears, ready to receive the query.
+
+### Constraints and Safety Limits
+
+Custom queries are sandboxed to keep the graph database safe and responsive:
+
+- **Read-only:** Only read operations are allowed. Statements that mutate the graph (`CREATE`, `MERGE`, `SET`, `DELETE`, `REMOVE`, `DROP`, `LOAD CSV`, `CALL { ... }` writes, etc.) are rejected before execution.
+- **Length limit:** Each query is capped at **10,000 characters**.
+- **Scoped to the selected scan:** Results are automatically scoped to the provider and scan selected on the left panel. There is no need to filter by tenant or scan identifier in the query body.
+
+### Example Queries
+
+The following examples are read-only and can be pasted directly into the editor.
+
+**List all S3 buckets in the scan:**
+
+```cypher
+MATCH (b:S3Bucket)
+RETURN b.name AS bucket, b.region AS region
+LIMIT 50
+```
+
+**Find IAM roles that can be assumed from the internet:**
+
+```cypher
+MATCH (r:AWSRole)
+WHERE r.trust_policy CONTAINS '"Principal":"*"'
+RETURN r.arn AS role_arn, r.name AS role_name
+LIMIT 25
+```
+
+**Find EC2 instances exposed to the internet with attached IAM roles:**
+
+```cypher
+MATCH (i:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(r:AWSRole)
+WHERE i.exposed_internet = true
+RETURN i.instanceid AS instance_id, r.arn AS role_arn
+LIMIT 25
+```
+
+**Inspect Prowler findings linked to a specific resource type:**
+
+```cypher
+MATCH (b:S3Bucket)-[:HAS_FINDING]->(f:ProwlerFinding)
+WHERE f.severity IN ['critical', 'high']
+RETURN b.name AS bucket, f.check_id AS check, f.severity AS severity
+LIMIT 50
+```
+
+### Tips for Writing Queries
+
+- Start small with `LIMIT` to inspect the shape of the data before broadening the pattern.
+- Use `RETURN` projections (`RETURN n.name, n.region`) instead of returning whole nodes to keep responses compact.
+- Combine resource nodes with `ProwlerFinding` nodes via `HAS_FINDING` to correlate misconfigurations with the affected resources.
+- When a query times out or returns no rows, simplify the pattern step by step until the first variant runs successfully, then add constraints back.
+
+### Cartography Schema Reference
+
+Attack Paths graphs are populated by [Cartography](https://github.com/cartography-cncf/cartography), an open-source graph ingestion framework. The node labels, relationship types, and properties available in custom queries follow the upstream Cartography schema for the corresponding provider.
+
+For the complete catalogue of node labels and relationships available in custom queries, refer to the official Cartography schema documentation:
+
+- **AWS:** [Cartography AWS Schema](https://github.com/cartography-cncf/cartography/blob/master/docs/root/modules/aws/schema.md)
+
+In addition to the upstream schema, Prowler enriches the graph with:
+
+- **`ProwlerFinding`** nodes representing Prowler check results, linked to affected resources via `HAS_FINDING` relationships.
+- **`Internet`** nodes used to model exposure paths from the public internet to internal resources.
+
+
+ AI assistants connected through Prowler MCP Server can fetch the exact
+ Cartography schema for the active scan via the
+ `prowler_app_get_attack_paths_cartography_schema` tool. This guarantees that
+ generated queries match the schema version pinned by the running Prowler
+ release.
+
+
## Executing a Query
To run the selected query against the scan data, click **Execute Query**. The button displays a loading state while the query processes.
diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md
index c115911475..10e462695d 100644
--- a/ui/CHANGELOG.md
+++ b/ui/CHANGELOG.md
@@ -17,6 +17,7 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🔄 Changed
- Attack Paths custom openCypher queries now use a code editor with syntax highlighting and line numbers [(#10445)](https://github.com/prowler-cloud/prowler/pull/10445)
+- Attack Paths custom openCypher queries now link to the Prowler documentation with examples and how-to guidance instead of the upstream Cartography schema URL
- Filter summary strip: removed redundant "Clear all" link next to pills (use top-bar Clear Filters instead) and switched chip variant from `outline` to `tag` for consistency [(#10481)](https://github.com/prowler-cloud/prowler/pull/10481)
### 🐞 Fixed
diff --git a/ui/actions/attack-paths/queries.adapter.test.ts b/ui/actions/attack-paths/queries.adapter.test.ts
index cd6bafd206..77298991f3 100644
--- a/ui/actions/attack-paths/queries.adapter.test.ts
+++ b/ui/actions/attack-paths/queries.adapter.test.ts
@@ -1,8 +1,8 @@
import { describe, expect, it } from "vitest";
+import { DOCS_URLS } from "@/lib/external-urls";
import {
ATTACK_PATH_QUERY_IDS,
- type AttackPathCartographySchemaAttributes,
type AttackPathQuery,
} from "@/types/attack-paths";
@@ -22,20 +22,9 @@ const presetQuery: AttackPathQuery = {
};
describe("buildAttackPathQueries", () => {
- it("prepends a custom 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",
- };
-
+ it("prepends a custom query that links to the Prowler documentation", () => {
// When
- const result = buildAttackPathQueries([presetQuery], schema);
+ const result = buildAttackPathQueries([presetQuery]);
// Then
expect(result[0]).toMatchObject({
@@ -44,8 +33,8 @@ describe("buildAttackPathQueries", () => {
name: "Custom openCypher query",
short_description: "Write and run your own read-only query",
documentation_link: {
- text: "Cartography schema used by Prowler for AWS graphs",
- link: schema.schema_url,
+ text: "Learn how to write custom openCypher queries",
+ link: DOCS_URLS.ATTACK_PATHS_CUSTOM_QUERIES,
},
},
});
diff --git a/ui/actions/attack-paths/queries.adapter.ts b/ui/actions/attack-paths/queries.adapter.ts
index 0b3120bb0a..b4635ed333 100644
--- a/ui/actions/attack-paths/queries.adapter.ts
+++ b/ui/actions/attack-paths/queries.adapter.ts
@@ -1,7 +1,7 @@
+import { DOCS_URLS } from "@/lib/external-urls";
import { MetaDataProps } from "@/types";
import {
ATTACK_PATH_QUERY_IDS,
- type AttackPathCartographySchemaAttributes,
AttackPathQueriesResponse,
AttackPathQuery,
QUERY_PARAMETER_INPUT_TYPES,
@@ -61,15 +61,12 @@ 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 CUSTOM_QUERY_DOCUMENTATION_LINK = {
+ text: "Learn how to write custom openCypher queries",
+ link: DOCS_URLS.ATTACK_PATHS_CUSTOM_QUERIES,
+} as const;
-const createCustomQuery = (
- schema?: AttackPathCartographySchemaAttributes,
-): AttackPathQuery => ({
+const createCustomQuery = (): AttackPathQuery => ({
type: "attack-paths-scans",
id: ATTACK_PATH_QUERY_IDS.CUSTOM,
attributes: {
@@ -79,12 +76,7 @@ const createCustomQuery = (
"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,
+ documentation_link: { ...CUSTOM_QUERY_DOCUMENTATION_LINK },
parameters: [
{
name: "query",
@@ -103,7 +95,6 @@ const createCustomQuery = (
export const buildAttackPathQueries = (
queries: AttackPathQuery[],
- schema?: AttackPathCartographySchemaAttributes,
): AttackPathQuery[] => {
- return [createCustomQuery(schema), ...queries];
+ return [createCustomQuery(), ...queries];
};
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.test.tsx
index cc61c16a4d..88ec6f139d 100644
--- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.test.tsx
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.test.tsx
@@ -16,27 +16,27 @@ const customQuery: AttackPathQuery = {
provider: "aws",
attribution: null,
documentation_link: {
- text: "Cartography schema used by Prowler for AWS graphs",
- link: "https://example.com/schema",
+ text: "Learn how to write custom openCypher queries",
+ link: "https://example.com/docs",
},
parameters: [],
},
};
describe("QueryDescription", () => {
- it("renders the schema documentation link inside an info alert", () => {
+ it("renders the documentation link inside an info alert", () => {
// Given
render();
// When
const alert = screen.getByRole("alert");
const link = screen.getByRole("link", {
- name: /cartography schema used by prowler for aws graphs/i,
+ name: /learn how to write custom opencypher queries/i,
});
// Then
expect(alert).toBeInTheDocument();
- expect(link).toHaveAttribute("href", "https://example.com/schema");
+ expect(link).toHaveAttribute("href", "https://example.com/docs");
});
it("does not render unsafe documentation or attribution URLs as clickable links", () => {
@@ -46,7 +46,7 @@ describe("QueryDescription", () => {
attributes: {
...customQuery.attributes,
documentation_link: {
- text: "Cartography schema used by Prowler for AWS graphs",
+ text: "Learn how to write custom openCypher queries",
link: "javascript:alert('xss')",
},
attribution: {
@@ -62,14 +62,14 @@ describe("QueryDescription", () => {
// Then
expect(
screen.queryByRole("link", {
- name: /cartography schema used by prowler for aws graphs/i,
+ name: /learn how to write custom opencypher queries/i,
}),
).not.toBeInTheDocument();
expect(
screen.queryByRole("link", { name: /unsafe source/i }),
).not.toBeInTheDocument();
expect(
- screen.getByText(/cartography schema used by prowler for aws graphs/i),
+ screen.getByText(/learn how to write custom opencypher queries/i),
).toBeInTheDocument();
expect(screen.getByText(/unsafe source/i)).toBeInTheDocument();
});
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx
index 86d87808f9..f6b1007f5a 100644
--- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx
@@ -12,7 +12,6 @@ import {
executeQuery,
getAttackPathScans,
getAvailableQueries,
- getCartographySchema,
} from "@/actions/attack-paths";
import { adaptQueryResultToGraphData } from "@/actions/attack-paths/query-result.adapter";
import { AutoRefresh } from "@/components/scans";
@@ -144,14 +143,10 @@ export default function AttackPathsPage() {
setQueriesLoading(true);
try {
- const [queriesData, schemaData] = await Promise.all([
- getAvailableQueries(scanId),
- getCartographySchema(scanId),
- ]);
+ const queriesData = await getAvailableQueries(scanId);
const availableQueries = buildAttackPathQueries(
queriesData?.data ?? [],
- schemaData?.data.attributes,
);
if (availableQueries.length > 0) {
diff --git a/ui/lib/external-urls.ts b/ui/lib/external-urls.ts
index c7b4f7fc63..3cdf760721 100644
--- a/ui/lib/external-urls.ts
+++ b/ui/lib/external-urls.ts
@@ -6,6 +6,8 @@ export const DOCS_URLS = {
"https://docs.prowler.com/user-guide/tutorials/prowler-app#step-8:-analyze-the-findings",
AWS_ORGANIZATIONS:
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
+ ATTACK_PATHS_CUSTOM_QUERIES:
+ "https://docs.prowler.com/user-guide/tutorials/prowler-app-attack-paths#writing-custom-opencypher-queries",
} as const;
// CloudFormation template URL for the ProwlerScan role.