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.