Practical utility cmdlets for Azure administration, governance, inventory, troubleshooting and operational automation. AzureUtils does not replace the Az modules — it encapsulates real day-to-day pain (inventory at scale, cross-cutting diagnostics, governance checks, safe fixes) behind simple commands with normalized, automation-ready output.
- PowerShell 7+ only (
CompatiblePSEditions = Core). Get-*commands return objects;Export-*commands write files and print a console report.- Read commands use
Get-*; mutating commands (later) support-WhatIf/-Confirm. - All command output and messages are in en-US.
| Module | Required | Purpose |
|---|---|---|
Az.Accounts |
Yes | Resolves the Azure context (Connect-AzAccount). |
Az.ResourceGraph |
Yes | Powers the inventory query (multi-scope, auto-paginated). |
ImportExcel |
Yes | Used by Export-AzureUtilsTagInventory to write the .xlsx. |
All three modules above are declared as RequiredModules, so Install-Module pulls them in automatically.
Install-Module AzureUtils -Scope CurrentUser # also installs Az.Accounts, Az.ResourceGraph, ImportExcel
# Pre-release:
Install-Module AzureUtils -AllowPrerelease -Scope CurrentUserExports an Azure resource + tag inventory to an Excel workbook via Azure Resource Graph. The cmdlet does not emit objects — it writes the .xlsx and prints a console report. Scope can be narrowed by management group(s), subscription(s), resource group(s), and a case-insensitive -NameContains match.
| Parameter | Multiple? | Purpose |
|---|---|---|
-ManagementGroupId |
yes (string[]) |
One or more management groups. |
-SubscriptionId |
yes (string[]) |
One or more subscriptions (default: all enabled in the context). |
-ResourceGroupName |
yes (string[]) |
Filter by resource group name(s). |
-NameContains |
no (single term) | Keep resources whose name contains this text. |
-FilterTags |
yes (string[]) |
Export only these tag keys, in this order (default: all tags found). |
-IncludeSubscription |
— | Also add the subscriptions themselves as rows (their own tags). |
-IncludeResourceGroup |
— | Also add the resource groups themselves as rows (their own tags). |
-IncludeTagUnsupported |
— | Keep resources whose type doesn't support tags (omitted by default) and add a colored Tag Support column. |
-OutputPath |
— (required) | Destination .xlsx file. |
-TableStyle |
— | Excel table style (default neutral Light1). |
-Quiet |
— | Suppress per-resource log lines (show a progress bar instead). |
-ManagementGroupIdand-SubscriptionIdare mutually exclusive (separate parameter sets). Requires theImportExcelmodule.
Connect-AzAccount
# 1) Everything in the current context, all tags as columns
Export-AzureUtilsTagInventory -OutputPath '.\inventory.xlsx'
# 2) A management group, only two tags (columns in this order)
Export-AzureUtilsTagInventory -ManagementGroupId 'PLAT' `
-FilterTags 'costCenter', 'environment' `
-OutputPath 'C:\Temp\report.xlsx'
# 3) Several management groups at once
Export-AzureUtilsTagInventory -ManagementGroupId 'PLAT', 'SANDBOX' -OutputPath '.\all.xlsx'
# 4) Narrow scope + suppress the per-resource log (progress bar instead)
Export-AzureUtilsTagInventory -SubscriptionId $sub1, $sub2 `
-ResourceGroupName 'rg-prod' -NameContains 'sql' `
-OutputPath '.\sql-prod.xlsx' -Quiet
# 5) Also inventory the subscriptions and resource groups themselves (their tags)
Export-AzureUtilsTagInventory -ManagementGroupId 'PLAT' `
-IncludeSubscription -IncludeResourceGroup `
-OutputPath '.\full.xlsx'
# 6) Keep tag-unsupported resource types and flag them in a 'Tag Support' column
Export-AzureUtilsTagInventory -IncludeTagUnsupported -OutputPath '.\inventory.xlsx'By default only resource-level tags are inventoried.
-IncludeSubscriptionand-IncludeResourceGroupadd the subscriptions / resource groups as extra rows (fromresourcecontainers) so tags applied at those scopes are captured too.By default, resources whose resource type does not support tags (e.g.
networkWatchers, classic resources) are omitted — they would only add emptyTAG_rows. The console header reports how many were skipped.-IncludeTagUnsupportedkeeps them and adds aTag Supportcolumn (Supportedin green /Not supportedin red). Tag support is resolved from the ARM Resource Providers API (capabilities→SupportsTags), with a curated fallback if that call is unavailable.
Console report:
Azure Tag Inventory Export
---------------------------------------------------
Scope: PLAT [Management Group]
Number of Subscriptions: 22
Number of Resources: 1432
Starting export...
[INFO] 1 of 1432 collecting tags of /subscriptions/.../networkWatchers/NetworkWatcher_brazilsouth
[ERROR] <message shown for any resource that fails>
[INFO] Collect Finish
Report exported to C:\Temp\report.xlsx
Excel layout: fixed columns resourceId, Resource Name, Sub Name, Resource Group Name, Resource Type, Region, followed by one TAG_<name> column. Without -FilterTags, every tag key found is a column (sorted union); with -FilterTags, only the listed keys appear, in the given order (blank where a resource lacks that tag). The table style defaults to a neutral look — change it with -TableStyle (any ImportExcel style name).
Reads an inventory .xlsx (the one produced by Export-AzureUtilsTagInventory, optionally edited) and applies the TAG_<name> values back onto each resource identified by its resourceId. The operation is a merge (Update-AzTag -Operation Merge):
- tags present on the resource but absent from the file are kept (never removed);
- a
TAG_<name>cell left empty is ignored (never created or changed); - a manually added
TAG_<name>column (with a value) creates that tag.
This cmdlet changes Azure resources, so it supports -WhatIf and -Confirm. Rows are grouped by subscription (the context is switched per group). Requires Az.Resources.
| Parameter | Purpose |
|---|---|
-InputPath (required, pos. 0) |
Path to the .xlsx to read (alias -Path). |
-WorksheetName |
Worksheet to read (default TagInventory). |
-Quiet |
Suppress the per-resource log lines (errors still shown). |
# Preview the changes without touching anything
Set-AzureUtilsTagInventory -InputPath 'C:\Temp\report.xlsx' -WhatIf
# Apply (edit the TAG_* cells / add TAG_<new> columns first)
Set-AzureUtilsTagInventory 'C:\Temp\report.xlsx'Round-trip: Export-AzureUtilsTagInventory → edit the workbook (change tag values, add TAG_<new> columns) → Set-AzureUtilsTagInventory to apply.
Finds resources that appear orphaned (unused/unassociated) via Azure Resource Graph and explains each in a Reason column. Returns objects (also shown as a colored console table). Categories (all by default, narrow with -Type):
-Type value |
Orphan when… |
|---|---|
Disk |
managed disk diskState = Unattached |
NetworkInterface |
NIC with no VM and no private endpoint |
PublicIP |
public IP with no IP configuration and no NAT gateway |
NetworkSecurityGroup |
NSG not associated to any NIC or subnet |
RouteTable |
route table not associated to any subnet |
Find-AzureUtilsOrphanResource # current context, all categories
Find-AzureUtilsOrphanResource -ManagementGroupId 'PLAT' -Type Disk, PublicIP
Find-AzureUtilsOrphanResource | Export-Csv .\orphans.csv -NoTypeInformationHeuristics — review before deleting anything.
Lists resource groups whose resource count (from resources) is zero, across the chosen scope.
Find-AzureUtilsEmptyResourceGroup
Find-AzureUtilsEmptyResourceGroup -SubscriptionId $sub1, $sub2Surfaces resources exposed to the public internet via Azure Resource Graph, with an Exposure column explaining each finding. Returns objects (also a colored console table). Categories (all by default, narrow with -Type):
-Type value |
Flags |
|---|---|
PublicIp |
public IP addresses that are associated (in use) |
PublicNetworkAccess |
resources with properties.publicNetworkAccess = Enabled (managed disks excluded — Enabled by default and not a real exposure) |
StorageOpen |
storage with anonymous blob access or networkAcls.defaultAction = Allow |
NsgInternetInbound |
NSG inbound Allow rules sourced from the Internet (* / 0.0.0.0/0) |
Find-AzureUtilsPublicResource
Find-AzureUtilsPublicResource -ManagementGroupId 'PLAT' -Type StorageOpen, NsgInternetInbound
Find-AzureUtilsPublicResource | Export-Csv .\public-exposure.csv -NoTypeInformationHeuristics — a resource may appear more than once when several categories apply. Review before acting.
Surfaces resources that are allocated (and may still cost money) while doing no work, with a Reason column. This is the idle companion to Find-AzureUtilsOrphanResource (which finds unassociated resources). Categories (-Type):
-Type value |
Idle when… |
|---|---|
StoppedVm |
VM is not running: stopped (still billed for compute) or deallocated (disks / static public IPs may still cost) |
EmptyAppServicePlan |
fixed-tier App Service Plan (numberOfSites = 0, not Free/Shared/Dynamic) |
StoppedAksCluster |
AKS cluster power state is Stopped |
Find-AzureUtilsIdleResource
Find-AzureUtilsIdleResource -Type StoppedVm, EmptyAppServicePlanLists resources missing one or more required tag keys, with a MissingTags list and a Reason. A tag counts as present only when it exists and is non-empty. Closes the loop on the tag-inventory cmdlets: define what governance requires, then find who's out of compliance.
Test-AzureUtilsTagCompliance -RequiredTag costCenter, environment
Test-AzureUtilsTagCompliance -RequiredTag owner -ManagementGroupId 'PLAT' |
Export-Csv .\tag-violations.csv -NoTypeInformationSurfaces configuration-hardening gaps (not public exposure — that's Find-AzureUtilsPublicResource) with a Finding column. Categories (-Type):
-Type value |
Flags |
|---|---|
StorageInsecureTransfer |
storage allowing non-HTTPS traffic (supportsHttpsTrafficOnly = false) |
StorageWeakTls |
storage with minimum TLS 1.0 / 1.1 |
WebAppHttpsOnly |
web apps not enforcing HTTPS-only (httpsOnly = false) |
KeyVaultNoPurgeProtection |
key vaults with purge protection disabled |
Find-AzureUtilsInsecureConfig
Find-AzureUtilsInsecureConfig -Type StorageWeakTls, StorageInsecureTransferLists resources whose current Azure Policy compliance state is NonCompliant (from the policyresources table), naming the offending assignment/definition in a Reason column. One object per (resource, policy) pair.
Find-AzureUtilsPolicyNonCompliant
Find-AzureUtilsPolicyNonCompliant -ManagementGroupId 'PLAT' |
Group-Object PolicyAssignment | Sort-Object Count -DescendingFinds RBAC role assignments whose principal no longer exists in Entra ID (orphaned — the "Identity not found" state in the portal). It lists every assignment from authorizationresources and resolves each principal against Microsoft Graph (directoryObjects/getByIds via Invoke-AzRestMethod — no extra module); the ones that don't resolve are reported.
The Resource Graph
principalTypeis frozen at creation and does not change when a principal is deleted, so orphaned assignments can't be found from the query alone — hence the directory lookup. Requires a directory-read permission (e.g.Directory.Read.All); if Graph is unreachable the cmdlet throws instead of reporting every assignment as orphaned.
Find-AzureUtilsStaleRoleAssignment
Find-AzureUtilsStaleRoleAssignment | Export-Csv .\stale-rbac.csv -NoTypeInformationFinds resources sharing the same name and type (case-insensitive) — likely accidental duplicates — with the group size in a Count column. Heuristic: the same name/type across dev/prod is often intentional.
Find-AzureUtilsDuplicateResource
Find-AzureUtilsDuplicateResource -ManagementGroupId 'PLAT' | Sort-Object Name, ResourceGroupAll Find-* cmdlets take -SubscriptionId (default: all enabled) or -ManagementGroupId (mutually exclusive) and emit objects for the pipeline.
The object-emitting sibling of Export-AzureUtilsTagInventory: same Azure Resource Graph backend and scope filters, but instead of writing an .xlsx it streams objects to the pipeline (ready for Where-Object, Group-Object, Export-Csv, ConvertTo-Json, …).
| Parameter | Purpose |
|---|---|
-SubscriptionId / -ManagementGroupId |
Scope (mutually exclusive). |
-ResourceGroupName |
Restrict to these resource groups. |
-NameContains |
Keep resources whose name contains this text. |
-ResourceType |
Restrict to these ARM resource types. |
-First |
Stop after N resources (0 = no limit). |
Get-AzureUtilsResourceInventory | Group-Object ResourceType | Sort-Object Count -Descending
Get-AzureUtilsResourceInventory -ResourceType 'microsoft.compute/virtualmachines' `
-NameContains 'prod' | Export-Csv .\vms.csv -NoTypeInformationInherits resource-group tags down onto the resources inside each group, via Update-AzTag -Operation Merge. By default only tag keys the resource is missing are added (existing values untouched); -Overwrite also replaces values that differ, and -Tag restricts which keys are inherited. Groups by subscription and supports -WhatIf / -Confirm. Requires Az.Resources.
| Parameter | Purpose |
|---|---|
-SubscriptionId / -ManagementGroupId |
Scope (mutually exclusive). |
-Tag |
Only inherit these tag keys (default: all group tags). |
-Overwrite |
Also overwrite differing values (default: add missing keys only). |
-Quiet |
Suppress per-resource log lines. |
# Preview which resources would inherit which group tags
Set-AzureUtilsTagFromResourceGroup -WhatIf
# Inherit only two keys from each resource group onto its resources
Set-AzureUtilsTagFromResourceGroup -Tag costCenter, environment -SubscriptionId $subVersioning is driven by the manifest (AzureUtils/AzureUtils.psd1):
- Work happens on
develop. BumpModuleVersion(andPrivateData.PSData.Prereleasefor pre-releases) there. - Merging
develop → main(which changesAzureUtils/AzureUtils.psd1) runsAuto-tag release on main, which:- resolves the version from the manifest and creates the git tag — stable
v{ModuleVersion}, pre-releasev{ModuleVersion}-{Prerelease}— only if it does not already exist; - triggers the publish workflow via
gh workflow runusing the built-ingithub.token.
- resolves the version from the manifest and creates the git tag — stable
Publish PowerShell Gallerythen runs aguard → validate → publishpipeline: it publishes only when the tag commit is reachable frommainand the tag matches the manifest version. The publish step is idempotent (an already-published version is treated as success).
No personal access token is required: the tag is pushed with the default
github.tokenand the publish workflow is started explicitly viagh workflow run(it also acceptsworkflow_dispatchwith arelease_taginput for manual re-runs).
| Secret | Used by | Notes |
|---|---|---|
PSGALLERY_API_KEY |
Publish PowerShell Gallery |
PowerShell Gallery API key. |
The auto-tag workflow needs permissions: contents: write and actions: write (already declared in the workflow) so it can push the tag and dispatch the publish workflow with the built-in token.
Install-Module Pester -MinimumVersion 5.5.0 -Scope CurrentUser
Invoke-Pester ./AzureUtils/Tests