Simplify user tasks like bulk creation, updates, password resets, deletions, license checks & more — all from one place.
🚀 Launch ToolkitNew hires, test accounts, automated joins—“Add user” events happen all the time. When auditors (or your security lead) ask “who created which accounts, and when?”, you don’t want to scramble through portals. This script pulls Entra ID (Azure AD) audit logs for “Add user” in the UserManagement category (last N days), exports the details to CSV, and emails the report to your admins/stakeholders—automatically.
# ===== Entra ID Audit: "Add 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" # One or more recipients (semicolon/comma-separated)
$Subject = 'Entra ID Audit: "Add 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 'Add user' 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 {
# 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 added)
$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_AddUser_{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: "Add user" (Last $DaysBack Days)</h3>
<p>Total events: <b>$totalEvents</b></p>
<p>Time window <b>$sinceIsod</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 admin consent | Reconnect with AuditLog.Read.All, Mail.Send; ensure admin consent granted. |
Get-MgAuditLogDirectoryAudit not recognized | Graph module missing/outdated | Install-Module Microsoft.Graph -Scope CurrentUser; Update-Module Microsoft.Graph. |
Email fails / no sent item | $FromUser not mailbox-enabled | Use a licensed mailbox-enabled account; verify Send-MgUserMail rights. |
Empty CSV | No “Add user” events in the window | Increase $DaysBack or validate in Entra admin center. |
Split error on recipients | Using incorrect .Split() overload | Use the fixed split: .Split(@(';',','), [StringSplitOptions]::RemoveEmptyEntries) |
This script turns raw Entra ID audit data into a clean, shareable CSV—so you can answer “who created which users?” instantly. Tweak the lookback, schedule it, or generalize for other activities, and you’ll have a lightweight but powerful identity governance signal delivered to stakeholders on repeat.
© m365corner.com. All Rights Reserved. Design by HTML Codex