public/maester/entra/Test-MtCaReferencedObjectsExist.ps1

function Test-MtCaReferencedObjectsExist {
<#
    .Synopsis
    Checks if any conditional access policies reference non-existent users, groups, or roles.

    .Description
    This test checks if all users, groups, and roles referenced in conditional access policies still exist in the tenant.
    Non-existent or deleted objects in conditional access policies can lead to unexpected behavior and security gaps.
    When a user, group, or role is deleted but still referenced in a policy, it may cause the policy to not apply as expected.

    The test examines:
    - Include/exclude users in conditional access policies
    - Include/exclude groups in conditional access policies
    - Include/exclude roles in conditional access policies (role definition IDs)

    Learn more:
    https://learn.microsoft.com/entra/identity/conditional-access/concept-conditional-access-users-groups

    .Example
    Test-MtCaReferencedObjectsExist

    .LINK
    https://maester.dev/docs/commands/Test-MtCaReferencedObjectsExist
#>

    [CmdletBinding()]
    [OutputType([bool])]
    param ()

    Write-Verbose 'Running Test-MtCaReferencedObjectsExist'

    try {
        # Get all policies (the state of policy does not have to be enabled)
        $policies = Get-MtConditionalAccessPolicy

        # Collect all referenced objects
        $allUsers = $policies.conditions.users.includeUsers + $policies.conditions.users.excludeUsers | Where-Object { $_ -ne 'All' -and $_ -ne 'GuestsOrExternalUsers' -and $_ -ne $null -and $_ -ne '' } | Select-Object -Unique
        $allGroups = $policies.conditions.users.includeGroups + $policies.conditions.users.excludeGroups | Where-Object { $_ -ne $null -and $_ -ne '' } | Select-Object -Unique
        $allRoles = $policies.conditions.users.includeRoles + $policies.conditions.users.excludeRoles | Where-Object { $_ -ne $null -and $_ -ne '' } | Select-Object -Unique

        # Collections to store non-existent objects
        $nonExistentUsers = [System.Collections.Generic.List[object]]::new()
        $nonExistentGroups = [System.Collections.Generic.List[object]]::new()
        $nonExistentRoles = [System.Collections.Generic.List[object]]::new()

        # Check users
        if ($allUsers) {
            Write-Verbose "Checking $($allUsers.Count) users"
            $allUsers | ForEach-Object {
                try {
                    $GraphErrorResult = $null
                    $user = $_
                    Invoke-MtGraphRequest -RelativeUri "users/$user" -ApiVersion beta -ErrorVariable GraphErrorResult -ErrorAction SilentlyContinue | Out-Null
                } catch {
                    if ($GraphErrorResult.Message -match '404 Not Found') {
                        $nonExistentUsers.Add($user) | Out-Null
                    }
                }
            }
        }

        # Check groups
        if ($allGroups) {
            Write-Verbose "Checking $($allGroups.Count) groups"
            $allGroups | ForEach-Object {
                try {
                    $GraphErrorResult = $null
                    $group = $_
                    Invoke-MtGraphRequest -RelativeUri "groups/$group" -ApiVersion beta -ErrorVariable GraphErrorResult -ErrorAction SilentlyContinue | Out-Null
                } catch {
                    if ($GraphErrorResult.Message -match '404 Not Found') {
                        $nonExistentGroups.Add($group) | Out-Null
                    }
                }
            }
        }

        # Check roles
        if ($allRoles) {
            Write-Verbose "Checking $($allRoles.Count) roles"
            $allRoles | ForEach-Object {
                try {
                    $GraphErrorResult = $null
                    $role = $_
                    Write-Verbose "Checking role: $role"
                    # Check roleManagement/directory/roleDefinitions as conditional access policies reference role definition IDs
                    Invoke-MtGraphRequest -RelativeUri "roleManagement/directory/roleDefinitions/$role" -ApiVersion beta -ErrorVariable GraphErrorResult -ErrorAction SilentlyContinue | Out-Null
                } catch {
                    Write-Verbose "Error checking role $role : $($GraphErrorResult.Message)"
                    if ($GraphErrorResult.Message -match '404 Not Found') {
                        $nonExistentRoles.Add($role) | Out-Null
                    }
                }
            }
        }

        # Check if any non-existent objects were found
        $totalNonExistentObjects = ($nonExistentUsers | Measure-Object).Count + ($nonExistentGroups | Measure-Object).Count + ($nonExistentRoles | Measure-Object).Count
        $result = $totalNonExistentObjects -eq 0

        if ($result) {
            $resultDescription = 'Well done! All users, groups, and roles referenced in Conditional Access policies exist in the tenant.'
            $resultMarkdown = $resultDescription
        } else {
            $resultDescription = 'These Conditional Access policies reference non-existent users, groups, or roles:'
            $impactedCaObjects = "`n`n#### Impacted Conditional Access policies`n`n"
            $impactedCaObjects += "| Conditional Access policy | Non-existent object | Object type | Condition | `n"
            $impactedCaObjects += "| --- | --- | --- | --- |`n"

            # Process non-existent users
            $nonExistentUsers | Sort-Object | ForEach-Object {
                $invalidUserId = $_
                $impactedPolicies = $policies | Where-Object { $_.conditions.users.includeUsers -contains $invalidUserId -or $_.conditions.users.excludeUsers -contains $invalidUserId }
                foreach ($impactedPolicy in $impactedPolicies) {
                    if ($impactedPolicy.conditions.users.includeUsers -contains $invalidUserId) {
                        $condition = 'include'
                    } elseif ($impactedPolicy.conditions.users.excludeUsers -contains $invalidUserId) {
                        $condition = 'exclude'
                    } else {
                        $condition = 'Unknown'
                    }
                    $policy = (Get-GraphObjectMarkdown -GraphObjects $impactedPolicy -GraphObjectType ConditionalAccess -AsPlainTextLink)
                    $impactedCaObjects += "| $policy | $invalidUserId | User | $condition | `n"
                }
            }

            # Process non-existent groups
            $nonExistentGroups | Sort-Object | ForEach-Object {
                $invalidGroupId = $_
                $impactedPolicies = $policies | Where-Object { $_.conditions.users.includeGroups -contains $invalidGroupId -or $_.conditions.users.excludeGroups -contains $invalidGroupId }
                foreach ($impactedPolicy in $impactedPolicies) {
                    if ($impactedPolicy.conditions.users.includeGroups -contains $invalidGroupId) {
                        $condition = 'include'
                    } elseif ($impactedPolicy.conditions.users.excludeGroups -contains $invalidGroupId) {
                        $condition = 'exclude'
                    } else {
                        $condition = 'Unknown'
                    }
                    $policy = (Get-GraphObjectMarkdown -GraphObjects $impactedPolicy -GraphObjectType ConditionalAccess -AsPlainTextLink)
                    $impactedCaObjects += "| $policy | $invalidGroupId | Group | $condition | `n"
                }
            }

            # Process non-existent roles
            $nonExistentRoles | Sort-Object | ForEach-Object {
                $invalidRoleId = $_
                $impactedPolicies = $policies | Where-Object { $_.conditions.users.includeRoles -contains $invalidRoleId -or $_.conditions.users.excludeRoles -contains $invalidRoleId }
                foreach ($impactedPolicy in $impactedPolicies) {
                    if ($impactedPolicy.conditions.users.includeRoles -contains $invalidRoleId) {
                        $condition = 'include'
                    } elseif ($impactedPolicy.conditions.users.excludeRoles -contains $invalidRoleId) {
                        $condition = 'exclude'
                    } else {
                        $condition = 'Unknown'
                    }
                    $policy = (Get-GraphObjectMarkdown -GraphObjects $impactedPolicy -GraphObjectType ConditionalAccess -AsPlainTextLink)
                    $impactedCaObjects += "| $policy | $invalidRoleId | Role | $condition | `n"
                }
            }

            $impactedCaObjects += "`n`nNote: Names are not available for deleted objects. If the object was deleted recently, it may be available in the recycle bin (for groups and users) or may need to be re-created (for roles).`n`n"
            $resultMarkdown = $resultDescription + $impactedCaObjects
        }

        Add-MtTestResultDetail -Result $resultMarkdown
        return $result

    } catch {
        Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_
        return $null
    }
}