feat(m365): add defender_safe_attachments_policy_enabled security check (#9833)

Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
This commit is contained in:
Andoni Alonso
2026-02-11 15:42:11 +01:00
committed by GitHub
parent 5d490fa185
commit fce1e4f3d2
10 changed files with 800 additions and 6 deletions
+1
View File
@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- `defender_safe_attachments_policy_enabled` check for M365 provider [(#9833)](https://github.com/prowler-cloud/prowler/pull/9833)
- `defender_safelinks_policy_enabled` check for M365 provider [(#9832)](https://github.com/prowler-cloud/prowler/pull/9832)
- AI Skills: Added a skill for creating new Attack Paths queries in openCypher, compatible with Neo4j and Neptune [(#9975)](https://github.com/prowler-cloud/prowler/pull/9975)
+3 -1
View File
@@ -387,7 +387,9 @@
{
"Id": "2.1.4",
"Description": "The Safe Attachments policy helps protect users from malware in email attachments by scanning attachments for viruses, malware, and other malicious content. When an email attachment is received by a user, Safe Attachments will scan the attachment in a secure environment and provide a verdict on whether the attachment is safe or not.",
"Checks": [],
"Checks": [
"defender_safe_attachments_policy_enabled"
],
"Attributes": [
{
"Section": "2 Microsoft 365 Defender",
+3 -1
View File
@@ -406,7 +406,9 @@
{
"Id": "2.1.4",
"Description": "The Safe Attachments policy helps protect users from malware in email attachments by scanning attachments for viruses, malware, and other malicious content. When an email attachment is received by a user, Safe Attachments will scan the attachment in a secure environment and provide a verdict on whether the attachment is safe or not.",
"Checks": [],
"Checks": [
"defender_safe_attachments_policy_enabled"
],
"Attributes": [
{
"Section": "2 Microsoft 365 Defender",
@@ -693,6 +693,7 @@
"defender_malware_policy_common_attachments_filter_enabled",
"defender_malware_policy_comprehensive_attachments_filter_applied",
"defender_malware_policy_notifications_internal_users_malware_enabled",
"defender_safe_attachments_policy_enabled",
"defender_safelinks_policy_enabled",
"defender_zap_for_teams_enabled",
"teams_external_domains_restricted",
@@ -65,7 +65,7 @@ class M365PowerShell(PowerShellSession):
cmdlet_name = cmdlet_match.group(1) if cmdlet_match else "Unknown"
logger.warning(
f"PowerShell cmdlet '{cmdlet_name}' is not available. "
f"This may indicate missing licensing (e.g., Microsoft Defender for Office 365) "
f"This may indicate missing module, licensing (e.g., Microsoft Defender for Office 365) "
f"or insufficient permissions. Related checks will be skipped."
)
else:
@@ -848,6 +848,55 @@ class M365PowerShell(PowerShellSession):
"Get-SharingPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_safe_attachments_policy(self) -> dict:
"""
Get Safe Attachments Policy.
Retrieves the Safe Attachments policy settings for Microsoft Defender for Office 365.
Returns:
dict: Safe Attachments policy settings in JSON format.
Example:
>>> get_safe_attachments_policy()
{
"Name": "Built-In Protection Policy",
"Identity": "Built-In Protection Policy",
"Enable": true,
"Action": "Block",
"QuarantineTag": "AdminOnlyAccessPolicy"
}
"""
return self.execute(
"Get-SafeAttachmentPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_safe_attachments_rule(self) -> dict:
"""
Get Safe Attachments Rules.
Retrieves the Safe Attachments rules that define which users, groups,
and domains are targeted by Safe Attachments policies.
Returns:
dict: Safe Attachments rules in JSON format.
Example:
>>> get_safe_attachments_rule()
{
"Name": "Custom Safe Attachments Rule",
"SafeAttachmentPolicy": "Custom Policy",
"State": "Enabled",
"Priority": 0,
"SentTo": ["user@contoso.com"],
"SentToMemberOf": ["group@contoso.com"],
"RecipientDomainIs": ["contoso.com"]
}
"""
return self.execute(
"Get-SafeAttachmentRule | ConvertTo-Json -Depth 10", json_parse=True
)
def get_advanced_threat_protection_policy(self) -> dict:
"""
Get Advanced Threat Protection Policy.
@@ -0,0 +1,40 @@
{
"Provider": "m365",
"CheckID": "defender_safe_attachments_policy_enabled",
"CheckTitle": "Safe Attachments policy is enabled with secure configuration",
"CheckType": [],
"ServiceName": "defender",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Defender Safe Attachments Policy",
"ResourceGroup": "security",
"Description": "Safe Attachments provides an additional layer of protection by scanning email attachments in a secure environment before delivering them to recipients.\n\nThe Built-In Protection Policy should have **Enable=True**, **Action=Block**, and **QuarantineTag=AdminOnlyAccessPolicy** to ensure malicious attachments are blocked and quarantined with admin-only access.",
"Risk": "Without properly configured Safe Attachments policies, malicious email attachments could reach users' mailboxes and potentially compromise the organization through:\n\n- **Malware delivery** via infected documents\n- **Ransomware attacks** through weaponized attachments\n- **Data exfiltration** using malicious scripts",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/defender-office-365/safe-attachments-about",
"https://learn.microsoft.com/en-us/defender-office-365/preset-security-policies"
],
"Remediation": {
"Code": {
"CLI": "Set-SafeAttachmentPolicy -Identity 'Built-In Protection Policy' -Enable $true -Action Block -QuarantineTag AdminOnlyAccessPolicy",
"NativeIaC": "",
"Other": "1. Go to Microsoft 365 Defender portal (https://security.microsoft.com)\n2. Navigate to Email & collaboration > Policies & rules > Threat policies\n3. Select Safe Attachments under Policies\n4. Click on Built-In Protection Policy\n5. Set Enable to True, Action to Block, and QuarantineTag to AdminOnlyAccessPolicy\n6. Save the configuration",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable Safe Attachments with the **Block** action to prevent malicious attachments from reaching users. Use **AdminOnlyAccessPolicy** for the quarantine tag to ensure only administrators can release quarantined items, following the principle of least privilege.",
"Url": "https://hub.prowler.com/check/defender_safe_attachments_policy_enabled"
}
},
"Categories": [
"email-security",
"e5"
],
"DependsOn": [],
"RelatedTo": [
"defender_malware_policy_common_attachments_filter_enabled"
],
"Notes": "This check requires Microsoft Defender for Office 365 Plan 1 or higher license."
}
@@ -0,0 +1,112 @@
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_safe_attachments_policy_enabled(Check):
"""
Check if Safe Attachments policy is properly configured in Microsoft Defender for Office 365.
This check verifies that Safe Attachments policies have the following settings
configured according to CIS Microsoft 365 Foundations Benchmark:
- Enable = True
- Action = Block
- QuarantineTag = AdminOnlyAccessPolicy
Note: The Built-in Protection Policy has fixed settings that cannot be changed
and always provides baseline protection.
"""
def execute(self) -> List[CheckReportM365]:
findings = []
if defender_client.safe_attachments_policies:
# Only Built-in Protection Policy exists (no custom policies with rules)
if not defender_client.safe_attachments_rules:
policy = next(iter(defender_client.safe_attachments_policies.values()))
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name=policy.name,
resource_id=policy.identity,
)
# Case 1: Only Built-in policy exists - always PASS (fixed settings)
report.status = "PASS"
report.status_extended = f"{policy.name} is the only Safe Attachments policy and provides baseline protection for all users."
findings.append(report)
# Multiple Safe Attachments Policies (Built-in + custom policies)
else:
for (
policy_name,
policy,
) in defender_client.safe_attachments_policies.items():
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name=policy_name,
resource_id=policy.identity,
)
if policy.is_built_in_protection:
# Case 2: Built-in policy with custom policies - always PASS
report.status = "PASS"
report.status_extended = (
f"{policy_name} provides baseline Safe Attachments protection, "
f"but could be overridden by a misconfigured custom policy for specific users."
)
findings.append(report)
else:
# Custom policy - check configuration
rule = defender_client.safe_attachments_rules.get(policy_name)
if not rule:
continue
included_resources = []
if rule.users:
included_resources.append(f"users: {', '.join(rule.users)}")
if rule.groups:
included_resources.append(
f"groups: {', '.join(rule.groups)}"
)
if rule.domains:
included_resources.append(
f"domains: {', '.join(rule.domains)}"
)
# If no users, groups, or domains specified, the policy applies to all recipients
included_resources_str = (
"; ".join(included_resources)
if included_resources
else "all users"
)
if self._is_policy_properly_configured(policy, rule):
# Case 2: Custom policy is properly configured
report.status = "PASS"
report.status_extended = (
f"Custom Safe Attachments policy {policy_name} is properly configured and includes {included_resources_str}, "
f"with priority {rule.priority} (0 is the highest)."
)
else:
# Case 3: Custom policy is not properly configured
report.status = "FAIL"
report.status_extended = (
f"Custom Safe Attachments policy {policy_name} is not properly configured and includes {included_resources_str}, "
f"with priority {rule.priority} (0 is the highest)."
)
findings.append(report)
return findings
def _is_policy_properly_configured(self, policy, rule) -> bool:
"""Check if a custom policy is properly configured according to CIS recommendations."""
return (
rule.state.lower() == "enabled"
and policy.enable
and policy.action == "Block"
and policy.quarantine_tag == "AdminOnlyAccessPolicy"
)
@@ -9,11 +9,12 @@ from prowler.providers.m365.m365_provider import M365Provider
class Defender(M365Service):
"""
Microsoft 365 Defender service class.
Microsoft Defender for Office 365 service class.
This class provides methods to retrieve and manage Microsoft Defender for Office 365
security policies and configurations, including malware filters, spam policies,
anti-phishing settings, ATP (Advanced Threat Protection), and Teams protection policies.
anti-phishing settings, Safe Attachments, Safe Links, ATP (Advanced Threat Protection),
and Teams protection policies.
Attributes:
malware_policies (list): List of malware filter policies.
@@ -26,7 +27,11 @@ class Defender(M365Service):
inbound_spam_policies (list): List of inbound spam filter policies.
inbound_spam_rules (dict): Dictionary of inbound spam filter rules.
report_submission_policy: Report submission policy configuration.
safe_attachments_policies (dict): Dictionary of Safe Attachments policies.
safe_attachments_rules (dict): Dictionary of Safe Attachments rules.
advanced_threat_protection_policy: Advanced Threat Protection policy configuration.
safe_links_policies (dict): Dictionary of Safe Links policies.
safe_links_rules (dict): Dictionary of Safe Links rules.
teams_protection_policy: Teams protection policy configuration.
"""
@@ -35,7 +40,7 @@ class Defender(M365Service):
Initialize the Defender service client.
Args:
provider: The M365Provider instance for authentication and connection.
provider: The M365Provider instance for authentication and configuration.
"""
super().__init__(provider)
self.malware_policies = []
@@ -48,6 +53,8 @@ class Defender(M365Service):
self.inbound_spam_policies = []
self.inbound_spam_rules = {}
self.report_submission_policy = None
self.safe_attachments_policies = {}
self.safe_attachments_rules = {}
self.advanced_threat_protection_policy = None
self.safe_links_policies = {}
self.safe_links_rules = {}
@@ -65,6 +72,8 @@ class Defender(M365Service):
self.inbound_spam_policies = self._get_inbound_spam_filter_policy()
self.inbound_spam_rules = self._get_inbound_spam_filter_rule()
self.report_submission_policy = self._get_report_submission_policy()
self.safe_attachments_policies = self._get_safe_attachments_policies()
self.safe_attachments_rules = self._get_safe_attachments_rules()
self.advanced_threat_protection_policy = (
self._get_advanced_threat_protection_policy()
)
@@ -431,6 +440,72 @@ class Defender(M365Service):
)
return report_submission_policy
def _get_safe_attachments_policies(self):
"""
Retrieve Safe Attachments policies from Microsoft Defender for Office 365.
Returns:
dict[str, SafeAttachmentsPolicy]: A dictionary of Safe Attachments policies keyed by name.
"""
logger.info("Microsoft365 - Getting Defender Safe Attachments policies...")
safe_attachments_policies = {}
try:
policies_data = self.powershell.get_safe_attachments_policy()
if not policies_data:
return safe_attachments_policies
if isinstance(policies_data, dict):
policies_data = [policies_data]
for policy in policies_data:
if policy:
policy_name = policy.get("Name", "")
is_built_in = policy_name == "Built-In Protection Policy"
safe_attachments_policies[policy_name] = SafeAttachmentsPolicy(
name=policy_name,
identity=policy.get("Identity", ""),
enable=policy.get("Enable", False),
action=policy.get("Action", ""),
quarantine_tag=policy.get("QuarantineTag", ""),
redirect=policy.get("Redirect", False),
redirect_address=policy.get("RedirectAddress", ""),
is_built_in_protection=is_built_in,
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return safe_attachments_policies
def _get_safe_attachments_rules(self):
"""
Retrieve Safe Attachments rules from Microsoft Defender for Office 365.
Returns:
dict[str, SafeAttachmentsRule]: A dictionary of Safe Attachments rules keyed by policy name.
"""
logger.info("Microsoft365 - Getting Defender Safe Attachments rules...")
safe_attachments_rules = {}
try:
rules_data = self.powershell.get_safe_attachments_rule()
if not rules_data:
return safe_attachments_rules
if isinstance(rules_data, dict):
rules_data = [rules_data]
for rule in rules_data:
if rule:
policy_name = rule.get("SafeAttachmentPolicy", "")
safe_attachments_rules[policy_name] = SafeAttachmentsRule(
state=rule.get("State", ""),
priority=rule.get("Priority", 0),
users=rule.get("SentTo"),
groups=rule.get("SentToMemberOf"),
domains=rule.get("RecipientDomainIs"),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return safe_attachments_rules
def _get_advanced_threat_protection_policy(self):
"""
Get the Advanced Threat Protection policy.
@@ -655,6 +730,50 @@ class ReportSubmissionPolicy(BaseModel):
report_chat_message_to_customized_address_enabled: bool
class SafeAttachmentsPolicy(BaseModel):
"""
Data model for Safe Attachments policy settings.
Attributes:
name: The name of the policy.
identity: The unique identifier of the policy.
enable: Whether the policy is enabled.
action: The action to take on malicious attachments (Allow, Block, Replace, DynamicDelivery).
quarantine_tag: The quarantine policy applied to detected messages.
redirect: Whether to redirect messages with detected attachments.
redirect_address: The email address to redirect messages to.
is_built_in_protection: Whether this is the Built-in Protection Policy.
"""
name: str
identity: str
enable: bool
action: str
quarantine_tag: str
redirect: bool
redirect_address: str
is_built_in_protection: bool = False
class SafeAttachmentsRule(BaseModel):
"""
Data model for Safe Attachments rule settings.
Attributes:
state: The state of the rule (Enabled/Disabled).
priority: The priority of the rule (0 is highest).
users: List of users the rule applies to.
groups: List of groups the rule applies to.
domains: List of domains the rule applies to.
"""
state: str
priority: int
users: Optional[list[str]]
groups: Optional[list[str]]
domains: Optional[list[str]]
class AdvancedThreatProtectionPolicy(BaseModel):
"""
Model for Advanced Threat Protection policy.
@@ -0,0 +1,468 @@
from unittest import mock
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_defender_safe_attachments_policy_enabled:
"""Tests for the defender_safe_attachments_policy_enabled check."""
def test_no_safe_attachments_policies(self):
"""Test when no Safe Attachments policies exist."""
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_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import (
defender_safe_attachments_policy_enabled,
)
defender_client.safe_attachments_policies = {}
defender_client.safe_attachments_rules = {}
check = defender_safe_attachments_policy_enabled()
result = check.execute()
assert len(result) == 0
def test_case1_only_builtin_policy(self):
"""Case 1: Only Built-in Protection Policy exists - always PASS."""
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_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import (
defender_safe_attachments_policy_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
SafeAttachmentsPolicy,
)
defender_client.safe_attachments_policies = {
"Built-In Protection Policy": SafeAttachmentsPolicy(
name="Built-In Protection Policy",
identity="Built-In-Protection-Policy-ID",
enable=True,
action="Block",
quarantine_tag="AdminOnlyAccessPolicy",
redirect=False,
redirect_address="",
is_built_in_protection=True,
)
}
defender_client.safe_attachments_rules = {}
check = defender_safe_attachments_policy_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "is the only Safe Attachments policy" in result[0].status_extended
assert "baseline protection for all users" in result[0].status_extended
def test_case2_builtin_and_custom_properly_configured(self):
"""Case 2: Built-in + custom policy properly configured - both PASS."""
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_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import (
defender_safe_attachments_policy_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
SafeAttachmentsPolicy,
SafeAttachmentsRule,
)
defender_client.safe_attachments_policies = {
"Built-In Protection Policy": SafeAttachmentsPolicy(
name="Built-In Protection Policy",
identity="Built-In-Protection-Policy-ID",
enable=True,
action="Block",
quarantine_tag="AdminOnlyAccessPolicy",
redirect=False,
redirect_address="",
is_built_in_protection=True,
),
"Custom Policy": SafeAttachmentsPolicy(
name="Custom Policy",
identity="Custom-Policy-ID",
enable=True,
action="Block",
quarantine_tag="AdminOnlyAccessPolicy",
redirect=False,
redirect_address="",
is_built_in_protection=False,
),
}
defender_client.safe_attachments_rules = {
"Custom Policy": SafeAttachmentsRule(
state="Enabled",
priority=0,
users=["user@example.com"],
groups=["Engineering"],
domains=["example.com"],
)
}
check = defender_safe_attachments_policy_enabled()
result = check.execute()
assert len(result) == 2
# Built-in policy PASS
builtin_result = next(
r for r in result if r.resource_name == "Built-In Protection Policy"
)
assert builtin_result.status == "PASS"
assert (
"provides baseline Safe Attachments protection"
in builtin_result.status_extended
)
# Custom policy PASS
custom_result = next(
r for r in result if r.resource_name == "Custom Policy"
)
assert custom_result.status == "PASS"
assert "is properly configured" in custom_result.status_extended
assert "users: user@example.com" in custom_result.status_extended
assert "groups: Engineering" in custom_result.status_extended
assert "domains: example.com" in custom_result.status_extended
assert "priority 0" in custom_result.status_extended
def test_case3_builtin_pass_custom_misconfigured(self):
"""Case 3: Built-in PASS + custom policy misconfigured - Built-in PASS, custom FAIL."""
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_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import (
defender_safe_attachments_policy_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
SafeAttachmentsPolicy,
SafeAttachmentsRule,
)
defender_client.safe_attachments_policies = {
"Built-In Protection Policy": SafeAttachmentsPolicy(
name="Built-In Protection Policy",
identity="Built-In-Protection-Policy-ID",
enable=True,
action="Block",
quarantine_tag="AdminOnlyAccessPolicy",
redirect=False,
redirect_address="",
is_built_in_protection=True,
),
"Custom Policy": SafeAttachmentsPolicy(
name="Custom Policy",
identity="Custom-Policy-ID",
enable=False, # Misconfigured
action="Allow", # Misconfigured
quarantine_tag="DefaultFullAccessPolicy", # Misconfigured
redirect=False,
redirect_address="",
is_built_in_protection=False,
),
}
defender_client.safe_attachments_rules = {
"Custom Policy": SafeAttachmentsRule(
state="Enabled",
priority=0,
users=["user@example.com"],
groups=None,
domains=None,
)
}
check = defender_safe_attachments_policy_enabled()
result = check.execute()
assert len(result) == 2
# Built-in policy still PASS
builtin_result = next(
r for r in result if r.resource_name == "Built-In Protection Policy"
)
assert builtin_result.status == "PASS"
# Custom policy FAIL
custom_result = next(
r for r in result if r.resource_name == "Custom Policy"
)
assert custom_result.status == "FAIL"
assert "is not properly configured" in custom_result.status_extended
assert "priority 0" in custom_result.status_extended
def test_custom_policy_without_rule_skipped(self):
"""Test that custom policies without associated rules are skipped."""
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_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import (
defender_safe_attachments_policy_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
SafeAttachmentsPolicy,
SafeAttachmentsRule,
)
defender_client.safe_attachments_policies = {
"Built-In Protection Policy": SafeAttachmentsPolicy(
name="Built-In Protection Policy",
identity="Built-In-Protection-Policy-ID",
enable=True,
action="Block",
quarantine_tag="AdminOnlyAccessPolicy",
redirect=False,
redirect_address="",
is_built_in_protection=True,
),
"Custom Policy Without Rule": SafeAttachmentsPolicy(
name="Custom Policy Without Rule",
identity="Custom-Policy-ID",
enable=True,
action="Block",
quarantine_tag="AdminOnlyAccessPolicy",
redirect=False,
redirect_address="",
is_built_in_protection=False,
),
}
# Rule for a different policy
defender_client.safe_attachments_rules = {
"Other Policy": SafeAttachmentsRule(
state="Enabled",
priority=0,
users=["user@example.com"],
groups=None,
domains=None,
)
}
check = defender_safe_attachments_policy_enabled()
result = check.execute()
# Only Built-in policy should be in results
assert len(result) == 1
assert result[0].resource_name == "Built-In Protection Policy"
assert result[0].status == "PASS"
def test_custom_policy_with_disabled_rule(self):
"""Test when custom policy has proper settings but disabled rule (FAIL)."""
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_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import (
defender_safe_attachments_policy_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
SafeAttachmentsPolicy,
SafeAttachmentsRule,
)
defender_client.safe_attachments_policies = {
"Built-In Protection Policy": SafeAttachmentsPolicy(
name="Built-In Protection Policy",
identity="Built-In-Protection-Policy-ID",
enable=True,
action="Block",
quarantine_tag="AdminOnlyAccessPolicy",
redirect=False,
redirect_address="",
is_built_in_protection=True,
),
"Custom Policy": SafeAttachmentsPolicy(
name="Custom Policy",
identity="Custom-Policy-ID",
enable=True,
action="Block",
quarantine_tag="AdminOnlyAccessPolicy",
redirect=False,
redirect_address="",
is_built_in_protection=False,
),
}
defender_client.safe_attachments_rules = {
"Custom Policy": SafeAttachmentsRule(
state="Disabled", # Disabled rule
priority=0,
users=["user@example.com"],
groups=None,
domains=None,
)
}
check = defender_safe_attachments_policy_enabled()
result = check.execute()
assert len(result) == 2
# Built-in policy PASS
builtin_result = next(
r for r in result if r.resource_name == "Built-In Protection Policy"
)
assert builtin_result.status == "PASS"
# Custom policy FAIL because rule is disabled
custom_result = next(
r for r in result if r.resource_name == "Custom Policy"
)
assert custom_result.status == "FAIL"
assert "is not properly configured" in custom_result.status_extended
def test_custom_policy_applies_to_all_users_when_no_scope(self):
"""Test that custom policy with no users/groups/domains shows 'all users'."""
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_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import (
defender_safe_attachments_policy_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
SafeAttachmentsPolicy,
SafeAttachmentsRule,
)
defender_client.safe_attachments_policies = {
"Built-In Protection Policy": SafeAttachmentsPolicy(
name="Built-In Protection Policy",
identity="Built-In-Protection-Policy-ID",
enable=True,
action="Block",
quarantine_tag="AdminOnlyAccessPolicy",
redirect=False,
redirect_address="",
is_built_in_protection=True,
),
"Houston Safe Attachments Policy test": SafeAttachmentsPolicy(
name="Houston Safe Attachments Policy test",
identity="Houston-Policy-ID",
enable=False, # Misconfigured
action="Allow",
quarantine_tag="DefaultFullAccessPolicy",
redirect=False,
redirect_address="",
is_built_in_protection=False,
),
}
defender_client.safe_attachments_rules = {
"Houston Safe Attachments Policy test": SafeAttachmentsRule(
state="Enabled",
priority=0,
users=None, # No users specified
groups=None, # No groups specified
domains=None, # No domains specified - applies to ALL users
)
}
check = defender_safe_attachments_policy_enabled()
result = check.execute()
assert len(result) == 2
# Custom policy should show "all users" in status_extended
custom_result = next(
r
for r in result
if r.resource_name == "Houston Safe Attachments Policy test"
)
assert custom_result.status == "FAIL"
assert "is not properly configured" in custom_result.status_extended
assert "all users" in custom_result.status_extended
assert "priority 0" in custom_result.status_extended