chore(m365_powershell): remove unnecessary test_credentials (#9204)

This commit is contained in:
Hugo Pereira Brito
2025-11-11 10:16:57 +01:00
committed by GitHub
parent 822d201159
commit 73a277f27b
5 changed files with 13 additions and 363 deletions

View File

@@ -42,6 +42,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS CodeArtifact service metadata to new format [(#8850)](https://github.com/prowler-cloud/prowler/pull/8850)
- Rename OCI provider to oraclecloud with oci alias [(#9126)](https://github.com/prowler-cloud/prowler/pull/9126)
- Remove unnecessary tests for M365_PowerShell module [(#9204)](https://github.com/prowler-cloud/prowler/pull/9204)
---

View File

@@ -2,10 +2,7 @@ import os
from prowler.lib.logger import logger
from prowler.lib.powershell.powershell import PowerShellSession
from prowler.providers.m365.exceptions.exceptions import (
M365CertificateCreationError,
M365GraphConnectionError,
)
from prowler.providers.m365.exceptions.exceptions import M365CertificateCreationError
from prowler.providers.m365.lib.jwt.jwt_decoder import decode_msal_token
from prowler.providers.m365.models import M365Credentials, M365IdentityInfo
@@ -138,79 +135,6 @@ class M365PowerShell(PowerShellSession):
result = self.execute(command, timeout=connect_timeout)
return result or "'execute_connect' command timeout reached"
def test_credentials(self, credentials: M365Credentials) -> bool:
"""
Test Microsoft 365 credentials by attempting to authenticate against Entra 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
authentication information to test.
Returns:
bool: True if credentials are valid and authentication succeeds, False otherwise.
"""
# Test Certificate Auth
if credentials.certificate_content and credentials.client_id:
try:
logger.info("Testing Microsoft Graph Certificate connection...")
self.test_graph_certificate_connection()
logger.info("Microsoft Graph Certificate connection successful")
teams_connection_successful = self.test_teams_certificate_connection()
if not teams_connection_successful:
self.test_exchange_certificate_connection()
return True
except Exception as e:
logger.error(f"Microsoft Graph Cer connection failed: {e}")
raise M365GraphConnectionError(
file=os.path.basename(__file__),
original_exception=e,
message="Check your Microsoft Application Certificate and ensure the app has proper permissions",
)
else:
try:
logger.info("Testing Microsoft Graph Client Secret connection...")
self.test_graph_connection()
logger.info("Microsoft Graph Client Secret connection successful")
return True
except Exception as e:
logger.error(f"Microsoft Graph Client Secret connection failed: {e}")
raise M365GraphConnectionError(
file=os.path.basename(__file__),
original_exception=e,
message="Check your Microsoft Application Client Secret and ensure the app has proper permissions",
)
def test_graph_connection(self) -> bool:
"""Test Microsoft Graph API connection and raise exception if it fails."""
try:
if self.execute("Write-Output $graphToken") == "":
raise M365GraphConnectionError(
file=os.path.basename(__file__),
message="Microsoft Graph token is empty or invalid.",
)
return True
except Exception as e:
logger.error(f"Microsoft Graph connection failed: {e}")
raise M365GraphConnectionError(
file=os.path.basename(__file__),
original_exception=e,
message=f"Failed to connect to Microsoft Graph API: {str(e)}",
)
def test_graph_certificate_connection(self) -> bool:
"""Test Microsoft Graph API connection using certificate and raise exception if it fails."""
result = self.execute_connect(
"Connect-Graph -Certificate $certificate -AppId $clientID -TenantId $tenantID"
)
if "Welcome to Microsoft Graph!" not in result:
logger.error(f"Microsoft Graph Certificate connection failed: {result}")
return False
return True
def test_teams_connection(self) -> bool:
"""Test Microsoft Teams API connection and raise exception if it fails."""
try:
@@ -926,7 +850,10 @@ def initialize_m365_powershell_modules():
bool: True if all modules were successfully initialized, False otherwise
"""
REQUIRED_MODULES = ["ExchangeOnlineManagement", "MicrosoftTeams", "MSAL.PS"]
REQUIRED_MODULES = [
"ExchangeOnlineManagement",
"MicrosoftTeams",
]
pwsh = PowerShellSession()
try:
@@ -938,7 +865,7 @@ def initialize_m365_powershell_modules():
# Install module if not installed
if not result:
install_result = pwsh.execute(
f'Install-Module "{module}" -Force -AllowClobber -Scope CurrentUser',
f"Install-Module {module} -Force -AllowClobber -Scope CurrentUser",
timeout=60,
)
if install_result:

View File

@@ -444,12 +444,7 @@ class M365Provider(Provider):
try:
if init_modules:
initialize_m365_powershell_modules()
if test_session.test_credentials(credentials):
return credentials
raise M365ConfigCredentialsError(
file=os.path.basename(__file__),
message="The provided credentials are not valid.",
)
return credentials
finally:
test_session.close()

View File

@@ -4,10 +4,7 @@ from unittest.mock import MagicMock, call, patch
import pytest
from prowler.lib.powershell.powershell import PowerShellSession
from prowler.providers.m365.exceptions.exceptions import (
M365CertificateCreationError,
M365GraphConnectionError,
)
from prowler.providers.m365.exceptions.exceptions import M365CertificateCreationError
from prowler.providers.m365.lib.powershell.m365_powershell import M365PowerShell
from prowler.providers.m365.models import M365Credentials, M365IdentityInfo
@@ -115,31 +112,6 @@ class Testm365PowerShell:
)
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(
client_id="test_client_id",
client_secret="test_client_secret",
tenant_id="test_tenant_id",
)
identity = M365IdentityInfo(
identity_id="test_id",
identity_type="Service Principal",
tenant_id="test_tenant",
tenant_domain="contoso.onmicrosoft.com",
tenant_domains=["contoso.onmicrosoft.com"],
location="test_location",
)
session = M365PowerShell(credentials, identity)
session.execute = MagicMock(return_value="sometoken")
result = session.test_credentials(credentials)
assert result is True
session.execute.assert_any_call("Write-Output $graphToken")
session.close()
@patch("subprocess.Popen")
def test_remove_ansi(self, mock_popen):
credentials = M365Credentials(
@@ -339,13 +311,14 @@ class Testm365PowerShell:
# Verify successful initialization
assert result is True
# Verify that execute was called for each module
assert mock_execute_obj.call_count == 9 # 3 modules * 3 commands each
assert (
mock_execute_obj.call_count == 2 * 3
) # number of modules * 3 commands each
# Verify success messages were logged
mock_info.assert_any_call(
"Successfully installed module ExchangeOnlineManagement"
)
mock_info.assert_any_call("Successfully installed module MicrosoftTeams")
mock_info.assert_any_call("Successfully installed module MSAL.PS")
@patch("subprocess.Popen")
def test_initialize_m365_powershell_modules_failure(self, mock_popen):
@@ -408,12 +381,11 @@ class Testm365PowerShell:
main()
# Verify all info messages were logged in the correct order
assert mock_info.call_count == 4
assert mock_info.call_count == 3
mock_info.assert_has_calls(
[
call("Successfully installed module ExchangeOnlineManagement"),
call("Successfully installed module MicrosoftTeams"),
call("Successfully installed module MSAL.PS"),
call("M365 PowerShell modules initialized successfully"),
]
)
@@ -456,96 +428,6 @@ class Testm365PowerShell:
# Verify no info messages were logged
mock_info.assert_not_called()
@patch("subprocess.Popen")
def test_test_graph_connection_success(self, mock_popen):
"""Test test_graph_connection when token is valid"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
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",
tenant_id="test_tenant",
tenant_domain="example.com",
tenant_domains=["example.com"],
location="test_location",
)
session = M365PowerShell(credentials, identity)
# Mock execute to return a valid token
session.execute = MagicMock(return_value="valid_token")
result = session.test_graph_connection()
assert result is True
session.execute.assert_called_once_with("Write-Output $graphToken")
session.close()
@patch("subprocess.Popen")
def test_test_graph_connection_empty_token(self, mock_popen):
"""Test test_graph_connection when token is empty"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
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",
tenant_id="test_tenant",
tenant_domain="example.com",
tenant_domains=["example.com"],
location="test_location",
)
session = M365PowerShell(credentials, identity)
# Mock execute to return empty token
session.execute = MagicMock(return_value="")
with pytest.raises(M365GraphConnectionError) as exc_info:
session.test_graph_connection()
assert "Microsoft Graph token is empty or invalid" in str(exc_info.value)
session.execute.assert_called_once_with("Write-Output $graphToken")
session.close()
@patch("subprocess.Popen")
def test_test_graph_connection_exception(self, mock_popen):
"""Test test_graph_connection when an exception occurs"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
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",
tenant_id="test_tenant",
tenant_domain="example.com",
tenant_domains=["example.com"],
location="test_location",
)
session = M365PowerShell(credentials, identity)
# Mock execute to raise an exception
session.execute = MagicMock(side_effect=Exception("PowerShell error"))
with pytest.raises(M365GraphConnectionError) as exc_info:
session.test_graph_connection()
assert "Failed to connect to Microsoft Graph API: PowerShell error" in str(
exc_info.value
)
session.close()
@patch("subprocess.Popen")
def test_test_teams_connection_success(self, mock_popen):
"""Test test_teams_connection when token is valid"""
@@ -1007,36 +889,6 @@ class Testm365PowerShell:
session.close()
@patch("subprocess.Popen")
def test_test_credentials_certificate_auth_success(self, mock_popen):
"""Test test_credentials method with certificate authentication - successful"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8")
credentials = M365Credentials(
client_id="test_client_id", certificate_content=certificate_content
)
identity = M365IdentityInfo()
# Create session without calling init_credential
with patch.object(M365PowerShell, "init_credential"):
session = M365PowerShell(credentials, identity)
# Mock successful certificate connections
# Note: The actual implementation uses "or" so if teams succeeds, exchange won't be called
session.test_teams_certificate_connection = MagicMock(return_value=True)
session.test_exchange_certificate_connection = MagicMock(return_value=True)
result = session.test_credentials(credentials)
assert result is True
session.test_teams_certificate_connection.assert_called_once()
# Exchange connection should NOT be called if teams connection succeeds (due to "or" logic)
session.test_exchange_certificate_connection.assert_not_called()
session.close()
@patch("subprocess.Popen")
def test_test_credentials_certificate_auth_failure(self, mock_popen):
"""Test test_credentials method with certificate authentication - failure"""
@@ -1057,9 +909,6 @@ class Testm365PowerShell:
session.test_teams_certificate_connection = MagicMock(return_value=False)
session.test_exchange_certificate_connection = MagicMock(return_value=False)
result = session.test_credentials(credentials)
assert result is True # Method always returns True after the try block
session.close()
@patch("subprocess.Popen")
@@ -1287,94 +1136,3 @@ class Testm365PowerShell:
assert any('$tenantDomain = "contoso.com"' in cmd for cmd in executed_commands)
session.close()
@patch("subprocess.Popen")
def test_test_credentials_certificate_auth_with_or_logic(self, mock_popen):
"""Test test_credentials method with certificate auth using OR logic between Teams and Exchange"""
certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8")
mock_process = MagicMock()
mock_popen.return_value = mock_process
mock_process.returncode = 0
# Create session with non-certificate credentials first
session = M365PowerShell(
M365Credentials(
client_id="test_client_id",
client_secret="test_secret",
tenant_id="test_tenant_id",
tenant_domains=["contoso.com"],
),
M365IdentityInfo(
tenant_id="test_tenant_id",
tenant_domain="contoso.com",
tenant_domains=["contoso.com"],
identity_id="test_identity_id",
identity_type="Service Principal with Certificate",
),
)
# Mock that Teams connection fails but Exchange succeeds
session.test_teams_certificate_connection = MagicMock(return_value=False)
session.test_exchange_certificate_connection = MagicMock(return_value=True)
result = session.test_credentials(
M365Credentials(
client_id="test_client_id",
tenant_id="test_tenant_id",
certificate_content=certificate_content,
tenant_domains=["contoso.com"],
)
)
assert result is True
session.test_teams_certificate_connection.assert_called_once()
session.test_exchange_certificate_connection.assert_called_once()
session.close()
@patch("subprocess.Popen")
def test_test_credentials_certificate_auth_both_fail(self, mock_popen):
"""Test test_credentials method with certificate auth when both Teams and Exchange fail"""
certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8")
mock_process = MagicMock()
mock_popen.return_value = mock_process
mock_process.returncode = 0
# Create session with non-certificate credentials first
session = M365PowerShell(
M365Credentials(
client_id="test_client_id",
client_secret="test_secret",
tenant_id="test_tenant_id",
tenant_domains=["contoso.com"],
),
M365IdentityInfo(
tenant_id="test_tenant_id",
tenant_domain="contoso.com",
tenant_domains=["contoso.com"],
identity_id="test_identity_id",
identity_type="Service Principal with Certificate",
),
)
# Mock that both connections fail
session.test_teams_certificate_connection = MagicMock(return_value=False)
session.test_exchange_certificate_connection = MagicMock(return_value=False)
# Even when both fail, the method should return True (this is the intended logic)
result = session.test_credentials(
M365Credentials(
client_id="test_client_id",
tenant_id="test_tenant_id",
certificate_content=certificate_content,
tenant_domains=["contoso.com"],
)
)
assert result is True
session.test_teams_certificate_connection.assert_called_once()
session.test_exchange_certificate_connection.assert_called_once()
session.close()

View File

@@ -932,37 +932,6 @@ class TestM365Provider:
assert result.certificate_content == certificate_content
assert identity.identity_type == "Service Principal with Certificate"
def test_setup_powershell_invalid_credentials(self):
"""Test setup_powershell with invalid credentials"""
credentials_dict = {
"client_id": "test_client_id",
"tenant_id": "test_tenant_id",
"client_secret": "test_client_secret",
}
with (
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(
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 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"""
with pytest.raises(M365BrowserAuthNoTenantIDError) as exception: