From 6b2d9b5580c3cea8654e790c900d42b10ebd83af Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:13:18 +0200 Subject: [PATCH] feat(ui): add Vercel provider (#10191) Co-authored-by: Daniel Barranquero --- ui/CHANGELOG.md | 1 + .../_components/accounts-selector.tsx | 2 + .../_components/provider-type-selector.tsx | 9 +++++ .../findings/table/provider-icon-cell.tsx | 2 + ui/components/icons/providers-badge/index.ts | 3 ++ .../providers-badge/vercel-provider-badge.tsx | 23 +++++++++++ .../providers/radio-group-provider.tsx | 6 +++ .../workflow/forms/base-credentials-form.tsx | 7 ++++ .../workflow/forms/connect-account-form.tsx | 21 +++++++--- .../vercel-credentials-form.tsx | 40 +++++++++++++++++++ .../ui/entities/get-provider-logo.tsx | 5 +++ ui/hooks/use-credentials-form.ts | 5 +++ ui/lib/external-urls.ts | 5 +++ .../build-crendentials.ts | 14 +++++++ .../provider-credential-fields.ts | 4 ++ ui/types/components.ts | 8 +++- ui/types/formSchemas.ts | 15 ++++++- ui/types/providers.ts | 2 + 18 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 ui/components/icons/providers-badge/vercel-provider-badge.tsx create mode 100644 ui/components/providers/workflow/forms/via-credentials/vercel-credentials-form.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 62071cfcca..941e7c5ff4 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Multi-tenant organization management: create, switch, edit, and delete organizations from the profile page [(#10491)](https://github.com/prowler-cloud/prowler/pull/10491) - Findings grouped view with drill-down table showing resources per check, resource detail drawer, infinite scroll pagination, and bulk mute support [(#10425)](https://github.com/prowler-cloud/prowler/pull/10425) - Resource events tool to Lighthouse AI [(#10412)](https://github.com/prowler-cloud/prowler/pull/10412) +- Vercel provider: connect Vercel teams via API token, scan deployments, domains, projects, and team settings [(#10191)](https://github.com/prowler-cloud/prowler/pull/10191) ### 🔄 Changed diff --git a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx index 362396f03f..441ad28697 100644 --- a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx @@ -18,6 +18,7 @@ import { MongoDBAtlasProviderBadge, OpenStackProviderBadge, OracleCloudProviderBadge, + VercelProviderBadge, } from "@/components/icons/providers-badge"; import { MultiSelect, @@ -48,6 +49,7 @@ const PROVIDER_ICON: Record = { alibabacloud: , cloudflare: , openstack: , + vercel: , }; /** Common props shared by both batch and instant modes. */ diff --git a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx index f324260e00..849d9e4d5e 100644 --- a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx @@ -83,6 +83,11 @@ const GoogleWorkspaceProviderBadge = lazy(() => default: m.GoogleWorkspaceProviderBadge, })), ); +const VercelProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.VercelProviderBadge, + })), +); type IconProps = { width: number; height: number }; @@ -150,6 +155,10 @@ const PROVIDER_DATA: Record< label: "OpenStack", icon: OpenStackProviderBadge, }, + vercel: { + label: "Vercel", + icon: VercelProviderBadge, + }, }; /** Common props shared by both batch and instant modes. */ diff --git a/ui/components/findings/table/provider-icon-cell.tsx b/ui/components/findings/table/provider-icon-cell.tsx index 46d4ba8004..3d061a0b32 100644 --- a/ui/components/findings/table/provider-icon-cell.tsx +++ b/ui/components/findings/table/provider-icon-cell.tsx @@ -13,6 +13,7 @@ import { MongoDBAtlasProviderBadge, OpenStackProviderBadge, OracleCloudProviderBadge, + VercelProviderBadge, } from "@/components/icons/providers-badge"; import { cn } from "@/lib/utils"; import { ProviderType } from "@/types"; @@ -32,6 +33,7 @@ export const PROVIDER_ICONS = { alibabacloud: AlibabaCloudProviderBadge, cloudflare: CloudflareProviderBadge, openstack: OpenStackProviderBadge, + vercel: VercelProviderBadge, } as const; interface ProviderIconCellProps { diff --git a/ui/components/icons/providers-badge/index.ts b/ui/components/icons/providers-badge/index.ts index be035c9e7d..729eaf4779 100644 --- a/ui/components/icons/providers-badge/index.ts +++ b/ui/components/icons/providers-badge/index.ts @@ -16,6 +16,7 @@ import { M365ProviderBadge } from "./m365-provider-badge"; import { MongoDBAtlasProviderBadge } from "./mongodbatlas-provider-badge"; import { OpenStackProviderBadge } from "./openstack-provider-badge"; import { OracleCloudProviderBadge } from "./oraclecloud-provider-badge"; +import { VercelProviderBadge } from "./vercel-provider-badge"; export { AlibabaCloudProviderBadge, @@ -32,6 +33,7 @@ export { MongoDBAtlasProviderBadge, OpenStackProviderBadge, OracleCloudProviderBadge, + VercelProviderBadge, }; // Map provider display names to their icon components @@ -50,4 +52,5 @@ export const PROVIDER_BADGE_BY_NAME: Record> = { "Alibaba Cloud": AlibabaCloudProviderBadge, Cloudflare: CloudflareProviderBadge, OpenStack: OpenStackProviderBadge, + Vercel: VercelProviderBadge, }; diff --git a/ui/components/icons/providers-badge/vercel-provider-badge.tsx b/ui/components/icons/providers-badge/vercel-provider-badge.tsx new file mode 100644 index 0000000000..424ec0184e --- /dev/null +++ b/ui/components/icons/providers-badge/vercel-provider-badge.tsx @@ -0,0 +1,23 @@ +import { IconSvgProps } from "@/types"; + +export const VercelProviderBadge: React.FC = ({ + size, + width, + height, + ...props +}) => ( + +); diff --git a/ui/components/providers/radio-group-provider.tsx b/ui/components/providers/radio-group-provider.tsx index 02466189a1..2609a1830f 100644 --- a/ui/components/providers/radio-group-provider.tsx +++ b/ui/components/providers/radio-group-provider.tsx @@ -23,6 +23,7 @@ import { MongoDBAtlasProviderBadge, OpenStackProviderBadge, OracleCloudProviderBadge, + VercelProviderBadge, } from "../icons/providers-badge"; import { FormMessage } from "../ui/form"; @@ -97,6 +98,11 @@ const PROVIDERS = [ label: "OpenStack", badge: OpenStackProviderBadge, }, + { + value: "vercel", + label: "Vercel", + badge: VercelProviderBadge, + }, ] as const; interface RadioGroupProviderProps { diff --git a/ui/components/providers/workflow/forms/base-credentials-form.tsx b/ui/components/providers/workflow/forms/base-credentials-form.tsx index 02a4628e5d..a1b5c8ad46 100644 --- a/ui/components/providers/workflow/forms/base-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/base-credentials-form.tsx @@ -32,6 +32,7 @@ import { OCICredentials, OpenStackCredentials, ProviderType, + VercelCredentials, } from "@/types"; import { ProviderTitleDocs } from "../provider-title-docs"; @@ -60,6 +61,7 @@ import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-for import { MongoDBAtlasCredentialsForm } from "./via-credentials/mongodbatlas-credentials-form"; import { OpenStackCredentialsForm } from "./via-credentials/openstack-credentials-form"; import { OracleCloudCredentialsForm } from "./via-credentials/oraclecloud-credentials-form"; +import { VercelCredentialsForm } from "./via-credentials/vercel-credentials-form"; type BaseCredentialsFormProps = { providerType: ProviderType; @@ -272,6 +274,11 @@ export const BaseCredentialsForm = ({ } /> )} + {providerType === "vercel" && ( + } + /> + )} {!hideActions && (
diff --git a/ui/components/providers/workflow/forms/connect-account-form.tsx b/ui/components/providers/workflow/forms/connect-account-form.tsx index 0b08aec1c3..15f299b47d 100644 --- a/ui/components/providers/workflow/forms/connect-account-form.tsx +++ b/ui/components/providers/workflow/forms/connect-account-form.tsx @@ -116,6 +116,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => { label: "Customer ID", placeholder: "e.g. C01234abc", }; + case "vercel": + return { + label: "Team ID", + placeholder: "e.g. team_xxxxxxxxxxxxxxxxxxxxxxxx", + }; default: return { label: "Provider UID", @@ -133,27 +138,31 @@ function applyBackStep({ }: { prevStep: number; awsMethod: "single" | null; - form: Pick, "setValue">; + form: Pick, "setValue" | "clearErrors">; setPrevStep: Dispatch>; setAwsMethod: Dispatch>; }) { // If in UID form after choosing single, go back to method selector if (prevStep === 2 && awsMethod === "single") { setAwsMethod(null); - form.setValue("providerUid", ""); - form.setValue("providerAlias", ""); + form.setValue("providerUid", "", { shouldValidate: false }); + form.setValue("providerAlias", "", { shouldValidate: false }); return; } setPrevStep((prev) => prev - 1); // Deselect the providerType if the user is going back to the first step if (prevStep === 2) { - form.setValue("providerType", undefined as unknown as ProviderType); + form.setValue("providerType", undefined as unknown as ProviderType, { + shouldValidate: false, + }); setAwsMethod(null); } // Reset the providerUid and providerAlias fields when going back - form.setValue("providerUid", ""); - form.setValue("providerAlias", ""); + form.setValue("providerUid", "", { shouldValidate: false }); + form.setValue("providerAlias", "", { shouldValidate: false }); + // Clear all validation errors so the radio buttons don't show red borders + form.clearErrors(); } export const ConnectAccountForm = ({ diff --git a/ui/components/providers/workflow/forms/via-credentials/vercel-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/vercel-credentials-form.tsx new file mode 100644 index 0000000000..559efe6340 --- /dev/null +++ b/ui/components/providers/workflow/forms/via-credentials/vercel-credentials-form.tsx @@ -0,0 +1,40 @@ +import { Control } from "react-hook-form"; + +import { WizardInputField } from "@/components/providers/workflow/forms/fields"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; +import { VercelCredentials } from "@/types"; + +export const VercelCredentialsForm = ({ + control, +}: { + control: Control; +}) => { + return ( + <> +
+
+ Connect via API Token +
+
+ Provide a Vercel API Token with read permissions to the resources you + want Prowler to assess. +
+
+ +
+ Tokens never leave your browser unencrypted and are stored as secrets in + the backend. You can revoke the token from the Vercel dashboard anytime + at vercel.com/account/tokens. +
+ + ); +}; diff --git a/ui/components/ui/entities/get-provider-logo.tsx b/ui/components/ui/entities/get-provider-logo.tsx index 6e5381aa66..7b8218c596 100644 --- a/ui/components/ui/entities/get-provider-logo.tsx +++ b/ui/components/ui/entities/get-provider-logo.tsx @@ -13,6 +13,7 @@ import { MongoDBAtlasProviderBadge, OpenStackProviderBadge, OracleCloudProviderBadge, + VercelProviderBadge, } from "@/components/icons/providers-badge"; import { ProviderType } from "@/types"; @@ -46,6 +47,8 @@ export const getProviderLogo = (provider: ProviderType) => { return ; case "openstack": return ; + case "vercel": + return ; default: return null; } @@ -81,6 +84,8 @@ export const getProviderName = (provider: ProviderType): string => { return "Cloudflare"; case "openstack": return "OpenStack"; + case "vercel": + return "Vercel"; default: return "Unknown Provider"; } diff --git a/ui/hooks/use-credentials-form.ts b/ui/hooks/use-credentials-form.ts index 68a34183f7..27f221fc2d 100644 --- a/ui/hooks/use-credentials-form.ts +++ b/ui/hooks/use-credentials-form.ts @@ -243,6 +243,11 @@ export const useCredentialsForm = ({ [ProviderCredentialFields.IMAGE_FILTER]: "", [ProviderCredentialFields.TAG_FILTER]: "", }; + case "vercel": + return { + ...baseDefaults, + [ProviderCredentialFields.VERCEL_API_TOKEN]: "", + }; default: return baseDefaults; } diff --git a/ui/lib/external-urls.ts b/ui/lib/external-urls.ts index 7b4cdf4c7e..c7b4f7fc63 100644 --- a/ui/lib/external-urls.ts +++ b/ui/lib/external-urls.ts @@ -92,6 +92,11 @@ export const getProviderHelpText = (provider: string) => { text: "Need help connecting your Google Workspace account?", link: "https://goto.prowler.com/provider-googleworkspace", }; + case "vercel": + return { + text: "Need help connecting your Vercel team?", + link: "https://goto.prowler.com/provider-vercel", + }; default: return { text: "How to setup a provider?", diff --git a/ui/lib/provider-credentials/build-crendentials.ts b/ui/lib/provider-credentials/build-crendentials.ts index 27f0babba1..d52e57cdfe 100644 --- a/ui/lib/provider-credentials/build-crendentials.ts +++ b/ui/lib/provider-credentials/build-crendentials.ts @@ -250,6 +250,16 @@ export const buildAlibabaCloudSecret = ( return filterEmptyValues(secret); }; +export const buildVercelSecret = (formData: FormData) => { + const secret = { + [ProviderCredentialFields.VERCEL_API_TOKEN]: getFormValue( + formData, + ProviderCredentialFields.VERCEL_API_TOKEN, + ), + }; + return filterEmptyValues(secret); +}; + export const buildOpenStackSecret = (formData: FormData) => { const secret = { [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: getFormValue( @@ -499,6 +509,10 @@ export const buildSecretConfig = ( secretType: "static", secret: buildGoogleWorkspaceSecret(formData), }), + vercel: () => ({ + secretType: "static", + secret: buildVercelSecret(formData), + }), }; const builder = secretBuilders[providerType]; diff --git a/ui/lib/provider-credentials/provider-credential-fields.ts b/ui/lib/provider-credentials/provider-credential-fields.ts index f4f2c60c3a..96ea5de2ae 100644 --- a/ui/lib/provider-credentials/provider-credential-fields.ts +++ b/ui/lib/provider-credentials/provider-credential-fields.ts @@ -88,6 +88,9 @@ export const ProviderCredentialFields = { GOOGLEWORKSPACE_CUSTOMER_ID: "customer_id", GOOGLEWORKSPACE_CREDENTIALS_CONTENT: "credentials_content", GOOGLEWORKSPACE_DELEGATED_USER: "delegated_user", + + // Vercel fields + VERCEL_API_TOKEN: "api_token", } as const; // Type for credential field values @@ -146,6 +149,7 @@ export const ErrorPointers = { GOOGLEWORKSPACE_CREDENTIALS_CONTENT: "/data/attributes/secret/credentials_content", GOOGLEWORKSPACE_DELEGATED_USER: "/data/attributes/secret/delegated_user", + VERCEL_API_TOKEN: "/data/attributes/secret/api_token", } as const; export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers]; diff --git a/ui/types/components.ts b/ui/types/components.ts index 33be9f2552..0f294ceb7d 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -381,6 +381,11 @@ export type GoogleWorkspaceCredentials = { [ProviderCredentialFields.PROVIDER_ID]: string; }; +export type VercelCredentials = { + [ProviderCredentialFields.VERCEL_API_TOKEN]: string; + [ProviderCredentialFields.PROVIDER_ID]: string; +}; + export type CredentialsFormSchema = | AWSCredentials | AWSCredentialsRole @@ -397,7 +402,8 @@ export type CredentialsFormSchema = | AlibabaCloudCredentialsRole | CloudflareCredentials | OpenStackCredentials - | GoogleWorkspaceCredentials; + | GoogleWorkspaceCredentials + | VercelCredentials; export interface SearchParamsProps { [key: string]: string | string[] | undefined; diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 7598757266..1c34757d3f 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -156,6 +156,11 @@ export const addProviderFormSchema = z "Customer ID must start with 'C' followed by alphanumeric characters (e.g., C01234abc)", ), }), + z.object({ + providerType: z.literal("vercel"), + [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), + providerUid: z.string().trim().min(1, "Team ID is required"), + }), ]), ); @@ -376,7 +381,15 @@ export const addCredentialsFormSchema = ( .string() .min(1, "Cloud name is required"), } - : {}), + : providerType === "vercel" + ? { + [ProviderCredentialFields.VERCEL_API_TOKEN]: + z + .string() + .trim() + .min(1, "API Token is required"), + } + : {}), }) .superRefine((data: Record, ctx) => { if (providerType === "m365") { diff --git a/ui/types/providers.ts b/ui/types/providers.ts index 61a18722a2..2669366536 100644 --- a/ui/types/providers.ts +++ b/ui/types/providers.ts @@ -13,6 +13,7 @@ export const PROVIDER_TYPES = [ "alibabacloud", "cloudflare", "openstack", + "vercel", ] as const; export type ProviderType = (typeof PROVIDER_TYPES)[number]; @@ -32,6 +33,7 @@ export const PROVIDER_DISPLAY_NAMES: Record = { alibabacloud: "Alibaba Cloud", cloudflare: "Cloudflare", openstack: "OpenStack", + vercel: "Vercel", }; export function getProviderDisplayName(providerId: string): string {