Files
prowler/tests/providers/m365/m365_provider_test.py
2025-05-23 12:27:57 +02:00

816 lines
31 KiB
Python

import os
from unittest.mock import patch
from uuid import uuid4
import pytest
from azure.core.credentials import AccessToken
from azure.identity import (
ClientSecretCredential,
DefaultAzureCredential,
InteractiveBrowserCredential,
)
from mock import MagicMock
from prowler.config.config import (
default_config_file_path,
default_fixer_config_file_path,
load_and_validate_config_file,
)
from prowler.providers.common.models import Connection
from prowler.providers.m365.exceptions.exceptions import (
M365HTTPResponseError,
M365InvalidProviderIdError,
M365MissingEnvironmentCredentialsError,
M365NoAuthenticationMethodError,
M365NotValidClientIdError,
M365NotValidClientSecretError,
M365NotValidPasswordError,
M365NotValidTenantIdError,
M365NotValidUserError,
M365UserNotBelongingToTenantError,
)
from prowler.providers.m365.m365_provider import M365Provider
from prowler.providers.m365.models import (
M365Credentials,
M365IdentityInfo,
M365RegionConfig,
)
from tests.providers.m365.m365_fixtures import (
CLIENT_ID,
CLIENT_SECRET,
DOMAIN,
IDENTITY_ID,
IDENTITY_TYPE,
LOCATION,
TENANT_ID,
)
class TestM365Provider:
def test_m365_provider(self):
tenant_id = None
client_id = None
client_secret = None
fixer_config = load_and_validate_config_file(
"m365", default_fixer_config_file_path
)
azure_region = "M365Global"
with (
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_session",
return_value=ClientSecretCredential(
client_id=CLIENT_ID,
tenant_id=TENANT_ID,
client_secret=CLIENT_SECRET,
),
),
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_identity",
return_value=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type=IDENTITY_TYPE,
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
location=LOCATION,
),
),
):
m365_provider = M365Provider(
sp_env_auth=True,
az_cli_auth=False,
browser_auth=False,
env_auth=False,
tenant_id=tenant_id,
client_id=client_id,
client_secret=client_secret,
region=azure_region,
config_path=default_config_file_path,
fixer_config=fixer_config,
)
assert m365_provider.region_config == M365RegionConfig(
name="M365Global",
authority=None,
base_url="https://graph.microsoft.com",
credential_scopes=["https://graph.microsoft.com/.default"],
)
assert m365_provider.identity == M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type=IDENTITY_TYPE,
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
location=LOCATION,
)
def test_m365_provider_env_auth(self):
tenant_id = None
client_id = None
client_secret = None
fixer_config = load_and_validate_config_file(
"m365", default_fixer_config_file_path
)
azure_region = "M365Global"
with (
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_session",
return_value=ClientSecretCredential(
client_id=CLIENT_ID,
tenant_id=TENANT_ID,
client_secret=CLIENT_SECRET,
),
),
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_identity",
return_value=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type=IDENTITY_TYPE,
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
location=LOCATION,
),
),
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_powershell",
return_value=M365Credentials(
user="test@test.com",
passwd="password",
),
),
):
m365_provider = M365Provider(
sp_env_auth=False,
az_cli_auth=False,
browser_auth=False,
env_auth=True,
tenant_id=tenant_id,
client_id=client_id,
client_secret=client_secret,
region=azure_region,
config_path=default_config_file_path,
fixer_config=fixer_config,
)
assert m365_provider.region_config == M365RegionConfig(
name="M365Global",
authority=None,
base_url="https://graph.microsoft.com",
credential_scopes=["https://graph.microsoft.com/.default"],
)
assert m365_provider.identity == M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type=IDENTITY_TYPE,
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
location=LOCATION,
)
def test_m365_provider_cli_auth(self):
"""Test M365 Provider initialization with CLI authentication"""
azure_region = "M365Global"
fixer_config = load_and_validate_config_file(
"m365", default_fixer_config_file_path
)
with (
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_session",
return_value=DefaultAzureCredential(
exclude_environment_credential=True,
exclude_cli_credential=False,
exclude_managed_identity_credential=True,
exclude_visual_studio_code_credential=True,
exclude_shared_token_cache_credential=True,
exclude_powershell_credential=True,
exclude_browser_credential=True,
),
),
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_identity",
return_value=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
location=LOCATION,
),
),
):
m365_provider = M365Provider(
sp_env_auth=False,
az_cli_auth=True,
browser_auth=False,
env_auth=False,
region=azure_region,
config_path=default_config_file_path,
fixer_config=fixer_config,
)
assert m365_provider.region_config == M365RegionConfig(
name="M365Global",
authority=None,
base_url="https://graph.microsoft.com",
credential_scopes=["https://graph.microsoft.com/.default"],
)
assert m365_provider.identity == M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
location=LOCATION,
)
assert isinstance(m365_provider.session, DefaultAzureCredential)
def test_m365_provider_browser_auth(self):
"""Test M365 Provider initialization with Browser authentication"""
azure_region = "M365Global"
fixer_config = load_and_validate_config_file(
"m365", default_fixer_config_file_path
)
with (
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_session",
return_value=InteractiveBrowserCredential(
tenant_id=TENANT_ID,
),
),
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_identity",
return_value=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
location=LOCATION,
),
),
):
m365_provider = M365Provider(
sp_env_auth=False,
az_cli_auth=False,
browser_auth=True,
env_auth=False,
tenant_id=TENANT_ID,
region=azure_region,
config_path=default_config_file_path,
fixer_config=fixer_config,
)
assert m365_provider.region_config == M365RegionConfig(
name="M365Global",
authority=None,
base_url="https://graph.microsoft.com",
credential_scopes=["https://graph.microsoft.com/.default"],
)
assert m365_provider.identity == M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
location=LOCATION,
)
assert isinstance(m365_provider.session, InteractiveBrowserCredential)
def test_test_connection_browser_auth(self):
with (
patch(
"prowler.providers.m365.m365_provider.DefaultAzureCredential"
) as mock_default_credential,
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_session"
) as mock_setup_session,
patch(
"prowler.providers.m365.m365_provider.GraphServiceClient"
) as mock_graph_client,
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_identity",
return_value=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
tenant_domains=["test.onmicrosoft.com"],
location=LOCATION,
),
),
):
# Mock the return value of DefaultAzureCredential
mock_credentials = MagicMock()
mock_credentials.get_token.return_value = AccessToken(
token="fake_token", expires_on=9999999999
)
mock_default_credential.return_value = mock_credentials
# Mock setup_session to return a mocked session object
mock_session = MagicMock()
mock_setup_session.return_value = mock_session
# Mock GraphServiceClient
mock_client = MagicMock()
mock_graph_client.return_value = mock_client
test_connection = M365Provider.test_connection(
browser_auth=True,
tenant_id=str(uuid4()),
region="M365Global",
raise_on_exception=False,
provider_id="test.onmicrosoft.com",
)
assert isinstance(test_connection, Connection)
assert test_connection.is_connected
assert test_connection.error is None
def test_test_connection_tenant_id_client_id_client_secret(self):
with (
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_session"
) as mock_setup_session,
patch(
"prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials"
) as mock_validate_static_credentials,
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_identity",
return_value=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
tenant_domains=["test.onmicrosoft.com"],
location=LOCATION,
),
),
):
# Mock setup_session to return a mocked session object
mock_session = MagicMock()
mock_setup_session.return_value = mock_session
# Mock ValidateStaticCredentials to avoid real API calls
mock_validate_static_credentials.return_value = None
test_connection = M365Provider.test_connection(
tenant_id=str(uuid4()),
region="M365Global",
raise_on_exception=False,
client_id=str(uuid4()),
client_secret=str(uuid4()),
provider_id="test.onmicrosoft.com",
)
assert isinstance(test_connection, Connection)
assert test_connection.is_connected
assert test_connection.error is None
def test_test_connection_tenant_id_client_id_client_secret_no_user_password(
self,
):
with patch(
"prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials"
) as mock_validate_static_credentials:
mock_validate_static_credentials.side_effect = M365NotValidUserError(
file=os.path.basename(__file__),
message="The provided M365 User is not valid.",
)
with pytest.raises(M365NotValidUserError) as exception:
M365Provider.test_connection(
tenant_id=str(uuid4()),
region="M365Global",
raise_on_exception=True,
client_id=str(uuid4()),
client_secret=str(uuid4()),
user=None,
password="test_password",
)
assert exception.type == M365NotValidUserError
assert "The provided M365 User is not valid." in str(exception.value)
def test_test_connection_tenant_id_client_id_client_secret_user_no_password(
self,
):
with patch(
"prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials"
) as mock_validate_static_credentials:
mock_validate_static_credentials.side_effect = M365NotValidPasswordError(
file=os.path.basename(__file__),
message="The provided M365 Password is not valid.",
)
with pytest.raises(M365NotValidPasswordError) as exception:
M365Provider.test_connection(
tenant_id=str(uuid4()),
region="M365Global",
raise_on_exception=True,
client_id=str(uuid4()),
client_secret=str(uuid4()),
user="test@example.com",
password=None,
)
assert exception.type == M365NotValidPasswordError
assert "The provided M365 Password is not valid." in str(exception.value)
def test_test_connection_with_httpresponseerror(self):
with patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_session"
) as mock_setup_session:
mock_setup_session.side_effect = M365HTTPResponseError(
file="test_file", original_exception="Simulated HttpResponseError"
)
with pytest.raises(M365HTTPResponseError) as exception:
M365Provider.test_connection(
az_cli_auth=True,
raise_on_exception=True,
)
assert exception.type == M365HTTPResponseError
assert (
exception.value.args[0]
== "[6003] Error in HTTP response from Microsoft 365 - Simulated HttpResponseError"
)
def test_test_connection_with_exception(self):
with patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_session"
) as mock_setup_session:
mock_setup_session.side_effect = Exception("Simulated Exception")
with pytest.raises(Exception) as exception:
M365Provider.test_connection(
sp_env_auth=True,
raise_on_exception=True,
)
assert exception.type is Exception
assert exception.value.args[0] == "Simulated Exception"
def test_test_connection_without_any_method(self):
with pytest.raises(M365NoAuthenticationMethodError) as exception:
M365Provider.test_connection()
assert exception.type == M365NoAuthenticationMethodError
assert (
"M365 provider requires at least one authentication method set: [--env-auth | --az-cli-auth | --sp-env-auth | --browser-auth]"
in exception.value.args[0]
)
def test_setup_powershell_valid_credentials(self):
credentials_dict = {
"user": "test@example.com",
"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,
),
):
result = M365Provider.setup_powershell(
env_auth=False,
m365_credentials=credentials_dict,
identity=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
tenant_domains=["test.onmicrosoft.com"],
location=LOCATION,
),
)
assert result.user == credentials_dict["user"]
assert result.passwd == credentials_dict["password"]
def test_setup_powershell_invalid_env_credentials(self):
credentials = None
with patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell"
) as mock_powershell:
mock_session = MagicMock()
mock_session.test_credentials.return_value = False
mock_powershell.return_value = mock_session
with pytest.raises(M365MissingEnvironmentCredentialsError) as exc_info:
M365Provider.setup_powershell(
env_auth=True, m365_credentials=credentials
)
assert (
"Missing M365_USER or M365_PASSWORD environment variables required for credentials authentication"
in str(exc_info.value)
)
mock_session.test_credentials.assert_not_called()
def test_test_connection_user_not_belonging_to_tenant(
self,
):
with patch(
"prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials"
) as mock_validate_static_credentials:
mock_validate_static_credentials.side_effect = M365UserNotBelongingToTenantError(
file=os.path.basename(__file__),
message="The provided M365 User does not belong to the specified tenant.",
)
with pytest.raises(M365UserNotBelongingToTenantError) as exception:
M365Provider.test_connection(
tenant_id="contoso.onmicrosoft.com",
region="M365Global",
raise_on_exception=True,
client_id=str(uuid4()),
client_secret=str(uuid4()),
user="user@otherdomain.com",
password="test_password",
)
assert exception.type == M365UserNotBelongingToTenantError
assert (
"The provided M365 User does not belong to the specified tenant."
in str(exception.value)
)
def test_validate_static_credentials_invalid_tenant_id(self):
with pytest.raises(M365NotValidTenantIdError) as exception:
M365Provider.validate_static_credentials(
tenant_id="invalid-tenant-id",
client_id="12345678-1234-5678-1234-567812345678",
client_secret="test_secret",
user="test@example.com",
password="test_password",
)
assert "The provided Tenant ID is not valid." in str(exception.value)
def test_validate_static_credentials_missing_client_id(self):
with pytest.raises(M365NotValidClientIdError) as exception:
M365Provider.validate_static_credentials(
tenant_id="12345678-1234-5678-1234-567812345678",
client_id="",
client_secret="test_secret",
user="test@example.com",
password="test_password",
)
assert "The provided Client ID is not valid." in str(exception.value)
def test_validate_static_credentials_missing_client_secret(self):
with pytest.raises(M365NotValidClientSecretError) as exception:
M365Provider.validate_static_credentials(
tenant_id="12345678-1234-5678-1234-567812345678",
client_id="12345678-1234-5678-1234-567812345678",
client_secret="",
user="test@example.com",
password="test_password",
)
assert "The provided Client Secret is not valid." in str(exception.value)
def test_validate_static_credentials_missing_user(self):
with pytest.raises(M365NotValidUserError) as exception:
M365Provider.validate_static_credentials(
tenant_id="12345678-1234-5678-1234-567812345678",
client_id="12345678-1234-5678-1234-567812345678",
client_secret="test_secret",
user="",
password="test_password",
)
assert "The provided User is not valid." in str(exception.value)
def test_validate_static_credentials_missing_password(self):
with pytest.raises(M365NotValidPasswordError) as exception:
M365Provider.validate_static_credentials(
tenant_id="12345678-1234-5678-1234-567812345678",
client_id="12345678-1234-5678-1234-567812345678",
client_secret="test_secret",
user="test@example.com",
password="",
)
assert "The provided Password is not valid." in str(exception.value)
def test_validate_arguments_missing_env_credentials(self):
with pytest.raises(M365MissingEnvironmentCredentialsError) as exception:
M365Provider.validate_arguments(
az_cli_auth=False,
sp_env_auth=False,
env_auth=True,
browser_auth=False,
tenant_id=None,
client_id="test_client_id",
client_secret="test_secret",
user=None,
password=None,
)
assert (
"M365 provider requires AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, M365_USER and M365_PASSWORD environment variables to be set when using --env-auth"
in str(exception.value)
)
def test_test_connection_invalid_provider_id(self):
with (
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_session"
) as mock_setup_session,
patch(
"prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials"
) as mock_validate_static_credentials,
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_identity",
return_value=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain="contoso.com",
tenant_domains=["contoso.com"],
location=LOCATION,
),
),
):
# Mock setup_session to return a mocked session object
mock_session = MagicMock()
mock_setup_session.return_value = mock_session
# Mock ValidateStaticCredentials to avoid real API calls
mock_validate_static_credentials.return_value = None
user_domain = "contoso.com"
provider_id = "Test.com"
with pytest.raises(M365InvalidProviderIdError) as exception:
M365Provider.test_connection(
tenant_id=str(uuid4()),
region="M365Global",
raise_on_exception=True,
client_id=str(uuid4()),
client_secret=str(uuid4()),
user=f"user@{user_domain}",
password="test_password",
provider_id=provider_id,
)
assert exception.type == M365InvalidProviderIdError
assert (
f"The provider ID {provider_id} does not match any of the service principal tenant domains: {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",
"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,
identity=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
tenant_domains=["test.onmicrosoft.com"],
location=LOCATION,
),
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",
"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,
identity=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
tenant_domains=["test.onmicrosoft.com"],
location=LOCATION,
),
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",
"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,
identity=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
tenant_domains=["test.onmicrosoft.com"],
location=LOCATION,
),
init_modules=True,
)
assert str(exc_info.value) == "Module initialization failed"
def test_test_connection_provider_id_not_in_tenant_domains(self):
"""Test that an exception is raised when provider_id is not in tenant_domains"""
with (
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_session"
) as mock_setup_session,
patch(
"prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials"
) as mock_validate_static_credentials,
patch(
"prowler.providers.m365.m365_provider.M365Provider.setup_identity",
return_value=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain="contoso.onmicrosoft.com",
tenant_domains=["contoso.onmicrosoft.com", "contoso.com"],
location=LOCATION,
),
),
):
# Mock setup_session to return a mocked session object
mock_session = MagicMock()
mock_setup_session.return_value = mock_session
# Mock ValidateStaticCredentials to avoid real API calls
mock_validate_static_credentials.return_value = None
provider_id = "test.onmicrosoft.com"
with pytest.raises(M365InvalidProviderIdError) as exception:
M365Provider.test_connection(
tenant_id=str(uuid4()),
region="M365Global",
raise_on_exception=True,
client_id=str(uuid4()),
client_secret=str(uuid4()),
user="user@contoso.onmicrosoft.com",
password="test_password",
provider_id=provider_id,
)
assert exception.type == M365InvalidProviderIdError
assert (
f"The provider ID {provider_id} does not match any of the service principal tenant domains: contoso.onmicrosoft.com, contoso.com"
in str(exception.value)
)