diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 1a67cd741a..3b78e84bb1 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 - Default JWT keys are generated and stored if they are missing from configuration [(#8655)](https://github.com/prowler-cloud/prowler/pull/8655) - `compliance_name` for each compliance [(#7920)](https://github.com/prowler-cloud/prowler/pull/7920) +- Support for M365 Certificate authentication [(#8538)](https://github.com/prowler-cloud/prowler/pull/8538) - API Key support [(#8805)](https://github.com/prowler-cloud/prowler/pull/8805) - SAML role mapping protection for single-admin tenants to prevent accidental lockout [(#8882)](https://github.com/prowler-cloud/prowler/pull/8882) - Support for `passed_findings` and `total_findings` fields in compliance requirement overview for accurate Prowler ThreatScore calculation [(#8582)](https://github.com/prowler-cloud/prowler/pull/8582) diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index daec67a79e..d282021d49 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -12018,6 +12018,33 @@ components: type: string description: The Azure tenant ID, representing the directory where the application is registered. + user: + type: email + description: 'Deprecated: User microsoft email address.' + password: + type: string + description: 'Deprecated: User password.' + required: + - client_id + - client_secret + - tenant_id + - user + - password + - type: object + title: M365 Certificate Credentials + properties: + client_id: + type: string + description: The Azure application (client) ID for authentication + in Azure AD. + tenant_id: + type: string + description: The Azure tenant ID, representing the directory + where the application is registered. + certificate_content: + type: string + description: The certificate content in base64 format for + certificate-based authentication. user: type: email description: User microsoft email address. @@ -12026,8 +12053,8 @@ components: description: User password. required: - client_id - - client_secret - tenant_id + - certificate_content - user - password - type: object @@ -13882,6 +13909,33 @@ components: type: string description: The Azure tenant ID, representing the directory where the application is registered. + user: + type: email + description: 'Deprecated: User microsoft email address.' + password: + type: string + description: 'Deprecated: User password.' + required: + - client_id + - client_secret + - tenant_id + - user + - password + - type: object + title: M365 Certificate Credentials + properties: + client_id: + type: string + description: The Azure application (client) ID for authentication + in Azure AD. + tenant_id: + type: string + description: The Azure tenant ID, representing the directory where + the application is registered. + certificate_content: + type: string + description: The certificate content in base64 format for certificate-based + authentication. user: type: email description: User microsoft email address. @@ -13890,8 +13944,8 @@ components: description: User password. required: - client_id - - client_secret - tenant_id + - certificate_content - user - password - type: object @@ -14130,6 +14184,33 @@ components: type: string description: The Azure tenant ID, representing the directory where the application is registered. + user: + type: email + description: 'Deprecated: User microsoft email address.' + password: + type: string + description: 'Deprecated: User password.' + required: + - client_id + - client_secret + - tenant_id + - user + - password + - type: object + title: M365 Certificate Credentials + properties: + client_id: + type: string + description: The Azure application (client) ID for authentication + in Azure AD. + tenant_id: + type: string + description: The Azure tenant ID, representing the directory + where the application is registered. + certificate_content: + type: string + description: The certificate content in base64 format for + certificate-based authentication. user: type: email description: User microsoft email address. @@ -14138,8 +14219,8 @@ components: description: User password. required: - client_id - - client_secret - tenant_id + - certificate_content - user - password - type: object @@ -14394,6 +14475,33 @@ components: type: string description: The Azure tenant ID, representing the directory where the application is registered. + user: + type: email + description: 'Deprecated: User microsoft email address.' + password: + type: string + description: 'Deprecated: User password.' + required: + - client_id + - client_secret + - tenant_id + - user + - password + - type: object + title: M365 Certificate Credentials + properties: + client_id: + type: string + description: The Azure application (client) ID for authentication + in Azure AD. + tenant_id: + type: string + description: The Azure tenant ID, representing the directory where + the application is registered. + certificate_content: + type: string + description: The certificate content in base64 format for certificate-based + authentication. user: type: email description: User microsoft email address. @@ -14402,8 +14510,8 @@ components: description: User password. required: - client_id - - client_secret - tenant_id + - certificate_content - user - password - type: object diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index d94caa41f7..71f946116c 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -1870,17 +1870,7 @@ class TestProviderSecretViewSet: "kubeconfig_content": "kubeconfig-content", }, ), - # M365 with STATIC secret - no user or password - ( - Provider.ProviderChoices.M365.value, - ProviderSecret.TypeChoices.STATIC, - { - "client_id": "client-id", - "client_secret": "client-secret", - "tenant_id": "tenant-id", - }, - ), - # M365 with user only + # M365 client secret credentials ( Provider.ProviderChoices.M365.value, ProviderSecret.TypeChoices.STATIC, @@ -1889,27 +1879,17 @@ class TestProviderSecretViewSet: "client_secret": "client-secret", "tenant_id": "tenant-id", "user": "test@domain.com", - }, - ), - # M365 with password only - ( - Provider.ProviderChoices.M365.value, - ProviderSecret.TypeChoices.STATIC, - { - "client_id": "client-id", - "client_secret": "client-secret", - "tenant_id": "tenant-id", "password": "supersecret", }, ), - # M365 with user and password + # M365 certificate credentials (valid base64) ( Provider.ProviderChoices.M365.value, ProviderSecret.TypeChoices.STATIC, { "client_id": "client-id", - "client_secret": "client-secret", "tenant_id": "tenant-id", + "certificate_content": "VGVzdCBjZXJ0aWZpY2F0ZSBjb250ZW50", "user": "test@domain.com", "password": "supersecret", }, @@ -2279,6 +2259,50 @@ class TestProviderSecretViewSet: ) assert response.status_code == status.HTTP_400_BAD_REQUEST + def test_m365_provider_secrets_invalid_certificate_base64( + self, authenticated_client, providers_fixture + ): + """Test M365 provider secret creation with invalid base64 certificate content""" + # Find M365 provider from fixture + m365_provider = None + for provider in providers_fixture: + if provider.provider == Provider.ProviderChoices.M365.value: + m365_provider = provider + break + + assert m365_provider is not None, "M365 provider not found in fixture" + + data = { + "data": { + "type": "provider-secrets", + "attributes": { + "name": "M365 Certificate Invalid Base64", + "secret_type": "static", + "secret": { + "client_id": "client-id", + "tenant_id": "tenant-id", + "certificate_content": "invalid-base64-content!@#$%", + "user": "test@domain.com", + "password": "supersecret", + }, + }, + "relationships": { + "provider": { + "data": {"type": "providers", "id": str(m365_provider.id)} + } + }, + } + } + response = authenticated_client.post( + reverse("providersecret-list"), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "certificate content is not valid base64 encoded data" in str( + response.json() + ) + @pytest.mark.django_db class TestScanViewSet: @@ -5781,10 +5805,12 @@ class TestScheduleViewSet: ) assert response.status_code == status.HTTP_404_NOT_FOUND + @patch("tasks.beat.perform_scheduled_scan_task.apply_async") @patch("api.v1.views.Task.objects.get") def test_schedule_daily_already_scheduled( self, mock_task_get, + mock_apply_async, authenticated_client, providers_fixture, tasks_fixture, @@ -5792,6 +5818,7 @@ class TestScheduleViewSet: provider, *_ = providers_fixture prowler_task = tasks_fixture[0] mock_task_get.return_value = prowler_task + mock_apply_async.return_value.id = prowler_task.id json_payload = { "provider_id": str(provider.id), } diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 76fa0b4911..522df89ee6 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -115,6 +115,40 @@ from rest_framework_json_api import serializers "description": "The Azure tenant ID, representing the directory where the application is " "registered.", }, + "user": { + "type": "email", + "description": "Deprecated: User microsoft email address.", + }, + "password": { + "type": "string", + "description": "Deprecated: User password.", + }, + }, + "required": [ + "client_id", + "client_secret", + "tenant_id", + "user", + "password", + ], + }, + { + "type": "object", + "title": "M365 Certificate Credentials", + "properties": { + "client_id": { + "type": "string", + "description": "The Azure application (client) ID for authentication in Azure AD.", + }, + "tenant_id": { + "type": "string", + "description": "The Azure tenant ID, representing the directory where the application is " + "registered.", + }, + "certificate_content": { + "type": "string", + "description": "The certificate content in base64 format for certificate-based authentication.", + }, "user": { "type": "email", "description": "User microsoft email address.", @@ -126,8 +160,8 @@ from rest_framework_json_api import serializers }, "required": [ "client_id", - "client_secret", "tenant_id", + "certificate_content", "user", "password", ], diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 32ebbb040d..70e002cd10 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1,3 +1,4 @@ +import base64 import json from datetime import datetime, timedelta, timezone @@ -1395,10 +1396,38 @@ class AzureProviderSecret(serializers.Serializer): class M365ProviderSecret(serializers.Serializer): client_id = serializers.CharField() - client_secret = serializers.CharField() + client_secret = serializers.CharField(required=False) tenant_id = serializers.CharField() user = serializers.EmailField(required=False) password = serializers.CharField(required=False) + certificate_content = serializers.CharField(required=False) + + def validate(self, attrs): + if attrs.get("client_secret") and attrs.get("certificate_content"): + raise serializers.ValidationError( + "You cannot provide both client_secret and certificate_content." + ) + if not attrs.get("client_secret") and not attrs.get("certificate_content"): + raise serializers.ValidationError( + "You must provide either client_secret or certificate_content." + ) + return super().validate(attrs) + + def validate_certificate_content(self, certificate_content): + """Validate that M365 certificate content is valid base64 encoded data.""" + if certificate_content: + try: + base64.b64decode(certificate_content, validate=True) + except Exception as e: + raise ValidationError( + { + "certificate_content": [ + f"The provided certificate content is not valid base64 encoded data: {str(e)}" + ] + }, + code="m365-certificate-content", + ) + return certificate_content class Meta: resource_name = "provider-secrets"