From d4a33c0d1c15324e5cabcebb285088b2e24bd40d Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:31:42 +0200 Subject: [PATCH] feat(ui): link every Attack Paths query to its Prowler Hub page (#12145) --- .../_components/query-description.test.tsx | 63 +++++++++++++++---- .../_components/query-description.tsx | 48 +++++++------- .../attack-paths-query-hub-links.added.md | 1 + ui/lib/external-urls.ts | 8 +++ 4 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 ui/changelog.d/attack-paths-query-hub-links.added.md 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 88ec6f139d..5f61ff8b44 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 @@ -1,7 +1,10 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; -import type { AttackPathQuery } from "@/types/attack-paths"; +import { + ATTACK_PATH_QUERY_IDS, + type AttackPathQuery, +} from "@/types/attack-paths"; import { QueryDescription } from "./query-description"; @@ -23,7 +26,51 @@ const customQuery: AttackPathQuery = { }, }; +const builtInQuery: AttackPathQuery = { + type: "attack-paths-scans", + id: "aws-sts-privesc-assume-role", + attributes: { + name: "Role Assumption for Privilege Escalation (STS-001)", + short_description: "Detect principals who can assume other IAM roles.", + description: + "Detect principals who can assume other IAM roles via sts:AssumeRole.", + provider: "aws", + attribution: null, + parameters: [], + }, +}; + describe("QueryDescription", () => { + it("renders a Prowler Hub link for a built-in query", () => { + // Given + render(); + + // When + const link = screen.getByRole("link", { name: /view on prowler hub/i }); + + // Then + expect(link).toHaveAttribute( + "href", + "https://hub.prowler.com/attack-paths/aws-sts-privesc-assume-role", + ); + }); + + it("does not render a Prowler Hub link for the custom query", () => { + // Given + const query: AttackPathQuery = { + ...customQuery, + id: ATTACK_PATH_QUERY_IDS.CUSTOM, + }; + + // When + render(); + + // Then + expect( + screen.queryByRole("link", { name: /view on prowler hub/i }), + ).not.toBeInTheDocument(); + }); + it("renders the documentation link inside an info alert", () => { // Given render(); @@ -39,9 +86,9 @@ describe("QueryDescription", () => { expect(link).toHaveAttribute("href", "https://example.com/docs"); }); - it("does not render unsafe documentation or attribution URLs as clickable links", () => { + it("does not render an unsafe documentation URL as a clickable link", () => { // Given - const queryWithUnsafeLinks: AttackPathQuery = { + const queryWithUnsafeLink: AttackPathQuery = { ...customQuery, attributes: { ...customQuery.attributes, @@ -49,15 +96,11 @@ describe("QueryDescription", () => { text: "Learn how to write custom openCypher queries", link: "javascript:alert('xss')", }, - attribution: { - text: "Unsafe source", - link: "javascript:alert('xss')", - }, }, }; // When - render(); + render(); // Then expect( @@ -65,12 +108,8 @@ describe("QueryDescription", () => { name: /learn how to write custom opencypher queries/i, }), ).not.toBeInTheDocument(); - expect( - screen.queryByRole("link", { name: /unsafe source/i }), - ).not.toBeInTheDocument(); expect( 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/_components/query-description.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.tsx index d1cb6e85fe..5335e4ab9d 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.tsx @@ -1,7 +1,11 @@ import { Info } from "lucide-react"; import { Alert, AlertDescription } from "@/components/shadcn"; -import type { AttackPathQuery } from "@/types/attack-paths"; +import { getAttackPathHubUrl } from "@/lib/external-urls"; +import { + ATTACK_PATH_QUERY_IDS, + type AttackPathQuery, +} from "@/types/attack-paths"; interface QueryDescriptionProps { query: AttackPathQuery; @@ -18,7 +22,12 @@ const isSafeUrl = (url: string): boolean => { export const QueryDescription = ({ query }: QueryDescriptionProps) => { const documentationLink = query.attributes.documentation_link; - const attribution = query.attributes.attribution; + // Every built-in query has a Prowler Hub page keyed by its id. The synthetic + // custom query has no catalog entry, so it gets no hub link. + const hubUrl = + query.id === ATTACK_PATH_QUERY_IDS.CUSTOM + ? null + : getAttackPathHubUrl(query.id); return ( @@ -26,6 +35,19 @@ export const QueryDescription = ({ query }: QueryDescriptionProps) => {

{query.attributes.description}

+ {hubUrl && ( +

+ + View on Prowler Hub + +

+ )} + {documentationLink && (

{isSafeUrl(documentationLink.link) ? ( @@ -42,28 +64,6 @@ export const QueryDescription = ({ query }: QueryDescriptionProps) => { )}

)} - - {attribution && ( -

- {isSafeUrl(attribution.link) ? ( - <> - Source:{" "} - - {attribution.text} - - - ) : ( - <> - Source: {attribution.text} - - )} -

- )}
); diff --git a/ui/changelog.d/attack-paths-query-hub-links.added.md b/ui/changelog.d/attack-paths-query-hub-links.added.md new file mode 100644 index 0000000000..7ab0ea5879 --- /dev/null +++ b/ui/changelog.d/attack-paths-query-hub-links.added.md @@ -0,0 +1 @@ +Attack Paths query info panel now links every query to its page on Prowler Hub diff --git a/ui/lib/external-urls.ts b/ui/lib/external-urls.ts index e4b0a1573e..05c96e2e48 100644 --- a/ui/lib/external-urls.ts +++ b/ui/lib/external-urls.ts @@ -18,6 +18,14 @@ export const DOCS_URLS = { AI_AGENTS: "https://docs.prowler.com/user-guide/ai-agents/", } as const; +// Prowler Hub — the public catalog of Prowler artifacts (checks, compliance, +// attack paths). Every built-in Attack Paths query has a page keyed by its +// query id, e.g. https://hub.prowler.com/attack-paths/aws-sts-privesc-assume-role +export const PROWLER_HUB_URL = "https://hub.prowler.com"; + +export const getAttackPathHubUrl = (queryId: string): string => + `${PROWLER_HUB_URL}/attack-paths/${encodeURIComponent(queryId)}`; + // CloudFormation template URL for the ProwlerScan role. // Also used (URL-encoded) as the templateURL param in the quick-create links // built by getAWSCredentialsTemplateLinks and getAWSOrgDeploymentQuickLink below.