Public/Find-AzureUtilsIdleResource.ps1

function Find-AzureUtilsIdleResource {
    <#
        .SYNOPSIS
            Finds idle Azure resources (running/allocated but doing no work).

        .DESCRIPTION
            Uses Azure Resource Graph to surface resources that still exist (and may
            still cost money) while serving no purpose, and explains each in a
            'Reason' column. This complements Find-AzureUtilsOrphanResource, which
            looks for *unassociated* resources - here the focus is *idle* ones.
            Returns one object per finding (also a colored console table). Categories
            (all by default, narrow with -Type):

                StoppedVm VMs that are not running - 'stopped' (still
                                     billed for compute) or 'deallocated' (disks
                                     and static public IPs may still cost)
                EmptyAppServicePlan fixed-tier App Service Plans hosting no apps
                StoppedAksCluster AKS clusters in the 'Stopped' power state

            These are heuristics; review before acting.

        .PARAMETER SubscriptionId
            One or more subscription IDs. Default: every enabled subscription.

        .PARAMETER ManagementGroupId
            One or more management groups to scan (requires Az.ResourceGraph).

        .PARAMETER Type
            Limit the idle categories to check. Defaults to all of them.

        .EXAMPLE
            Find-AzureUtilsIdleResource

        .EXAMPLE
            Find-AzureUtilsIdleResource -Type StoppedVm, EmptyAppServicePlan |
                Sort-Object SubscriptionName, ResourceGroup

        .EXAMPLE
            Find-AzureUtilsIdleResource | Export-Csv .\idle.csv -NoTypeInformation

        .OUTPUTS
            AzureUtils.IdleResource
    #>

    [CmdletBinding(DefaultParameterSetName = 'Subscriptions')]
    [OutputType('AzureUtils.IdleResource')]
    param(
        [Parameter(ParameterSetName = 'Subscriptions')]
        [string[]] $SubscriptionId,

        [Parameter(ParameterSetName = 'ManagementGroup', Mandatory)]
        [string[]] $ManagementGroupId,

        [ValidateSet('StoppedVm', 'EmptyAppServicePlan', 'StoppedAksCluster')]
        [string[]] $Type = @('StoppedVm', 'EmptyAppServicePlan', 'StoppedAksCluster')
    )

    $null = Assert-AzureUtilsContext

    $resolved = Resolve-AzureUtilsScope -SubscriptionId $SubscriptionId -ManagementGroupId $ManagementGroupId
    if ($resolved.Type -eq 'Subscription' -and -not $resolved.HasSubscriptions) {
        Write-Warning 'No accessible subscriptions in scope.'
        return
    }

    $query   = New-AzureUtilsIdleResourceQuery -Type $Type
    $nameMap = $resolved.NameMap
    $scope   = $resolved.Scope
    Write-Verbose "Idle-resource query:`n$query"

    Invoke-AzureUtilsGraphQuery -Query $query @scope | ForEach-Object {
        $subId = [string]$_.subscriptionId
        [pscustomobject]@{
            PSTypeName       = 'AzureUtils.IdleResource'
            Name             = $_.name
            ResourceType     = $_.type
            ResourceGroup    = $_.resourceGroup
            Location         = $_.location
            SubscriptionName = if ($nameMap.ContainsKey($subId)) { $nameMap[$subId] } else { $subId }
            SubscriptionId   = $subId
            Reason           = $_.reason
            Tags             = ConvertTo-AzureUtilsHashtable -InputObject $_.tags
            ResourceId       = $_.id
        }
    }
}