mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
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:
committed by
Pepe Fagoaga
parent
2f6e83ad0c
commit
64082b5038
@@ -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]
|
||||
|
||||
+30
@@ -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": ""
|
||||
}
|
||||
+47
@@ -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()
|
||||
|
||||
+135
@@ -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
|
||||
Reference in New Issue
Block a user