“Sequence” events in the Entra Audit log and how to work with them

A question over at Q&A highlighted some changes in the way Microsoft is generating Entra audit log records for license operations. We have talked about issues surrounding this process a lot in the past, for example in this article over at Practical 365. Apart from missing entries, a common pain was records being truncated and thus now showing the whole picture. Now, looks like Microsoft is using a different model to “bundle” multiple audit events together in order to preserve all the details. So let’s go over an example.

The first thing to note here is that the change in behavior does not seem to affect all Change user license events, but only “lengthy” ones. In most cases, you can expect to run into such events when working with any of the “bundle” SKUs, such as Office 365 E1/E3/E5 or their Microsoft 365 counterparts. I haven’t bothered to check the exact “cutoff” size, but the example below with three “simple” licenses assigned to the user fits within the allowed limit. This event’s modifiedProperties blob has a “length” of 4801 characters, and if I am to make an educated guess, I’d say 10k is the cutoff.

EntraAuditSequence4

Things get a lot more interesting when “bundle” licenses are involved. For example, my own user account has the Microsoft 365 E5 SKU assigned as primary, along with a bunch of additional licenses. Due to the sheer number of services bundled in the E5 SKU, the corresponding assignedLicenses, assignedPlans and LicenseAssignmentDetail properties are plain massive in size. In turn, any change in the set of assigned licenses or services results in a blob of ~72k characters or so!

As the audit log has not been designed to deal with such large entries, this causes the corresponding record to be truncated, effectively rendering the event useless. So, to combat this, Microsoft is now “splitting” such events into multiple “sequences” that combined give you the full record. Such “sequence” events do not have the modifiedProperties blob but feature a set of properties under the additionalDetails blob, as follows (see screenshot below for an example):

  • id – a GUID that has the same value across all “sequence” events in the same “group”. Interestingly, you cannot correlate said GUID against any “merged” events (see below).
  • seq – an integer designating the position of the given event in the “sequence”
  • b – contains the record data in JSON format
  • c – gives us the total number of records in the “sequence”

In addition to said “sequence” events, Microsoft is now also generating a “merged” event that only features a single notable property within the modifiedProperties blob, namely LicenseAssignmentDetail. The resulting event should contain sufficient information to allow you to determine which licenses/services were updated on the user, while staying within the acceptable size limits (9385 chars in my case). This event however has its unique Correlation ID value, and as such cannot be directly matched against the “sequence” events.

The screenshot below illustrates the set of events generated for the license change performed against my user account. I used the Graph API endpoints to retrieve and display them via PowerShell, due to the limited functionality of the portal’s UI. As you can see, we have a total of nine events, all for the same operation and target user and within a comparable timestamp. Eight events have a matching correlationId value and are in fact part of the same “sequence”. Few seconds later, Microsoft has also generated the “merged” event, visible on top.

EntraAuditSequence

For the sake of completeness, here’s how an example “sequence” events looks like. The screenshot is taken from the UI and only focuses on the Additional details blob of the entry, where we can see the id, seq, b (trimmed) and c values. This tells us we’re looking at the 5th entry out of sequence of 6. Note the truncated JSON output at the end. (The two screenshots are for different users/difference licensing operations, hence the different number of sequence events.)

EntraAuditSequence1

Interestingly, no “merged” event is generated in some instances, such as the one above (we have six sequence events and no merged one). Thus, if you want to get the details on the license change event, sometimes you have no choice but to work with the set of “sequence” entries. Needless to say, it is beyond inconvenient to do so via the UI, where you have to manually copy the b value of which event and combine them in the correct order to fetch the complete JSON. Only after doing so you are able to see the “old” and “new” values, and getting the diff between the two poses another challenge. Luckily, you do not have to rely on the UI, so let’s examine how we can review such entries via PowerShell.

To get the events in question, you can use the following cmdlet from the Graph SDK for PowerShell:

$auditEvents = Get-MgAuditLogDirectoryAudit -Filter "activityDisplayName eq 'Change user license'"

If you are not a big fan of the SDK, you can instead use the underlying Graph API endpoint directly:

$res = Invoke-WebRequest -Uri "https://graph.microsoft.com/beta/auditLogs/directoryAudits?`$filter=activityDisplayName+eq+'Change+user+license'" -Headers $authHeader

$auditEvents = ($res.Content | ConvertFrom-Json).value

Either way, the $auditEvents variable should contain all matching events. Among these, you will have “sequence” events, such as the ones we described above, and potentially some “merged” events. The latter give you all the needed information as is, and do not require additional transformation. Unfortunately, as mentioned above already, Microsoft does not seem to be generating them all the time, so in most cases you’d be stuck with examining the “sequence” events. You can distinguish such by the presence of seq key within the additionalDetails blob. In other words, you can use this:

$seqEvents = $auditEvents | ? { $_.additionalDetails -and $_.additionalDetails.key -eq "seq" }

As mentioned above, each “sequence” has its own correlationId, which will be stamped on every record part of the sequence. Thus, we can loop over each unique correlationId value and process the events therein in the correct order, ending up with the combined JSON. The order itself can be inferred from the value of the seq property, whereas the c one gives us the total count of events in the sequence. With this information at hand, we end up with the following code:

foreach ($cid in ($seqEvents.correlationId | select -Unique)) {
    $events = $seqEvents | ? { $_.correlationId -eq $cid }
    if (!$events) { continue }

    $result = @{};$resultJSON = ""
    #order the events based on the sequence number and merge their payload
    foreach ($event in $events) {
        $eventSeq = ($event.additionalDetails | ? {$_.key -eq "seq"}).value
        $result[$eventSeq] = ($event.additionalDetails | ? { $_.key -eq "b" }).value
    }

    for ($i = 1; $i -lt ($result.Count +1); $i++) {
        $resultJSON += $result["$i"]
    }
}

The $resultJSON variable will hold the concatenated (and properly ordered) set of modified properties for each sequence of events. You can then expand the targetUpdatedProperties property to list each of the modified properties with its old and new value. Since some of these can be quite lengthy, we might as well take advantage of PowerShell’s Compare-Object to get the “diff” between the oldValue and newValue blobs. The code below does just that and spills the output to the console:

    Write-Host "-------------------"
    Write-Host "Changed properties for sequence $cid"
    Write-Host ""
    foreach ($key in (($resultJSON | ConvertFrom-Json).targetUpdatedProperties | ConvertFrom-Json)) {
        Write-Host "Property: $($key.Name)"
        Write-Host "Old Value: $($key.OldValue | ConvertTo-Json -Compress)"
        Write-Host "New Value: $($key.NewValue | ConvertTo-Json -Compress)"
        if ($key.OldValue -and $key.NewValue) {
            $change = Compare-Object -ReferenceObject $key.OldValue -DifferenceObject $key.NewValue
            if ($change -and ($change.InputObject.GetType().Name -eq "String")) {
                Write-Host "Change: $($change.InputObject)"
            } else {
                Write-Host "Change: $($change.InputObject | select -ExcludeProperty InputObject | ConvertTo-Json -Compress)"
            }
        }
        elseif ($key.OldValue) {
            Write-Host "Change: $($key.OldValue)"
        }
        elseif ($key.NewValue) {
            Write-Host "Change: $($key.NewValue)"
        }
        else {}
        Write-Host ""
    }
    Write-Host ""
}

Again, be warned that some of the modified properties can be quite lengthy, so dumping their values on the screen is not a good idea. Instead, you can focus only on the diff between the two, by commenting the corresponding instances of Write-Host in the code above. This should leave you with much easier to manage output, resembling something like:

EntraAuditSequence3The output is much easier to read now and we can see that the value of the assignedLicense property shows a change in the Power Pages vTrial for Makers SKU, which is exactly what was modified in this instance. You might notice however that the change in assignedPlan does not include all the individual service plans associated with said SKU. If you consult the licensing reference page, you will find that the SKU also includes Common Data Services and Exchange Foundation plans. As the user in question has multiple other SKUs assigned, said service plans remain active even after removing the Power Pages vTrial for Makers SKU. Similar logic is followed for the change in LicenseAssignmentDetail.

In some cases things might be more complicated. In the example below we’ve removed the Business apps (free) SKU, which comes with two services: Microsoft Invoicing and Bookings. The former is now deprecated, and Bookings is also included in the “bundle” Office 365 E3 license assigned to the user, so we have an explanation for the lack of changes in assignedPlan. On the other hand, the LicenseAssignmentDetail change is questionable, as it corresponds to the Microsoft Power Apps Plan 2 Trial SKU, which was not modified in the current operation. This is also evident from the timestamp visible therein, dated October 2021.

EntraAuditSequence2

From all the above, it’s certainly not that hard to jump to the simple conclusion that Microsoft could have made things a lot easier. While I can appreciate the effort put into ensuring we can get the full change detail, and I can also see the benefit of having both the old and new value presented, at the end of the day most customers are looking for a simple answer to the “what changed question”, and having to deal with 10k JSON is probably not the best way to address that. Perhaps “merged” events were supposed to help with, but in my experience they are not always present.

P.S. As all Change user license events are accompanied by a matching Update user event, it comes as no surprise that you can observe the exact same behavior with the latter, too. Just replace the event name in the PowerShell samples above and you can reuse the code 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.