mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-05 03:12:14 +00:00
feat(ui): link every Attack Paths query to its Prowler Hub page (#12145)
This commit is contained in:
+51
-12
@@ -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(<QueryDescription query={builtInQuery} />);
|
||||
|
||||
// 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(<QueryDescription query={query} />);
|
||||
|
||||
// Then
|
||||
expect(
|
||||
screen.queryByRole("link", { name: /view on prowler hub/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the documentation link inside an info alert", () => {
|
||||
// Given
|
||||
render(<QueryDescription query={customQuery} />);
|
||||
@@ -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(<QueryDescription query={queryWithUnsafeLinks} />);
|
||||
render(<QueryDescription query={queryWithUnsafeLink} />);
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
+24
-24
@@ -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 (
|
||||
<Alert variant="info">
|
||||
@@ -26,6 +35,19 @@ export const QueryDescription = ({ query }: QueryDescriptionProps) => {
|
||||
<AlertDescription className="w-full gap-2">
|
||||
<p className="whitespace-pre-line">{query.attributes.description}</p>
|
||||
|
||||
{hubUrl && (
|
||||
<p className="text-xs">
|
||||
<a
|
||||
href={hubUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium underline"
|
||||
>
|
||||
View on Prowler Hub
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{documentationLink && (
|
||||
<p className="text-xs">
|
||||
{isSafeUrl(documentationLink.link) ? (
|
||||
@@ -42,28 +64,6 @@ export const QueryDescription = ({ query }: QueryDescriptionProps) => {
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{attribution && (
|
||||
<p className="text-xs">
|
||||
{isSafeUrl(attribution.link) ? (
|
||||
<>
|
||||
Source:{" "}
|
||||
<a
|
||||
href={attribution.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{attribution.text}
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Source: <span>{attribution.text}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Attack Paths query info panel now links every query to its page on Prowler Hub
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user