Azure Resource Graph: 10 queries for Azure inventory and monitoring

Inventory VMs, unattached disks, public IPs, tags, and Arc servers with ten read-only queries, interpretation guidance, and paginated collection.

01

See your Azure estate without changing resources

How many VMs do we have, which disks need a decision, and which Arc servers need a connectivity review? Azure Resource Graph (ARG) lets you ask these questions across subscriptions. Here are ten read-only queries, with interpretation and checks before taking action.

ARG uses a subset of Kusto Query Language (KQL), but this is not a Defender Advanced Hunting guide. It explores Azure resource configuration rather than endpoint security telemetry. It is also distinct from Microsoft Graph for Microsoft 365 identities and licenses.

02

Prerequisites: explicit scope and read-only access

Start in Azure Portal > Resource Graph Explorer: select subscriptions, paste a query, and run it. The scripts below use PowerShell with Az.Accounts and Az.ResourceGraph installed through your approved process. Record module versions; do not update them during collection.

The identity needs Azure RBAC read access to the target resources; Reader at the necessary scope suits this workflow. Contributor or Owner is unnecessary. Results remain permission-limited: a successful collection can silently be partial. Have subscription owners confirm the expected list.

ARG is a free service subject to quotas. Azure resources and additional scheduling, storage, or alerting services can incur separate charges. These inventory queries do not require purchasing Defender or Microsoft Graph licenses.

Connect and run a query
# Setup only: install approved Az.Accounts / Az.ResourceGraph versions beforehand
Import-Module Az.Accounts
Import-Module Az.ResourceGraph
Get-Module Az.Accounts,Az.ResourceGraph | Select-Object Name,Version
$tenantId = '11111111-1111-1111-1111-111111111111' # fictional
$subscriptionIds = @('22222222-2222-2222-2222-222222222222') # fictional
Connect-AzAccount -Tenant $tenantId -ErrorAction Stop
Get-AzContext | Select-Object Account,Tenant,Subscription
Get-AzSubscription -TenantId $tenantId -ErrorAction Stop |
  Select-Object Id,Name,State
# Paste ONE numbered query into this literal here-string:
$query = @'
Resources
| project id, name, type, subscriptionId
| order by id asc
'@
$page = Search-AzGraph -Query $query -Subscription $subscriptionIds -First 1000 -ErrorAction Stop
$page.Data
03

Interpret results within their limits

Resources contains indexed properties, not an instantaneous transactional view. After a change, confirm the current value with the resource provider. Property-path casing matters; missing fields may also reflect the indexed API version. Inspect the schema and a known resource before generalizing a filter.

All ten queries use the same RBAC read requirement, without needing VM data access, storage keys, or secrets. Replace only fictional examples and tag conventions. Never put secrets in tags or exports.

04

1. Count resources by subscription and type

Compare this distribution with expected scope. An aggregate row is not a resource, and this count does not enumerate all service data such as files, messages, or application objects.

ARG query — read-only
Resources
| summarize resourceCount=count() by subscriptionId, type
| order by subscriptionId asc, type asc
05

2. Inventory Azure virtual machines

Use id to reconcile with the CMDB, rather than name alone. ProvisioningState describes provisioning; Succeeded does not prove Windows, Linux, or the application responds. This selection excludes VM Scale Set instances.

ARG query — read-only
Resources
| where type =~ 'microsoft.compute/virtualmachines'
| project id, name, subscriptionId, resourceGroup, location,
    vmSize=tostring(properties.hardwareProfile.vmSize),
    osType=tostring(properties.storageProfile.osDisk.osType),
    provisioningState=tostring(properties.provisioningState)
| order by id asc
06

3. Find unattached managed disks

Unattached does not mean unnecessary: the disk may support recovery, migration, or standby. Confirm current state with Compute, ownership, backup, and dependencies before deciding. Disk size alone cannot establish exact savings.

ARG query — read-only
Resources
| where type =~ 'microsoft.compute/disks'
| where tostring(properties.diskState) =~ 'Unattached'
| project id, name, subscriptionId, resourceGroup, location,
    diskGB=toint(properties.diskSizeGB), sku=tostring(sku.name),
    managedBy, timeCreated=todatetime(properties.timeCreated)
| order by id asc
07

4. Inspect public IP resources

Include IP resources without an assigned address. A public IP does not prove a port is reachable: examine associations, firewalls, NSGs, routes, and service configuration. Empty association fields alone do not establish that it is orphaned.

ARG query — read-only
Resources
| where type =~ 'microsoft.network/publicipaddresses'
| project id, name, subscriptionId, resourceGroup, location,
    ip=tostring(properties.ipAddress), sku=tostring(sku.name),
    allocation=tostring(properties.publicIPAllocationMethod),
    ipConfigurationId=tostring(properties.ipConfiguration.id),
    natGatewayId=tostring(properties.natGateway.id)
| order by id asc
08

5. Find missing governance tags

Owner and Environment are fictional convention keys. Adapt casing and allowed values. Some resource types do not support tags; check applicability before declaring noncompliance. Resource-group tags are not automatically copied to contained resources.

ARG query — read-only
Resources
| extend owner=tostring(tags['Owner']),
    environment=tostring(tags['Environment'])
| where isempty(owner) or isempty(environment)
| project id, name, type, subscriptionId, resourceGroup, owner, environment
| order by id asc
09

6. Add subscription display names

The left join retains resources even when no subscription display name is found. Keep subscriptionId as the stable identifier; labels can change. Incorrect normalization or multiple rows per key can multiply results.

ARG query — read-only
Resources
| join kind=leftouter (
    ResourceContainers
    | where type =~ 'microsoft.resources/subscriptions'
    | project subscriptionId, subscriptionName=name
  ) on subscriptionId
| project id, name, type, subscriptionId, subscriptionName, resourceGroup, location
| order by id asc
10

7. Inventory Azure Arc-enabled servers

These rows represent Arc-registered machines, not every on-premises server. Missing fields may reflect schema or indexing. Compare agent versions with Arc documentation; do not compare version numbers as alphabetic strings.

ARG query — read-only
Resources
| where type =~ 'microsoft.hybridcompute/machines'
| project id, name, subscriptionId, resourceGroup, location,
    status=tostring(properties.status),
    agentVersion=tostring(properties.agentVersion),
    osName=tostring(properties.osName),
    osVersion=tostring(properties.osVersion)
| order by id asc
11

8. Prioritize Arc statuses for investigation

A status other than Connected triggers review: planned maintenance, retired machine, networking, proxy, or agent. It does not by itself prove an OS outage. Validate with the owner and Arc connectivity diagnostics without deleting registration.

ARG query — read-only
Resources
| where type =~ 'microsoft.hybridcompute/machines'
| extend arcStatus=tostring(properties.status)
| where isempty(arcStatus) or arcStatus !~ 'Connected'
| project id, name, subscriptionId, resourceGroup, arcStatus
| order by id asc
12

9. List storage accounts and declared controls

These properties guide review, not an exposure verdict. Distinguish public network access, anonymous blob access, authentication, and private endpoints. Resolve empty values against the resource provider rather than assuming a control is enabled or disabled.

ARG query — read-only
Resources
| where type =~ 'microsoft.storage/storageaccounts'
| project id, name, subscriptionId, resourceGroup, location,
    publicNetworkAccess=tostring(properties.publicNetworkAccess),
    allowBlobPublicAccess=tostring(properties.allowBlobPublicAccess),
    supportsHttpsTrafficOnly=tostring(properties.supportsHttpsTrafficOnly),
    minimumTlsVersion=tostring(properties.minimumTlsVersion)
| order by id asc
13

10. Find provisioning states that need review

This deliberately narrow filter does not cover every resource type or status model. Next inspect Activity Log and the relevant provider operation. No results do not establish availability, compliance, or application health.

ARG query — read-only
Resources
| extend provisioningState=tostring(properties.provisioningState)
| where provisioningState in~ ('Failed', 'Canceled')
| project id, name, type, subscriptionId, resourceGroup, provisioningState
| order by id asc
14

Pagination: 1,000 rows are not a complete inventory

A response returns at most 1,000 rows. For lists, retain id and a stable order, avoid take, limit, or sample, and follow SkipToken. Aggregation such as query 1 produces groups rather than resource rows; do not blindly apply a resource-export routine to it.

The example exports common columns only; add query-specific fields to Select-Object. No token is not universal proof of completeness: some query shapes prevent paging. Also check counts and truncation indicators available in your client.

Paged collection — cloud read, local CSV write
# Use a row-level query from 2–10, retaining id and order by id.
# Same query and subscription list on every page; no take/limit/sample.
$rows = [System.Collections.Generic.List[object]]::new()
$token = $null
do {
  $parameters = @{
    Query=$query; Subscription=$subscriptionIds; First=1000
    ErrorAction='Stop'
  }
  if ($token) { $parameters.SkipToken = $token }
  $page = Search-AzGraph @parameters
  foreach ($row in $page.Data) { $rows.Add($row) }
  $token = $page.SkipToken
} while ($token)
# Local output only; choose an approved restricted directory.
$rows | Select-Object id,name,type,subscriptionId,resourceGroup,location |
  Export-Csv -LiteralPath './azure-inventory.csv' -NoTypeInformation -Encoding utf8
15

A useful follow-up report without automatic remediation

Record full resource ID, query, scope, collection time, finding, and owner. Priority depends on business risk, not row count. Never turn an export directly into a Remove-AzResource loop.

Fictional findingValidationProposed action
disk-recovery-01 unattachedOwner, recovery plan, current Compute stateRetain pending purpose and backup validation
srv-arc-demo not ConnectedMaintenance, agent, proxy, connectivityInvestigate; do not delete Arc registration
Owner missing on vm-demo-01Tag support and responsible teamPrepare an approved pilot tag
16

Move to monitoring, then controlled changes

  • Pilot one known subscription: compare selected IDs against the portal and provider, then validate totals before broadening scope.
  • For scheduled collection, use a managed identity where available with limited read access. Protect exports, define retention, and monitor collection failures too.
  • ARG is not an availability or metrics engine. Complement it with Azure Monitor, Activity Log, and relevant diagnostics; a saved query alone sends no alerts. Validate availability status and cost for any chosen alert integration.
  • Before a change: fresh provider read, owner, dependencies, backup, ticket, window, and pilot. Record prior configuration; define verification and rollback before writing.
  • For a pilot tag, restore only the changed key while preserving concurrent changes. Disk deletion has no simple undo: tested restoration is essential. This guide provides no deletion.
  • For 403, examine identity and scope without automatically broadening rights. For throttling, honor reported delays and quotas; space requests. For missing fields, check schema and casing before concluding.
17

Turn inventory into governance

These queries provide a starting point for CMDB reconciliation, ownership, and cost or configuration reviews. They replace neither a complete assessment nor end-to-end monitoring. Azure infrastructure consulting can help define scope, make collection dependable, and turn findings into measured changes.