From e25ff209b3f0750f1913d9c57200697b6a2baec3 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:41:59 +0200 Subject: [PATCH] feat(m365): automate `PowerShell` modules installation (#7618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andoni A <14891798+andoniaf@users.noreply.github.com> Co-authored-by: Adrián Jesús Peña Rodríguez --- Dockerfile | 41 ++- api/Dockerfile | 41 ++- prowler/lib/powershell/powershell.py | 7 +- prowler/providers/common/provider.py | 1 + .../providers/m365/lib/arguments/arguments.py | 13 +- .../m365/lib/powershell/m365_powershell.py | 92 +++++-- prowler/providers/m365/m365_provider.py | 14 +- .../lib/powershell/m365_powershell_test.py | 233 +++++++++++++++++- tests/providers/m365/m365_provider_test.py | 92 ++++++- 9 files changed, 479 insertions(+), 55 deletions(-) diff --git a/Dockerfile b/Dockerfile index eb79e31cce..0e9fb89a03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,43 @@ -FROM python:3.12.10-alpine3.20 +FROM python:3.12.10-slim-bookworm AS build LABEL maintainer="https://github.com/prowler-cloud/prowler" LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler" -# Update system dependencies and install essential tools -#hadolint ignore=DL3018 -RUN apk --no-cache upgrade && apk --no-cache add curl git gcc python3-dev musl-dev linux-headers +ARG POWERSHELL_VERSION=7.5.0 + +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends wget libicu72 \ + && rm -rf /var/lib/apt/lists/* + +# Install PowerShell +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" = "x86_64" ]; then \ + wget --progress=dot:giga https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell-${POWERSHELL_VERSION}-linux-x64.tar.gz -O /tmp/powershell.tar.gz ; \ + elif [ "$ARCH" = "aarch64" ]; then \ + wget --progress=dot:giga https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell-${POWERSHELL_VERSION}-linux-arm64.tar.gz -O /tmp/powershell.tar.gz ; \ + else \ + echo "Unsupported architecture: $ARCH" && exit 1 ; \ + fi && \ + 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 --gid 1000 prowler && \ + adduser --uid 1000 --gid 1000 --disabled-password --gecos "" prowler -# Create non-root user -RUN mkdir -p /home/prowler && \ - echo 'prowler:x:1000:1000:prowler:/home/prowler:' > /etc/passwd && \ - echo 'prowler:x:1000:' > /etc/group && \ - chown -R prowler:prowler /home/prowler USER prowler -# Copy necessary files WORKDIR /home/prowler + +# Copy necessary files COPY prowler/ /home/prowler/prowler/ COPY dashboard/ /home/prowler/dashboard/ COPY pyproject.toml /home/prowler COPY README.md /home/prowler/ +COPY prowler/providers/m365/lib/powershell/m365_powershell.py /home/prowler/prowler/providers/m365/lib/powershell/m365_powershell.py # Install Python dependencies ENV HOME='/home/prowler' @@ -35,6 +53,9 @@ RUN pip install --no-cache-dir --upgrade pip && \ RUN poetry install --compile && \ rm -rf ~/.cache/pip +# Install PowerShell modules +RUN poetry run python prowler/providers/m365/lib/powershell/m365_powershell.py + # Remove deprecated dash dependencies RUN pip uninstall dash-html-components -y && \ pip uninstall dash-core-components -y diff --git a/api/Dockerfile b/api/Dockerfile index 3569d003e7..cf0fb37556 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,13 +1,33 @@ -FROM python:3.12.8-alpine3.20 AS build +FROM python:3.12.10-slim-bookworm 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 +ARG POWERSHELL_VERSION=7.5.0 +ENV POWERSHELL_VERSION=${POWERSHELL_VERSION} + +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends wget libicu72 \ + && rm -rf /var/lib/apt/lists/* + +# Install PowerShell +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" = "x86_64" ]; then \ + wget --progress=dot:giga https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell-${POWERSHELL_VERSION}-linux-x64.tar.gz -O /tmp/powershell.tar.gz ; \ + elif [ "$ARCH" = "aarch64" ]; then \ + wget --progress=dot:giga https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell-${POWERSHELL_VERSION}-linux-arm64.tar.gz -O /tmp/powershell.tar.gz ; \ + else \ + echo "Unsupported architecture: $ARCH" && exit 1 ; \ + fi && \ + 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 --gid 1000 prowler && \ + adduser --uid 1000 --gid 1000 --disabled-password --gecos "" prowler -RUN apk --no-cache upgrade && \ - addgroup -g 1000 prowler && \ - adduser -D -u 1000 -G prowler prowler USER prowler WORKDIR /home/prowler @@ -27,18 +47,13 @@ RUN poetry install --no-root && \ COPY docker-entrypoint.sh ./docker-entrypoint.sh +RUN poetry run python "$(poetry env info --path)/src/prowler/prowler/providers/m365/lib/powershell/m365_powershell.py" + WORKDIR /home/prowler/backend # Development image -# hadolint ignore=DL3006 FROM build AS dev -USER 0 -# hadolint ignore=DL3018 -RUN apk --no-cache add curl vim - -USER prowler - ENTRYPOINT ["../docker-entrypoint.sh", "dev"] # Production image diff --git a/prowler/lib/powershell/powershell.py b/prowler/lib/powershell/powershell.py index 26cd2fe952..1e13a74306 100644 --- a/prowler/lib/powershell/powershell.py +++ b/prowler/lib/powershell/powershell.py @@ -191,8 +191,11 @@ class PowerShellSession: error_thread.daemon = True error_thread.start() - result = result_queue.get(timeout=timeout) or default - error_result = error_queue.get(timeout=1) + try: + result = result_queue.get(timeout=timeout) or default + error_result = error_queue.get(timeout=1) or None + except queue.Empty: + result = default if error_result: logger.error(f"PowerShell error output: {error_result}") diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 0efde909ef..aaa94b21d1 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -221,6 +221,7 @@ class Provider(ABC): az_cli_auth=arguments.az_cli_auth, browser_auth=arguments.browser_auth, tenant_id=arguments.tenant_id, + init_modules=arguments.init_modules, fixer_config=fixer_config, ) elif "nhn" in provider_class_name.lower(): diff --git a/prowler/providers/m365/lib/arguments/arguments.py b/prowler/providers/m365/lib/arguments/arguments.py index a1980c4bd9..e88ef7982b 100644 --- a/prowler/providers/m365/lib/arguments/arguments.py +++ b/prowler/providers/m365/lib/arguments/arguments.py @@ -35,16 +35,9 @@ def init_parser(self): help="Microsoft 365 Tenant ID to be used with --browser-auth option", ) m365_parser.add_argument( - "--user", - nargs="?", - default=None, - help="Microsoft 365 user email", - ) - m365_parser.add_argument( - "--encypted-password", - nargs="?", - default=None, - help="Microsoft 365 encrypted password", + "--init-modules", + action="store_true", + help="Initialize Microsoft 365 PowerShell modules", ) # Regions m365_regions_subparser = m365_parser.add_argument_group("Regions") diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 91974a0b15..df4a26dfd8 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -2,6 +2,7 @@ import os import msal +from prowler.lib.logger import logger from prowler.lib.powershell.powershell import PowerShellSession from prowler.providers.m365.exceptions.exceptions import ( M365UserNotBelongingToTenantError, @@ -26,6 +27,7 @@ class M365PowerShell(PowerShellSession): Attributes: credentials (M365Credentials): The Microsoft 365 credentials used for authentication. + required_modules (list): List of required PowerShell modules for M365 operations. Note: This class requires the Microsoft Teams and Exchange Online PowerShell modules @@ -371,6 +373,24 @@ class M365PowerShell(PowerShellSession): "Get-MailboxAuditBypassAssociation | ConvertTo-Json", json_parse=True ) + def get_mailbox_policy(self) -> dict: + """ + Get Mailbox Policy. + + Retrieves the current mailbox policy settings for Exchange Online. + + Returns: + dict: Mailbox policy settings in JSON format. + + Example: + >>> get_mailbox_policy() + { + "Id": "OwaMailboxPolicy-Default", + "AdditionalStorageProvidersAvailable": True + } + """ + return self.execute("Get-OwaMailboxPolicy | ConvertTo-Json", json_parse=True) + def get_external_mail_config(self) -> dict: """ Get Exchange Online External Mail Configuration. @@ -467,20 +487,64 @@ class M365PowerShell(PowerShellSession): "Get-HostedContentFilterPolicy | ConvertTo-Json", json_parse=True ) - def get_mailbox_policy(self) -> dict: - """ - Get Mailbox Policy. - Retrieves the current mailbox policy settings for Exchange Online. +# This function is used to install the required M365 PowerShell modules in Docker containers +def initialize_m365_powershell_modules(): + """ + Initialize required PowerShell modules. - Returns: - dict: Mailbox policy settings in JSON format. + Checks if the required PowerShell modules are installed and installs them if necessary. + This method ensures that all required modules for M365 operations are available. - Example: - >>> get_mailbox_policy() - { - "Id": "OwaMailboxPolicy-Default", - "AdditionalStorageProvidersAvailable": True - } - """ - return self.execute("Get-OwaMailboxPolicy | ConvertTo-Json", json_parse=True) + Returns: + bool: True if all modules were successfully initialized, False otherwise + """ + + REQUIRED_MODULES = [ + "ExchangeOnlineManagement", + "MicrosoftTeams", + ] + + pwsh = PowerShellSession() + try: + for module in REQUIRED_MODULES: + try: + # Check if module is already installed + result = pwsh.execute( + f"Get-Module -ListAvailable -Name {module}", timeout=5 + ) + + # Install module if not installed + if not result: + install_result = pwsh.execute( + f'Install-Module -Name "{module}" -Force -AllowClobber -Scope CurrentUser', + timeout=30, + ) + if install_result: + logger.warning( + f"Unexpected output while installing module {module}: {install_result}" + ) + else: + logger.info(f"Successfully installed module {module}") + + # Import module + pwsh.execute(f'Import-Module -Name "{module}" -Force', timeout=1) + + except Exception as error: + logger.error(f"Failed to initialize module {module}: {str(error)}") + return False + + return True + finally: + pwsh.close() + + +def main(): + if initialize_m365_powershell_modules(): + logger.info("M365 PowerShell modules initialized successfully") + else: + logger.error("Failed to initialize M365 PowerShell modules") + + +if __name__ == "__main__": + main() diff --git a/prowler/providers/m365/m365_provider.py b/prowler/providers/m365/m365_provider.py index 4898b5c311..81b359728c 100644 --- a/prowler/providers/m365/m365_provider.py +++ b/prowler/providers/m365/m365_provider.py @@ -53,7 +53,10 @@ from prowler.providers.m365.exceptions.exceptions import ( M365TenantIdAndClientSecretNotBelongingToClientIdError, ) from prowler.providers.m365.lib.mutelist.mutelist import M365Mutelist -from prowler.providers.m365.lib.powershell.m365_powershell import M365PowerShell +from prowler.providers.m365.lib.powershell.m365_powershell import ( + M365PowerShell, + initialize_m365_powershell_modules, +) from prowler.providers.m365.lib.regions.regions import get_regions_config from prowler.providers.m365.models import ( M365Credentials, @@ -115,6 +118,7 @@ class M365Provider(Provider): client_secret: str = None, user: str = None, encrypted_password: str = None, + init_modules: bool = False, region: str = "M365Global", config_content: dict = None, config_path: str = None, @@ -202,6 +206,7 @@ class M365Provider(Provider): env_auth=env_auth, m365_credentials=m365_credentials, provider_id=self.identity.tenant_domain, + init_modules=init_modules, ) # Audit Config @@ -373,7 +378,10 @@ class M365Provider(Provider): @staticmethod def setup_powershell( - env_auth: bool = False, m365_credentials: dict = {}, provider_id: str = None + env_auth: bool = False, + m365_credentials: dict = {}, + provider_id: str = None, + init_modules: bool = False, ) -> M365Credentials: """Gets the M365 credentials. @@ -423,6 +431,8 @@ class M365Provider(Provider): test_session = M365PowerShell(credentials) try: if test_session.test_credentials(credentials): + if init_modules: + initialize_m365_powershell_modules() return credentials raise M365EnvironmentUserCredentialsError( file=os.path.basename(__file__), diff --git a/tests/providers/m365/lib/powershell/m365_powershell_test.py b/tests/providers/m365/lib/powershell/m365_powershell_test.py index 72f5e72810..66094aa581 100644 --- a/tests/providers/m365/lib/powershell/m365_powershell_test.py +++ b/tests/providers/m365/lib/powershell/m365_powershell_test.py @@ -1,7 +1,8 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, call, patch import pytest +from prowler.lib.powershell.powershell import PowerShellSession from prowler.providers.m365.exceptions.exceptions import ( M365UserNotBelongingToTenantError, ) @@ -265,6 +266,7 @@ class Testm365PowerShell: - Error in stderr - Timeout in stdout - Empty output + - Empty queue handling """ # Setup mock_process = MagicMock() @@ -308,6 +310,89 @@ class Testm365PowerShell: result = session.read_output() assert result == "" + # Test 5: Empty queue handling + mock_process.stdout.readline.side_effect = [] # No output at all + mock_process.stderr.readline.return_value = f"Write-Error: {session.END}\n" + result = session.read_output(timeout=0.1, default="empty_queue") + assert result == "empty_queue" + + # Test 6: Empty error queue handling + mock_process.stdout.readline.side_effect = ["test output\n", f"{session.END}\n"] + mock_process.stderr.readline.side_effect = [] # No error output + with patch("prowler.lib.logger.logger.error") as mock_error: + result = session.read_output() + assert result == "test output" + mock_error.assert_not_called() + + # Test 7: Both queues empty + mock_process.stdout.readline.side_effect = [] # No output + mock_process.stderr.readline.side_effect = [] # No error output + result = session.read_output(timeout=0.1, default="both_empty") + assert result == "both_empty" + + session.close() + + @patch("subprocess.Popen") + def test_read_output_queue_empty(self, mock_popen): + """Test read_output when both queues are empty""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + session = M365PowerShell(credentials) + + # Mock process to return empty queues + mock_process.stdout.readline.side_effect = [] # No output + mock_process.stderr.readline.side_effect = [] # No error output + + # Test with default value + result = session.read_output(timeout=0.1, default="empty_queue") + assert result == "empty_queue" + + # Test without default value + result = session.read_output(timeout=0.1) + assert result == "" + + session.close() + + @patch("subprocess.Popen") + def test_read_output_error_queue_empty(self, mock_popen): + """Test read_output when error queue is empty but stdout has content""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + session = M365PowerShell(credentials) + + # Mock process to return content in stdout but empty stderr + mock_process.stdout.readline.side_effect = ["test output\n", f"{session.END}\n"] + mock_process.stderr.readline.side_effect = [] # No error output + + with patch("prowler.lib.logger.logger.error") as mock_error: + result = session.read_output() + assert result == "test output" + mock_error.assert_not_called() + + session.close() + + @patch("subprocess.Popen") + def test_read_output_result_queue_empty(self, mock_popen): + """Test read_output when result queue is empty but stderr has content""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + credentials = M365Credentials(user="test@example.com", passwd="test_password") + session = M365PowerShell(credentials) + + # Mock process to return empty stdout but content in stderr + mock_process.stdout.readline.side_effect = [] # No output + mock_process.stderr.readline.side_effect = [ + "Error message\n", + f"Write-Error: {session.END}\n", + ] + + with patch("prowler.lib.logger.logger.error") as mock_error: + result = session.read_output(timeout=0.1, default="default") + assert result == "default" + mock_error.assert_called_once_with("PowerShell error output: Error message") + session.close() @patch("subprocess.Popen") @@ -346,3 +431,149 @@ class Testm365PowerShell: mock_process.stdin.flush.assert_called_once() mock_process.terminate.assert_called_once() + + @patch("subprocess.Popen") + def test_initialize_m365_powershell_modules_success(self, mock_popen): + """Test initialize_m365_powershell_modules when all modules are successfully initialized""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + # Mock the execute method to simulate successful module installation + def mock_execute(command, *args, **kwargs): + if "Get-Module" in command: + return None # Module not installed + elif "Install-Module" in command: + return None # Installation successful + elif "Import-Module" in command: + return None # Import successful + return None + + with ( + patch.object( + PowerShellSession, "execute", side_effect=mock_execute + ) as mock_execute_obj, + patch("prowler.lib.logger.logger.info") as mock_info, + ): + from prowler.providers.m365.lib.powershell.m365_powershell import ( + initialize_m365_powershell_modules, + ) + + result = initialize_m365_powershell_modules() + + # Verify successful initialization + assert result is True + # Verify that execute was called for each module + assert mock_execute_obj.call_count == 6 # 2 modules * 3 commands each + # Verify success messages were logged + mock_info.assert_any_call( + "Successfully installed module ExchangeOnlineManagement" + ) + mock_info.assert_any_call("Successfully installed module MicrosoftTeams") + + @patch("subprocess.Popen") + def test_initialize_m365_powershell_modules_failure(self, mock_popen): + """Test initialize_m365_powershell_modules when module initialization fails""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + # Mock the execute method to simulate installation failure + def mock_execute(command, *args, **kwargs): + if "Get-Module" in command: + return None # Module not installed + elif "Install-Module" in command: + raise Exception("Installation failed") + return None + + with ( + patch.object( + PowerShellSession, "execute", side_effect=mock_execute + ) as mock_execute_obj, + patch("prowler.lib.logger.logger.error") as mock_error, + ): + from prowler.providers.m365.lib.powershell.m365_powershell import ( + initialize_m365_powershell_modules, + ) + + result = initialize_m365_powershell_modules() + + # Verify failed initialization + assert result is False + # Verify that execute was called at least twice + assert mock_execute_obj.call_count >= 2 + # Verify error was logged + mock_error.assert_called_with( + "Failed to initialize module ExchangeOnlineManagement: Installation failed" + ) + + @patch("subprocess.Popen") + def test_main_success(self, mock_popen): + """Test main() function when module initialization is successful""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + # Mock the execute method to simulate successful module installation + def mock_execute(command, *args, **kwargs): + if "Get-Module" in command: + return None # Module not installed + elif "Install-Module" in command: + return None # Installation successful + elif "Import-Module" in command: + return None # Import successful + return None + + with ( + patch.object(PowerShellSession, "execute", side_effect=mock_execute), + patch("prowler.lib.logger.logger.info") as mock_info, + patch("prowler.lib.logger.logger.error") as mock_error, + ): + from prowler.providers.m365.lib.powershell.m365_powershell import main + + main() + + # Verify all info messages were logged in the correct order + assert mock_info.call_count == 3 + mock_info.assert_has_calls( + [ + call("Successfully installed module ExchangeOnlineManagement"), + call("Successfully installed module MicrosoftTeams"), + call("M365 PowerShell modules initialized successfully"), + ] + ) + # Verify no error was logged + mock_error.assert_not_called() + + @patch("subprocess.Popen") + def test_main_failure(self, mock_popen): + """Test main() function when module initialization fails""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + # Mock the execute method to simulate installation failure + def mock_execute(command, *args, **kwargs): + if "Get-Module" in command: + return None # Module not installed + elif "Install-Module" in command: + raise Exception("Installation failed") + return None + + with ( + patch.object(PowerShellSession, "execute", side_effect=mock_execute), + patch("prowler.lib.logger.logger.info") as mock_info, + patch("prowler.lib.logger.logger.error") as mock_error, + ): + from prowler.providers.m365.lib.powershell.m365_powershell import main + + main() + + # Verify all error messages were logged in the correct order + assert mock_error.call_count == 2 + mock_error.assert_has_calls( + [ + call( + "Failed to initialize module ExchangeOnlineManagement: Installation failed" + ), + call("Failed to initialize M365 PowerShell modules"), + ] + ) + # Verify no info messages were logged + mock_info.assert_not_called() diff --git a/tests/providers/m365/m365_provider_test.py b/tests/providers/m365/m365_provider_test.py index 9fc991b60f..c891ab2d45 100644 --- a/tests/providers/m365/m365_provider_test.py +++ b/tests/providers/m365/m365_provider_test.py @@ -449,9 +449,11 @@ class TestM365Provider: "client_secret": "test_client_secret", } - with patch( - "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials", - return_value=True, + with ( + patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials", + return_value=True, + ), ): result = M365Provider.setup_powershell( env_auth=False, @@ -622,3 +624,87 @@ class TestM365Provider: f"Provider ID {provider_id} does not match Application tenant domain {user_domain}" in str(exception.value) ) + + def test_provider_init_modules_false(self): + """Test that initialize_m365_powershell_modules is not called when init_modules is False""" + credentials_dict = { + "user": "test@example.com", + "encrypted_password": "test_password", + "client_id": "test_client_id", + "tenant_id": "test_tenant_id", + "client_secret": "test_client_secret", + } + + with ( + patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials", + return_value=True, + ), + patch( + "prowler.providers.m365.m365_provider.initialize_m365_powershell_modules" + ) as mock_init_modules, + ): + M365Provider.setup_powershell( + env_auth=False, + m365_credentials=credentials_dict, + provider_id="test_provider_id", + init_modules=False, + ) + mock_init_modules.assert_not_called() + + def test_provider_init_modules_true(self): + """Test that initialize_m365_powershell_modules is called when init_modules is True""" + credentials_dict = { + "user": "test@example.com", + "encrypted_password": "test_password", + "client_id": "test_client_id", + "tenant_id": "test_tenant_id", + "client_secret": "test_client_secret", + } + + with ( + patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials", + return_value=True, + ), + patch( + "prowler.providers.m365.m365_provider.initialize_m365_powershell_modules" + ) as mock_init_modules, + ): + M365Provider.setup_powershell( + env_auth=False, + m365_credentials=credentials_dict, + provider_id="test_provider_id", + init_modules=True, + ) + mock_init_modules.assert_called_once() + + def test_setup_powershell_init_modules_failure(self): + """Test that setup_powershell handles initialization failures correctly""" + credentials_dict = { + "user": "test@example.com", + "encrypted_password": "test_password", + "client_id": "test_client_id", + "tenant_id": "test_tenant_id", + "client_secret": "test_client_secret", + } + + with ( + patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials", + return_value=True, + ), + patch( + "prowler.providers.m365.m365_provider.initialize_m365_powershell_modules", + side_effect=Exception("Module initialization failed"), + ), + ): + with pytest.raises(Exception) as exc_info: + M365Provider.setup_powershell( + env_auth=False, + m365_credentials=credentials_dict, + provider_id="test_provider_id", + init_modules=True, + ) + + assert str(exc_info.value) == "Module initialization failed"