From ab6d05637dbdbd114aa029f00344cc511881f41c Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Fri, 2 May 2025 08:46:14 +0200 Subject: [PATCH] feat(exchange): add new check `exchange_organization_mailtips_enabled` (#7637) Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com> --- docs/tutorials/configuration_file.md | 5 + prowler/CHANGELOG.md | 1 + prowler/config/config.py | 2 +- prowler/config/config.yaml | 3 + .../__init__.py | 0 ...rganization_mailtips_enabled.metadata.json | 30 ++++ .../exchange_organization_mailtips_enabled.py | 54 +++++++ .../services/exchange/exchange_service.py | 16 +++ tests/config/fixtures/config.yaml | 13 ++ ...anization_mailbox_auditing_enabled_test.py | 16 ++- ...ange_organization_mailtips_enabled_test.py | 136 ++++++++++++++++++ .../exchange/exchange_service_test.py | 14 +- 12 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/__init__.py create mode 100644 prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.metadata.json create mode 100644 prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.py create mode 100644 tests/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled_test.py diff --git a/docs/tutorials/configuration_file.md b/docs/tutorials/configuration_file.md index 9ee25ea7e7..78b9865183 100644 --- a/docs/tutorials/configuration_file.md +++ b/docs/tutorials/configuration_file.md @@ -106,6 +106,7 @@ The following list includes all the Microsoft 365 checks with configurable varia |---------------------------------------------------------------|--------------------------------------------------|-----------------| | `entra_admin_users_sign_in_frequency_enabled` | `sign_in_frequency` | Integer | | `teams_external_file_sharing_restricted` | `allowed_cloud_storage_services` | List of Strings | +| `exchange_organization_mailtips_enabled` | `recommended_mailtips_large_audience_threshold` | Integer | ## Config YAML File Structure @@ -520,5 +521,9 @@ m365: #"allow_google_drive", #"allow_share_file", ] + # Exchange Organization Settings + # m365.exchange_organization_mailtips_enabled + recommended_mailtips_large_audience_threshold: 25 # maximum number of recipients + ``` diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 181d775120..886611fa82 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 MailTips full enabled for Exchange in M365 [(#7637)](https://github.com/prowler-cloud/prowler/pull/7637) ### Fixed diff --git a/prowler/config/config.py b/prowler/config/config.py index bcb7eb8b8c..62f4987879 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -132,7 +132,7 @@ def load_and_validate_config_file(provider: str, config_file_path: str) -> dict: else: config = config_file if config_file else {} # Not to break Azure, K8s and GCP does not support or use the old config format - if provider in ["azure", "gcp", "kubernetes"]: + if provider in ["azure", "gcp", "kubernetes", "m365"]: config = {} return config diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 8c6d0e4dfb..062cae1644 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -493,3 +493,6 @@ m365: #"allow_google_drive", #"allow_share_file", ] + # Exchange Organization Settings + # m365.exchange_organization_mailtips_enabled + recommended_mailtips_large_audience_threshold: 25 # maximum number of recipients diff --git a/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/__init__.py b/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.metadata.json new file mode 100644 index 0000000000..bb727e4c3f --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "m365", + "CheckID": "exchange_organization_mailtips_enabled", + "CheckTitle": "Ensure MailTips are enabled for end users.", + "CheckType": [], + "ServiceName": "exchange", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Exchange Organization Configuration", + "Description": "Ensure that MailTips are enabled in Exchange Online to provide users with informative messages while composing emails, helping to avoid issues such as sending to large groups or external recipients unintentionally.", + "Risk": "Without MailTips, users may inadvertently send sensitive information externally or generate non-delivery reports, leading to communication errors and potential data exposure.", + "RelatedUrl": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/mailtips/mailtips", + "Remediation": { + "Code": { + "CLI": "$TipsParams = @{ MailTipsAllTipsEnabled = $true; MailTipsExternalRecipientsTipsEnabled = $true; MailTipsGroupMetricsEnabled = $true; MailTipsLargeAudienceThreshold = '25' }; Set-OrganizationConfig @TipsParams", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable MailTips features in Exchange Online and configure the large audience threshold appropriately to assist users when composing emails.", + "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-organizationconfig?view=exchange-ps" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.py b/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.py new file mode 100644 index 0000000000..52aa8dc060 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.py @@ -0,0 +1,54 @@ +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_mailtips_enabled(Check): + """ + Check if MailTips are enabled for Exchange Online. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check for MailTips in Exchange Online. + + This method checks if MailTips are 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 = ( + "MailTips are not fully enabled for Exchange Online." + ) + + if ( + organization_config.mailtips_enabled + and organization_config.mailtips_external_recipient_enabled + and organization_config.mailtips_group_metrics_enabled + and organization_config.mailtips_large_audience_threshold + <= exchange_client.audit_config.get( + "recommended_mailtips_large_audience_threshold", 25 + ) + ): + report.status = "PASS" + report.status_extended = ( + "MailTips are fully 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 dbcc44499f..eb76c7bf9e 100644 --- a/prowler/providers/m365/services/exchange/exchange_service.py +++ b/prowler/providers/m365/services/exchange/exchange_service.py @@ -37,6 +37,18 @@ class Exchange(M365Service): audit_disabled=organization_configuration.get( "AuditDisabled", False ), + mailtips_enabled=organization_configuration.get( + "MailTipsAllTipsEnabled", True + ), + mailtips_external_recipient_enabled=organization_configuration.get( + "MailTipsExternalRecipientsTipsEnabled", False + ), + mailtips_group_metrics_enabled=organization_configuration.get( + "MailTipsGroupMetricsEnabled", True + ), + mailtips_large_audience_threshold=organization_configuration.get( + "MailTipsLargeAudienceThreshold", 25 + ), ) except Exception as error: logger.error( @@ -137,6 +149,10 @@ class Organization(BaseModel): name: str guid: str audit_disabled: bool + mailtips_enabled: bool + mailtips_external_recipient_enabled: bool + mailtips_group_metrics_enabled: bool + mailtips_large_audience_threshold: int class MailboxAuditConfig(BaseModel): diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index 56215b8c41..6f6c5029fa 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -434,3 +434,16 @@ m365: # Conditional Access Policy # policy.session_controls.sign_in_frequency.frequency in hours sign_in_frequency: 4 + # Teams Settings + # m365.teams_external_file_sharing_restricted + allowed_cloud_storage_services: + [ + #"allow_box", + #"allow_drop_box", + #"allow_egnyte", + #"allow_google_drive", + #"allow_share_file", + ] + # Exchange Organization Settings + # m365.exchange_organization_mailtips_enabled + recommended_mailtips_large_audience_threshold: 25 # maximum number of recipients 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 104b050541..82860f3b7d 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 @@ -57,7 +57,13 @@ class Test_exchange_organization_mailbox_auditing_enabled: ) exchange_client.organization_config = Organization( - audit_disabled=True, name="test", guid="test" + audit_disabled=True, + name="test", + guid="test", + mailtips_enabled=True, + mailtips_external_recipient_enabled=True, + mailtips_group_metrics_enabled=True, + mailtips_large_audience_threshold=25, ) check = exchange_organization_mailbox_auditing_enabled() @@ -99,7 +105,13 @@ class Test_exchange_organization_mailbox_auditing_enabled: ) exchange_client.organization_config = Organization( - audit_disabled=False, name="test", guid="test" + audit_disabled=False, + name="test", + guid="test", + mailtips_enabled=True, + mailtips_external_recipient_enabled=True, + mailtips_group_metrics_enabled=True, + mailtips_large_audience_threshold=25, ) check = exchange_organization_mailbox_auditing_enabled() 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 new file mode 100644 index 0000000000..cbcc6266c5 --- /dev/null +++ b/tests/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled_test.py @@ -0,0 +1,136 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_exchange_organization_mailtips_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_mailtips_enabled.exchange_organization_mailtips_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_mailtips_enabled.exchange_organization_mailtips_enabled import ( + exchange_organization_mailtips_enabled, + ) + + check = exchange_organization_mailtips_enabled() + result = check.execute() + assert result == [] + + def test_mailtips_not_fully_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_mailtips_enabled.exchange_organization_mailtips_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_mailtips_enabled.exchange_organization_mailtips_enabled import ( + exchange_organization_mailtips_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + Organization, + ) + + exchange_client.audit_config = { + "recommended_mailtips_large_audience_threshold": 25 + } + + exchange_client.organization_config = Organization( + name="test-org", + guid="org-guid", + audit_disabled=False, + mailtips_enabled=False, + mailtips_external_recipient_enabled=False, + mailtips_group_metrics_enabled=True, + mailtips_large_audience_threshold=25, + ) + + check = exchange_organization_mailtips_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "MailTips are not fully enabled for Exchange Online." + ) + assert result[0].resource_name == "test-org" + assert result[0].resource_id == "org-guid" + assert result[0].location == "global" + assert result[0].resource == exchange_client.organization_config.dict() + + def test_mailtips_fully_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_mailtips_enabled.exchange_organization_mailtips_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_mailtips_enabled.exchange_organization_mailtips_enabled import ( + exchange_organization_mailtips_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + Organization, + ) + + exchange_client.audit_config = { + "recommended_mailtips_large_audience_threshold": 25 + } + + exchange_client.organization_config = Organization( + name="test-org", + guid="org-guid", + audit_disabled=False, + mailtips_enabled=True, + mailtips_external_recipient_enabled=True, + mailtips_group_metrics_enabled=True, + mailtips_large_audience_threshold=25, + ) + + check = exchange_organization_mailtips_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "MailTips are fully enabled for Exchange Online." + ) + assert result[0].resource_name == "test-org" + assert result[0].resource_id == "org-guid" + assert result[0].location == "global" + assert result[0].resource == exchange_client.organization_config.dict() diff --git a/tests/providers/m365/services/exchange/exchange_service_test.py b/tests/providers/m365/services/exchange/exchange_service_test.py index 42cc16a507..0a30e0c7f7 100644 --- a/tests/providers/m365/services/exchange/exchange_service_test.py +++ b/tests/providers/m365/services/exchange/exchange_service_test.py @@ -14,7 +14,15 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider def mock_exchange_get_organization_config(_): - return Organization(audit_disabled=True, name="test", guid="test") + return Organization( + audit_disabled=True, + name="test", + guid="test", + mailtips_enabled=True, + mailtips_external_recipient_enabled=False, + mailtips_group_metrics_enabled=True, + mailtips_large_audience_threshold=25, + ) def mock_exchange_get_mailbox_audit_config(_): @@ -94,6 +102,10 @@ class Test_Exchange_Service: assert organization_config.name == "test" assert organization_config.guid == "test" assert organization_config.audit_disabled 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 + assert organization_config.mailtips_large_audience_threshold == 25 exchange_client.powershell.close()