feat(exchange): add new check exchange_transport_rules_whitelist_disabled (#7569)

Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
This commit is contained in:
Daniel Barranquero
2025-04-23 19:47:51 +02:00
committed by Pepe Fagoaga
parent 2f6e83ad0c
commit 64082b5038
8 changed files with 307 additions and 0 deletions
+1
View File
@@ -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)
@@ -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.
@@ -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]
@@ -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 <RuleName>",
"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": ""
}
@@ -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
@@ -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()
@@ -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