Home / Azure

AzureCheat sheetBeginner

Azure Resource Graph queries for daily operations, a cheat sheet

Copy-paste Resource Graph queries for inventory, orphaned resources, risky NSG rules, unprotected VMs, resource health and who changed what, across every subscription at once.

Deepak Bhardwaj7 min read

Applies toAzure Resource Graph, Azure CLI with the resource-graph extension, Az.ResourceGraph PowerShell module

Azure Resource Graph (ARG) answers "where is it, how many are there, and who changed it" across every subscription you can read, in seconds. It uses KQL, like Log Analytics, but it queries resource configuration, not logs or metrics. Keep these queries in a file and adapt them.

1. Running queries

Portal: Resource Graph Explorer. CLI and PowerShell:

az extension add --name resource-graph
az graph query -q "resources | summarize count() by type | order by count_ desc" --first 1000 -o table

# Scope to a management group or a list of subscriptions
az graph query -q "resources | count" --management-groups mg-prod
az graph query -q "resources | count" --subscriptions <sub-id-1> <sub-id-2>
Install-Module Az.ResourceGraph -Scope CurrentUser
Search-AzGraph -Query "resources | summarize count() by type" -First 1000 -UseTenantScope

Tip

The CLI returns 100 rows by default. Use --first (up to 1000) and --skip-token from the previous result to page further. In PowerShell, use -First and -SkipToken. Without -UseTenantScope or -ManagementGroup, Search-AzGraph only searches the subscriptions in your current context.

Tables you will use most

Table Contains
resources Almost every ARM resource and its properties
resourcecontainers Subscriptions, resource groups, management groups
resourcechanges Create/update/delete history for resources (recent window only)
healthresources Resource Health availability status
policyresources Azure Policy compliance states
advisorresources Azure Advisor recommendations
recoveryservicesresources Azure Backup protected items and jobs

2. Inventory

VMs with size, OS and power state

resources
| where type =~ "microsoft.compute/virtualmachines"
| extend size = tostring(properties.hardwareProfile.vmSize),
         os = tostring(properties.storageProfile.osDisk.osType),
         power = tostring(properties.extended.instanceView.powerState.code)
| project name, resourceGroup, subscriptionId, location, size, os, power
| order by power asc, name asc

power returns values such as PowerState/running and PowerState/deallocated. A VM that is PowerState/stopped (shut down from inside the guest) is still billed for compute.

Subscription names next to resource counts

resources
| summarize resourceCount = count() by subscriptionId
| join kind=leftouter (
    resourcecontainers
    | where type == "microsoft.resources/subscriptions"
    | project subscriptionId, subscriptionName = name
) on subscriptionId
| project subscriptionName, subscriptionId, resourceCount
| order by resourceCount desc

Resources missing an owner tag

resources
| where isempty(tostring(tags["owner"]))
| summarize missing = count() by type, resourceGroup
| order by missing desc

Warning

Tag keys are matched as written. If some teams use Owner and others owner, check both or normalise them with Azure Policy before you trust the report.

3. Orphaned resources that still cost money

Unattached managed disks

resources
| where type =~ "microsoft.compute/disks"
| where properties.diskState == "Unattached"
| project name, resourceGroup, sku = tostring(sku.name),
          sizeGB = toint(properties.diskSizeGB), created = todatetime(properties.timeCreated)
| order by sizeGB desc

Public IPs that are not associated with anything

resources
| where type =~ "microsoft.network/publicipaddresses"
| where isnull(properties.ipConfiguration) and isnull(properties.natGateway)
| project name, resourceGroup, ip = tostring(properties.ipAddress), sku = tostring(sku.name)

NICs with no VM and no private endpoint

resources
| where type =~ "microsoft.network/networkinterfaces"
| where isnull(properties.virtualMachine) and isnull(properties.privateEndpoint)
| project name, resourceGroup, location

Caution

"Unattached" is not the same as "safe to delete". A disk may be a deliberate copy kept before a repair, or the OS disk of a VM someone plans to rebuild. Confirm with the owner, snapshot if in doubt, and only then delete.

4. Security and resilience checks

NSG rules that allow SSH or RDP from the internet

resources
| where type =~ "microsoft.network/networksecuritygroups"
| mv-expand rule = properties.securityRules
| where rule.properties.direction == "Inbound" and rule.properties.access == "Allow"
| where tostring(rule.properties.sourceAddressPrefix) in ("*", "Internet", "0.0.0.0/0")
| extend port = tostring(rule.properties.destinationPortRange)
| where port in ("*", "22", "3389")
| project nsg = name, resourceGroup, rule = tostring(rule.name), port,
          priority = toint(rule.properties.priority)

This only checks the single-value destinationPortRange field. Rules that list several ports use destinationPortRanges (an array); add an mv-expand on that if your team writes rules that way.

Storage accounts with weak settings

resources
| where type =~ "microsoft.storage/storageaccounts"
| project name, resourceGroup,
          publicNetwork = tostring(properties.publicNetworkAccess),
          blobPublicAccess = tostring(properties.allowBlobPublicAccess),
          minTls = tostring(properties.minimumTlsVersion),
          httpsOnly = tostring(properties.supportsHttpsTrafficOnly)
| where blobPublicAccess == "true" or minTls != "TLS1_2" or httpsOnly == "false"

VMs that are not protected by Azure Backup

resources
| where type =~ "microsoft.compute/virtualmachines"
| extend vmId = tolower(id)
| join kind=leftouter (
    recoveryservicesresources
    | where type =~ "microsoft.recoveryservices/vaults/backupfabrics/protectioncontainers/protecteditems"
    | where properties.backupManagementType == "AzureIaasVM"
    | project protectedVmId = tolower(tostring(properties.sourceResourceId))
) on $left.vmId == $right.protectedVmId
| where isempty(protectedVmId)
| project name, resourceGroup, subscriptionId, location

Lowercase both sides of the join. Resource IDs are not always returned in the same case, and a mismatch makes protected VMs look unprotected.

5. Health, compliance and Advisor

Resources that Resource Health does not report as Available

healthresources
| where type =~ "microsoft.resourcehealth/availabilitystatuses"
| extend state = tostring(properties.availabilityState)
| where state != "Available"
| project id, state, reason = tostring(properties.reasonType)

Non-compliant resources by policy

policyresources
| where type =~ "microsoft.policyinsights/policystates"
| where properties.complianceState == "NonCompliant"
| summarize nonCompliant = count() by policy = tostring(properties.policyDefinitionName)
| order by nonCompliant desc

Cost recommendations from Advisor

advisorresources
| where type == "microsoft.advisor/recommendations"
| where properties.category == "Cost"
| project resource = tostring(properties.impactedValue),
          problem = tostring(properties.shortDescription.problem),
          solution = tostring(properties.shortDescription.solution)

6. Who changed what

During an incident, the first question is usually "what changed?".

resourcechanges
| extend changeTime = todatetime(properties.changeAttributes.timestamp),
         target = tostring(properties.targetResourceId),
         changeType = tostring(properties.changeType),
         changedBy = tostring(properties.changeAttributes.changedBy),
         client = tostring(properties.changeAttributes.clientType)
| where changeTime > ago(1d)
| where target has "rg-prod-web"
| project changeTime, changeType, target, changedBy, client, properties.changes
| order by changeTime desc

changedBy holds a UPN or an application ID. System means a platform change not tied to a user action, and Unspecified means no identity information was recorded. Change history only covers a recent window (about 14 days at the time of writing), so for anything older use the Activity Log in Log Analytics.

7. When results look wrong

Symptom Likely cause Fix
Only 100 rows CLI default page size Add --first 1000 and page with --skip-token
A subscription is missing No read access, or PowerShell limited to the current context Check RBAC; use -UseTenantScope or --management-groups
Property is always empty Wrong path or case in the property name Run resources | where name == "x" | project properties and copy the exact path
Resource changed a minute ago doesn't show yet ARG is updated shortly after ARM, not instantly Wait a few minutes or query ARM directly with az resource show
Query throttled Too many queries in a short window from scripts Batch queries, add retries with back-off, and cache results

Prevent it next time

  • Save the queries you run every week as shared queries in Resource Graph Explorer so the whole team uses the same version.
  • Pin the orphaned-resource and NSG queries to a dashboard or an Azure Workbook and review them weekly.
  • Turn the checks you keep finding into Azure Policy (deny or audit) so the problem stops being created, instead of being found later.