mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
Compare commits
2 Commits
f8bededc9b
...
feat/verce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5267ca0710 | ||
|
|
4918a9b018 |
@@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.1.15 on 2026-02-26 22:11
|
||||
|
||||
import django.contrib.postgres.indexes
|
||||
from django.db import migrations
|
||||
|
||||
import api.db_utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("api", "0083_image_provider"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'vercel';",
|
||||
reverse_sql=migrations.RunSQL.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="provider",
|
||||
name="provider",
|
||||
field=api.db_utils.ProviderEnumField(
|
||||
choices=[
|
||||
("aws", "AWS"),
|
||||
("azure", "Azure"),
|
||||
("gcp", "GCP"),
|
||||
("kubernetes", "Kubernetes"),
|
||||
("m365", "M365"),
|
||||
("github", "GitHub"),
|
||||
("mongodbatlas", "MongoDB Atlas"),
|
||||
("iac", "IaC"),
|
||||
("oraclecloud", "Oracle Cloud Infrastructure"),
|
||||
("alibabacloud", "Alibaba Cloud"),
|
||||
("cloudflare", "Cloudflare"),
|
||||
("openstack", "OpenStack"),
|
||||
("vercel", "Vercel"),
|
||||
("image", "Image"),
|
||||
],
|
||||
default="aws",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="resource",
|
||||
index=django.contrib.postgres.indexes.GinIndex(
|
||||
fields=["text_search"], name="gin_resources_search_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -292,6 +292,7 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
ALIBABACLOUD = "alibabacloud", _("Alibaba Cloud")
|
||||
CLOUDFLARE = "cloudflare", _("Cloudflare")
|
||||
OPENSTACK = "openstack", _("OpenStack")
|
||||
VERCEL = "vercel", _("Vercel")
|
||||
IMAGE = "image", _("Image")
|
||||
|
||||
@staticmethod
|
||||
@@ -427,6 +428,15 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
pointer="/data/attributes/uid",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_vercel_uid(value):
|
||||
if not re.match(r"^team_[a-zA-Z0-9]{16,32}$", value):
|
||||
raise ModelValidationError(
|
||||
detail="Vercel provider ID must be a valid Vercel Team ID (e.g., team_xxxxxxxxxxxxxxxxxxxxxxxx).",
|
||||
code="vercel-uid",
|
||||
pointer="/data/attributes/uid",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_image_uid(value):
|
||||
if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._/:@-]{2,249}$", value):
|
||||
|
||||
@@ -36,6 +36,7 @@ if TYPE_CHECKING:
|
||||
)
|
||||
from prowler.providers.openstack.openstack_provider import OpenstackProvider
|
||||
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
|
||||
from prowler.providers.vercel.vercel_provider import VercelProvider
|
||||
|
||||
|
||||
class CustomOAuth2Client(OAuth2Client):
|
||||
@@ -90,6 +91,7 @@ def return_prowler_provider(
|
||||
| MongodbatlasProvider
|
||||
| OpenstackProvider
|
||||
| OraclecloudProvider
|
||||
| VercelProvider
|
||||
):
|
||||
"""Return the Prowler provider class based on the given provider type.
|
||||
|
||||
@@ -165,6 +167,10 @@ def return_prowler_provider(
|
||||
from prowler.providers.image.image_provider import ImageProvider
|
||||
|
||||
prowler_provider = ImageProvider
|
||||
case Provider.ProviderChoices.VERCEL.value:
|
||||
from prowler.providers.vercel.vercel_provider import VercelProvider
|
||||
|
||||
prowler_provider = VercelProvider
|
||||
case _:
|
||||
raise ValueError(f"Provider type {provider.provider} not supported")
|
||||
return prowler_provider
|
||||
@@ -225,6 +231,11 @@ def get_prowler_provider_kwargs(
|
||||
# clouds_yaml_content, clouds_yaml_cloud and provider_id are validated
|
||||
# in the provider itself, so it's not needed here.
|
||||
pass
|
||||
elif provider.provider == Provider.ProviderChoices.VERCEL.value:
|
||||
prowler_provider_kwargs = {
|
||||
**prowler_provider_kwargs,
|
||||
"team_id": provider.uid,
|
||||
}
|
||||
elif provider.provider == Provider.ProviderChoices.IMAGE.value:
|
||||
# Detect whether uid is a registry URL (e.g. "docker.io/andoniaf") or
|
||||
# a concrete image reference (e.g. "docker.io/andoniaf/myimage:latest").
|
||||
@@ -270,6 +281,7 @@ def initialize_prowler_provider(
|
||||
| MongodbatlasProvider
|
||||
| OpenstackProvider
|
||||
| OraclecloudProvider
|
||||
| VercelProvider
|
||||
):
|
||||
"""Initialize a Prowler provider instance based on the given provider type.
|
||||
|
||||
@@ -321,6 +333,13 @@ def prowler_provider_connection_test(provider: Provider) -> Connection:
|
||||
"raise_on_exception": False,
|
||||
}
|
||||
return prowler_provider.test_connection(**openstack_kwargs)
|
||||
elif provider.provider == Provider.ProviderChoices.VERCEL.value:
|
||||
vercel_kwargs = {
|
||||
**prowler_provider_kwargs,
|
||||
"team_id": provider.uid,
|
||||
"raise_on_exception": False,
|
||||
}
|
||||
return prowler_provider.test_connection(**vercel_kwargs)
|
||||
elif provider.provider == Provider.ProviderChoices.IMAGE.value:
|
||||
image_kwargs = {
|
||||
"image": provider.uid,
|
||||
|
||||
@@ -1550,6 +1550,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
|
||||
serializer = OpenStackCloudsYamlProviderSecret(data=secret)
|
||||
elif provider_type == Provider.ProviderChoices.IMAGE.value:
|
||||
serializer = ImageProviderSecret(data=secret)
|
||||
elif provider_type == Provider.ProviderChoices.VERCEL.value:
|
||||
serializer = VercelProviderSecret(data=secret)
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
{"provider": f"Provider type not supported {provider_type}"}
|
||||
@@ -1748,6 +1750,13 @@ class ImageProviderSecret(serializers.Serializer):
|
||||
return attrs
|
||||
|
||||
|
||||
class VercelProviderSecret(serializers.Serializer):
|
||||
api_token = serializers.CharField()
|
||||
|
||||
class Meta:
|
||||
resource_name = "provider-secrets"
|
||||
|
||||
|
||||
class AlibabaCloudProviderSecret(serializers.Serializer):
|
||||
access_key_id = serializers.CharField()
|
||||
access_key_secret = serializers.CharField()
|
||||
|
||||
@@ -288,6 +288,13 @@
|
||||
"user-guide/providers/openstack/getting-started-openstack",
|
||||
"user-guide/providers/openstack/authentication"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Vercel",
|
||||
"pages": [
|
||||
"user-guide/providers/vercel/getting-started-vercel",
|
||||
"user-guide/providers/vercel/authentication"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
115
docs/user-guide/providers/vercel/authentication.mdx
Normal file
115
docs/user-guide/providers/vercel/authentication.mdx
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: 'Vercel Authentication in Prowler'
|
||||
---
|
||||
|
||||
Prowler for Vercel authenticates using a Vercel API Token (Bearer Token). Tokens can be scoped to a specific team or granted account-wide access.
|
||||
|
||||
## Required Permissions
|
||||
|
||||
Prowler requires read-only access to Vercel resources. The following API token scopes are needed:
|
||||
|
||||
| Scope | Description |
|
||||
|-------|-------------|
|
||||
| `User:Read` | Read access to user profile and token metadata |
|
||||
| `Team:Read` | Read access to team settings, members, and SSO configuration |
|
||||
| `Project:Read` | Read access to project settings and environment variables |
|
||||
| `Deployment:Read` | Read access to deployment details and protection settings |
|
||||
| `Domain:Read` | Read access to domain configuration, DNS records, and SSL certificates |
|
||||
| `Firewall:Read` | Read access to WAF configuration and rules |
|
||||
|
||||
<Warning>
|
||||
Ensure the API token has access to all resources to be scanned. If permissions are missing, some checks may fail or return incomplete results. Features gated by Vercel plan (such as managed WAF rulesets) are reported with a MANUAL status when inaccessible.
|
||||
</Warning>
|
||||
|
||||
## API Token
|
||||
|
||||
### Step 1: Create an API Token
|
||||
|
||||
1. **Log into the Vercel Dashboard**
|
||||
- Go to [https://vercel.com/account/tokens](https://vercel.com/account/tokens) and sign in
|
||||
|
||||
2. **Create a New Token**
|
||||
- Click "Create"
|
||||
- Give the token a descriptive name (e.g., "Prowler Security Scanner")
|
||||
|
||||
3. **Configure Token Scope**
|
||||
- **Full Account:** Select "Full Account" to scan all teams and projects
|
||||
- **Team-scoped:** Select a specific team to restrict the token to that team's resources
|
||||
|
||||
4. **Set Expiration**
|
||||
- Choose an appropriate expiration period
|
||||
- Prowler checks for expired and stale tokens, so regular rotation is recommended
|
||||
|
||||
5. **Copy the Token**
|
||||
- Copy the token immediately after creation — Vercel only displays it once
|
||||
|
||||
### Step 2: Store the Token Securely
|
||||
|
||||
Store the API token as an environment variable:
|
||||
|
||||
```bash
|
||||
export VERCEL_TOKEN="your-api-token-here"
|
||||
```
|
||||
|
||||
For team-scoped scans, also set the team ID:
|
||||
|
||||
```bash
|
||||
export VERCEL_TEAM="team_xxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Never commit API tokens to version control or share them in plain text. Use environment variables or a secrets manager.
|
||||
</Warning>
|
||||
|
||||
### Step 3: Run Prowler
|
||||
|
||||
```bash
|
||||
prowler vercel
|
||||
```
|
||||
|
||||
Alternatively, pass credentials directly via CLI arguments:
|
||||
|
||||
```bash
|
||||
prowler vercel --vercel-token "your-api-token-here" --vercel-team "your-team-id"
|
||||
```
|
||||
|
||||
## Team-Scoped Tokens
|
||||
|
||||
When a token is scoped to a specific team, all API requests must include the `teamId` parameter. Prowler handles this automatically when the team ID is provided via `VERCEL_TEAM` or `--vercel-team`.
|
||||
|
||||
<Note>
|
||||
The Team ID can be found in the Vercel Dashboard under "Settings" → "General" for the selected team. It follows the format `team_xxxxxxxxxxxxxxxxxxxxxxxx`.
|
||||
</Note>
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Security Recommendations
|
||||
|
||||
- **Use team-scoped tokens** — Restrict access to the minimum required scope
|
||||
- **Use environment variables** — Never hardcode credentials in scripts or commands
|
||||
- **Rotate tokens regularly** — Create new tokens periodically and revoke old ones
|
||||
- **Set token expiration** — Avoid creating tokens without an expiration date
|
||||
- **Monitor token usage** — Review the "Last Used" column in the Vercel tokens dashboard for suspicious activity
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Vercel Credentials Not Found" Error
|
||||
|
||||
This error occurs when no API token is provided. Ensure `VERCEL_TOKEN` is set or pass `--vercel-token` as a CLI argument.
|
||||
|
||||
### "Insufficient Permissions for the Vercel API Token" (403)
|
||||
|
||||
- Verify the API token is correct and has not expired
|
||||
- For team-scoped tokens, ensure `VERCEL_TEAM` or `--vercel-team` is set with the correct team ID
|
||||
- Check that the token has the [required permissions](#required-permissions)
|
||||
|
||||
### "Invalid or Expired Vercel API Token" (401)
|
||||
|
||||
- The token has been revoked or has expired
|
||||
- Generate a new token at [https://vercel.com/account/tokens](https://vercel.com/account/tokens)
|
||||
|
||||
### "Team Not Found or Not Accessible" (404/403)
|
||||
|
||||
- Verify the team ID or slug is correct
|
||||
- Ensure the API token has access to the specified team
|
||||
- Check that the team exists and has not been deleted
|
||||
122
docs/user-guide/providers/vercel/getting-started-vercel.mdx
Normal file
122
docs/user-guide/providers/vercel/getting-started-vercel.mdx
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: 'Getting Started with Vercel'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx";
|
||||
|
||||
<VersionBadge version="5.20.0" />
|
||||
|
||||
Prowler for Vercel scans team and project configurations for security misconfigurations, including deployment protection, environment variable exposure, WAF settings, SSO enforcement, and more.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running Prowler with the Vercel provider, ensure the following requirements are met:
|
||||
|
||||
1. A Vercel account with at least one project
|
||||
2. A Vercel API Token with read permissions (see [Authentication](/user-guide/providers/vercel/authentication))
|
||||
3. (Optional) A Vercel Team ID or slug to scope the scan to a specific team
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Set Up Authentication
|
||||
|
||||
Store the Vercel API token as an environment variable:
|
||||
|
||||
```bash
|
||||
export VERCEL_TOKEN="your-api-token-here"
|
||||
```
|
||||
|
||||
To scope the scan to a specific team, set the team ID or slug:
|
||||
|
||||
```bash
|
||||
export VERCEL_TEAM="team_xxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
### Step 2: Run Prowler
|
||||
|
||||
Run a scan across all accessible Vercel resources:
|
||||
|
||||
```bash
|
||||
prowler vercel
|
||||
```
|
||||
|
||||
Prowler will automatically discover projects, domains, deployments, and team settings, then run security checks against them.
|
||||
|
||||
## Authentication
|
||||
|
||||
Prowler reads Vercel credentials from environment variables or CLI arguments. To set credentials before running Prowler:
|
||||
|
||||
**Environment Variables:**
|
||||
```bash
|
||||
export VERCEL_TOKEN="your-api-token-here"
|
||||
export VERCEL_TEAM="your-team-id-or-slug"
|
||||
prowler vercel
|
||||
```
|
||||
|
||||
**CLI Arguments:**
|
||||
```bash
|
||||
prowler vercel --vercel-token "your-api-token-here" --vercel-team "your-team-id-or-slug"
|
||||
```
|
||||
|
||||
<Note>
|
||||
If both environment variables and CLI arguments are provided, CLI arguments take precedence.
|
||||
</Note>
|
||||
|
||||
## Filtering Projects
|
||||
|
||||
By default, Prowler scans all projects accessible with the provided credentials. To scan only specific projects, use the `--project` argument:
|
||||
|
||||
```bash
|
||||
prowler vercel --project my-project-name
|
||||
```
|
||||
|
||||
Multiple projects can be specified:
|
||||
|
||||
```bash
|
||||
prowler vercel --project project-one project-two
|
||||
```
|
||||
|
||||
## Scoping to a Team
|
||||
|
||||
When using a team-scoped API token, the `--vercel-team` argument or `VERCEL_TEAM` environment variable must be set. Prowler uses this value to scope all API requests to the team context:
|
||||
|
||||
```bash
|
||||
prowler vercel --vercel-team team_xxxxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Team-scoped API tokens require the team ID to be provided. Without it, Vercel returns a 403 error because the token only has permissions within the team context.
|
||||
</Warning>
|
||||
|
||||
## Supported Services
|
||||
|
||||
Prowler checks the following Vercel services:
|
||||
|
||||
| Service | Description | Example Checks |
|
||||
|---------|-------------|----------------|
|
||||
| **Authentication** | API token hygiene | Expired tokens, stale tokens |
|
||||
| **Deployment** | Deployment security settings | Preview access protection, stable production targets |
|
||||
| **Domain** | Domain and DNS configuration | SSL certificates, DNS verification, wildcard exposure |
|
||||
| **Project** | Project-level security settings | Deployment protection, environment variable encryption, fork protection |
|
||||
| **Security** | Web Application Firewall (WAF) | WAF enablement, managed rulesets, rate limiting, IP blocking |
|
||||
| **Team** | Team governance and access | SSO enforcement, directory sync, member role least privilege |
|
||||
|
||||
## Configuration
|
||||
|
||||
Prowler uses a configuration file to customize provider behavior. The Vercel configuration includes:
|
||||
|
||||
```yaml
|
||||
vercel:
|
||||
# Maximum number of retries for API requests (default is 2)
|
||||
max_retries: 2
|
||||
```
|
||||
|
||||
To use a custom configuration:
|
||||
|
||||
```bash
|
||||
prowler vercel --config-file /path/to/config.yaml
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Authentication](/user-guide/providers/vercel/authentication) - Detailed guide on creating API tokens and required permissions
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
} from "@/components/icons/providers-badge";
|
||||
import {
|
||||
MultiSelect,
|
||||
@@ -40,6 +41,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} />,
|
||||
};
|
||||
|
||||
interface AccountsSelectorProps {
|
||||
|
||||
@@ -73,6 +73,11 @@ const OpenStackProviderBadge = lazy(() =>
|
||||
default: m.OpenStackProviderBadge,
|
||||
})),
|
||||
);
|
||||
const VercelProviderBadge = lazy(() =>
|
||||
import("@/components/icons/providers-badge").then((m) => ({
|
||||
default: m.VercelProviderBadge,
|
||||
})),
|
||||
);
|
||||
|
||||
type IconProps = { width: number; height: number };
|
||||
|
||||
@@ -132,6 +137,10 @@ const PROVIDER_DATA: Record<
|
||||
label: "OpenStack",
|
||||
icon: OpenStackProviderBadge,
|
||||
},
|
||||
vercel: {
|
||||
label: "Vercel",
|
||||
icon: VercelProviderBadge,
|
||||
},
|
||||
};
|
||||
|
||||
type ProviderTypeSelectorProps = {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
} from "@/components/icons/providers-badge";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
@@ -27,6 +28,7 @@ export const PROVIDER_ICONS = {
|
||||
alibabacloud: AlibabaCloudProviderBadge,
|
||||
cloudflare: CloudflareProviderBadge,
|
||||
openstack: OpenStackProviderBadge,
|
||||
vercel: VercelProviderBadge,
|
||||
} as const;
|
||||
|
||||
interface ProviderIconCellProps {
|
||||
|
||||
@@ -14,6 +14,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,
|
||||
@@ -28,6 +29,7 @@ export {
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
};
|
||||
|
||||
// Map provider display names to their icon components
|
||||
@@ -44,4 +46,5 @@ export const PROVIDER_ICONS: 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>
|
||||
);
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
} from "../icons/providers-badge";
|
||||
import { FormMessage } from "../ui/form";
|
||||
|
||||
@@ -85,6 +86,11 @@ const PROVIDERS = [
|
||||
label: "OpenStack",
|
||||
badge: OpenStackProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "vercel",
|
||||
label: "Vercel",
|
||||
badge: VercelProviderBadge,
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface RadioGroupProviderProps {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
OCICredentials,
|
||||
OpenStackCredentials,
|
||||
ProviderType,
|
||||
VercelCredentials,
|
||||
} from "@/types";
|
||||
|
||||
import { ProviderTitleDocs } from "../provider-title-docs";
|
||||
@@ -56,6 +57,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;
|
||||
@@ -256,6 +258,11 @@ export const BaseCredentialsForm = ({
|
||||
control={form.control as unknown as Control<OpenStackCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "vercel" && (
|
||||
<VercelCredentialsForm
|
||||
control={form.control as unknown as Control<VercelCredentials>}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hideActions && (
|
||||
<div className="flex w-full justify-end gap-4">
|
||||
|
||||
@@ -106,6 +106,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => {
|
||||
label: "Project ID",
|
||||
placeholder: "e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
};
|
||||
case "vercel":
|
||||
return {
|
||||
label: "Team ID",
|
||||
placeholder: "e.g. team_xxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: "Provider UID",
|
||||
@@ -123,27 +128,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,42 @@
|
||||
"use client";
|
||||
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
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>
|
||||
<CustomInput
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
} from "@/components/icons/providers-badge";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
@@ -40,6 +41,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;
|
||||
}
|
||||
@@ -71,6 +74,8 @@ export const getProviderName = (provider: ProviderType): string => {
|
||||
return "Cloudflare";
|
||||
case "openstack":
|
||||
return "OpenStack";
|
||||
case "vercel":
|
||||
return "Vercel";
|
||||
default:
|
||||
return "Unknown Provider";
|
||||
}
|
||||
|
||||
@@ -226,6 +226,11 @@ export const useCredentialsForm = ({
|
||||
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: "",
|
||||
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]: "",
|
||||
};
|
||||
case "vercel":
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.VERCEL_API_TOKEN]: "",
|
||||
};
|
||||
default:
|
||||
return baseDefaults;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import { IntegrationType } from "../types/integrations";
|
||||
export const DOCS_URLS = {
|
||||
FINDINGS_ANALYSIS:
|
||||
"https://docs.prowler.com/user-guide/tutorials/prowler-app#step-8:-analyze-the-findings",
|
||||
AWS_ORGANIZATIONS:
|
||||
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
|
||||
} as const;
|
||||
|
||||
export const getProviderHelpText = (provider: string) => {
|
||||
@@ -70,6 +68,11 @@ export const getProviderHelpText = (provider: string) => {
|
||||
text: "Need help connecting your OpenStack cloud?",
|
||||
link: "https://goto.prowler.com/provider-openstack",
|
||||
};
|
||||
case "vercel":
|
||||
return {
|
||||
text: "Need help connecting your Vercel team?",
|
||||
link: "https://docs.prowler.com/user-guide/providers/vercel/getting-started-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(
|
||||
@@ -450,6 +460,10 @@ export const buildSecretConfig = (
|
||||
secretType: "static",
|
||||
secret: buildOpenStackSecret(formData),
|
||||
}),
|
||||
vercel: () => ({
|
||||
secretType: "static",
|
||||
secret: buildVercelSecret(formData),
|
||||
}),
|
||||
};
|
||||
|
||||
const builder = secretBuilders[providerType];
|
||||
|
||||
@@ -76,6 +76,9 @@ export const ProviderCredentialFields = {
|
||||
// OpenStack fields
|
||||
OPENSTACK_CLOUDS_YAML_CONTENT: "clouds_yaml_content",
|
||||
OPENSTACK_CLOUDS_YAML_CLOUD: "clouds_yaml_cloud",
|
||||
|
||||
// Vercel fields
|
||||
VERCEL_API_TOKEN: "api_token",
|
||||
} as const;
|
||||
|
||||
// Type for credential field values
|
||||
@@ -125,6 +128,7 @@ export const ErrorPointers = {
|
||||
CLOUDFLARE_API_EMAIL: "/data/attributes/secret/api_email",
|
||||
OPENSTACK_CLOUDS_YAML_CONTENT: "/data/attributes/secret/clouds_yaml_content",
|
||||
OPENSTACK_CLOUDS_YAML_CLOUD: "/data/attributes/secret/clouds_yaml_cloud",
|
||||
VERCEL_API_TOKEN: "/data/attributes/secret/api_token",
|
||||
} as const;
|
||||
|
||||
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];
|
||||
|
||||
@@ -355,6 +355,11 @@ export type OpenStackCredentials = {
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type VercelCredentials = {
|
||||
[ProviderCredentialFields.VERCEL_API_TOKEN]: string;
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type CredentialsFormSchema =
|
||||
| AWSCredentials
|
||||
| AWSCredentialsRole
|
||||
@@ -369,7 +374,8 @@ export type CredentialsFormSchema =
|
||||
| AlibabaCloudCredentials
|
||||
| AlibabaCloudCredentialsRole
|
||||
| CloudflareCredentials
|
||||
| OpenStackCredentials;
|
||||
| OpenStackCredentials
|
||||
| VercelCredentials;
|
||||
|
||||
export interface SearchParamsProps {
|
||||
[key: string]: string | string[] | undefined;
|
||||
|
||||
@@ -140,6 +140,14 @@ export const addProviderFormSchema = z
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("vercel"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Team ID is required"),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -306,7 +314,14 @@ export const addCredentialsFormSchema = (
|
||||
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]:
|
||||
z.string().min(1, "Cloud name is required"),
|
||||
}
|
||||
: {}),
|
||||
: providerType === "vercel"
|
||||
? {
|
||||
[ProviderCredentialFields.VERCEL_API_TOKEN]:
|
||||
z
|
||||
.string()
|
||||
.min(1, "API Token is required"),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
.superRefine((data: Record<string, string | undefined>, ctx) => {
|
||||
if (providerType === "m365") {
|
||||
@@ -420,37 +435,17 @@ export const addCredentialsRoleFormSchema = (providerType: string) =>
|
||||
[ProviderCredentialFields.ROLE_SESSION_NAME]: z.string().optional(),
|
||||
[ProviderCredentialFields.CREDENTIALS_TYPE]: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (
|
||||
.refine(
|
||||
(data) =>
|
||||
data[ProviderCredentialFields.CREDENTIALS_TYPE] !==
|
||||
"access-secret-key"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasAccessKey =
|
||||
(data[ProviderCredentialFields.AWS_ACCESS_KEY_ID] || "").trim()
|
||||
.length > 0;
|
||||
const hasSecretAccessKey =
|
||||
(data[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY] || "").trim()
|
||||
.length > 0;
|
||||
|
||||
if (!hasAccessKey) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "AWS Access Key ID is required.",
|
||||
path: [ProviderCredentialFields.AWS_ACCESS_KEY_ID],
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasSecretAccessKey) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "AWS Secret Access Key is required.",
|
||||
path: [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY],
|
||||
});
|
||||
}
|
||||
})
|
||||
"access-secret-key" ||
|
||||
(data[ProviderCredentialFields.AWS_ACCESS_KEY_ID] &&
|
||||
data[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]),
|
||||
{
|
||||
message: "AWS Access Key ID and Secret Access Key are required.",
|
||||
path: [ProviderCredentialFields.AWS_ACCESS_KEY_ID],
|
||||
},
|
||||
)
|
||||
: providerType === "alibabacloud"
|
||||
? z.object({
|
||||
[ProviderCredentialFields.PROVIDER_ID]: z.string(),
|
||||
|
||||
@@ -11,6 +11,7 @@ export const PROVIDER_TYPES = [
|
||||
"alibabacloud",
|
||||
"cloudflare",
|
||||
"openstack",
|
||||
"vercel",
|
||||
] as const;
|
||||
|
||||
export type ProviderType = (typeof PROVIDER_TYPES)[number];
|
||||
@@ -28,6 +29,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