From d816d73174354950c3601c960d89683f20259aa7 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Tue, 22 Apr 2025 18:28:18 +0200 Subject: [PATCH] feat(defender): add new check `defender_antispam_connection_filter_policy_empty_ip_allowlist` (#7492) Co-authored-by: HugoPBrito Co-authored-by: MrCloudSec --- prowler/CHANGELOG.md | 1 + .../m365/lib/powershell/m365_powershell.py | 20 +++ .../__init__.py | 0 ...er_policy_empty_ip_allowlist.metadata.json | 30 +++++ ...ection_filter_policy_empty_ip_allowlist.py | 43 ++++++ .../services/defender/defender_service.py | 22 ++++ ...n_filter_policy_empty_ip_allowlist_test.py | 124 ++++++++++++++++++ .../defender/m365_defender_service_test.py | 28 ++++ 8 files changed, 268 insertions(+) create mode 100644 prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/__init__.py create mode 100644 prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.metadata.json create mode 100644 prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.py create mode 100644 tests/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 975b5d978e..210a5003d8 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add service Exchange to Microsoft365 with one check for Organizations Mailbox Auditing enabled [(#7408)](https://github.com/prowler-cloud/prowler/pull/7408) - 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_email_sending_to_channel_disabled` [(#7533)](https://github.com/prowler-cloud/prowler/pull/7533) +- Add new check for AllowList not used in the Connection Filter Policy from service Defender in M365 [(#7492)](https://github.com/prowler-cloud/prowler/pull/7492) - Add new check for DKIM enabled for service Defender in M365 [(#7485)](https://github.com/prowler-cloud/prowler/pull/7485) ### Fixed diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 9e40f8e6a3..b248bd30bd 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -306,6 +306,26 @@ class M365PowerShell(PowerShellSession): """ return self.execute("Get-MailboxAuditBypassAssociation | ConvertTo-Json") + def get_connection_filter_policy(self) -> dict: + """ + Get Exchange Online Connection Filter Policy. + + Retrieves the current connection filter policy settings for Exchange Online. + + Returns: + dict: Connection filter policy settings in JSON format. + + Example: + >>> get_connection_filter_policy() + { + "Identity": "Default", + "IPAllowList": []" + } + """ + return self.execute( + "Get-HostedConnectionFilterPolicy -Identity Default | ConvertTo-Json" + ) + def get_dkim_config(self) -> dict: """ Get DKIM Signing Configuration. diff --git a/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/__init__.py b/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.metadata.json b/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.metadata.json new file mode 100644 index 0000000000..9b46539a82 --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "m365", + "CheckID": "defender_antispam_connection_filter_policy_empty_ip_allowlist", + "CheckTitle": "Ensure the Anti-Spam Connection Filter Policy IP Allowlist is empty or undefined.", + "CheckType": [], + "ServiceName": "defender", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Defender Anti-Spam Policy", + "Description": "This check focuses on Microsoft 365 organizations with Exchange Online mailboxes or standalone Exchange Online Protection (EOP) organizations. It ensures that the connection filter policy's IP Allowlist is empty or undefined to prevent bypassing spam filtering and sender authentication checks, which could lead to successful delivery of malicious emails.", + "Risk": "Using the IP Allowlist without additional verification like mail flow rules poses a risk, as emails from these sources skip essential security checks (SPF, DKIM, DMARC). This could allow attackers to deliver harmful emails directly to the Inbox.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "Set-HostedConnectionFilterPolicy -Identity Default -IPAllowList @{}", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration and select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies, select Anti-spam and click on the Connection filter policy (Default). 5. Remove IP entries from the allow list. 6. Click Save.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that the IP Allowlist in your connection filter policy is empty or undefined to prevent bypassing essential security checks.", + "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-hostedconnectionfilterpolicy?view=exchange-ps" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.py b/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.py new file mode 100644 index 0000000000..054195ae7a --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.py @@ -0,0 +1,43 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.defender.defender_client import defender_client + + +class defender_antispam_connection_filter_policy_empty_ip_allowlist(Check): + """ + Check if the IP Allowlist is not used in the Defender connection filter policy. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check to verify if the IP Allowlist is not used. + + This method checks the Defender connection filter policy to determine if the + IP Allowlist is empty or undefined. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + policy = defender_client.connection_filter_policy + if policy: + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name="Defender Antispam Connection Filter Policy", + resource_id=policy.identity, + ) + report.status = "PASS" + report.status_extended = f"IP Allowlist is not used in the Antispam Connection Filter Policy {policy.identity}." + + if policy.ip_allow_list: + report.status = "FAIL" + report.status_extended = f"IP Allowlist is used in the Antispam Connection Filter Policy {policy.identity} with IPs: {policy.ip_allow_list}." + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/defender/defender_service.py b/prowler/providers/m365/services/defender/defender_service.py index 3fd256a085..1528dcd4b0 100644 --- a/prowler/providers/m365/services/defender/defender_service.py +++ b/prowler/providers/m365/services/defender/defender_service.py @@ -16,6 +16,7 @@ class Defender(M365Service): self.outbound_spam_rules = self._get_outbound_spam_filter_rule() self.antiphishing_policies = self._get_antiphising_policy() self.antiphising_rules = self._get_antiphising_rules() + self.connection_filter_policy = self._get_connection_filter_policy() self.dkim_configurations = self._get_dkim_config() self.powershell.close() @@ -94,6 +95,22 @@ class Defender(M365Service): ) return antiphishing_rules + def _get_connection_filter_policy(self): + logger.info("Microsoft365 - Getting connection filter policy...") + connection_filter_policy = None + try: + policy = self.powershell.get_connection_filter_policy() + if policy: + connection_filter_policy = ConnectionFilterPolicy( + ip_allow_list=policy.get("IPAllowList", []), + identity=policy.get("Identity", ""), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return connection_filter_policy + def _get_dkim_config(self): logger.info("Microsoft365 - Getting DKIM settings...") dkim_configs = [] @@ -185,6 +202,11 @@ class AntiphishingRule(BaseModel): state: str +class ConnectionFilterPolicy(BaseModel): + ip_allow_list: list + identity: str + + class DkimConfig(BaseModel): dkim_signing_enabled: bool id: str diff --git a/tests/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist_test.py b/tests/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist_test.py new file mode 100644 index 0000000000..e4dfeb772b --- /dev/null +++ b/tests/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist_test.py @@ -0,0 +1,124 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_defender_antispam_connection_filter_policy_empty_ip_allowlist: + def test_ip_allow_list_not_used(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_antispam_connection_filter_policy_empty_ip_allowlist.defender_antispam_connection_filter_policy_empty_ip_allowlist.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_connection_filter_policy_empty_ip_allowlist.defender_antispam_connection_filter_policy_empty_ip_allowlist import ( + defender_antispam_connection_filter_policy_empty_ip_allowlist, + ) + from prowler.providers.m365.services.defender.defender_service import ( + ConnectionFilterPolicy, + ) + + defender_client.connection_filter_policy = ConnectionFilterPolicy( + ip_allow_list=[], + identity="Default", + ) + + check = defender_antispam_connection_filter_policy_empty_ip_allowlist() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "IP Allowlist is not used in the Antispam Connection Filter Policy Default." + ) + assert result[0].resource == defender_client.connection_filter_policy.dict() + assert ( + result[0].resource_name == "Defender Antispam Connection Filter Policy" + ) + assert result[0].resource_id == "Default" + assert result[0].location == "global" + + def test_ip_allow_list_used(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_antispam_connection_filter_policy_empty_ip_allowlist.defender_antispam_connection_filter_policy_empty_ip_allowlist.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_connection_filter_policy_empty_ip_allowlist.defender_antispam_connection_filter_policy_empty_ip_allowlist import ( + defender_antispam_connection_filter_policy_empty_ip_allowlist, + ) + from prowler.providers.m365.services.defender.defender_service import ( + ConnectionFilterPolicy, + ) + + defender_client.connection_filter_policy = ConnectionFilterPolicy( + ip_allow_list=["192.168.0.1", "10.0.0.5"], + identity="Default", + ) + + check = defender_antispam_connection_filter_policy_empty_ip_allowlist() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + "IP Allowlist is used in the Antispam Connection Filter Policy Default with IPs: ['192.168.0.1', '10.0.0.5']." + ) + assert result[0].resource == defender_client.connection_filter_policy.dict() + assert ( + result[0].resource_name == "Defender Antispam Connection Filter Policy" + ) + assert result[0].resource_id == "Default" + assert result[0].location == "global" + + def test_no_connection_filter_policy(self): + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_antispam_connection_filter_policy_empty_ip_allowlist.defender_antispam_connection_filter_policy_empty_ip_allowlist.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_connection_filter_policy_empty_ip_allowlist.defender_antispam_connection_filter_policy_empty_ip_allowlist import ( + defender_antispam_connection_filter_policy_empty_ip_allowlist, + ) + + defender_client.connection_filter_policy = None + + check = defender_antispam_connection_filter_policy_empty_ip_allowlist() + result = check.execute() + assert len(result) == 0 diff --git a/tests/providers/m365/services/defender/m365_defender_service_test.py b/tests/providers/m365/services/defender/m365_defender_service_test.py index 3738f30a94..2d3de6e0f6 100644 --- a/tests/providers/m365/services/defender/m365_defender_service_test.py +++ b/tests/providers/m365/services/defender/m365_defender_service_test.py @@ -5,6 +5,7 @@ from prowler.providers.m365.models import M365IdentityInfo from prowler.providers.m365.services.defender.defender_service import ( AntiphishingPolicy, AntiphishingRule, + ConnectionFilterPolicy, Defender, DkimConfig, MalwarePolicy, @@ -69,6 +70,13 @@ def mock_defender_get_antiphising_rules(_): } +def mock_defender_get_connection_filter_policy(_): + return ConnectionFilterPolicy( + ip_allow_list=[], + identity="Default", + ) + + def mock_defender_get_dkim_config(_): return [ DkimConfig(dkim_signing_enabled=True, id="domain1"), @@ -218,6 +226,26 @@ class Test_Defender_Service: assert antiphishing_rules["Policy2"].state == "Disabled" defender_client.powershell.close() + @patch( + "prowler.providers.m365.services.defender.defender_service.Defender._get_connection_filter_policy", + new=mock_defender_get_connection_filter_policy, + ) + def test__get_connection_filter_policy(self): + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + ): + defender_client = Defender( + set_mocked_m365_provider( + identity=M365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + connection_filter_policy = defender_client.connection_filter_policy + assert connection_filter_policy.ip_allow_list == [] + assert connection_filter_policy.identity == "Default" + defender_client.powershell.close() + @patch( "prowler.providers.m365.services.defender.defender_service.Defender._get_dkim_config", new=mock_defender_get_dkim_config,