mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
feat(m365): add defender_safelinks_policy_enabled security check (#9832)
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_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)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -318,7 +318,9 @@
|
||||
{
|
||||
"Id": "2.1.1",
|
||||
"Description": "Enabling Safe Links policy for Office applications allows URL's that exist inside of Office documents and email applications opened by Office, Office Online and Office mobile to be processed against Defender for Office time-of-click verification and rewritten if required.**Note:** E5 Licensing includes a number of Built-in Protection policies. When auditing policies note which policy you are viewing, and keep in mind CIS recommendations often extend the Default or Build-in Policies provided by MS. In order to **Pass** the highest priority policy must match all settings recommended.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"defender_safelinks_policy_enabled"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "2 Microsoft 365 Defender",
|
||||
|
||||
@@ -339,7 +339,9 @@
|
||||
{
|
||||
"Id": "2.1.1",
|
||||
"Description": "Safe Links for Office Applications is a feature in Microsoft 365 that provides URL scanning and rewriting of inbound email messages in mail flow, and time-of-click verification of URLs and links in email messages, other Microsoft 365 applications such as Teams, and other locations such as SharePoint Online. It can help protect organizations from malicious links used in phishing and other attacks.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"defender_safelinks_policy_enabled"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "2 Microsoft 365 Defender",
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
"defender_antiphishing_policy_configured",
|
||||
"defender_antispam_outbound_policy_configured",
|
||||
"defender_malware_policy_notifications_internal_users_malware_enabled",
|
||||
"defender_safelinks_policy_enabled",
|
||||
"defender_zap_for_teams_enabled",
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled",
|
||||
"entra_identity_protection_sign_in_risk_enabled",
|
||||
@@ -692,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_safelinks_policy_enabled",
|
||||
"defender_zap_for_teams_enabled",
|
||||
"teams_external_domains_restricted",
|
||||
"teams_external_users_cannot_start_conversations"
|
||||
@@ -729,6 +731,7 @@
|
||||
],
|
||||
"Checks": [
|
||||
"defender_antiphishing_policy_configured",
|
||||
"defender_safelinks_policy_enabled",
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled"
|
||||
]
|
||||
},
|
||||
@@ -833,10 +836,11 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"teams_external_domains_restricted",
|
||||
"teams_external_users_cannot_start_conversations",
|
||||
"defender_safelinks_policy_enabled",
|
||||
"sharepoint_external_sharing_managed",
|
||||
"sharepoint_external_sharing_restricted",
|
||||
"sharepoint_external_sharing_managed"
|
||||
"teams_external_domains_restricted",
|
||||
"teams_external_users_cannot_start_conversations"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -193,10 +193,21 @@ class PowerShellSession:
|
||||
result = default
|
||||
|
||||
if error_result:
|
||||
logger.error(f"PowerShell error output: {error_result}")
|
||||
self._process_error(error_result)
|
||||
|
||||
return result
|
||||
|
||||
def _process_error(self, error_result: str) -> None:
|
||||
"""
|
||||
Process error output from the PowerShell command.
|
||||
|
||||
Subclasses can override this to provide custom error handling.
|
||||
|
||||
Args:
|
||||
error_result (str): The error output from the PowerShell command.
|
||||
"""
|
||||
logger.error(f"PowerShell error output: {error_result}")
|
||||
|
||||
def json_parse_output(self, output: str) -> dict:
|
||||
"""
|
||||
Parse command execution output to JSON format.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.powershell.powershell import PowerShellSession
|
||||
@@ -46,6 +49,28 @@ class M365PowerShell(PowerShellSession):
|
||||
self.tenant_identity = identity
|
||||
self.init_credential(credentials)
|
||||
|
||||
@override
|
||||
def _process_error(self, error_result: str) -> None:
|
||||
"""
|
||||
Process PowerShell errors with M365-specific handling.
|
||||
|
||||
Detects cmdlet not found errors which typically indicate missing licensing
|
||||
(e.g., Microsoft Defender for Office 365) or insufficient permissions.
|
||||
|
||||
Args:
|
||||
error_result (str): The error output from the PowerShell command.
|
||||
"""
|
||||
if "is not recognized as a name of a cmdlet" in error_result:
|
||||
cmdlet_match = re.search(r"'([^']+)'.*is not recognized", error_result)
|
||||
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"or insufficient permissions. Related checks will be skipped."
|
||||
)
|
||||
else:
|
||||
super()._process_error(error_result)
|
||||
|
||||
def clean_certificate_content(self, cert_content: str) -> str:
|
||||
"""
|
||||
Clean certificate content for PowerShell consumption.
|
||||
@@ -883,6 +908,57 @@ class M365PowerShell(PowerShellSession):
|
||||
json_parse=True,
|
||||
)
|
||||
|
||||
def get_safe_links_policy(self) -> dict:
|
||||
"""
|
||||
Get Safe Links Policy.
|
||||
|
||||
Retrieves the current Safe Links policy settings for Microsoft Defender for Office 365.
|
||||
|
||||
Returns:
|
||||
dict: Safe Links policy settings in JSON format.
|
||||
|
||||
Example:
|
||||
>>> get_safe_links_policy()
|
||||
{
|
||||
"Name": "Built-In Protection Policy",
|
||||
"Identity": "Built-In Protection Policy",
|
||||
"EnableSafeLinksForEmail": true,
|
||||
"EnableSafeLinksForTeams": true,
|
||||
"EnableSafeLinksForOffice": true,
|
||||
"TrackClicks": true,
|
||||
"AllowClickThrough": false,
|
||||
"ScanUrls": true,
|
||||
"EnableForInternalSenders": true,
|
||||
"DeliverMessageAfterScan": true,
|
||||
"DisableUrlRewrite": false
|
||||
}
|
||||
"""
|
||||
return self.execute(
|
||||
"Get-SafeLinksPolicy | ConvertTo-Json -Depth 10", json_parse=True
|
||||
)
|
||||
|
||||
def get_safe_links_rule(self) -> dict:
|
||||
"""
|
||||
Get Safe Links Rule.
|
||||
|
||||
Retrieves the current Safe Links rule settings for Microsoft Defender for Office 365.
|
||||
|
||||
Returns:
|
||||
dict: Safe Links rule settings in JSON format.
|
||||
|
||||
Example:
|
||||
>>> get_safe_links_rule()
|
||||
{
|
||||
"Name": "Safe Links Rule",
|
||||
"State": "Enabled",
|
||||
"Priority": 0,
|
||||
"SafeLinksPolicy": "Policy Name"
|
||||
}
|
||||
"""
|
||||
return self.execute(
|
||||
"Get-SafeLinksRule | ConvertTo-Json -Depth 10", json_parse=True
|
||||
)
|
||||
|
||||
|
||||
# This function is used to install the required M365 PowerShell modules in Docker containers
|
||||
def initialize_m365_powershell_modules():
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "defender_safelinks_policy_enabled",
|
||||
"CheckTitle": "Safe Links policy is enabled and properly configured in Microsoft Defender for Office 365.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "defender",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Defender Safe Links Policy",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "Safe Links is a Microsoft Defender for Office 365 feature that provides URL scanning and rewriting of inbound email messages, as well as time-of-click verification of URLs and links in email messages, Teams, and Office apps.\n\nThis check verifies that the Safe Links policy is properly configured with all recommended settings enabled.",
|
||||
"Risk": "Without properly configured Safe Links protection, users may be vulnerable to **phishing attacks** and **malicious URLs** in emails, Teams messages, and Office documents.\n\nAttackers could bypass security by using URLs that redirect to malicious content after initial scanning.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/defender-office-365/safe-links-about",
|
||||
"https://learn.microsoft.com/en-us/defender-office-365/safe-links-policies-configure"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "Set-SafeLinksPolicy -Identity 'Built-In Protection Policy' -EnableSafeLinksForEmail $true -EnableSafeLinksForTeams $true -EnableSafeLinksForOffice $true -TrackClicks $true -AllowClickThrough $false -ScanUrls $true -EnableForInternalSenders $true -DeliverMessageAfterScan $true -DisableUrlRewrite $false",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to Microsoft 365 Defender at https://security.microsoft.com\n2. Click to expand Email & collaboration and select Policies & rules\n3. Select Threat policies\n4. Under Policies, select Safe Links\n5. Select or create a policy and configure these settings:\n - Enable Safe Links for Email: On\n - Enable Safe Links for Teams: On\n - Enable Safe Links for Office: On\n - Track user clicks: On\n - Let users click through to the original URL: Off\n - Scan URLs: On\n - Apply Safe Links to messages sent within the organization: On\n - Wait for URL scanning to complete before delivering the message: On\n - Do not rewrite URLs: Off\n6. Save the policy",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable and properly configure Safe Links policies to protect users from malicious URLs in emails, Teams, and Office applications. Ensure URL scanning and time-of-click verification are enabled across all communication channels.",
|
||||
"Url": "https://hub.prowler.com/check/defender_safelinks_policy_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"email-security",
|
||||
"e5"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"defender_antiphishing_policy_configured"
|
||||
],
|
||||
"Notes": "Safe Links requires Microsoft Defender for Office 365 Plan 1 (P1) or higher licensing."
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
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_safelinks_policy_enabled(Check):
|
||||
"""
|
||||
Check if Safe Links policy is enabled and properly configured in Microsoft Defender for Office 365.
|
||||
|
||||
This check verifies that Safe Links policies have the following settings
|
||||
configured according to CIS Microsoft 365 Foundations Benchmark:
|
||||
|
||||
- EnableSafeLinksForEmail = True
|
||||
- EnableSafeLinksForTeams = True
|
||||
- EnableSafeLinksForOffice = True
|
||||
- TrackClicks = True
|
||||
- AllowClickThrough = False
|
||||
- ScanUrls = True
|
||||
- EnableForInternalSenders = True
|
||||
- DeliverMessageAfterScan = True
|
||||
- DisableUrlRewrite = False
|
||||
|
||||
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_links_policies:
|
||||
# Only Built-in Protection Policy exists (no custom policies with rules)
|
||||
if not defender_client.safe_links_rules:
|
||||
policy = next(iter(defender_client.safe_links_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 Links policy and provides baseline protection for all users."
|
||||
findings.append(report)
|
||||
|
||||
# Multiple Safe Links Policies (Built-in + custom policies)
|
||||
else:
|
||||
for policy_name, policy in defender_client.safe_links_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 Links 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_links_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 Links 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 Links 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_safe_links_for_email
|
||||
and policy.enable_safe_links_for_teams
|
||||
and policy.enable_safe_links_for_office
|
||||
and policy.track_clicks
|
||||
and not policy.allow_click_through
|
||||
and policy.scan_urls
|
||||
and policy.enable_for_internal_senders
|
||||
and policy.deliver_message_after_scan
|
||||
and not policy.disable_url_rewrite
|
||||
)
|
||||
@@ -9,18 +9,18 @@ from prowler.providers.m365.m365_provider import M365Provider
|
||||
|
||||
class Defender(M365Service):
|
||||
"""
|
||||
Microsoft 365 Defender service implementation.
|
||||
Microsoft Defender for Office 365 service client.
|
||||
|
||||
Provides access to Microsoft Defender for Office 365 configurations including
|
||||
malware policies, spam filtering, anti-phishing, and Teams protection settings.
|
||||
Provides access to Defender security policies including malware filtering,
|
||||
anti-spam, anti-phishing, Safe Links, Teams protection, and DKIM configurations.
|
||||
"""
|
||||
|
||||
def __init__(self, provider: M365Provider):
|
||||
"""
|
||||
Initialize the Defender service.
|
||||
Initialize the Defender service client.
|
||||
|
||||
Args:
|
||||
provider: The M365 provider instance.
|
||||
provider: The M365Provider instance for authentication and connection.
|
||||
"""
|
||||
super().__init__(provider)
|
||||
self.malware_policies = []
|
||||
@@ -33,6 +33,8 @@ class Defender(M365Service):
|
||||
self.inbound_spam_policies = []
|
||||
self.inbound_spam_rules = {}
|
||||
self.report_submission_policy = None
|
||||
self.safe_links_policies = {}
|
||||
self.safe_links_rules = {}
|
||||
self.teams_protection_policy = None
|
||||
if self.powershell:
|
||||
if self.powershell.connect_exchange_online():
|
||||
@@ -47,6 +49,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_links_policies = self._get_safe_links_policy()
|
||||
self.safe_links_rules = self._get_safe_links_rule()
|
||||
self.teams_protection_policy = self._get_teams_protection_policy()
|
||||
self.powershell.close()
|
||||
|
||||
@@ -366,10 +370,10 @@ class Defender(M365Service):
|
||||
|
||||
def _get_report_submission_policy(self):
|
||||
"""
|
||||
Retrieve the Defender report submission policy.
|
||||
Get the Defender report submission policy.
|
||||
|
||||
Returns:
|
||||
ReportSubmissionPolicy: The report submission policy configuration.
|
||||
ReportSubmissionPolicy: The report submission policy configuration or None.
|
||||
"""
|
||||
logger.info("Microsoft365 - Getting Defender report submission policy...")
|
||||
report_submission_policy = None
|
||||
@@ -408,6 +412,84 @@ class Defender(M365Service):
|
||||
)
|
||||
return report_submission_policy
|
||||
|
||||
def _get_safe_links_policy(self):
|
||||
"""
|
||||
Get Safe Links policies from Microsoft Defender for Office 365.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping policy names to SafeLinksPolicy objects.
|
||||
"""
|
||||
logger.info("Microsoft365 - Getting Defender Safe Links policies...")
|
||||
safe_links_policies = {}
|
||||
try:
|
||||
safe_links_policy_data = self.powershell.get_safe_links_policy()
|
||||
if not safe_links_policy_data:
|
||||
return safe_links_policies
|
||||
if isinstance(safe_links_policy_data, dict):
|
||||
safe_links_policy_data = [safe_links_policy_data]
|
||||
for policy in safe_links_policy_data:
|
||||
if policy:
|
||||
safe_links_policies[policy.get("Name", "")] = SafeLinksPolicy(
|
||||
name=policy.get("Name", ""),
|
||||
identity=policy.get("Identity", ""),
|
||||
enable_safe_links_for_email=policy.get(
|
||||
"EnableSafeLinksForEmail", False
|
||||
),
|
||||
enable_safe_links_for_teams=policy.get(
|
||||
"EnableSafeLinksForTeams", False
|
||||
),
|
||||
enable_safe_links_for_office=policy.get(
|
||||
"EnableSafeLinksForOffice", False
|
||||
),
|
||||
track_clicks=policy.get("TrackClicks", False),
|
||||
allow_click_through=policy.get("AllowClickThrough", True),
|
||||
scan_urls=policy.get("ScanUrls", False),
|
||||
enable_for_internal_senders=policy.get(
|
||||
"EnableForInternalSenders", False
|
||||
),
|
||||
deliver_message_after_scan=policy.get(
|
||||
"DeliverMessageAfterScan", False
|
||||
),
|
||||
disable_url_rewrite=policy.get("DisableUrlRewrite", True),
|
||||
is_built_in_protection=policy.get("IsBuiltInProtection", False),
|
||||
is_default=policy.get("IsDefault", False),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return safe_links_policies
|
||||
|
||||
def _get_safe_links_rule(self):
|
||||
"""
|
||||
Get Safe Links rules from Microsoft Defender for Office 365.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping policy names to SafeLinksRule objects.
|
||||
"""
|
||||
logger.info("Microsoft365 - Getting Defender Safe Links rules...")
|
||||
safe_links_rules = {}
|
||||
try:
|
||||
safe_links_rule_data = self.powershell.get_safe_links_rule()
|
||||
if not safe_links_rule_data:
|
||||
return safe_links_rules
|
||||
if isinstance(safe_links_rule_data, dict):
|
||||
safe_links_rule_data = [safe_links_rule_data]
|
||||
for rule in safe_links_rule_data:
|
||||
if rule:
|
||||
safe_links_rules[rule.get("SafeLinksPolicy", "")] = SafeLinksRule(
|
||||
state=rule.get("State", "Disabled"),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("SentTo", None),
|
||||
groups=rule.get("SentToMemberOf", None),
|
||||
domains=rule.get("RecipientDomainIs", None),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return safe_links_rules
|
||||
|
||||
def _get_teams_protection_policy(self):
|
||||
"""
|
||||
Retrieve the Teams protection policy including ZAP settings.
|
||||
@@ -513,7 +595,7 @@ class InboundSpamRule(BaseModel):
|
||||
|
||||
|
||||
class ReportSubmissionPolicy(BaseModel):
|
||||
"""Model for Defender report submission policy settings."""
|
||||
"""Model for Defender Report Submission Policy configuration."""
|
||||
|
||||
report_junk_to_customized_address: bool
|
||||
report_not_junk_to_customized_address: bool
|
||||
@@ -525,6 +607,34 @@ class ReportSubmissionPolicy(BaseModel):
|
||||
report_chat_message_to_customized_address_enabled: bool
|
||||
|
||||
|
||||
class SafeLinksPolicy(BaseModel):
|
||||
"""Model for Defender Safe Links Policy configuration."""
|
||||
|
||||
name: str
|
||||
identity: str
|
||||
enable_safe_links_for_email: bool
|
||||
enable_safe_links_for_teams: bool
|
||||
enable_safe_links_for_office: bool
|
||||
track_clicks: bool
|
||||
allow_click_through: bool
|
||||
scan_urls: bool
|
||||
enable_for_internal_senders: bool
|
||||
deliver_message_after_scan: bool
|
||||
disable_url_rewrite: bool
|
||||
is_built_in_protection: bool
|
||||
is_default: bool
|
||||
|
||||
|
||||
class SafeLinksRule(BaseModel):
|
||||
"""Model for Defender Safe Links Rule configuration."""
|
||||
|
||||
state: str
|
||||
priority: int
|
||||
users: Optional[list[str]]
|
||||
groups: Optional[list[str]]
|
||||
domains: Optional[list[str]]
|
||||
|
||||
|
||||
class TeamsProtectionPolicy(BaseModel):
|
||||
"""Model for Teams protection policy settings including ZAP configuration."""
|
||||
|
||||
|
||||
+521
@@ -0,0 +1,521 @@
|
||||
from unittest import mock
|
||||
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_defender_safelinks_policy_enabled:
|
||||
"""Tests for the defender_safelinks_policy_enabled check."""
|
||||
|
||||
def test_no_safe_links_policies(self):
|
||||
"""Test when no Safe Links 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_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import (
|
||||
defender_safelinks_policy_enabled,
|
||||
)
|
||||
|
||||
defender_client.safe_links_policies = {}
|
||||
defender_client.safe_links_rules = {}
|
||||
|
||||
check = defender_safelinks_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_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import (
|
||||
defender_safelinks_policy_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
SafeLinksPolicy,
|
||||
)
|
||||
|
||||
defender_client.safe_links_policies = {
|
||||
"Built-In Protection Policy": SafeLinksPolicy(
|
||||
name="Built-In Protection Policy",
|
||||
identity="Built-In-Protection-Policy-ID",
|
||||
enable_safe_links_for_email=True,
|
||||
enable_safe_links_for_teams=True,
|
||||
enable_safe_links_for_office=True,
|
||||
track_clicks=True,
|
||||
allow_click_through=False,
|
||||
scan_urls=True,
|
||||
enable_for_internal_senders=True,
|
||||
deliver_message_after_scan=True,
|
||||
disable_url_rewrite=False,
|
||||
is_built_in_protection=True,
|
||||
is_default=False,
|
||||
)
|
||||
}
|
||||
defender_client.safe_links_rules = {}
|
||||
|
||||
check = defender_safelinks_policy_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "is the only Safe Links 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_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import (
|
||||
defender_safelinks_policy_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
SafeLinksPolicy,
|
||||
SafeLinksRule,
|
||||
)
|
||||
|
||||
defender_client.safe_links_policies = {
|
||||
"Built-In Protection Policy": SafeLinksPolicy(
|
||||
name="Built-In Protection Policy",
|
||||
identity="Built-In-Protection-Policy-ID",
|
||||
enable_safe_links_for_email=True,
|
||||
enable_safe_links_for_teams=True,
|
||||
enable_safe_links_for_office=True,
|
||||
track_clicks=True,
|
||||
allow_click_through=False,
|
||||
scan_urls=True,
|
||||
enable_for_internal_senders=True,
|
||||
deliver_message_after_scan=True,
|
||||
disable_url_rewrite=False,
|
||||
is_built_in_protection=True,
|
||||
is_default=False,
|
||||
),
|
||||
"Custom Policy": SafeLinksPolicy(
|
||||
name="Custom Policy",
|
||||
identity="Custom-Policy-ID",
|
||||
enable_safe_links_for_email=True,
|
||||
enable_safe_links_for_teams=True,
|
||||
enable_safe_links_for_office=True,
|
||||
track_clicks=True,
|
||||
allow_click_through=False,
|
||||
scan_urls=True,
|
||||
enable_for_internal_senders=True,
|
||||
deliver_message_after_scan=True,
|
||||
disable_url_rewrite=False,
|
||||
is_built_in_protection=False,
|
||||
is_default=False,
|
||||
),
|
||||
}
|
||||
defender_client.safe_links_rules = {
|
||||
"Custom Policy": SafeLinksRule(
|
||||
state="Enabled",
|
||||
priority=0,
|
||||
users=["user@example.com"],
|
||||
groups=["Engineering"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_safelinks_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 Links 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_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import (
|
||||
defender_safelinks_policy_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
SafeLinksPolicy,
|
||||
SafeLinksRule,
|
||||
)
|
||||
|
||||
defender_client.safe_links_policies = {
|
||||
"Built-In Protection Policy": SafeLinksPolicy(
|
||||
name="Built-In Protection Policy",
|
||||
identity="Built-In-Protection-Policy-ID",
|
||||
enable_safe_links_for_email=True,
|
||||
enable_safe_links_for_teams=True,
|
||||
enable_safe_links_for_office=True,
|
||||
track_clicks=True,
|
||||
allow_click_through=False,
|
||||
scan_urls=True,
|
||||
enable_for_internal_senders=True,
|
||||
deliver_message_after_scan=True,
|
||||
disable_url_rewrite=False,
|
||||
is_built_in_protection=True,
|
||||
is_default=False,
|
||||
),
|
||||
"Custom Policy": SafeLinksPolicy(
|
||||
name="Custom Policy",
|
||||
identity="Custom-Policy-ID",
|
||||
enable_safe_links_for_email=False, # Misconfigured
|
||||
enable_safe_links_for_teams=False, # Misconfigured
|
||||
enable_safe_links_for_office=True,
|
||||
track_clicks=True,
|
||||
allow_click_through=True, # Misconfigured
|
||||
scan_urls=True,
|
||||
enable_for_internal_senders=True,
|
||||
deliver_message_after_scan=True,
|
||||
disable_url_rewrite=False,
|
||||
is_built_in_protection=False,
|
||||
is_default=False,
|
||||
),
|
||||
}
|
||||
defender_client.safe_links_rules = {
|
||||
"Custom Policy": SafeLinksRule(
|
||||
state="Enabled",
|
||||
priority=0,
|
||||
users=["user@example.com"],
|
||||
groups=None,
|
||||
domains=None,
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_safelinks_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_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import (
|
||||
defender_safelinks_policy_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
SafeLinksPolicy,
|
||||
SafeLinksRule,
|
||||
)
|
||||
|
||||
defender_client.safe_links_policies = {
|
||||
"Built-In Protection Policy": SafeLinksPolicy(
|
||||
name="Built-In Protection Policy",
|
||||
identity="Built-In-Protection-Policy-ID",
|
||||
enable_safe_links_for_email=True,
|
||||
enable_safe_links_for_teams=True,
|
||||
enable_safe_links_for_office=True,
|
||||
track_clicks=True,
|
||||
allow_click_through=False,
|
||||
scan_urls=True,
|
||||
enable_for_internal_senders=True,
|
||||
deliver_message_after_scan=True,
|
||||
disable_url_rewrite=False,
|
||||
is_built_in_protection=True,
|
||||
is_default=False,
|
||||
),
|
||||
"Custom Policy Without Rule": SafeLinksPolicy(
|
||||
name="Custom Policy Without Rule",
|
||||
identity="Custom-Policy-ID",
|
||||
enable_safe_links_for_email=True,
|
||||
enable_safe_links_for_teams=True,
|
||||
enable_safe_links_for_office=True,
|
||||
track_clicks=True,
|
||||
allow_click_through=False,
|
||||
scan_urls=True,
|
||||
enable_for_internal_senders=True,
|
||||
deliver_message_after_scan=True,
|
||||
disable_url_rewrite=False,
|
||||
is_built_in_protection=False,
|
||||
is_default=False,
|
||||
),
|
||||
}
|
||||
# Rule for a different policy
|
||||
defender_client.safe_links_rules = {
|
||||
"Other Policy": SafeLinksRule(
|
||||
state="Enabled",
|
||||
priority=0,
|
||||
users=["user@example.com"],
|
||||
groups=None,
|
||||
domains=None,
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_safelinks_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_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import (
|
||||
defender_safelinks_policy_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
SafeLinksPolicy,
|
||||
SafeLinksRule,
|
||||
)
|
||||
|
||||
defender_client.safe_links_policies = {
|
||||
"Built-In Protection Policy": SafeLinksPolicy(
|
||||
name="Built-In Protection Policy",
|
||||
identity="Built-In-Protection-Policy-ID",
|
||||
enable_safe_links_for_email=True,
|
||||
enable_safe_links_for_teams=True,
|
||||
enable_safe_links_for_office=True,
|
||||
track_clicks=True,
|
||||
allow_click_through=False,
|
||||
scan_urls=True,
|
||||
enable_for_internal_senders=True,
|
||||
deliver_message_after_scan=True,
|
||||
disable_url_rewrite=False,
|
||||
is_built_in_protection=True,
|
||||
is_default=False,
|
||||
),
|
||||
"Custom Policy": SafeLinksPolicy(
|
||||
name="Custom Policy",
|
||||
identity="Custom-Policy-ID",
|
||||
enable_safe_links_for_email=True,
|
||||
enable_safe_links_for_teams=True,
|
||||
enable_safe_links_for_office=True,
|
||||
track_clicks=True,
|
||||
allow_click_through=False,
|
||||
scan_urls=True,
|
||||
enable_for_internal_senders=True,
|
||||
deliver_message_after_scan=True,
|
||||
disable_url_rewrite=False,
|
||||
is_built_in_protection=False,
|
||||
is_default=False,
|
||||
),
|
||||
}
|
||||
defender_client.safe_links_rules = {
|
||||
"Custom Policy": SafeLinksRule(
|
||||
state="Disabled", # Disabled rule
|
||||
priority=0,
|
||||
users=["user@example.com"],
|
||||
groups=None,
|
||||
domains=None,
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_safelinks_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_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import (
|
||||
defender_safelinks_policy_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
SafeLinksPolicy,
|
||||
SafeLinksRule,
|
||||
)
|
||||
|
||||
defender_client.safe_links_policies = {
|
||||
"Built-In Protection Policy": SafeLinksPolicy(
|
||||
name="Built-In Protection Policy",
|
||||
identity="Built-In-Protection-Policy-ID",
|
||||
enable_safe_links_for_email=True,
|
||||
enable_safe_links_for_teams=True,
|
||||
enable_safe_links_for_office=True,
|
||||
track_clicks=True,
|
||||
allow_click_through=False,
|
||||
scan_urls=True,
|
||||
enable_for_internal_senders=True,
|
||||
deliver_message_after_scan=True,
|
||||
disable_url_rewrite=False,
|
||||
is_built_in_protection=True,
|
||||
is_default=False,
|
||||
),
|
||||
"Houston Safe Links Policy test": SafeLinksPolicy(
|
||||
name="Houston Safe Links Policy test",
|
||||
identity="Houston-Policy-ID",
|
||||
enable_safe_links_for_email=False, # Misconfigured
|
||||
enable_safe_links_for_teams=False,
|
||||
enable_safe_links_for_office=False,
|
||||
track_clicks=False,
|
||||
allow_click_through=True,
|
||||
scan_urls=False,
|
||||
enable_for_internal_senders=False,
|
||||
deliver_message_after_scan=False,
|
||||
disable_url_rewrite=True,
|
||||
is_built_in_protection=False,
|
||||
is_default=False,
|
||||
),
|
||||
}
|
||||
defender_client.safe_links_rules = {
|
||||
"Houston Safe Links Policy test": SafeLinksRule(
|
||||
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_safelinks_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 Links 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