Home / Azure

AzureCheat sheetBeginner

Who changed or deleted it? Finding the caller in the Azure Activity Log, a cheat sheet

CLI, PowerShell, KQL and Resource Graph queries to find who changed, restarted or deleted an Azure resource, when, from where and what changed.

Deepak Bhardwaj6 min read

Applies toAzure Activity Log, Azure Resource Graph resourcechanges, Log Analytics AzureActivity table, Azure CLI 2.x, Az PowerShell

"The NSG rule is gone." "The VM was resized overnight." "Who deleted the storage account?" Every control-plane write, delete and action in Azure is recorded in the Activity Log with the caller, time, IP address and result. This cheat sheet has the commands and queries to find the entry quickly and turn it into an answer.

Which source to use

Source What it gives you How far back
Activity Log (portal, az monitor activity-log) Caller, operation, status, IP, correlation ID 90 days
Resource Graph resourcechanges Property-level before/after values, plus changedBy and client type 14 days
AzureActivity table in Log Analytics Same as the Activity Log, queryable with KQL across subscriptions Your workspace retention

Tip

The Activity Log covers the control plane only: ARM operations such as create, update, delete, start and listKeys. It doesn't show who read a blob or a secret, or who ran SQL inside a database. Those actions are in the resource's own diagnostic logs (for example StorageBlobLogs or AzureDiagnostics), if you enabled them.

Azure CLI

# Everything in a resource group in the last 24 hours (default window is 6h)
az monitor activity-log list -g rg-app-prod --offset 24h \
  --query "[].{time:eventTimestamp, caller:caller, op:operationName.value, status:status.value, resource:resourceId}" \
  -o table

# One resource
RID=$(az network nsg show -g rg-net -n nsg-snet-app --query id -o tsv)
az monitor activity-log list --resource-id $RID --offset 7d \
  --query "[].{time:eventTimestamp, caller:caller, op:operationName.value, status:status.value}" -o table

# Explicit time window around the incident
az monitor activity-log list -g rg-app-prod \
  --start-time 2026-10-25T22:00:00Z --end-time 2026-10-26T02:00:00Z -o table

# Only deletes that succeeded
az monitor activity-log list -g rg-app-prod --offset 7d \
  --query "[?contains(operationName.value, '/delete') && status.value=='Succeeded'].{time:eventTimestamp, caller:caller, resource:resourceId}" \
  -o table

# What a given person or service principal did
az monitor activity-log list --caller ops.admin@contoso.com --offset 3d -o table

# All events for one operation (Started, Accepted, Succeeded/Failed share an ID)
az monitor activity-log list --correlation-id <correlation-id> -o json

For a resource group that no longer exists, search at subscription level and filter on the operation:

az monitor activity-log list --offset 30d --max-events 5000 \
  --query "[?operationName.value=='Microsoft.Resources/subscriptions/resourceGroups/delete'].{time:eventTimestamp, caller:caller, status:status.value, rg:resourceGroupName}" \
  -o table

Azure PowerShell

Get-AzActivityLog -ResourceGroupName rg-app-prod -StartTime (Get-Date).AddDays(-1) |
  Select-Object EventTimestamp, Caller, @{n='Operation';e={$_.OperationName.Value}},
                @{n='Status';e={$_.Status.Value}}, ResourceId |
  Sort-Object EventTimestamp -Descending | Format-Table -AutoSize

Get-AzActivityLog -ResourceId $rid -StartTime (Get-Date).AddDays(-7) |
  Where-Object { $_.OperationName.Value -like '*/delete' }

Get-AzActivityLog -Caller 'ops.admin@contoso.com' -StartTime (Get-Date).AddDays(-3)

Who is this caller?

Caller looks like It is Resolve with
name@domain.com A user Check whether a PIM activation was active at that time
A GUID A service principal or managed identity az ad sp show --id <guid> --query "{name:displayName, appId:appId, type:servicePrincipalType}"
Empty, or a platform name A platform or automatic action (autoscale, policy remediation, maintenance) Read the event's claims and properties, and check Azure Policy remediation tasks and autoscale history
# Where did the call come from? IP and the client app
az monitor activity-log list --correlation-id <id> \
  --query "[0].{ip:claims.ipaddr, appid:claims.appid, upn:claims.\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn\", http:httpRequest}"

The httpRequest.clientIpAddress and claims.appid fields tell you whether the change came from the portal, a pipeline agent or a script on someone's laptop. The claim keys vary by event, so dump the full JSON when a field is empty.

What exactly changed: Resource Graph

The Activity Log tells you a write happened. resourcechanges tells you which property changed. Install the extension with az extension add --name resource-graph.

// All changes to one resource in the last 7 days, with who and what
resourcechanges
| extend changeTime = todatetime(properties.changeAttributes.timestamp),
         targetResourceId = tostring(properties.targetResourceId),
         changeType = tostring(properties.changeType),
         changedBy = tostring(properties.changeAttributes.changedBy),
         clientType = tostring(properties.changeAttributes.clientType)
| where targetResourceId =~ "/subscriptions/<sub>/resourceGroups/rg-net/providers/Microsoft.Network/networkSecurityGroups/nsg-snet-app"
| where changeTime > ago(7d)
| project changeTime, changeType, changedBy, clientType, properties.changes
| order by changeTime desc
// Everything deleted in a resource group
resourcechanges
| where resourceGroup =~ "rg-app-prod"
| extend changeTime = todatetime(properties.changeAttributes.timestamp),
         changeType = tostring(properties.changeType),
         changedBy = tostring(properties.changeAttributes.changedBy)
| where changeType == "Delete"
| project changeTime, changedBy, targetResourceId = tostring(properties.targetResourceId)
| order by changeTime desc
// VM size changes
resourcechanges
| extend vmSize = properties.changes["properties.hardwareProfile.vmSize"],
         changeTime = todatetime(properties.changeAttributes.timestamp)
| where isnotempty(vmSize)
| project changeTime, targetResourceId = tostring(properties.targetResourceId),
          changedBy = tostring(properties.changeAttributes.changedBy),
          previousSize = vmSize.previousValue, newSize = vmSize.newValue

Run these with az graph query -q "<query>" --first 100, or in Resource Graph Explorer. changedBy can be System (a platform change) or Unspecified (no identity information was available). In those cases, fall back to the Activity Log and match by correlation ID and time.

KQL on the AzureActivity table

Use this when the Activity Log is exported to Log Analytics and you need more than 90 days, or several subscriptions at once.

// Successful deletes in the last 7 days
AzureActivity
| where TimeGenerated > ago(7d)
| where OperationNameValue endswith "/DELETE" and ActivityStatusValue == "Success"
| project TimeGenerated, Caller, CallerIpAddress, OperationNameValue, ResourceGroup, _ResourceId
| order by TimeGenerated desc
// Who touched production in the change freeze window
AzureActivity
| where TimeGenerated between (datetime(2026-10-25 18:00) .. datetime(2026-10-26 06:00))
| where ResourceGroup startswith "RG-PROD" or ResourceGroup startswith "rg-prod"
| where CategoryValue == "Administrative" and ActivityStatusValue in ("Start", "Success")
| summarize ops = count(), actions = make_set(OperationNameValue, 20) by Caller, CallerIpAddress
| order by ops desc
// Role assignments created or removed (privilege changes)
AzureActivity
| where TimeGenerated > ago(30d)
| where OperationNameValue has "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS"
| project TimeGenerated, Caller, OperationNameValue, ActivityStatusValue, _ResourceId, Properties_d

Status values in AzureActivity (Start, Success, Failure, Accepted) differ from the CLI's Started/Succeeded/Failed. The case of OperationNameValue and ResourceGroup can also vary. Use =~, has or endswith (all case-insensitive), not ==.

Prevent it next time

Keep more than 90 days and make it searchable. Send the subscription Activity Log to a central workspace:

az monitor diagnostic-settings subscription create -n activity-to-law --location westeurope \
  --workspace /subscriptions/<sub>/resourceGroups/rg-monitor/providers/Microsoft.OperationalInsights/workspaces/law-central \
  --logs "[{category:Administrative,enabled:true},{category:Security,enabled:true},{category:Policy,enabled:true},{category:ServiceHealth,enabled:true},{category:ResourceHealth,enabled:true}]"

At scale, assign the built-in policy that configures the Activity Log export at management group level instead.

Warning

Finding the caller after the fact doesn't bring a deleted resource back. Put CanNotDelete locks on production resource groups (az lock create --name no-delete --lock-type CanNotDelete -g rg-app-prod). Create activity log alerts on high-impact operations, such as resource group delete, role assignment write and Key Vault delete, so you hear about them within minutes instead of the next morning.

  • Give operators standing Reader access, and grant write access through PIM so every change maps to a named, time-boxed activation.
  • Use one service principal or managed identity per pipeline, with a clear display name, so a GUID in the Activity Log identifies one pipeline.