Collectors/Cost.ps1

function Get-AerCostWaste {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string[]] $SubscriptionIds,
        $SubscriptionMap
    )

    $subLookup = @{}
    if ($SubscriptionMap -is [hashtable]) { $subLookup = $SubscriptionMap }
    elseif ($SubscriptionMap) { $SubscriptionMap.PSObject.Properties | ForEach-Object { $subLookup[$_.Name] = $_.Value } }
    function SubName($id) { if (-not $id) { return '' }; $n = $subLookup[$id.ToLowerInvariant()]; if ($n) { $n } else { $id } }
    function ArgRows($q) { Expand-AerRows (Invoke-AerArgQuery -SubscriptionIds $SubscriptionIds -Query $q) }
    function Get-CheckData($base) {
        $count = 0; $res = @()
        try { $c = @(ArgRows "$base | summarize Count=count()"); if ($c -and $null -ne $c[0].Count) { $count = [int]$c[0].Count } } catch { }
        try {
            $res = @(ArgRows "$base | project id, name, type=tolower(type), resourceGroup, subscriptionId | limit 200" | ForEach-Object {
                [pscustomobject]@{ Name = "$($_.name)"; Type = ("$($_.type)" -replace '^microsoft\.', ''); ResourceGroup = "$($_.resourceGroup)"; SubscriptionName = (SubName $_.subscriptionId); Id = "$($_.id)" }
            })
        } catch { }
        return @{ Count = $count; Resources = $res }
    }

    # Each check = an ARG heuristic for idle/orphaned resources that typically
    # incur cost without delivering value. Base yields the affected resources.
    $checks = @(
        @{ Title = 'Orphaned managed disks'; ResourceType = 'microsoft.compute/disks'; Note = 'Not attached to any VM'
           Base = "resources | where type =~ 'microsoft.compute/disks' | where isnull(managedBy) or managedBy == ''" }
        @{ Title = 'Unassociated public IP addresses'; ResourceType = 'microsoft.network/publicipaddresses'; Note = 'Not linked to any resource'
           Base = "resources | where type =~ 'microsoft.network/publicipaddresses' | where isnull(properties.ipConfiguration)" }
        @{ Title = 'Stopped VMs still provisioned'; ResourceType = 'microsoft.compute/virtualmachines'; Note = 'Deallocated but still incurring storage costs'
           Base = "resources | where type =~ 'microsoft.compute/virtualmachines' | where properties.extended.instanceView.powerState.code =~ 'PowerState/deallocated'" }
        @{ Title = 'Empty App Service Plans'; ResourceType = 'microsoft.web/serverfarms'; Note = 'No apps deployed'
           Base = "resources | where type =~ 'microsoft.web/serverfarms' | where properties.numberOfSites == 0" }
        @{ Title = 'Load balancers with no backend pools'; ResourceType = 'microsoft.network/loadbalancers'; Note = 'Idle, no backend configured'
           Base = "resources | where type =~ 'microsoft.network/loadbalancers' | where array_length(properties.backendAddressPools) == 0" }
    )

    $items = foreach ($check in $checks) {
        $d = Get-CheckData $check.Base
        [pscustomobject]@{
            Title        = $check.Title
            ResourceType = $check.ResourceType
            Note         = $check.Note
            Count        = $d.Count
            Resources    = @($d.Resources)
        }
    }

    return [pscustomobject]@{
        TotalWastedResources = ($items | Measure-Object -Property Count -Sum).Sum
        Items                = @($items | Sort-Object Count -Descending)
    }
}