đź”§ New: User Management Graph PowerShell Toolkit

Simplify user tasks like bulk creation, updates, password resets, deletions, license checks & more — all from one place.

🚀 Launch Toolkit

Email Entra ID “Update User” Audit Events with Graph PowerShell

In 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.


i) Script

# ===== 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
                            

ii) How the Script Works

  1. Connects to Microsoft Graph using AuditLog.Read.All (to read directory audit logs) and Mail.Send (to send email).
  2. Sets the time window with $DaysBack and converts it to UTC ISO format ($sinceIso).
  3. Filters Entra ID Audit Logs for events where category = 'UserManagement' and activityDisplayName = 'Update user'.
  4. Shapes the results into objects that capture the event date, initiator, targets, and even changed properties (when available).
  5. Exports the results to CSV and sends an email with the report attached along with an HTML summary.

iii) Further Enhancements

  • Parameterize the activity so the script can also capture “Delete user”, “Add user”, or “Reset password” with minimal changes.
  • Filter by initiator to spot updates performed only by specific admin accounts.
  • Include IP/Client info from additional properties for more forensic detail.
  • Alert thresholds to trigger high-priority notifications if user updates exceed a set number.
  • Scheduled automation via Task Scheduler, Azure Automation, or Functions.

iv) Use Cases

  • Security monitoring: Detect unexpected or mass user updates.
  • Change management: Validate if HR-driven updates (titles, departments) are being synced correctly.
  • Compliance reporting: Provide auditors with logs of all account updates.
  • Forensics: Track user attribute changes during investigations.

v) Possible Errors & Solutions

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.

vi) Conclusion

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.


Graph PowerShell Explorer Widget

20 Graph PowerShell cmdlets with easily accessible "working" examples.


Permission Required

Example:


                


                


                

© m365corner.com. All Rights Reserved. Design by HTML Codex