Private/Get-VMInventory.ps1

function Get-VMInventory {
    <#
    .SYNOPSIS
        Returns a normalized VM inventory for a subscription, including the cheap signals
        (tags, scale-set membership, priority, creation time) used to exclude managed /
        ephemeral compute before any metric pull.

    .DESCRIPTION
        Prefers Azure Resource Graph (one paged query, returns tags + virtualMachineScaleSet
        + priority + timeCreated). Falls back to Get-AzVM when Az.ResourceGraph / Search-AzGraph
        is unavailable (then Created is $null, so -MinAgeDays can't apply). Both paths return
        the same record shape.

    .PARAMETER SubscriptionId
        Subscription to inventory (Resource Graph is scoped to it). The fallback uses the
        current Az context, so the caller must have selected this subscription.

    .PARAMETER ResourceGroupName
        Optional resource-group narrowing filter.

    .PARAMETER VMName
        Optional VM-name narrowing filter (exact names).

    .OUTPUTS
        [pscustomobject[]] records: Id, Name, ResourceGroup, Location, SkuName, Tags, Vmss,
        Priority, Created.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject[]])]
    param(
        [Parameter(Mandatory)] [string] $SubscriptionId,
        [string]   $ResourceGroupName,
        [string[]] $VMName
    )

    $records = [System.Collections.Generic.List[pscustomobject]]::new()

    if (Get-Command Search-AzGraph -ErrorAction SilentlyContinue) {
        $query = @'
resources
| where type == 'microsoft.compute/virtualmachines'
| project id, name, resourceGroup, location,
          skuName = tostring(properties.hardwareProfile.vmSize),
          vmss = tostring(properties.virtualMachineScaleSet.id),
          priority = tostring(properties.priority),
          created = tostring(properties.timeCreated),
          tags
'@

        $skipToken = $null
        do {
            $argParams = @{ Subscription = $SubscriptionId; Query = $query; First = 1000; ErrorAction = 'Stop' }
            if ($skipToken) { $argParams['SkipToken'] = $skipToken }
            $page = Search-AzGraph @argParams
            foreach ($r in $page) {
                $records.Add([pscustomobject]@{
                    Id            = $r.id
                    Name          = $r.name
                    ResourceGroup = $r.resourceGroup
                    Location      = $r.location
                    SkuName       = $r.skuName
                    Tags          = $r.tags
                    Vmss          = $r.vmss
                    Priority      = $r.priority
                    Created       = if ($r.created) { [datetime]$r.created } else { $null }
                })
            }
            $skipToken = $page.SkipToken
        } while ($skipToken)
    }
    else {
        Write-Verbose "Az.ResourceGraph not available; falling back to Get-AzVM (no creation-time signal)."
        $vmParams = @{ Status = $false; ErrorAction = 'Stop' }
        if ($ResourceGroupName) { $vmParams['ResourceGroupName'] = $ResourceGroupName }
        foreach ($vm in (Get-AzVM @vmParams)) {
            $records.Add([pscustomobject]@{
                Id            = $vm.Id
                Name          = $vm.Name
                ResourceGroup = $vm.ResourceGroupName
                Location      = $vm.Location
                SkuName       = $vm.HardwareProfile.VmSize
                Tags          = $vm.Tags
                Vmss          = $vm.VirtualMachineScaleSet.Id
                Priority      = $vm.Priority
                Created       = $null
            })
        }
    }

    $out = $records.ToArray()
    if ($ResourceGroupName) { $out = @($out | Where-Object { $_.ResourceGroup -ieq $ResourceGroupName }) }
    if ($VMName)            { $out = @($out | Where-Object { $_.Name -in $VMName }) }
    return $out
}