Back to script library
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 Name
If ("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 5000
If ($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 = $Session
UserId = $AuditData.UserId
Activity = $AuditData.Operation
Status = $AuditData.ResultStatus
Timestamp = (Get-Date $Record.CreationDate -format 'dd-MMM-yyyy HH:mm:ss')
RecordType = $Record.RecordType
ClientIP = $AuditData.ClientIP
Id = $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 utf8
Write-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 reports
If (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