From c938a2569322e5ef804490c4032833433f2bd4f0 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Fri, 2 May 2025 12:44:39 +0200 Subject: [PATCH] feat(exchange): add new check `exchange_organization_modern_authentication_enabled` (#7636) Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com> --- prowler/CHANGELOG.md | 1 + .../__init__.py | 0 ...odern_authentication_enabled.metadata.json | 30 ++++ ...anization_modern_authentication_enabled.py | 46 +++++++ .../services/exchange/exchange_service.py | 4 + ...anization_mailbox_auditing_enabled_test.py | 2 + ...ange_organization_mailtips_enabled_test.py | 2 + ...tion_modern_authentication_enabled_test.py | 130 ++++++++++++++++++ .../exchange/exchange_service_test.py | 2 + 9 files changed, 217 insertions(+) create mode 100644 prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/__init__.py create mode 100644 prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.metadata.json create mode 100644 prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.py create mode 100644 tests/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 19426c4e10..fc76ec6948 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -36,6 +36,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add new check `teams_meeting_presenters_restricted` [(#7613)](https://github.com/prowler-cloud/prowler/pull/7613) - Add new check `teams_meeting_chat_anonymous_users_disabled` [(#7579)](https://github.com/prowler-cloud/prowler/pull/7579) - Add Prowler Threat Score Compliance Framework [(#7603)](https://github.com/prowler-cloud/prowler/pull/7603) +- Add new check for Modern Authentication enabled for Exchange Online in M365 [(#7636)](https://github.com/prowler-cloud/prowler/pull/7636) - 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 Roles Assignment Policy with no AddIns for Exchange in M365 [(#7644)](https://github.com/prowler-cloud/prowler/pull/7644) diff --git a/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/__init__.py b/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.metadata.json new file mode 100644 index 0000000000..b78cd9cf19 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "m365", + "CheckID": "exchange_organization_modern_authentication_enabled", + "CheckTitle": "Ensure Modern Authentication for Exchange Online is enabled.", + "CheckType": [], + "ServiceName": "exchange", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "Exchange Organization Configuration", + "Description": "Ensure that modern authentication is enabled for Exchange Online, requiring exchange and mailboxes clients to use strong authentication mechanisms instead of basic authentication.", + "Risk": "If modern authentication is not enabled, Exchange Online email clients may fall back to basic authentication, making it easier for attackers to bypass multifactor authentication and compromise user credentials.", + "RelatedUrl": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/enable-or-disable-modern-authentication-in-exchange-online", + "Remediation": { + "Code": { + "CLI": "Set-OrganizationConfig -OAuth2ClientProfileEnabled $True", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable modern authentication in Exchange Online to enforce secure authentication methods for email clients.", + "Url": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/enable-or-disable-modern-authentication-in-exchange-online" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.py b/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.py new file mode 100644 index 0000000000..93b13d133d --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.py @@ -0,0 +1,46 @@ +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_organization_modern_authentication_enabled(Check): + """ + Check if Modern Authentication is enabled for Exchange Online. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check for Modern Authentication in Exchange Online. + + This method checks if Modern Authentication is enabled in the Exchange organization configuration. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + organization_config = exchange_client.organization_config + if organization_config: + report = CheckReportM365( + metadata=self.metadata(), + resource=organization_config, + resource_name=organization_config.name, + resource_id=organization_config.guid, + ) + report.status = "FAIL" + report.status_extended = ( + "Modern Authentication is not enabled for Exchange Online." + ) + + if organization_config.oauth_enabled: + report.status = "PASS" + report.status_extended = ( + "Modern Authentication is enabled for Exchange Online." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/exchange/exchange_service.py b/prowler/providers/m365/services/exchange/exchange_service.py index af2ecbce03..67f0564e74 100644 --- a/prowler/providers/m365/services/exchange/exchange_service.py +++ b/prowler/providers/m365/services/exchange/exchange_service.py @@ -44,6 +44,9 @@ class Exchange(M365Service): audit_disabled=organization_configuration.get( "AuditDisabled", False ), + oauth_enabled=organization_configuration.get( + "OAuth2ClientProfileEnabled", True + ), mailtips_enabled=organization_configuration.get( "MailTipsAllTipsEnabled", True ), @@ -235,6 +238,7 @@ class Organization(BaseModel): name: str guid: str audit_disabled: bool + oauth_enabled: bool mailtips_enabled: bool mailtips_external_recipient_enabled: bool mailtips_group_metrics_enabled: bool diff --git a/tests/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled_test.py b/tests/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled_test.py index 82860f3b7d..374c8a42c9 100644 --- a/tests/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled_test.py +++ b/tests/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled_test.py @@ -60,6 +60,7 @@ class Test_exchange_organization_mailbox_auditing_enabled: audit_disabled=True, name="test", guid="test", + oauth_enabled=True, mailtips_enabled=True, mailtips_external_recipient_enabled=True, mailtips_group_metrics_enabled=True, @@ -108,6 +109,7 @@ class Test_exchange_organization_mailbox_auditing_enabled: audit_disabled=False, name="test", guid="test", + oauth_enabled=True, mailtips_enabled=True, mailtips_external_recipient_enabled=True, mailtips_group_metrics_enabled=True, diff --git a/tests/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled_test.py b/tests/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled_test.py index cbcc6266c5..d3355b7f3e 100644 --- a/tests/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled_test.py +++ b/tests/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled_test.py @@ -64,6 +64,7 @@ class Test_exchange_organization_mailtips_enabled: name="test-org", guid="org-guid", audit_disabled=False, + oauth_enabled=True, mailtips_enabled=False, mailtips_external_recipient_enabled=False, mailtips_group_metrics_enabled=True, @@ -116,6 +117,7 @@ class Test_exchange_organization_mailtips_enabled: name="test-org", guid="org-guid", audit_disabled=False, + oauth_enabled=True, mailtips_enabled=True, mailtips_external_recipient_enabled=True, mailtips_group_metrics_enabled=True, diff --git a/tests/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled_test.py b/tests/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled_test.py new file mode 100644 index 0000000000..419453c397 --- /dev/null +++ b/tests/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled_test.py @@ -0,0 +1,130 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_exchange_organization_modern_authentication_enabled: + def test_no_organization(self): + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + exchange_client.organization_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_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled import ( + exchange_organization_modern_authentication_enabled, + ) + + check = exchange_organization_modern_authentication_enabled() + result = check.execute() + assert len(result) == 0 + + def test_modern_authentication_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_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled import ( + exchange_organization_modern_authentication_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + Organization, + ) + + exchange_client.organization_config = Organization( + oauth_enabled=False, + name="test", + guid="test", + audit_disabled=False, + mailtips_enabled=False, + mailtips_external_recipient_enabled=False, + mailtips_group_metrics_enabled=True, + mailtips_large_audience_threshold=25, + ) + + check = exchange_organization_modern_authentication_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Modern Authentication is not enabled for Exchange Online." + ) + assert result[0].resource == exchange_client.organization_config.dict() + assert result[0].resource_name == "test" + assert result[0].resource_id == "test" + assert result[0].location == "global" + + def test_modern_authentication_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_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled import ( + exchange_organization_modern_authentication_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + Organization, + ) + + exchange_client.organization_config = Organization( + oauth_enabled=True, + name="test", + guid="test", + audit_disabled=False, + mailtips_enabled=False, + mailtips_external_recipient_enabled=False, + mailtips_group_metrics_enabled=True, + mailtips_large_audience_threshold=25, + ) + + check = exchange_organization_modern_authentication_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Modern Authentication is enabled for Exchange Online." + ) + assert result[0].resource == exchange_client.organization_config.dict() + assert result[0].resource_name == "test" + assert result[0].resource_id == "test" + assert result[0].location == "global" diff --git a/tests/providers/m365/services/exchange/exchange_service_test.py b/tests/providers/m365/services/exchange/exchange_service_test.py index 0aa484aab0..afeb326e46 100644 --- a/tests/providers/m365/services/exchange/exchange_service_test.py +++ b/tests/providers/m365/services/exchange/exchange_service_test.py @@ -21,6 +21,7 @@ def mock_exchange_get_organization_config(_): audit_disabled=True, name="test", guid="test", + oauth_enabled=True, mailtips_enabled=True, mailtips_external_recipient_enabled=False, mailtips_group_metrics_enabled=True, @@ -183,6 +184,7 @@ class Test_Exchange_Service: assert organization_config.name == "test" assert organization_config.guid == "test" assert organization_config.audit_disabled is True + assert organization_config.oauth_enabled is True assert organization_config.mailtips_enabled is True assert organization_config.mailtips_external_recipient_enabled is False assert organization_config.mailtips_group_metrics_enabled is True