mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
feat(m365): add defender_safe_attachments_policy_enabled security check (#9833)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
+40
@@ -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."
|
||||
}
|
||||
+112
@@ -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.
|
||||
|
||||
+468
@@ -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
|
||||
Reference in New Issue
Block a user