diff --git a/api/src/backend/api/migrations/0017_alter_provider_provider.py b/api/src/backend/api/migrations/0017_alter_provider_provider.py new file mode 100644 index 0000000000..9a5acde76f --- /dev/null +++ b/api/src/backend/api/migrations/0017_alter_provider_provider.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.7 on 2025-04-16 08:47 + +from django.db import migrations + +import api.db_utils + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0016_finding_compliance_resource_details_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="provider", + name="provider", + field=api.db_utils.ProviderEnumField( + choices=[ + ("aws", "AWS"), + ("azure", "Azure"), + ("gcp", "GCP"), + ("kubernetes", "Kubernetes"), + ("m365", "M365"), + ], + default="aws", + ), + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 69b7ead46a..162d9f632e 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -191,6 +191,7 @@ class Provider(RowLevelSecurityProtectedModel): AZURE = "azure", _("Azure") GCP = "gcp", _("GCP") KUBERNETES = "kubernetes", _("Kubernetes") + M365 = "m365", _("M365") @staticmethod def validate_aws_uid(value): @@ -214,6 +215,19 @@ class Provider(RowLevelSecurityProtectedModel): pointer="/data/attributes/uid", ) + @staticmethod + def validate_m365_uid(value): + try: + val = UUID(value, version=4) + if str(val) != value: + raise ValueError + except ValueError: + raise ModelValidationError( + detail="M365 tenant ID must be a valid UUID.", + code="m365-uid", + pointer="/data/attributes/uid", + ) + @staticmethod def validate_gcp_uid(value): if not re.match(r"^[a-z][a-z0-9-]{5,29}$", value): diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index 02a97249c6..c42c465e72 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -19,6 +19,7 @@ from prowler.providers.aws.aws_provider import AwsProvider from prowler.providers.azure.azure_provider import AzureProvider from prowler.providers.gcp.gcp_provider import GcpProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider +from prowler.providers.m365.m365_provider import M365Provider class TestMergeDicts: @@ -104,6 +105,7 @@ class TestReturnProwlerProvider: (Provider.ProviderChoices.GCP.value, GcpProvider), (Provider.ProviderChoices.AZURE.value, AzureProvider), (Provider.ProviderChoices.KUBERNETES.value, KubernetesProvider), + (Provider.ProviderChoices.M365.value, M365Provider), ], ) def test_return_prowler_provider(self, provider_type, expected_provider): @@ -176,6 +178,10 @@ class TestGetProwlerProviderKwargs: Provider.ProviderChoices.KUBERNETES.value, {"context": "provider_uid"}, ), + ( + Provider.ProviderChoices.M365.value, + {"context": "provider_uid"}, + ), ], ) def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs): diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index 31d009dba5..dc16b65189 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -11,6 +11,7 @@ from prowler.providers.azure.azure_provider import AzureProvider from prowler.providers.common.models import Connection from prowler.providers.gcp.gcp_provider import GcpProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider +from prowler.providers.m365.m365_provider import M365Provider class CustomOAuth2Client(OAuth2Client): @@ -51,14 +52,14 @@ def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict: def return_prowler_provider( provider: Provider, -) -> [AwsProvider | AzureProvider | GcpProvider | KubernetesProvider]: +) -> [AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider]: """Return the Prowler provider class based on the given provider type. Args: provider (Provider): The provider object containing the provider type and associated secrets. Returns: - AwsProvider | AzureProvider | GcpProvider | KubernetesProvider: The corresponding provider class. + AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: The corresponding provider class. Raises: ValueError: If the provider type specified in `provider.provider` is not supported. @@ -72,6 +73,8 @@ def return_prowler_provider( prowler_provider = AzureProvider case Provider.ProviderChoices.KUBERNETES.value: prowler_provider = KubernetesProvider + case Provider.ProviderChoices.M365.value: + prowler_provider = M365Provider case _: raise ValueError(f"Provider type {provider.provider} not supported") return prowler_provider @@ -92,6 +95,11 @@ def get_prowler_provider_kwargs(provider: Provider) -> dict: **prowler_provider_kwargs, "subscription_ids": [provider.uid], } + elif provider.provider == Provider.ProviderChoices.M365.value: + prowler_provider_kwargs = { + **prowler_provider_kwargs, + "tenant_id": provider.uid, + } elif provider.provider == Provider.ProviderChoices.GCP.value: prowler_provider_kwargs = { **prowler_provider_kwargs, @@ -104,15 +112,15 @@ def get_prowler_provider_kwargs(provider: Provider) -> dict: def initialize_prowler_provider( provider: Provider, -) -> AwsProvider | AzureProvider | GcpProvider | KubernetesProvider: +) -> AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: """Initialize a Prowler provider instance based on the given provider type. Args: provider (Provider): The provider object containing the provider type and associated secrets. Returns: - AwsProvider | AzureProvider | GcpProvider | KubernetesProvider: An instance of the corresponding provider class - (`AwsProvider`, `AzureProvider`, `GcpProvider`, or `KubernetesProvider`) initialized with the + AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: An instance of the corresponding provider class + (`AwsProvider`, `AzureProvider`, `GcpProvider`, `KubernetesProvider` or `M365Provider`) initialized with the provider's secrets. """ prowler_provider = return_prowler_provider(provider) diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 96fe46eb85..bc47516802 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1141,6 +1141,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer): serializer = GCPProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.KUBERNETES.value: serializer = KubernetesProviderSecret(data=secret) + elif provider_type == Provider.ProviderChoices.M365.value: + serializer = M365ProviderSecret(data=secret) else: raise serializers.ValidationError( {"provider": f"Provider type not supported {provider_type}"} @@ -1180,6 +1182,13 @@ class AzureProviderSecret(serializers.Serializer): resource_name = "provider-secrets" +class M365ProviderSecret(serializers.Serializer): + client_id = serializers.CharField() + client_secret = serializers.CharField() + user = serializers.EmailField() + encrypted_password = serializers.CharField() + + class GCPProviderSecret(serializers.Serializer): client_id = serializers.CharField() client_secret = serializers.CharField() @@ -1306,6 +1315,35 @@ class AWSRoleAssumptionProviderSecret(serializers.Serializer): }, "required": ["client_id", "client_secret", "tenant_id"], }, + { + "type": "object", + "title": "M365 Static Credentials", + "properties": { + "client_id": { + "type": "string", + "description": "The Azure application (client) ID for authentication in Azure AD.", + }, + "client_secret": { + "type": "string", + "description": "The client secret associated with the application (client) ID, providing " + "secure access.", + }, + "user": { + "type": "email", + "description": "User microsoft email address.", + }, + "encrypted_password": { + "type": "string", + "description": "User password.", + }, + }, + "required": [ + "client_id", + "client_secret", + "user", + "encrypted_password", + ], + }, { "type": "object", "title": "GCP Static Credentials",