Private/New-AzureUtilsOrphanResourceQuery.ps1

function New-AzureUtilsOrphanResourceQuery {
    <#
        .SYNOPSIS
            Builds the Resource Graph (KQL) query for orphaned resources.
        .DESCRIPTION
            Pure, side-effect-free. Emits a 'where' over the selected orphan types
            and a 'reason' column explaining why each resource is considered
            orphaned. Null-safe association checks treat missing arrays as empty.
        .PARAMETER Type
            Which orphan categories to include. Defaults to all.
        .OUTPUTS
            System.String (the KQL query)
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [ValidateSet('Disk', 'NetworkInterface', 'PublicIP', 'NetworkSecurityGroup', 'RouteTable')]
        [string[]] $Type = @('Disk', 'NetworkInterface', 'PublicIP', 'NetworkSecurityGroup', 'RouteTable')
    )

    $conditions = [System.Collections.Generic.List[string]]::new()

    if ($Type -contains 'Disk') {
        $conditions.Add("(type =~ 'microsoft.compute/disks' and tostring(properties.diskState) =~ 'Unattached')")
    }
    if ($Type -contains 'NetworkInterface') {
        $conditions.Add("(type =~ 'microsoft.network/networkinterfaces' and isnull(properties.virtualMachine) and isnull(properties.privateEndpoint))")
    }
    if ($Type -contains 'PublicIP') {
        $conditions.Add("(type =~ 'microsoft.network/publicipaddresses' and isnull(properties.ipConfiguration) and isnull(properties.natGateway))")
    }
    if ($Type -contains 'NetworkSecurityGroup') {
        $conditions.Add("(type =~ 'microsoft.network/networksecuritygroups' and (isnull(properties.networkInterfaces) or array_length(properties.networkInterfaces) == 0) and (isnull(properties.subnets) or array_length(properties.subnets) == 0))")
    }
    if ($Type -contains 'RouteTable') {
        $conditions.Add("(type =~ 'microsoft.network/routetables' and (isnull(properties.subnets) or array_length(properties.subnets) == 0))")
    }

    $where = $conditions -join "`n or "

    return @"
resources
| where $where
| extend reason = case(
    type =~ 'microsoft.compute/disks', 'Unattached managed disk',
    type =~ 'microsoft.network/networkinterfaces', 'Network interface not attached',
    type =~ 'microsoft.network/publicipaddresses', 'Public IP not associated',
    type =~ 'microsoft.network/networksecuritygroups', 'Network security group not associated',
    type =~ 'microsoft.network/routetables', 'Route table not associated',
    'Orphaned')
| project id, name, type, resourceGroup, location, subscriptionId, tags, reason
"@

}