mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-03 22:17:03 +00:00
Compare commits
38 Commits
dependabot
...
feat/verce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73b784f72d | ||
|
|
f38942ffde | ||
|
|
8f26ecb80e | ||
|
|
9031ec087f | ||
|
|
57470c7c98 | ||
|
|
3f428639c9 | ||
|
|
ee31c715c1 | ||
|
|
7fb5e7eb56 | ||
|
|
c3760a8c28 | ||
|
|
d3d26edb12 | ||
|
|
1c70d911f8 | ||
|
|
776c17ee65 | ||
|
|
db18e47467 | ||
|
|
ea5ba82333 | ||
|
|
2d5e948c96 | ||
|
|
f9ccc89177 | ||
|
|
f8bededc9b | ||
|
|
273c8e4318 | ||
|
|
274cd07c18 | ||
|
|
cc7fa7d49a | ||
|
|
fb62b81a9b | ||
|
|
29cc9eae19 | ||
|
|
0186e9f304 | ||
|
|
6cfa67d007 | ||
|
|
e583cfdafd | ||
|
|
a25c5d4e6a | ||
|
|
109ee80836 | ||
|
|
a97a8b63af | ||
|
|
1a1317c89c | ||
|
|
f363e74a3e | ||
|
|
786d00d69c | ||
|
|
67fb058b2a | ||
|
|
49841dd77a | ||
|
|
842dfc19b8 | ||
|
|
5267ca0710 | ||
|
|
4918a9b018 | ||
|
|
5c2b51d1bf | ||
|
|
ba54da28d6 |
@@ -7,6 +7,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
### 🚀 Added
|
||||
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
} from "@/components/icons/providers-badge";
|
||||
import {
|
||||
MultiSelect,
|
||||
@@ -48,6 +49,7 @@ const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
|
||||
alibabacloud: <AlibabaCloudProviderBadge width={18} height={18} />,
|
||||
cloudflare: <CloudflareProviderBadge width={18} height={18} />,
|
||||
openstack: <OpenStackProviderBadge width={18} height={18} />,
|
||||
vercel: <VercelProviderBadge width={18} height={18} />,
|
||||
};
|
||||
|
||||
/** Common props shared by both batch and instant modes. */
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, FC<IconSvgProps>> = {
|
||||
"Alibaba Cloud": AlibabaCloudProviderBadge,
|
||||
Cloudflare: CloudflareProviderBadge,
|
||||
OpenStack: OpenStackProviderBadge,
|
||||
Vercel: VercelProviderBadge,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IconSvgProps } from "@/types";
|
||||
|
||||
export const VercelProviderBadge: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 256 256"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<rect width="256" height="256" fill="#000000" rx="60" />
|
||||
<path d="M128 45L217 195H39L128 45Z" fill="#ffffff" />
|
||||
</svg>
|
||||
);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" && (
|
||||
<VercelCredentialsForm
|
||||
control={form.control as unknown as Control<VercelCredentials>}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hideActions && (
|
||||
<div className="flex w-full justify-end gap-4">
|
||||
|
||||
@@ -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<UseFormReturn<FormValues>, "setValue">;
|
||||
form: Pick<UseFormReturn<FormValues>, "setValue" | "clearErrors">;
|
||||
setPrevStep: Dispatch<SetStateAction<number>>;
|
||||
setAwsMethod: Dispatch<SetStateAction<"single" | null>>;
|
||||
}) {
|
||||
// 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 = ({
|
||||
|
||||
@@ -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<VercelCredentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
Connect via API Token
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Provide a Vercel API Token with read permissions to the resources you
|
||||
want Prowler to assess.
|
||||
</div>
|
||||
</div>
|
||||
<WizardInputField
|
||||
control={control}
|
||||
name={ProviderCredentialFields.VERCEL_API_TOKEN}
|
||||
type="password"
|
||||
label="API Token"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter your Vercel API Token"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
/>
|
||||
<div className="text-default-400 text-xs">
|
||||
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.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 <CloudflareProviderBadge width={35} height={35} />;
|
||||
case "openstack":
|
||||
return <OpenStackProviderBadge width={35} height={35} />;
|
||||
case "vercel":
|
||||
return <VercelProviderBadge width={35} height={35} />;
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -243,6 +243,11 @@ export const useCredentialsForm = ({
|
||||
[ProviderCredentialFields.IMAGE_FILTER]: "",
|
||||
[ProviderCredentialFields.TAG_FILTER]: "",
|
||||
};
|
||||
case "vercel":
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.VERCEL_API_TOKEN]: "",
|
||||
};
|
||||
default:
|
||||
return baseDefaults;
|
||||
}
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, string | undefined>, ctx) => {
|
||||
if (providerType === "m365") {
|
||||
|
||||
@@ -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<ProviderType, string> = {
|
||||
alibabacloud: "Alibaba Cloud",
|
||||
cloudflare: "Cloudflare",
|
||||
openstack: "OpenStack",
|
||||
vercel: "Vercel",
|
||||
};
|
||||
|
||||
export function getProviderDisplayName(providerId: string): string {
|
||||
|
||||
Reference in New Issue
Block a user