Simplify user tasks like bulk creation, updates, password resets, deletions, license checks & more — all from one place.
🚀 Launch ToolkitPassword changes are some of the most sensitive events in any identity system. Whether they’re legitimate self-service resets or part of an incident investigation, admins need visibility into who changed their password and when. This script queries Entra ID Audit Logs for the “Change password (self-service)” event under the UserManagement category, exports the results to a CSV, and emails the report to administrators or stakeholders automatically.
# ===== Entra ID Audit: "Change password (self-service)" (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: "User Password Change" 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 'Change password (self-service)' and activityDateTime ge $sinceIso"
# -------- Query Audit Logs (Directory Audits) ----------
$auditEntries = Get-MgAuditLogDirectoryAudit -All -Filter $filter `
-Property "activityDateTime,activityDisplayName,category,correlationId,result,resultReason,initiatedBy,targetResources"
# -------- 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 {
$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 (users whose passwords were changed)
$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 {}
}
[PSCustomObject]@{
ActivityDateTime = $e.ActivityDateTime
Activity = $e.ActivityDisplayName
Category = $e.Category
Result = $e.Result
ResultReason = $e.ResultReason
CorrelationId = $e.CorrelationId
InitiatedByName = $initiatorName
InitiatedByUPN = $initiatorUpn
TargetNames = ($targetNames -join "; ")
TargetUPNs = ($targetUpns -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_UserPasswordChange_{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: "User Password Change" (Last $DaysBack Days)</h3>
<p>Total events: <b>$totalEvents</b></p>
<p>Time window (UTC): since <b>$sinceIso</b></p>
<p>Attached CSV includes ActivityDateTime, Initiator, Targets, Result, and CorrelationId.</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 scopes or consent | Reconnect with AuditLog.Read.All & Mail.Send; ensure admin consent. |
Get-MgAuditLogDirectoryAudit not recognized | Graph module missing/outdated | Run Install-Module Microsoft.Graph -Scope CurrentUser; update if needed. |
Empty CSV | No “Change password (self-service)” events in timeframe | Increase $DaysBack or validate in Entra Admin Center. |
Email not sent | $FromUser not mailbox-enabled | Use a licensed mailbox-enabled sender. |
Split error on recipients | Incorrect .Split() usage | Use .Split(@(';',','), [StringSplitOptions]::RemoveEmptyEntries) as shown. |
Tracking password changes is essential for both security visibility and compliance. With this script, you can automatically fetch “Change password (self-service)” audit logs from Entra ID, export them to a CSV, and send them straight to your inbox. Schedule it for regular runs and enhance with alerting, and you’ll have a reliable guardrail for one of the most critical user activities in your tenant.
© m365corner.com. All Rights Reserved. Design by HTML Codex