HomeNewsletterCommunityToolsArchiveBlogToday's NewsAboutServicesQuick Links Subscribe free
← Back to Blog
Entra ID Conditional AccessEntra IDZero TrustPowerShellSecurityMicrosoft Graph

Do You Know Which Users Have ZERO Conditional Access Policy Applied? Here's How to Check

IA
Imran Awan
16 July 2026

Every admin I know has the same four baseline Conditional Access policies: require MFA for all users, block legacy authentication, require a compliant device for corporate resources, and block sign-ins from risky locations. Ask them if those policies protect everyone in the tenant and they will say yes without hesitating. Now ask them to actually prove it — to list, right now, without opening PowerShell or spending an afternoon clicking through the portal, the users in their tenant who have zero Conditional Access policy applied to them at all. Most cannot answer. Not because the data doesn't exist, but because nobody has ever built the report that surfaces it.

That gap is the subject of this post. Conditional Access does not work the way most admins mentally model it. Policies are not a blanket that silently covers "everyone, forever." They target specific groups, specific applications, and specific conditions — and every one of those targeting decisions is a place a user, a guest, an app, or a service account can slip through unnoticed. Below: exactly why that happens, how to manually spot-check one suspected gap with the Conditional Access What If tool, and a complete PowerShell script that answers the real question properly — for every identity in your tenant, is there at least one enforced policy that actually applies to them?

The problem: your CA baseline has gaps you cannot see

Conditional Access targeting defaults to specific, named scopes — a group, an app, a role — not an implicit "all users, forever" the way a lot of admins assume once the policy is live. That distinction is exactly where coverage silently erodes over time. Five patterns account for almost every real-world gap I have found while auditing tenants:

New starters

A user is created in Entra ID today. Your CA policies target a dynamic or assigned security group built months ago. If provisioning doesn't add the new user to that group immediately — a common lag with HR-driven joiner workflows — the user signs in completely outside your MFA and device-compliance requirements until the next group sync.

Orphaned app registrations

Someone in another team registers a new enterprise application and nobody loops in security. Your "block legacy auth" and "require compliant device" policies were scoped to a specific list of cloud apps built two years ago — the new app was never added, so it inherits none of your baseline protection.

Exclusion group creep

An old exclusion group — created for a one-off pilot, a vendor migration, or a "temporary" break-glass workaround — is still referenced in your MFA policy's exclude list. Nobody ever removes members from it. It has grown from 3 accounts to 40 over three years.

Break-glass accounts

Emergency-access accounts are correctly excluded from CA so a bad policy change can never lock out the last admin. Correct in principle — but if that exclusion is never revisited, and the account credentials or scope quietly expand, it becomes a standing, completely unmonitored bypass.

Workload identities

Service principals and managed identities authenticate too — and most admins don't realise they need their own, separate Conditional Access policies. Conditional Access for workload identities is a distinct, newer capability with completely different targeting (client applications, not users). A user-scoped MFA policy does nothing for a compromised service principal.

Watch out: "We have MFA for all users" is a policy description, not a guarantee. It only means a policy named that exists and is enabled — it says nothing about whether its include/exclude targeting actually resolves to literally every identity that can authenticate against your tenant today.

Why it happens: how policy targeting actually works

Every Conditional Access policy evaluates a conditions.users block for user-facing policies (or conditions.clientApplications for workload identity policies) with four fields that determine who the policy actually reaches:

FieldWhat it doesWhere gaps hide
includeUsersSpecific user IDs, or the special values All, None, GuestsOrExternalUsersA policy scoped by mistake to a static list of user IDs never grows as the tenant does
excludeUsersUser IDs always removed from coverage, regardless of include rulesExclusions always win over inclusions — a single stale exclusion silently defeats an otherwise-correct policy
includeGroups / excludeGroupsExpands to every current member of the referenced group(s) at evaluation timeGroup membership drifts continuously; the policy's intent ("everyone in Finance") and its current effective membership quietly diverge
includeApplicationsWhich cloud apps / resources the policy protects — All or a specific listNew app registrations and enterprise apps are not automatically added to an existing app-scoped policy

The exclusion creep problem in practice

Here is the pattern I see most often on real tenants. A "Require MFA for all users" policy is built correctly: includeUsers = All. Six months later, a contractor migration needed a temporary carve-out, so someone created SG-MFA-Exclusions-Temp and added it to the policy's exclude list. The migration finished. The group was never deleted, and — because it's a security group with no owner review cadence — three more "just add them for now" requests over the following year pushed membership from 4 accounts to 31. The policy still says "MFA for all users" in its name. It has not meant that in over a year.

Group membership changes are also never retroactively re-evaluated against the design intent of a policy — Entra ID has no concept of "this group was supposed to represent X population, alert me if it drifts." It simply evaluates current membership at sign-in time, silently, forever. And critically: Entra ID ships no native report that answers "which users currently have zero applicable Conditional Access policy." The Insights and reporting workbook shows policy-by-policy application counts, not a per-user, cross-policy union. This has to be computed by walking every enabled policy's effective include/exclude set and checking every identity against it — which is exactly what the script later in this post does.

Gotcha: A policy set to Report-only shows up in the Entra portal identically to an enabled policy — same name, same targeting, same "on" appearance in the policy list. It does not enforce anything. Users covered only by report-only policies have exactly the same real-world protection as users with zero policies at all. Any audit — manual or scripted — must treat State = enabledForReportingButNotEnforced as a separate bucket, not as coverage.

How to verify: spot-checking one gap with the What If tool

Before reaching for a script, the fastest way to sanity-check a single suspected gap — "does user X actually get MFA challenged, or not?" — is the built-in What If tool.

  1. Sign in to the Microsoft Entra admin centre as at least a Conditional Access Administrator or Security Administrator
  2. Browse to Entra ID › Conditional Access › Policies, then select What If from the left-hand menu
  3. Under User, search for and select the identity you want to test — for example a newly created starter, or an account you suspect is caught by a stale exclusion group
  4. Under Cloud apps or actions, choose the resource you care about — typically Office 365 or All cloud apps
  5. Optionally set sign-in conditions: device platform, client app type, and IP address / named location, to simulate a specific real-world sign-in scenario
  6. Click What If and review the two result tabs — Policies that will apply and Policies that will not apply, each with the specific reason (target, condition, or exclusion) why
Conditional Access — What If results for j.doe@contoso.com
Block legacy authentication
Include: All users · All cloud apps
Will apply
Require compliant device
Include: SG-Corp-Managed-Devices · Office 365
Will apply
Require MFA for all users
Include: All users · Exclude: SG-MFA-Exclusions-Temp
Will not apply — user is in an excluded group

That single result is the whole problem in miniature: two policies apply as expected, but the baseline MFA policy silently does not — because this test user was added to a "temporary" exclusion group eleven months ago for a project that finished in week two.

Don't: rely on the What If tool as your coverage audit strategy. It evaluates exactly one identity, against exactly the sign-in conditions you specify, one run at a time — it does not scale to checking a few thousand users, and it does not evaluate workload identity (service principal) Conditional Access policies at all. It is a verification tool for a hypothesis you already have, not a discovery tool for gaps you don't know exist yet. For that, you need to check every identity programmatically — which is what the rest of this post covers.

The fix: Get-ConditionalAccessCoverageGap.ps1

The script below uses the Microsoft Graph PowerShell SDK to do exactly what the portal can't: pull every Conditional Access policy, expand every referenced group into its actual current membership, and check every user and every workload identity (service principal) in the tenant against that resolved set. Anyone left with zero enforced policy applying to them is flagged red.

Download the script — GitHub

Free and open source. Download Get-ConditionalAccessCoverageGap.ps1 from the Imran76Awan/Daily-Tasks repository on GitHub — no sign-in required.

Get-ConditionalAccessCoverageGap.ps1 View full repo →

Here is what the first run against a test tenant looks like, before any remediation:

PowerShell 7 — Get-ConditionalAccessCoverageGap.ps1 (initial run)
Connecting to Microsoft Graph...
Connected as: svc-ca-audit@contoso.com  Tenant: ████████-████-████-████-████████████
Retrieving Conditional Access policies...
  Total policies found        : 9
  Enforced (State=enabled)    : 6
  Report-only (not enforced)  : 2
  Disabled (ignored)          : 1
  No workload identity (client application) policies found — every service
  principal will show as RED unless you add one. This usually means Entra ID
  Workload Identities Premium has not been configured yet.

Retrieving users...
  Enabled users found          : 1,187

Evaluating Conditional Access coverage per user...

Retrieving service principals (workload identities)...
  Application service principals found : 64

================================================================
  CONDITIONAL ACCESS COVERAGE GAP AUDIT - 16 Jul 2026 09:14
================================================================
  Identities scanned            : 1,251
  GREEN  (enforced coverage)     : 1,246
  AMBER  (report-only only)      : 1
  RED    (zero applicable policy): 4
  ----------------------------------------------------------------
  Enforced coverage              : 99.6%
================================================================

RED identities (no enforced Conditional Access policy applies):

IdentityType      DisplayName                Identifier                        UserType
------------      -----------                ----------                        --------
User              Amelia H████              a.h████@contoso.com              Member
User              Contractor B████          b████@vendor-b2b.com              Guest
User              Ops-BreakGlass-02         bg2█████@contoso.onmicrosoft.com Member
ServicePrincipal  Finance-Reporting-API     ████████-████-████-████-████████████  Workload

AMBER identities (covered ONLY by report-only policies — not enforced):

IdentityType      DisplayName                Identifier                        ReportOnlyOnly
------------      -----------                ----------                        --------------
User              Priya K████               p.k████@contoso.com              Require phishing-resistant MFA (pilot)

ACTION: build a true catch-all policy (All users / All cloud apps) with a
tightly scoped break-glass exclusion group, add a separate workload identity
policy for service principals, then re-run this script to confirm 0 RED.

Reading that output: Amelia H. was a starter created four days earlier and hadn't yet synced into SG-CA-Baseline-All-Staff. The Contractor account was a guest caught by an old SG-MFA-Exclusions-Temp group from a project that ended over a year ago. Ops-BreakGlass-02 was a second emergency-access account nobody remembered existed, let alone reviewed. And Finance-Reporting-API is exactly the workload-identity gap most admins never think to check — a service principal with an application-only Graph token and no Conditional Access policy of any kind covering it.

The core of the script is a group-expansion and set-comparison function — it resolves every group referenced by includeGroups / excludeGroups into a flat set of member IDs (cached, so a group referenced by five policies is only expanded once), then checks whether a given user is included and not excluded:

Get-ConditionalAccessCoverageGap.ps1 — group expansion & coverage test
function Get-ExpandedGroupMemberIds {
    param([string[]]$GroupIds)
    $result = [System.Collections.Generic.HashSet[string]]::new()
    foreach ($groupId in $GroupIds) {
        if (-not $script:GroupMemberCache.ContainsKey($groupId)) {
            $members = Get-MgGroupMember -GroupId $groupId -All
            $script:GroupMemberCache[$groupId] = @($members | ForEach-Object { $_.Id })
        }
        foreach ($id in $script:GroupMemberCache[$groupId]) { [void]$result.Add($id) }
    }
    return $result
}

function Test-UserCoveredByPolicy {
    param($Policy, [string]$UserId, [string]$UserType)
    $u = $Policy.Conditions.Users
    $includeSet = Get-ExpandedGroupMemberIds -GroupIds $u.IncludeGroups
    $excludeSet = Get-ExpandedGroupMemberIds -GroupIds $u.ExcludeGroups
    $includesAll = $u.IncludeUsers -contains 'All'

    $isIncluded = $includesAll -or $includeSet.Contains($UserId) -or ($u.IncludeUsers -contains $UserId)
    if (-not $isIncluded) { return $false }

    # exclude always wins — this is where stale exclusion groups silently defeat coverage
    if ($excludeSet.Contains($UserId) -or ($u.ExcludeUsers -contains $UserId)) { return $false }
    return $true
}

Workload identities are checked the same way, but against Conditions.ClientApplications.IncludeServicePrincipals / ExcludeServicePrincipals instead of the user block — a completely separate targeting mechanism, which is exactly why a user-scoped MFA policy provides zero protection for a compromised service principal.

Note: This is an Entra-only control; there's no Group Policy equivalent for Conditional Access. CA policies live entirely in the cloud identity plane and are evaluated at every token issuance against Entra ID — there is no on-premises GPO/CSP path that replicates, overrides, or supplements this targeting. If you want coverage enforced, it has to be built and audited entirely inside Entra ID.

Remediation: closing the gaps the script finds

Remediation Sequence
1
Build a true catch-all policy
Target All users and All cloud apps explicitly, with a single, deliberately and tightly scoped break-glass exclusion group — not an ad hoc list of user accounts pasted into the exclude field. Name it something unambiguous like SG-CA-BreakGlass-Excl and give it an owner who reviews membership quarterly.
2
Add a separate workload identity CA policy
Scope it to service principals via Conditions › Client applications, targeting the service principals that hold sensitive Graph or API permissions first. This capability requires Microsoft Entra ID Workload Identities Premium — confirm licensing before you build the policy.
3
Fix the immediate RED and AMBER results
Add the missed starter to the baseline group, prune the stale contractor exclusion group down to its currently-justified members, formally review and re-scope the forgotten break-glass account, and switch the pilot MFA policy from report-only to enabled once you've confirmed its impact.
4
Re-run the script VERIFY
Confirm RED drops to zero after each change, not just once at the end — remediating one gap can occasionally reveal another (for example, adding a catch-all policy can surface an app that was never in scope of any app-scoped policy).
Tip: Put your break-glass exclusion group behind Privileged Identity Management (PIM) for Groups with a mandatory approval workflow for membership changes, and set up an Entra ID access review on it every 90 days. A break-glass account is only safe if its exclusion is monitored at least as closely as its usage.

Proof it worked: re-running after remediation

After adding the catch-all policy with the correctly scoped break-glass exclusion group, adding the workload identity policy for service principals, fixing the starter's group membership, pruning the stale exclusion group, and switching the pilot MFA policy to enforced — here is the same script run again against the same tenant:

PowerShell 7 — Get-ConditionalAccessCoverageGap.ps1 (post-remediation run)
Connecting to Microsoft Graph...
Connected as: svc-ca-audit@contoso.com  Tenant: ████████-████-████-████-████████████
Retrieving Conditional Access policies...
  Total policies found        : 11
  Enforced (State=enabled)    : 9
  Report-only (not enforced)  : 1
  Disabled (ignored)          : 1

Retrieving users...
  Enabled users found          : 1,188

Evaluating Conditional Access coverage per user...

Retrieving service principals (workload identities)...
  Application service principals found : 64

================================================================
  CONDITIONAL ACCESS COVERAGE GAP AUDIT - 16 Jul 2026 15:52
================================================================
  Identities scanned            : 1,252
  GREEN  (enforced coverage)     : 1,252
  AMBER  (report-only only)      : 0
  RED    (zero applicable policy): 0
  ----------------------------------------------------------------
  Enforced coverage              : 100.0%
================================================================

No zero-coverage identities found in this pass. Re-run periodically — new
users, new app registrations, and group membership changes can reopen gaps.

Zero RED, zero AMBER, 100% enforced coverage — including Finance-Reporting-API, now covered by the new workload identity policy. That number will not stay at 100% by itself. New starters, new app registrations, and ordinary group membership churn will reopen gaps within weeks. Schedule this script — a monthly Azure Automation runbook or a Graph-permissioned scheduled task is enough — and treat any RED result as an incident to triage, not a report to file away.

💼 Need this done for you?
Want me to run this audit against your live tenant?

I'll run the Conditional Access coverage script against your tenant, walk you through every gap it finds, and help you build a catch-all policy that doesn't lock anyone out — including workload identities.

💬 See how I can help → 📅 Book a free call

References

Have you actually checked your tenant for this gap, or are you assuming your baseline policies cover everyone because the names say so? Run the script and find out — I'd genuinely like to know what you find, especially on the workload identity side. Drop a comment or reply on LinkedIn with your RED count before remediation.

Share this post
LinkedIn X / Twitter Reddit Bluesky

More from EndpointWeekly

Entra ID
How Many of Your Global Admins Are PERMANENTLY Global Admins?…
Standing privileged access is the top finding in every security review — and turning on…
Entra ID
The Primary Refresh Token (PRT): How Entra ID SSO Actually Works…
The PRT is the token that powers silent SSO across every Microsoft 365 app on your…
Entra ID
Entra Conditional Access: WHfB Enforcement Deadline July 2026
Microsoft's July 2026 deadline for phishing-resistant MFA enforcement is approaching.…