fix(m365): skip defender preset policies without rules (#12809)

(cherry picked from commit 61ef44a03b)
This commit is contained in:
Pedro Martín
2026-09-15 07:57:46 +00:00
parent a944a8317b
commit b022a058da
11 changed files with 400 additions and 0 deletions
@@ -0,0 +1 @@
`KeyError` in M365 Defender malware, anti-phishing and inbound anti-spam checks when the tenant has Standard or Strict preset security policies
@@ -55,6 +55,12 @@ class defender_antiphishing_policy_configured(Check):
policy_name,
policy,
) in defender_client.antiphishing_policies.items():
# Preset security policies are scoped by protection policy rules, not filter rules
if (
not policy.default
and policy.name not in defender_client.antiphishing_rules
):
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
@@ -51,6 +51,12 @@ class defender_antispam_policy_inbound_no_allowed_domains(Check):
default_policy_well_configured = False
for policy in defender_client.inbound_spam_policies:
# Preset security policies are scoped by protection policy rules, not filter rules
if (
not policy.default
and policy.identity not in defender_client.inbound_spam_rules
):
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
@@ -51,6 +51,12 @@ class defender_malware_policy_common_attachments_filter_enabled(Check):
default_policy_well_configured = False
for policy in defender_client.malware_policies:
# Preset security policies are scoped by protection policy rules, not filter rules
if (
not policy.is_default
and policy.identity not in defender_client.malware_rules
):
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
@@ -114,6 +114,12 @@ class defender_malware_policy_comprehensive_attachments_filter_applied(Check):
default_policy_well_configured = False
for policy in defender_client.malware_policies:
# Preset security policies are scoped by protection policy rules, not filter rules
if (
not policy.is_default
and policy.identity not in defender_client.malware_rules
):
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
@@ -51,6 +51,12 @@ class defender_malware_policy_notifications_internal_users_malware_enabled(Check
default_policy_well_configured = False
for policy in defender_client.malware_policies:
# Preset security policies are scoped by protection policy rules, not filter rules
if (
not policy.is_default
and policy.identity not in defender_client.malware_rules
):
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
@@ -521,3 +521,86 @@ class Test_defender_antiphishing_policy_configured:
check = defender_antiphishing_policy_configured()
result = check.execute()
assert len(result) == 0
def test_preset_policy_without_rule_is_skipped(self):
defender_client = mock.MagicMock()
defender_client.audited_tenant = "audited_tenant"
defender_client.audited_domain = DOMAIN
defender_client.audit_config = {}
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_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import (
defender_antiphishing_policy_configured,
)
from prowler.providers.m365.services.defender.defender_service import (
AntiphishingPolicy,
AntiphishingRule,
)
defender_client.antiphishing_policies = {
"Default": AntiphishingPolicy(
name="Default",
spoof_intelligence=True,
spoof_intelligence_action="Quarantine",
dmarc_reject_action="Quarantine",
dmarc_quarantine_action="Quarantine",
safety_tips=True,
unauthenticated_sender_action=True,
show_tag=True,
honor_dmarc_policy=True,
default=True,
),
"Standard Preset Security Policy1663355404982": AntiphishingPolicy(
name="Standard Preset Security Policy1663355404982",
spoof_intelligence=True,
spoof_intelligence_action="Quarantine",
dmarc_reject_action="Quarantine",
dmarc_quarantine_action="Quarantine",
safety_tips=True,
unauthenticated_sender_action=True,
show_tag=True,
honor_dmarc_policy=True,
default=False,
),
"Custom1": AntiphishingPolicy(
name="Custom1",
spoof_intelligence=True,
spoof_intelligence_action="Quarantine",
dmarc_reject_action="Quarantine",
dmarc_quarantine_action="Quarantine",
safety_tips=True,
unauthenticated_sender_action=True,
show_tag=True,
honor_dmarc_policy=True,
default=False,
),
}
defender_client.antiphishing_rules = {
"Custom1": AntiphishingRule(
state="Enabled",
priority=1,
users=["user1@example.com"],
groups=None,
domains=None,
)
}
check = defender_antiphishing_policy_configured()
result = check.execute()
assert len(result) == 2
assert "Standard Preset Security Policy1663355404982" not in [
finding.resource_id for finding in result
]
@@ -410,3 +410,65 @@ class Test_defender_antispam_policy_inbound_no_allowed_domains:
check = defender_antispam_policy_inbound_no_allowed_domains()
result = check.execute()
assert len(result) == 0
def test_preset_policy_without_rule_is_skipped(self):
defender_client = mock.MagicMock()
defender_client.audited_tenant = "audited_tenant"
defender_client.audited_domain = DOMAIN
defender_client.audit_config = {}
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,
InboundSpamRule,
)
defender_client.inbound_spam_policies = [
DefenderInboundSpamPolicy(
identity="Default",
allowed_sender_domains=[],
default=True,
),
DefenderInboundSpamPolicy(
identity="Standard Preset Security Policy1663355404982",
allowed_sender_domains=[],
default=False,
),
DefenderInboundSpamPolicy(
identity="Custom1",
allowed_sender_domains=[],
default=False,
),
]
defender_client.inbound_spam_rules = {
"Custom1": InboundSpamRule(
state="Enabled",
priority=1,
users=["user1@example.com"],
groups=None,
domains=None,
)
}
check = defender_antispam_policy_inbound_no_allowed_domains()
result = check.execute()
assert len(result) == 2
assert "Standard Preset Security Policy1663355404982" not in [
finding.resource_id for finding in result
]
@@ -442,3 +442,77 @@ class Test_defender_malware_policy_common_attachments_filter_enabled:
check = defender_malware_policy_common_attachments_filter_enabled()
result = check.execute()
assert len(result) == 0
def test_preset_policy_without_rule_is_skipped(self):
defender_client = mock.MagicMock()
defender_client.audited_tenant = "audited_tenant"
defender_client.audited_domain = DOMAIN
defender_client.audit_config = {}
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_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled import (
defender_malware_policy_common_attachments_filter_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
MalwarePolicy,
MalwareRule,
)
defender_client.audit_config = {
"recommended_blocked_file_types": ["exe", "bat"]
}
defender_client.malware_policies = [
MalwarePolicy(
identity="Default",
enable_file_filter=True,
enable_internal_sender_admin_notifications=True,
internal_sender_admin_address="admin@example.com",
file_types=["exe", "bat"],
is_default=True,
),
MalwarePolicy(
identity="Standard Preset Security Policy1663355404982",
enable_file_filter=True,
enable_internal_sender_admin_notifications=True,
internal_sender_admin_address="admin@example.com",
file_types=["exe", "bat"],
is_default=False,
),
MalwarePolicy(
identity="Custom1",
enable_file_filter=True,
enable_internal_sender_admin_notifications=True,
internal_sender_admin_address="admin@example.com",
file_types=["exe", "bat"],
is_default=False,
),
]
defender_client.malware_rules = {
"Custom1": MalwareRule(
state="Enabled",
priority=1,
users=["user1@example.com"],
groups=None,
domains=None,
)
}
check = defender_malware_policy_common_attachments_filter_enabled()
result = check.execute()
assert len(result) == 2
assert "Standard Preset Security Policy1663355404982" not in [
finding.resource_id for finding in result
]
@@ -452,3 +452,77 @@ class Test_defender_malware_policy_comprehensive_attachments_filter_applied:
check = defender_malware_policy_comprehensive_attachments_filter_applied()
result = check.execute()
assert len(result) == 0
def test_preset_policy_without_rule_is_skipped(self):
defender_client = mock.MagicMock()
defender_client.audited_tenant = "audited_tenant"
defender_client.audited_domain = DOMAIN
defender_client.audit_config = {}
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_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
defender_malware_policy_comprehensive_attachments_filter_applied,
)
from prowler.providers.m365.services.defender.defender_service import (
MalwarePolicy,
MalwareRule,
)
defender_client.audit_config = {
"recommended_blocked_file_types": ["exe", "bat"]
}
defender_client.malware_policies = [
MalwarePolicy(
identity="Default",
enable_file_filter=True,
enable_internal_sender_admin_notifications=True,
internal_sender_admin_address="admin@example.com",
file_types=["exe", "bat"],
is_default=True,
),
MalwarePolicy(
identity="Standard Preset Security Policy1663355404982",
enable_file_filter=True,
enable_internal_sender_admin_notifications=True,
internal_sender_admin_address="admin@example.com",
file_types=["exe", "bat"],
is_default=False,
),
MalwarePolicy(
identity="Custom1",
enable_file_filter=True,
enable_internal_sender_admin_notifications=True,
internal_sender_admin_address="admin@example.com",
file_types=["exe", "bat"],
is_default=False,
),
]
defender_client.malware_rules = {
"Custom1": MalwareRule(
state="Enabled",
priority=1,
users=["user1@example.com"],
groups=None,
domains=None,
)
}
check = defender_malware_policy_comprehensive_attachments_filter_applied()
result = check.execute()
assert len(result) == 2
assert "Standard Preset Security Policy1663355404982" not in [
finding.resource_id for finding in result
]
@@ -456,3 +456,79 @@ class Test_defender_malware_policy_notifications_internal_users_malware_enabled:
)
result = check.execute()
assert len(result) == 0
def test_preset_policy_without_rule_is_skipped(self):
defender_client = mock.MagicMock()
defender_client.audited_tenant = "audited_tenant"
defender_client.audited_domain = DOMAIN
defender_client.audit_config = {}
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_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import (
defender_malware_policy_notifications_internal_users_malware_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
MalwarePolicy,
MalwareRule,
)
defender_client.audit_config = {
"recommended_blocked_file_types": ["exe", "bat"]
}
defender_client.malware_policies = [
MalwarePolicy(
identity="Default",
enable_file_filter=True,
enable_internal_sender_admin_notifications=True,
internal_sender_admin_address="admin@example.com",
file_types=["exe", "bat"],
is_default=True,
),
MalwarePolicy(
identity="Standard Preset Security Policy1663355404982",
enable_file_filter=True,
enable_internal_sender_admin_notifications=True,
internal_sender_admin_address="admin@example.com",
file_types=["exe", "bat"],
is_default=False,
),
MalwarePolicy(
identity="Custom1",
enable_file_filter=True,
enable_internal_sender_admin_notifications=True,
internal_sender_admin_address="admin@example.com",
file_types=["exe", "bat"],
is_default=False,
),
]
defender_client.malware_rules = {
"Custom1": MalwareRule(
state="Enabled",
priority=1,
users=["user1@example.com"],
groups=None,
domains=None,
)
}
check = (
defender_malware_policy_notifications_internal_users_malware_enabled()
)
result = check.execute()
assert len(result) == 2
assert "Standard Preset Security Policy1663355404982" not in [
finding.resource_id for finding in result
]