diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 886611fa82..17cb4ada64 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -38,6 +38,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add Prowler Threat Score Compliance Framework [(#7603)](https://github.com/prowler-cloud/prowler/pull/7603) - Add new check `sharepoint_onedrive_sync_restricted_unmanaged_devices` [(#7589)](https://github.com/prowler-cloud/prowler/pull/7589) - Add new check for Additional Storage restricted for Exchange in M365 [(#7638)](https://github.com/prowler-cloud/prowler/pull/7638) +- Add new check for SMTP Auth disabled for Exchange in M365 [(#7640)](https://github.com/prowler-cloud/prowler/pull/7640) - Add new check for MailTips full enabled for Exchange in M365 [(#7637)](https://github.com/prowler-cloud/prowler/pull/7637) ### Fixed diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index df4a26dfd8..1046a45b7d 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -487,6 +487,23 @@ class M365PowerShell(PowerShellSession): "Get-HostedContentFilterPolicy | ConvertTo-Json", json_parse=True ) + def get_transport_config(self) -> dict: + """ + Get Exchange Online Transport Configuration. + + Retrieves the current transport configuration settings for Exchange Online. + + Returns: + dict: Transport configuration settings in JSON format. + + Example: + >>> get_transport_config() + { + "SmtpClientAuthenticationDisabled": True, + } + """ + return self.execute("Get-TransportConfig | ConvertTo-Json", json_parse=True) + # This function is used to install the required M365 PowerShell modules in Docker containers def initialize_m365_powershell_modules(): diff --git a/prowler/providers/m365/services/exchange/exchange_service.py b/prowler/providers/m365/services/exchange/exchange_service.py index eb76c7bf9e..03ad304c5e 100644 --- a/prowler/providers/m365/services/exchange/exchange_service.py +++ b/prowler/providers/m365/services/exchange/exchange_service.py @@ -14,6 +14,7 @@ class Exchange(M365Service): self.mailboxes_config = [] self.external_mail_config = [] self.transport_rules = [] + self.transport_config = None self.mailbox_policy = None if self.powershell: @@ -22,6 +23,7 @@ class Exchange(M365Service): self.mailboxes_config = self._get_mailbox_audit_config() self.external_mail_config = self._get_external_mail_config() self.transport_rules = self._get_transport_rules() + self.transport_config = self._get_transport_config() self.mailbox_policy = self._get_mailbox_policy() self.powershell.close() @@ -126,6 +128,23 @@ class Exchange(M365Service): ) return transport_rules + def _get_transport_config(self): + logger.info("Microsoft365 - Getting transport configuration...") + transport_config = [] + try: + transport_configuration = self.powershell.get_transport_config() + if transport_configuration: + transport_config = TransportConfig( + smtp_auth_disabled=transport_configuration.get( + "SmtpClientAuthenticationDisabled", False + ), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return transport_config + def _get_mailbox_policy(self): logger.info("Microsoft365 - Getting mailbox policy configuration...") mailboxes_policy = None @@ -172,6 +191,10 @@ class TransportRule(BaseModel): sender_domain_is: list[str] +class TransportConfig(BaseModel): + smtp_auth_disabled: bool + + class MailboxPolicy(BaseModel): id: str additional_storage_enabled: bool diff --git a/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/__init__.py b/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled.metadata.json new file mode 100644 index 0000000000..023efb30b3 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "m365", + "CheckID": "exchange_transport_config_smtp_auth_disabled", + "CheckTitle": "Ensure SMTP AUTH is disabled.", + "CheckType": [], + "ServiceName": "exchange", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Exchange Transport Config", + "Description": "Ensure that SMTP AUTH is disabled at the organization level in Exchange Online to reduce exposure to legacy protocols that can be exploited for malicious use.", + "Risk": "Leaving SMTP AUTH enabled allows legacy clients to authenticate using outdated methods, increasing the risk of credential compromise and unauthorized email sending.", + "RelatedUrl": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission", + "Remediation": { + "Code": { + "CLI": "Set-TransportConfig -SmtpClientAuthenticationDisabled $true", + "NativeIaC": "", + "Other": "1. Navigate to Exchange admin center https://admin.exchange.microsoft.com. 2. Select Settings > Mail flow. 3. Ensure 'Turn off SMTP AUTH protocol for your organization' is checked.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable SMTP AUTH at the organization level to support secure, modern authentication practices and block legacy protocol usage.", + "Url": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled.py b/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled.py new file mode 100644 index 0000000000..f842c59b87 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled.py @@ -0,0 +1,44 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.exchange.exchange_client import exchange_client + + +class exchange_transport_config_smtp_auth_disabled(Check): + """Check if SMTP AUTH is disabled in Exchange Online Transport Config. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """Execute the check for SMTP AUTH setting in Transport Config. + + This method checks if SMTP AUTH is disabled at the organization level in Exchange Online. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + transport_config = exchange_client.transport_config + if transport_config: + report = CheckReportM365( + metadata=self.metadata(), + resource=transport_config, + resource_name="Transport Configuration", + resource_id="transport_config", + ) + report.status = "FAIL" + report.status_extended = ( + "SMTP AUTH is enabled in the Exchange Online Transport Config." + ) + + if transport_config.smtp_auth_disabled: + report.status = "PASS" + report.status_extended = ( + "SMTP AUTH is disabled in the Exchange Online Transport Config." + ) + + findings.append(report) + + return findings diff --git a/tests/providers/m365/services/exchange/exchange_service_test.py b/tests/providers/m365/services/exchange/exchange_service_test.py index 0a30e0c7f7..60e6e853eb 100644 --- a/tests/providers/m365/services/exchange/exchange_service_test.py +++ b/tests/providers/m365/services/exchange/exchange_service_test.py @@ -8,6 +8,7 @@ from prowler.providers.m365.services.exchange.exchange_service import ( MailboxAuditConfig, MailboxPolicy, Organization, + TransportConfig, TransportRule, ) from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider @@ -60,6 +61,12 @@ def mock_exchange_get_transport_rules(_): ] +def mock_exchange_get_transport_config(_): + return TransportConfig( + smtp_auth_disabled=True, + ) + + def mock_exchange_get_mailbox_policy(_): return MailboxPolicy( id="test", @@ -203,3 +210,23 @@ class Test_Exchange_Service: assert mailbox_policy.id == "test" assert mailbox_policy.additional_storage_enabled is True exchange_client.powershell.close() + + @patch( + "prowler.providers.m365.services.exchange.exchange_service.Exchange._get_transport_config", + new=mock_exchange_get_transport_config, + ) + def test_get_transport_config(self): + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + ): + exchange_client = Exchange( + set_mocked_m365_provider( + identity=M365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + transport_config = exchange_client.transport_config + assert transport_config.smtp_auth_disabled is True + + exchange_client.powershell.close() diff --git a/tests/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled_test.py b/tests/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled_test.py new file mode 100644 index 0000000000..b1e904b473 --- /dev/null +++ b/tests/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled_test.py @@ -0,0 +1,112 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_exchange_transport_config_smtp_auth_disabled: + def test_no_transport_config(self): + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + exchange_client.transport_config = None + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_transport_config_smtp_auth_disabled.exchange_transport_config_smtp_auth_disabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_transport_config_smtp_auth_disabled.exchange_transport_config_smtp_auth_disabled import ( + exchange_transport_config_smtp_auth_disabled, + ) + + check = exchange_transport_config_smtp_auth_disabled() + result = check.execute() + assert len(result) == 0 + + def test_smtp_auth_enabled(self): + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_transport_config_smtp_auth_disabled.exchange_transport_config_smtp_auth_disabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_service import ( + TransportConfig, + ) + from prowler.providers.m365.services.exchange.exchange_transport_config_smtp_auth_disabled.exchange_transport_config_smtp_auth_disabled import ( + exchange_transport_config_smtp_auth_disabled, + ) + + exchange_client.transport_config = TransportConfig(smtp_auth_disabled=False) + + check = exchange_transport_config_smtp_auth_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "SMTP AUTH is enabled in the Exchange Online Transport Config." + ) + assert result[0].resource == exchange_client.transport_config.dict() + assert result[0].resource_name == "Transport Configuration" + assert result[0].resource_id == "transport_config" + assert result[0].location == "global" + + def test_smtp_auth_disabled(self): + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_transport_config_smtp_auth_disabled.exchange_transport_config_smtp_auth_disabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_service import ( + TransportConfig, + ) + from prowler.providers.m365.services.exchange.exchange_transport_config_smtp_auth_disabled.exchange_transport_config_smtp_auth_disabled import ( + exchange_transport_config_smtp_auth_disabled, + ) + + exchange_client.transport_config = TransportConfig(smtp_auth_disabled=True) + + check = exchange_transport_config_smtp_auth_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "SMTP AUTH is disabled in the Exchange Online Transport Config." + ) + assert result[0].resource == exchange_client.transport_config.dict() + assert result[0].resource_name == "Transport Configuration" + assert result[0].resource_id == "transport_config" + assert result[0].location == "global"