← All cheat sheets

AZURE RESOURCE INCIDENT RESPONSE

Plain-text reference · 17 KB. Read it, search it (Ctrl-F) or print it.

A triage reference for a suspected compromise of Azure resources: virtual
machines, storage accounts, key vaults and networks. It covers which logs
exist, how to read them with az CLI and KQL, how to contain without losing
evidence, and how to preserve disks. It is for investigation and response,
NOT for attacking Azure. az lines need Azure CLI and a reader role on the
subscription; KQL runs in a Log Analytics workspace or Microsoft Sentinel.
Everything is read-only except the diagnostic-setting line in section 2
and sections 8 and 9, which change resources on purpose.

Identity (sign-ins, risky users, app consents, Graph session revocation)
is in the Entra ID and GCP Detection sheet; general KQL in the Sentinel
KQL sheet. Placeholders: <sub> subscription ID, rg-prod resource group,
vm-web01 VM, stprod01 storage account, kv-example key vault, nsg-web
network security group, <region> Azure region, <caller> UPN or object ID.

1. WHICH LOG HAS WHAT#

Activity log    control plane: create, update, delete and action (POST)
                calls through Azure Resource Manager, plus RBAC changes.
                On by default, kept 90 days, then deleted. Plain reads
                (GET) are NOT recorded.
Resource logs   data plane: blob reads, secret reads and so on. NOT
                collected until you create a diagnostic setting, and
                nothing is recorded before that.
Flow logs       IP flows (5-tuple, allow/deny, bytes) per virtual network,
                written to a storage account once a minute.
Defender alerts Microsoft Defender for Cloud detections; also appear in
                the activity log category "Security".
The activity log is the only place that records who created a resource.
Entries take about 3 to 20 minutes to appear, Key Vault logs up to 10.
Activity log categories: Administrative, Service Health, Resource Health,
Alert, Autoscale, Recommendation, Security, Policy.

2. CHECK LOGGING FIRST (attackers turn it off: ATT&CK T1685.002)#

az monitor diagnostic-settings subscription list   # activity log export
az monitor diagnostic-settings list --resource <resource-id>
az monitor diagnostic-settings show --name <setting> --resource <id>
Blob logs live on the blob service, not on the account:
  --resource /subscriptions/<sub>/resourceGroups/rg-prod/providers/\
Microsoft.Storage/storageAccounts/stprod01/blobServices/default
Blob categories: StorageRead, StorageWrite, StorageDelete. Key Vault
category: AuditEvent (or the category group "audit").
No setting = no data-plane history. Add one now, before you contain:
  az monitor diagnostic-settings create --name ir-kv \
    --resource <kv-resource-id> --workspace <workspace-resource-id> \
    --logs '[{"categoryGroup":"audit","enabled":true}]' \
    --export-to-resource-specific true
Limits: up to five diagnostic settings per resource; storage or event
hub destinations must be in the same region as the resource.
Flags in the activity log: operations ending in
  microsoft.insights/diagnosticsettings/delete or .../write
on a resource you care about, just before or during the incident.

3. ACTIVITY LOG WITH AZ CLI#

az monitor activity-log list --offset 7d --max-events 1000 -o table
az monitor activity-log list --caller <caller> --offset 30d \
  --max-events 1000 \
  --query "[].{t:eventTimestamp,op:operationName.value,\
st:status.value,res:resourceId}" -o table
az monitor activity-log list --correlation-id <guid> --offset 30d
az monitor activity-log list --resource-group rg-prod --status Failed \
  --offset 2d --max-events 500
Notes: defaults are only the last 6 hours (--offset) and 50 events
(--max-events), so set both. Each write appears twice or more (Started,
then Succeeded or Failed); events with the same correlationId belong to
one action. Older than 90 days only exists if it was exported (section 2).
Key fields: caller, eventTimestamp (UTC), operationName, status,
resourceId, correlationId, httpRequest.clientIpAddress, authorization
(action, role, scope), claims (the token used).

4. CONTROL-PLANE OPERATIONS TO LOOK FOR#

Compute (ATT&CK T1651 Cloud Administration Command, T1578):
  Microsoft.Compute/virtualMachines/runCommand/action  # action Run Cmd
  Microsoft.Compute/virtualMachines/runCommands/write  # managed Run Cmd
  Microsoft.Compute/virtualMachines/extensions/write   # e.g. scripts
  Microsoft.Compute/snapshots/write                    # T1578.001
  Microsoft.Compute/disks/beginGetAccess/action        # disk SAS export
  Microsoft.Compute/snapshots/beginGetAccess/action    # snapshot export
Access (T1098.003 Additional Cloud Roles, T1098.001 credentials):
  Microsoft.Authorization/roleAssignments/write
  Microsoft.Authorization/roleDefinitions/write        # custom role
  Microsoft.Authorization/locks/delete                 # lock removed
  Microsoft.ManagedIdentity/userAssignedIdentities/\
federatedIdentityCredentials/write                     # outside trust
Storage (T1530 Data from Cloud Storage, T1537 sharing out):
  Microsoft.Storage/storageAccounts/listKeys/action    # key read
  Microsoft.Storage/storageAccounts/regenerateKey/action
  Microsoft.Storage/storageAccounts/listAccountSas/action
  Microsoft.Storage/storageAccounts/write              # public/network
Key Vault and network:
  Microsoft.KeyVault/vaults/accessPolicies/write       # who may read
  Microsoft.KeyVault/vaults/write / vaults/delete
  Microsoft.Network/networkSecurityGroups/securityRules/write
                                                 # T1686.001 Cloud Firewall
Tenant level: Microsoft.Authorization/elevateAccess/action means a Global
Administrator gave themselves User Access Administrator at root scope
("/"). It shows under Monitor > Activity log > Directory Activity and in
Entra audit logs, not in a subscription's log. Check:
  az role assignment list --role "User Access Administrator" --scope "/"

5. KQL HUNTS ON AzureActivity#

AzureActivity stores the activity log in a workspace. Values can differ
in case: use =~, in~ and has, never ==. Status is "Success" or
"Succeeded" depending on the source, so match both.
// Sensitive operations in the last 7 days
let ops = dynamic([
  "microsoft.compute/virtualmachines/runcommand/action",
  "microsoft.compute/virtualmachines/runcommands/write",
  "microsoft.compute/virtualmachines/extensions/write",
  "microsoft.compute/snapshots/write",
  "microsoft.compute/disks/begingetaccess/action",
  "microsoft.authorization/roleassignments/write",
  "microsoft.storage/storageaccounts/listkeys/action",
  "microsoft.storage/storageaccounts/listaccountsas/action",
  "microsoft.keyvault/vaults/accesspolicies/write",
  "microsoft.insights/diagnosticsettings/delete"]);
AzureActivity
| where TimeGenerated > ago(7d)
| where OperationNameValue in~ (ops)
| where ActivityStatusValue in~ ("Success", "Succeeded")
| project TimeGenerated, Caller, CallerIpAddress, OperationNameValue,
    _ResourceId, CorrelationId
| order by TimeGenerated asc

// Everything one caller did, grouped
AzureActivity
| where TimeGenerated > ago(30d) and Caller =~ "<caller>"
| summarize n=count(), first=min(TimeGenerated), last=max(TimeGenerated)
    by OperationNameValue, ResourceGroup
| order by first asc

// Caller + IP pairs never seen before the last day
AzureActivity
| where TimeGenerated > ago(30d) and CategoryValue =~ "Administrative"
| summarize first=min(TimeGenerated) by Caller, CallerIpAddress
| where first > ago(1d)

// Bulk deletes (sabotage, ATT&CK T1485 / T1490)
AzureActivity
| where TimeGenerated > ago(1d)
| where OperationNameValue endswith "/delete"
| where ActivityStatusValue in~ ("Success", "Succeeded")
| summarize n=count(), what=make_set(OperationNameValue, 20)
    by Caller, bin(TimeGenerated, 1h)
| where n > 10

6. DATA PLANE: STORAGE AND KEY VAULT LOGS#

StorageBlobLogs (needs the blob diagnostic setting from section 2):
// Who read the most data
StorageBlobLogs
| where TimeGenerated > ago(7d)
| where OperationName in ("GetBlob", "ListBlobs")
| summarize ops=count(), bytes=sum(ResponseBodySize)
    by AccountName, CallerIpAddress, AuthenticationType, RequesterUpn
| order by bytes desc
// Anonymous (public) reads
StorageBlobLogs
| where TimeGenerated > ago(7d) and AuthenticationType == "Anonymous"
| project TimeGenerated, OperationName, Uri, CallerIpAddress
// Which key or SAS was used
StorageBlobLogs
| where TimeGenerated > ago(7d)
| summarize n=count() by AuthenticationType, AuthenticationHash
Notes: CallerIpAddress includes the port. AuthenticationHash holds
SHA-256 hashes of the credential; the Storage log reference documents
the form key1(<hash>) or key2(<hash>), plus SasSignature(<hash>) for a
SAS, which shows the signing key and lets you follow one SAS token.
RequesterUpn and RequesterObjectId are only set for Entra ID
(OAuth) requests. Run summarize count() by AuthenticationType first to
see the values your workspace uses.
AZKVAuditLogs (resource-specific table; older settings write to
AzureDiagnostics instead):
// Secret and key use by caller (ATT&CK T1555.006)
AZKVAuditLogs
| where TimeGenerated > ago(7d)
| where OperationName in ("SecretGet", "SecretList", "SecretListVersions",
    "KeyDecrypt", "KeyUnwrap", "SecretBackup", "KeyBackup")
| summarize n=count(), items=dcount(Id)
    by CallerIpAddress, tostring(Identity), OperationName
| order by n desc
// Denied requests (probing). Skip 401: clients get one as the normal
// first authentication challenge.
AZKVAuditLogs
| where TimeGenerated > ago(7d) and HttpStatusCode == 403
| summarize n=count() by CallerIpAddress, OperationName
Flags: one identity reading many secrets in minutes; SecretList then
SecretGet on every item; reads from an IP outside your ranges;
SecretPurge, KeyPurge or VaultDelete.

7. NETWORK FLOWS AND DEFENDER ALERTS#

NSG flow logs retire on 30 September 2027 and new ones can no longer be
created; use virtual network flow logs.
az network watcher flow-log list --location <region> -o table
Flow tuple fields: time, src IP, dst IP, src port, dst port, protocol,
direction (I/O), state (B begin, C continuing, E end, D deny), then
encryption, packets and bytes. With traffic analytics on, query
NTANetAnalytics:
NTANetAnalytics
| where TimeGenerated > ago(1d) and SubType =~ "FlowLog"
| where FlowType == "MaliciousFlow" or FlowStatus =~ "Denied"
| project TimeGenerated, SrcIp, DestIp, DestPort, L4Protocol,
    FlowDirection, FlowStatus, FlowType
Defender for Cloud alerts (needs the matching Defender plans):
az security alert list -o table
SecurityAlert
| where TimeGenerated > ago(7d)
| where AlertType startswith "ARM_" or AlertType startswith "KV_"
    or AlertType startswith "Storage."
| project TimeGenerated, AlertName, AlertSeverity, CompromisedEntity,
    Tactics, Techniques
Prefixes: ARM_ Resource Manager, KV_ Key Vault, Storage. Storage.
Mark progress: az security alert update --location <region> \
  --name <alert-name> --status inprogress

8. CONTAIN WITHOUT DESTROYING EVIDENCE (these lines change resources)#

Order: preserve (section 9) or at least snapshot, then cut access. Do NOT
delete the VM, its disks or the storage account.
Stop deletion while you work. Owner and User Access Administrator can
remove a lock, so watch for Microsoft.Authorization/locks/delete:
  az lock create --name ir-hold --lock-type CanNotDelete \
    --resource-group rg-prod
Isolate a VM with a deny rule whose priority number is lower than every
allow rule (attach a dedicated NSG with az network nic update
--network-security-group if the NIC has none):
  az network nsg rule create -g rg-prod --nsg-name nsg-web \
    --name ir-deny-in --priority 100 --direction Inbound --access Deny \
    --protocol '*' --source-address-prefixes '*' \
    --destination-address-prefixes '*' --destination-port-ranges '*'
  (repeat with --direction Outbound --name ir-deny-out; allow your
  forensic workstation first with a lower priority number)
NSG limits: rule changes only affect NEW connections, an open session
stays up. The VM agent's traffic to 168.63.129.16 (used by Run Command
and extensions) is not filtered by NSGs; Azure DNS is blocked only by a
deny rule on the AzurePlatformDNS service tag.
Memory is lost on stop or deallocate; capture it in the guest first if
you need it (see the Memory Forensics sheet).
Remove attacker access (record it first, sections 3 and 4):
  az role assignment list --all --assignee <caller> -o table
  az role assignment delete --assignee <caller> --role <role> \
    --scope <scope>
  az vm run-command list -g rg-prod --vm-name vm-web01  # managed ones
  az vm extension list -g rg-prod --vm-name vm-web01 -o table
Storage keys and SAS:
  az storage account keys renew -g rg-prod -n stprod01 --key primary
  az storage account keys renew -g rg-prod -n stprod01 --key secondary
  az storage account revoke-delegation-keys -g rg-prod -n stprod01
  az storage account update -g rg-prod -n stprod01 \
    --allow-blob-public-access false --allow-shared-key-access false
  Renewing a key also invalidates every account and service SAS signed
  with it; user delegation SAS needs revoke-delegation-keys (cached, so
  allow a short delay). A service SAS tied to a stored access policy can
  be cut by changing or deleting that policy. Disallowing shared key
  breaks apps that use keys: check AuthenticationType first.
Key Vault:
  az keyvault update -g rg-prod -n kv-example --default-action Deny
  az keyvault update -g rg-prod -n kv-example --enable-purge-protection true
  Then rotate every secret the attacker read (section 6). Soft-delete
  keeps deleted items 7 to 90 days (90 by default); purge protection
  cannot be turned off again once on.
Elevated access: remove the root-scope assignment if it was not yours:
  az role assignment delete --assignee <caller> \
    --role "User Access Administrator" --scope "/"

9. PRESERVE DISKS AND LOGS#

Snapshot every OS and data disk of the VM (writes a new resource):
  az vm show -g rg-prod -n vm-web01 \
    --query "storageProfile.osDisk.managedDisk.id" -o tsv
  az vm show -g rg-prod -n vm-web01 \
    --query "storageProfile.dataDisks[].managedDisk.id" -o tsv
  az snapshot create -g rg-prod --name vm-web01-os-ir --source <disk-id>
  (the snapshot is created in the resource group's region, so use the
  disk's group or pass --location; copy it out to evidence storage)
Export for analysis with a short read-only SAS, then revoke it:
  az snapshot grant-access -g rg-prod -n vm-web01-os-ir \
    --duration-in-seconds 3600 --access-level Read --query accessSas -o tsv
  az storage blob download --blob-url "<sas-url>" --file os.vhd
  sha256sum os.vhd | tee os.vhd.sha256
  az snapshot revoke-access -g rg-prod -n vm-web01-os-ir
Keep copies WORM in a separate evidence subscription:
  az storage container legal-hold set --account-name <evidence-account> \
    --container-name evidence --tags case2026001
Notes: ephemeral OS disks live only on the host and cannot be
snapshotted. The SAS URL is a secret: anyone holding it can download the
disk until it expires or is revoked. Export activity log and resource
logs for the incident window (Log Analytics export, or the PT1H.json
blobs under insights-* containers) before the 90-day activity log limit.

10. QUICK TRIAGE ORDER#

1. Check diagnostic settings; add audit logging where it is missing.
2. Activity log for the window: sensitive operations, who, from where,
   grouped by correlationId.
3. Role assignments, custom roles, federated credentials, elevateAccess.
4. Run Command and extensions on each VM in scope.
5. Storage and Key Vault data-plane logs: what was read, with which key,
   SAS or identity.
6. Flow logs and Defender alerts for the same IPs.
7. Lock, snapshot and export disks with hashes; legal hold on evidence.
8. Contain: NSG deny, remove assignments, renew keys, revoke delegation
   keys, close vault network access, rotate secrets.
9. Rebuild from known-good images; keep the lock until the case closes.

Operation names, table columns and az commands were checked against
Microsoft Learn (Azure Monitor activity log schema and table reference,
diagnostic settings, Blob Storage and Key Vault monitoring references,
Network Watcher flow logs, Azure RBAC resource provider operations, Run
Command, managed disk snapshots and export, resource locks, storage keys
and SAS, Key Vault soft-delete and network security, Defender for Cloud
alert reference, elevate access) and Azure CLI 2.91.0 help output, not
run against a live tenant. Technique IDs per MITRE ATT&CK Enterprise v20.

Defensive reference on CyberRamen. Offensive / red-team sheets live on OffensiveRamen.com.