Mallow
Azure Tips · April 12, 2024

Azure Change Analysis

Majid Maskati

By Majid Maskati

Azure Change Analysis

Using Azure Change Analysis to track infrastructure changes.

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 a create, update or delete of the resource
  • For resource updates propertyChangeType indicates whether this is an insert, update or removal of the resource property
  • The propertyName as well as its previousValue and newValue
  • changedBy is the name or object id of the principal that 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