Back to Checks
m365

Safe Links policy is enabled and properly configured in Microsoft Defender for Office 365.

defender_safelinks_policy_enabled
Service: defenderSeverity: medium
OfficialBuilt-inVerified
Categories:email-securitye5
Resource Type:NotDefined

Microsoft Defender for Office 365 Safe Links is a 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.

This 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.

Attackers could bypass security by using URLs that redirect to malicious content after initial scanning.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler m365 --checks defender_safelinks_policy_enabled

Recommendation

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.

Remediation

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

Other

1Navigate to Microsoft 365 Defender at https://security.microsoft.com
2Click to expand Email & collaboration and select Policies & rules
3Select Threat policies
4Under Policies, select Safe Links
5Select or create a policy and configure these settings:
6- Enable Safe Links for Email: On
7- Enable Safe Links for Teams: On
8- Enable Safe Links for Office: On
9- Track user clicks: On
10- Let users click through to the original URL: Off
11- Scan URLs: On
12- Apply Safe Links to messages sent within the organization: On
13- Wait for URL scanning to complete before delivering the message: On
14- Do not rewrite URLs: Off
15Save the policy

Check Code

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
        )