Compare commits

..

5 Commits

Author SHA1 Message Date
Daniel Barranquero
f124038390 feat: add changelog 2026-04-14 16:57:44 +02:00
Daniel Barranquero
af3284afff Merge branch 'master' into PROWLER-1366-scan-checks-depending-on-the-plan-type-for-vercel 2026-04-14 16:56:19 +02:00
Daniel Barranquero
ff7e8133e9 chore: add billing helper 2026-04-14 10:29:20 +02:00
Daniel Barranquero
04a6dca608 chore: modify some pro checks 2026-04-13 17:14:20 +02:00
Daniel Barranquero
18160e615f chore(vercel): add disclaimer for checks depending on billing plan 2026-04-13 13:06:48 +02:00
32 changed files with 400 additions and 34 deletions

27
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -1876,6 +1876,7 @@ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "contextlib2"
@@ -3044,7 +3045,7 @@ files = [
[package.dependencies]
attrs = ">=22.2.0"
jsonschema-specifications = ">=2023.03.6"
jsonschema-specifications = ">=2023.3.6"
referencing = ">=0.28.4"
rpds-py = ">=0.7.1"
@@ -3124,7 +3125,7 @@ files = [
]
[package.dependencies]
certifi = ">=14.05.14"
certifi = ">=14.5.14"
durationpy = ">=0.7"
google-auth = ">=1.0.1"
oauthlib = ">=3.2.2"
@@ -4934,7 +4935,7 @@ files = [
]
[package.dependencies]
astroid = ">=3.3.8,<=3.4.0-dev0"
astroid = ">=3.3.8,<=3.4.0.dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
@@ -5390,25 +5391,25 @@ files = [
[[package]]
name = "requests"
version = "2.33.1"
version = "2.32.4"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.10"
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"},
{file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"},
{file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"},
{file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"},
]
[package.dependencies]
certifi = ">=2023.5.7"
certifi = ">=2017.4.17"
charset_normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.26,<3"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "requests-file"
@@ -5780,10 +5781,10 @@ files = [
]
[package.dependencies]
botocore = ">=1.37.4,<2.0a.0"
botocore = ">=1.37.4,<2.0a0"
[package.extras]
crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
crt = ["botocore[crt] (>=1.37.4,<2.0a0)"]
[[package]]
name = "safety"

View File

@@ -22,6 +22,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Bump Poetry to `2.3.4` and consolidate SDK workflows onto the `setup-python-poetry` composite action with opt-in lockfile regeneration [(#10681)](https://github.com/prowler-cloud/prowler/pull/10681)
- Normalize Conditional Access platform values in Entra models and simplify platform-based checks [(#10635)](https://github.com/prowler-cloud/prowler/pull/10635)
- Update Vercel checks to return personalized finding status extended depending on billing plan [(#10663)](https://github.com/prowler-cloud/prowler/pull/10663)
---

View File

@@ -0,0 +1,59 @@
from typing import Optional
from prowler.providers.common.provider import Provider
def extract_billing_plan(data: Optional[dict]) -> Optional[str]:
"""Return the Vercel billing plan from a user or team payload.
Vercel's REST API consistently returns the plan identifier at
``data["billing"]["plan"]`` (e.g. ``"hobby"``, ``"pro"``, ``"enterprise"``)
on both ``GET /v2/user`` and ``GET /v2/teams`` responses, even though the
field is not part of the public OpenAPI schema.
"""
if not isinstance(data, dict):
return None
billing = data.get("billing")
if not isinstance(billing, dict):
return None
plan = billing.get("plan")
return plan.lower() if isinstance(plan, str) else None
def resolve_scope_billing_plan(scope_id: Optional[str] = None) -> Optional[str]:
"""Resolve the billing plan for a Vercel scope ID, if it is available."""
provider = Provider.get_global_provider()
identity = getattr(provider, "identity", None)
if not identity:
return None
if scope_id:
if (
identity.team
and identity.team.id == scope_id
and identity.team.billing_plan
):
return identity.team.billing_plan
for team in identity.teams:
if team.id == scope_id and team.billing_plan:
return team.billing_plan
if identity.user_id == scope_id:
return identity.billing_plan
return None
if identity.team and identity.team.billing_plan:
return identity.team.billing_plan
return identity.billing_plan
def plan_reason_suffix(
billing_plan: Optional[str], unsupported_plans: set[str], explanation: str
) -> str:
"""Return a plan-based explanation suffix only when the plan proves it."""
if billing_plan in unsupported_plans:
return f" This may be expected because {explanation}"
return ""

View File

@@ -84,10 +84,10 @@ class VercelService:
)
if response.status_code == 403:
# Plan limitation or permission error — return None for graceful handling
# Endpoint unavailable for this token/scope; let checks handle it gracefully
logger.warning(
f"{self.service} - Access denied for {path} (403). "
"This may be a plan limitation."
"This may be caused by plan or permission restrictions."
)
return None

View File

@@ -21,6 +21,7 @@ class VercelTeamInfo(BaseModel):
id: str
name: str
slug: str
billing_plan: Optional[str] = None
class VercelIdentityInfo(BaseModel):
@@ -29,6 +30,7 @@ class VercelIdentityInfo(BaseModel):
user_id: Optional[str] = None
username: Optional[str] = None
email: Optional[str] = None
billing_plan: Optional[str] = None
team: Optional[VercelTeamInfo] = None
teams: list[VercelTeamInfo] = Field(default_factory=list)

View File

@@ -34,5 +34,5 @@
"RelatedTo": [
"project_deployment_protection_enabled"
],
"Notes": ""
"Notes": "Required billing plan: Enterprise, or as a paid add-on for Pro plans."
}

View File

@@ -1,6 +1,10 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportVercel
from prowler.providers.vercel.lib.billing import (
plan_reason_suffix,
resolve_scope_billing_plan,
)
from prowler.providers.vercel.services.project.project_client import project_client
@@ -35,9 +39,11 @@ class project_password_protection_enabled(Check):
)
else:
report.status = "FAIL"
billing_plan = resolve_scope_billing_plan(project.team_id)
report.status_extended = (
f"Project {project.name} does not have password protection "
f"configured for deployments."
f"{plan_reason_suffix(billing_plan, {'hobby'}, 'password protection is not available on the Vercel Hobby plan.')}"
)
findings.append(report)

View File

@@ -34,5 +34,5 @@
"RelatedTo": [
"project_deployment_protection_enabled"
],
"Notes": ""
"Notes": "Protecting production deployments requires Enterprise, or Pro plans with supported paid deployment protection options."
}

View File

@@ -1,6 +1,10 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportVercel
from prowler.providers.vercel.lib.billing import (
plan_reason_suffix,
resolve_scope_billing_plan,
)
from prowler.providers.vercel.services.project.project_client import project_client
@@ -35,9 +39,11 @@ class project_production_deployment_protection_enabled(Check):
)
else:
report.status = "FAIL"
billing_plan = resolve_scope_billing_plan(project.team_id)
report.status_extended = (
f"Project {project.name} does not have deployment protection "
f"enabled on production deployments."
f"{plan_reason_suffix(billing_plan, {'hobby'}, 'protecting production deployments is not available on the Vercel Hobby plan.')}"
)
findings.append(report)

View File

@@ -32,5 +32,5 @@
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
"Notes": "Required billing plan: Pro or Enterprise."
}

View File

@@ -1,6 +1,10 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportVercel
from prowler.providers.vercel.lib.billing import (
plan_reason_suffix,
resolve_scope_billing_plan,
)
from prowler.providers.vercel.services.project.project_client import project_client
@@ -31,9 +35,11 @@ class project_skew_protection_enabled(Check):
)
else:
report.status = "FAIL"
billing_plan = resolve_scope_billing_plan(project.team_id)
report.status_extended = (
f"Project {project.name} does not have skew protection enabled, "
f"which may cause version mismatches during deployments."
f"{plan_reason_suffix(billing_plan, {'hobby'}, 'skew protection is not available on the Vercel Hobby plan.')}"
)
findings.append(report)

View File

@@ -9,7 +9,7 @@
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"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.",
"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. Availability varies by ruleset, and the check reports MANUAL when the firewall configuration cannot be assessed from the API.",
"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": [
@@ -19,11 +19,11 @@
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Vercel dashboard\n2. Navigate to the project Settings > Security > Firewall\n3. Enable managed rulesets from the available options\n4. Review and configure ruleset sensitivity levels\n5. Note: This feature requires an Enterprise plan",
"Other": "1. Sign in to the Vercel dashboard\n2. Navigate to the project Settings > Security > Firewall\n3. Enable the managed rulesets that are available for your plan\n4. Review and configure ruleset sensitivity levels\n5. If the API does not expose firewall configuration for the project, verify the rulesets manually in the dashboard",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable managed WAF rulesets to benefit from Vercel-curated protection against common attack patterns. If you are on a plan that does not support managed rulesets, consider upgrading to the Enterprise plan for enhanced security features.",
"Text": "Enable the managed WAF rulesets that are available for your Vercel plan to benefit from curated protection against common attack patterns. If the API does not expose firewall configuration for the project, verify the rulesets manually in the dashboard.",
"Url": "https://hub.prowler.com/check/security_managed_rulesets_enabled"
}
},
@@ -34,5 +34,5 @@
"RelatedTo": [
"security_waf_enabled"
],
"Notes": "This check is plan-gated. If the Vercel API returns a 403 for managed rulesets, the check reports MANUAL status indicating that an Enterprise plan is required."
"Notes": "Managed ruleset availability varies by ruleset. OWASP Core Ruleset requires Enterprise, while Bot Protection and AI Bots managed rulesets are available on all plans."
}

View File

@@ -17,8 +17,8 @@ class security_managed_rulesets_enabled(Check):
"""Execute the Vercel Managed Rulesets Enabled check.
Iterates over all firewall configurations and checks if managed
rulesets are enabled. Reports MANUAL status when the feature is
not available due to plan limitations.
rulesets are enabled. Reports MANUAL status when the firewall
configuration cannot be assessed from the API.
Returns:
List[CheckReportVercel]: A list of reports for each project.
@@ -31,8 +31,9 @@ class security_managed_rulesets_enabled(Check):
report.status = "MANUAL"
report.status_extended = (
f"Project {config.project_name} ({config.project_id}) "
f"could not be assessed for managed rulesets. "
f"Enterprise plan required to access this feature."
f"could not be assessed for managed rulesets because the "
f"firewall configuration endpoint was not accessible. "
f"Manual verification is required."
)
elif config.managed_rulesets:
report.status = "PASS"

View File

@@ -32,7 +32,7 @@ class Security(VercelService):
)
if data is None:
# 403 — plan limitation, store with managed_rulesets=None
# Firewall config endpoint unavailable for this project/token
self.firewall_configs[project.id] = VercelFirewallConfig(
project_id=project.id,
project_name=project.name,
@@ -118,7 +118,7 @@ class VercelFirewallConfig(BaseModel):
project_name: Optional[str] = None
team_id: Optional[str] = None
firewall_enabled: bool = False
managed_rulesets: Optional[dict] = None # None means plan-gated (403)
managed_rulesets: Optional[dict] = None # None means config endpoint unavailable
custom_rules: list[dict] = Field(default_factory=list)
ip_blocking_rules: list[dict] = Field(default_factory=list)
rate_limiting_rules: list[dict] = Field(default_factory=list)

View File

@@ -25,11 +25,12 @@ class security_waf_enabled(Check):
report = CheckReportVercel(metadata=self.metadata(), resource=config)
if config.managed_rulesets is None:
# 403 — plan limitation, cannot determine WAF status
# Firewall config could not be retrieved for this project
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"could not be checked for WAF status because the firewall "
f"configuration endpoint was not accessible. "
f"Manual verification is required."
)
elif config.firewall_enabled:

View File

@@ -35,5 +35,5 @@
"RelatedTo": [
"team_saml_sso_enabled"
],
"Notes": ""
"Notes": "Required billing plan: Enterprise."
}

View File

@@ -1,6 +1,7 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportVercel
from prowler.providers.vercel.lib.billing import plan_reason_suffix
from prowler.providers.vercel.services.team.team_client import team_client
@@ -40,6 +41,7 @@ class team_directory_sync_enabled(Check):
report.status_extended = (
f"Team {team.name} does not have directory sync (SCIM) enabled. "
f"User provisioning and deprovisioning must be managed manually."
f"{plan_reason_suffix(team.billing_plan, {'hobby', 'pro'}, 'directory sync (SCIM) is only available on Vercel Enterprise plans.')}"
)
findings.append(report)

View File

@@ -35,5 +35,5 @@
"RelatedTo": [
"team_saml_sso_enforced"
],
"Notes": ""
"Notes": "Required billing plan: Pro or Enterprise."
}

View File

@@ -1,6 +1,7 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportVercel
from prowler.providers.vercel.lib.billing import plan_reason_suffix
from prowler.providers.vercel.services.team.team_client import team_client
@@ -38,6 +39,7 @@ class team_saml_sso_enabled(Check):
report.status = "FAIL"
report.status_extended = (
f"Team {team.name} does not have SAML SSO enabled."
f"{plan_reason_suffix(team.billing_plan, {'hobby'}, 'SAML SSO is not available on the Vercel Hobby plan.')}"
)
findings.append(report)

View File

@@ -35,5 +35,5 @@
"RelatedTo": [
"team_saml_sso_enabled"
],
"Notes": ""
"Notes": "Required billing plan: Pro or Enterprise."
}

View File

@@ -1,6 +1,7 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportVercel
from prowler.providers.vercel.lib.billing import plan_reason_suffix
from prowler.providers.vercel.services.team.team_client import team_client
@@ -43,6 +44,7 @@ class team_saml_sso_enforced(Check):
else:
report.status_extended = (
f"Team {team.name} does not have SAML SSO enforced."
f"{plan_reason_suffix(team.billing_plan, {'hobby'}, 'SAML SSO is not available on the Vercel Hobby plan.')}"
)
findings.append(report)

View File

@@ -4,6 +4,7 @@ from typing import Optional
from pydantic import BaseModel, Field
from prowler.lib.logger import logger
from prowler.providers.vercel.lib.billing import extract_billing_plan
from prowler.providers.vercel.lib.service.service import VercelService
@@ -67,6 +68,7 @@ class Team(VercelService):
id=team_data.get("id", team_id),
name=team_data.get("name", ""),
slug=team_data.get("slug", ""),
billing_plan=extract_billing_plan(team_data),
saml=saml_config,
directory_sync_enabled=dir_sync,
created_at=created_at,
@@ -151,6 +153,7 @@ class VercelTeam(BaseModel):
id: str
name: str
slug: str
billing_plan: Optional[str] = None
saml: Optional[SAMLConfig] = None
directory_sync_enabled: bool = False
members: list[VercelTeamMember] = Field(default_factory=list)

View File

@@ -20,6 +20,7 @@ from prowler.providers.vercel.exceptions.exceptions import (
VercelRateLimitError,
VercelSessionError,
)
from prowler.providers.vercel.lib.billing import extract_billing_plan
from prowler.providers.vercel.lib.mutelist.mutelist import VercelMutelist
from prowler.providers.vercel.models import (
VercelIdentityInfo,
@@ -195,6 +196,7 @@ class VercelProvider(Provider):
user_id = user_data.get("id")
username = user_data.get("username")
email = user_data.get("email")
billing_plan = extract_billing_plan(user_data)
# Get team info
team_info = None
@@ -214,6 +216,7 @@ class VercelProvider(Provider):
id=team_data.get("id", session.team_id),
name=team_data.get("name", ""),
slug=team_data.get("slug", ""),
billing_plan=extract_billing_plan(team_data),
)
all_teams = [team_info]
elif team_response.status_code in (404, 403):
@@ -239,6 +242,7 @@ class VercelProvider(Provider):
id=t.get("id", ""),
name=t.get("name", ""),
slug=t.get("slug", ""),
billing_plan=extract_billing_plan(t),
)
)
if all_teams:
@@ -253,6 +257,7 @@ class VercelProvider(Provider):
user_id=user_id,
username=username,
email=email,
billing_plan=billing_plan,
team=team_info,
teams=all_teams,
)

View File

@@ -142,3 +142,40 @@ class Test_project_password_protection_enabled:
== f"Project {PROJECT_NAME} does not have password protection configured for deployments."
)
assert result[0].team_id == TEAM_ID
def test_no_password_protection_hobby_plan(self):
project_client = mock.MagicMock
project_client.projects = {
PROJECT_ID: VercelProject(
id=PROJECT_ID,
name=PROJECT_NAME,
team_id=TEAM_ID,
password_protection=None,
)
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_vercel_provider(billing_plan="hobby"),
),
mock.patch(
"prowler.providers.vercel.services.project.project_password_protection_enabled.project_password_protection_enabled.project_client",
new=project_client,
),
):
from prowler.providers.vercel.services.project.project_password_protection_enabled.project_password_protection_enabled import (
project_password_protection_enabled,
)
check = project_password_protection_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == PROJECT_ID
assert result[0].resource_name == PROJECT_NAME
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Project {PROJECT_NAME} does not have password protection configured for deployments. This may be expected because password protection is not available on the Vercel Hobby plan."
)
assert result[0].team_id == TEAM_ID

View File

@@ -149,3 +149,40 @@ class Test_project_production_deployment_protection_enabled:
== f"Project {PROJECT_NAME} does not have deployment protection enabled on production deployments."
)
assert result[0].team_id == TEAM_ID
def test_protection_null_hobby_plan(self):
project_client = mock.MagicMock
project_client.projects = {
PROJECT_ID: VercelProject(
id=PROJECT_ID,
name=PROJECT_NAME,
team_id=TEAM_ID,
production_deployment_protection=None,
)
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_vercel_provider(billing_plan="hobby"),
),
mock.patch(
"prowler.providers.vercel.services.project.project_production_deployment_protection_enabled.project_production_deployment_protection_enabled.project_client",
new=project_client,
),
):
from prowler.providers.vercel.services.project.project_production_deployment_protection_enabled.project_production_deployment_protection_enabled import (
project_production_deployment_protection_enabled,
)
check = project_production_deployment_protection_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == PROJECT_ID
assert result[0].resource_name == PROJECT_NAME
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Project {PROJECT_NAME} does not have deployment protection enabled on production deployments. This may be expected because protecting production deployments is not available on the Vercel Hobby plan."
)
assert result[0].team_id == TEAM_ID

View File

@@ -105,3 +105,40 @@ class Test_project_skew_protection_enabled:
== f"Project {PROJECT_NAME} does not have skew protection enabled, which may cause version mismatches during deployments."
)
assert result[0].team_id == TEAM_ID
def test_skew_protection_disabled_hobby_plan(self):
project_client = mock.MagicMock
project_client.projects = {
PROJECT_ID: VercelProject(
id=PROJECT_ID,
name=PROJECT_NAME,
team_id=TEAM_ID,
skew_protection=False,
)
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_vercel_provider(billing_plan="hobby"),
),
mock.patch(
"prowler.providers.vercel.services.project.project_skew_protection_enabled.project_skew_protection_enabled.project_client",
new=project_client,
),
):
from prowler.providers.vercel.services.project.project_skew_protection_enabled.project_skew_protection_enabled import (
project_skew_protection_enabled,
)
check = project_skew_protection_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == PROJECT_ID
assert result[0].resource_name == PROJECT_NAME
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Project {PROJECT_NAME} does not have skew protection enabled, which may cause version mismatches during deployments. This may be expected because skew protection is not available on the Vercel Hobby plan."
)
assert result[0].team_id == TEAM_ID

View File

@@ -150,6 +150,6 @@ class Test_security_managed_rulesets_enabled:
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"Project {PROJECT_NAME} ({PROJECT_ID}) could not be assessed for managed rulesets. Enterprise plan required to access this feature."
== f"Project {PROJECT_NAME} ({PROJECT_ID}) could not be assessed for managed rulesets because the firewall configuration endpoint was not accessible. Manual verification is required."
)
assert result[0].team_id == TEAM_ID

View File

@@ -113,3 +113,43 @@ class Test_security_waf_enabled:
== f"Project {PROJECT_NAME} ({PROJECT_ID}) does not have the Web Application Firewall enabled."
)
assert result[0].team_id == TEAM_ID
def test_waf_status_unavailable(self):
security_client = mock.MagicMock
security_client.firewall_configs = {
PROJECT_ID: VercelFirewallConfig(
project_id=PROJECT_ID,
project_name=PROJECT_NAME,
team_id=TEAM_ID,
firewall_enabled=False,
managed_rulesets=None,
id=PROJECT_ID,
name=PROJECT_NAME,
)
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_vercel_provider(),
),
mock.patch(
"prowler.providers.vercel.services.security.security_waf_enabled.security_waf_enabled.security_client",
new=security_client,
),
):
from prowler.providers.vercel.services.security.security_waf_enabled.security_waf_enabled import (
security_waf_enabled,
)
check = security_waf_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == PROJECT_ID
assert result[0].resource_name == PROJECT_NAME
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"Project {PROJECT_NAME} ({PROJECT_ID}) could not be checked for WAF status because the firewall configuration endpoint was not accessible. Manual verification is required."
)
assert result[0].team_id == TEAM_ID

View File

@@ -105,3 +105,41 @@ class Test_team_directory_sync_enabled:
== f"Team {TEAM_NAME} does not have directory sync (SCIM) enabled. User provisioning and deprovisioning must be managed manually."
)
assert result[0].team_id == ""
def test_directory_sync_disabled_pro_plan(self):
team_client = mock.MagicMock
team_client.teams = {
TEAM_ID: VercelTeam(
id=TEAM_ID,
name=TEAM_NAME,
slug=TEAM_SLUG,
directory_sync_enabled=False,
billing_plan="pro",
)
}
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_directory_sync_enabled.team_directory_sync_enabled.team_client",
new=team_client,
),
):
from prowler.providers.vercel.services.team.team_directory_sync_enabled.team_directory_sync_enabled import (
team_directory_sync_enabled,
)
check = team_directory_sync_enabled()
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 == "FAIL"
assert (
result[0].status_extended
== f"Team {TEAM_NAME} does not have directory sync (SCIM) enabled. User provisioning and deprovisioning must be managed manually. This may be expected because directory sync (SCIM) is only available on Vercel Enterprise plans."
)
assert result[0].team_id == ""

View File

@@ -106,3 +106,42 @@ class Test_team_saml_sso_enabled:
== f"Team {TEAM_NAME} does not have SAML SSO enabled."
)
assert result[0].team_id == ""
def test_saml_disabled_hobby_plan(self):
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),
billing_plan="hobby",
members=[],
)
}
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_saml_sso_enabled.team_saml_sso_enabled.team_client",
new=team_client,
),
):
from prowler.providers.vercel.services.team.team_saml_sso_enabled.team_saml_sso_enabled import (
team_saml_sso_enabled,
)
check = team_saml_sso_enabled()
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 == "FAIL"
assert (
result[0].status_extended
== f"Team {TEAM_NAME} does not have SAML SSO enabled. This may be expected because SAML SSO is not available on the Vercel Hobby plan."
)
assert result[0].team_id == ""

View File

@@ -142,3 +142,41 @@ class Test_team_saml_sso_enforced:
== f"Team {TEAM_NAME} does not have SAML SSO enforced."
)
assert result[0].team_id == ""
def test_saml_disabled_hobby_plan(self):
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),
billing_plan="hobby",
)
}
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_saml_sso_enforced.team_saml_sso_enforced.team_client",
new=team_client,
),
):
from prowler.providers.vercel.services.team.team_saml_sso_enforced.team_saml_sso_enforced import (
team_saml_sso_enforced,
)
check = team_saml_sso_enforced()
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 == "FAIL"
assert (
result[0].status_extended
== f"Team {TEAM_NAME} does not have SAML SSO enforced. This may be expected because SAML SSO is not available on the Vercel Hobby plan."
)
assert result[0].team_id == ""

View File

@@ -33,6 +33,7 @@ def set_mocked_vercel_provider(
team_id: str = TEAM_ID,
identity: VercelIdentityInfo = None,
audit_config: dict = None,
billing_plan: str = None,
):
"""Create a mocked VercelProvider for testing."""
provider = MagicMock()
@@ -46,10 +47,12 @@ def set_mocked_vercel_provider(
user_id=USER_ID,
username=USERNAME,
email=USER_EMAIL,
billing_plan=billing_plan,
team=VercelTeamInfo(
id=TEAM_ID,
name=TEAM_NAME,
slug=TEAM_SLUG,
billing_plan=billing_plan,
),
)
provider.audit_config = audit_config or {"max_retries": 3}