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

Report audit events via email runbook

Azure Automation runbook that queries the audit log and sends email about audit events; can also run interactively with app-only Graph authentication.

Connect & set up

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

Connect-MgGraph -Identity

Run it

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

param(
[int] $LookbackDays = 7,
[string] $StartDate = (Get-Date).AddDays(-$LookbackDays),
[string] $EndDate = (Get-Date).AddDays(1)
)
Connect-MgGraph -Identity
Set-MgRequestContext -MaxRetry 10 -RetryDelay 15
$AuditQueryName = ("Azure Automation Runbook Scan created at {0}" -f (Get-Date -format 'dd-MMM-yyyy HH:mm'))
$AuditQueryStart = (Get-Date $StartDate -format s)
$AuditQueryEnd = (Get-Date $EndDate -format s)
[array]$AuditQueryOperations = "Update user."
$AuditQueryParameters = @{}
#$AuditQueryParameters.Add("@odata.type","#microsoft.graph.security.auditLogQuery")
$AuditQueryParameters.Add("displayName", $AuditQueryName)
$AuditQueryParameters.Add("OperationFilters", $AuditQueryOperations)
$AuditQueryParameters.Add("filterStartDateTime", $AuditQueryStart)
$AuditQueryParameters.Add("filterEndDateTime", $AuditQueryEnd)
# Submit the audit query
$AuditJob = New-MgBetaSecurityAuditLogQuery -BodyParameter $AuditQueryParameters
# Check the audit query status every 20 seconds until it completes
[int]$i = 1
[int]$SleepSeconds = 20
$SearchFinished = $false; [int]$SecondsElapsed = 20
Write-Host "Checking audit query status..."
Start-Sleep -Seconds 30
# This cmdlet is not working...
#$AuditQueryStatus = Get-MgBetaSecurityAuditLogQuery -AuditLogQueryId $AuditJob.Id
$Uri = ("https://graph.microsoft.com/beta/security/auditLog/queries/{0}" -f $AuditJob.id)
$AuditQueryStatus = 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, $AuditQueryStatus.status)
If ($AuditQueryStatus.status -eq 'succeeded') {
$SearchFinished = $true
} Else {
Start-Sleep -Seconds $SleepSeconds
$SecondsElapsed = $SecondsElapsed + $SleepSeconds
# $AuditQueryStatus = Get-MgBetaSecurityAuditLogQuery -AuditLogQueryId $AuditJob.Id
$AuditQueryStatus = Invoke-MgGraphRequest -Uri $Uri -Method Get
}
}
# Fetch the audit records returned by the query
# This cmdlet isn't working either
# [array]$AuditRecords = Get-MgBetaSecurityAuditLogQueryRecord -AuditLogQueryId $AuditJob.Id -All -PageSize 999
$Uri = ("https://graph.microsoft.com/beta/security/auditLog/queries/{0}/records" -f $AuditJob.Id)
[array]$SearchRecords = Invoke-MgGraphRequest -Uri $Uri -Method GET
[array]$AuditRecords = $SearchRecords.value
$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 ("Audit query {0} returned {1} records" -f $AuditQueryName, $AuditRecords.Count)
$Report = [System.Collections.Generic.List[Object]]::new()
ForEach ($Rec in $AuditRecords) {
$AuditData = $Rec.AuditData
If ('UserPrincipalName' -in $rec.AuditData.ModifiedProperties.Name) {
# Extract the values for the old and new UPNs from the Auditdata payload
$OldUPN = $AuditData.ModifiedProperties | Where-Object {$_.Name -eq 'UserPrincipalName'} | Select-Object -ExpandProperty OldValue
$MatchedData = $OldUPN -match '\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' | Out-Null
$OldUPN = $Matches[0]
$NewUPN = $AuditData.ModifiedProperties | Where-Object {$_.Name -eq 'UserPrincipalName'} | Select-Object -ExpandProperty NewValue
$MatchedData = $NewUPN -match '\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' | Out-Null
$NewUPN = $Matches[0]
If ($OldUPN -ne $NewUPN) {
$ReportLine = [PSCustomObject][Ordered]@{
Timestamp = $Rec.CreatedDateTime
Actor = $Rec.userPrincipalName
Operation = $Rec.operation
OldValue = $OldUPN
NewValue = $NewUPN
}
$Report.Add($ReportLine)
}
}
}
$Report | Sort-Object {$_.Timestamp -as [datetime]} | Format-Table -AutoSize
$MsgFrom = 'Customer.Services@office365itpros.com'
# Define some variables used to construct the HTML content in the message body
# HTML header with styles
$HtmlHead="<html>
<style>
BODY{font-family: Arial; font-size: 10pt;}
H1{font-size: 22px;}
H2{font-size: 18px; padding-top: 10px;}
H3{font-size: 16px; padding-top: 8px;}
H4{font-size: 8px; padding-top: 4px;}
</style>"
$MsgSubject = "Audit Events for Your Review"
$ToRecipients = @{}
$ToRecipients.Add("emailAddress",@{'address'='tony.redmond@office365itpros.com'})
[array]$MsgTo = $ToRecipients
# Customize the message
$HtmlHeader = "<p><h2>Administrative alert: User Principal Name Updates in the last week</h2></p>"
# Add some content for the message - obviously, this is very customizable and should reflect what you want to say about the data being reported
$HtmlBody = "<h1>Please Check these audit events</h1><p></p>"
$HtmlBody = $HtmlBody + ($Report | ConvertTo-Html -Fragment)
$HtmlBody = $HtmlBody + "<p>These audit records are highlighted because of the impact a change to a user principal name can have on systems.</p>"
$HtmlBody = $HtmlBody + "<p><h4>Generated:</strong> $(Get-Date -Format 'dd-MMM-yyyy HH:mm')</h4></p>"
$HtmlMsg = $HtmlHead + $HtmlHeader + $HtmlBody + "<p>"
# Construct the message body
$MsgBody = @{}
$MsgBody.Add('Content', "$($HtmlMsg)")
$MsgBody.Add('ContentType','html')
$Message = @{}
$Message.Add('subject', $MsgSubject)
$Message.Add('toRecipients', $MsgTo)
$Message.Add('body', $MsgBody)
$Params = @{}
$Params.Add('message', $Message)
$Params.Add('saveToSentItems', $true)
$Params.Add('isDeliveryReceiptRequested', $true)
Send-MgUserMail -UserId $MsgFrom -BodyParameter $Params

Parameters

ParameterDefaultNotes
-LookbackDays7Number of days back to include in the audit log query.
-StartDate(Get-Date).AddDays(-7)Start of the reporting window.
-EndDate(Get-Date).AddDays(1)End of the reporting window.
Attribution