Private/Hydration/Get-HydrationDeleteCandidates.ps1

function Get-HydrationDeleteCandidates {
    <#
    .SYNOPSIS
        Finds Graph objects that are safe delete candidates for hydration cleanup.
    .DESCRIPTION
        Enumerates objects from Graph list endpoints, keeps only objects created by the
        Intune Hydration Kit, and restricts matches to known template names.
        If a list response omits description or notes, the function performs a targeted
        GET for likely matches so the hydration marker can still be verified safely.
    .PARAMETER Endpoint
        One or more beta Graph endpoints to enumerate.
    .PARAMETER InputObject
        Pre-enumerated Graph objects to evaluate. Use DeleteUrl for nested resources
        whose delete URL cannot be derived from a flat endpoint.
    .PARAMETER KnownTemplateNames
        Case-insensitive HashSet of current template names used to scope deletes.
    .PARAMETER CandidateFilter
        Optional predicate for endpoints that contain multiple Graph resource types.
        The object is skipped when the predicate returns false.
    .OUTPUTS
        hashtable[] containing Name, Id, and Url for batch delete operations.
    #>

    [CmdletBinding()]
    [OutputType([hashtable[]])]
    param(
        [Parameter()]
        [string[]]$Endpoint,

        [Parameter()]
        [AllowEmptyCollection()]
        [object[]]$InputObject = @(),

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [AllowNull()]
        [System.Collections.Generic.HashSet[string]]$KnownTemplateNames,

        [Parameter()]
        [scriptblock]$CandidateFilter
    )

    $deleteCandidates = @()

    function Add-HydrationDeleteCandidate {
        param(
            [Parameter(Mandatory)]
            [object]$GraphObject,

            [Parameter()]
            [string]$EndpointPath
        )

        $objectName = if ($GraphObject.displayName) {
            $GraphObject.displayName
        } elseif ($GraphObject.name) {
            $GraphObject.name
        } elseif ($GraphObject.Name) {
            $GraphObject.Name
        } else {
            'Unknown'
        }

        if ($CandidateFilter -and -not [bool](& $CandidateFilter $GraphObject)) {
            Write-Verbose "Skipping '$objectName' - object type is not in scope for this delete operation"
            return $null
        }

        $fullObjectUri = if ($GraphObject.FullObjectUri) {
            $GraphObject.FullObjectUri
        } elseif ($EndpointPath) {
            "$EndpointPath/$($GraphObject.id)"
        } else {
            $null
        }

        $deleteDecision = Resolve-HydrationMarkedDeleteCandidate `
            -Name $objectName `
            -Description $GraphObject.description `
            -Notes $GraphObject.notes `
            -KnownTemplateNames $KnownTemplateNames `
            -FullObjectUri $fullObjectUri

        if (-not $deleteDecision.IsMatch) {
            Write-Verbose "Skipping '$objectName' - $($deleteDecision.Message)"
            return $null
        }

        $deleteUrl = if ($GraphObject.DeleteUrl) {
            $GraphObject.DeleteUrl
        } elseif ($EndpointPath) {
            "/$($EndpointPath -replace '^beta/', '')/$($GraphObject.id)"
        } else {
            $null
        }

        if ([string]::IsNullOrWhiteSpace($deleteUrl)) {
            Write-Warning "Skipping '$objectName' - no delete URL could be resolved"
            return $null
        }

        return @{
            Name = $objectName
            Id   = $GraphObject.id
            Url  = $deleteUrl
        }
    }

    foreach ($candidateObject in $InputObject) {
        $candidate = Add-HydrationDeleteCandidate -GraphObject $candidateObject
        if ($candidate) {
            $deleteCandidates += $candidate
        }
    }

    foreach ($currentEndpoint in $Endpoint) {
        # Endpoints may carry a $filter query for listing; strip it for per-object URIs
        $endpointPath = ($currentEndpoint -split '\?', 2)[0]
        try {
            $allPolicies = Get-GraphPagedResults -Uri $currentEndpoint
            foreach ($policy in $allPolicies) {
                $candidate = Add-HydrationDeleteCandidate -GraphObject $policy -EndpointPath $endpointPath
                if ($candidate) {
                    $deleteCandidates += $candidate
                }
            }
        } catch {
            Write-Warning "Failed to list policies from $currentEndpoint : $_"
        }
    }

    return $deleteCandidates
}