diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 95a00748d8..a6526afb28 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 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) - Add new check `teams_unmanaged_communication_disabled` [(#7561)](https://github.com/prowler-cloud/prowler/pull/7561) diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index bca020cf9c..7a47a2025f 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -342,6 +342,25 @@ class M365PowerShell(PowerShellSession): """ return self.execute("Get-MailboxAuditBypassAssociation | ConvertTo-Json") + def get_transport_rules(self) -> dict: + """ + Get Exchange Online Transport Rules. + + Retrieves the current transport rules configured in Exchange Online. + + Returns: + dict: Transport rules in JSON format. + + Example: + >>> get_transport_rules() + { + "Name": "Rule1", + "SetSCL": -1, + "SenderDomainIs": ["example.com"] + } + """ + return self.execute("Get-TransportRule | ConvertTo-Json") + def get_connection_filter_policy(self) -> dict: """ Get Exchange Online Connection Filter Policy. diff --git a/prowler/providers/m365/services/exchange/exchange_service.py b/prowler/providers/m365/services/exchange/exchange_service.py index f369edb6b5..9fd547f7b7 100644 --- a/prowler/providers/m365/services/exchange/exchange_service.py +++ b/prowler/providers/m365/services/exchange/exchange_service.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic import BaseModel from prowler.lib.logger import logger @@ -11,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.transport_rules = self._get_transport_rules() self.powershell.close() def _get_organization_config(self): @@ -53,6 +56,30 @@ class Exchange(M365Service): ) return mailboxes_config + def _get_transport_rules(self): + logger.info("Microsoft365 - Getting transport rules configuration...") + transport_rules = [] + try: + rules_data = self.powershell.get_transport_rules() + if not rules_data: + return transport_rules + if isinstance(rules_data, dict): + rules_data = [rules_data] + for rule in rules_data: + if rule: + transport_rules.append( + TransportRule( + name=rule.get("Name", ""), + scl=rule.get("SetSCL", None), + sender_domain_is=rule.get("SenderDomainIs", []), + ) + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return transport_rules + class Organization(BaseModel): name: str @@ -64,3 +91,9 @@ class MailboxAuditConfig(BaseModel): name: str id: str audit_bypass_enabled: bool + + +class TransportRule(BaseModel): + name: str + scl: Optional[int] + sender_domain_is: list[str] diff --git a/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/__init__.py b/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.metadata.json new file mode 100644 index 0000000000..d762b3be0e --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "m365", + "CheckID": "exchange_transport_rules_whitelist_disabled", + "CheckTitle": "Ensure mail transport rules do not whitelist specific domains", + "CheckType": [], + "ServiceName": "exchange", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Exchange Transport Rules", + "Description": "Mail flow rules (transport rules) in Exchange Online are used to identify and take action on messages that flow through the organization.", + "Risk": "Whitelisting domains in transport rules bypasses regular malware and phishing scanning, which can enable an attacker to launch attacks against your users from a safe haven domain.", + "RelatedUrl": "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/configuration-best-practices", + "Remediation": { + "Code": { + "CLI": "Remove-TransportRule -Identity ", + "NativeIaC": "", + "Other": "1. Navigate to Exchange admin center https://admin.exchange.microsoft.com.. 2. Click to expand Mail Flow and then select Rules. 3. For each rule that whitelists specific domains, select the rule and click the 'Delete' icon.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Remove transport rules that whitelist specific domains to ensure proper scanning.", + "Url": "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} 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 new file mode 100644 index 0000000000..ec74c9ff75 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.py @@ -0,0 +1,47 @@ +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_rules_whitelist_disabled(Check): + """ + Check to ensure that no mail transport rules whitelist specific domains. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check to validate that no transport rules whitelist specific domains. + + This method retrieves all transport rules from the Exchange service and evaluates + whether any of them whitelist specific domains. A report is generated for each + transport rule. + + Returns: + List[CheckReportM365]: A list of findings with the status of each transport rule. + """ + findings = [] + + for rule in exchange_client.transport_rules: + report = CheckReportM365( + metadata=self.metadata(), + resource=rule, + resource_name=rule.name, + resource_id="ExchangeTransportRule", + ) + + report.status = "PASS" + report.status_extended = ( + 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)}." + + 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 83e1af83a5..1d74474e3d 100644 --- a/tests/providers/m365/services/exchange/exchange_service_test.py +++ b/tests/providers/m365/services/exchange/exchange_service_test.py @@ -6,6 +6,7 @@ from prowler.providers.m365.services.exchange.exchange_service import ( Exchange, MailboxAuditConfig, Organization, + TransportRule, ) from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider @@ -21,6 +22,21 @@ def mock_exchange_get_mailbox_audit_config(_): ] +def mock_exchange_get_transport_rules(_): + return [ + TransportRule( + name="test", + scl=-1, + sender_domain_is=["example.com"], + ), + TransportRule( + name="test2", + scl=0, + sender_domain_is=["example.com"], + ), + ] + + class Test_Exchange_Service: def test_get_client(self): with ( @@ -84,3 +100,29 @@ class Test_Exchange_Service: assert mailbox_audit_config[1].audit_bypass_enabled is True exchange_client.powershell.close() + + @patch( + "prowler.providers.m365.services.exchange.exchange_service.Exchange._get_transport_rules", + new=mock_exchange_get_transport_rules, + ) + def test_get_transport_rules(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_rules = exchange_client.transport_rules + assert len(transport_rules) == 2 + assert transport_rules[0].name == "test" + assert transport_rules[0].scl == -1 + assert transport_rules[0].sender_domain_is == ["example.com"] + assert transport_rules[1].name == "test2" + assert transport_rules[1].scl == 0 + assert transport_rules[1].sender_domain_is == ["example.com"] + + exchange_client.powershell.close() 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 new file mode 100644 index 0000000000..ad51a64b58 --- /dev/null +++ b/tests/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled_test.py @@ -0,0 +1,135 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_exchange_transport_rules_whitelist_disabled: + def test_no_whitelist_domains(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_rules_whitelist_disabled.exchange_transport_rules_whitelist_disabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_service import ( + TransportRule, + ) + from prowler.providers.m365.services.exchange.exchange_transport_rules_whitelist_disabled.exchange_transport_rules_whitelist_disabled import ( + exchange_transport_rules_whitelist_disabled, + ) + + exchange_client.transport_rules = [ + TransportRule(name="Rule1", scl=0, sender_domain_is=[]), + TransportRule(name="Rule2", scl=0, sender_domain_is=["example.com"]), + ] + + check = exchange_transport_rules_whitelist_disabled() + result = check.execute() + + assert len(result) == 2 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "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." + ) + assert result[1].resource_name == "Rule2" + + def test_whitelist_detected(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_rules_whitelist_disabled.exchange_transport_rules_whitelist_disabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_service import ( + TransportRule, + ) + from prowler.providers.m365.services.exchange.exchange_transport_rules_whitelist_disabled.exchange_transport_rules_whitelist_disabled import ( + exchange_transport_rules_whitelist_disabled, + ) + + exchange_client.transport_rules = [ + TransportRule( + name="WhitelistRule", scl=-1, sender_domain_is=["whitelist.com"] + ), + TransportRule(name="NoWhitelistRule", scl=-1, sender_domain_is=[]), + ] + + check = exchange_transport_rules_whitelist_disabled() + result = check.execute() + + assert len(result) == 2 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Transport rule 'WhitelistRule' whitelists domains: whitelist.com." + ) + assert result[0].resource_name == "WhitelistRule" + assert result[0].resource_id == "ExchangeTransportRule" + assert result[0].location == "global" + assert result[1].status == "PASS" + assert ( + result[1].status_extended + == "Transport rule 'NoWhitelistRule' does not whitelist any domains." + ) + assert result[1].resource_name == "NoWhitelistRule" + assert result[1].resource_id == "ExchangeTransportRule" + assert result[1].location == "global" + + def test_empty_rule_list(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_rules_whitelist_disabled.exchange_transport_rules_whitelist_disabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_transport_rules_whitelist_disabled.exchange_transport_rules_whitelist_disabled import ( + exchange_transport_rules_whitelist_disabled, + ) + + exchange_client.transport_rules = [] + + check = exchange_transport_rules_whitelist_disabled() + result = check.execute() + + assert len(result) == 0