feat(m365): add M365 certificate auth to API (#8538)

This commit is contained in:
Hugo Pereira Brito
2025-10-09 16:50:28 +02:00
committed by GitHub
parent e80eed6baf
commit b74744b135
5 changed files with 228 additions and 29 deletions
+1
View File
@@ -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)
+112 -4
View File
@@ -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
+50 -23
View File
@@ -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),
}
@@ -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",
],
+30 -1
View File
@@ -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"