Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ba54da2
chore: init feature branch for Vercel provider
alejandrobailo Feb 27, 2026
5c2b51d
feat(sdk): add Vercel provider with 30 security checks
alejandrobailo Feb 27, 2026
842dfc1
fix(vercel): remove __init__.py from test directories
alejandrobailo Feb 27, 2026
49841dd
refactor(sdk): rename environment checks to project_environment
alejandrobailo Mar 3, 2026
67fb058
fix(sdk): normalize check metadata format
alejandrobailo Mar 3, 2026
786d00d
feat(sdk): make stable branches configurable via audit_config
alejandrobailo Mar 3, 2026
f363e74
chore(sdk): remove Vercel compliance files
alejandrobailo Mar 3, 2026
1a1317c
Merge branch 'master' into feat/vercel-sdk
danibarranqueroo Mar 17, 2026
a97a8b6
chore: vercel provider revision
danibarranqueroo Mar 17, 2026
109ee80
chore: update metadata
danibarranqueroo Mar 17, 2026
a25c5d4
chore: add missing check tests
danibarranqueroo Mar 17, 2026
e583cfd
feat(vercel): add example mutelist
danibarranqueroo Mar 17, 2026
6cfa67d
chore: add vercel to outputs and to html
danibarranqueroo Mar 17, 2026
0186e9f
chore: remove cli authentication flags
danibarranqueroo Mar 17, 2026
29cc9ea
fix: remove init from tests files
danibarranqueroo Mar 17, 2026
fb62b81
fix: parser tests
danibarranqueroo Mar 18, 2026
cc7fa7d
chore: update asserts in every unit test
danibarranqueroo Mar 18, 2026
274cd07
chore: update services format
danibarranqueroo Mar 19, 2026
273c8e4
Merge branch 'master' into feat/vercel-sdk
danibarranqueroo Mar 19, 2026
f8beded
chore: fix black
danibarranqueroo Mar 19, 2026
f9ccc89
chore: update metadata
danibarranqueroo Mar 20, 2026
2d5e948
feat: scan all teams when no team is specified
danibarranqueroo Mar 20, 2026
ea5ba82
fix tests
danibarranqueroo Mar 20, 2026
db18e47
feat: add docs and modify gh workflows
danibarranqueroo Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions prowler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- CheckMetadata Pydantic validators [(#8584)](https://github.com/prowler-cloud/prowler/pull/8583)
- `entra_conditional_access_policy_require_mfa_for_admin_portals` check for Azure provider and update CIS compliance [(#10330)](https://github.com/prowler-cloud/prowler/pull/10330)
- `organization_repository_deletion_limited` check for GitHub provider [(#10185)](https://github.com/prowler-cloud/prowler/pull/10185)
- `Vercel` provider support with 30 checks [(#10189)](https://github.com/prowler-cloud/prowler/pull/10189)

### 🔄 Changed

Expand Down
5 changes: 5 additions & 0 deletions prowler/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
from prowler.providers.nhn.models import NHNOutputOptions
from prowler.providers.openstack.models import OpenStackOutputOptions
from prowler.providers.oraclecloud.models import OCIOutputOptions
from prowler.providers.vercel.models import VercelOutputOptions


def prowler():
Expand Down Expand Up @@ -385,6 +386,10 @@ def prowler():
output_options = OpenStackOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
elif provider == "vercel":
output_options = VercelOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)

# Run the quick inventory for the provider if available
if hasattr(args, "quick_inventory") and args.quick_inventory:
Expand Down
1 change: 1 addition & 0 deletions prowler/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class Provider(str, Enum):
ALIBABACLOUD = "alibabacloud"
OPENSTACK = "openstack"
IMAGE = "image"
VERCEL = "vercel"


# Providers that delegate scanning to an external tool (e.g. Trivy, promptfoo)
Expand Down
8 changes: 8 additions & 0 deletions prowler/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -609,3 +609,11 @@ cloudflare:
# Maximum number of retries for API requests (default is 2)
# Set to 0 to disable retries
max_retries: 3

# Vercel Configuration
vercel:
# vercel.deployment_production_uses_stable_target
# Branches considered stable for production deployments
stable_branches:
- "main"
- "master"
5 changes: 5 additions & 0 deletions prowler/lib/check/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,11 @@ def execute(
is_finding_muted_args["project_id"] = (
global_provider.identity.project_id
)
elif global_provider.type == "vercel":
team = getattr(global_provider.identity, "team", None)
is_finding_muted_args["team_id"] = (
team.id if team else global_provider.identity.user_id
)
for finding in check_findings:
if global_provider.type == "cloudflare":
is_finding_muted_args["account_id"] = finding.account_id
Expand Down
44 changes: 44 additions & 0 deletions prowler/lib/check/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,50 @@ def __init__(self, metadata: Dict, resource: Any) -> None:
self.location = getattr(resource, "location", self.project_id)


@dataclass
class CheckReportVercel(Check_Report):
"""Contains the Vercel Check's finding information.

Vercel is a global platform - team_id is the scoping context.
All resource-related attributes are derived from the resource object.
"""

resource_name: str
resource_id: str
team_id: str

def __init__(
self,
metadata: Dict,
resource: Any,
resource_name: str = None,
resource_id: str = None,
team_id: str = None,
) -> None:
"""Initialize the Vercel Check's finding information.

Args:
metadata: Check metadata dictionary
resource: The Vercel resource being checked
resource_name: Override for resource name
resource_id: Override for resource ID
team_id: Override for team ID
"""
super().__init__(metadata, resource)
self.resource_name = resource_name or getattr(
resource, "name", getattr(resource, "resource_name", "")
)
self.resource_id = resource_id or getattr(
resource, "id", getattr(resource, "resource_id", "")
)
self.team_id = team_id or getattr(resource, "team_id", "")

@property
def region(self) -> str:
"""Vercel is global - return 'global'."""
return "global"


# Testing Pending
def load_check_metadata(metadata_file: str) -> CheckMetadata:
"""
Expand Down
5 changes: 3 additions & 2 deletions prowler/lib/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ def __init__(self):
self.parser = argparse.ArgumentParser(
prog="prowler",
formatter_class=RawTextHelpFormatter,
usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,googleworkspace,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,dashboard,iac,image} ...",
usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,googleworkspace,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,vercel,dashboard,iac,image} ...",
epilog="""
Available Cloud Providers:
{aws,azure,gcp,kubernetes,m365,github,googleworkspace,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack}
{aws,azure,gcp,kubernetes,m365,github,googleworkspace,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,vercel}
aws AWS Provider
azure Azure Provider
gcp GCP Provider
Expand All @@ -47,6 +47,7 @@ def __init__(self):
image Container Image Provider
nhn NHN Provider (Unofficial)
mongodbatlas MongoDB Atlas Provider (Beta)
vercel Vercel Provider

Available components:
dashboard Local dashboard
Expand Down
13 changes: 13 additions & 0 deletions prowler/lib/outputs/finding.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,19 @@ def generate_output(
output_data["resource_uid"] = check_output.resource_id
output_data["region"] = check_output.zone_name

elif provider.type == "vercel":
output_data["auth_method"] = "api_token"
team = get_nested_attribute(provider, "identity.team")
output_data["account_uid"] = (
team.id if team else get_nested_attribute(provider, "identity.user_id")
)
output_data["account_name"] = (
team.name if team else get_nested_attribute(provider, "identity.username")
)
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_id
output_data["region"] = "global"

elif provider.type == "alibabacloud":
output_data["auth_method"] = get_nested_attribute(
provider, "identity.identity_arn"
Expand Down
2 changes: 2 additions & 0 deletions prowler/lib/outputs/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def stdout_report(finding, color, verbose, status, fix):
details = finding.zone_name
if finding.check_metadata.Provider == "googleworkspace":
details = finding.location
if finding.check_metadata.Provider == "vercel":
details = finding.resource_name

if (verbose or fix) and (not status or finding.status in status):
if finding.muted:
Expand Down
8 changes: 8 additions & 0 deletions prowler/lib/outputs/summary_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ def display_summary_table(
elif provider.type == "image":
entity_type = "Image"
audited_entities = ", ".join(provider.images)
elif provider.type == "vercel":
entity_type = "Team"
if provider.identity.team:
audited_entities = (
f"{provider.identity.team.name} ({provider.identity.team.slug})"
)
else:
audited_entities = provider.identity.username or "Personal Account"

# Check if there are findings and that they are not all MANUAL
if findings and not all(finding.status == "MANUAL" for finding in findings):
Expand Down
9 changes: 9 additions & 0 deletions prowler/providers/common/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,15 @@ def init_global_provider(arguments: Namespace) -> None:
mutelist_path=arguments.mutelist_file,
fixer_config=fixer_config,
)
elif "vercel" in provider_class_name.lower():
provider_class(
api_token=getattr(arguments, "vercel_token", None),
team_id=getattr(arguments, "vercel_team", None),
projects=getattr(arguments, "project", None),
config_path=arguments.config_file,
mutelist_path=arguments.mutelist_file,
fixer_config=fixer_config,
)

except TypeError as error:
logger.critical(
Expand Down
Empty file.
Empty file.
126 changes: 126 additions & 0 deletions prowler/providers/vercel/exceptions/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from prowler.exceptions.exceptions import ProwlerException


class VercelBaseException(ProwlerException):
"""Base exception for Vercel provider errors."""

VERCEL_ERROR_CODES = {
(13000, "VercelCredentialsError"): {
"message": "Vercel credentials not found or invalid.",
"remediation": "Set the VERCEL_TOKEN environment variable or pass --vercel-token with a valid Vercel API token. Generate one at https://vercel.com/account/tokens.",
},
(13001, "VercelAuthenticationError"): {
"message": "Authentication to Vercel API failed.",
"remediation": "Verify your Vercel API token is valid and has not expired. Check at https://vercel.com/account/tokens.",
},
(13002, "VercelSessionError"): {
"message": "Failed to create a Vercel API session.",
"remediation": "Check network connectivity and ensure the Vercel API is reachable at https://api.vercel.com.",
},
(13003, "VercelIdentityError"): {
"message": "Failed to retrieve Vercel identity information.",
"remediation": "Ensure the API token has permissions to read user and team information.",
},
(13004, "VercelInvalidTeamError"): {
"message": "The specified Vercel team was not found or is not accessible.",
"remediation": "Verify the team ID or slug is correct and that your token has access to the team.",
},
(13005, "VercelInvalidProviderIdError"): {
"message": "The provided Vercel provider ID is invalid.",
"remediation": "Ensure the provider UID matches a valid Vercel team ID or user ID format.",
},
(13006, "VercelAPIError"): {
"message": "An error occurred while calling the Vercel API.",
"remediation": "Check the Vercel API status at https://www.vercel-status.com/ and retry the request.",
},
(13007, "VercelRateLimitError"): {
"message": "Rate limited by the Vercel API.",
"remediation": "Wait and retry. Vercel API rate limits vary by endpoint. See https://vercel.com/docs/rest-api#rate-limits.",
},
(13008, "VercelPlanLimitationError"): {
"message": "This feature requires a higher Vercel plan.",
"remediation": "Some security features (e.g., WAF managed rulesets) require Vercel Enterprise. Upgrade your plan or skip these checks.",
},
}

def __init__(self, code, file=None, original_exception=None, message=None):
provider = "Vercel"
error_info = self.VERCEL_ERROR_CODES.get((code, self.__class__.__name__))
if error_info is None:
error_info = {
"message": message or "Unknown Vercel error.",
"remediation": "Check the Vercel API documentation for more details.",
}
elif message:
error_info = error_info.copy()
error_info["message"] = message
super().__init__(
code=code,
source=provider,
file=file,
original_exception=original_exception,
error_info=error_info,
)


class VercelCredentialsError(VercelBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
13000, file=file, original_exception=original_exception, message=message
)


class VercelAuthenticationError(VercelBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
13001, file=file, original_exception=original_exception, message=message
)


class VercelSessionError(VercelBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
13002, file=file, original_exception=original_exception, message=message
)


class VercelIdentityError(VercelBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
13003, file=file, original_exception=original_exception, message=message
)


class VercelInvalidTeamError(VercelBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
13004, file=file, original_exception=original_exception, message=message
)


class VercelInvalidProviderIdError(VercelBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
13005, file=file, original_exception=original_exception, message=message
)


class VercelAPIError(VercelBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
13006, file=file, original_exception=original_exception, message=message
)


class VercelRateLimitError(VercelBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
13007, file=file, original_exception=original_exception, message=message
)


class VercelPlanLimitationError(VercelBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
13008, file=file, original_exception=original_exception, message=message
)
Empty file.
Empty file.
35 changes: 35 additions & 0 deletions prowler/providers/vercel/lib/arguments/arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
def init_parser(self):
"""Init the Vercel provider CLI parser."""
vercel_parser = self.subparsers.add_parser(
"vercel",
parents=[self.common_providers_parser],
help="Vercel Provider",
)

# Authentication
auth_group = vercel_parser.add_argument_group("Authentication")
auth_group.add_argument(
"--vercel-token",
nargs="?",
default=None,
metavar="TOKEN",
help="Vercel API Bearer Token. Falls back to VERCEL_TOKEN environment variable.",
)

# Scope
scope_group = vercel_parser.add_argument_group("Scope")
scope_group.add_argument(
"--vercel-team",
nargs="?",
default=None,
metavar="TEAM_ID",
help="Vercel Team ID or slug to scope the scan. Falls back to VERCEL_TEAM environment variable.",
)
scope_group.add_argument(
"--project",
"--projects",
nargs="*",
default=None,
metavar="PROJECT",
help="Filter scan to specific Vercel project names or IDs.",
)
Empty file.
20 changes: 20 additions & 0 deletions prowler/providers/vercel/lib/mutelist/mutelist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from prowler.lib.check.models import CheckReportVercel
from prowler.lib.mutelist.mutelist import Mutelist
from prowler.lib.outputs.utils import unroll_dict, unroll_tags


class VercelMutelist(Mutelist):
"""Vercel-specific mutelist helper."""

def is_finding_muted(
self,
finding: CheckReportVercel,
team_id: str,
) -> bool:
return self.is_muted(
team_id,
finding.check_metadata.CheckID,
"global", # Vercel is a global service
finding.resource_id or finding.resource_name,
unroll_dict(unroll_tags(finding.resource_tags)),
)
Empty file.
Loading
Loading