Back to Checks
m365

Conditional Access policy blocks access from untrusted locations

entra_conditional_access_policy_untrusted_locations_blocked
Service: entraSeverity: medium
OfficialBuilt-inVerified
Categories:identity-accesse3
Resource Type:NotDefined

At least one enabled Conditional Access policy should target all users and all resources, include all network locations, exclude trusted locations, and set the grant control to Block. This blocks access originating from locations that are not explicitly trusted by the organization.

Risk

Without a location-based block policy, users can authenticate from any geography or network, including regions where the organization has no presence, giving attackers operating from those locations a viable path to access resources with stolen credentials.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler m365 --checks entra_conditional_access_policy_untrusted_locations_blocked

Recommendation

Create a Conditional Access policy that blocks access from all locations except trusted named locations, targeting all users and resources, excluding only documented break-glass accounts.

Remediation

Other

1Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/
2Go to **Protection** > **Conditional Access** > **Policies** > **New policy**
3Under **Users**, include **All users** (exclude only documented break-glass accounts)
4Under **Target resources**, select **All resources**
5Under **Network**, include **Any location** and exclude **All trusted locations** (or selected trusted named locations)
6Under **Grant**, select **Block access**
7Set the policy to **On** and click **Create**

Check Code

from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
from prowler.providers.m365.services.entra.entra_service import (
    ConditionalAccessGrantControl,
    ConditionalAccessPolicyState,
)


class entra_conditional_access_policy_untrusted_locations_blocked(Check):
    """Check if a Conditional Access policy blocks access from untrusted locations.

    At least one enabled Conditional Access policy should target all users and all
    resources, include all network locations, exclude trusted locations, and block
    access, so that access from untrusted locations is denied.

    - PASS: An enabled Conditional Access policy blocks access from untrusted
      locations.
    - FAIL: No Conditional Access policy blocks access from untrusted locations.
    """

    def _excludes_only_trusted(self, exclude_locations, trusted_location_ids) -> bool:
        """Check whether every excluded location is trusted.

        Args:
            exclude_locations: Location IDs excluded by the policy.
            trusted_location_ids: IDs of collected trusted named locations.

        Returns:
            True if at least one location is excluded and all exclusions are trusted.
        """
        if not exclude_locations:
            return False
        return all(
            location_id == "AllTrusted" or location_id in trusted_location_ids
            for location_id in exclude_locations
        )

    def execute(self) -> list[CheckReportM365]:
        """Execute the untrusted-locations Conditional Access policy check.

        Returns:
            A list containing the untrusted-locations policy evaluation report.
        """
        findings = []
        report = CheckReportM365(
            metadata=self.metadata(),
            resource={},
            resource_name="Conditional Access Policies",
            resource_id="conditionalAccessPolicies",
        )
        report.status = "FAIL"
        report.status_extended = (
            "No Conditional Access Policy blocks access from untrusted locations."
        )

        trusted_location_ids = {
            location.id
            for location in entra_client.named_locations
            if location.is_trusted
        }
        untrusted_location_ids = {
            location.id
            for location in entra_client.named_locations
            if not location.is_trusted
        }

        for policy in entra_client.conditional_access_policies.values():
            if policy.state == ConditionalAccessPolicyState.DISABLED:
                continue

            if "All" not in policy.conditions.user_conditions.included_users:
                continue

            if (
                "All"
                not in policy.conditions.application_conditions.included_applications
                or policy.conditions.application_conditions.excluded_applications
            ):
                continue

            locations = policy.conditions.locations
            if not locations:
                continue

            if "All" not in locations.include_locations and not any(
                location_id in untrusted_location_ids
                for location_id in locations.include_locations
            ):
                continue

            # A trusted-location exclusion must exist so trusted networks keep access.
            if not self._excludes_only_trusted(
                locations.exclude_locations, trusted_location_ids
            ):
                continue

            if (
                ConditionalAccessGrantControl.BLOCK
                not in policy.grant_controls.built_in_controls
            ):
                continue

            report = CheckReportM365(
                metadata=self.metadata(),
                resource=policy,
                resource_name=policy.display_name,
                resource_id=policy.id,
            )
            if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
                report.status = "FAIL"
                report.status_extended = f"Conditional Access Policy '{policy.display_name}' blocks untrusted locations but is in report-only mode."
            else:
                report.status = "PASS"
                report.status_extended = f"Conditional Access Policy '{policy.display_name}' blocks access from untrusted locations."
                break

        findings.append(report)
        return findings