mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
chore(vercel): add disclaimer for checks depending on billing plan (#10663)
This commit is contained in:
@@ -377,6 +377,50 @@ class TestCheckMetadataValidators:
|
||||
check_metadata = CheckMetadata(**valid_metadata)
|
||||
assert check_metadata.Categories == ["encryption", "logging", "secrets"]
|
||||
|
||||
def test_valid_vercel_plan_categories_success(self):
|
||||
"""Test Vercel plan categories are accepted using hyphen-separated names."""
|
||||
valid_metadata = {
|
||||
"Provider": "vercel",
|
||||
"CheckID": "test_check",
|
||||
"CheckTitle": "Test Check",
|
||||
"CheckType": [],
|
||||
"ServiceName": "test",
|
||||
"SubServiceName": "subtest",
|
||||
"ResourceIdTemplate": "template",
|
||||
"Severity": "high",
|
||||
"ResourceType": "TestResource",
|
||||
"Description": "Test description",
|
||||
"Risk": "Test risk",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "test command",
|
||||
"NativeIaC": "test native",
|
||||
"Other": "test other",
|
||||
"Terraform": "test terraform",
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "test recommendation",
|
||||
"Url": "https://hub.prowler.com/check/test_check",
|
||||
},
|
||||
},
|
||||
"Categories": [
|
||||
"vercel-hobby-plan",
|
||||
"vercel-pro-plan",
|
||||
"vercel-enterprise-plan",
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "Test notes",
|
||||
}
|
||||
|
||||
check_metadata = CheckMetadata(**valid_metadata)
|
||||
assert check_metadata.Categories == [
|
||||
"vercel-hobby-plan",
|
||||
"vercel-pro-plan",
|
||||
"vercel-enterprise-plan",
|
||||
]
|
||||
|
||||
def test_valid_category_failure_non_string(self):
|
||||
"""Test valid category validation fails with non-string category"""
|
||||
invalid_metadata = {
|
||||
@@ -454,7 +498,7 @@ class TestCheckMetadataValidators:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
CheckMetadata(**invalid_metadata)
|
||||
assert (
|
||||
"Categories can only contain lowercase letters, numbers and hyphen"
|
||||
"Categories can only contain lowercase letters, numbers, and hyphen '-'"
|
||||
in str(exc_info.value)
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.vercel.lib.service.service import VercelService
|
||||
|
||||
|
||||
class TestVercelService:
|
||||
def test_get_returns_none_and_logs_info_on_expected_403(self):
|
||||
service = VercelService.__new__(VercelService)
|
||||
service.audit_config = {"max_retries": 0}
|
||||
service.service = "security"
|
||||
service._team_id = None
|
||||
service._base_url = "https://api.vercel.com"
|
||||
|
||||
response = mock.MagicMock()
|
||||
response.status_code = 403
|
||||
|
||||
service._http_session = mock.MagicMock()
|
||||
service._http_session.get.return_value = response
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.vercel.lib.service.service.logger"
|
||||
) as logger_mock:
|
||||
result = service._get("/v1/security/firewall/config/active")
|
||||
|
||||
assert result is None
|
||||
logger_mock.info.assert_called_once_with(
|
||||
"security - Access denied for /v1/security/firewall/config/active (403). "
|
||||
"This may be caused by plan or permission restrictions."
|
||||
)
|
||||
+38
@@ -142,3 +142,41 @@ 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,
|
||||
billing_plan="hobby",
|
||||
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
|
||||
|
||||
+38
@@ -149,3 +149,41 @@ 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,
|
||||
billing_plan="hobby",
|
||||
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
|
||||
|
||||
@@ -5,6 +5,7 @@ from tests.providers.vercel.vercel_fixtures import (
|
||||
PROJECT_ID,
|
||||
PROJECT_NAME,
|
||||
TEAM_ID,
|
||||
USER_ID,
|
||||
set_mocked_vercel_provider,
|
||||
)
|
||||
|
||||
@@ -43,3 +44,69 @@ class TestProjectService:
|
||||
"ai_bots": {"active": False, "action": "deny"},
|
||||
}
|
||||
assert project.bot_id_enabled is True
|
||||
|
||||
def test_list_projects_uses_scoped_team_billing_plan(self):
|
||||
service = Project.__new__(Project)
|
||||
service.provider = set_mocked_vercel_provider(
|
||||
billing_plan="enterprise",
|
||||
team_billing_plan="hobby",
|
||||
)
|
||||
service.projects = {}
|
||||
service._paginate = mock.MagicMock(
|
||||
return_value=[
|
||||
{
|
||||
"id": PROJECT_ID,
|
||||
"name": PROJECT_NAME,
|
||||
"accountId": TEAM_ID,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
service._list_projects()
|
||||
|
||||
project = service.projects[PROJECT_ID]
|
||||
assert project.billing_plan == "hobby"
|
||||
|
||||
def test_list_projects_uses_user_billing_plan_for_user_scoped_project(self):
|
||||
service = Project.__new__(Project)
|
||||
service.provider = set_mocked_vercel_provider(
|
||||
billing_plan="enterprise",
|
||||
team_billing_plan="hobby",
|
||||
)
|
||||
service.projects = {}
|
||||
service._paginate = mock.MagicMock(
|
||||
return_value=[
|
||||
{
|
||||
"id": PROJECT_ID,
|
||||
"name": PROJECT_NAME,
|
||||
"accountId": USER_ID,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
service._list_projects()
|
||||
|
||||
project = service.projects[PROJECT_ID]
|
||||
assert project.billing_plan == "enterprise"
|
||||
|
||||
def test_list_projects_does_not_guess_billing_plan_without_scope(self):
|
||||
service = Project.__new__(Project)
|
||||
service.provider = set_mocked_vercel_provider(
|
||||
billing_plan="enterprise",
|
||||
team_billing_plan="hobby",
|
||||
)
|
||||
service.provider.session.team_id = None
|
||||
service.projects = {}
|
||||
service._paginate = mock.MagicMock(
|
||||
return_value=[
|
||||
{
|
||||
"id": PROJECT_ID,
|
||||
"name": PROJECT_NAME,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
service._list_projects()
|
||||
|
||||
project = service.projects[PROJECT_ID]
|
||||
assert project.billing_plan is None
|
||||
|
||||
+38
@@ -105,3 +105,41 @@ 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,
|
||||
billing_plan="hobby",
|
||||
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
|
||||
|
||||
+38
@@ -111,3 +111,41 @@ class Test_security_custom_rules_configured:
|
||||
== f"Project {PROJECT_NAME} ({PROJECT_ID}) does not have any custom firewall rules configured."
|
||||
)
|
||||
assert result[0].team_id == TEAM_ID
|
||||
|
||||
def test_custom_rules_status_unavailable_hobby_plan(self):
|
||||
security_client = mock.MagicMock
|
||||
security_client.firewall_configs = {
|
||||
PROJECT_ID: VercelFirewallConfig(
|
||||
project_id=PROJECT_ID,
|
||||
project_name=PROJECT_NAME,
|
||||
team_id=TEAM_ID,
|
||||
billing_plan="hobby",
|
||||
firewall_config_accessible=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_custom_rules_configured.security_custom_rules_configured.security_client",
|
||||
new=security_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.vercel.services.security.security_custom_rules_configured.security_custom_rules_configured import (
|
||||
security_custom_rules_configured,
|
||||
)
|
||||
|
||||
check = security_custom_rules_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Project {PROJECT_NAME} ({PROJECT_ID}) could not be assessed for custom firewall rules because the firewall configuration endpoint was not accessible. Manual verification is required. This may be expected because custom firewall rules are not available on the Vercel Hobby plan."
|
||||
)
|
||||
|
||||
+38
@@ -111,3 +111,41 @@ class Test_security_ip_blocking_rules_configured:
|
||||
== f"Project {PROJECT_NAME} ({PROJECT_ID}) does not have any IP blocking rules configured."
|
||||
)
|
||||
assert result[0].team_id == TEAM_ID
|
||||
|
||||
def test_ip_rules_status_unavailable_hobby_plan(self):
|
||||
security_client = mock.MagicMock
|
||||
security_client.firewall_configs = {
|
||||
PROJECT_ID: VercelFirewallConfig(
|
||||
project_id=PROJECT_ID,
|
||||
project_name=PROJECT_NAME,
|
||||
team_id=TEAM_ID,
|
||||
billing_plan="hobby",
|
||||
firewall_config_accessible=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_ip_blocking_rules_configured.security_ip_blocking_rules_configured.security_client",
|
||||
new=security_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.vercel.services.security.security_ip_blocking_rules_configured.security_ip_blocking_rules_configured import (
|
||||
security_ip_blocking_rules_configured,
|
||||
)
|
||||
|
||||
check = security_ip_blocking_rules_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Project {PROJECT_NAME} ({PROJECT_ID}) could not be assessed for IP blocking rules because the firewall configuration endpoint was not accessible. Manual verification is required. This may be expected because IP blocking rules are not available on the Vercel Hobby plan."
|
||||
)
|
||||
|
||||
+41
-1
@@ -121,6 +121,7 @@ class Test_security_managed_rulesets_enabled:
|
||||
project_id=PROJECT_ID,
|
||||
project_name=PROJECT_NAME,
|
||||
team_id=TEAM_ID,
|
||||
firewall_config_accessible=False,
|
||||
firewall_enabled=False,
|
||||
managed_rulesets=None,
|
||||
id=PROJECT_ID,
|
||||
@@ -150,6 +151,45 @@ 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
|
||||
|
||||
def test_managed_rulesets_plan_gated_non_enterprise_scope(self):
|
||||
security_client = mock.MagicMock
|
||||
security_client.firewall_configs = {
|
||||
PROJECT_ID: VercelFirewallConfig(
|
||||
project_id=PROJECT_ID,
|
||||
project_name=PROJECT_NAME,
|
||||
team_id=TEAM_ID,
|
||||
billing_plan="pro",
|
||||
firewall_config_accessible=False,
|
||||
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_managed_rulesets_enabled.security_managed_rulesets_enabled.security_client",
|
||||
new=security_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.vercel.services.security.security_managed_rulesets_enabled.security_managed_rulesets_enabled import (
|
||||
security_managed_rulesets_enabled,
|
||||
)
|
||||
|
||||
check = security_managed_rulesets_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== 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. This may be expected because some managed WAF rulesets, including the OWASP Core Ruleset, are only available on Vercel Enterprise plans."
|
||||
)
|
||||
|
||||
+38
@@ -111,3 +111,41 @@ class Test_security_rate_limiting_configured:
|
||||
== f"Project {PROJECT_NAME} ({PROJECT_ID}) does not have any rate limiting rules configured."
|
||||
)
|
||||
assert result[0].team_id == TEAM_ID
|
||||
|
||||
def test_rate_limiting_status_unavailable_hobby_plan(self):
|
||||
security_client = mock.MagicMock
|
||||
security_client.firewall_configs = {
|
||||
PROJECT_ID: VercelFirewallConfig(
|
||||
project_id=PROJECT_ID,
|
||||
project_name=PROJECT_NAME,
|
||||
team_id=TEAM_ID,
|
||||
billing_plan="hobby",
|
||||
firewall_config_accessible=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_rate_limiting_configured.security_rate_limiting_configured.security_client",
|
||||
new=security_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.vercel.services.security.security_rate_limiting_configured.security_rate_limiting_configured import (
|
||||
security_rate_limiting_configured,
|
||||
)
|
||||
|
||||
check = security_rate_limiting_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Project {PROJECT_NAME} ({PROJECT_ID}) could not be assessed for rate limiting rules because the firewall configuration endpoint was not accessible. Manual verification is required. This may be expected because rate limiting rules are not available on the Vercel Hobby plan."
|
||||
)
|
||||
|
||||
@@ -7,7 +7,12 @@ from tests.providers.vercel.vercel_fixtures import PROJECT_ID, PROJECT_NAME, TEA
|
||||
|
||||
class TestSecurityService:
|
||||
def test_fetch_firewall_config_reads_active_version_and_normalizes_response(self):
|
||||
project = VercelProject(id=PROJECT_ID, name=PROJECT_NAME, team_id=TEAM_ID)
|
||||
project = VercelProject(
|
||||
id=PROJECT_ID,
|
||||
name=PROJECT_NAME,
|
||||
team_id=TEAM_ID,
|
||||
billing_plan="pro",
|
||||
)
|
||||
service = Security.__new__(Security)
|
||||
service.firewall_configs = {}
|
||||
|
||||
@@ -89,6 +94,7 @@ class TestSecurityService:
|
||||
)
|
||||
|
||||
config = service.firewall_configs[PROJECT_ID]
|
||||
assert config.billing_plan == "pro"
|
||||
assert config.firewall_enabled is True
|
||||
assert config.managed_rulesets == {"owasp": {"active": True, "action": "deny"}}
|
||||
assert [rule["id"] for rule in config.custom_rules] == ["rule-custom"]
|
||||
|
||||
+80
@@ -113,3 +113,83 @@ 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_config_accessible=False,
|
||||
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
|
||||
|
||||
def test_waf_status_unavailable_hobby_plan(self):
|
||||
security_client = mock.MagicMock
|
||||
security_client.firewall_configs = {
|
||||
PROJECT_ID: VercelFirewallConfig(
|
||||
project_id=PROJECT_ID,
|
||||
project_name=PROJECT_NAME,
|
||||
team_id=TEAM_ID,
|
||||
billing_plan="hobby",
|
||||
firewall_config_accessible=False,
|
||||
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].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. This may be expected because the Web Application Firewall is not available on the Vercel Hobby plan."
|
||||
)
|
||||
|
||||
+38
@@ -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 == ""
|
||||
|
||||
+39
@@ -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 == ""
|
||||
|
||||
+38
@@ -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 == ""
|
||||
|
||||
@@ -33,6 +33,8 @@ def set_mocked_vercel_provider(
|
||||
team_id: str = TEAM_ID,
|
||||
identity: VercelIdentityInfo = None,
|
||||
audit_config: dict = None,
|
||||
billing_plan: str = None,
|
||||
team_billing_plan: str = None,
|
||||
):
|
||||
"""Create a mocked VercelProvider for testing."""
|
||||
provider = MagicMock()
|
||||
@@ -42,15 +44,22 @@ def set_mocked_vercel_provider(
|
||||
team_id=team_id,
|
||||
http_session=MagicMock(),
|
||||
)
|
||||
resolved_team_billing_plan = (
|
||||
team_billing_plan if team_billing_plan is not None else billing_plan
|
||||
)
|
||||
team_info = VercelTeamInfo(
|
||||
id=TEAM_ID,
|
||||
name=TEAM_NAME,
|
||||
slug=TEAM_SLUG,
|
||||
billing_plan=resolved_team_billing_plan,
|
||||
)
|
||||
provider.identity = identity or VercelIdentityInfo(
|
||||
user_id=USER_ID,
|
||||
username=USERNAME,
|
||||
email=USER_EMAIL,
|
||||
team=VercelTeamInfo(
|
||||
id=TEAM_ID,
|
||||
name=TEAM_NAME,
|
||||
slug=TEAM_SLUG,
|
||||
),
|
||||
billing_plan=billing_plan,
|
||||
team=team_info,
|
||||
teams=[team_info],
|
||||
)
|
||||
provider.audit_config = audit_config or {"max_retries": 3}
|
||||
provider.fixer_config = {}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
from prowler.lib.check.models import CheckMetadata
|
||||
|
||||
|
||||
class TestVercelMetadata:
|
||||
EXPECTED_CATEGORIES = {
|
||||
"authentication_no_stale_tokens": [
|
||||
"trust-boundaries",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"authentication_token_not_expired": [
|
||||
"trust-boundaries",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"deployment_production_uses_stable_target": [
|
||||
"trust-boundaries",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"domain_dns_properly_configured": [
|
||||
"trust-boundaries",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"domain_ssl_certificate_valid": ["encryption", "vercel-hobby-plan"],
|
||||
"domain_verified": ["trust-boundaries", "vercel-hobby-plan"],
|
||||
"project_auto_expose_system_env_disabled": [
|
||||
"trust-boundaries",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"project_deployment_protection_enabled": [
|
||||
"internet-exposed",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"project_directory_listing_disabled": [
|
||||
"internet-exposed",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"project_environment_no_overly_broad_target": [
|
||||
"secrets",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"project_environment_no_secrets_in_plain_type": [
|
||||
"secrets",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"project_environment_production_vars_not_in_preview": [
|
||||
"secrets",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"project_git_fork_protection_enabled": [
|
||||
"internet-exposed",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"project_password_protection_enabled": [
|
||||
"internet-exposed",
|
||||
"vercel-pro-plan",
|
||||
],
|
||||
"project_production_deployment_protection_enabled": [
|
||||
"internet-exposed",
|
||||
"vercel-pro-plan",
|
||||
],
|
||||
"project_skew_protection_enabled": ["resilience", "vercel-pro-plan"],
|
||||
"security_custom_rules_configured": [
|
||||
"internet-exposed",
|
||||
"vercel-pro-plan",
|
||||
],
|
||||
"security_ip_blocking_rules_configured": [
|
||||
"internet-exposed",
|
||||
"vercel-pro-plan",
|
||||
],
|
||||
"security_managed_rulesets_enabled": [
|
||||
"internet-exposed",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"security_rate_limiting_configured": [
|
||||
"internet-exposed",
|
||||
"vercel-pro-plan",
|
||||
],
|
||||
"security_waf_enabled": ["internet-exposed", "vercel-pro-plan"],
|
||||
"team_directory_sync_enabled": [
|
||||
"trust-boundaries",
|
||||
"vercel-enterprise-plan",
|
||||
],
|
||||
"team_member_role_least_privilege": [
|
||||
"trust-boundaries",
|
||||
"vercel-hobby-plan",
|
||||
],
|
||||
"team_no_stale_invitations": ["trust-boundaries", "vercel-hobby-plan"],
|
||||
"team_saml_sso_enabled": ["trust-boundaries", "vercel-pro-plan"],
|
||||
"team_saml_sso_enforced": ["trust-boundaries", "vercel-pro-plan"],
|
||||
}
|
||||
|
||||
def test_vercel_checks_use_legacy_and_plan_categories(self):
|
||||
vercel_metadata = CheckMetadata.get_bulk(provider="vercel")
|
||||
|
||||
assert set(vercel_metadata) == set(self.EXPECTED_CATEGORIES)
|
||||
|
||||
for check_id, expected_categories in self.EXPECTED_CATEGORIES.items():
|
||||
assert vercel_metadata[check_id].Categories == expected_categories
|
||||
Reference in New Issue
Block a user