Simplify user tasks like bulk creation, updates, password resets, deletions, license checks & more — all from one place.
🚀 Launch ToolkitIn large environments, user attributes change constantly—phone numbers, job titles, departments, and even license-related flags. While most of these updates are routine, some can have security or compliance implications. Having an automated way to track who updated which user, when, and what was changed provides both visibility and control. This script queries Entra ID Audit Logs for the “Update user” activity under the UserManagement category, exports the results to CSV, and emails the report to administrators or stakeholders.
# ===== Entra ID Audit: "Update user" (UserManagement) -> CSV -> Email =====
# Requires: Microsoft.Graph module
# Scopes: AuditLog.Read.All, Mail.Send
# -------- Email & Time Window ----------
$FromUser = "admin@contoso.com" # Sender (must have mailbox)
$ToList = "it-ops@contoso.com;secops@contoso.com" # Recipients (; or , separated)
$Subject = 'Entra ID Audit: "Update user" Activity Report'
$DaysBack = 7 # Lookback window (days)
$CsvOutDir = "$env:TEMP"
# -------- Connect to Microsoft Graph ----------
Import-Module Microsoft.Graph -ErrorAction Stop
Connect-MgGraph -Scopes "AuditLog.Read.All","Mail.Send"
# -------- Build filter (UTC ISO format) ----------
$sinceIso = (Get-Date).ToUniversalTime().AddDays(-1 * $DaysBack).ToString("o")
$filter = "category eq 'UserManagement' and activityDisplayName eq 'Update user' and activityDateTime ge $sinceIso"
# -------- Query Audit Logs (Directory Audits) ----------
$auditEntries = Get-MgAuditLogDirectoryAudit -All -Filter $filter `
-Property "activityDateTime,activityDisplayName,category,correlationId,result,resultReason,loggedByService,initiatedBy,targetResources,additionalDetails"
# -------- Shape rows for CSV ----------
$rows = foreach ($e in $auditEntries) {
# Initiator (user or app)
$initiatorUpn = $null; $initiatorName = $null
try {
if ($e.InitiatedBy.User) {
$initiatorUpn = $e.InitiatedBy.User.UserPrincipalName
$initiatorName = $e.InitiatedBy.User.DisplayName
} elseif ($e.InitiatedBy.App) {
$initiatorUpn = $e.InitiatedBy.App.AppId
$initiatorName = $e.InitiatedBy.App.DisplayName
} else {
# fallback for older SDK shapes
$iu = $e.InitiatedBy.AdditionalProperties['user']
$ia = $e.InitiatedBy.AdditionalProperties['app']
if ($iu) { $initiatorUpn = $iu['userPrincipalName']; $initiatorName = $iu['displayName'] }
elseif ($ia) { $initiatorUpn = $ia['appId']; $initiatorName = $ia['displayName'] }
}
} catch {}
# Targets (the users that were updated)
$targetNames = @()
$targetUpns = @()
foreach ($t in ($e.TargetResources | Where-Object { $_ })) {
try {
if ($t.UserPrincipalName) { $targetUpns += $t.UserPrincipalName }
elseif ($t.AdditionalProperties['userPrincipalName']) { $targetUpns += $t.AdditionalProperties['userPrincipalName'] }
if ($t.DisplayName) { $targetNames += $t.DisplayName }
elseif ($t.AdditionalProperties['displayName']) { $targetNames += $t.AdditionalProperties['displayName'] }
} catch {}
}
# Optional: summarize changed properties
$changedProps = @()
try {
foreach ($d in ($e.AdditionalDetails | Where-Object { $_ })) {
if ($d.Key -and $d.Value) { $changedProps += ("{0}={1}" -f $d.Key, $d.Value) }
}
} catch {}
[PSCustomObject]@{
ActivityDateTime = $e.ActivityDateTime
Activity = $e.ActivityDisplayName
Category = $e.Category
Result = $e.Result
ResultReason = $e.ResultReason
LoggedByService = $e.LoggedByService
CorrelationId = $e.CorrelationId
InitiatedByName = $initiatorName
InitiatedByUPN = $initiatorUpn
TargetNames = ($targetNames -join "; ")
TargetUPNs = ($targetUpns -join "; ")
ChangedDetails = ($changedProps -join "; ")
}
}
# -------- Export to CSV ----------
if (-not (Test-Path -Path $CsvOutDir)) { New-Item -ItemType Directory -Path $CsvOutDir | Out-Null }
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
$csvPath = Join-Path $CsvOutDir ("Entra_Audit_UpdateUser_{0}.csv" -f $ts)
$rows | Sort-Object ActivityDateTime -Descending | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
# -------- Prepare HTML Body ----------
$totalEvents = $rows.Count
$summaryHtml = @"
<html>
<body style='font-family:Segoe UI,Arial,sans-serif'>
<h3>Entra ID Audit Report: "Update user" (Last $DaysBack Days)</h3>
<p>Total events: <b>$totalEvents</b></p>
<p>Time window (UTC): <b>$sinceIso</b></p>
<p>Attached CSV includes ActivityDateTime, Initiator, Targets, Result, and ChangedDetails (when available).</p>
</body>
</html>
"@
# -------- Prepare Attachment ----------
$fileBytes = [System.IO.File]::ReadAllBytes($csvPath)
$base64Content = [System.Convert]::ToBase64String($fileBytes)
$csvFileName = [System.IO.Path]::GetFileName($csvPath)
$attachment = @{
"@odata.type" = "#microsoft.graph.fileAttachment"
name = $csvFileName
contentBytes = $base64Content
contentType = "text/csv"
}
# -------- Build recipients array (split on ; or ,) ----------
$recipients = @()
$ToList.Split(@(';', ','), [System.StringSplitOptions]::RemoveEmptyEntries) | ForEach-Object {
$addr = $_.Trim()
if ($addr) { $recipients += @{ emailAddress = @{ address = $addr } } }
}
# -------- Prepare and Send Email ----------
$mail = @{
message = @{
subject = "$Subject"
body = @{
contentType = "HTML"
content = $summaryHtml
}
toRecipients = $recipients
attachments = @($attachment)
}
saveToSentItems = $true
}
Send-MgUserMail -UserId $FromUser -BodyParameter $mail
Write-Host "Done. CSV saved at: $csvPath" -ForegroundColor Green
Error | Cause | Solution |
---|---|---|
Authorization_RequestDenied | Missing Graph permissions | Reconnect with AuditLog.Read.All and Mail.Send, ensure admin consent. |
Get-MgAuditLogDirectoryAudit not recognized | Graph module not installed/outdated | Install/Update Microsoft.Graph and re-import. |
Empty CSV | No “Update user” events in time window | Increase $DaysBack or validate in Entra Admin Center. |
Email not sent | $FromUser not mailbox-enabled | Ensure sender has a mailbox license and rights to send. |
Split error on recipients | Incorrect .Split() syntax | Use .Split(@(';',','), [StringSplitOptions]::RemoveEmptyEntries) as shown. |
Tracking user update events is critical to maintaining visibility over account changes in Entra ID. This script automates the collection of “Update user” audit events, exports them to a clean CSV, and emails them to your admins or stakeholders. By scheduling and enhancing it, you can turn this into a reliable control point for identity governance, compliance, and security operations.
© m365corner.com. All Rights Reserved. Design by HTML Codex