Microsoft Entra ID security

How to find Conditional Access policies with deleted objects

List Conditional Access policies that name deleted users or groups with Microsoft Graph PowerShell, check a policy in the Entra admin centre, or use Altivane's Conditional Access report.

By the Published

To find Conditional Access policies with deleted objects, collect the user and group IDs each policy includes or excludes, look them up in the directory, and keep the policies that name an ID the directory no longer returns. A policy names users and groups by ID, so the ID stays in the policy after the object is deleted. A user or Microsoft 365 group deleted in the last 30 days can be restored with the same ID, and so, in preview, can a cloud security group.

Choose the method that fits the job
MethodBest fit
PowerShellA CSV of each policy setting that names a user or group no longer in the directory.
Admin centreChecking 1 policy's users and groups by eye.
AltivaneRecommended for recurring reviews, covering roles, apps and locations too, with the count rated.

Before you start

Use an account in one of these roles: Security Reader, Global Reader, Security Administrator or Conditional Access Administrator. Conditional Access needs Microsoft Entra ID P1. The PowerShell example requests Policy.Read.All and Directory.Read.All; your organisation may require administrator consent.

Know what the script checks. It looks up the users and groups each policy includes or excludes. All, None and GuestsOrExternalUsers are keywords, not IDs, so it leaves them out. The examples read and export; they don't edit any policy.

Option 1

Find policies with deleted objects with PowerShell

Install the modules once, then connect, gather the IDs the policies name and look them up in batches of up to 1,000.

PowerShell
# Install once, if the modules are not already installed.
$modules = 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Identity.SignIns',
    'Microsoft.Graph.DirectoryObjects'
Install-Module $modules -Scope CurrentUser
PowerShell
$ErrorActionPreference = 'Stop'
Connect-MgGraph -Scopes 'Policy.Read.All', 'Directory.Read.All' -NoWelcome
try {
    $keywords = 'All', 'None', 'GuestsOrExternalUsers'
    $references = @(foreach ($policy in Get-MgIdentityConditionalAccessPolicy -All) {
        $users = $policy.Conditions.Users
        $lists = [ordered]@{
            'Included user'  = $users.IncludeUsers
            'Excluded user'  = $users.ExcludeUsers
            'Included group' = $users.IncludeGroups
            'Excluded group' = $users.ExcludeGroups
        }
        foreach ($setting in $lists.Keys) {
            foreach ($id in @($lists[$setting]) | Where-Object { $_ -and $_ -notin $keywords }) {
                [pscustomobject]@{
                    Policy  = $policy.DisplayName
                    State   = $policy.State
                    Setting = $setting
                    Id      = $id
                }
            }
        }
    })
    $ids = @($references | ForEach-Object Id | Sort-Object -Unique)
    $found = @{}
    for ($i = 0; $i -lt $ids.Count; $i += 1000) {
        $batch = $ids[$i..([Math]::Min($i + 999, $ids.Count - 1))]
        Get-MgDirectoryObjectById -Ids $batch -Types 'user', 'group' |
            ForEach-Object { $found[$_.Id] = $true }
    }
    $report = @($references | Where-Object { -not $found[$_.Id] } | Sort-Object Policy, Setting)
    if ($report.Count -eq 0) {
        Write-Output 'Every user and group the policies name is still in the directory.'
    } else {
        $report | Export-Csv -Path './ca-deleted-objects.csv' -NoTypeInformation -Encoding utf8
        $count = @($report | ForEach-Object Policy | Sort-Object -Unique).Count
        Write-Output "$($report.Count) references in $count policies name a deleted user or group."
    }
} finally {
    Disconnect-MgGraph | Out-Null
}

Each row is 1 setting of 1 policy, such as Excluded group, with the ID the directory didn't return. An excluded ID matters most: the accounts it was meant to leave out may now be covered. State shows whether the policy is enabled, report-only or disabled. The script checks users and groups only; named locations need their own comparison.

Option 2

Check a policy's users and groups in the admin centre

  1. Sign in to the Microsoft Entra admin centre for the correct tenant.
  2. Go to Entra ID > Conditional Access > Policies and open a policy.
  3. Under Assignments, select Users and note the users and groups on the Include and Exclude tabs. Leave the policy without saving.
  4. Look for each one in Entra ID > Users or Entra ID > Groups. One that is missing may be in Deleted users or Deleted groups, where it can be restored for 30 days.

This suits checking 1 policy you are about to change. A deleted object is hard to spot by eye across many policies, so for every policy at once, use PowerShell or a report.

Common questions

Can I fix it by restoring the account or group?

Within 30 days, for users and Microsoft 365 groups, and in preview for cloud security groups. They are soft deleted and keep their object ID, so the policy finds them again once restored. App registrations, service principals and named locations can be restored for 30 days too. Once an object is deleted for good, edit the policy to remove the reference.

How do I find out who deleted it?

The audit log records a Delete user or Delete group event for each deletion, with who made it. To keep that record past the log's 7 or 30 days, see how to export the audit log.

Does the script check named locations?

No. It keeps to users and groups. To compare policies with the named locations, see how to find unused named locations, which reads the location IDs the policies use.

Microsoft references

Technical references checked on 7 November 2026. The example scripts have been reviewed and syntax checked; they have not been run against a live tenant.

Talk to us about Altivane

Questions, or pricing? Get in touch.

Contact us