From af6198e6c25cdc1743febf3d4bf39b8d03118ec8 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:20:49 +0200 Subject: [PATCH] feat(api): integrate Vercel provider into API layer (#10190) Co-authored-by: Daniel Barranquero --- api/CHANGELOG.md | 1 + .../api/migrations/0087_vercel_provider.py | 40 ++++++ api/src/backend/api/models.py | 12 +- api/src/backend/api/specs/v1.yaml | 132 ++++++++++++++++++ api/src/backend/api/tests/test_utils.py | 24 ++++ api/src/backend/api/tests/test_views.py | 56 +++++++- api/src/backend/api/utils.py | 19 +++ .../api/v1/serializer_utils/providers.py | 11 ++ api/src/backend/api/v1/serializers.py | 9 ++ api/src/backend/conftest.py | 7 + 10 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 api/src/backend/api/migrations/0087_vercel_provider.py diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 3edcd8e215..e143d7886f 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the **Prowler API** are documented in this file. ### 🚀 Added - `VALKEY_SCHEME`, `VALKEY_USERNAME`, and `VALKEY_PASSWORD` environment variables to configure Celery broker TLS/auth connection details for Valkey/ElastiCache [(#10420)](https://github.com/prowler-cloud/prowler/pull/10420) +- `Vercel` provider support [(#10190)](https://github.com/prowler-cloud/prowler/pull/10190) ### 🔄 Changed diff --git a/api/src/backend/api/migrations/0087_vercel_provider.py b/api/src/backend/api/migrations/0087_vercel_provider.py new file mode 100644 index 0000000000..84a07b3194 --- /dev/null +++ b/api/src/backend/api/migrations/0087_vercel_provider.py @@ -0,0 +1,40 @@ +from django.db import migrations + +import api.db_utils + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0086_attack_paths_cleanup_periodic_task"), + ] + + operations = [ + migrations.AlterField( + model_name="provider", + name="provider", + field=api.db_utils.ProviderEnumField( + choices=[ + ("aws", "AWS"), + ("azure", "Azure"), + ("gcp", "GCP"), + ("kubernetes", "Kubernetes"), + ("m365", "M365"), + ("github", "GitHub"), + ("mongodbatlas", "MongoDB Atlas"), + ("iac", "IaC"), + ("oraclecloud", "Oracle Cloud Infrastructure"), + ("alibabacloud", "Alibaba Cloud"), + ("cloudflare", "Cloudflare"), + ("openstack", "OpenStack"), + ("image", "Image"), + ("googleworkspace", "Google Workspace"), + ("vercel", "Vercel"), + ], + default="aws", + ), + ), + migrations.RunSQL( + "ALTER TYPE provider ADD VALUE IF NOT EXISTS 'vercel';", + reverse_sql=migrations.RunSQL.noop, + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 78bfec5262..5e0880be08 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -4,11 +4,11 @@ import re from datetime import datetime, timedelta, timezone from uuid import UUID, uuid4 +import defusedxml from allauth.socialaccount.models import SocialApp from config.custom_logging import BackendLogger from config.settings.social_login import SOCIALACCOUNT_PROVIDERS from cryptography.fernet import Fernet, InvalidToken -import defusedxml from defusedxml import ElementTree as ET from django.conf import settings from django.contrib.auth.models import AbstractBaseUser @@ -295,6 +295,7 @@ class Provider(RowLevelSecurityProtectedModel): OPENSTACK = "openstack", _("OpenStack") IMAGE = "image", _("Image") GOOGLEWORKSPACE = "googleworkspace", _("Google Workspace") + VERCEL = "vercel", _("Vercel") @staticmethod def validate_aws_uid(value): @@ -438,6 +439,15 @@ class Provider(RowLevelSecurityProtectedModel): pointer="/data/attributes/uid", ) + @staticmethod + def validate_vercel_uid(value): + if not re.match(r"^team_[a-zA-Z0-9]{16,32}$", value): + raise ModelValidationError( + detail="Vercel provider ID must be a valid Vercel Team ID (e.g., team_xxxxxxxxxxxxxxxxxxxxxxxx).", + code="vercel-uid", + pointer="/data/attributes/uid", + ) + @staticmethod def validate_image_uid(value): if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._/:@-]{2,249}$", value): diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 95ab72a5c2..7819f8010c 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -372,6 +372,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -387,6 +388,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -409,6 +411,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -426,6 +429,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -1351,6 +1355,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -1366,6 +1371,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -1827,6 +1833,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -1842,6 +1849,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -1864,6 +1872,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -1881,6 +1890,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -2429,6 +2439,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -2444,6 +2455,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -2466,6 +2478,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -2483,6 +2496,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -2939,6 +2953,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -2954,6 +2969,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -2976,6 +2992,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -2993,6 +3010,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -3447,6 +3465,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -3462,6 +3481,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -3484,6 +3504,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -3501,6 +3522,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -3943,6 +3965,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -3958,6 +3981,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -3980,6 +4004,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -3997,6 +4022,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -5780,6 +5806,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -5795,6 +5822,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -5817,6 +5845,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -5834,6 +5863,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - name: filter[search] @@ -5955,6 +5985,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -5970,6 +6001,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -5992,6 +6024,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -6009,6 +6042,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - name: filter[search] @@ -6119,6 +6153,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -6134,6 +6169,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -6155,6 +6191,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -6172,6 +6209,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - name: filter[search] @@ -6314,6 +6352,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -6329,6 +6368,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -6351,6 +6391,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -6368,6 +6409,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -6523,6 +6565,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -6538,6 +6581,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -6560,6 +6604,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -6577,6 +6622,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -6726,6 +6772,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -6741,6 +6788,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -6762,6 +6810,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -6779,6 +6828,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - name: filter[search] @@ -6970,6 +7020,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -6985,6 +7036,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -7007,6 +7059,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -7024,6 +7077,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -7144,6 +7198,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -7159,6 +7214,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -7181,6 +7237,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -7198,6 +7255,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -7342,6 +7400,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -7357,6 +7416,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -7379,6 +7439,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -7396,6 +7457,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -8181,6 +8243,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -8196,6 +8259,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider__in] schema: @@ -8218,6 +8282,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -8235,6 +8300,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -8257,6 +8323,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -8272,6 +8339,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -8294,6 +8362,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -8311,6 +8380,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - name: filter[search] @@ -8980,6 +9050,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -8995,6 +9066,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -9017,6 +9089,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -9034,6 +9107,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -9527,6 +9601,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -9542,6 +9617,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -9564,6 +9640,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -9581,6 +9658,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -9887,6 +9965,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -9902,6 +9981,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -9924,6 +10004,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -9941,6 +10022,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -10253,6 +10335,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -10268,6 +10351,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -10290,6 +10374,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -10307,6 +10392,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -11129,6 +11215,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- * `aws` - AWS * `azure` - Azure @@ -11144,6 +11231,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel - in: query name: filter[provider_type__in] schema: @@ -11166,6 +11254,7 @@ paths: - mongodbatlas - openstack - oraclecloud + - vercel description: |- Multiple values may be separated by commas. @@ -11183,6 +11272,7 @@ paths: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel explode: false style: form - in: query @@ -18463,6 +18553,15 @@ components: required: - clouds_yaml_content - clouds_yaml_cloud + - type: object + title: Vercel API Token + properties: + api_token: + type: string + description: Vercel API token for authentication. Can be scoped + to a specific team. + required: + - api_token writeOnly: true required: - secret @@ -19465,6 +19564,7 @@ components: - openstack - image - googleworkspace + - vercel type: string description: |- * `aws` - AWS @@ -19481,6 +19581,7 @@ components: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel x-spec-enum-id: c0d56cad8ab9abe5 uid: type: string @@ -19601,6 +19702,7 @@ components: - openstack - image - googleworkspace + - vercel type: string x-spec-enum-id: c0d56cad8ab9abe5 description: |- @@ -19620,6 +19722,7 @@ components: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel uid: type: string title: Unique identifier for the provider, set by the provider @@ -19671,6 +19774,7 @@ components: - openstack - image - googleworkspace + - vercel type: string x-spec-enum-id: c0d56cad8ab9abe5 description: |- @@ -19690,6 +19794,7 @@ components: * `openstack` - OpenStack * `image` - Image * `googleworkspace` - Google Workspace + * `vercel` - Vercel uid: type: string minLength: 3 @@ -20539,6 +20644,15 @@ components: required: - clouds_yaml_content - clouds_yaml_cloud + - type: object + title: Vercel API Token + properties: + api_token: + type: string + description: Vercel API token for authentication. Can be scoped + to a specific team. + required: + - api_token writeOnly: true required: - secret_type @@ -20955,6 +21069,15 @@ components: required: - clouds_yaml_content - clouds_yaml_cloud + - type: object + title: Vercel API Token + properties: + api_token: + type: string + description: Vercel API token for authentication. Can be scoped + to a specific team. + required: + - api_token writeOnly: true required: - secret_type @@ -21381,6 +21504,15 @@ components: required: - clouds_yaml_content - clouds_yaml_cloud + - type: object + title: Vercel API Token + properties: + api_token: + type: string + description: Vercel API token for authentication. Can be scoped + to a specific team. + required: + - api_token writeOnly: true required: - secret diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index a3b1bf5736..485d84ce6b 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -33,6 +33,7 @@ from prowler.providers.m365.m365_provider import M365Provider from prowler.providers.mongodbatlas.mongodbatlas_provider import MongodbatlasProvider from prowler.providers.openstack.openstack_provider import OpenstackProvider from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider +from prowler.providers.vercel.vercel_provider import VercelProvider class TestMergeDicts: @@ -128,6 +129,7 @@ class TestReturnProwlerProvider: (Provider.ProviderChoices.CLOUDFLARE.value, CloudflareProvider), (Provider.ProviderChoices.OPENSTACK.value, OpenstackProvider), (Provider.ProviderChoices.IMAGE.value, ImageProvider), + (Provider.ProviderChoices.VERCEL.value, VercelProvider), ], ) def test_return_prowler_provider(self, provider_type, expected_provider): @@ -218,6 +220,24 @@ class TestProwlerProviderConnectionTest: registry_token="tok123", ) + @patch("api.utils.return_prowler_provider") + def test_prowler_provider_connection_test_vercel_provider( + self, mock_return_prowler_provider + ): + """Test connection test for Vercel provider passes team_id.""" + provider = MagicMock() + provider.uid = "team_abcdef1234567890" + provider.provider = Provider.ProviderChoices.VERCEL.value + provider.secret.secret = {"api_token": "vercel_token_123"} + mock_return_prowler_provider.return_value = MagicMock() + + prowler_provider_connection_test(provider) + mock_return_prowler_provider.return_value.test_connection.assert_called_once_with( + api_token="vercel_token_123", + team_id="team_abcdef1234567890", + raise_on_exception=False, + ) + @patch("api.utils.return_prowler_provider") def test_prowler_provider_connection_test_image_provider_no_creds( self, mock_return_prowler_provider @@ -284,6 +304,10 @@ class TestGetProwlerProviderKwargs: Provider.ProviderChoices.OPENSTACK.value, {}, ), + ( + Provider.ProviderChoices.VERCEL.value, + {"team_id": "provider_uid"}, + ), ], ) def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs): diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 9aad2bcff6..cef42d7853 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -1715,6 +1715,46 @@ class TestProviderViewSet: "min_length", "uid", ), + # Vercel UID validation - missing team_ prefix + ( + { + "provider": "vercel", + "uid": "abcdef1234567890abcdef12", + "alias": "test", + }, + "vercel-uid", + "uid", + ), + # Vercel UID validation - too short after prefix + ( + { + "provider": "vercel", + "uid": "team_abc123", + "alias": "test", + }, + "vercel-uid", + "uid", + ), + # Vercel UID validation - contains special characters + ( + { + "provider": "vercel", + "uid": "team_abcdef-1234567890ab", + "alias": "test", + }, + "vercel-uid", + "uid", + ), + # Vercel UID validation - too long (33 chars after prefix) + ( + { + "provider": "vercel", + "uid": "team_abcdefghijklmnopqrstuvwxyz1234567", + "alias": "test", + }, + "vercel-uid", + "uid", + ), # Google Workspace UID validation - missing 'C' prefix ( { @@ -1918,21 +1958,21 @@ class TestProviderViewSet: ( "uid.icontains", "1", - 11, + 12, ), ("alias", "aws_testing_1", 1), ("alias.icontains", "aws", 2), - ("inserted_at", TODAY, 12), + ("inserted_at", TODAY, 13), ( "inserted_at.gte", "2024-01-01", - 12, + 13, ), ("inserted_at.lte", "2024-01-01", 0), ( "updated_at.gte", "2024-01-01", - 12, + 13, ), ("updated_at.lte", "2024-01-01", 0), ] @@ -2557,6 +2597,14 @@ class TestProviderSecretViewSet: "delegated_user": "admin@example.com", }, ), + # Vercel with API Token + ( + Provider.ProviderChoices.VERCEL.value, + ProviderSecret.TypeChoices.STATIC, + { + "api_token": "fake-vercel-api-token-for-testing", + }, + ), ], ) def test_provider_secrets_create_valid( diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index 6d4102af55..8501c5409e 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -39,6 +39,7 @@ if TYPE_CHECKING: ) from prowler.providers.openstack.openstack_provider import OpenstackProvider from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider + from prowler.providers.vercel.vercel_provider import VercelProvider class CustomOAuth2Client(OAuth2Client): @@ -94,6 +95,7 @@ def return_prowler_provider( | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider + | VercelProvider ): """Return the Prowler provider class based on the given provider type. @@ -175,6 +177,10 @@ def return_prowler_provider( from prowler.providers.image.image_provider import ImageProvider prowler_provider = ImageProvider + case Provider.ProviderChoices.VERCEL.value: + from prowler.providers.vercel.vercel_provider import VercelProvider + + prowler_provider = VercelProvider case _: raise ValueError(f"Provider type {provider.provider} not supported") return prowler_provider @@ -235,6 +241,11 @@ def get_prowler_provider_kwargs( # clouds_yaml_content, clouds_yaml_cloud and provider_id are validated # in the provider itself, so it's not needed here. pass + elif provider.provider == Provider.ProviderChoices.VERCEL.value: + prowler_provider_kwargs = { + **prowler_provider_kwargs, + "team_id": provider.uid, + } elif provider.provider == Provider.ProviderChoices.IMAGE.value: # Detect whether uid is a registry URL (e.g. "docker.io/andoniaf") or # a concrete image reference (e.g. "docker.io/andoniaf/myimage:latest"). @@ -281,6 +292,7 @@ def initialize_prowler_provider( | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider + | VercelProvider ): """Initialize a Prowler provider instance based on the given provider type. @@ -332,6 +344,13 @@ def prowler_provider_connection_test(provider: Provider) -> Connection: "raise_on_exception": False, } return prowler_provider.test_connection(**openstack_kwargs) + elif provider.provider == Provider.ProviderChoices.VERCEL.value: + vercel_kwargs = { + **prowler_provider_kwargs, + "team_id": provider.uid, + "raise_on_exception": False, + } + return prowler_provider.test_connection(**vercel_kwargs) elif provider.provider == Provider.ProviderChoices.IMAGE.value: image_kwargs = { "image": provider.uid, diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 034bc79cb2..f8d67f0e3b 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -404,6 +404,17 @@ from rest_framework_json_api import serializers }, "required": ["clouds_yaml_content", "clouds_yaml_cloud"], }, + { + "type": "object", + "title": "Vercel API Token", + "properties": { + "api_token": { + "type": "string", + "description": "Vercel API token for authentication. Can be scoped to a specific team.", + }, + }, + "required": ["api_token"], + }, ] } ) diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 993c680557..2870ad3d7e 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1573,6 +1573,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer): serializer = OpenStackCloudsYamlProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.IMAGE.value: serializer = ImageProviderSecret(data=secret) + elif provider_type == Provider.ProviderChoices.VERCEL.value: + serializer = VercelProviderSecret(data=secret) else: raise serializers.ValidationError( {"provider": f"Provider type not supported {provider_type}"} @@ -1779,6 +1781,13 @@ class ImageProviderSecret(serializers.Serializer): return attrs +class VercelProviderSecret(serializers.Serializer): + api_token = serializers.CharField() + + class Meta: + resource_name = "provider-secrets" + + class AlibabaCloudProviderSecret(serializers.Serializer): access_key_id = serializers.CharField() access_key_secret = serializers.CharField() diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index 515c7f5769..ce1f24f638 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -565,6 +565,12 @@ def providers_fixture(tenants_fixture): alias="googleworkspace_testing", tenant_id=tenant.id, ) + provider13 = Provider.objects.create( + provider="vercel", + uid="team_abcdef1234567890ab", + alias="vercel_testing", + tenant_id=tenant.id, + ) return ( provider1, @@ -579,6 +585,7 @@ def providers_fixture(tenants_fixture): provider10, provider11, provider12, + provider13, )