Table of Contents
AuditLogQuery Is Not a Drop-In Replacement for Search-UnifiedAuditLog
When I wrote about Microsoft’s decision to retire the Search-UnifiedAuditLog cmdlet, I noted that the conversion process is not a matter of switching an Exchange Online management cmdlet for a Microsoft Graph PowerShell SDK cmdlet. It couldn’t be. The underlying implementations are completely different. Search-UnifiedAuditLog is a synchronous cmdlet that processes a search query and returns results immediately. The AuditLogQuery API uses a job-based model where scripts submit a query, wait for Purview Audit to process the job, and then retrieve the results.
In other words, this conversion is not a drop-in replacement of one part for another. Tenant administrators and developers have some work to do to update code, check what they’ve done, and test the output. AI tools might be of some help to do an initial conversion, but manual intervention is required to complete the process. There are just too many variations to deal with.
The Good and Bad Things about the Unified Audit Log
Microsoft did a great thing by creating the unified audit log. They made it even better by making sure that all Microsoft 365 workloads support the unified audit log to capture details of both user-initiated and internal system actions. Unfortunately, things fell apart after that and the lack of consistency which exists in audit log data makes the conversion more complicated than it might be.
All audit records have some mandatory properties such as the operation, timestamp, and actor. The mandatory properties are not a problem. The issue lies in the AuditData property. AuditData is where audit records hold the most interesting information. Mining AuditData reveals all kinds of information about who did what and even perhaps why (always a guess). The problem is that workloads determine the properties and content written into their audit records. The audit data for some workloads (like SharePoint Online) are reasonably straightforward to interpret. Other workloads are much harder, such as product SKU details in the audit records for Entra ID license assignments.
Changes in Property Names and Format Used by the AuditLogQuery API
Which brings me to another complication. When it outputs records, the AuditLogQuery API makes some significant changes in property name when compared to the names used by Search-UnifiedAuditLog. In just the audit record header, we find that UserIds becomes UserPrincipalName, Operations is now Operation, and CreationDate becomes CreatedDateTime. The new property names are logical and align with property names used in other Graph APIs, but the fact that property names change means that care must be paid during script conversion to make sure that the correct property names are used everywhere.
Dealing with AuditData
Interrogating the AuditData property requires attention during conversion. Audit records returned by the Graph no longer use JSON to format the data held in the AuditData property. Scripts which use Search-UnifiedAuditLog often include a command like that shown below to convert the JSON and populate an array with the properties and values:
[array]$AuditData = $Rec.AuditData | ConvertFrom-Json
The form of the AuditData property returned for audit records differs depending on whether a Graph API or SDK cmdlet retrieves the records. When a Graph API request is used, AuditData is a PowerShell object where the individual properties can be dot referenced. For example:
$Rec.AuditData.SiteUrl
When the Get-MgSecurityAuditLogQueryRecord retrieves the audit records, AuditData is returned as a PowerShell object where the data is in the additionalProperties property, which holds a dictionary of name/value pairs. The reason for the difference is because the AuditData properties are not strongly typed, meaning that the generation process for the Microsoft Graph PowerShell SDK cmdlets do not create individual properties within AuditData and use additionalProperties to hold the properties instead:
$AuditData = $Rec.AuditData.additionalProperties $Auditdata.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Dictionary`2 System.Object

Scripts can reference properties in the dictionary with a dot reference or lookup:
$AuditData.SiteUrl
Or:
$AuditData["SiteUrl"]
Both methods return the value of the referenced property.
Properties exposed through the additionalProperties dictionary should be referenced using the exact names returned by the dictionary. For example, SiteUrl returns a value, while siteUrl does not. Scripts that use incorrect letter casing to reference dictionary entries will not retrieve the expected values.
Steps in Script Conversion
The steps involved in script conversion are much the same as for creating an audit query job from scratch:
- Initiate an interactive or app-only session with the appropriate Graph permissions. For example, AuditLogsQuery.Read.All allows access to audit events generated by all workloads.
- Audit query parameters can be passed as named parameters for the New-MgSecurityAuditLogQuery cmdlet (Table 1) or as properties in a request body (a hash table). If you use Graph API requests, you must use a request body. Some parameters (ResultSize, SessionId, and SessionCommand) are not used by the Graph API.
- Submit the audit job. This action places the job on the processing queue. Purview will pick up the job when demand on its service allows to process the query.
- Monitor the status of the audit job to detect when processing is complete. Depending on service load and the number of records requested, processing can take ten minutes or longer. Scripts that depend on checking the audit log for information such as the date of the last file or email created by a user no longer have a route to a quick response. They must either wait for a queued audit job to finish or use another method (if one exists).
- Retrieve the audit records and populate an array for processing.
- Extract audit data from the audit records. This is usually done with a loop through the audit records to find, format, and harvest information about auditable actions of interest from the retrieved set. It is at this point when great care must be taken to ensure that the script uses the correct property names, especially when the AuditData property can be represented in two different forms.
| Search-UnifiedAuditLog cmdlet parameters | New-MgSecurityAuditLogQuery cmdlet parameters | Notes |
| UserIds | UserPrincipalNamesFilter | Array of user principal names to search for. |
| No equivalent | AdministrativeUnitIdFilters | Array of administrative unit identifiers to limit search results. |
| StartDate and EndDate | filterStartDateTime, filterEndDateTime | Start and end date for search job. Some organizations use UTC dates for all searches. |
| RecordType | recordTypeFilters | Array of record types to search for (like ExchangeOnline). |
| Operations | operationFilters | Array of auditable actions such as MailItemsAccessed and FileModified to search for. |
| ObjectIds | objectIdFilters | Array of object identifiers. |
| FreeText | keywordFilter | Free text search of audit records. |
| IPAddresses | ipAddressFilter | Array of IP addresses to search for. |
Remember that other steps might be needed to create a fully functional script. For example, in the example script described below, the code fetches mailbox data to create a hash table containing user principal names and display names. When connected to Exchange Online, it’s easy to run the Get-ExoMailbox cmdlet to fetch the mailbox information. When connected to the Graph to run audit jobs, different code using the Get-MgUser cmdlet is needed to do the same job.
A Working Example
A working example is worth several thousand words. I had a script to analyze system events captured in the audit log, and I used it as an example. The script fetches all audit records for the last seven days. In larger tenants you’ll need to use a smaller period to stay within the 50,000 record limit for Search-UnifiedAuditLog. The script discards all audit records for actions performed by tenant users before analyzing the events for system actions. You can download the script from the Microsoft 365 for IT Pros GitHub repository. The script contains code for both methods.
Happy converting!
Need help to write and manage PowerShell scripts for Microsoft 365, including Azure Automation runbooks? Get a copy of the Automating Microsoft 365 with PowerShell eBook, available standalone or as part of the Microsoft 365 for IT Pros eBook bundle.