Private/Get-AzureUtilsExistingPrincipal.ps1

function Get-AzureUtilsExistingPrincipal {
    <#
        .SYNOPSIS
            Returns the subset of directory principal ids that still exist in Entra ID.
        .DESCRIPTION
            Resolves the given principal ids against Microsoft Graph
            (POST /directoryObjects/getByIds) via Invoke-AzRestMethod (Az.Accounts),
            in batches of up to 1000 ids per request (the Graph limit). Returns a
            case-insensitive set of the ids that resolved - i.e. the principals that
            still exist. Ids that do NOT come back have been deleted from Entra ID.

            The set is returned so the caller can flag role assignments whose principal
            is absent. If Graph cannot be reached (e.g. missing directory-read
            permission), this throws so the caller never mistakes an API failure for
            "every principal is orphaned".
        .PARAMETER PrincipalId
            The principal object ids to resolve.
        .OUTPUTS
            System.Collections.Generic.HashSet[string] (case-insensitive) of existing ids.
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Generic.HashSet[string]])]
    param(
        [string[]] $PrincipalId
    )

    $existing = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

    $ids = @($PrincipalId | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
    # Return with the unary comma so PowerShell does not enumerate the HashSet on
    # output (which would drop its case-insensitive comparer and hand the caller a
    # plain, case-sensitive array).
    if ($ids.Count -eq 0) { return , $existing }

    $batchSize = 1000
    for ($i = 0; $i -lt $ids.Count; $i += $batchSize) {
        $batch   = $ids[$i..([math]::Min($i + $batchSize - 1, $ids.Count - 1))]
        $payload = @{ ids = @($batch) } | ConvertTo-Json -Depth 3 -Compress

        $response = Invoke-AzRestMethod -Method POST `
            -Uri 'https://graph.microsoft.com/v1.0/directoryObjects/getByIds' `
            -Payload $payload -ErrorAction Stop

        if ([int]$response.StatusCode -ge 400) {
            throw [System.Management.Automation.RuntimeException]::new(
                "Microsoft Graph getByIds failed with status $($response.StatusCode). A directory-read permission (e.g. Directory.Read.All) is required to detect orphaned role assignments. Response: $($response.Content)"
            )
        }

        foreach ($obj in (($response.Content | ConvertFrom-Json).value)) {
            if ($obj.id) { $null = $existing.Add([string]$obj.id) }
        }
    }

    return , $existing
}