Entra / Microsoft 365 · Users & guests
Report individual user sessions
An example script to report the events for an individual user session based on the session identifier assigned by Entra ID.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([int] $LookbackDays = 30,[string] $StartDate = (Get-Date).AddDays(-$LookbackDays) # Sign-in records only valid for 30 days)[array]$Modules = Get-Module | Select-Object -ExpandProperty NameIf ("ExchangeOnlineManagement" -notin $Modules) {Write-Error "The ExchangeOnlineManagement module is not loaded. Connecting now."Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop}Connect-MgGraph -Scopes AuditLog.Read.All, User.Read.All -NoWelcome$UserId = Read-Host "Enter the user ID to report sessions for " # Prompt for the user ID$UserId = $UserId.ToLower()Try {$User = Get-MgUser -UserId $UserId -ErrorAction Stop} Catch {Write-Error "User not found"Break}Write-Host ("Checking for sign-in sessions for user {0} ({1})" -f $User.DisplayName, $UserId)[array]$Logs = Get-MgBetaAuditLogSignIn -Filter "userPrincipalName eq '$UserId'" -All[array]$Sessions = $Logs | Group-Object SessionId -NoElement | Select-Object -ExpandProperty Name# Remove the blank session ID for old records$Sessions = $Sessions | Where-Object {([string]::IsNullOrEmpty($_)) -eq $false }If ($Sessions.Count -eq 0) {Write-Host "No sessions found for user $UserId"Break} Else {Write-Host ("Found {0} sessions for user {1}" -f $Sessions.Count, $UserId)}# Define HTML style$HtmlStyle = @"<style>body { font-family: Segoe UI, Arial, sans-serif; background: #f4f6f8; color: #222; }h1 { background: #0078d4; color: #fff; padding: 16px; border-radius: 6px 6px 0 0; margin-bottom: 0; }table { border-collapse: collapse; width: 100%; background: #fff; border-radius: 0 0 6px 6px; overflow: hidden; }th, td { padding: 10px 12px; text-align: left; }th { background: #e5eaf1; color: #222; }tr { background: #fff; color: #222; }tr:nth-child(even) { background: #f0f4fa; color: #222; }tr:hover { background: #d0e7fa; color: #222; }.caption { font-size: 14px; color: #555; margin-bottom: 12px; }</style>"@[string]$HTMLTable = $null$FullReport = [System.Collections.Generic.List[Object]]::new()ForEach ($Session in $Sessions) {Write-Host ("Searching for audit records for {0} in session {1}" -f $UserId, $Session)[array]$Records = Search-UnifiedAuditLog -Formatted -StartDate $StartDate -EndDate (Get-Date) -UserIds $UserId -freeText $Session -SessionCommand ReturnLargeSet -ResultSize 5000If ($Records.Count -eq 0) {Write-Host "No audit records found for session $Session"Continue} Else {$Records = $Records | Sort-Object Identity -Unique$Records = $Records | Sort-Object {$_.CreationDate -as [datetime]}Write-Host ("Found {0} audit records for session {1}" -f $Records.Count, $Session)$Report = [System.Collections.Generic.List[Object]]::new()}ForEach ($Record in $Records) {$AuditData = $Record.AuditData | ConvertFrom-Json$ReportLine = [PSCustomObject]@{SessionId = $SessionUserId = $AuditData.UserIdActivity = $AuditData.OperationStatus = $AuditData.ResultStatusTimestamp = (Get-Date $Record.CreationDate -format 'dd-MMM-yyyy HH:mm:ss')RecordType = $Record.RecordTypeClientIP = $AuditData.ClientIPId = $AuditData.Id}$Report.Add($ReportLine)}# Create the HTML content for the session records$HTMLSession = $Report | ConvertTo-HTML -Fragment -As Table$HTMLSessionHeading = "<h2>Audit Events found for Session ID: $Session</h2><p>"$HTMLTable = $HTMLTable + $HTMLSessionHeading + $HTMLSession$FullReport.AddRange($Report)}$HtmlReport = @"<html><head>$HtmlStyle<title>Audit Records found for $UserId</title></head><body><p>Report generated: <b>$((Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss 'UTC'"))</b></p><h1>Audit Records Found for $UserId</h1><p><p>Please check these audit events to validate that the account is not being used incorrectly.</p>$HtmlTable</body></html>"@Write-Host "Generating HTML report for audit records found for user $UserId"$OutputHTMLFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\AuditEventsForSessions.html"$HtmlReport | Out-File -FilePath $OutputHTMLFile -Encoding utf8Write-Host "A HTML report containing the audit records is available in $OutputHTMLFile"Write-Host "Generating an Excel or CSV report for audit records found for user $UserId"# Generate reportsIf (Get-Module ImportExcel -ListAvailable) {$ExcelGenerated = $True$ExcelTitle = ("Audit events for {0}" -f $UserId)Import-Module ImportExcel -ErrorAction SilentlyContinue$OutputXLSXFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\AuditEventsForSessions.xlsx"If (Test-Path $OutputXLSXFile) {Remove-Item $OutputXLSXFile -ErrorAction SilentlyContinue}$FullReport | Export-Excel -Path $OutputXLSXFile -WorksheetName "Audit Events" -Title $ExcelTitle -TitleBold -TableName "AuditEvents"} Else {$OutputCSVFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\AuditEventsForSessions.csv"$FullReport | Export-Csv -Path $OutputCSVFile -NoTypeInformation -Encoding Utf8}If ($ExcelGenerated) {Write-Host ("An Excel worksheet containing the report data is available in {0}" -f $OutputXLSXFile)} Else {Write-Host ("A CSV file containing the report data is available in {0}" -f $OutputCSVFile)}# Unsupported record types: PlannerTask, Discovery, AzureActiveDirectoryStsLogon# unsupported operations: Add member to group, MoveToDeletedItems# Session Id is only available after a successful authentication, so some records will not have a session ID.
Parameters
ParameterDefaultNotes
-LookbackDays30Number of days back to search sign-in records (Entra ID retains sign-ins for 30 days).-StartDate(Get-Date).AddDays(-30) # Sign-in records only valid for 30 daysStart of the reporting window.Attribution
Author
Office365itpros