mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
Compare commits
4 Commits
f8bededc9b
...
db18e47467
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db18e47467 | ||
|
|
ea5ba82333 | ||
|
|
2d5e948c96 | ||
|
|
f9ccc89177 |
7
.github/labeler.yml
vendored
7
.github/labeler.yml
vendored
@@ -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:
|
||||
|
||||
8
.github/test-impact.yml
vendored
8
.github/test-impact.yml
vendored
@@ -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
|
||||
# ============================================
|
||||
|
||||
24
.github/workflows/sdk-tests.yml
vendored
24
.github/workflows/sdk-tests.yml
vendored
@@ -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'
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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 |
|
||||
|
||||
137
docs/user-guide/providers/vercel/authentication.mdx
Normal file
137
docs/user-guide/providers/vercel/authentication.mdx
Normal 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".
|
||||
|
||||

|
||||
|
||||
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**.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
108
docs/user-guide/providers/vercel/getting-started-vercel.mdx
Normal file
108
docs/user-guide/providers/vercel/getting-started-vercel.mdx
Normal 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 |
BIN
docs/user-guide/providers/vercel/images/vercel-create-token.png
Normal file
BIN
docs/user-guide/providers/vercel/images/vercel-create-token.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
BIN
docs/user-guide/providers/vercel/images/vercel-team-id.png
Normal file
BIN
docs/user-guide/providers/vercel/images/vercel-team-id.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 310 KiB |
@@ -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.
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}) "
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 == ""
|
||||
|
||||
Reference in New Issue
Block a user