Compare commits

...

4 Commits

Author SHA1 Message Date
Daniel Barranquero
db18e47467 feat: add docs and modify gh workflows 2026-03-20 16:40:23 +01:00
Daniel Barranquero
ea5ba82333 fix tests 2026-03-20 14:23:55 +01:00
Daniel Barranquero
2d5e948c96 feat: scan all teams when no team is specified 2026-03-20 13:56:46 +01:00
Daniel Barranquero
f9ccc89177 chore: update metadata 2026-03-20 12:25:19 +01:00
60 changed files with 555 additions and 127 deletions

7
.github/labeler.yml vendored
View File

@@ -67,6 +67,11 @@ provider/googleworkspace:
- any-glob-to-any-file: "prowler/providers/googleworkspace/**"
- any-glob-to-any-file: "tests/providers/googleworkspace/**"
provider/vercel:
- changed-files:
- any-glob-to-any-file: "prowler/providers/vercel/**"
- any-glob-to-any-file: "tests/providers/vercel/**"
github_actions:
- changed-files:
- any-glob-to-any-file: ".github/workflows/*"
@@ -102,6 +107,8 @@ mutelist:
- any-glob-to-any-file: "tests/providers/openstack/lib/mutelist/**"
- any-glob-to-any-file: "prowler/providers/googleworkspace/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/googleworkspace/lib/mutelist/**"
- any-glob-to-any-file: "prowler/providers/vercel/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/vercel/lib/mutelist/**"
integration/s3:
- changed-files:

View File

@@ -177,6 +177,14 @@ modules:
- tests/providers/llm/**
e2e: []
- name: sdk-vercel
match:
- prowler/providers/vercel/**
- prowler/compliance/vercel/**
tests:
- tests/providers/vercel/**
e2e: []
# ============================================
# SDK - Lib modules
# ============================================

View File

@@ -470,6 +470,30 @@ jobs:
flags: prowler-py${{ matrix.python-version }}-googleworkspace
files: ./googleworkspace_coverage.xml
# Vercel Provider
- name: Check if Vercel files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-vercel
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/vercel/**
./tests/**/vercel/**
./poetry.lock
- name: Run Vercel tests
if: steps.changed-vercel.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/vercel --cov-report=xml:vercel_coverage.xml tests/providers/vercel
- name: Upload Vercel coverage to Codecov
if: steps.changed-vercel.outputs.any_changed == 'true'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: prowler-py${{ matrix.python-version }}-vercel
files: ./vercel_coverage.xml
# Lib
- name: Check if Lib files changed
if: steps.check-changes.outputs.any_changed == 'true'

View File

@@ -119,6 +119,7 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
| Image | N/A | N/A | N/A | N/A | Official | CLI, API |
| Google Workspace | 1 | 1 | 0 | 1 | Official | CLI |
| OpenStack | 27 | 4 | 0 | 8 | Official | UI, API, CLI |
| Vercel | 30 | 6 | 0 | 5 | Official | CLI |
| NHN | 6 | 2 | 1 | 0 | Unofficial | CLI |
> [!Note]

View File

@@ -296,6 +296,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"
]
}
]
},

View File

@@ -37,6 +37,7 @@ The supported providers right now are:
| [Infra as Code](/user-guide/providers/iac/getting-started-iac) | Official | Repositories | UI, API, CLI |
| [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | Organizations | UI, API, CLI |
| [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | UI, API, CLI |
| [Vercel](/user-guide/providers/vercel/getting-started-vercel) | Official | Teams / Projects | CLI |
| [LLM](/user-guide/providers/llm/getting-started-llm) | Official | Models | CLI |
| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images | CLI, API |
| [Google Workspace](/user-guide/providers/googleworkspace/getting-started-googleworkspace) | Official | Domains | CLI |

View File

@@ -0,0 +1,137 @@
---
title: "Vercel Authentication in Prowler"
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="5.21.0" />
Prowler for Vercel authenticates using an **API Token**.
## Required Permissions
Prowler requires read-only access to Vercel teams, projects, deployments, domains, and security settings. The API Token must have access to the target team scope.
<Note>
Vercel API Tokens inherit the permissions of the user that created them. Ensure the user has at least a **Viewer** role on the team to be scanned.
</Note>
| Resource | Access | Description |
|----------|--------|-------------|
| Teams | Read | Required to list teams, members, and SSO configuration |
| Projects | Read | Required to list projects, environment variables, and deployment protection settings |
| Deployments | Read | Required to list deployments and protection status |
| Domains | Read | Required to list domains, DNS records, and SSL certificates |
| Firewall | Read | Required to read WAF rules, rate limiting, and IP blocking configuration |
---
## API Token
### Step 1: Create an API Token
1. Log into the [Vercel Dashboard](https://vercel.com/dashboard).
2. Click the account avatar in the bottom-left corner and select "Settings".
![Vercel Account Settings](/user-guide/providers/vercel/images/vercel-account-settings.png)
3. In the left sidebar, click "Tokens".
4. Under **Create Token**, enter a descriptive name (e.g., "Prowler Scan").
5. Select the **Scope** — choose the team to be scanned or "Full Account" for all teams.
6. Set an **Expiration** date, or select "No expiration" for continuous scanning.
7. Click **Create**.
![Create Vercel Token](/user-guide/providers/vercel/images/vercel-create-token.png)
8. Copy the token immediately.
<Warning>
Vercel only displays the token once. Copy it immediately and store it securely. If lost, a new token must be created.
</Warning>
### Step 2: Provide the Token to Prowler
Export the token as an environment variable:
```console
export VERCEL_TOKEN="your-api-token-here"
prowler vercel
```
---
## Team Scoping (Optional)
By default, Prowler auto-discovers all teams the authenticated user belongs to and scans each one. To restrict the scan to a specific team, provide the Team ID.
### Locate the Team ID
1. In the Vercel Dashboard, navigate to "Settings" for the target team.
2. Scroll down to the **Team ID** section and copy the value.
![Vercel Team ID](/user-guide/providers/vercel/images/vercel-team-id.png)
### Provide the Team ID to Prowler
Export the Team ID as an environment variable:
```console
export VERCEL_TOKEN="your-api-token-here"
export VERCEL_TEAM="team_Yj41RYnEfdjpqxzAecFgwYAR"
prowler vercel
```
---
## Environment Variables Reference
| Variable | Required | Description |
|----------|----------|-------------|
| `VERCEL_TOKEN` | Yes | Vercel API Bearer Token |
| `VERCEL_TEAM` | No | Team ID or slug to scope the scan to a single team |
---
## Best Practices
- **Create a dedicated token for Prowler** — Avoid reusing tokens shared with other integrations.
- **Use environment variables** — Never hardcode credentials in scripts or commands.
- **Scope tokens to specific teams** — When possible, limit token access to the team being scanned.
- **Set token expiration** — Use time-limited tokens and rotate them regularly.
- **Use least privilege** — Assign the Viewer role to the user creating the token unless write access is explicitly needed.
---
## Troubleshooting
### "Vercel credentials not found" Error
This error occurs when no API Token is provided. Ensure the `VERCEL_TOKEN` environment variable is set:
```console
export VERCEL_TOKEN="your-api-token-here"
```
### "Invalid or expired Vercel API token" Error
- Verify the API Token is correct and has not expired.
- Check that the token has not been revoked in the Vercel Dashboard under "Settings" > "Tokens".
### "Insufficient permissions" Error
- Ensure the user that created the token has at least a **Viewer** role on the target team.
- If scanning a specific team, verify the token scope includes that team.
### "Team not found or not accessible" Error
This error occurs when the provided `VERCEL_TEAM` value does not match an accessible team. Verify the Team ID is correct:
1. Navigate to the team "Settings" in the Vercel Dashboard.
2. Copy the exact **Team ID** value from the settings page.
### "Rate limit exceeded" Error
Vercel applies rate limits to API requests. Prowler automatically retries rate-limited requests up to 3 times with exponential backoff. If this error persists:
- Reduce the number of projects being scanned in a single run using the `--project` argument.
- Wait a few minutes and retry the scan.

View File

@@ -0,0 +1,108 @@
---
title: "Getting Started With Vercel on Prowler"
---
import { VersionBadge } from "/snippets/version-badge.mdx"
Prowler for Vercel scans teams and projects for security misconfigurations, including deployment protection, environment variable exposure, WAF rules, domain configuration, team access controls, and more.
## Prerequisites
Set up authentication for Vercel with the [Vercel Authentication](/user-guide/providers/vercel/authentication) guide before starting:
- Create a Vercel API Token with access to the target team
- Identify the Team ID (optional, required to scope the scan to a single team)
## Prowler CLI
<VersionBadge version="5.21.0" />
### Step 1: Set Up Authentication
Follow the [Vercel Authentication](/user-guide/providers/vercel/authentication) guide to create an API Token, then export it:
```console
export VERCEL_TOKEN="your-api-token-here"
```
Optionally, scope the scan to a specific team:
```console
export VERCEL_TEAM="team_Yj41RYnEfdjpqxzAecFgwYAR"
```
### Step 2: Run the First Scan
Run a baseline scan after credentials are configured:
```console
prowler vercel
```
Prowler automatically discovers all teams accessible with the provided token and runs security checks against them.
### Step 3: Filter the Scan Scope (Optional)
#### Filter by Team
To scan a specific team, set the `VERCEL_TEAM` environment variable with the Team ID or slug:
```console
export VERCEL_TEAM="team_Yj41RYnEfdjpqxzAecFgwYAR"
prowler vercel
```
<Note>
When no team is specified, Prowler auto-discovers all teams the authenticated user belongs to and scans each one.
</Note>
#### Filter by Project
To scan only specific projects, use the `--project` argument:
```console
prowler vercel --project my-project-name
```
Multiple projects can be specified:
```console
prowler vercel --project my-project-name another-project
```
Project IDs are also supported:
```console
prowler vercel --project prj_abc123def456
```
### Step 4: Use a Custom Configuration (Optional)
Prowler uses a configuration file to customize provider behavior. The Vercel configuration includes:
```yaml
vercel:
# Maximum number of retries for API requests (default is 3)
max_retries: 3
```
To use a custom configuration:
```console
prowler vercel --config-file /path/to/config.yaml
```
---
## Supported Services
Prowler for Vercel includes security checks across the following services:
| Service | Description |
|---------|-------------|
| **Authentication** | Token expiration and staleness checks |
| **Deployment** | Preview deployment access and production stability |
| **Domain** | DNS configuration, SSL certificates, and wildcard exposure |
| **Project** | Deployment protection, environment variable security, fork protection, and skew protection |
| **Security** | Web Application Firewall (WAF), rate limiting, IP blocking, and managed rulesets |
| **Team** | SSO enforcement, directory sync, member access, and invitation hygiene |

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

View File

@@ -35,6 +35,13 @@ class VercelService:
# Thread pool for parallel API calls
self.thread_pool = ThreadPoolExecutor(max_workers=MAX_WORKERS)
@property
def _all_team_ids(self) -> list[str]:
"""Return team IDs to scan: explicit team_id, or all auto-discovered teams."""
if self._team_id:
return [self._team_id]
return [t.id for t in self.provider.identity.teams]
def _get(self, path: str, params: dict = None) -> dict:
"""Make a rate-limit-aware GET request to the Vercel API.

View File

@@ -30,6 +30,7 @@ class VercelIdentityInfo(BaseModel):
username: Optional[str] = None
email: Optional[str] = None
team: Optional[VercelTeamInfo] = None
teams: list[VercelTeamInfo] = Field(default_factory=list)
class VercelOutputOptions(ProviderOutputOptions):

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelAuthToken",
"ResourceType": "NotDefined",
"ResourceGroup": "governance",
"Description": "Checks whether Vercel API tokens have been active within the last 90 days. Stale tokens that remain unused for extended periods represent unnecessary access credentials that increase the attack surface. Tokens with no recorded activity are also flagged.",
"Risk": "Stale tokens that have not been used for over 90 days may belong to decommissioned integrations, former team members, or forgotten automation. These tokens remain valid and could be compromised or misused without detection, as their inactivity makes suspicious usage harder to notice in access logs.",
"Description": "**Vercel API tokens** are assessed for **staleness** by checking whether each token has been active within the last 90 days. Stale tokens that remain unused for extended periods represent unnecessary access credentials that increase the attack surface. Tokens with no recorded activity are also flagged.",
"Risk": "Stale tokens that have not been used for over **90 days** may belong to decommissioned integrations, former team members, or forgotten automation. These tokens remain **valid** and could be compromised or misused without detection, as their inactivity makes suspicious usage harder to notice in access logs.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/rest-api#authentication"

View File

@@ -16,20 +16,36 @@ class Authentication(VercelService):
self._list_tokens()
def _list_tokens(self):
"""List all API tokens for the authenticated user."""
"""List all API tokens for the authenticated user and their teams."""
# Always fetch personal tokens (no teamId filter)
self._fetch_tokens_for_scope(team_id=None)
# Also fetch tokens scoped to each team
for tid in self._all_team_ids:
self._fetch_tokens_for_scope(team_id=tid)
logger.info(f"Authentication - Found {len(self.tokens)} token(s)")
def _fetch_tokens_for_scope(self, team_id: str = None):
"""Fetch tokens for a specific scope (personal or team).
Args:
team_id: Team ID to fetch tokens for. None for personal tokens.
"""
try:
data = self._get("/v5/user/tokens")
# Always set teamId key explicitly — _get won't auto-inject when key
# is present, and requests skips None values from query params.
params = {"teamId": team_id}
data = self._get("/v5/user/tokens", params=params)
if not data:
return
tokens = data.get("tokens", [])
seen_ids: set[str] = set()
for token in tokens:
token_id = token.get("id", "")
if not token_id or token_id in seen_ids:
if not token_id or token_id in self.tokens:
continue
seen_ids.add(token_id)
active_at = None
if token.get("activeAt"):
@@ -58,14 +74,13 @@ class Authentication(VercelService):
expires_at=expires_at,
scopes=token.get("scopes", []),
origin=token.get("origin"),
team_id=token.get("teamId") or self.provider.session.team_id,
team_id=token.get("teamId") or team_id,
)
logger.info(f"Authentication - Found {len(self.tokens)} token(s)")
except Exception as error:
scope = f"team {team_id}" if team_id else "personal"
logger.error(
f"Authentication - Error listing tokens: "
f"Authentication - Error listing tokens for {scope}: "
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "VercelAuthToken",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Checks whether Vercel API tokens have not expired. Expired tokens indicate poor token lifecycle management and may suggest that integrations or automation relying on these tokens are failing silently. Tokens without an expiration date are considered valid.",
"Risk": "Expired tokens indicate that token lifecycle management is not being followed. While expired tokens cannot be used for authentication, their presence suggests that token rotation practices are not in place. Integrations or CI/CD pipelines relying on expired tokens will fail, potentially causing service disruptions.",
"Description": "**Vercel API tokens** are assessed for **expiration status** to identify tokens that have exceeded their validity period. Expired tokens indicate poor token lifecycle management and may suggest that integrations or automation relying on these tokens are failing silently. Tokens without an expiration date are considered valid.",
"Risk": "Expired tokens indicate that **token lifecycle management** is not being followed. While expired tokens cannot be used for authentication, their presence suggests that token rotation practices are not in place. Integrations or **CI/CD pipelines** relying on expired tokens will fail, potentially causing service disruptions.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/rest-api#authentication"

View File

@@ -24,6 +24,7 @@ class authentication_token_not_expired(Check):
List[CheckReportVercel]: A list of reports for each token.
"""
findings = []
now = datetime.now(timezone.utc)
for token in authentication_client.tokens.values():
report = CheckReportVercel(
metadata=self.metadata(),
@@ -38,7 +39,7 @@ class authentication_token_not_expired(Check):
f"Token '{token.name}' ({token.id}) does not have an expiration "
f"date set and is currently valid."
)
elif token.expires_at > datetime.now(timezone.utc):
elif token.expires_at > now:
report.status = "PASS"
report.status_extended = (
f"Token '{token.name}' ({token.id}) is valid and expires "

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "VercelDeployment",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Checks whether Vercel preview deployments have deployment protection configured. Preview deployments without protection are publicly accessible to anyone who knows or guesses the URL, potentially exposing unreleased features, staging data, or internal endpoints.",
"Risk": "Without deployment protection on preview deployments, any person who obtains or guesses a preview URL can view unreleased application code, test data, or internal API endpoints. This increases the attack surface and may leak sensitive business logic or credentials embedded in preview builds.",
"Description": "**Vercel preview deployments** are assessed for **deployment protection** configuration. Preview deployments without protection are publicly accessible to anyone who knows or guesses the URL, potentially exposing unreleased features, staging data, or internal endpoints.",
"Risk": "Without **deployment protection** on preview deployments, any person who obtains or guesses a preview URL can view **unreleased application code**, test data, or internal API endpoints. This increases the attack surface and may leak sensitive business logic or credentials embedded in preview builds.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/deployment-protection"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelDeployment",
"ResourceType": "NotDefined",
"ResourceGroup": "devops",
"Description": "Checks whether Vercel production deployments are sourced from a stable branch (main or master). Deploying to production from feature branches bypasses standard CI/CD review processes and may introduce untested or incomplete code into the production environment.",
"Risk": "Production deployments from feature branches may contain untested, incomplete, or unapproved code changes. This bypasses the standard code review and merge workflow, increasing the risk of shipping bugs, security vulnerabilities, or breaking changes to end users.",
"Description": "**Vercel production deployments** are assessed for **source branch stability** by verifying they are sourced from a stable branch (`main` or `master`). Deploying to production from feature branches bypasses standard CI/CD review processes and may introduce untested or incomplete code into the production environment.",
"Risk": "Production deployments from **feature branches** may contain untested, incomplete, or unapproved code changes. This bypasses the standard **code review and merge workflow**, increasing the risk of shipping bugs, security vulnerabilities, or breaking changes to end users.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/deployments/git"

View File

@@ -37,7 +37,7 @@ class deployment_production_uses_stable_target(Check):
stable_branches = deployment_client.audit_config.get(
"stable_branches", ["main", "master"]
)
branch = deployment.git_source.get("branch", "")
branch = deployment.git_source.get("branch") or ""
if branch in stable_branches:
report.status = "PASS"
report.status_extended = (

View File

@@ -100,4 +100,4 @@ class VercelDeployment(BaseModel):
project_name: Optional[str] = None
team_id: Optional[str] = None
git_source: Optional[dict] = None
deployment_protection: Optional[str] = None
deployment_protection: Optional[dict] = None

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelDomain",
"ResourceType": "NotDefined",
"ResourceGroup": "network",
"Description": "Checks whether Vercel domains have their DNS records properly configured to point to Vercel's infrastructure. Misconfigured DNS can result in domains that fail to serve content, SSL certificate provisioning failures, and degraded user experience.",
"Risk": "Misconfigured DNS records can cause the domain to be unreachable, preventing users from accessing the application. It can also prevent SSL certificate provisioning, resulting in browser security warnings. Stale DNS configurations may point to decommissioned infrastructure, creating a risk of subdomain takeover.",
"Description": "**Vercel domains** are assessed for **DNS configuration** to verify records properly point to Vercel's infrastructure. Misconfigured DNS can result in domains that fail to serve content, SSL certificate provisioning failures, and degraded user experience.",
"Risk": "**Misconfigured DNS records** can cause the domain to be unreachable, preventing users from accessing the application. It can also prevent **SSL certificate provisioning**, resulting in browser security warnings. Stale DNS configurations may point to decommissioned infrastructure, creating a risk of **subdomain takeover**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/projects/domains"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelDomain",
"ResourceType": "NotDefined",
"ResourceGroup": "network",
"Description": "Checks whether Vercel domains have wildcard DNS records (e.g., *.example.com) that could route traffic from any subdomain to the application. Wildcard records increase the attack surface by allowing arbitrary subdomains to resolve and serve content.",
"Risk": "Wildcard DNS records allow any subdomain to resolve to the Vercel deployment, which can be exploited for phishing, cookie scoping attacks, or bypassing Content Security Policy restrictions. Attackers may use arbitrary subdomains to create convincing phishing pages or to exploit trust relationships between subdomains.",
"Description": "**Vercel domains** are assessed for **wildcard DNS exposure** by checking whether wildcard DNS records (e.g., `*.example.com`) could route traffic from any subdomain to the application. Wildcard records increase the attack surface by allowing arbitrary subdomains to resolve and serve content.",
"Risk": "**Wildcard DNS records** allow any subdomain to resolve to the Vercel deployment, which can be exploited for **phishing**, cookie scoping attacks, or bypassing **Content Security Policy** restrictions. Attackers may use arbitrary subdomains to create convincing phishing pages or to exploit trust relationships between subdomains.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/projects/domains"

View File

@@ -31,7 +31,7 @@ class domain_no_wildcard_dns_exposure(Check):
wildcard_records = []
for record in domain.dns_records:
record_name = record.get("name", "")
record_name = record.get("name") or ""
if record_name == "*" or record_name.startswith("*."):
wildcard_records.append(record_name)

View File

@@ -1,16 +1,16 @@
{
"Provider": "vercel",
"CheckID": "domain_ssl_certificate_valid",
"CheckTitle": "Vercel domains have a valid SSL certificate provisioned",
"CheckTitle": "Vercel domains have an SSL certificate provisioned",
"CheckType": [],
"ServiceName": "domain",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "VercelDomain",
"ResourceType": "NotDefined",
"ResourceGroup": "network",
"Description": "Checks whether Vercel domains have an SSL certificate provisioned. Vercel automatically provisions and renews SSL certificates for properly configured domains. A missing SSL certificate indicates a configuration issue that leaves traffic unencrypted.",
"Risk": "Without an SSL certificate, traffic between users and the domain is transmitted in plain text. This exposes sensitive data such as authentication tokens, form submissions, and personal information to interception via man-in-the-middle attacks. Search engines also penalize non-HTTPS sites, reducing visibility.",
"Description": "**Vercel domains** are assessed for **SSL certificate provisioning** to verify a valid certificate is in place. Vercel automatically provisions and renews SSL certificates for properly configured domains. A missing SSL certificate indicates a configuration issue that leaves traffic unencrypted.",
"Risk": "Without an **SSL certificate**, traffic between users and the domain is transmitted in **plain text**. This exposes sensitive data such as authentication tokens, form submissions, and personal information to interception via **man-in-the-middle attacks**. Search engines also penalize non-HTTPS sites, reducing visibility.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/encryption"

View File

@@ -32,7 +32,7 @@ class domain_ssl_certificate_valid(Check):
if domain.ssl_certificate is not None:
report.status = "PASS"
report.status_extended = (
f"Domain {domain.name} has a valid SSL certificate provisioned."
f"Domain {domain.name} has an SSL certificate provisioned."
)
else:
report.status = "FAIL"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "VercelDomain",
"ResourceType": "NotDefined",
"ResourceGroup": "network",
"Description": "Checks whether Vercel domains have passed ownership verification. Unverified domains may not serve traffic correctly and could indicate a pending or incomplete domain setup. Domain verification confirms that the domain owner has authorized Vercel to manage the domain.",
"Risk": "Unverified domains may fail to resolve or serve content, causing downtime for users. An unverified domain could also indicate a stale or orphaned configuration, or a domain that was added but never properly transferred, creating potential for domain takeover if the ownership verification is left incomplete.",
"Description": "**Vercel domains** are assessed for **ownership verification** status. Unverified domains may not serve traffic correctly and could indicate a pending or incomplete domain setup. Domain verification confirms that the domain owner has authorized Vercel to manage the domain.",
"Risk": "**Unverified domains** may fail to resolve or serve content, causing **downtime** for users. An unverified domain could also indicate a stale or orphaned configuration, or a domain that was added but never properly transferred, creating potential for **domain takeover** if the ownership verification is left incomplete.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/projects/domains"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Vercel can automatically expose system environment variables (such as VERCEL_URL, VERCEL_ENV, VERCEL_GIT_COMMIT_SHA) to the build and runtime environment. When enabled, these variables are injected into every deployment and may be accessible in client-side JavaScript bundles if not handled carefully, leaking internal infrastructure details.",
"Risk": "Automatically exposed system environment variables can reveal deployment URLs, Git metadata, environment names, and other internal details. If these values are inadvertently included in client-side bundles, attackers can use them to map infrastructure, identify staging environments, or craft targeted attacks against specific deployment instances.",
"Description": "**Vercel projects** are assessed for **automatic system environment variable exposure** (`VERCEL_URL`, `VERCEL_ENV`, `VERCEL_GIT_COMMIT_SHA`). When enabled, these variables are injected into every deployment and may be accessible in client-side JavaScript bundles if not handled carefully, leaking internal infrastructure details.",
"Risk": "Automatically exposed **system environment variables** can reveal deployment URLs, Git metadata, environment names, and other internal details. If these values are inadvertently included in **client-side bundles**, attackers can use them to map infrastructure, identify staging environments, or craft targeted attacks against specific deployment instances.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/projects/environment-variables/system-environment-variables"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Vercel Deployment Protection restricts access to preview deployments by requiring authentication before visitors can view them. When disabled, anyone with the preview URL can access in-progress or staging versions of your application, potentially exposing unreleased features, debug information, or internal endpoints.",
"Risk": "Without deployment protection on preview deployments, any person who obtains or guesses a preview URL can view unreleased application code, test data, or internal API endpoints. This increases the attack surface and may leak sensitive business logic or credentials embedded in preview builds.",
"Description": "**Vercel projects** are assessed for **deployment protection** configuration, which restricts access to preview deployments by requiring authentication before visitors can view them. When disabled, anyone with the preview URL can access in-progress or staging versions of the application, potentially exposing unreleased features, debug information, or internal endpoints.",
"Risk": "Without **deployment protection** on preview deployments, any person who obtains or guesses a preview URL can view **unreleased application code**, test data, or internal API endpoints. This increases the attack surface and may leak sensitive business logic or credentials embedded in preview builds.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/deployment-protection"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Vercel's directory listing feature, when enabled, allows visitors to browse the file structure of a deployment when no index file is present in a directory. This can expose source files, configuration files, and other assets that should not be publicly accessible.",
"Risk": "Enabled directory listing allows attackers to enumerate the file structure of the deployment, potentially discovering backup files, configuration files, source maps, or other sensitive assets. This information disclosure can be leveraged to identify attack vectors or access files that were not intended to be public.",
"Description": "**Vercel projects** are assessed for **directory listing** configuration. When enabled, this feature allows visitors to browse the file structure of a deployment when no index file is present in a directory, potentially exposing source files, configuration files, and other assets that should not be publicly accessible.",
"Risk": "Enabled **directory listing** allows attackers to enumerate the file structure of the deployment, potentially discovering backup files, configuration files, source maps, or other **sensitive assets**. This information disclosure can be leveraged to identify attack vectors or access files that were not intended to be public.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/projects/project-configuration"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "**Vercel project environment variables** are assessed for **overly broad targeting** by checking whether any variable targets all three environments (production, preview, development) simultaneously, which violates the principle of least privilege.",
"Risk": "Environment variables targeting all environments share the same values across production, preview, and development, increasing blast radius if credentials are compromised. Production secrets are exposed to weaker environments, making it harder to isolate and track unauthorized changes.",
"Risk": "Environment variables targeting **all environments** share the same values across production, preview, and development, increasing **blast radius** if credentials are compromised. Production secrets are exposed to weaker environments, making it harder to isolate and track unauthorized changes.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/environment-variables"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "**Vercel project environment variables** are assessed for **secret exposure** by checking whether variables with secret-like name suffixes (*_KEY, *_SECRET, *_TOKEN, *_PASSWORD, *_API_KEY, *_PRIVATE_KEY) are stored using the 'plain' type, which makes their values readable.",
"Risk": "Secrets stored as plain text environment variables are visible to all team members with project access and may appear in API responses. Plaintext secrets can be read through the Vercel dashboard or API, enabling unauthorized modification of connected services or disruption of integrations.",
"Description": "**Vercel project environment variables** are assessed for **secret exposure** by checking whether variables with secret-like name suffixes (`*_KEY`, `*_SECRET`, `*_TOKEN`, `*_PASSWORD`, `*_API_KEY`, `*_PRIVATE_KEY`) are stored using the `plain` type, which makes their values readable.",
"Risk": "Secrets stored as **plain text** environment variables are visible to all team members with project access and may appear in API responses. Plaintext secrets can be read through the Vercel dashboard or API, enabling **unauthorized modification** of connected services or disruption of integrations.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/environment-variables"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "**Vercel project environment variables** are assessed for **environment separation** by checking whether sensitive variables (type 'secret' or 'encrypted') that target the 'production' environment also target 'preview', which could expose production credentials to untrusted preview builds.",
"Risk": "Preview deployments are often triggered by pull requests, including those from external contributors or forks. Sharing production secrets with preview environments can lead to credential theft. Production API keys and database credentials could be exfiltrated by malicious code in preview builds and used to modify or disrupt live services.",
"Description": "**Vercel project environment variables** are assessed for **environment separation** by checking whether sensitive variables (type `secret` or `encrypted`) that target the `production` environment also target `preview`, which could expose production credentials to untrusted preview builds.",
"Risk": "Preview deployments are often triggered by **pull requests**, including those from external contributors or forks. Sharing **production secrets** with preview environments can lead to credential theft. Production API keys and database credentials could be exfiltrated by malicious code in preview builds and used to modify or disrupt live services.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/environment-variables"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "**Vercel project environment variables** are assessed for **encryption** by checking whether variables with sensitive-looking names (containing KEY, SECRET, TOKEN, PASSWORD, CREDENTIAL, API_KEY, PRIVATE, AUTH) are stored with type 'encrypted' or 'secret' rather than 'plain'.",
"Risk": "Environment variables stored as plain text can be read by anyone with project access and are visible in build logs. API keys, passwords, and tokens in plain text can be exposed, allowing attackers to modify external services, compromise data, or cause service disruption.",
"Description": "**Vercel project environment variables** are assessed for **encryption** by checking whether variables with sensitive-looking names (containing `KEY`, `SECRET`, `TOKEN`, `PASSWORD`, `CREDENTIAL`, `API_KEY`, `PRIVATE`, `AUTH`) are stored with type `encrypted` or `secret` rather than `plain`.",
"Risk": "Environment variables stored as **plain text** can be read by anyone with project access and are visible in build logs. API keys, passwords, and tokens in plain text can be exposed, allowing attackers to **modify external services**, compromise data, or cause service disruption.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/environment-variables"

View File

@@ -1,9 +1,10 @@
import re
from typing import List
from prowler.lib.check.models import Check, CheckReportVercel
from prowler.providers.vercel.services.project.project_client import project_client
SENSITIVE_PATTERNS = {
SENSITIVE_PATTERNS = [
"KEY",
"SECRET",
"TOKEN",
@@ -12,7 +13,10 @@ SENSITIVE_PATTERNS = {
"API_KEY",
"PRIVATE",
"AUTH",
}
]
# Pre-compiled regex: each pattern must appear as a whole word (bounded by _ or string edges)
_SENSITIVE_RE = re.compile(r"(?:^|_)(?:" + "|".join(SENSITIVE_PATTERNS) + r")(?:_|$)")
class project_environment_sensitive_vars_encrypted(Check):
@@ -40,7 +44,7 @@ class project_environment_sensitive_vars_encrypted(Check):
plain_sensitive_keys = []
for env_var in project.environment_variables:
upper_key = env_var.key.upper()
if any(pattern in upper_key for pattern in SENSITIVE_PATTERNS):
if _SENSITIVE_RE.search(upper_key):
if env_var.type not in ("encrypted", "secret"):
plain_sensitive_keys.append(env_var.key)

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "devops",
"Description": "Vercel Git Fork Protection controls whether pull requests from forked repositories can trigger deployments and access environment variables. When disabled, anyone who forks a public repository can submit a pull request that triggers a Vercel build with access to the project's environment variables, including secrets and API keys.",
"Risk": "Without Git fork protection, an attacker can fork a public repository, modify the build process to exfiltrate environment variables (API keys, database credentials, third-party tokens), and submit a pull request. The Vercel build triggered by the PR would execute the attacker's code with access to the project's secrets, leading to credential theft and potential full system compromise.",
"Description": "**Vercel projects** are assessed for **Git fork protection** configuration, which controls whether pull requests from forked repositories can trigger deployments and access environment variables. When disabled, anyone who forks a public repository can submit a pull request that triggers a Vercel build with access to the project's environment variables, including secrets and API keys.",
"Risk": "Without **Git fork protection**, an attacker can fork a public repository, modify the build process to **exfiltrate environment variables** (API keys, database credentials, third-party tokens), and submit a pull request. The Vercel build triggered by the PR would execute the attacker's code with access to the project's secrets, leading to **credential theft** and potential full system compromise.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/deployment-protection/managing-deployment-protection#git-fork-protection"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Vercel Password Protection adds a shared-password gate in front of deployments, requiring visitors to enter a password before they can access the application. This provides an additional layer of access control beyond Vercel Authentication, useful for sharing preview deployments with external stakeholders who do not have Vercel accounts.",
"Risk": "Without password protection, deployments are accessible to anyone who has the URL. For projects that contain pre-release features, client work, or sensitive content, this means unauthorized individuals can view and interact with the application without any authentication barrier.",
"Description": "**Vercel projects** are assessed for **password protection** configuration, which adds a shared-password gate in front of deployments requiring visitors to enter a password before they can access the application. This provides an additional layer of access control beyond Vercel Authentication, useful for sharing preview deployments with external stakeholders who do not have Vercel accounts.",
"Risk": "Without **password protection**, deployments are accessible to anyone who has the URL. For projects that contain pre-release features, client work, or sensitive content, this means **unauthorized individuals** can view and interact with the application without any authentication barrier.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/deployment-protection/methods-to-protect-deployments/password-protection"

View File

@@ -23,7 +23,7 @@ class project_password_protection_enabled(Check):
for project in project_client.projects.values():
report = CheckReportVercel(metadata=self.metadata(), resource=project)
if project.password_protection is not None and isinstance(
if project.password_protection and isinstance(
project.password_protection, dict
):
report.status = "PASS"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Vercel Deployment Protection for production restricts access to the live production deployment by requiring Vercel Authentication or other access controls. When enabled, visitors must authenticate before accessing the production URL, adding a layer of defense for internal applications, staging environments promoted to production, or projects that should not be publicly accessible.",
"Risk": "Without production deployment protection, the live production deployment is fully accessible to anyone on the internet. For internal tools, admin panels, or pre-launch applications this means unauthorized users can interact with production systems, potentially exploiting vulnerabilities, accessing sensitive data, or abusing application functionality.",
"Description": "**Vercel projects** are assessed for **production deployment protection** configuration, which restricts access to the live production deployment by requiring Vercel Authentication or other access controls. When enabled, visitors must authenticate before accessing the production URL, adding a layer of defense for internal applications or projects that should not be publicly accessible.",
"Risk": "Without **production deployment protection**, the live production deployment is fully accessible to anyone on the internet. For internal tools, admin panels, or pre-launch applications this means **unauthorized users** can interact with production systems, potentially exploiting vulnerabilities, accessing sensitive data, or abusing application functionality.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/deployment-protection"

View File

@@ -41,8 +41,6 @@ class Project(VercelService):
# Parse deployment protection
dp = None
proj.get("protectionBypass", {})
proj.get("ssoProtection", {})
dp_raw = proj.get("deploymentProtection", {}) or {}
preview_dp = dp_raw.get("deploymentType", "none")

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "VercelProject",
"ResourceType": "NotDefined",
"ResourceGroup": "compute",
"Description": "Vercel Skew Protection ensures that clients interacting with your application always communicate with the correct deployment version, even during active rollouts. Without it, clients may fetch assets or make API calls against a different deployment version than the one that served the initial page, causing hydration errors, broken functionality, or data inconsistencies.",
"Risk": "Without skew protection, users may experience version mismatches during deployment rollouts where the HTML is served from one deployment version but subsequent client-side navigation or API calls hit a newer version. This can cause broken user interfaces, failed client-side transitions, or data corruption from incompatible API contract changes.",
"Description": "**Vercel projects** are assessed for **skew protection**, which ensures clients always communicate with the correct deployment version during rollouts. Without it, clients may fetch assets or call APIs against a different version than the one that served the initial page, causing hydration errors or broken functionality.",
"Risk": "Without **skew protection**, users may experience **version mismatches** during deployment rollouts where the HTML is served from one deployment version but subsequent client-side navigation or API calls hit a newer version. This can cause broken user interfaces, failed client-side transitions, or **data corruption** from incompatible API contract changes.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/deployments/skew-protection"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelFirewallConfig",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Checks whether Vercel projects have at least one custom firewall rule configured. Custom rules allow fine-grained control over traffic based on request attributes such as path, headers, user agent, and geographic location, providing application-specific protection beyond managed rulesets.",
"Risk": "Without custom firewall rules, the application lacks application-specific traffic filtering. Generic managed rulesets may not cover all threat vectors unique to the application. Custom rules are needed to block known attack patterns, restrict access to sensitive paths, and enforce application-level security policies.",
"Description": "**Vercel projects** are assessed for **custom firewall rule** configuration. Custom rules allow fine-grained control over traffic based on request attributes such as path, headers, user agent, and geographic location, providing application-specific protection beyond managed rulesets.",
"Risk": "Without **custom firewall rules**, the application lacks application-specific traffic filtering. Generic managed rulesets may not cover all threat vectors unique to the application. Custom rules are needed to block **known attack patterns**, restrict access to sensitive paths, and enforce application-level security policies.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/vercel-firewall/custom-rules"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelFirewallConfig",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Checks whether Vercel projects have at least one IP blocking rule configured. IP blocking rules allow you to deny access from known malicious IP addresses or ranges, reducing the attack surface and preventing traffic from untrusted sources.",
"Risk": "Without IP blocking rules, all traffic is accepted regardless of source IP. Known malicious IPs, abuse networks, and previously identified attackers can freely access the application. This increases the risk of automated scanning, credential stuffing, and targeted attacks from known threat sources.",
"Description": "**Vercel projects** are assessed for **IP blocking rule** configuration. IP blocking rules allow denying access from known malicious IP addresses or ranges, reducing the attack surface and preventing traffic from untrusted sources.",
"Risk": "Without **IP blocking rules**, all traffic is accepted regardless of source IP. Known malicious IPs, abuse networks, and previously identified attackers can freely access the application. This increases the risk of **automated scanning**, credential stuffing, and targeted attacks from known threat sources.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/vercel-firewall"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "VercelFirewallConfig",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Checks whether Vercel managed WAF rulesets are enabled for each project. Managed rulesets are curated by Vercel and provide protection against known attack patterns including OWASP Top 10 threats. This feature requires an Enterprise plan and reports MANUAL status when unavailable.",
"Risk": "Without managed rulesets enabled, the firewall lacks curated protection rules against well-known attack patterns. The application relies solely on custom rules, which may miss new or evolving threats that managed rulesets are designed to detect and block automatically.",
"Description": "**Vercel projects** are assessed for **managed WAF ruleset** enablement. Managed rulesets are curated by Vercel and provide protection against known attack patterns including **OWASP Top 10** threats. This feature requires an Enterprise plan and reports MANUAL status when unavailable.",
"Risk": "Without **managed rulesets** enabled, the firewall lacks curated protection rules against well-known attack patterns. The application relies solely on custom rules, which may miss **new or evolving threats** that managed rulesets are designed to detect and block automatically.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/vercel-firewall/managed-rulesets"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelFirewallConfig",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Checks whether Vercel projects have at least one rate limiting rule configured. Rate limiting protects applications from abuse, brute-force attacks, and DDoS attempts by restricting the number of requests from a single source within a given time window.",
"Risk": "Without rate limiting, the application is vulnerable to brute-force attacks on authentication endpoints, API abuse, resource exhaustion, and denial-of-service attacks. Attackers can overwhelm the application with excessive requests, degrading performance for legitimate users or exploiting endpoints without throttling.",
"Description": "**Vercel projects** are assessed for **rate limiting rule** configuration. Rate limiting protects applications from abuse, brute-force attacks, and DDoS attempts by restricting the number of requests from a single source within a given time window.",
"Risk": "Without **rate limiting**, the application is vulnerable to **brute-force attacks** on authentication endpoints, API abuse, resource exhaustion, and denial-of-service attacks. Attackers can overwhelm the application with excessive requests, degrading performance for legitimate users or exploiting endpoints without throttling.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/vercel-firewall"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "VercelFirewallConfig",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Checks whether the Vercel Web Application Firewall (WAF) is enabled for each project. The WAF provides protection against common web attacks including SQL injection, cross-site scripting (XSS), and other OWASP Top 10 threats.",
"Risk": "Without the Web Application Firewall enabled, the application is directly exposed to common web attacks including SQL injection, cross-site scripting, request smuggling, and other exploits. Attackers can exploit these vulnerabilities to steal data, deface the application, or gain unauthorized access.",
"Description": "**Vercel projects** are assessed for **Web Application Firewall (WAF)** enablement. The WAF provides protection against common web attacks including **SQL injection**, **cross-site scripting (XSS)**, and other OWASP Top 10 threats.",
"Risk": "Without the **Web Application Firewall** enabled, the application is directly exposed to common web attacks including **SQL injection**, **cross-site scripting**, request smuggling, and other exploits. Attackers can exploit these vulnerabilities to steal data, deface the application, or gain unauthorized access.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/security/vercel-firewall"

View File

@@ -24,7 +24,15 @@ class security_waf_enabled(Check):
for config in security_client.firewall_configs.values():
report = CheckReportVercel(metadata=self.metadata(), resource=config)
if config.firewall_enabled:
if config.managed_rulesets is None:
# 403 — plan limitation, cannot determine WAF status
report.status = "MANUAL"
report.status_extended = (
f"Project {config.project_name} ({config.project_id}) "
f"could not be checked for WAF status due to plan limitations. "
f"Manual verification is required."
)
elif config.firewall_enabled:
report.status = "PASS"
report.status_extended = (
f"Project {config.project_name} ({config.project_id}) "

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelTeam",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Checks whether the Vercel team has directory sync (SCIM) enabled. Directory sync automates user provisioning and deprovisioning by synchronizing team membership with an external identity provider, ensuring timely access revocation when employees leave.",
"Risk": "Without directory sync, user provisioning and deprovisioning must be managed manually, increasing the risk of orphaned accounts remaining active after employees leave or change roles. Manual processes are error-prone and may lead to unauthorized access persisting longer than intended.",
"Description": "**Vercel team** is assessed for **directory sync (SCIM)** enablement. Directory sync automates user provisioning and deprovisioning by synchronizing team membership with an external identity provider, ensuring timely access revocation when employees leave.",
"Risk": "Without **directory sync**, user provisioning and deprovisioning must be managed manually, increasing the risk of **orphaned accounts** remaining active after employees leave or change roles. Manual processes are error-prone and may lead to unauthorized access persisting longer than intended.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/accounts/team-members-and-roles",

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelTeam",
"ResourceType": "NotDefined",
"ResourceGroup": "governance",
"Description": "Checks whether any active team members have a join date older than 90 days. Long-standing access without periodic review may indicate stale permissions that should be audited to ensure continued need and appropriate role assignment.",
"Risk": "Team members who have had access for extended periods without review may have accumulated unnecessary permissions or may no longer require access. Without periodic access reviews, former contractors, role-changed employees, or inactive members may retain access to production resources.",
"Description": "**Vercel team members** are assessed for **stale access** by checking whether any active members have a join date older than 90 days. Long-standing access without periodic review may indicate stale permissions that should be audited to ensure continued need and appropriate role assignment.",
"Risk": "Team members who have had access for **extended periods** without review may have accumulated unnecessary permissions or may no longer require access. Without **periodic access reviews**, former contractors, role-changed employees, or inactive members may retain access to production resources.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/accounts/team-members-and-roles"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "VercelTeam",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Checks whether the proportion of team members with the OWNER role does not exceed 20% of total active members. An excessive number of owners increases the attack surface and risk of accidental or malicious configuration changes.",
"Risk": "Having too many team owners increases the blast radius of compromised accounts and the risk of unauthorized changes to billing, security settings, and team membership. Each owner has full administrative privileges over the team.",
"Description": "**Vercel team members** are assessed for **least privilege** by checking whether the proportion of members with the `OWNER` role exceeds 20% of total active members. An excessive number of owners increases the attack surface and risk of accidental or malicious configuration changes.",
"Risk": "Having too many **team owners** increases the **blast radius** of compromised accounts and the risk of unauthorized changes to billing, security settings, and team membership. Each owner has full administrative privileges over the team.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/accounts/team-members-and-roles"

View File

@@ -45,7 +45,14 @@ class team_member_role_least_privilege(Check):
owner_count = len(owners)
owner_percentage = (owner_count / total_active) * 100
if owner_percentage <= 20:
if total_active < 5 and owner_count <= 1:
report.status = "PASS"
report.status_extended = (
f"Team {team.name} has {owner_count} owner(s) out of "
f"{total_active} active members. Small team with minimum "
f"required owner — least privilege threshold not applicable."
)
elif owner_percentage <= 20:
report.status = "PASS"
report.status_extended = (
f"Team {team.name} has {owner_count} owner(s) out of "

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "VercelTeam",
"ResourceType": "NotDefined",
"ResourceGroup": "governance",
"Description": "Checks whether the Vercel team has pending invitations that have been outstanding for more than 30 days. Stale invitations may indicate abandoned onboarding processes or forgotten invitation links that could be exploited.",
"Risk": "Stale pending invitations represent unresolved access grants. If invitation links are intercepted or forwarded to unintended recipients, they could be used to gain unauthorized access to the team. Old invitations also indicate poor access lifecycle management.",
"Description": "**Vercel team** is assessed for **stale invitations** by checking whether pending invitations have been outstanding for more than 30 days. Stale invitations may indicate abandoned onboarding processes or forgotten invitation links that could be exploited.",
"Risk": "**Stale pending invitations** represent unresolved access grants. If invitation links are intercepted or forwarded to unintended recipients, they could be used to gain **unauthorized access** to the team. Old invitations also indicate poor access lifecycle management.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/accounts/team-members-and-roles"

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "VercelTeam",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Checks whether the Vercel team has SAML single sign-on (SSO) enabled. SAML SSO enables centralized identity management through an external identity provider, ensuring consistent authentication policies across the organization.",
"Risk": "Without SAML SSO, team members authenticate using individual Vercel credentials that are not centrally managed. This increases the risk of credential sprawl, inconsistent password policies, and inability to enforce organization-wide authentication controls such as MFA.",
"Description": "**Vercel team** is assessed for **SAML single sign-on (SSO)** enablement. SAML SSO enables centralized identity management through an external identity provider, ensuring consistent authentication policies across the organization.",
"Risk": "Without **SAML SSO**, team members authenticate using individual Vercel credentials that are not centrally managed. This increases the risk of **credential sprawl**, inconsistent password policies, and inability to enforce organization-wide authentication controls such as **MFA**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/accounts/team-members-and-roles",

View File

@@ -7,10 +7,10 @@
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "VercelTeam",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Checks whether the Vercel team enforces SAML SSO for all members. When enforced, all team members must authenticate through the configured identity provider, preventing the use of individual Vercel credentials.",
"Risk": "Without SAML SSO enforcement, team members can bypass centralized authentication and log in with individual credentials even when SAML is configured. This undermines identity governance, allows circumvention of MFA policies, and creates gaps in access auditing.",
"Description": "**Vercel team** is assessed for **SAML SSO enforcement** across all members. When enforced, all team members must authenticate through the configured identity provider, preventing the use of individual Vercel credentials.",
"Risk": "Without **SAML SSO enforcement**, team members can bypass centralized authentication and log in with individual credentials even when SAML is configured. This undermines **identity governance**, allows circumvention of MFA policies, and creates gaps in access auditing.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://vercel.com/docs/accounts/team-members-and-roles",

View File

@@ -16,15 +16,20 @@ class Team(VercelService):
self._fetch_team()
def _fetch_team(self):
"""Fetch team details and members if team_id is set."""
team_id = self.provider.session.team_id
if not team_id:
logger.info("Team - No team ID configured, skipping team checks")
"""Fetch team details and members for all teams in scope."""
team_ids = self._all_team_ids
if not team_ids:
logger.info("Team - No teams found, skipping team checks")
return
for team_id in team_ids:
self._fetch_single_team(team_id)
def _fetch_single_team(self, team_id: str):
"""Fetch details and members for a single team."""
try:
# Fetch team details
team_data = self._get(f"/v2/teams/{team_id}")
# Fetch team details (pass teamId explicitly for auto-discovered teams)
team_data = self._get(f"/v2/teams/{team_id}", params={"teamId": team_id})
if not team_data:
return
@@ -77,14 +82,18 @@ class Team(VercelService):
except Exception as error:
logger.error(
f"Team - Error fetching team: "
f"Team - Error fetching team {team_id}: "
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _fetch_members(self, team: "VercelTeam"):
"""Fetch all members for a team."""
try:
raw_members = self._paginate(f"/v2/teams/{team.id}/members", "members")
raw_members = self._paginate(
f"/v2/teams/{team.id}/members",
"members",
params={"teamId": team.id},
)
for member in raw_members:
joined_at = None

View File

@@ -196,9 +196,12 @@ class VercelProvider(Provider):
username = user_data.get("username")
email = user_data.get("email")
# Get team info if team_id is set
# Get team info
team_info = None
all_teams = []
if session.team_id:
# Specific team requested — fetch just that one
params = {"teamId": session.team_id}
team_response = http.get(
f"{session.base_url}/v2/teams/{session.team_id}",
@@ -212,6 +215,7 @@ class VercelProvider(Provider):
name=team_data.get("name", ""),
slug=team_data.get("slug", ""),
)
all_teams = [team_info]
elif team_response.status_code in (404, 403):
raise VercelInvalidTeamError(
file=os.path.basename(__file__),
@@ -219,12 +223,38 @@ class VercelProvider(Provider):
)
else:
team_response.raise_for_status()
else:
# No team specified — auto-discover all teams the user belongs to
try:
teams_response = http.get(
f"{session.base_url}/v2/teams",
params={"limit": 100},
timeout=30,
)
if teams_response.status_code == 200:
teams_data = teams_response.json().get("teams", [])
for t in teams_data:
all_teams.append(
VercelTeamInfo(
id=t.get("id", ""),
name=t.get("name", ""),
slug=t.get("slug", ""),
)
)
if all_teams:
logger.info(
f"Auto-discovered {len(all_teams)} team(s): "
f"{', '.join(t.name for t in all_teams)}"
)
except Exception as teams_error:
logger.warning(f"Could not auto-discover teams: {teams_error}")
return VercelIdentityInfo(
user_id=user_id,
username=username,
email=email,
team=team_info,
teams=all_teams,
)
except VercelInvalidTeamError:
raise
@@ -305,6 +335,11 @@ class VercelProvider(Provider):
report_lines.append(
f"Team: {Fore.YELLOW}{self.identity.team.name} ({self.identity.team.slug}){Style.RESET_ALL}"
)
elif self.identity.teams:
team_names = ", ".join(f"{t.name} ({t.slug})" for t in self.identity.teams)
report_lines.append(
f"Scope: {Fore.YELLOW}Personal Account + {len(self.identity.teams)} team(s): {team_names}{Style.RESET_ALL}"
)
else:
report_lines.append(
f"Scope: {Fore.YELLOW}Personal Account{Style.RESET_ALL}"

View File

@@ -42,7 +42,7 @@ class Test_deployment_preview_not_publicly_accessible:
id=DEPLOYMENT_ID,
name="my-app-abc123",
target="preview",
deployment_protection="standard",
deployment_protection={"level": "standard"},
project_id=PROJECT_ID,
project_name=PROJECT_NAME,
team_id=TEAM_ID,
@@ -122,7 +122,7 @@ class Test_deployment_preview_not_publicly_accessible:
id=DEPLOYMENT_ID,
name="my-app-abc123",
target="production",
deployment_protection="standard",
deployment_protection={"level": "standard"},
project_id=PROJECT_ID,
project_name=PROJECT_NAME,
team_id=TEAM_ID,

View File

@@ -65,7 +65,7 @@ class Test_domain_ssl_certificate_valid:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Domain {DOMAIN_NAME} has a valid SSL certificate provisioned."
== f"Domain {DOMAIN_NAME} has an SSL certificate provisioned."
)
assert result[0].team_id == TEAM_ID

View File

@@ -42,6 +42,7 @@ class Test_security_waf_enabled:
project_name=PROJECT_NAME,
team_id=TEAM_ID,
firewall_enabled=True,
managed_rulesets={},
id=PROJECT_ID,
name=PROJECT_NAME,
)
@@ -81,6 +82,7 @@ class Test_security_waf_enabled:
project_name=PROJECT_NAME,
team_id=TEAM_ID,
firewall_enabled=False,
managed_rulesets={},
id=PROJECT_ID,
name=PROJECT_NAME,
)

View File

@@ -77,11 +77,12 @@ class Test_team_member_role_least_privilege:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Team {TEAM_NAME} has 0 owner(s) out of 1 active members (0%), which is within the recommended 20% threshold."
== f"Team {TEAM_NAME} has 0 owner(s) out of 1 active members. Small team with minimum required owner — least privilege threshold not applicable."
)
assert result[0].team_id == ""
def test_member_owner_role(self):
def test_small_team_single_owner(self):
"""Small team (<5 members) with 1 owner gets a PASS (small team exception)."""
team_client = mock.MagicMock
team_client.teams = {
TEAM_ID: VercelTeam(
@@ -100,6 +101,53 @@ class Test_team_member_role_least_privilege:
)
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_vercel_provider(),
),
mock.patch(
"prowler.providers.vercel.services.team.team_member_role_least_privilege.team_member_role_least_privilege.team_client",
new=team_client,
),
):
from prowler.providers.vercel.services.team.team_member_role_least_privilege.team_member_role_least_privilege import (
team_member_role_least_privilege,
)
check = team_member_role_least_privilege()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == TEAM_ID
assert result[0].resource_name == TEAM_NAME
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Team {TEAM_NAME} has 1 owner(s) out of 1 active members. Small team with minimum required owner — least privilege threshold not applicable."
)
assert result[0].team_id == ""
def test_large_team_too_many_owners(self):
"""Large team (>=5 members) with >20% owners gets a FAIL."""
team_client = mock.MagicMock
team_client.teams = {
TEAM_ID: VercelTeam(
id=TEAM_ID,
name=TEAM_NAME,
slug=TEAM_SLUG,
saml=SAMLConfig(status="disabled", enforced=False),
members=[
VercelTeamMember(
id=f"member_{i}",
email=f"member{i}@example.com",
role="OWNER" if i <= 2 else "MEMBER",
status="active",
)
for i in range(1, 6)
],
)
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -122,6 +170,6 @@ class Test_team_member_role_least_privilege:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Team {TEAM_NAME} has 1 owner(s) out of 1 active members (100%), which exceeds the recommended 20% threshold."
== f"Team {TEAM_NAME} has 2 owner(s) out of 5 active members (40%), which exceeds the recommended 20% threshold."
)
assert result[0].team_id == ""