M365 Unified Audit Log (UAL) Forensic Manual
PRIMARY CRIME SCENE
EXCHANGE ONLINE & PURVIEW
In a 100% cloud-native organization, the Unified Audit Log (UAL) is the primary audit log for all user, administrative, and application actions across Exchange Online, SharePoint Online, OneDrive, Microsoft Teams, and Microsoft Entra ID.
🎯 Master Workload & RecordType Reference
Section titled “🎯 Master Workload & RecordType Reference”| Workload | RecordType | Key Operations to Hunt | Forensic Value |
|---|---|---|---|
| Exchange Online | ExchangeItem (2) | MailItemsAccessed, SendAs, SendOnBehalf | Proves adversary actually read or exfiltrated specific emails |
| Exchange Admin | ExchangeAdmin (1) | New-InboxRule, Set-Mailbox, Add-MailboxPermission | Mailbox delegation backdoors and forwarding rules |
| SharePoint / OneDrive | SharePointFileOperation (6) | FileDownloaded, FileAccessed, AnonymousLinkCreated | Mass document staging, exfiltration, and external sharing |
| Entra ID | AzureActiveDirectory (8) | ConsentToApplication, Add service principal credentials | OAuth consent persistence and backdoored enterprise apps |
| Purview / Security | SecurityComplianceCenterEOPFilter (38) | New-ComplianceSearch, New-ComplianceSearchAction | Uncovering unauthorized searches or evidence tampering |
🔬 Critical Forensic Operation: MailItemsAccessed (E5 / Purview Audit)
Section titled “🔬 Critical Forensic Operation: MailItemsAccessed (E5 / Purview Audit)”Standard Exchange logs only prove that a mailbox was accessed. MailItemsAccessed proves which specific message IDs were read or downloaded via sync:
# Query MailItemsAccessed for a compromised user$audit = Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) ` -RecordType ExchangeItem -Operations "MailItemsAccessed" ` -FreeText "victim.user@company.com" -ResultSize 5000
# Parse raw AuditData JSON to extract exact subject and item IDs$audit | ForEach-Object { $data = $_.AuditData | ConvertFrom-Json [PSCustomObject]@{ CreationTime = $_.CreationDate ClientIP = $data.ClientIPAddress AppId = $data.AppId LogonType = $data.LogonType Folders = ($data.Item.ParentFolder.FolderPath -join "; ") ItemCount = $data.Item.Count }} | Format-Table -AutoSize⚡ Production High-Speed Extraction Script
Section titled “⚡ Production High-Speed Extraction Script”The following production script handles UAL pagination, throttling (HTTP 429), and flattens nested AuditData into a structured timeline:
<#.SYNOPSIS Paginates and extracts UAL records to forensic CSV.#>param ( [Parameter(Mandatory=$true)] [string]$TargetUser,
[int]$DaysBack = 14, [string]$OutputFile = "C:\Forensics\UAL_Extracted.csv")
# Connect-ExchangeOnline -ShowBanner:$false$startDate = (Get-Date).AddDays(-$DaysBack)$endDate = Get-Date
Write-Host "[*] Extracting UAL records for: $TargetUser from $startDate to $endDate" -ForegroundColor Cyan$sessionCommand = "Search-UnifiedAuditLog"
$allRecords = [System.Collections.Generic.List[PSObject]]::new()$sessionIndex = 0$pageSize = 2000
do { $records = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate ` -FreeText $TargetUser -ResultSize $pageSize -SessionIndex $sessionIndex
if ($records.Count -gt 0) { foreach ($r in $records) { $parsed = $r.AuditData | ConvertFrom-Json $allRecords.Add([PSCustomObject]@{ CreationTime = $r.CreationDate RecordType = $r.RecordType Operation = $r.Operations User = $r.UserIds Workload = $parsed.Workload ClientIP = $parsed.ClientIPAddress UserAgent = $parsed.UserAgent ResultStatus = $parsed.ResultStatus RawAuditData = $r.AuditData }) } Write-Host " [+] Fetched $($records.Count) records (Total: $($allRecords.Count))" -ForegroundColor Green $sessionIndex += $records.Count Start-Sleep -Milliseconds 500 # Avoid throttling }} while ($records.Count -eq $pageSize)
$allRecords | Export-Csv -Path $OutputFile -NoTypeInformationWrite-Host "[✓] Extraction Complete! Saved $($allRecords.Count) events to $OutputFile" -ForegroundColor Yellow🔍 KQL Correlation: Querying UAL in Microsoft Sentinel
Section titled “🔍 KQL Correlation: Querying UAL in Microsoft Sentinel”If Microsoft 365 logs are streamed to Sentinel or Log Analytics, query the OfficeActivity table:
// Detect mass file downloads within a 15-minute windowOfficeActivity| where TimeGenerated > ago(7d)| where Operation == "FileDownloaded"| summarize DownloadCount = count(), UniqueFiles = dcount(OfficeObjectId), TargetFiles = make_set(OfficeFileName, 15) by UserId, ClientIP, bin(TimeGenerated, 15m)| where DownloadCount > 50| order by DownloadCount desc