Public/Export-AzureUtilsTagInventory.ps1

function Export-AzureUtilsTagInventory {
    <#
        .SYNOPSIS
            Exports an Azure resource + tag inventory to an Excel workbook.

        .DESCRIPTION
            Reads every Azure resource in scope via Azure Resource Graph and writes
            an .xlsx workbook with the fixed columns resourceId, Resource Name,
            Sub Name, Resource Group Name, Resource Type, Region, followed by one
            'TAG_<name>' column per tag. By default every tag key found is exported;
            -FilterTags restricts the export to the listed keys (in the given order).

            Scope can be narrowed by management group(s), subscription(s), resource
            group(s) and a case-insensitive -NameContains match. Progress is shown
            on the console as a leveled report; the cmdlet does not emit objects to
            the pipeline.

            Requires Az.ResourceGraph and the ImportExcel module. Messages are en-US.

        .PARAMETER ManagementGroupId
            One or more management groups to inventory.

        .PARAMETER SubscriptionId
            One or more subscription IDs. Default: every enabled subscription in the
            current context.

        .PARAMETER ResourceGroupName
            Filter by one or more resource group names (case-insensitive).

        .PARAMETER NameContains
            Keep only resources whose name contains this text (case-insensitive).

        .PARAMETER FilterTags
            Export only these tag keys (one 'TAG_<name>' column each, in this order).
            When omitted, every tag key found is exported.

        .PARAMETER OutputPath
            Destination .xlsx file path.

        .PARAMETER TableStyle
            Excel table style name (ImportExcel). Defaults to a neutral 'Light1'.

        .PARAMETER Quiet
            Suppress the per-resource '[INFO] N of TOTAL ...' lines. The header,
            summary, error lines and final status are still shown.

        .EXAMPLE
            Export-AzureUtilsTagInventory -OutputPath '.\inventory.xlsx'

            Inventories every enabled subscription in the current context and writes
            an Excel workbook with one column per tag found.

        .EXAMPLE
            Export-AzureUtilsTagInventory -ManagementGroupId 'PLAT' `
                -FilterTags 'costCenter', 'environment' `
                -OutputPath 'C:\Temp\report.xlsx'

            Inventories a management group, exporting only the 'costCenter' and
            'environment' tags (as columns TAG_costCenter, TAG_environment, in that
            order).

        .EXAMPLE
            Export-AzureUtilsTagInventory -SubscriptionId $sub1, $sub2 `
                -ResourceGroupName 'rg-prod' -NameContains 'sql' `
                -OutputPath '.\sql-prod.xlsx' -Quiet

            Limits the scope to two subscriptions, the 'rg-prod' resource group and
            resources whose name contains 'sql', and suppresses the per-resource log
            (a progress bar is shown instead).

        .LINK
            https://github.com/hendersonandrade/powershell-module-azureUtils
    #>

    [CmdletBinding(DefaultParameterSetName = 'Subscriptions')]
    param(
        [Parameter(ParameterSetName = 'ManagementGroup', Mandatory)]
        [string[]] $ManagementGroupId,

        [Parameter(ParameterSetName = 'Subscriptions')]
        [string[]] $SubscriptionId,

        [string[]] $ResourceGroupName,

        [string] $NameContains,

        [string[]] $FilterTags,

        [Parameter(Mandatory)]
        [string] $OutputPath,

        [string] $TableStyle = 'Light1',

        [switch] $Quiet
    )

    $null = Assert-AzureUtilsContext

    if (-not (Get-Command -Name 'Export-Excel' -ErrorAction SilentlyContinue)) {
        if (Get-Module -ListAvailable -Name 'ImportExcel' -ErrorAction SilentlyContinue) {
            Import-Module ImportExcel -ErrorAction Stop
        }
        else {
            throw [System.Management.Automation.RuntimeException]::new(
                "Export-AzureUtilsTagInventory requires the 'ImportExcel' module. Install it with: Install-Module ImportExcel -Scope CurrentUser"
            )
        }
    }

    # Resolve scope, the subscriptionId -> name lookup, and a friendly scope label.
    $scope   = @{}
    $nameMap = @{}
    if ($PSCmdlet.ParameterSetName -eq 'ManagementGroup') {
        $scope['ManagementGroup'] = $ManagementGroupId
        foreach ($s in (Resolve-AzureUtilsSubscription)) { $nameMap[$s.Id] = $s.Name }  # best-effort names
        $scopeLabel = $ManagementGroupId -join ', '
        $scopeType  = 'Management Group'
    }
    else {
        $subs = Resolve-AzureUtilsSubscription -SubscriptionId $SubscriptionId
        if ($subs.Count -eq 0) {
            Write-AzureUtilsStatus -Level WARN -Message 'No accessible subscriptions in scope; nothing to export.'
            return
        }
        foreach ($s in $subs) { $nameMap[$s.Id] = $s.Name }
        $scope['Subscription'] = @($subs.Id)
        $scopeLabel = if ($SubscriptionId) { ($subs.Name -join ', ') } else { 'All enabled subscriptions' }
        $scopeType  = 'Subscription'
    }

    $baseQuery = New-AzureUtilsResourceQuery -ResourceGroupName $ResourceGroupName -NameContains $NameContains
    Write-Verbose "Base query:`n$baseQuery"

    # ---- Collect resources (tags arrive with the query); count from the data so
    # the header totals are exact. A progress bar gives live feedback while
    # Resource Graph is paginated. ----
    $dataQuery = "$baseQuery`n| project id, name, type, resourceGroup, location, subscriptionId, tags"
    $records   = [System.Collections.Generic.List[object]]::new()
    $errors    = [System.Collections.Generic.List[string]]::new()
    $collected = 0
    $activity  = 'Collecting Azure resources from Azure Resource Graph'

    Invoke-AzureUtilsGraphQuery -Query $dataQuery @scope | ForEach-Object {
        try {
            $records.Add(($_ | ConvertTo-AzureUtilsTagRecord -SubscriptionName $nameMap))
            $collected++
            if ($collected % 50 -eq 0) {
                Write-Progress -Id 1 -Activity $activity -Status "$collected resources collected" -PercentComplete -1
            }
        }
        catch {
            $errors.Add($_.Exception.Message)
        }
    }
    Write-Progress -Id 1 -Activity $activity -Completed

    $total = $records.Count
    $subCount = if ($PSCmdlet.ParameterSetName -eq 'ManagementGroup') {
        ($records | ForEach-Object { $_.SubscriptionId } | Where-Object { $_ } | Sort-Object -Unique).Count
    }
    else {
        $subs.Count
    }

    # ---- Report header (exact totals) ----
    Write-Host ''
    Write-Host 'Azure Tag Inventory Export' -ForegroundColor Cyan
    Write-Host ('-' * 51) -ForegroundColor DarkGray
    Write-Host (" Scope: {0} [{1}]" -f $scopeLabel, $scopeType)
    Write-Host (" Number of Subscriptions: {0}" -f $subCount)
    Write-Host (" Number of Resources: {0}" -f $total)
    Write-Host ''
    Write-Host 'Starting export...'

    # ---- Per-resource report (suppressed by -Quiet; errors always shown) ----
    if (-not $Quiet) {
        $n = 0
        foreach ($rec in $records) {
            $n++
            Write-AzureUtilsStatus -Level INFO -Message ("{0} of {1} collecting tags of {2}" -f $n, $total, $rec.ResourceId)
        }
    }
    foreach ($message in $errors) {
        Write-AzureUtilsStatus -Level ERROR -Message $message
    }

    Write-AzureUtilsStatus -Level INFO -Message 'Collect Finish'

    if ($records.Count -eq 0) {
        Write-AzureUtilsStatus -Level WARN -Message 'No resources found in scope; nothing to export.'
        return
    }

    # ---- Write the workbook ----
    $excelRows = @(Get-AzureUtilsTagExcelRow -Record $records -TagKey $FilterTags)
    $excelRows | Export-Excel -Path $OutputPath -WorksheetName 'TagInventory' `
        -TableName 'TagInventory' -TableStyle $TableStyle -AutoSize -FreezeTopRow -BoldTopRow

    Write-Host ("Report exported to {0}" -f $OutputPath)
}