chore(m365): deprecate user auth (#8865)

This commit is contained in:
Hugo Pereira Brito
2025-10-09 12:24:24 +02:00
committed by GitHub
parent 1a7f52fc9c
commit ecf749fce8
15 changed files with 200 additions and 1199 deletions

View File

@@ -182,9 +182,6 @@ Microsoft 365 requires specifying the auth method:
# To use service principal authentication for MSGraph and PowerShell modules
prowler m365 --sp-env-auth
# To use both service principal (for MSGraph) and user credentials (for PowerShell modules)
prowler m365 --env-auth
# To use az cli authentication
prowler m365 --az-cli-auth

View File

@@ -5,17 +5,13 @@ Prowler for Microsoft 365 supports multiple authentication types. Authentication
**Prowler App:**
- [**Service Principal Application**](#service-principal-authentication-recommended) (**Recommended**)
- [**Service Principal with User Credentials**](#service-principal-and-user-credentials-authentication) (Being deprecated)
- [**Service Principal with User Credentials**](#service-principal-and-user-credentials-authentication) (Deprecated)
**Prowler CLI:**
- [**Service Principal Application**](#service-principal-authentication-recommended) (**Recommended**)
- [**Service Principal with User Credentials**](#service-principal-and-user-credentials-authentication) (Being deprecated)
- [**Interactive browser authentication**](#interactive-browser-authentication)
???+ warning
The Service Principal with User Credentials method will be deprecated in October 2025 when Microsoft enforces MFA in all tenants, which will not allow user authentication without interactive methods.
## Required Permissions
To run the full Prowler provider, including PowerShell checks, two types of permission scopes must be set in **Microsoft Entra ID**.
@@ -30,7 +26,6 @@ When using service principal authentication, add these **Application Permissions
- `Directory.Read.All`: Required for all services.
- `Policy.Read.All`: Required for all services.
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
- `User.Read` (IMPORTANT: this must be set as **delegated**): Required for the sign-in.
**External API Permissions:**
@@ -43,20 +38,6 @@ When using service principal authentication, add these **Application Permissions
???+ note
This is the **recommended authentication method** because it allows running the full M365 provider including PowerShell checks, providing complete coverage of all available security checks.
### Service Principal + User Credentials Authentication Permissions
When using service principal with user credentials authentication, you need **both** sets of permissions:
**1. Service Principal Application Permissions**:
- All the Microsoft Graph API permissions listed above are required.
- External API permissions listed above are **not needed**.
**2. User-Level Permissions**: These are set at the `M365_USER` level, so the user used to run Prowler must have one of the following roles:
- `Global Reader` (recommended): Allows reading all required information.
- `Exchange Administrator` and `Teams Administrator`: User needs both roles for the same access as Global Reader.
### Browser Authentication Permissions
When using browser authentication, permissions are delegated to the user, so the user must have the appropriate permissions rather than the application.
@@ -144,30 +125,6 @@ When using browser authentication, permissions are delegated to the user, so the
![Grant Admin Consent](../microsoft365/img/grant-external-api-permissions.png)
#### Assign User Roles (For User Authentication)
When using Service Principal with User Credentials authentication, assign the following roles to the user:
1. Go to Users > All Users > Click on the email for the user
![User Overview](../microsoft365/img/user-info-page.png)
2. Click "Assigned Roles"
![User Roles](../microsoft365/img/user-role-page.png)
3. Click "Add assignments", then search and select:
- `Global Reader` (recommended)
- OR `Exchange Administrator` and `Teams Administrator` (both required)
![Global Reader Screenshots](../microsoft365/img/global-reader.png)
4. Click next, assign the role as "Active", and click "Assign"
![Grant Admin Consent for Role](../microsoft365/img/grant-admin-consent-for-role.png)
---
## Service Principal Authentication (Recommended)
@@ -192,48 +149,6 @@ If the external API permissions described in the mentioned section above are not
???+ note
In order to scan all the checks from M365 required permissions to the service principal application must be added. Refer to the [PowerShell Module Permissions](#grant-powershell-module-permissions-for-service-principal-authentication) section for more information.
## Service Principal and User Credentials Authentication
*Available for both Prowler App and Prowler CLI*
**Authentication flag for CLI:** `--env-auth`
???+ warning
This method is not recommended and will be deprecated in October 2025. Use the **Service Principal Application** authentication method instead.
This method builds upon Service Principal authentication by adding User Credentials. Configure the following environment variables: `M365_USER` and `M365_PASSWORD`.
```console
export AZURE_CLIENT_ID="XXXXXXXXX"
export AZURE_CLIENT_SECRET="XXXXXXXXX"
export AZURE_TENANT_ID="XXXXXXXXX"
export M365_USER="your_email@example.com"
export M365_PASSWORD="examplepassword"
```
These two new environment variables are **required** in this authentication method 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 **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
Newly created users must sign in with the account first, as Microsoft prompts for password change. Without completing this step, user authentication fails because Microsoft marks the initial password as expired.
???+ warning
The user must not be MFA capable. Microsoft does not allow MFA capable users to authenticate programmatically. See [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-acquire-token-username-password?tabs=dotnet) for more information.
???+ warning
Using a tenant domain other than the one assigned — even if it belongs to the same tenant — will cause Prowler to fail, as Microsoft authentication will not succeed.
Ensure the correct domain is used for the authenticating user.
![User Domains](img/user-domains.png)
- `M365_PASSWORD` must be the user password.
???+ note
Previously an encrypted password was required, but now the user password is accepted directly. Prowler handles the password encryption.
## Interactive Browser Authentication
@@ -471,7 +386,7 @@ The required modules are automatically installed when running Prowler with the `
Example command:
```console
python3 prowler-cli.py m365 --verbose --log-level ERROR --env-auth --init-modules
python3 prowler-cli.py m365 --verbose --log-level ERROR --sp-env-auth --init-modules
```
If the modules are already installed, running this command will not cause issues—it will simply verify that the necessary modules are available.

View File

@@ -55,11 +55,6 @@ Configure authentication for Microsoft 365 by following the [Microsoft 365 Authe
- Tenant ID
- `AZURE_CLIENT_SECRET` from the Service Principal setup
If using user authentication, also add:
- `M365_USER` (email using the assigned domain in tenant)
- `M365_PASSWORD` (user password)
![Prowler Cloud M365 Credentials](./img/m365-credentials.png)
3. Click "Next"
@@ -85,7 +80,6 @@ PowerShell 7.4+ is required for comprehensive Microsoft 365 security coverage. I
Select an authentication method from the [Microsoft 365 Authentication](authentication.md) guide:
- **Service Principal Application** (recommended): `--sp-env-auth`
- **Service Principal with User Credentials**: `--env-auth`
- **Interactive Browser Authentication**: `--browser-auth`
### Basic Usage

View File

@@ -194,10 +194,10 @@ If you are adding an **EKS**, **GKE**, **AKS** or external cluster, follow these
For M365, you must enter your Domain ID and choose the authentication method you want to use:
- Service Principal Authentication (Recommended)
- User Authentication (only works if the user does not have MFA enabled)
- User Authentication (Deprecated)
???+ note
User authentication with M365_USER and M365_PASSWORD is optional and will only work if the account does not enforce MFA.
???+ warning
User authentication with M365_USER and M365_PASSWORD is deprecated and will be removed.
For full setup instructions and requirements, check the [Microsoft 365 provider requirements](./microsoft365/getting-started-m365.md).

View File

@@ -30,6 +30,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS Athena service metadata to new format [(#8790)](https://github.com/prowler-cloud/prowler/pull/8790)
- Update AWS CloudFormation service metadata to new format [(#8828)](https://github.com/prowler-cloud/prowler/pull/8828)
- Update AWS Lambda service metadata to new format [(#8825)](https://github.com/prowler-cloud/prowler/pull/8825)
- Deprecate user authentication for M365 provider [(#8865)](https://github.com/prowler-cloud/prowler/pull/8865)
### Fixed
- Fix SNS topics showing empty AWS_ResourceID in Quick Inventory output [(#8762)](https://github.com/prowler-cloud/prowler/issues/8762)

View File

@@ -220,7 +220,6 @@ class Provider(ABC):
config_path=arguments.config_file,
mutelist_path=arguments.mutelist_file,
sp_env_auth=arguments.sp_env_auth,
env_auth=arguments.env_auth,
az_cli_auth=arguments.az_cli_auth,
browser_auth=arguments.browser_auth,
certificate_auth=arguments.certificate_auth,

View File

@@ -94,26 +94,6 @@ class M365BaseException(ProwlerException):
"message": "Tenant Id is required for Microsoft 365 static credentials. Make sure you are using the correct credentials.",
"remediation": "Check the Microsoft 365 Tenant ID and ensure it is properly set up.",
},
(6022, "M365MissingEnvironmentCredentialsError"): {
"message": "User and Password environment variables are needed to use Credentials authentication method.",
"remediation": "Ensure your environment variables are properly set up.",
},
(6023, "M365UserCredentialsError"): {
"message": "The provided User credentials are not valid.",
"remediation": "Check the User credentials and ensure they are valid.",
},
(6024, "M365NotValidUserError"): {
"message": "The provided User is not valid.",
"remediation": "Check the User and ensure it is a valid user.",
},
(6025, "M365NotValidPasswordError"): {
"message": "The provided Password is not valid.",
"remediation": "Check the Password and ensure it is a valid password.",
},
(6026, "M365UserNotBelongingToTenantError"): {
"message": "The provided User does not belong to the specified tenant.",
"remediation": "Check the User email domain and ensure it belongs to the specified tenant.",
},
(6027, "M365GraphConnectionError"): {
"message": "Failed to establish connection to Microsoft Graph API.",
"remediation": "Check your Microsoft Application credentials and ensure the app has proper permissions.",
@@ -315,41 +295,6 @@ class M365NotTenantIdButClientIdAndClientSecretError(M365CredentialsError):
)
class M365MissingEnvironmentCredentialsError(M365CredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
6022, file=file, original_exception=original_exception, message=message
)
class M365UserCredentialsError(M365CredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
6023, file=file, original_exception=original_exception, message=message
)
class M365NotValidUserError(M365CredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
6024, file=file, original_exception=original_exception, message=message
)
class M365NotValidPasswordError(M365CredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
6025, file=file, original_exception=original_exception, message=message
)
class M365UserNotBelongingToTenantError(M365CredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
6026, file=file, original_exception=original_exception, message=message
)
class M365GraphConnectionError(M365CredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(

View File

@@ -1,3 +1,6 @@
import argparse
def init_parser(self):
"""Init the M365 Provider CLI parser"""
m365_parser = self.subparsers.add_parser(
@@ -13,15 +16,16 @@ def init_parser(self):
action="store_true",
help="Use Azure CLI authentication to log in against Microsoft 365",
)
m365_auth_modes_group.add_argument(
"--env-auth",
action="store_true",
help="Use User and Password environment variables authentication to log in against Microsoft 365",
)
m365_auth_modes_group.add_argument(
"--sp-env-auth",
action="store_true",
help="Use Azure Service Principal environment variables authentication to log in against Microsoft 365",
help="Use Service Principal environment variables authentication to log in against Microsoft 365",
)
m365_auth_modes_group.add_argument(
"--env-auth",
dest="sp_env_auth",
action="store_true",
help=argparse.SUPPRESS,
)
m365_auth_modes_group.add_argument(
"--browser-auth",

View File

@@ -1,13 +1,10 @@
import os
import platform
from prowler.lib.logger import logger
from prowler.lib.powershell.powershell import PowerShellSession
from prowler.providers.m365.exceptions.exceptions import (
M365CertificateCreationError,
M365GraphConnectionError,
M365UserCredentialsError,
M365UserNotBelongingToTenantError,
)
from prowler.providers.m365.lib.jwt.jwt_decoder import decode_jwt, decode_msal_token
from prowler.providers.m365.models import M365Credentials, M365IdentityInfo
@@ -76,10 +73,9 @@ class M365PowerShell(PowerShellSession):
"""
Initialize PowerShell credential object for Microsoft 365 authentication.
Supports three authentication methods:
1. User authentication (username/password) - Will be deprecated in October 2025
2. Application authentication (client_id/client_secret)
3. Certificate authentication (certificate_content in base64/application_id)
Supports two authentication methods:
1. Application authentication (client_id/client_secret)
2. Certificate authentication (certificate_content in base64/client_id)
Args:
credentials (M365Credentials): The credentials object containing
@@ -115,22 +111,6 @@ class M365PowerShell(PowerShellSession):
self.execute(f'$tenantID = "{sanitized_tenant_id}"')
self.execute(f'$tenantDomain = "{credentials.tenant_domains[0]}"')
# User Auth (Will be deprecated in October 2025)
elif credentials.user and credentials.passwd:
credentials.encrypted_passwd = self.encrypt_password(credentials.passwd)
# Sanitize user and password
sanitized_user = self.sanitize(credentials.user)
sanitized_encrypted_passwd = self.sanitize(credentials.encrypted_passwd)
# Securely convert encrypted password to SecureString
self.execute(f'$user = "{sanitized_user}"')
self.execute(
f'$secureString = "{sanitized_encrypted_passwd}" | ConvertTo-SecureString'
)
self.execute(
"$credential = New-Object System.Management.Automation.PSCredential ($user, $secureString)"
)
else:
# Application Auth
self.execute(f'$clientID = "{credentials.client_id}"')
@@ -143,51 +123,13 @@ class M365PowerShell(PowerShellSession):
'$graphToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $graphtokenBody | Select-Object -ExpandProperty Access_Token'
)
def encrypt_password(self, password: str) -> str:
"""
Encrypts a password using Windows CryptProtectData on Windows systems
or UTF-16LE encoding on other systems.
Args:
password (str): The password to encrypt
Returns:
str: The encrypted password in hexadecimal format
Raises:
ValueError: If password is None or empty
"""
try:
if platform.system() == "Windows":
import win32crypt
encrypted_blob = win32crypt.CryptProtectData(
password.encode("utf-16le"), None, None, None, None, 0
)
encrypted_bytes = encrypted_blob
if isinstance(encrypted_blob, tuple):
encrypted_bytes = encrypted_blob[1]
elif hasattr(encrypted_blob, "data"):
encrypted_bytes = encrypted_blob.data
return encrypted_bytes.hex()
else:
return password.encode("utf-16le").hex()
except Exception as error:
raise Exception(
f"[{os.path.basename(__file__)}] Error encrypting password: {str(error)}"
)
def test_credentials(self, credentials: M365Credentials) -> bool:
"""
Test Microsoft 365 credentials by attempting to authenticate against Entra ID.
Supports testing three authentication methods:
1. User authentication (username/password)
2. Application authentication (client_id/client_secret)
3. Certificate authentication (certificate_content in base64/application_id)
Supports testing two authentication methods:
1. Application authentication (client_id/client_secret)
2. Certificate authentication (certificate_content in base64/client_id)
Args:
credentials (M365Credentials): The credentials object containing
@@ -204,50 +146,6 @@ class M365PowerShell(PowerShellSession):
except Exception as e:
logger.error(f"Exchange Online Certificate connection failed: {e}")
# Test User Auth
elif credentials.user and credentials.passwd:
self.execute(
f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString' # encrypted password already sanitized
)
self.execute(
f'$credential = New-Object System.Management.Automation.PSCredential("{self.sanitize(credentials.user)}", $securePassword)'
)
user_domain = credentials.user.split("@")[1]
if not any(
user_domain.endswith(domain)
for domain in self.tenant_identity.tenant_domains
):
raise M365UserNotBelongingToTenantError(
file=os.path.basename(__file__),
message=f"The user domain {user_domain} does not match any of the tenant domains: {', '.join(self.tenant_identity.tenant_domains)}",
)
# Validate credentials
# Test Exchange Online connection
result = self.execute("Connect-ExchangeOnline -Credential $credential")
if "https://aka.ms/exov3-module" not in result:
if "AADSTS" in result: # Entra Security Token Service Error
raise M365UserCredentialsError(
file=os.path.basename(__file__),
message=result,
)
# Test Microsoft Teams connection
result = self.execute("Connect-MicrosoftTeams -Credential $credential")
if self.tenant_identity.user not in result:
if "AADSTS" in result: # Entra Security Token Service Error
raise M365UserCredentialsError(
file=os.path.basename(__file__),
message=result,
)
else: # Unknown error, could be a permission issue or modules not installed
raise M365UserCredentialsError(
file=os.path.basename(__file__),
message=f"Error connecting to PowerShell modules: {result if result else 'Unknown error'}",
)
return True
else:
# Test Microsoft Graph connection
try:
@@ -317,21 +215,6 @@ class M365PowerShell(PowerShellSession):
return False
return True
def test_teams_user_connection(self) -> bool:
"""Test Microsoft Teams API connection using user authentication and raise exception if it fails."""
result = self.execute("Connect-MicrosoftTeams -Credential $credential")
if self.tenant_identity.user not in result:
logger.error(f"Microsoft Teams User Auth connection failed: {result}.")
return False
connection = self.execute("Get-CsTeamsClientConfiguration")
if not connection:
logger.error(
"Microsoft Teams User Auth connection failed: Please check your permissions and try again."
)
return False
return True
def test_exchange_connection(self) -> bool:
"""Test Exchange Online API connection and raise exception if it fails."""
try:
@@ -368,30 +251,14 @@ class M365PowerShell(PowerShellSession):
return False
return True
def test_exchange_user_connection(self) -> bool:
"""Test Exchange Online API connection using user authentication and raise exception if it fails."""
result = self.execute("Connect-ExchangeOnline -Credential $credential")
if "https://aka.ms/exov3-module" not in result:
logger.error(f"Exchange Online User Auth connection failed: {result}.")
return False
connection = self.execute("Get-OrganizationConfig")
if not connection:
logger.error(
"Exchange Online User Auth connection failed: Please check your permissions and try again."
)
return False
return True
def connect_microsoft_teams(self) -> dict:
"""
Connect to Microsoft Teams Module PowerShell Module.
Establishes a connection to Microsoft Teams using the initialized credentials.
Supports three authentication methods:
1. User authentication (username/password)
2. Application authentication (client_id/client_secret)
3. Certificate authentication (certificate_content in base64/application_id)
Supports two authentication methods:
1. Application authentication (client_id/client_secret)
2. Certificate authentication (certificate_content in base64/client_id)
Returns:
dict: Connection status information in JSON format.
@@ -402,12 +269,8 @@ class M365PowerShell(PowerShellSession):
# Certificate Auth
if self.execute("Write-Output $certificate") != "":
return self.test_teams_certificate_connection()
# User Auth
if self.execute("Write-Output $credential") != "":
return self.test_teams_user_connection()
# Application Auth
else:
return self.test_teams_connection()
return self.test_teams_connection()
def get_teams_settings(self) -> dict:
"""
@@ -494,10 +357,9 @@ class M365PowerShell(PowerShellSession):
Connect to Exchange Online PowerShell Module.
Establishes a connection to Exchange Online using the initialized credentials.
Supports three authentication methods:
1. User authentication (username/password)
2. Application authentication (client_id/client_secret)
3. Certificate authentication (certificate_content in base64/application_id)
Supports two authentication methods:
1. Application authentication (client_id/client_secret)
2. Certificate authentication (certificate_content in base64/client_id)
Returns:
dict: Connection status information in JSON format.
@@ -508,12 +370,8 @@ class M365PowerShell(PowerShellSession):
# Certificate Auth
if self.execute("Write-Output $certificate") != "":
return self.test_exchange_certificate_connection()
# User Auth
if self.execute("Write-Output $credential") != "":
return self.test_exchange_user_connection()
# Application Auth
else:
return self.test_exchange_connection()
return self.test_exchange_connection()
def get_audit_log_config(self) -> dict:
"""

View File

@@ -40,7 +40,6 @@ from prowler.providers.m365.exceptions.exceptions import (
M365HTTPResponseError,
M365InteractiveBrowserCredentialError,
M365InvalidProviderIdError,
M365MissingEnvironmentCredentialsError,
M365NoAuthenticationMethodError,
M365NotTenantIdButClientIdAndClientSecretError,
M365NotValidCertificateContentError,
@@ -52,7 +51,6 @@ from prowler.providers.m365.exceptions.exceptions import (
M365SetUpSessionError,
M365TenantIdAndClientIdNotBelongingToClientSecretError,
M365TenantIdAndClientSecretNotBelongingToClientIdError,
M365UserCredentialsError,
)
from prowler.providers.m365.lib.mutelist.mutelist import M365Mutelist
from prowler.providers.m365.lib.powershell.m365_powershell import (
@@ -96,7 +94,7 @@ class M365Provider(Provider):
mutelist(self) -> M365Mutelist: Returns the mutelist object associated with the M365 provider.
setup_region_config(cls, region): Sets up the region configuration for the M365 provider.
print_credentials(self): Prints the M365 credentials information.
setup_session(cls, az_cli_auth, app_env_auth, browser_auth, managed_identity_auth, tenant_id, region_config): Set up the M365 session with the specified authentication method.
setup_session(cls, az_cli_auth, sp_env_auth, browser_auth, managed_identity_auth, tenant_id, region_config): Set up the M365 session with the specified authentication method.
"""
_type: str = "m365"
@@ -109,10 +107,12 @@ class M365Provider(Provider):
# TODO: this is not optional, enforce for all providers
audit_metadata: Audit_Metadata
# TODO: The user and password parameters are deprecated and will be removed in a future version.
# They are currently only kept for backwards compatibility with the API.
# Use client credentials or certificate authentication instead.
def __init__(
self,
sp_env_auth: bool = False,
env_auth: bool = False,
az_cli_auth: bool = False,
browser_auth: bool = False,
certificate_auth: bool = False,
@@ -163,14 +163,11 @@ class M365Provider(Provider):
self.validate_arguments(
az_cli_auth,
sp_env_auth,
env_auth,
browser_auth,
certificate_auth,
tenant_id,
client_id,
client_secret,
user,
password,
certificate_content,
certificate_path,
)
@@ -185,8 +182,6 @@ class M365Provider(Provider):
tenant_id=tenant_id,
client_id=client_id,
client_secret=client_secret,
user=user,
password=password,
certificate_content=certificate_content,
certificate_path=certificate_path,
)
@@ -195,7 +190,6 @@ class M365Provider(Provider):
self._session = self.setup_session(
az_cli_auth,
sp_env_auth,
env_auth,
browser_auth,
certificate_auth,
certificate_path,
@@ -207,7 +201,6 @@ class M365Provider(Provider):
# Set up the identity
self._identity = self.setup_identity(
sp_env_auth,
env_auth,
browser_auth,
az_cli_auth,
certificate_auth,
@@ -216,7 +209,6 @@ class M365Provider(Provider):
# Set up PowerShell session credentials
self._credentials = self.setup_powershell(
env_auth=env_auth,
sp_env_auth=sp_env_auth,
certificate_auth=certificate_auth,
certificate_path=certificate_path,
@@ -294,14 +286,11 @@ class M365Provider(Provider):
def validate_arguments(
az_cli_auth: bool,
sp_env_auth: bool,
env_auth: bool,
browser_auth: bool,
certificate_auth: bool,
tenant_id: str,
client_id: str,
client_secret: str,
user: str,
password: str,
certificate_content: str,
certificate_path: str,
):
@@ -311,14 +300,11 @@ class M365Provider(Provider):
Args:
az_cli_auth (bool): Flag indicating whether Azure CLI authentication is enabled.
sp_env_auth (bool): Flag indicating whether application authentication with environment variables is enabled.
env_auth: (bool): Flag indicating whether to use application and PowerShell authentication with environment variables.
browser_auth (bool): Flag indicating whether browser authentication is enabled.
certificate_auth (bool): Flag indicating whether certificate authentication is enabled.
tenant_id (str): The M365 Tenant ID.
client_id (str): The M365 Client ID.
client_secret (str): The M365 Client Secret.
user (str): The M365 User Account.
password (str): The M365 User Password.
certificate_content (str): The M365 Certificate Content.
certificate_path (str): The path to the certificate file.
@@ -327,7 +313,7 @@ class M365Provider(Provider):
"""
if not client_id and not client_secret:
if not browser_auth and tenant_id and not env_auth:
if not browser_auth and tenant_id:
raise M365BrowserAuthNoFlagError(
file=os.path.basename(__file__),
message="M365 tenant ID error: browser authentication flag (--browser-auth) not found",
@@ -336,12 +322,11 @@ class M365Provider(Provider):
not az_cli_auth
and not sp_env_auth
and not browser_auth
and not env_auth
and not certificate_auth
):
raise M365NoAuthenticationMethodError(
file=os.path.basename(__file__),
message="M365 provider requires at least one authentication method set: [--env-auth | --az-cli-auth | --sp-env-auth | --browser-auth | --certificate-auth]",
message="M365 provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth | --certificate-auth]",
)
elif browser_auth and not tenant_id:
raise M365BrowserAuthNoTenantIDError(
@@ -354,12 +339,7 @@ class M365Provider(Provider):
file=os.path.basename(__file__),
message="Tenant Id is required for M365 static credentials. Make sure you are using the correct credentials.",
)
if (
not certificate_content
and not certificate_path
and not (user and password)
and not client_secret
):
if not certificate_content and not certificate_path and not client_secret:
raise M365ConfigCredentialsError(
file=os.path.basename(__file__),
message="You must provide a valid set of credentials. Please check your credentials and try again.",
@@ -404,7 +384,6 @@ class M365Provider(Provider):
@staticmethod
def setup_powershell(
env_auth: bool = False,
sp_env_auth: bool = False,
certificate_auth: bool = False,
certificate_path: str = None,
@@ -415,51 +394,22 @@ class M365Provider(Provider):
"""Gets the M365 credentials.
Args:
env_auth: (bool): Flag indicating whether to use application and PowerShell authentication with environment variables.
sp_env_auth (bool): Flag indicating whether to use application authentication with environment variables.
Returns:
M365Credentials: Object containing the user credentials.
If env_auth is True, retrieves from environment variables.
If False, returns empty credentials.
M365Credentials: Object containing the credentials for PowerShell operations.
"""
logger.info("M365 provider: Setting up PowerShell session...")
credentials = None
if m365_credentials:
credentials = M365Credentials(
user=m365_credentials.get("user", None),
passwd=m365_credentials.get("password", None),
client_id=m365_credentials.get("client_id", ""),
client_secret=m365_credentials.get("client_secret", ""),
tenant_id=m365_credentials.get("tenant_id", ""),
certificate_content=m365_credentials.get("certificate_content", ""),
tenant_domains=identity.tenant_domains,
)
elif env_auth:
m365_user = getenv("M365_USER")
m365_password = getenv("M365_PASSWORD")
client_id = getenv("AZURE_CLIENT_ID")
client_secret = getenv("AZURE_CLIENT_SECRET")
tenant_id = getenv("AZURE_TENANT_ID")
if not m365_user or not m365_password:
logger.critical(
"M365 provider: Missing M365_USER or M365_PASSWORD environment variables needed for credentials authentication"
)
raise M365MissingEnvironmentCredentialsError(
file=os.path.basename(__file__),
message="Missing M365_USER or M365_PASSWORD environment variables required for credentials authentication.",
)
credentials = M365Credentials(
client_id=client_id,
client_secret=client_secret,
tenant_id=tenant_id,
tenant_domains=identity.tenant_domains,
user=m365_user,
passwd=m365_password,
)
elif sp_env_auth:
client_id = getenv("AZURE_CLIENT_ID")
client_secret = getenv("AZURE_CLIENT_SECRET")
@@ -488,9 +438,6 @@ class M365Provider(Provider):
)
if credentials:
if identity and credentials.user:
identity.user = credentials.user
identity.identity_type = "Service Principal and User Credentials"
if identity and credentials.certificate_content:
identity.identity_type = "Service Principal with Certificate"
test_session = M365PowerShell(credentials, identity)
@@ -499,9 +446,9 @@ class M365Provider(Provider):
initialize_m365_powershell_modules()
if test_session.test_credentials(credentials):
return credentials
raise M365UserCredentialsError(
raise M365ConfigCredentialsError(
file=os.path.basename(__file__),
message="The provided User credentials are not valid.",
message="The provided credentials are not valid.",
)
finally:
test_session.close()
@@ -523,11 +470,7 @@ class M365Provider(Provider):
f"M365 Tenant Domain: {Fore.YELLOW}{self._identity.tenant_domain}{Style.RESET_ALL} M365 Tenant ID: {Fore.YELLOW}{self._identity.tenant_id}{Style.RESET_ALL}",
f"M365 Identity Type: {Fore.YELLOW}{self._identity.identity_type}{Style.RESET_ALL} M365 Identity ID: {Fore.YELLOW}{self._identity.identity_id}{Style.RESET_ALL}",
]
if self.credentials and self.credentials.user:
report_lines.append(
f"M365 User: {Fore.YELLOW}{self.credentials.user}{Style.RESET_ALL}"
)
elif self.credentials and self.credentials.certificate_content:
if self.credentials and self.credentials.certificate_content:
report_lines.append(
f"M365 Certificate Thumbprint: {Fore.YELLOW}{self._identity.certificate_thumbprint}{Style.RESET_ALL}"
)
@@ -542,7 +485,6 @@ class M365Provider(Provider):
def setup_session(
az_cli_auth: bool,
sp_env_auth: bool,
env_auth: bool,
browser_auth: bool,
certificate_auth: bool,
certificate_path: str,
@@ -563,8 +505,6 @@ class M365Provider(Provider):
- tenant_id: The M365 Active Directory tenant ID.
- client_id: The M365 client ID.
- client_secret: The M365 client secret
- user: The M365 user email
- password: The M365 user password
- certificate_content: The M365 certificate content
- certificate_path: The path to the certificate file.
- provider_id: The M365 provider ID (in this case the Tenant ID).
@@ -579,7 +519,7 @@ class M365Provider(Provider):
"""
logger.info("M365 provider: Setting up session...")
if not browser_auth:
if sp_env_auth or env_auth:
if sp_env_auth:
try:
M365Provider.check_service_principal_creds_env_vars()
except M365EnvironmentVariableError as environment_credentials_error:
@@ -623,8 +563,6 @@ class M365Provider(Provider):
tenant_id=m365_credentials["tenant_id"],
client_id=m365_credentials["client_id"],
client_secret=m365_credentials["client_secret"],
user=m365_credentials["user"],
password=m365_credentials["password"],
)
return credentials
except ClientAuthenticationError as error:
@@ -675,7 +613,7 @@ class M365Provider(Provider):
try:
credentials = DefaultAzureCredential(
exclude_environment_credential=not (
sp_env_auth or env_auth or certificate_auth
sp_env_auth or certificate_auth
),
exclude_cli_credential=not az_cli_auth,
# M365 Auth using Managed Identity is not supported
@@ -738,7 +676,6 @@ class M365Provider(Provider):
def test_connection(
az_cli_auth: bool = False,
sp_env_auth: bool = False,
env_auth: bool = False,
browser_auth: bool = False,
certificate_auth: bool = False,
tenant_id: str = None,
@@ -746,8 +683,6 @@ class M365Provider(Provider):
raise_on_exception: bool = True,
client_id: str = None,
client_secret: str = None,
user: str = None,
password: str = None,
certificate_content: str = None,
certificate_path: str = None,
provider_id: str = None,
@@ -760,7 +695,6 @@ class M365Provider(Provider):
az_cli_auth (bool): Flag indicating whether to use Azure CLI authentication.
sp_env_auth (bool): Flag indicating whether to use application authentication with environment variables.
env_auth: (bool): Flag indicating whether to use application and PowerShell authentication with environment variables.
browser_auth (bool): Flag indicating whether to use interactive browser authentication.
certificate_auth (bool): Flag indicating whether to use certificate authentication.
tenant_id (str): The M365 Active Directory tenant ID.
@@ -768,8 +702,6 @@ class M365Provider(Provider):
raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails.
client_id (str): The M365 client ID.
client_secret (str): The M365 client secret.
user (str): The M365 user email.
password (str): The M365 password.
provider_id (str): The M365 provider ID (in this case the Tenant ID).
@@ -797,14 +729,11 @@ class M365Provider(Provider):
M365Provider.validate_arguments(
az_cli_auth,
sp_env_auth,
env_auth,
browser_auth,
certificate_auth,
tenant_id,
client_id,
client_secret,
user,
password,
certificate_content,
certificate_path,
)
@@ -813,20 +742,11 @@ class M365Provider(Provider):
# Get the dict from the static credentials
m365_credentials = None
if tenant_id and client_id and client_secret:
if not user and not password:
m365_credentials = M365Provider.validate_static_credentials(
tenant_id=tenant_id,
client_id=client_id,
client_secret=client_secret,
)
else:
m365_credentials = M365Provider.validate_static_credentials(
tenant_id=tenant_id,
client_id=client_id,
client_secret=client_secret,
user=user,
password=password,
)
m365_credentials = M365Provider.validate_static_credentials(
tenant_id=tenant_id,
client_id=client_id,
client_secret=client_secret,
)
elif tenant_id and client_id and certificate_content:
m365_credentials = M365Provider.validate_static_credentials(
tenant_id=tenant_id,
@@ -838,7 +758,6 @@ class M365Provider(Provider):
session = M365Provider.setup_session(
az_cli_auth,
sp_env_auth,
env_auth,
browser_auth,
certificate_auth,
certificate_path,
@@ -854,7 +773,6 @@ class M365Provider(Provider):
# Set up Identity
identity = M365Provider.setup_identity(
sp_env_auth,
env_auth,
browser_auth,
az_cli_auth,
certificate_auth,
@@ -877,7 +795,6 @@ class M365Provider(Provider):
# Set up PowerShell credentials
M365Provider.setup_powershell(
env_auth,
sp_env_auth,
certificate_auth,
certificate_path,
@@ -1046,7 +963,6 @@ class M365Provider(Provider):
@staticmethod
def setup_identity(
sp_env_auth,
env_auth,
browser_auth,
az_cli_auth,
certificate_auth,
@@ -1058,7 +974,6 @@ class M365Provider(Provider):
Args:
az_cli_auth (bool): Flag indicating if Azure CLI authentication is used.
sp_env_auth (bool): Flag indicating if application authentication with environment variables is used.
env_auth: (bool): Flag indicating whether to use application and PowerShell authentication with environment variables.
browser_auth (bool): Flag indicating if interactive browser authentication is used.
client_id (str): The M365 client ID.
@@ -1116,13 +1031,6 @@ class M365Provider(Provider):
or session.credentials[0]._credential.client_id
or "Unknown user id (Missing AAD permissions)"
)
elif env_auth:
identity.identity_type = "Service Principal and User Credentials"
identity.identity_id = (
getenv("AZURE_CLIENT_ID")
or session.credentials[0]._credential.client_id
or "Unknown user id (Missing AAD permissions)"
)
elif certificate_auth:
identity.identity_type = "Service Principal with Certificate"
identity.identity_id = (
@@ -1174,8 +1082,6 @@ class M365Provider(Provider):
tenant_id: str = None,
client_id: str = None,
client_secret: str = None,
user: str = None,
password: str = None,
certificate_content: str = None,
certificate_path: str = None,
) -> dict:
@@ -1186,8 +1092,6 @@ class M365Provider(Provider):
tenant_id (str): The M365 Active Directory tenant ID.
client_id (str): The M365 client ID.
client_secret (str): The M365 client secret.
user (str): The M365 user email.
password (str): The M365 user password.
certificate_content (str): The M365 Certificate Content.
certificate_path (str): The path to the certificate file.
@@ -1257,8 +1161,6 @@ class M365Provider(Provider):
"tenant_id": tenant_id,
"client_id": client_id,
"client_secret": client_secret,
"user": user,
"password": password,
"certificate_content": certificate_content,
"certificate_path": certificate_path,
}

View File

@@ -25,9 +25,6 @@ class M365RegionConfig(BaseModel):
class M365Credentials(BaseModel):
user: Optional[str] = None
passwd: Optional[str] = None
encrypted_passwd: Optional[str] = None
client_id: str = ""
client_secret: Optional[str] = None
tenant_id: str = ""

View File

@@ -147,29 +147,6 @@ class TestM365Arguments:
assert kwargs["action"] == "store_true"
assert "Azure CLI authentication" in kwargs["help"]
def test_env_auth_argument_configuration(self):
"""Test that env-auth argument is configured correctly"""
mock_m365_args = MagicMock()
mock_m365_args.subparsers = self.mock_subparsers
mock_m365_args.common_providers_parser = MagicMock()
arguments.init_parser(mock_m365_args)
# Find the env-auth argument call
calls = self.mock_auth_modes_group.add_argument.call_args_list
env_auth_call = None
for call in calls:
if call[0][0] == "--env-auth":
env_auth_call = call
break
assert env_auth_call is not None
# Check argument configuration
kwargs = env_auth_call[1]
assert kwargs["action"] == "store_true"
assert "User and Password environment variables" in kwargs["help"]
def test_sp_env_auth_argument_configuration(self):
"""Test that sp-env-auth argument is configured correctly"""
mock_m365_args = MagicMock()
@@ -191,7 +168,7 @@ class TestM365Arguments:
# Check argument configuration
kwargs = sp_env_call[1]
assert kwargs["action"] == "store_true"
assert "Azure Service Principal environment variables" in kwargs["help"]
assert "Service Principal environment variables" in kwargs["help"]
def test_browser_auth_argument_configuration(self):
"""Test that browser-auth argument is configured correctly"""
@@ -336,28 +313,7 @@ class TestM365ArgumentsIntegration:
args = parser.parse_args(["m365", "--az-cli-auth"])
assert args.az_cli_auth is True
assert args.env_auth is False
assert args.sp_env_auth is False
assert args.browser_auth is False
assert args.certificate_auth is False
def test_real_argument_parsing_env_auth(self):
"""Test parsing arguments with environment authentication"""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
common_parser = argparse.ArgumentParser(add_help=False)
mock_m365_args = MagicMock()
mock_m365_args.subparsers = subparsers
mock_m365_args.common_providers_parser = common_parser
arguments.init_parser(mock_m365_args)
# Parse arguments with environment auth
args = parser.parse_args(["m365", "--env-auth"])
assert args.az_cli_auth is False
assert args.env_auth is True
assert not hasattr(args, "env_auth")
assert args.sp_env_auth is False
assert args.browser_auth is False
assert args.certificate_auth is False
@@ -378,7 +334,7 @@ class TestM365ArgumentsIntegration:
args = parser.parse_args(["m365", "--sp-env-auth"])
assert args.az_cli_auth is False
assert args.env_auth is False
assert not hasattr(args, "env_auth")
assert args.sp_env_auth is True
assert args.browser_auth is False
assert args.certificate_auth is False
@@ -406,7 +362,7 @@ class TestM365ArgumentsIntegration:
)
assert args.az_cli_auth is False
assert args.env_auth is False
assert not hasattr(args, "env_auth")
assert args.sp_env_auth is False
assert args.browser_auth is True
assert args.certificate_auth is False
@@ -428,7 +384,7 @@ class TestM365ArgumentsIntegration:
args = parser.parse_args(["m365", "--certificate-auth"])
assert args.az_cli_auth is False
assert args.env_auth is False
assert not hasattr(args, "env_auth")
assert args.sp_env_auth is False
assert args.browser_auth is False
assert args.certificate_auth is True
@@ -495,7 +451,7 @@ class TestM365ArgumentsIntegration:
args = parser.parse_args(["m365"])
assert args.az_cli_auth is False
assert args.env_auth is False
assert not hasattr(args, "env_auth")
assert args.sp_env_auth is False
assert args.browser_auth is False
assert args.certificate_auth is False
@@ -547,7 +503,7 @@ class TestM365ArgumentsIntegration:
# This should raise SystemExit due to mutually exclusive group
try:
parser.parse_args(["m365", "--az-cli-auth", "--env-auth"])
parser.parse_args(["m365", "--az-cli-auth", "--sp-env-auth"])
assert False, "Expected SystemExit due to mutually exclusive arguments"
except SystemExit:
# This is expected
@@ -636,6 +592,6 @@ class TestM365ArgumentsIntegration:
assert args.certificate_path == "/home/user/cert.pem"
assert args.tenant_id == "12345678-1234-1234-1234-123456789012"
assert args.az_cli_auth is False
assert args.env_auth is False
assert not hasattr(args, "env_auth")
assert args.sp_env_auth is False
assert args.browser_auth is False

View File

@@ -7,8 +7,6 @@ from prowler.lib.powershell.powershell import PowerShellSession
from prowler.providers.m365.exceptions.exceptions import (
M365CertificateCreationError,
M365GraphConnectionError,
M365UserCredentialsError,
M365UserNotBelongingToTenantError,
)
from prowler.providers.m365.lib.powershell.m365_powershell import M365PowerShell
from prowler.providers.m365.models import M365Credentials, M365IdentityInfo
@@ -19,7 +17,11 @@ class Testm365PowerShell:
def test_init(self, mock_popen):
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="User",
@@ -40,7 +42,11 @@ class Testm365PowerShell:
@patch("subprocess.Popen")
def test_sanitize(self, _):
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="User",
@@ -77,180 +83,50 @@ class Testm365PowerShell:
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(
user="test@example.com",
passwd="test_password",
encrypted_passwd="test_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",
identity_type="Service Principal",
tenant_id="test_tenant",
tenant_domain="example.com",
tenant_domains=["example.com"],
location="test_location",
)
session = M365PowerShell(credentials, identity)
with patch.object(M365PowerShell, "init_credential") as mock_init:
session = M365PowerShell(credentials, identity)
mock_init.assert_called_once_with(credentials)
# Mock encrypt_password to return a known value
session.encrypt_password = MagicMock(return_value="encrypted_password")
session.execute = MagicMock()
session.init_credential(credentials)
# Call original init_credential to verify application authentication setup
M365PowerShell.init_credential(session, credentials)
# Verify encrypt_password was called
session.encrypt_password.assert_any_call(credentials.passwd)
# Verify execute was called with the correct commands
session.execute.assert_any_call(f'$user = "{credentials.user}"')
session.execute.assert_any_call('$clientID = "test_client_id"')
session.execute.assert_any_call('$clientSecret = "test_client_secret"')
session.execute.assert_any_call('$tenantID = "test_tenant_id"')
session.execute.assert_any_call(
f'$secureString = "{credentials.encrypted_passwd}" | ConvertTo-SecureString'
'$graphtokenBody = @{ Grant_Type = "client_credentials"; Scope = "https://graph.microsoft.com/.default"; Client_Id = $clientID; Client_Secret = $clientSecret }'
)
session.execute.assert_any_call(
"$credential = New-Object System.Management.Automation.PSCredential ($user, $secureString)"
'$graphToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $graphtokenBody | Select-Object -ExpandProperty Access_Token'
)
session.close()
@patch("subprocess.Popen")
def test_test_credentials_exchange_success(self, mock_popen):
mock_process = MagicMock()
mock_popen.return_value = mock_process
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",
user="test@contoso.onmicrosoft.com",
)
session = M365PowerShell(credentials, identity)
# Mock encrypt_password to return a known value
session.encrypt_password = MagicMock(return_value="encrypted_password")
# Mock execute to simulate successful Exchange connection
def mock_execute_side_effect(command):
if "Connect-ExchangeOnline" in command:
return "Connected successfully https://aka.ms/exov3-module"
return ""
session.execute = MagicMock(side_effect=mock_execute_side_effect)
# Execute the test
result = session.test_credentials(credentials)
assert result is True
# Verify execute was called with the correct commands
session.execute.assert_any_call(
f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString'
)
session.execute.assert_any_call(
f'$credential = New-Object System.Management.Automation.PSCredential("{session.sanitize(credentials.user)}", $securePassword)'
)
# Exchange connection should be tested
session.execute.assert_any_call(
"Connect-ExchangeOnline -Credential $credential"
)
# Verify Teams connection was NOT called (since Exchange succeeded)
teams_calls = [
call
for call in session.execute.call_args_list
if "Connect-MicrosoftTeams" in str(call)
]
assert (
len(teams_calls) == 0
), "Teams connection should not be called when Exchange succeeds"
session.close()
@patch("subprocess.Popen")
def test_test_credentials_exchange_fail_teams_success(self, mock_popen):
mock_process = MagicMock()
mock_popen.return_value = mock_process
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",
user="test@contoso.onmicrosoft.com",
)
session = M365PowerShell(credentials, identity)
# Mock encrypt_password to return a known value
session.encrypt_password = MagicMock(return_value="encrypted_password")
# Mock execute to simulate Exchange fail and Teams success
def mock_execute_side_effect(command):
if "Connect-ExchangeOnline" in command:
return (
"Connection failed" # No "https://aka.ms/exov3-module" in response
)
elif "Connect-MicrosoftTeams" in command:
return "Connected successfully test@contoso.onmicrosoft.com"
return ""
session.execute = MagicMock(side_effect=mock_execute_side_effect)
# Execute the test
result = session.test_credentials(credentials)
assert result is True
# Verify execute was called with the correct commands
session.execute.assert_any_call(
f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString'
)
session.execute.assert_any_call(
f'$credential = New-Object System.Management.Automation.PSCredential("{session.sanitize(credentials.user)}", $securePassword)'
)
# Both Exchange and Teams connections should be tested
session.execute.assert_any_call(
"Connect-ExchangeOnline -Credential $credential"
)
session.execute.assert_any_call(
"Connect-MicrosoftTeams -Credential $credential"
)
session.close()
@patch("subprocess.Popen")
def test_test_credentials_application_auth(self, mock_popen):
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(
user="",
passwd="",
encrypted_passwd="",
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Application",
identity_type="Service Principal",
tenant_id="test_tenant",
tenant_domain="contoso.onmicrosoft.com",
tenant_domains=["contoso.onmicrosoft.com"],
@@ -264,162 +140,13 @@ class Testm365PowerShell:
session.execute.assert_any_call("Write-Output $graphToken")
session.close()
@patch("subprocess.Popen")
@patch("msal.ConfidentialClientApplication")
def test_test_credentials_user_not_belonging_to_tenant(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 = {
"access_token": "test_token"
}
credentials = M365Credentials(
user="user@otherdomain.com",
passwd="test_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(M365UserNotBelongingToTenantError) as exception:
session.test_credentials(credentials)
assert exception.type == M365UserNotBelongingToTenantError
assert (
"The user domain otherdomain.com does not match any of the tenant domains: contoso.onmicrosoft.com"
in str(exception.value)
)
# Verify MSAL was not called since domain validation failed first
mock_msal.assert_not_called()
mock_msal_instance.acquire_token_by_username_password.assert_not_called()
session.close()
@patch("subprocess.Popen")
def test_test_credentials_auth_failure_aadsts_error(self, mock_popen):
mock_process = MagicMock()
mock_popen.return_value = mock_process
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 encrypt_password and execute to simulate AADSTS error
session.encrypt_password = MagicMock(return_value="encrypted_password")
session.execute = MagicMock(
return_value="AADSTS50126: Error validating credentials due to invalid username or password"
)
with pytest.raises(M365UserCredentialsError) as exc_info:
session.test_credentials(credentials)
assert (
"AADSTS50126: Error validating credentials due to invalid username or password"
in str(exc_info.value)
)
# Verify execute was called with the correct commands
session.execute.assert_any_call(
f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString'
)
session.execute.assert_any_call(
f'$credential = New-Object System.Management.Automation.PSCredential("{session.sanitize(credentials.user)}", $securePassword)'
)
session.execute.assert_any_call(
"Connect-ExchangeOnline -Credential $credential"
)
session.close()
@patch("subprocess.Popen")
def test_test_credentials_auth_failure_no_access_token(self, mock_popen):
mock_process = MagicMock()
mock_popen.return_value = mock_process
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 encrypt_password and execute to simulate AADSTS invalid grant error
session.encrypt_password = MagicMock(return_value="encrypted_password")
session.execute = MagicMock(
return_value="AADSTS70002: The request body must contain the following parameter: 'client_secret' or 'client_assertion'."
)
with pytest.raises(M365UserCredentialsError) as exc_info:
session.test_credentials(credentials)
assert (
"AADSTS70002: The request body must contain the following parameter: 'client_secret' or 'client_assertion'."
in str(exc_info.value)
)
# Verify execute was called with the correct commands
session.execute.assert_any_call(
f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString'
)
session.execute.assert_any_call(
f'$credential = New-Object System.Management.Automation.PSCredential("{session.sanitize(credentials.user)}", $securePassword)'
)
session.execute.assert_any_call(
"Connect-ExchangeOnline -Credential $credential"
)
session.close()
@patch("subprocess.Popen")
def test_remove_ansi(self, mock_popen):
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="User",
@@ -446,7 +173,11 @@ class Testm365PowerShell:
def test_execute(self, mock_popen):
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="User",
@@ -469,7 +200,11 @@ class Testm365PowerShell:
"""Test the read_output method with various scenarios"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="User",
@@ -516,7 +251,11 @@ class Testm365PowerShell:
def test_json_parse_output(self, mock_popen):
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="User",
@@ -549,7 +288,11 @@ class Testm365PowerShell:
def test_close(self, mock_popen):
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="User",
@@ -718,7 +461,11 @@ class Testm365PowerShell:
"""Test test_graph_connection when token is valid"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Application",
@@ -743,7 +490,11 @@ class Testm365PowerShell:
"""Test test_graph_connection when token is empty"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Application",
@@ -769,7 +520,11 @@ class Testm365PowerShell:
"""Test test_graph_connection when an exception occurs"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Application",
@@ -797,7 +552,11 @@ class Testm365PowerShell:
"""Test test_teams_connection when token is valid"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Application",
@@ -835,7 +594,11 @@ class Testm365PowerShell:
"""Test test_teams_connection when token lacks required permissions"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Application",
@@ -870,7 +633,11 @@ class Testm365PowerShell:
"""Test test_teams_connection when an exception occurs"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Application",
@@ -899,7 +666,11 @@ class Testm365PowerShell:
"""Test test_exchange_connection when token is valid"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Application",
@@ -937,7 +708,11 @@ class Testm365PowerShell:
"""Test test_exchange_connection when token lacks required permissions"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Application",
@@ -972,7 +747,11 @@ class Testm365PowerShell:
"""Test test_exchange_connection when an exception occurs"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
credentials = M365Credentials(user="test@example.com", passwd="test_password")
credentials = M365Credentials(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Application",
@@ -995,58 +774,6 @@ class Testm365PowerShell:
)
session.close()
@patch("subprocess.Popen")
def test_encrypt_password(self, mock_popen):
credentials = M365Credentials(user="test@example.com", passwd="test_password")
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="User",
tenant_id="test_tenant",
tenant_domain="example.com",
tenant_domains=["example.com"],
location="test_location",
)
session = M365PowerShell(credentials, identity)
# Test non-Windows system (should use utf-16le hex encoding)
from unittest import mock
with mock.patch("platform.system", return_value="Linux"):
result = session.encrypt_password("password123")
expected = "password123".encode("utf-16le").hex()
assert result == expected
# Test Windows system with tuple return
with mock.patch("platform.system", return_value="Windows"):
import sys
win32crypt_mock = mock.MagicMock()
win32crypt_mock.CryptProtectData.return_value = (None, b"encrypted_bytes")
sys.modules["win32crypt"] = win32crypt_mock
result = session.encrypt_password("password123")
assert result == b"encrypted_bytes".hex()
# Clean up mock
del sys.modules["win32crypt"]
# Test error handling
with mock.patch("platform.system", return_value="Windows"):
import sys
win32crypt_mock = mock.MagicMock()
win32crypt_mock.CryptProtectData.side_effect = Exception("Test error")
sys.modules["win32crypt"] = win32crypt_mock
with pytest.raises(Exception) as exc_info:
session.encrypt_password("password123")
assert "Error encrypting password: Test error" in str(exc_info.value)
# Clean up mock
del sys.modules["win32crypt"]
session.close()
@patch("subprocess.Popen")
def test_clean_certificate_content(self, mock_popen):
"""Test clean_certificate_content method with various certificate content formats"""

View File

@@ -23,7 +23,9 @@ LOCATION = "global"
def set_mocked_m365_provider(
session_credentials: DefaultAzureCredential = DefaultAzureCredential(),
credentials: M365Credentials = M365Credentials(
user="user@email.com", passwd="111111aa111111aaa1111"
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
tenant_id=TENANT_ID,
),
identity: M365IdentityInfo = M365IdentityInfo(
identity_id=IDENTITY_ID,

View File

@@ -29,20 +29,15 @@ from prowler.providers.m365.exceptions.exceptions import (
M365GetTokenIdentityError,
M365HTTPResponseError,
M365InvalidProviderIdError,
M365MissingEnvironmentCredentialsError,
M365NoAuthenticationMethodError,
M365NotTenantIdButClientIdAndClientSecretError,
M365NotValidCertificateContentError,
M365NotValidCertificatePathError,
M365NotValidClientIdError,
M365NotValidClientSecretError,
M365NotValidPasswordError,
M365NotValidTenantIdError,
M365NotValidUserError,
M365TenantIdAndClientIdNotBelongingToClientSecretError,
M365TenantIdAndClientSecretNotBelongingToClientIdError,
M365UserCredentialsError,
M365UserNotBelongingToTenantError,
)
from prowler.providers.m365.m365_provider import M365Provider
from prowler.providers.m365.models import (
@@ -97,8 +92,6 @@ class TestM365Provider:
client_id=CLIENT_ID,
tenant_id=TENANT_ID,
client_secret=CLIENT_SECRET,
user="",
passwd="",
),
),
):
@@ -106,71 +99,6 @@ class TestM365Provider:
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,
@@ -228,7 +156,6 @@ class TestM365Provider:
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,
@@ -278,7 +205,6 @@ class TestM365Provider:
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,
@@ -398,68 +324,6 @@ class TestM365Provider:
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,
patch(
"prowler.providers.m365.m365_provider.M365Provider.validate_arguments"
),
patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"),
):
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,
patch(
"prowler.providers.m365.m365_provider.M365Provider.validate_arguments"
),
patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"),
):
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(
@@ -513,27 +377,24 @@ class TestM365Provider:
assert exception.type == M365NoAuthenticationMethodError
assert (
"M365 provider requires at least one authentication method set: [--env-auth | --az-cli-auth | --sp-env-auth | --browser-auth | --certificate-auth]"
"M365 provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth | --certificate-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,
),
):
with patch("prowler.providers.m365.m365_provider.M365PowerShell") as mock_ps:
mock_session = MagicMock()
mock_session.test_credentials.return_value = True
mock_session.close = MagicMock()
mock_ps.return_value = mock_session
result = M365Provider.setup_powershell(
env_auth=False,
m365_credentials=credentials_dict,
identity=M365IdentityInfo(
identity_id=IDENTITY_ID,
@@ -544,42 +405,9 @@ class TestM365Provider:
location=LOCATION,
),
)
assert result.user == credentials_dict["user"]
assert result.passwd == credentials_dict["password"]
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,
patch(
"prowler.providers.m365.m365_provider.M365Provider.validate_arguments"
),
patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"),
):
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)
)
assert result.client_id == credentials_dict["client_id"]
assert result.client_secret == credentials_dict["client_secret"]
def test_validate_static_credentials_invalid_tenant_id(self):
with pytest.raises(M365NotValidTenantIdError) as exception:
@@ -587,8 +415,6 @@ class TestM365Provider:
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)
@@ -598,8 +424,6 @@ class TestM365Provider:
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)
@@ -609,36 +433,12 @@ class TestM365Provider:
tenant_id="12345678-1234-5678-1234-567812345678",
client_id="12345678-1234-5678-1234-567812345678",
client_secret="",
user="test@example.com",
password="test_password",
)
assert (
"You must provide a client secret, certificate content or certificate path. Please check your credentials and try again."
in str(exception.value)
)
def test_validate_arguments_missing_env_credentials(self):
with pytest.raises(M365ConfigCredentialsError) as exception:
M365Provider.validate_arguments(
az_cli_auth=False,
sp_env_auth=False,
env_auth=True,
browser_auth=False,
certificate_auth=False,
tenant_id="test_tenant_id",
client_id="test_client_id",
client_secret=None,
user=None,
password=None,
certificate_content=None,
certificate_path=None,
)
assert (
"You must provide a valid set of credentials. Please check your credentials and try again."
in str(exception.value)
)
def test_test_connection_invalid_provider_id(self):
with (
patch(
@@ -680,8 +480,6 @@ class TestM365Provider:
raise_on_exception=True,
client_id=str(uuid4()),
client_secret=str(uuid4()),
user=f"user@{user_domain}",
password="test_password",
provider_id=provider_id,
)
@@ -694,24 +492,23 @@ class TestM365Provider:
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.M365PowerShell") as mock_ps,
patch(
"prowler.providers.m365.m365_provider.initialize_m365_powershell_modules"
) as mock_init_modules,
):
mock_session = MagicMock()
mock_session.test_credentials.return_value = True
mock_session.close = MagicMock()
mock_ps.return_value = mock_session
M365Provider.setup_powershell(
env_auth=False,
m365_credentials=credentials_dict,
identity=M365IdentityInfo(
identity_id=IDENTITY_ID,
@@ -728,24 +525,23 @@ class TestM365Provider:
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.M365PowerShell") as mock_ps,
patch(
"prowler.providers.m365.m365_provider.initialize_m365_powershell_modules"
) as mock_init_modules,
):
mock_session = MagicMock()
mock_session.test_credentials.return_value = True
mock_session.close = MagicMock()
mock_ps.return_value = mock_session
M365Provider.setup_powershell(
env_auth=False,
m365_credentials=credentials_dict,
identity=M365IdentityInfo(
identity_id=IDENTITY_ID,
@@ -762,26 +558,25 @@ class TestM365Provider:
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.M365PowerShell") as mock_ps,
patch(
"prowler.providers.m365.m365_provider.initialize_m365_powershell_modules",
side_effect=Exception("Module initialization failed"),
),
):
mock_session = MagicMock()
mock_session.test_credentials.return_value = True
mock_session.close = MagicMock()
mock_ps.return_value = mock_session
with pytest.raises(Exception) as exc_info:
M365Provider.setup_powershell(
env_auth=False,
m365_credentials=credentials_dict,
identity=M365IdentityInfo(
identity_id=IDENTITY_ID,
@@ -837,8 +632,6 @@ class TestM365Provider:
raise_on_exception=True,
client_id=str(uuid4()),
client_secret=str(uuid4()),
user="user@contoso.onmicrosoft.com",
password="test_password",
provider_id=provider_id,
)
@@ -893,7 +686,6 @@ class TestM365Provider:
sp_env_auth=False,
az_cli_auth=False,
browser_auth=False,
env_auth=False,
certificate_auth=True,
tenant_id=tenant_id,
client_id=client_id,
@@ -1067,64 +859,6 @@ class TestM365Provider:
check_certificate_content=True
)
def test_setup_powershell_env_auth_missing_credentials(self):
"""Test setup_powershell with env_auth but missing environment variables"""
with (
patch.dict(os.environ, {}, clear=True),
pytest.raises(M365MissingEnvironmentCredentialsError) as exception,
):
M365Provider.setup_powershell(
env_auth=True,
identity=M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="User",
tenant_id=TENANT_ID,
tenant_domain=DOMAIN,
tenant_domains=["test.onmicrosoft.com"],
location=LOCATION,
),
)
assert exception.type == M365MissingEnvironmentCredentialsError
assert (
"Missing M365_USER or M365_PASSWORD environment variables required for credentials authentication."
in str(exception.value)
)
def test_setup_powershell_env_auth_success(self):
"""Test setup_powershell with env_auth and valid environment variables"""
with (
patch.dict(
os.environ,
{
"M365_USER": "test@example.com",
"M365_PASSWORD": "password",
"AZURE_CLIENT_ID": CLIENT_ID,
"AZURE_CLIENT_SECRET": CLIENT_SECRET,
"AZURE_TENANT_ID": TENANT_ID,
},
),
patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials",
return_value=True,
),
):
result = M365Provider.setup_powershell(
env_auth=True,
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 == "test@example.com"
assert result.passwd == "password"
assert result.client_id == CLIENT_ID
def test_setup_powershell_sp_env_auth_success(self):
"""Test setup_powershell with sp_env_auth and valid environment variables"""
with (
@@ -1136,11 +870,13 @@ class TestM365Provider:
"AZURE_TENANT_ID": TENANT_ID,
},
),
patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials",
return_value=True,
),
patch("prowler.providers.m365.m365_provider.M365PowerShell") as mock_ps,
):
mock_session = MagicMock()
mock_session.test_credentials.return_value = True
mock_session.close = MagicMock()
mock_ps.return_value = mock_session
result = M365Provider.setup_powershell(
sp_env_auth=True,
identity=M365IdentityInfo(
@@ -1170,11 +906,13 @@ class TestM365Provider:
"M365_CERTIFICATE_CONTENT": certificate_content,
},
),
patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials",
return_value=True,
),
patch("prowler.providers.m365.m365_provider.M365PowerShell") as mock_ps,
):
mock_session = MagicMock()
mock_session.test_credentials.return_value = True
mock_session.close = MagicMock()
mock_ps.return_value = mock_session
identity = M365IdentityInfo(
identity_id=IDENTITY_ID,
identity_type="Service Principal with Certificate",
@@ -1197,22 +935,21 @@ class TestM365Provider:
def test_setup_powershell_invalid_credentials(self):
"""Test setup_powershell with invalid credentials"""
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=False,
),
pytest.raises(M365UserCredentialsError) as exception,
patch("prowler.providers.m365.m365_provider.M365PowerShell") as mock_ps,
pytest.raises(M365ConfigCredentialsError) as exception,
):
mock_session = MagicMock()
mock_session.test_credentials.return_value = False
mock_session.close = MagicMock()
mock_ps.return_value = mock_session
M365Provider.setup_powershell(
env_auth=False,
m365_credentials=credentials_dict,
identity=M365IdentityInfo(
identity_id=IDENTITY_ID,
@@ -1223,9 +960,8 @@ class TestM365Provider:
location=LOCATION,
),
)
assert exception.type == M365UserCredentialsError
assert "The provided User credentials are not valid." in str(exception.value)
assert exception.type == M365ConfigCredentialsError
assert "The provided credentials are not valid." in str(exception.value)
def test_validate_arguments_browser_auth_without_tenant_id(self):
"""Test validate_arguments with browser_auth but missing tenant_id"""
@@ -1233,14 +969,11 @@ class TestM365Provider:
M365Provider.validate_arguments(
az_cli_auth=False,
sp_env_auth=False,
env_auth=False,
browser_auth=True,
certificate_auth=False,
tenant_id=None,
client_id=None,
client_secret=None,
user=None,
password=None,
certificate_content=None,
certificate_path=None,
)
@@ -1257,14 +990,11 @@ class TestM365Provider:
M365Provider.validate_arguments(
az_cli_auth=False,
sp_env_auth=False,
env_auth=False,
browser_auth=False,
certificate_auth=False,
tenant_id=TENANT_ID,
client_id=None,
client_secret=None,
user=None,
password=None,
certificate_content=None,
certificate_path=None,
)
@@ -1280,14 +1010,11 @@ class TestM365Provider:
M365Provider.validate_arguments(
az_cli_auth=False,
sp_env_auth=False,
env_auth=False,
browser_auth=False,
certificate_auth=False,
tenant_id=None,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
user=None,
password=None,
certificate_content=None,
certificate_path=None,
)
@@ -1326,8 +1053,6 @@ class TestM365Provider:
"tenant_id": TENANT_ID,
"client_id": CLIENT_ID,
"client_secret": None,
"user": None,
"password": None,
"certificate_content": certificate_content,
},
),
@@ -1392,8 +1117,6 @@ class TestM365Provider:
tenant_id=str(uuid4()),
client_id=str(uuid4()),
client_secret="test_secret",
user="test@example.com",
password="test_password",
)
assert exception.type == M365ClientIdAndClientSecretNotBelongingToTenantIdError
@@ -1419,8 +1142,6 @@ class TestM365Provider:
tenant_id=str(uuid4()),
client_id=str(uuid4()),
client_secret="test_secret",
user="test@example.com",
password="test_password",
)
assert exception.type == M365TenantIdAndClientSecretNotBelongingToClientIdError
@@ -1446,8 +1167,6 @@ class TestM365Provider:
tenant_id=str(uuid4()),
client_id=str(uuid4()),
client_secret="test_secret",
user="test@example.com",
password="test_password",
)
assert exception.type == M365TenantIdAndClientIdNotBelongingToClientSecretError
@@ -1529,7 +1248,6 @@ class TestM365Provider:
result = M365Provider.setup_session(
az_cli_auth=False,
sp_env_auth=False,
env_auth=False,
browser_auth=False,
certificate_auth=True,
certificate_path=None,
@@ -1580,7 +1298,6 @@ class TestM365Provider:
M365Provider.setup_session(
az_cli_auth=False,
sp_env_auth=False,
env_auth=False,
browser_auth=False,
certificate_auth=True,
certificate_path=None,
@@ -1600,8 +1317,6 @@ class TestM365Provider:
"tenant_id": TENANT_ID,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"user": None,
"password": None,
"certificate_content": certificate_content,
}
@@ -1623,7 +1338,6 @@ class TestM365Provider:
result = M365Provider.setup_session(
az_cli_auth=False,
sp_env_auth=False,
env_auth=False,
browser_auth=False,
certificate_auth=False,
certificate_path=None,
@@ -1671,14 +1385,11 @@ class TestM365Provider:
M365Provider.validate_arguments(
az_cli_auth=False,
sp_env_auth=False,
env_auth=False,
browser_auth=False,
certificate_auth=True,
tenant_id=TENANT_ID,
client_id=CLIENT_ID,
client_secret=None,
user=None,
password=None,
certificate_content=certificate_content,
certificate_path=None,
)
@@ -1689,14 +1400,11 @@ class TestM365Provider:
M365Provider.validate_arguments(
az_cli_auth=False,
sp_env_auth=False,
env_auth=False,
browser_auth=False,
certificate_auth=False,
tenant_id=TENANT_ID,
client_id=CLIENT_ID,
client_secret=None,
user=None,
password=None,
certificate_content=None,
certificate_path=None,
)
@@ -1991,7 +1699,6 @@ class TestM365Provider:
result = M365Provider.setup_session(
az_cli_auth=False,
sp_env_auth=False,
env_auth=False,
browser_auth=False,
certificate_auth=True,
certificate_path=certificate_path,
@@ -2018,8 +1725,6 @@ class TestM365Provider:
"tenant_id": TENANT_ID,
"client_id": CLIENT_ID,
"client_secret": None,
"user": None,
"password": None,
"certificate_content": None,
"certificate_path": certificate_path,
}
@@ -2036,7 +1741,6 @@ class TestM365Provider:
result = M365Provider.setup_session(
az_cli_auth=False,
sp_env_auth=False,
env_auth=False,
browser_auth=False,
certificate_auth=False,
certificate_path=None,