Back to script library
Entra / Microsoft 365 · Compliance & audit

Search unified audit log with Graph

Creates and polls a Microsoft Graph audit log query, then exports matching unified audit records for review.

Connect & set up

Run these once per session. All scopes are read-only unless the script makes changes.

Connect-MgGraph -Scopes AuditLogsQuery.Read.All -NoWelcome

Run it

The main script. Copy it, or download the .ps1 and run it from your console.

param(
[string] $StartDate = "$null; $EndDate = $null; $Operations = $null",
[string] $EndDate = "Read-Host "End Date for audit search"
)
Connect-MgGraph -Scopes AuditLogsQuery.Read.All -NoWelcome
$SearchId = $null
# Construct basic search parameters
# For multiple operations, use "operationFilters" = @("fileaccessed","filedeleted")
# For record type filters, use "recordTypeFilters" = @("sharePointFileOperation","threatIntelligence")
[array]$UserOperations = Read-Host "Enter the audit operations to search for (separated by commas)"
[array]$Operations = $UserOperations.split(",").trim(" ").tolower()
Try {
$StartDateSearch = (Get-Date $StartDate -format s) + "Z"
}
Catch {
Write-Host ("{0} is not a valid date" -f $StartDate)
Break
}
Try {
$EndDateSearch = (Get-Date $EndDate -format s) + "Z"
}
Catch {
Write-Host ("{0} is not a valid date" -f $EndDate)
Break
}
If (!($Operations)) {
Write-Host "No audit operations specified - exiting"
Break
}
$SearchName = ("Audit Search created {0}" -f (Get-Date -format 'dd-MMM-yyyy HH:mm'))
$SearchParameters = @{
"displayName" = $SearchName
"filterStartDateTime" = $StartDateSearch
"filterEndDateTime" = $EndDateSearch
"operationFilters" = $Operations
}
Write-Host "Creating an audit search query..."
$Uri = "https://graph.microsoft.com/beta/security/auditLog/queries"
$SearchQuery = Invoke-MgGraphRequest -Method POST -Uri $Uri -Body $SearchParameters
$SearchId = $SearchQuery.Id
If ($null -eq $SearchId) {
Write-Host "Search not created"
Break
} Else {
$SearchId = $SearchQuery.Id
Write-Host ("Audit log search created with id: {0} and name {1}" -f $SearchId, $SearchQuery.displayname)
}
[int]$i = 1
[int]$SleepSeconds = 20
$SearchFinished = $false; [int]$SecondsElapsed = 20
Write-Host "Checking audit query status..."
Start-Sleep -Seconds 20
$Uri = ("https://graph.microsoft.com/beta/security/auditLog/queries/{0}" -f $SearchId)
$SearchStatus = Invoke-MgGraphRequest -Uri $Uri -Method GET
While ($SearchFinished -eq $false) {
$i++
Write-Host ("Waiting for audit search to complete. Check {0} after {1} seconds. Current state {2}" -f $i, $SecondsElapsed, $SearchStatus.status)
If ($SearchStatus.status -eq 'succeeded') {
$SearchFinished = $true
} Else {
Start-Sleep -Seconds $SleepSeconds
$SecondsElapsed = $SecondsElapsed + $SleepSeconds
$SearchStatus = Invoke-MgGraphRequest -Uri $Uri -Method GET
}
}
Write-Host "Fetching audit records found by the search..."
$Uri = ("https://graph.microsoft.com/beta/security/auditLog/queries/{0}/records?`$Top=999" -f $SearchId)
[array]$SearchRecords = Invoke-MgGraphRequest -Uri $Uri -Method GET
[array]$AuditRecords = $SearchRecords.value
# Paginate to fetch all available audit records
$NextLink = $SearchRecords.'@odata.NextLink'
While ($null -ne $NextLink) {
$SearchRecords = $null
[array]$SearchRecords = Invoke-MgGraphRequest -Uri $NextLink -Method GET
$AuditRecords += $SearchRecords.value
Write-Host ("{0} audit records fetched so far..." -f $AuditRecords.count)
$NextLink = $SearchRecords.'@odata.NextLink'
}
Write-Host ("Total of {0} audit records found" -f $AuditRecords.count) -ForegroundColor Red
$Report = [System.Collections.Generic.List[Object]]::new()
ForEach ($Record in $AuditRecords) {
$ReportLine = [PSCustomObject][Ordered]@{
Service = $Record.Service
Timestamp = $Record.CreatedDateTime
UPN = $Record.userPrincipalName
Operation = $Record.operation
}
$Report.Add($ReportLine)
}
$Report | Sort-Object {$_.Timestamp -as [datetime]} | Out-GridView -Title 'Audit Records'
Attribution