diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index ef4f2da150..1bb4df58ab 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add check for Bypass Disable in every Mailbox for service Defender in M365 [(#7418)](https://github.com/prowler-cloud/prowler/pull/7418) - Add new check `teams_external_domains_restricted` [(#7557)](https://github.com/prowler-cloud/prowler/pull/7557) - Add new check `teams_email_sending_to_channel_disabled` [(#7533)](https://github.com/prowler-cloud/prowler/pull/7533) +- Add new check for External Mails Tagged for service Exchange in M365 [(#7580)](https://github.com/prowler-cloud/prowler/pull/7580) - Add new check for WhiteList not used in Transport Rules for service Defender in M365 [(#7569)](https://github.com/prowler-cloud/prowler/pull/7569) - Add check for Inbound Antispam Policy with no allowed domains from service Defender in M365 [(#7500)](https://github.com/prowler-cloud/prowler/pull/7500) - Add new check `teams_meeting_anonymous_user_join_disabled` [(#7565)](https://github.com/prowler-cloud/prowler/pull/7565) diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 7a47a2025f..8e4df5b94d 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -342,6 +342,24 @@ class M365PowerShell(PowerShellSession): """ return self.execute("Get-MailboxAuditBypassAssociation | ConvertTo-Json") + def get_external_mail_config(self) -> dict: + """ + Get Exchange Online External Mail Configuration. + + Retrieves the current external mail configuration settings for Exchange Online. + + Returns: + dict: External mail configuration settings in JSON format. + + Example: + >>> get_external_mail_config() + { + "Identity": "MyExternalMail", + "ExternalMailTagEnabled": true + } + """ + return self.execute("Get-ExternalInOutlook | ConvertTo-Json") + def get_transport_rules(self) -> dict: """ Get Exchange Online Transport Rules. diff --git a/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/__init__.py b/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled.metadata.json new file mode 100644 index 0000000000..a1ddac7890 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "m365", + "CheckID": "exchange_external_email_tagging_enabled", + "CheckTitle": "Ensure email from external senders is identified.", + "CheckType": [], + "ServiceName": "exchange", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Exchange External Mail Tagging", + "Description": "Ensure that emails from external senders are identified using the native External tag experience in Outlook clients, which helps users recognize messages originating outside the organization.", + "Risk": "If external email tagging is not enabled, users may be unable to quickly identify emails coming from outside the organization, increasing the risk of phishing or social engineering attacks.", + "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-externalinoutlook?view=exchange-ps", + "Remediation": { + "Code": { + "CLI": "Set-ExternalInOutlook -Enabled $true", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable the External tag for Outlook to help users visually identify emails from outside the organization.", + "Url": "https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled.py b/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled.py new file mode 100644 index 0000000000..5d2c3eeb81 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled.py @@ -0,0 +1,41 @@ +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_external_email_tagging_enabled(Check): + """Ensure email from external senders is identified. + + This check verifies that the native "External" sender tag feature is enabled + in Exchange so that messages from outside the organization are automatically marked. + """ + + def execute(self) -> List[CheckReportM365]: + """Run the check to validate that external sender tagging is enabled. + + Iterates through the external mail configuration to determine if the + ExternalInOutlook setting is turned on and generates a report accordingly. + + Returns: + List[CheckReportM365]: A list of reports for each organization identity. + """ + findings = [] + + for mail_config in exchange_client.external_mail_config: + report = CheckReportM365( + metadata=self.metadata(), + resource=mail_config, + resource_name=mail_config.identity, + resource_id=mail_config.identity, + ) + report.status = "FAIL" + report.status_extended = f"External sender tagging is disabled for Exchange identity {mail_config.identity}." + + if mail_config.external_mail_tag_enabled: + report.status = "PASS" + report.status_extended = f"External sender tagging is enabled for Exchange identity {mail_config.identity}." + + 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 9fd547f7b7..c1d544787f 100644 --- a/prowler/providers/m365/services/exchange/exchange_service.py +++ b/prowler/providers/m365/services/exchange/exchange_service.py @@ -13,6 +13,7 @@ class Exchange(M365Service): self.powershell.connect_exchange_online() self.organization_config = self._get_organization_config() 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.powershell.close() @@ -56,6 +57,31 @@ class Exchange(M365Service): ) return mailboxes_config + def _get_external_mail_config(self): + logger.info("Microsoft365 - Getting external mail configuration...") + external_mail_config = [] + try: + external_mail_configuration = self.powershell.get_external_mail_config() + if not external_mail_configuration: + return external_mail_config + if isinstance(external_mail_configuration, dict): + external_mail_configuration = [external_mail_configuration] + for external_mail in external_mail_configuration: + if external_mail: + external_mail_config.append( + ExternalMailConfig( + identity=external_mail.get("Identity", ""), + external_mail_tag_enabled=external_mail.get( + "Enabled", False + ), + ) + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return external_mail_config + def _get_transport_rules(self): logger.info("Microsoft365 - Getting transport rules configuration...") transport_rules = [] @@ -93,6 +119,11 @@ class MailboxAuditConfig(BaseModel): audit_bypass_enabled: bool +class ExternalMailConfig(BaseModel): + identity: str + external_mail_tag_enabled: bool + + class TransportRule(BaseModel): name: str scl: Optional[int] diff --git a/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.py b/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.py index ec74c9ff75..58cd7b23bc 100644 --- a/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.py +++ b/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.py @@ -35,12 +35,12 @@ class exchange_transport_rules_whitelist_disabled(Check): report.status = "PASS" report.status_extended = ( - f"Transport rule '{rule.name}' does not whitelist any domains." + f"Transport rule {rule.name} does not whitelist any domains." ) if rule.sender_domain_is and rule.scl == -1: report.status = "FAIL" - report.status_extended = f"Transport rule '{rule.name}' whitelists domains: {', '.join(rule.sender_domain_is)}." + report.status_extended = f"Transport rule {rule.name} whitelists domains: {', '.join(rule.sender_domain_is)}." findings.append(report) diff --git a/tests/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled_test.py b/tests/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled_test.py new file mode 100644 index 0000000000..557461d935 --- /dev/null +++ b/tests/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled_test.py @@ -0,0 +1,173 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_exchange_external_email_tagging_enabled: + def test_external_tagging_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_external_email_tagging_enabled.exchange_external_email_tagging_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled import ( + exchange_external_email_tagging_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + ExternalMailConfig, + ) + + exchange_client.external_mail_config = [ + ExternalMailConfig(identity="Org1", external_mail_tag_enabled=True) + ] + + check = exchange_external_email_tagging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "External sender tagging is enabled for Exchange identity Org1." + ) + assert result[0].resource == exchange_client.external_mail_config[0].dict() + assert result[0].resource_name == "Org1" + assert result[0].resource_id == "Org1" + assert result[0].location == "global" + + def test_external_tagging_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_external_email_tagging_enabled.exchange_external_email_tagging_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled import ( + exchange_external_email_tagging_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + ExternalMailConfig, + ) + + exchange_client.external_mail_config = [ + ExternalMailConfig(identity="Org2", external_mail_tag_enabled=False) + ] + + check = exchange_external_email_tagging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "External sender tagging is disabled for Exchange identity Org2." + ) + assert result[0].resource == exchange_client.external_mail_config[0].dict() + assert result[0].resource_name == "Org2" + assert result[0].resource_id == "Org2" + assert result[0].location == "global" + + def test_multiple_configs_mixed_status(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_external_email_tagging_enabled.exchange_external_email_tagging_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled import ( + exchange_external_email_tagging_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + ExternalMailConfig, + ) + + exchange_client.external_mail_config = [ + ExternalMailConfig( + identity="OrgEnabled", external_mail_tag_enabled=True + ), + ExternalMailConfig( + identity="OrgDisabled", external_mail_tag_enabled=False + ), + ] + + check = exchange_external_email_tagging_enabled() + result = check.execute() + + assert len(result) == 2 + + assert result[0].status == "PASS" + assert result[0].resource_name == "OrgEnabled" + assert ( + result[0].status_extended + == "External sender tagging is enabled for Exchange identity OrgEnabled." + ) + + assert result[1].status == "FAIL" + assert result[1].resource_name == "OrgDisabled" + assert ( + result[1].status_extended + == "External sender tagging is disabled for Exchange identity OrgDisabled." + ) + + def test_no_mail_configs(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_external_email_tagging_enabled.exchange_external_email_tagging_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled import ( + exchange_external_email_tagging_enabled, + ) + + exchange_client.external_mail_config = [] + + check = exchange_external_email_tagging_enabled() + result = check.execute() + + assert len(result) == 0 diff --git a/tests/providers/m365/services/exchange/exchange_service_test.py b/tests/providers/m365/services/exchange/exchange_service_test.py index 1d74474e3d..8131157265 100644 --- a/tests/providers/m365/services/exchange/exchange_service_test.py +++ b/tests/providers/m365/services/exchange/exchange_service_test.py @@ -4,6 +4,7 @@ from unittest.mock import patch from prowler.providers.m365.models import M365IdentityInfo from prowler.providers.m365.services.exchange.exchange_service import ( Exchange, + ExternalMailConfig, MailboxAuditConfig, Organization, TransportRule, @@ -22,6 +23,19 @@ def mock_exchange_get_mailbox_audit_config(_): ] +def mock_exchange_get_external_mail_config(_): + return [ + ExternalMailConfig( + identity="test", + external_mail_tag_enabled=True, + ), + ExternalMailConfig( + identity="test2", + external_mail_tag_enabled=False, + ), + ] + + def mock_exchange_get_transport_rules(_): return [ TransportRule( @@ -101,6 +115,29 @@ class Test_Exchange_Service: exchange_client.powershell.close() + @patch( + "prowler.providers.m365.services.exchange.exchange_service.Exchange._get_external_mail_config", + new=mock_exchange_get_external_mail_config, + ) + def test_get_external_mail_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) + ) + ) + external_mail_config = exchange_client.external_mail_config + assert len(external_mail_config) == 2 + assert external_mail_config[0].identity == "test" + assert external_mail_config[0].external_mail_tag_enabled is True + assert external_mail_config[1].identity == "test2" + assert external_mail_config[1].external_mail_tag_enabled is False + exchange_client.powershell.close() + @patch( "prowler.providers.m365.services.exchange.exchange_service.Exchange._get_transport_rules", new=mock_exchange_get_transport_rules, diff --git a/tests/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled_test.py b/tests/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled_test.py index ad51a64b58..daea4fd1de 100644 --- a/tests/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled_test.py +++ b/tests/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled_test.py @@ -41,14 +41,14 @@ class Test_exchange_transport_rules_whitelist_disabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == "Transport rule 'Rule1' does not whitelist any domains." + == "Transport rule Rule1 does not whitelist any domains." ) assert result[0].resource_name == "Rule1" assert result[0].resource_id == "ExchangeTransportRule" assert result[1].status == "PASS" assert ( result[1].status_extended - == "Transport rule 'Rule2' does not whitelist any domains." + == "Transport rule Rule2 does not whitelist any domains." ) assert result[1].resource_name == "Rule2" @@ -91,7 +91,7 @@ class Test_exchange_transport_rules_whitelist_disabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Transport rule 'WhitelistRule' whitelists domains: whitelist.com." + == "Transport rule WhitelistRule whitelists domains: whitelist.com." ) assert result[0].resource_name == "WhitelistRule" assert result[0].resource_id == "ExchangeTransportRule" @@ -99,7 +99,7 @@ class Test_exchange_transport_rules_whitelist_disabled: assert result[1].status == "PASS" assert ( result[1].status_extended - == "Transport rule 'NoWhitelistRule' does not whitelist any domains." + == "Transport rule NoWhitelistRule does not whitelist any domains." ) assert result[1].resource_name == "NoWhitelistRule" assert result[1].resource_id == "ExchangeTransportRule"