diff --git a/docs/getting-started/requirements.md b/docs/getting-started/requirements.md index ee8ca59ef9..3f017a6735 100644 --- a/docs/getting-started/requirements.md +++ b/docs/getting-started/requirements.md @@ -168,7 +168,9 @@ export M365_PASSWORD="6500780061006d0070006c006500700061007300730077006f00720064 These two new environment variables are **required** to execute the PowerShell modules needed to retrieve information from M365 services. Prowler uses Service Principal authentication to access Microsoft Graph and user credentials to authenticate to Microsoft PowerShell modules. -- `M365_USER` should be your Microsoft account email using the default domain. This means it must look like `example@YourCompany.onmicrosoft.com`. +- `M365_USER` should be your Microsoft account email using the **assigned domain in the tenant**. This means it must look like `example@YourCompany.onmicrosoft.com` or `example@YourCompany.com`, but it must be the exact domain assigned to that user in the tenant. + ???+ warning + If the user is newly created, you need to sign in with that account first, as Microsoft will prompt you to change the password. If you don’t complete this step, user authentication will fail because Microsoft marks the initial password as expired. To ensure that you are using the default domain you can see how to verify it [here](../tutorials/microsoft365/getting-started-m365.md#step-1-obtain-your-domain). diff --git a/docs/tutorials/microsoft365/getting-started-m365.md b/docs/tutorials/microsoft365/getting-started-m365.md index bf9d6b0664..91ed35116a 100644 --- a/docs/tutorials/microsoft365/getting-started-m365.md +++ b/docs/tutorials/microsoft365/getting-started-m365.md @@ -172,6 +172,9 @@ Follow these steps to assign the role: ![Grant Admin Consent for Role](./img/grant-admin-consent-for-role.png) +???+ warning + Remember that if the user is newly created, you need to sign in with that account first, as Microsoft will prompt you to change the password. If you don’t complete this step, user authentication will fail because Microsoft marks the initial password as expired. + --- ### Get your encrypted password diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index e46e41c7e4..3262cb5744 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -161,10 +161,12 @@ class M365PowerShell(PowerShellSession): ) if result is None: - return False + raise Exception( + "Unexpected error: Acquiring token in behalf of user did not return a result." + ) if "access_token" not in result: - return False + raise Exception(f"MsGraph Error {result.get('error_description')}") return True diff --git a/prowler/providers/m365/m365_provider.py b/prowler/providers/m365/m365_provider.py index e37d9d73cd..9d30bd26be 100644 --- a/prowler/providers/m365/m365_provider.py +++ b/prowler/providers/m365/m365_provider.py @@ -196,6 +196,8 @@ class M365Provider(Provider): self._identity = self.setup_identity( sp_env_auth, env_auth, + browser_auth, + az_cli_auth, self._session, ) @@ -505,6 +507,7 @@ class M365Provider(Provider): Exception: If failed to retrieve M365 credentials. """ + logger.info("M365 provider: Setting up session...") if not browser_auth: if sp_env_auth or env_auth: try: @@ -717,6 +720,8 @@ class M365Provider(Provider): identity = M365Provider.setup_identity( sp_env_auth, env_auth, + browser_auth, + az_cli_auth, session, ) @@ -732,6 +737,8 @@ class M365Provider(Provider): message=f"The provider ID {provider_id} does not match any of the service principal tenant domains: {', '.join(identity.tenant_domains)}", ) + logger.info("M365 provider: Identity retrieved successfully") + # Set up PowerShell credentials if user and password: M365Provider.setup_powershell( @@ -739,13 +746,12 @@ class M365Provider(Provider): m365_credentials, identity, ) + logger.info("M365 provider: Connection to PowerShell successful") else: logger.info( "M365 provider: Connection to PowerShell has not been requested" ) - logger.info("M365 provider: Connection to PowerShell successful") - return Connection(is_connected=True) # Exceptions from setup_region_config @@ -876,6 +882,8 @@ class M365Provider(Provider): def setup_identity( sp_env_auth, env_auth, + browser_auth, + az_cli_auth, session, ): """ @@ -891,7 +899,7 @@ class M365Provider(Provider): Returns: M365IdentityInfo: An instance of M365IdentityInfo containing the identity information. """ - logger.info("M365 provider: Setting up identity ...") + logger.info("M365 provider: Setting up identity...") # TODO: fill this object with real values not default and set to none identity = M365IdentityInfo() @@ -943,7 +951,7 @@ class M365Provider(Provider): identity.identity_type = "Service Principal" elif env_auth: identity.identity_type = "Service Principal and User Credentials" - else: + elif browser_auth or az_cli_auth: identity.identity_type = "User" try: logger.info( diff --git a/tests/providers/m365/lib/powershell/m365_powershell_test.py b/tests/providers/m365/lib/powershell/m365_powershell_test.py index 739dd7e8ce..1fa4753dad 100644 --- a/tests/providers/m365/lib/powershell/m365_powershell_test.py +++ b/tests/providers/m365/lib/powershell/m365_powershell_test.py @@ -257,7 +257,70 @@ class Testm365PowerShell: session.process.stdin.write = MagicMock() session.read_output = MagicMock(return_value="decrypted_password") - assert session.test_credentials(credentials) is False + with pytest.raises(Exception) as exc_info: + session.test_credentials(credentials) + assert ( + "Unexpected error: Acquiring token in behalf of user did not return a result." + in str(exc_info.value) + ) + + mock_msal.assert_called_once_with( + client_id="test_client_id", + client_credential="test_client_secret", + authority="https://login.microsoftonline.com/test_tenant_id", + ) + mock_msal_instance.acquire_token_by_username_password.assert_called_once_with( + username="test@contoso.onmicrosoft.com", + password="test_password", + scopes=["https://graph.microsoft.com/.default"], + ) + + session.close() + + @patch("subprocess.Popen") + @patch("msal.ConfidentialClientApplication") + def test_test_credentials_auth_failure_no_access_token(self, mock_msal, mock_popen): + mock_process = MagicMock() + mock_popen.return_value = mock_process + mock_msal_instance = MagicMock() + mock_msal.return_value = mock_msal_instance + mock_msal_instance.acquire_token_by_username_password.return_value = { + "error_description": "invalid_grant: authentication failed" + } + + credentials = M365Credentials( + user="test@contoso.onmicrosoft.com", + passwd="test_password", + encrypted_passwd="test_encrypted_password", + client_id="test_client_id", + client_secret="test_client_secret", + tenant_id="test_tenant_id", + ) + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="User", + tenant_id="test_tenant", + tenant_domain="contoso.onmicrosoft.com", + tenant_domains=["contoso.onmicrosoft.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + + # Mock the execute method to return the decrypted password + def mock_execute(command, *args, **kwargs): + if "Write-Output" in command: + return "decrypted_password" + return None + + session.execute = MagicMock(side_effect=mock_execute) + session.process.stdin.write = MagicMock() + session.read_output = MagicMock(return_value="decrypted_password") + + with pytest.raises(Exception) as exc_info: + session.test_credentials(credentials) + assert "MsGraph Error invalid_grant: authentication failed" in str( + exc_info.value + ) mock_msal.assert_called_once_with( client_id="test_client_id",