diff --git a/api/Dockerfile b/api/Dockerfile index 3569d003e7..4e3896a156 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -3,11 +3,23 @@ FROM python:3.12.8-alpine3.20 AS build LABEL maintainer="https://github.com/prowler-cloud/api" # hadolint ignore=DL3018 -RUN apk --no-cache add gcc python3-dev musl-dev linux-headers curl-dev +RUN apk --no-cache add gcc python3-dev musl-dev linux-headers curl-dev \ + ca-certificates less ncurses-terminfo-base krb5-libs libgcc libintl libssl3 \ + libstdc++ tzdata userspace-rcu zlib icu-libs curl lttng-ust openssh-client && \ + apk --no-cache upgrade -RUN apk --no-cache upgrade && \ - addgroup -g 1000 prowler && \ +# Install PowerShell +RUN curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz -o /tmp/powershell.tar.gz && \ + mkdir -p /opt/microsoft/powershell/7 && \ + tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 && \ + chmod +x /opt/microsoft/powershell/7/pwsh && \ + ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh && \ + rm /tmp/powershell.tar.gz + +# Add prowler user +RUN addgroup -g 1000 prowler && \ adduser -D -u 1000 -G prowler prowler + USER prowler WORKDIR /home/prowler @@ -17,11 +29,10 @@ COPY pyproject.toml ./ RUN pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir poetry -COPY src/backend/ ./backend/ +COPY src/backend/ ./backend/ ENV PATH="/home/prowler/.local/bin:$PATH" -# Add `--no-root` to avoid installing the current project as a package RUN poetry install --no-root && \ rm -rf ~/.cache/pip diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 162d9f632e..01f88178f0 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -217,13 +217,9 @@ class Provider(RowLevelSecurityProtectedModel): @staticmethod def validate_m365_uid(value): - try: - val = UUID(value, version=4) - if str(val) != value: - raise ValueError - except ValueError: + if not re.match(r"^[a-zA-Z0-9-]+\.onmicrosoft\.com$", value): raise ModelValidationError( - detail="M365 tenant ID must be a valid UUID.", + detail="M365 tenant ID must be a valid domain.", code="m365-uid", pointer="/data/attributes/uid", ) diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index dc16b65189..95fa789682 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -98,7 +98,7 @@ def get_prowler_provider_kwargs(provider: Provider) -> dict: elif provider.provider == Provider.ProviderChoices.M365.value: prowler_provider_kwargs = { **prowler_provider_kwargs, - "tenant_id": provider.uid, + "domain_id": provider.uid, } elif provider.provider == Provider.ProviderChoices.GCP.value: prowler_provider_kwargs = { @@ -138,10 +138,17 @@ def prowler_provider_connection_test(provider: Provider) -> Connection: Connection: A connection object representing the result of the connection test for the specified provider. """ prowler_provider = return_prowler_provider(provider) + try: prowler_provider_kwargs = provider.secret.secret except Provider.secret.RelatedObjectDoesNotExist as secret_error: return Connection(is_connected=False, error=secret_error) + + if provider.provider == Provider.ProviderChoices.M365.value: + return prowler_provider.test_connection( + **prowler_provider_kwargs, domain_id=provider.uid, raise_on_exception=False + ) + return prowler_provider.test_connection( **prowler_provider_kwargs, provider_id=provider.uid, raise_on_exception=False ) diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py new file mode 100644 index 0000000000..231e5dc1bb --- /dev/null +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -0,0 +1,172 @@ +from drf_spectacular.utils import extend_schema_field +from rest_framework_json_api import serializers + + +@extend_schema_field( + { + "oneOf": [ + { + "type": "object", + "title": "AWS Static Credentials", + "properties": { + "aws_access_key_id": { + "type": "string", + "description": "The AWS access key ID. Required for environments where no IAM role is being " + "assumed and direct AWS access is needed.", + }, + "aws_secret_access_key": { + "type": "string", + "description": "The AWS secret access key. Must accompany 'aws_access_key_id' to authorize " + "access to AWS resources.", + }, + "aws_session_token": { + "type": "string", + "description": "The session token associated with temporary credentials. Only needed for " + "session-based or temporary AWS access.", + }, + }, + "required": ["aws_access_key_id", "aws_secret_access_key"], + }, + { + "type": "object", + "title": "AWS Assume Role", + "properties": { + "role_arn": { + "type": "string", + "description": "The Amazon Resource Name (ARN) of the role to assume. Required for AWS role " + "assumption.", + }, + "external_id": { + "type": "string", + "description": "An identifier to enhance security for role assumption.", + }, + "aws_access_key_id": { + "type": "string", + "description": "The AWS access key ID. Only required if the environment lacks pre-configured " + "AWS credentials.", + }, + "aws_secret_access_key": { + "type": "string", + "description": "The AWS secret access key. Required if 'aws_access_key_id' is provided or if " + "no AWS credentials are pre-configured.", + }, + "aws_session_token": { + "type": "string", + "description": "The session token for temporary credentials, if applicable.", + }, + "session_duration": { + "type": "integer", + "minimum": 900, + "maximum": 43200, + "default": 3600, + "description": "The duration (in seconds) for the role session.", + }, + "role_session_name": { + "type": "string", + "description": "An identifier for the role session, useful for tracking sessions in AWS logs. " + "The regex used to validate this parameter is a string of characters consisting of " + "upper- and lower-case alphanumeric characters with no spaces. You can also include " + "underscores or any of the following characters: =,.@-\n\n" + "Examples:\n" + "- MySession123\n" + "- User_Session-1\n" + "- Test.Session@2", + "pattern": "^[a-zA-Z0-9=,.@_-]+$", + }, + }, + "required": ["role_arn", "external_id"], + }, + { + "type": "object", + "title": "Azure 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.", + }, + "tenant_id": { + "type": "string", + "description": "The Azure tenant ID, representing the directory where the application is " + "registered.", + }, + }, + "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.", + }, + "tenant_id": { + "type": "string", + "description": "The Azure tenant ID, representing the directory where the application is " + "registered.", + }, + "user": { + "type": "email", + "description": "User microsoft email address.", + }, + "encrypted_password": { + "type": "string", + "description": "User encrypted password.", + }, + }, + "required": [ + "client_id", + "client_secret", + "tenant_id", + "user", + "encrypted_password", + ], + }, + { + "type": "object", + "title": "GCP Static Credentials", + "properties": { + "client_id": { + "type": "string", + "description": "The client ID from Google Cloud, used to identify the application for GCP " + "access.", + }, + "client_secret": { + "type": "string", + "description": "The client secret associated with the GCP client ID, required for secure " + "access.", + }, + "refresh_token": { + "type": "string", + "description": "A refresh token that allows the application to obtain new access tokens for " + "extended use.", + }, + }, + "required": ["client_id", "client_secret", "refresh_token"], + }, + { + "type": "object", + "title": "Kubernetes Static Credentials", + "properties": { + "kubeconfig_content": { + "type": "string", + "description": "The content of the Kubernetes kubeconfig file, encoded as a string.", + } + }, + "required": ["kubeconfig_content"], + }, + ] + } +) +class ProviderSecretField(serializers.JSONField): + pass diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index bc47516802..920384ba65 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -42,6 +42,7 @@ from api.v1.serializer_utils.integrations import ( IntegrationCredentialField, S3ConfigSerializer, ) +from api.v1.serializer_utils.providers import ProviderSecretField # Tokens @@ -1185,9 +1186,13 @@ class AzureProviderSecret(serializers.Serializer): class M365ProviderSecret(serializers.Serializer): client_id = serializers.CharField() client_secret = serializers.CharField() + tenant_id = serializers.CharField() user = serializers.EmailField() encrypted_password = serializers.CharField() + class Meta: + resource_name = "provider-secrets" + class GCPProviderSecret(serializers.Serializer): client_id = serializers.CharField() @@ -1220,170 +1225,6 @@ class AWSRoleAssumptionProviderSecret(serializers.Serializer): resource_name = "provider-secrets" -@extend_schema_field( - { - "oneOf": [ - { - "type": "object", - "title": "AWS Static Credentials", - "properties": { - "aws_access_key_id": { - "type": "string", - "description": "The AWS access key ID. Required for environments where no IAM role is being " - "assumed and direct AWS access is needed.", - }, - "aws_secret_access_key": { - "type": "string", - "description": "The AWS secret access key. Must accompany 'aws_access_key_id' to authorize " - "access to AWS resources.", - }, - "aws_session_token": { - "type": "string", - "description": "The session token associated with temporary credentials. Only needed for " - "session-based or temporary AWS access.", - }, - }, - "required": ["aws_access_key_id", "aws_secret_access_key"], - }, - { - "type": "object", - "title": "AWS Assume Role", - "properties": { - "role_arn": { - "type": "string", - "description": "The Amazon Resource Name (ARN) of the role to assume. Required for AWS role " - "assumption.", - }, - "external_id": { - "type": "string", - "description": "An identifier to enhance security for role assumption.", - }, - "aws_access_key_id": { - "type": "string", - "description": "The AWS access key ID. Only required if the environment lacks pre-configured " - "AWS credentials.", - }, - "aws_secret_access_key": { - "type": "string", - "description": "The AWS secret access key. Required if 'aws_access_key_id' is provided or if " - "no AWS credentials are pre-configured.", - }, - "aws_session_token": { - "type": "string", - "description": "The session token for temporary credentials, if applicable.", - }, - "session_duration": { - "type": "integer", - "minimum": 900, - "maximum": 43200, - "default": 3600, - "description": "The duration (in seconds) for the role session.", - }, - "role_session_name": { - "type": "string", - "description": "An identifier for the role session, useful for tracking sessions in AWS logs. " - "The regex used to validate this parameter is a string of characters consisting of " - "upper- and lower-case alphanumeric characters with no spaces. You can also include " - "underscores or any of the following characters: =,.@-\n\n" - "Examples:\n" - "- MySession123\n" - "- User_Session-1\n" - "- Test.Session@2", - "pattern": "^[a-zA-Z0-9=,.@_-]+$", - }, - }, - "required": ["role_arn", "external_id"], - }, - { - "type": "object", - "title": "Azure 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.", - }, - "tenant_id": { - "type": "string", - "description": "The Azure tenant ID, representing the directory where the application is " - "registered.", - }, - }, - "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", - "properties": { - "client_id": { - "type": "string", - "description": "The client ID from Google Cloud, used to identify the application for GCP " - "access.", - }, - "client_secret": { - "type": "string", - "description": "The client secret associated with the GCP client ID, required for secure " - "access.", - }, - "refresh_token": { - "type": "string", - "description": "A refresh token that allows the application to obtain new access tokens for " - "extended use.", - }, - }, - "required": ["client_id", "client_secret", "refresh_token"], - }, - { - "type": "object", - "title": "Kubernetes Static Credentials", - "properties": { - "kubeconfig_content": { - "type": "string", - "description": "The content of the Kubernetes kubeconfig file, encoded as a string.", - } - }, - "required": ["kubeconfig_content"], - }, - ] - } -) -class ProviderSecretField(serializers.JSONField): - pass - - class ProviderSecretSerializer(RLSSerializer): """ Serializer for the ProviderSecret model. diff --git a/prowler/providers/m365/m365_provider.py b/prowler/providers/m365/m365_provider.py index c9b269e9d7..a5816543e0 100644 --- a/prowler/providers/m365/m365_provider.py +++ b/prowler/providers/m365/m365_provider.py @@ -606,12 +606,12 @@ class M365Provider(Provider): browser_auth: bool = False, tenant_id: str = None, region: str = "M365Global", - raise_on_exception=True, - client_id=None, - client_secret=None, - user=None, - encrypted_password=None, - provider_id=None, + raise_on_exception: bool = True, + client_id: str = None, + client_secret: str = None, + user: str = None, + encrypted_password: str = None, + provider_id: str = None, ) -> Connection: """Test connection to M365 subscription.