From deb1e0ff3433a9cf2dab4e4857da598e2e410a63 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Wed, 23 Apr 2025 19:29:24 +0200 Subject: [PATCH] feat(defender): Add new check `defender_antispam_policy_inbound_no_allowed_domains` (#7500) Co-authored-by: HugoPBrito Co-authored-by: MrCloudSec --- prowler/CHANGELOG.md | 1 + .../m365/lib/powershell/m365_powershell.py | 18 +++ .../__init__.py | 0 ...y_inbound_no_allowed_domains.metadata.json | 30 +++++ ...ispam_policy_inbound_no_allowed_domains.py | 42 ++++++ .../services/defender/defender_service.py | 31 +++++ ..._policy_inbound_no_allowed_domains_test.py | 126 ++++++++++++++++++ .../defender/m365_defender_service_test.py | 34 +++++ 8 files changed, 282 insertions(+) create mode 100644 prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/__init__.py create mode 100644 prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.metadata.json create mode 100644 prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.py create mode 100644 tests/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index a938b5cf3e..9d77a9198b 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 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) - Add new check `teams_external_users_cannot_start_conversations` [(#7562)](https://github.com/prowler-cloud/prowler/pull/7562) diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 7b272cc748..bca020cf9c 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -379,3 +379,21 @@ class M365PowerShell(PowerShellSession): } """ return self.execute("Get-DkimSigningConfig | ConvertTo-Json") + + def get_inbound_spam_filter_policy(self) -> dict: + """ + Get Inbound Spam Filter Policy. + + Retrieves the current inbound spam filter policy settings for Exchange Online. + + Returns: + dict: Inbound spam filter policy settings in JSON format. + + Example: + >>> get_inbound_spam_filter_policy() + { + "Identity": "Default", + "AllowedSenderDomains": "[]" + } + """ + return self.execute("Get-HostedContentFilterPolicy | ConvertTo-Json") diff --git a/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/__init__.py b/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.metadata.json b/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.metadata.json new file mode 100644 index 0000000000..2d4537c400 --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "m365", + "CheckID": "defender_antispam_policy_inbound_no_allowed_domains", + "CheckTitle": "Ensure inbound anti-spam policies do not contain allowed domains", + "CheckType": [], + "ServiceName": "defender", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "Defender Anti-Spam Policy", + "Description": "Ensure that inbound anti-spam policies do not have any domains listed in the AllowedSenderDomains. Messages from these domains bypass most email protections, increasing the risk of successful phishing attacks.", + "Risk": "Having domains in the AllowedSenderDomains list allows emails from these domains to bypass essential security checks, increasing the risk of phishing attacks and other malicious activities.", + "RelatedUrl": "https://learn.microsoft.com/en-us/defender-office-365/anti-spam-protection-about#allow-and-block-lists-in-anti-spam-policies", + "Remediation": { + "Code": { + "CLI": "Set-HostedContentFilterPolicy -Identity -AllowedSenderDomains @{}", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft 365 Defender (https://security.microsoft.com). 2. Click to expand Email & collaboration and select Policies & rules > Threat policies. 3. Under Policies, select Anti-spam. 4. Open each out-of-compliance inbound anti-spam policy by clicking on it. 5. Click Edit allowed and blocked senders and domains. 6. Select Allow domains. 7. Delete each domain from the domains list. 8. Click Done > Save. 9. Repeat as needed.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that the AllowedSenderDomains list in your inbound anti-spam policies is empty to prevent bypassing essential security checks.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-the-allowed-sender-domains?view=o365-worldwide" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.py b/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.py new file mode 100644 index 0000000000..71dbb7e45f --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.py @@ -0,0 +1,42 @@ +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_policy_inbound_no_allowed_domains(Check): + """ + Check if the inbound anti-spam policies do not contain allowed domains in Microsoft 365 Defender. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check to verify if inbound anti-spam policies do not contain allowed domains. + + This method checks each inbound anti-spam policy to determine if the AllowedSenderDomains + list is empty or undefined. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + for policy in defender_client.inbound_spam_policies: + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name="Defender Inbound Spam Policy", + resource_id=policy.identity, + ) + report.status = "PASS" + report.status_extended = f"Inbound anti-spam policy {policy.identity} does not contain allowed domains." + + if policy.allowed_sender_domains: + report.status = "FAIL" + report.status_extended = f"Inbound anti-spam policy {policy.identity} contains allowed domains: {policy.allowed_sender_domains}." + + 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 51f53a0606..4fc86aa51c 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.inbound_spam_policies = self._get_inbound_spam_filter_policy() self.connection_filter_policy = self._get_connection_filter_policy() self.dkim_configurations = self._get_dkim_config() self.powershell.close() @@ -179,6 +180,31 @@ class Defender(M365Service): ) return outbound_spam_rules + def _get_inbound_spam_filter_policy(self): + logger.info("Microsoft365 - Getting Defender inbound spam filter policy...") + inbound_spam_policies = [] + try: + inbound_spam_policy = self.powershell.get_inbound_spam_filter_policy() + if not inbound_spam_policy: + return inbound_spam_policies + if isinstance(inbound_spam_policy, dict): + inbound_spam_policy = [inbound_spam_policy] + for policy in inbound_spam_policy: + if policy: + inbound_spam_policies.append( + DefenderInboundSpamPolicy( + identity=policy.get("Identity", ""), + allowed_sender_domains=policy.get( + "AllowedSenderDomains", [] + ), + ) + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return inbound_spam_policies + class MalwarePolicy(BaseModel): enable_file_filter: bool @@ -224,3 +250,8 @@ class OutboundSpamPolicy(BaseModel): class OutboundSpamRule(BaseModel): state: str + + +class DefenderInboundSpamPolicy(BaseModel): + identity: str + allowed_sender_domains: list[str] = [] diff --git a/tests/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains_test.py b/tests/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains_test.py new file mode 100644 index 0000000000..feba0c5bb4 --- /dev/null +++ b/tests/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains_test.py @@ -0,0 +1,126 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_defender_antispam_policy_inbound_no_allowed_domains: + def test_policy_without_allowed_domains(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_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import ( + defender_antispam_policy_inbound_no_allowed_domains, + ) + from prowler.providers.m365.services.defender.defender_service import ( + DefenderInboundSpamPolicy, + ) + + defender_client.inbound_spam_policies = [ + DefenderInboundSpamPolicy( + identity="Policy1", + allowed_sender_domains=[], + ) + ] + + check = defender_antispam_policy_inbound_no_allowed_domains() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Inbound anti-spam policy Policy1 does not contain allowed domains." + ) + assert result[0].resource == defender_client.inbound_spam_policies[0].dict() + assert result[0].resource_name == "Defender Inbound Spam Policy" + assert result[0].resource_id == "Policy1" + assert result[0].location == "global" + + def test_policy_with_allowed_domains(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_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import ( + defender_antispam_policy_inbound_no_allowed_domains, + ) + from prowler.providers.m365.services.defender.defender_service import ( + DefenderInboundSpamPolicy, + ) + + defender_client.inbound_spam_policies = [ + DefenderInboundSpamPolicy( + identity="Policy2", + allowed_sender_domains=["bad-domain.com"], + ) + ] + + check = defender_antispam_policy_inbound_no_allowed_domains() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inbound anti-spam policy Policy2 contains allowed domains: ['bad-domain.com']." + ) + assert result[0].resource == defender_client.inbound_spam_policies[0].dict() + assert result[0].resource_name == "Defender Inbound Spam Policy" + assert result[0].resource_id == "Policy2" + assert result[0].location == "global" + + def test_no_inbound_spam_policies(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_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import ( + defender_antispam_policy_inbound_no_allowed_domains, + ) + + defender_client.inbound_spam_policies = [] + + check = defender_antispam_policy_inbound_no_allowed_domains() + 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 28d9d47919..efce49f843 100644 --- a/tests/providers/m365/services/defender/m365_defender_service_test.py +++ b/tests/providers/m365/services/defender/m365_defender_service_test.py @@ -7,6 +7,7 @@ from prowler.providers.m365.services.defender.defender_service import ( AntiphishingRule, ConnectionFilterPolicy, Defender, + DefenderInboundSpamPolicy, DkimConfig, MalwarePolicy, OutboundSpamPolicy, @@ -70,6 +71,19 @@ def mock_defender_get_antiphising_rules(_): } +def mock_defender_get_inbound_spam_policy(_): + return [ + DefenderInboundSpamPolicy( + identity="Policy1", + allowed_sender_domains=[], + ), + DefenderInboundSpamPolicy( + identity="Policy2", + allowed_sender_domains=["example.com"], + ), + ] + + def mock_defender_get_connection_filter_policy(_): return ConnectionFilterPolicy( ip_allow_list=[], @@ -323,3 +337,23 @@ class Test_Defender_Service: outbound_spam_rules = defender_client.outbound_spam_rules assert outbound_spam_rules["Policy1"].state == "Enabled" assert outbound_spam_rules["Policy2"].state == "Disabled" + + @patch( + "prowler.providers.m365.services.defender.defender_service.Defender._get_inbound_spam_filter_policy", + new=mock_defender_get_inbound_spam_policy, + ) + def test__get_inbound_spam_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) + ) + ) + inbound_spam_policies = defender_client.inbound_spam_policies + assert inbound_spam_policies[0].allowed_sender_domains == [] + assert inbound_spam_policies[1].allowed_sender_domains == ["example.com"] + defender_client.powershell.close()