feat(ui): pre-fill Cloudflare and GitHub token creation URLs (#12349)

This commit is contained in:
lydiavilchez
2026-08-06 16:36:10 +02:00
committed by GitHub
parent d8c8027215
commit e2cae35d38
9 changed files with 519 additions and 7 deletions
@@ -0,0 +1 @@
Surface pre-configured credential creation links in the add-provider wizard. Cloudflare exposes the User API Token template and an Account-Owned template pinned to the Cloudflare Account ID entered in the wizard, GitHub exposes the personal-repositories template and an organization-scanning template pinned to the identifier entered in the wizard
@@ -213,6 +213,7 @@ export const BaseCredentialsForm = ({
<GitHubCredentialsForm
control={form.control}
credentialsType={effectiveVia || undefined}
providerUid={providerUid}
/>
)}
{providerType === "iac" && (
@@ -256,6 +257,7 @@ export const BaseCredentialsForm = ({
control={
form.control as unknown as Control<CloudflareTokenCredentials>
}
providerUid={providerUid}
/>
)}
{providerType === "cloudflare" && effectiveVia === "api_key" && (
@@ -0,0 +1,86 @@
import { render, screen } from "@testing-library/react";
import { FormProvider, useForm } from "react-hook-form";
import { describe, expect, it } from "vitest";
import { CloudflareTokenCredentials } from "@/types";
import { CloudflareApiTokenCredentialsForm } from "./cloudflare-api-token-credentials-form";
// Wraps the form in a react-hook-form context so the WizardInputField mounts
// without exploding. We are testing the surrounding links, not the input.
const Harness = ({ providerUid }: { providerUid?: string }) => {
const form = useForm<CloudflareTokenCredentials>();
return (
<FormProvider {...form}>
<CloudflareApiTokenCredentialsForm
control={form.control}
providerUid={providerUid}
/>
</FormProvider>
);
};
const USER_URL =
"https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner";
describe("CloudflareApiTokenCredentialsForm", () => {
it("always renders the User API Token link with the correct href and safe target attributes", () => {
// Given
render(<Harness />);
// When
const link = screen.getByRole("link", {
name: /create a pre-configured user api token/i,
});
// Then
expect(link).toHaveAttribute("href", USER_URL);
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
it("does not render the Account-Owned link when providerUid is missing so the user is not offered an ambiguous duplicate", () => {
// Given
render(<Harness />);
// Then
expect(
screen.queryByRole("link", {
name: /create a pre-configured account-owned api token/i,
}),
).not.toBeInTheDocument();
});
it("renders the Account-Owned link routed through Cloudflare's dashboard `to=` param with the account id substituted when providerUid is provided", () => {
// Given
render(<Harness providerUid="a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" />);
// When
const link = screen.getByRole("link", {
name: /create a pre-configured account-owned api token/i,
});
// Then
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
const parsed = new URL(link.getAttribute("href") ?? "");
expect(parsed.origin).toBe("https://dash.cloudflare.com");
expect(parsed.pathname).toBe("/");
expect(parsed.searchParams.get("to")).toBe(
"/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/api-tokens",
);
expect(parsed.searchParams.get("name")).toBe("Prowler Security Scanner");
});
it("ignores whitespace around providerUid so a stray user-typed space does not hide the Account-Owned link", () => {
// Given
render(<Harness providerUid=" " />);
// Then
expect(
screen.queryByRole("link", {
name: /create a pre-configured account-owned api token/i,
}),
).not.toBeInTheDocument();
});
});
@@ -3,14 +3,31 @@
import { Control } from "react-hook-form";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { Button } from "@/components/shadcn";
import {
buildCloudflareAccountOwnedApiTokenUrl,
PRECONFIGURED_CREDENTIAL_URLS,
} from "@/lib/external-urls";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { CloudflareTokenCredentials } from "@/types";
interface CloudflareApiTokenCredentialsFormProps {
control: Control<CloudflareTokenCredentials>;
// Cloudflare Account ID captured in the previous wizard step. When present,
// it flows into the account-owned token URL as the account path segment so
// Cloudflare lands the user directly on the correct account's Create Custom
// Token page. When absent, only the user-scoped link is shown to avoid
// relying on Cloudflare's `:account` router placeholder, which can silently
// fall through when the user is signed into more than one account.
providerUid?: string;
}
export const CloudflareApiTokenCredentialsForm = ({
control,
}: {
control: Control<CloudflareTokenCredentials>;
}) => {
providerUid,
}: CloudflareApiTokenCredentialsFormProps) => {
const trimmedProviderUid = providerUid?.trim();
return (
<>
<div className="flex flex-col">
@@ -33,6 +50,28 @@ export const CloudflareApiTokenCredentialsForm = ({
variant="bordered"
isRequired
/>
<div className="flex flex-col items-start gap-1">
<Button variant="link" size="link-sm" asChild>
<a
href={PRECONFIGURED_CREDENTIAL_URLS.CLOUDFLARE_API_TOKEN_USER}
target="_blank"
rel="noopener noreferrer"
>
Create a pre-configured User API Token
</a>
</Button>
{trimmedProviderUid && (
<Button variant="link" size="link-sm" asChild>
<a
href={buildCloudflareAccountOwnedApiTokenUrl(trimmedProviderUid)}
target="_blank"
rel="noopener noreferrer"
>
Create a pre-configured Account-Owned API Token
</a>
</Button>
)}
</div>
<div className="text-text-neutral-tertiary text-xs">
Tokens never leave your browser unencrypted and are stored as secrets in
the backend. You can revoke the token from the Cloudflare dashboard
@@ -0,0 +1,87 @@
import { render, screen } from "@testing-library/react";
import { FormProvider, useForm } from "react-hook-form";
import { describe, expect, it } from "vitest";
import { GitHubPersonalAccessTokenForm } from "./github-personal-access-token-form";
// Wraps the form in a react-hook-form context so the WizardInputField mounts
// without exploding. We are testing the surrounding links, not the input.
const Harness = ({ providerUid }: { providerUid?: string }) => {
const form = useForm();
return (
<FormProvider {...form}>
<GitHubPersonalAccessTokenForm
control={form.control}
providerUid={providerUid}
/>
</FormProvider>
);
};
const USER_URL =
"https://github.com/settings/personal-access-tokens/new?name=Prowler+Security+Scanner&description=Fine-grained+PAT+for+Prowler+security+scanning&expires_in=90&administration=read&contents=read&vulnerability_alerts=read&emails=read";
const expectSafeExternalLink = (href: string) => (name: RegExp) => {
const link = screen.getByRole("link", { name });
expect(link).toHaveAttribute("href", href);
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
};
describe("GitHubPersonalAccessTokenForm", () => {
it("renders the personal-repositories link with the correct href and safe target attributes", () => {
// Given
render(<Harness />);
// Then
expectSafeExternalLink(USER_URL)(
/create a pre-configured token for personal repositories/i,
);
});
it("does not render the organization link when providerUid is missing so the user is not offered an identical-looking duplicate", () => {
// Given
render(<Harness />);
// Then
expect(
screen.queryByRole("link", {
name: /create a pre-configured token for organization/i,
}),
).not.toBeInTheDocument();
});
it("renders the organization link with the identifier pinned as target_name when providerUid is provided", () => {
// Given
render(<Harness providerUid="prowler-cloud" />);
// When
const orgLink = screen.getByRole("link", {
name: /create a pre-configured token for organization prowler-cloud/i,
});
// Then
expect(orgLink).toHaveAttribute("target", "_blank");
expect(orgLink).toHaveAttribute("rel", "noopener noreferrer");
const orgUrl = new URL(orgLink.getAttribute("href") ?? "");
expect(orgUrl.origin + orgUrl.pathname).toBe(
"https://github.com/settings/personal-access-tokens/new",
);
expect(orgUrl.searchParams.get("target_name")).toBe("prowler-cloud");
expect(orgUrl.searchParams.get("organization_administration")).toBe("read");
expect(orgUrl.searchParams.get("members")).toBe("read");
expect(orgUrl.searchParams.get("emails")).toBeNull();
});
it("ignores whitespace around providerUid so a stray user-typed space does not hide the organization link", () => {
// Given
render(<Harness providerUid=" " />);
// Then
expect(
screen.queryByRole("link", {
name: /create a pre-configured token for organization/i,
}),
).not.toBeInTheDocument();
});
});
@@ -3,13 +3,29 @@
import { Control } from "react-hook-form";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { Button } from "@/components/shadcn";
import {
buildGitHubPersonalAccessTokenOrgUrl,
PRECONFIGURED_CREDENTIAL_URLS,
} from "@/lib/external-urls";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
interface GitHubPersonalAccessTokenFormProps {
control: Control<any>;
// GitHub identifier entered in the previous wizard step. When it names an
// organization, it flows into the org-scoped token URL as `target_name` so
// GitHub pre-selects the right Resource Owner and surfaces the org-only
// permissions (`organization_administration`, `members`). When absent, only
// the personal-repositories link is shown.
providerUid?: string;
}
export const GitHubPersonalAccessTokenForm = ({
control,
}: {
control: Control<any>;
}) => {
providerUid,
}: GitHubPersonalAccessTokenFormProps) => {
const trimmedProviderUid = providerUid?.trim();
return (
<>
<div className="flex flex-col">
@@ -30,6 +46,30 @@ export const GitHubPersonalAccessTokenForm = ({
variant="bordered"
isRequired
/>
<div className="flex flex-col items-start gap-1">
<Button variant="link" size="link-sm" asChild>
<a
href={
PRECONFIGURED_CREDENTIAL_URLS.GITHUB_PERSONAL_ACCESS_TOKEN_USER
}
target="_blank"
rel="noopener noreferrer"
>
Create a pre-configured token for personal repositories
</a>
</Button>
{trimmedProviderUid && (
<Button variant="link" size="link-sm" asChild>
<a
href={buildGitHubPersonalAccessTokenOrgUrl(trimmedProviderUid)}
target="_blank"
rel="noopener noreferrer"
>
{`Create a pre-configured token for organization ${trimmedProviderUid}`}
</a>
</Button>
)}
</div>
</>
);
};
@@ -11,15 +11,22 @@ import {
interface GitHubCredentialsFormProps {
control: Control<any>;
credentialsType?: string;
providerUid?: string;
}
export const GitHubCredentialsForm = ({
control,
credentialsType,
providerUid,
}: GitHubCredentialsFormProps) => {
switch (credentialsType) {
case "personal_access_token":
return <GitHubPersonalAccessTokenForm control={control} />;
return (
<GitHubPersonalAccessTokenForm
control={control}
providerUid={providerUid}
/>
);
case "oauth_app":
return <GitHubOAuthAppForm control={control} />;
case "github_app":
+175
View File
@@ -6,8 +6,11 @@ import { describe, expect, it } from "vitest";
import { PROVIDER_WIZARD_STEP } from "@/types/provider-wizard";
import {
buildCloudflareAccountOwnedApiTokenUrl,
buildGitHubPersonalAccessTokenOrgUrl,
getAWSCredentialsTemplateLinks,
getAWSOrgDeploymentQuickLink,
PRECONFIGURED_CREDENTIAL_URLS,
getProviderHelpText,
PROWLER_CF_TEMPLATE_URL,
} from "./external-urls";
@@ -110,6 +113,178 @@ describe("getAWSOrgDeploymentQuickLink", () => {
});
});
describe("PRECONFIGURED_CREDENTIAL_URLS", () => {
it("keeps the Cloudflare User API Token URL under the profile route with the four required read scopes", () => {
// Snapshot check: fixes the exact URL so a stray edit to the permission
// scopes, token name, account/zone selectors or console origin trips a
// failing test instead of silently shipping a broken pre-configured
// token flow to users. Matches the "User API Token" link in
// docs/user-guide/providers/cloudflare/authentication.mdx.
expect(PRECONFIGURED_CREDENTIAL_URLS.CLOUDFLARE_API_TOKEN_USER).toBe(
"https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner",
);
});
it("carries the four Prowler read scopes as decoded permissionGroupKeys on the Cloudflare User API Token URL", () => {
// Semantic contract: the URL must request read on account_settings, zone,
// zone_settings and dns and reuse the shared Prowler token name.
const parsed = new URL(
PRECONFIGURED_CREDENTIAL_URLS.CLOUDFLARE_API_TOKEN_USER,
);
const permissionGroupKeys = JSON.parse(
parsed.searchParams.get("permissionGroupKeys") ?? "[]",
);
expect(permissionGroupKeys).toEqual([
{ key: "account_settings", type: "read" },
{ key: "zone", type: "read" },
{ key: "zone_settings", type: "read" },
{ key: "dns", type: "read" },
]);
expect(parsed.searchParams.get("name")).toBe("Prowler Security Scanner");
});
it("keeps the GitHub user-scope PAT URL pre-filled with the four required read permissions", () => {
// Snapshot check: fixes the exact URL so any accidental scope broadening
// (or a rename of `expires_in` / permission slugs on GitHub's side) trips
// a failing test. Matches the "user repositories" URL published in
// docs/user-guide/providers/github/authentication.mdx.
expect(
PRECONFIGURED_CREDENTIAL_URLS.GITHUB_PERSONAL_ACCESS_TOKEN_USER,
).toBe(
"https://github.com/settings/personal-access-tokens/new?name=Prowler+Security+Scanner&description=Fine-grained+PAT+for+Prowler+security+scanning&expires_in=90&administration=read&contents=read&vulnerability_alerts=read&emails=read",
);
});
it("carries only read-level permissions on the GitHub user-scope PAT URL and no organization-only scopes", () => {
// Semantic contract: every permission query-param must be `read`, and the
// two organization-only permissions must NOT leak into the user URL
// (otherwise GitHub would reject the whole permission set with a
// Resource-Owner mismatch when the caller is a personal account).
const parsed = new URL(
PRECONFIGURED_CREDENTIAL_URLS.GITHUB_PERSONAL_ACCESS_TOKEN_USER,
);
expect(parsed.searchParams.get("name")).toBe("Prowler Security Scanner");
expect(parsed.searchParams.get("expires_in")).toBe("90");
expect(parsed.searchParams.get("organization_administration")).toBeNull();
expect(parsed.searchParams.get("members")).toBeNull();
const NON_PERMISSION_PARAMS = new Set([
"name",
"description",
"expires_in",
]);
for (const [key, value] of Array.from(parsed.searchParams.entries())) {
if (NON_PERMISSION_PARAMS.has(key)) continue;
expect(
value,
`permission "${key}" should be granted at "read" level`,
).toBe("read");
}
});
});
describe("buildCloudflareAccountOwnedApiTokenUrl", () => {
it("pins the token to the account by routing through the dashboard `to=` param with the account id substituted", () => {
// Cloudflare's SPA only reads the pre-fill query params
// (`permissionGroupKeys`, `name`) when the user arrives via the dashboard
// router with a `to=` value — navigating straight to
// `/<accountId>/api-tokens/create?params` renders the form but drops the
// params on the floor. Substituting the account id in place of the docs'
// `:account` placeholder keeps the pre-fill working and avoids ambiguity
// for users signed into multiple accounts.
const url = new URL(
buildCloudflareAccountOwnedApiTokenUrl(
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
),
);
expect(url.origin).toBe("https://dash.cloudflare.com");
expect(url.pathname).toBe("/");
expect(url.searchParams.get("to")).toBe(
"/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/api-tokens",
);
expect(url.searchParams.get("name")).toBe("Prowler Security Scanner");
const permissionGroupKeys = JSON.parse(
url.searchParams.get("permissionGroupKeys") ?? "[]",
);
expect(permissionGroupKeys).toEqual([
{ key: "account_settings", type: "read" },
{ key: "zone", type: "read" },
{ key: "zone_settings", type: "read" },
{ key: "dns", type: "read" },
]);
});
it("keeps the `to=` path unencoded so Cloudflare's router matches it", () => {
// Cloudflare's router matches on the raw string in `to`, so the slashes
// inside `/<accountId>/api-tokens` must NOT be percent-encoded.
// URLSearchParams would encode them; asserting the raw substring guards
// against a future refactor that swaps the manual query-string build for
// URLSearchParams.
const url = buildCloudflareAccountOwnedApiTokenUrl(
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
);
expect(url).toContain("?to=/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/api-tokens&");
});
it("URL-encodes account ids that contain characters requiring escaping", () => {
// Real Cloudflare account ids are hex strings today, but the wizard
// accepts whatever the user typed. Encoding the account id inside the
// `to=` path prevents a stray `/` or `#` from silently breaking the URL:
// an unencoded `#` would truncate the query string, and an unencoded `/`
// would let Cloudflare mis-parse the account id boundary.
const url = buildCloudflareAccountOwnedApiTokenUrl("acct/with#gaps");
expect(url).toContain("?to=/acct%2Fwith%23gaps/api-tokens&");
});
});
describe("buildGitHubPersonalAccessTokenOrgUrl", () => {
it("pins the token Resource Owner via target_name and requests the org-only permissions", () => {
// Without a `target_name`, GitHub silently ignores the two organization
// permissions (`organization_administration`, `members`) because the
// Resource Owner defaults to the caller's personal account. Pinning the
// owner is what makes the pre-checked org permissions actually surface.
const url = new URL(buildGitHubPersonalAccessTokenOrgUrl("prowler-cloud"));
expect(url.origin + url.pathname).toBe(
"https://github.com/settings/personal-access-tokens/new",
);
expect(url.searchParams.get("target_name")).toBe("prowler-cloud");
expect(url.searchParams.get("organization_administration")).toBe("read");
expect(url.searchParams.get("members")).toBe("read");
expect(url.searchParams.get("administration")).toBe("read");
expect(url.searchParams.get("contents")).toBe("read");
expect(url.searchParams.get("vulnerability_alerts")).toBe("read");
expect(url.searchParams.get("name")).toBe("Prowler Security Scanner");
expect(url.searchParams.get("expires_in")).toBe("90");
});
it("omits the account-only `emails` permission from the org URL", () => {
// `emails` is an account-level permission that only makes sense when the
// Resource Owner is a personal user account. Including it on an org URL
// would cause GitHub to reject the whole permission set.
const url = new URL(buildGitHubPersonalAccessTokenOrgUrl("prowler-cloud"));
expect(url.searchParams.get("emails")).toBeNull();
});
it("URL-encodes an organization slug that contains characters requiring escaping", () => {
// GitHub org slugs cannot contain `&` or spaces today, but callers pass
// whatever the wizard collected verbatim, so the helper must not blindly
// concatenate. URLSearchParams enforces percent-encoding.
const url = new URL(
buildGitHubPersonalAccessTokenOrgUrl("prowler & friends"),
);
expect(url.searchParams.get("target_name")).toBe("prowler & friends");
expect(url.search).toContain("target_name=prowler+%26+friends");
});
});
describe("getProviderHelpText", () => {
const AWS_SHORTLINK = "https://goto.prowler.com/provider-aws";
const AWS_CREDENTIALS_STEP_DOCS =
+75
View File
@@ -46,6 +46,81 @@ export const BILLING_URL = "https://cloud.prowler.com/billing";
const CF_QUICKCREATE_BASE_URL =
"https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate";
// Deep links that open each provider's cloud console with the credential
// creation form pre-filled with the exact permissions, scopes and name
// Prowler needs. The full 16-provider audit (which providers support this and
// which do not) lives in the PROWLER-2187 PR description; keep both in sync
// when adding or removing entries here.
// AWS has its own CloudFormation quick-create link built in
// `getAWSCredentialsTemplateLinks` below.
export const PRECONFIGURED_CREDENTIAL_URLS = {
// Opens the Cloudflare "Create Custom Token" form under the user profile
// pre-filled with the four read-only scopes Prowler needs
// (`Account Settings`, `Zone`, `Zone Settings`, `DNS`) and the token name.
// Kept in sync with the "User API Token" URL published in
// docs/user-guide/providers/cloudflare/authentication.mdx.
CLOUDFLARE_API_TOKEN_USER:
"https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner",
// Opens the GitHub fine-grained PAT creation form pre-filled with the four
// read-only permissions Prowler needs to scan a user's own repositories.
// Kept in sync with the "user repositories" URL published in
// docs/user-guide/providers/github/authentication.mdx.
GITHUB_PERSONAL_ACCESS_TOKEN_USER:
"https://github.com/settings/personal-access-tokens/new?name=Prowler+Security+Scanner&description=Fine-grained+PAT+for+Prowler+security+scanning&expires_in=90&administration=read&contents=read&vulnerability_alerts=read&emails=read",
} as const;
// Builds the account-owned Cloudflare API Token URL for the given account.
// Uses the dashboard router pattern (`?to=/<accountId>/api-tokens&...`)
// published in docs/user-guide/providers/cloudflare/authentication.mdx, with
// the account id substituted in place of the docs' `:account` placeholder to
// avoid ambiguity when the user is signed into multiple accounts. Navigating
// directly to `/<accountId>/api-tokens/create` does NOT pre-fill the form —
// Cloudflare only reads the pre-fill params when they arrive via the router.
// Same four read-only scopes as the user token URL.
export const buildCloudflareAccountOwnedApiTokenUrl = (
accountId: string,
): string => {
// Cloudflare's router expects the `to=` value with unencoded slashes; using
// URLSearchParams would percent-encode them and break the redirect, so we
// assemble the query string manually and only encode the pieces that need
// it.
const encodedAccountId = encodeURIComponent(accountId);
const permissionGroupKeys = encodeURIComponent(
JSON.stringify([
{ key: "account_settings", type: "read" },
{ key: "zone", type: "read" },
{ key: "zone_settings", type: "read" },
{ key: "dns", type: "read" },
]),
);
const name = encodeURIComponent("Prowler Security Scanner");
return `https://dash.cloudflare.com/?to=/${encodedAccountId}/api-tokens&permissionGroupKeys=${permissionGroupKeys}&name=${name}`;
};
// Builds the organization-scoped GitHub fine-grained PAT URL. GitHub validates
// permissions against the token's Resource Owner and only surfaces
// `organization_administration` and `members` when it is an organization, so
// we pin the owner via `target_name` and skip account-only permissions
// (`emails`) that the docs' org template does not request. Kept in sync with
// the "organization scanning" URL published in
// docs/user-guide/providers/github/authentication.mdx.
export const buildGitHubPersonalAccessTokenOrgUrl = (
targetName: string,
): string => {
const params = new URLSearchParams({
name: "Prowler Security Scanner",
description: "Fine-grained PAT for Prowler organization security scanning",
expires_in: "90",
target_name: targetName,
administration: "read",
contents: "read",
vulnerability_alerts: "read",
organization_administration: "read",
members: "read",
});
return `https://github.com/settings/personal-access-tokens/new?${params.toString()}`;
};
export interface AWSOrgDeploymentQuickLinkParams {
externalId: string;
organizationalUnitId: string;