Skip to content

Instantly share code, notes, and snippets.

@maskati
Last active April 11, 2024 10:19
Show Gist options
  • Save maskati/7d73e62de4a5cbb6b90608085d1291b7 to your computer and use it in GitHub Desktop.
Save maskati/7d73e62de4a5cbb6b90608085d1291b7 to your computer and use it in GitHub Desktop.
Azure Change Analysis query including change actor details

Azure Change Analysis enhances the visibility of changes made to Azure resources. It does this by tracking these changes at the subscription level and recording them in the Azure Resource Graph's resourceschanges table.

As of March 2024, change tracking now also includes detailed information about the principal that initiated the change, the client type (e.g. Azure Portal, Azure CLI, ARM template), and the operation which resulted in the change (e.g. Microsoft.Web/sites/write). This enhancement means you no longer need to consult Azure activity logs separately to understand who initiated a change and what action they performed. Everything is conveniently available within the Change Analysis.

The structure of the resourceschanges table can make it challenging to clearly view resource and property changes at a glance. However, you can overcome this with some Kusto magic. Below, i've provided an Azure Resource Graph query that simplifies these details into a more digestible table format, including all pertinent change details.

  • resourceChangeType indicates whether this is an create, update or delete of the resource
  • and for resource updates propertyChangeType indicates whether this is an insert, update or removal of the resource property
  • the propertyName as well as it's previousValue and newValue
  • changedBy is the name or object id of the principal the initiated the change
  • operation is the Azure ARM operation which resulted in the change
  • correlationId can be used to lookup related changes and events from the Azure activity log
  • activityLogLink is link to the Azure Portal activity log prefiltered to the change event correlationId and timestamp
resourcechanges
| extend timestamp = todatetime(properties.changeAttributes.timestamp)
| extend changedBy = tostring(properties.changeAttributes.changedBy)
| extend clientType = tostring(properties.changeAttributes.clientType)
| extend operation = tostring(properties.changeAttributes.operation)
| extend correlationId = tostring(properties.changeAttributes.correlationId)
| extend resourceChangeType = tostring(properties.changeType)
| extend targetResourceId = properties.targetResourceId
| parse targetResourceId with '/subscriptions/' subscriptionId '/resourceGroups/' resourceGroup '/providers/' resourceProvider '/' resourceType '/' resource
| extend resourceProviderType = strcat(resourceProvider, '/', resourceType)
| extend activityLogQuery = strcat('{"query":{"subscriptions":["', subscriptionId, '"],"searchString":"', correlationId, '","timeSpan":"3","startTime":"', replace(' ', 'T', format_datetime(timestamp - 3h, 'yyyy-MM-dd HH:mm:ss.fff')), 'Z","endTime":"', replace(' ', 'T', format_datetime(timestamp + 3h, 'yyyy-MM-dd HH:mm:ss.fff')), 'Z"}}')
| extend activityLogLink = iff(correlationId=='00000000-0000-0000-0000-000000000000', '-', strcat('https://portal.azure.com/#view/Microsoft_Azure_ActivityLog/ActivityLogBlade/queryInputs~/', url_encode_component(activityLogQuery)))
| extend changes = iff(array_length(bag_keys(properties.changes))==0, dynamic({'-':dynamic({'propertyChangeType':'-'})}), properties.changes)
| mv-expand propertyName = bag_keys(changes) to typeof(string)
| extend propertyChangeType = tostring(changes[propertyName].propertyChangeType)
| extend previousValue = coalesce(changes[propertyName].previousValue, '-')
| extend newValue = coalesce(changes[propertyName].newValue, '-')
| sort by timestamp, subscriptionId, resourceGroup, resourceProviderType, resource, resourceChangeType, propertyChangeType, propertyName
| project timestamp, subscriptionId, resourceGroup, resourceProviderType, resource, resourceChangeType, propertyChangeType, propertyName, previousValue, newValue, operation, changedBy, clientType, correlationId, activityLogLink

Below is an example output of change analysis captured changes as a result of creating a Logic App resource (bottom of the table), updating the workflow, disabling the workflow and finally deleting the resource (top of the table): image

Finally, below is a PowerShell script for listing the same change analysis information locally without going to the Azure Portal. The output can also be exported to a file or copy-pasted into Excel for further analysis.

$q=@"
resourcechanges
| extend timestamp = todatetime(properties.changeAttributes.timestamp)
| extend changedBy = tostring(properties.changeAttributes.changedBy)
| extend clientType = tostring(properties.changeAttributes.clientType)
| extend operation = tostring(properties.changeAttributes.operation)
| extend correlationId = tostring(properties.changeAttributes.correlationId)
| extend resourceChangeType = tostring(properties.changeType)
| extend targetResourceId = properties.targetResourceId
| parse targetResourceId with '/subscriptions/' subscriptionId '/resourceGroups/' resourceGroup '/providers/' resourceProvider '/' resourceType '/' resource
| extend resourceProviderType = strcat(resourceProvider, '/', resourceType)
| extend activityLogQuery = strcat('{"query":{"subscriptions":["', subscriptionId, '"],"searchString":"', correlationId, '","timeSpan":"3","startTime":"', replace(' ', 'T', format_datetime(timestamp - 3h, 'yyyy-MM-dd HH:mm:ss.fff')), 'Z","endTime":"', replace(' ', 'T', format_datetime(timestamp + 3h, 'yyyy-MM-dd HH:mm:ss.fff')), 'Z"}}')
| extend activityLogLink = iff(correlationId=='00000000-0000-0000-0000-000000000000', '-', strcat('https://portal.azure.com/#view/Microsoft_Azure_ActivityLog/ActivityLogBlade/queryInputs~/', url_encode_component(activityLogQuery)))
| extend changes = iff(array_length(bag_keys(properties.changes))==0, dynamic({'-':dynamic({'propertyChangeType':'-'})}), properties.changes)
| mv-expand propertyName = bag_keys(changes) to typeof(string)
| extend propertyChangeType = tostring(changes[propertyName].propertyChangeType)
| extend previousValue = coalesce(changes[propertyName].previousValue, '-')
| extend newValue = coalesce(changes[propertyName].newValue, '-')
| sort by timestamp, subscriptionId, resourceGroup, resourceProviderType, resource, resourceChangeType, propertyChangeType, propertyName
| project timestamp, subscriptionId, resourceGroup, resourceProviderType, resource, resourceChangeType, propertyChangeType, propertyName, previousValue, newValue, operation, changedBy, clientType, correlationId, activityLogLink
"@;$c=@();$r=$null;do{if($null -eq $r.skip_token){$r=$q|az graph query -q '@-'|convertfrom-json}else{$r=$q|az graph query -q '@-' --skip-token $r.skip_token|convertfrom-json};$c+=$r.data;}until($null -eq $r.skip_token);$c|select timestamp, subscriptionId, resourceGroup, resourceProviderType, resource, resourceChangeType, propertyChangeType, propertyName, previousValue, newValue, operation, changedBy, clientType, correlationId, activityLogLink|ogv -t 'Azure Resource Changes';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment