Entra / Microsoft 365 ยท Users & guests
Report daily sign-ins
Analyze and report daily sign-ins from the Entra ID sign-in audit log, including risky users, and email the results to tenant administrators.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-MgGraph -Scopes "AuditLog.Read.All", "User.Read.All", "Organization.Read.All", "IdentityRiskyUser.Read.All" -NoWelcome
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([string] $TenantId = "$TenantData.Id",[int] $LookbackDays = 1,[string] $StartDate = "$Today.AddDays(-$LookbackDays).ToString('yyyy-MM-ddT00:00:00Z')",[string] $EndDate = "$Today.ToString('yyyy-MM-ddT00:00:00Z')",[string] $DestinationEmailAddress = "")Connect-MgGraph -Scopes "AuditLog.Read.All", "User.Read.All", "Organization.Read.All", "IdentityRiskyUser.Read.All"# Get tenant identifier$TenantData = Get-MgOrganization# Set dates up to find what happened yesterday$Today = Get-DateWrite-Output "Retrieving interactive sign-in records from $StartDate to $EndDate"[array]$SignInRecords = Get-MgBetaAuditLogSignIn -All -Filter "createdDateTime ge $StartDate and createdDateTime lt $EndDate" -PageSize 999If ($SignInRecords.Count -eq 0) {Write-Output "No sign-in records found"Break} Else {Write-Output ("Found {0} sign-in records to process..." -f $SignInRecords.Count)$Report = [System.Collections.Generic.List[Object]]::new()}# Get Risky Users Information[array]$RiskyUsers = Get-MgRiskyUser | Where-Object {$_.RiskDetail -eq "none" -and $_.RiskState -eq "atRisk"} | Sort-Object {$_.RiskLastUpdatedDateTime -as [datetime]} -DescendingForEach ($RiskyUser in $RiskyUsers) {$ReportLine = [PSCustomObject]@{UserPrincipalName = $RiskyUser.UserPrincipalNameRiskLevel = $RiskyUser.RiskLevelRiskState = $RiskyUser.RiskStateRiskDetail = $RiskyUser.RiskDetailRiskLastUpdated = $RiskyUser.RiskLastUpdatedDateTimeDaysAtRisk = (New-TimeSpan -Start $RiskyUser.RiskLastUpdatedDateTime -End (Get-Date)).Days}$Report.Add($ReportLine)}[array]$AppSignIns = $SignInRecords | Where-Object { $_.AppDisplayName -ne $null -and $_.Status.ErrorCode -eq 0 } | Group-Object -Property AppDisplayName -NoElement[array]$UserSignIns = $SignInRecords | Where-Object { $_.UserDisplayName -ne $null -and $_.Status.ErrorCode -eq 0} | Group-Object -Property UserDisplayName -NoElement# Some failed sign-ins have a null userPrincipalName, notably when a tennat account fails with an attempt using B2B authentication with another tenant[array]$FailedSignIns = $SignInRecords | Where-Object { $_.Status.ErrorCode -ne 0 -and -not [string]::IsNullOrWhiteSpace($_.userPrincipalName) } | Group-Object -Property UserDisplayName -NoElement[array]$SingleFactorSignIns = $SignInRecords | Where-Object { $_.AuthenticationRequirement -eq "singleFactorAuthentication" -and $_.Status.ErrorCode -eq 0 }[array]$MfaSignIns = $SignInRecords | Where-Object { $_.ConditionalAccessStatus -eq "Success" -and $_.Status.ErrorCode -eq 0 }[array]$IncomingGuestMemberSignIns = $SignInRecords | Where-Object { $_.UserType -eq "Guest" -and $_.Status.ErrorCode -eq 0 -and $_.ResourceTenantId -eq $TenantId }[array]$OutboundGuestMemberSignIns = $SignInRecords | Where-Object { $_.UserType -eq "Guest" -and $_.Status.ErrorCode -eq 0 -and $_.ResourceTenantId -ne $TenantId }[array]$MfaUsers = $MfaSignIns | Group-Object userPrincipalName -NoElement | Select-Object -ExpandProperty Name[array]$SingleFactorUsers = $SingleFactorSignIns | Group-Object userPrincipalName -NoElement | Select-Object -ExpandProperty Name[array]$SingleFactorApps = $SingleFactorSignIns | Group-Object AppDisplayName -NoElement | Select-Object -ExpandProperty Name[int]$CASuccess = $SignInRecords | Where-Object { $_.ConditionalAccessStatus -eq "Success" -and $_.Status.ErrorCode -eq 0 } | Measure-Object | Select-Object -ExpandProperty Count[array]$CAFailureEvents = $SignInRecords | Where-Object { $_.ConditionalAccessStatus -eq "Failure" }[int]$CANotApplied = $SignInRecords | Where-Object { $_.ConditionalAccessStatus -eq "NotApplied" -and $_.Status.ErrorCode -eq 0 } | Measure-Object | Select-Object -ExpandProperty Count[int]$CAFailures = $CAFailureEvents.count[array]$CAFailureUsers = $CAFailureEvents | Group-Object UserDisplayName -NoElement | Select-Object -ExpandProperty Name# Build attachnent for the email using signin dataIf (Get-Module ImportExcel -ListAvailable) {$ExcelGenerated = $trueImport-Module ImportExcel -ErrorAction SilentlyContinue$ExcelOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\Daily Sign In Report.xlsx"If (Test-Path $ExcelOutputFile) {Remove-Item $ExcelOutputFile -ErrorAction SilentlyContinue}$SignInRecords | Export-Excel -Path $ExcelOutputFile -WorksheetName "Sign-in Records" -Title ("Daily Sign In Report {0}" -f (Get-Date -format 'dd-MMM-yyyy')) -TitleBold -TableName "SignInRecords" -AutoSize -AutoFilter$AttachmentFile = $ExcelOutputFile} Else {$CSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\Daily Sign In Report.CSV"$SignInRecords | Export-Csv -Path $CSVOutputFile -NoTypeInformation -Encoding Utf8$AttachmentFile = $CSVOutputFile}If ($ExcelGenerated) {Write-Output ("Excel worksheet output written to {0}" -f $ExcelOutputFile)} Else {Write-Output ("CSV output file written to {0}" -f $CSVOutputFile)}# Send the spreadsheet as an email attachment$EncodedAttachmentFile = [Convert]::ToBase64String([IO.File]::ReadAllBytes($AttachmentFile))$MsgAttachments = @(@{'@odata.type' = '#microsoft.graph.fileAttachment'Name = (Split-Path $AttachmentFile -Leaf)ContentBytes = $EncodedAttachmentFileContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})Write-Output ""Write-Output ("Daily Sign-In Report for {0}" -f (Get-Date $EndDate -format 'dd MMMM yyyy'))Write-Output ""Write-Output "Top ten Applications signed into during the day"Write-Output "-----------------------------------------------"$AppSignIns | Sort-Object Count -Descending | Select-Object -First 10 | Format-Table Name, Count -AutoSizeWrite-Output ""Write-Output "Top five users with successful sign-ins during the day"Write-Output "------------------------------------------------------"$UserSignIns | Sort-Object Count -Descending | Select-Object -First 5 | Format-Table Name, Count -AutoSizeWrite-Output ""Write-Output "Top five users with failed sign-ins during the day"Write-Output "--------------------------------------------------"$FailedSignIns | Sort-Object Count -Descending | Select-Object -First 5 | Format-Table Name, Count -AutoSizeWrite-Output ""Write-Output ("Number of users who signed in using single-factor authentication: {0}" -f ($SingleFactorUsers.count))Write-Output ("Number of users who signed in using multi-factor authentication: {0}" -f ($MfaUsers.count))Write-Output ("Accounts signed in using single-factor authentication: {0}" -f ($SingleFactorUsers -join ", "))Write-Output ("Apps accessed using single-factor authentication: {0}" -f ($SingleFactorApps -join ", "))Write-Output ("Incoming guest user sign-ins: {0}" -f ($IncomingGuestMemberSignIns.count))Write-Output ("Outbound guest user sign-ins: {0}" -f ($OutboundGuestMemberSignIns.count))Write-Output ("Number of sign-ins where Conditional Access succeeded: {0}" -f $CASuccess)Write-Output ("Number of sign-ins where Conditional Access was not applied: {0}" -f $CANotApplied)Write-Output ("Number of sign-ins where Conditional Access failed: {0}" -f $CAFailures)Write-Output ("Users with sign-ins where Conditional Access failed: {0}" -f ($CAFailureUsers -join ", "))Write-Output ""# Prepare HTML fragments for inclusion in the email body$TopApps = $AppSignIns | Sort-Object Count -Descending | Select-Object -First 10$AppHTML = $TopApps |Select-Object @{Name='Application';Expression={$_.Name}},@{Name='Sign In Count';Expression={$_.Count}} |ConvertTo-Html -As Table -Fragment$TopUsers = $UserSignIns | Sort-Object Count -Descending | Select-Object -First 5$UserHTML = $TopUsers |Select-Object @{Name='User';Expression={$_.Name}},@{Name='Sign In Count';Expression={$_.Count}} |ConvertTo-Html -As Table -Fragment$TopFailedSignInUsers = $FailedSignIns | Sort-Object Count -Descending | Select-Object -First 5$FailedSignInsHTML = $TopFailedSignInUsers |Select-Object @{Name='User';Expression={$_.Name}},@{Name='Failed Sign In Count';Expression={$_.Count}} |ConvertTo-Html -As Table -Fragment[string]$SingleFactorAppsList = $SingleFactorApps -join ", "[string]$SingleFactorUsersList = $SingleFactorUsers -join ", "[string]$CAFailureUsersList = $CAFailureUsers -join ", "$MsgFrom = 'Customer.Services@office365itpros.com'# Build the array of a single TO recipient detailed in a hash table - change the sender and recipient to the appropriate recipient for your tenant$ToRecipient = @{}$ToRecipient.Add("emailAddress",@{'address'=$DestinationEmailAddress})[array]$MsgTo = $ToRecipient# Define the message subject$MsgSubject = "Important: Daily Sign-in Report for the {0} tenant on {1}" -f $TenantData.DisplayName, (Get-Date $EndDate -format 'dd MMMM yyyy')# Create the HTML content$HtmlMsg = "</body></html><p>The output file for the <b>Daily Sign-in Report</b> is attached to this message. Please review the information at your convenience</p>"$HtmlMsg += "<h2>Daily Sign-ins Summary</h2>"$HtmlMsg += "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse:collapse;'>"$HtmlMsg += "<tr><th align='left'>Metric</th><th align='left'>Value</th></tr>"$HtmlMsg += "<tr><td>Total Sign-ins</td><td>{0}</td></tr>" -f $SignInRecords.Count$HtmlMsg += "<tr><td>Applications Signed Into</td><td>{0}</td></tr>" -f $AppSignIns.Count$HtmlMsg += "<tr><td>Users Signed In</td><td>{0}</td></tr>" -f $UserSignIns.Count$HtmlMsg += "<tr><td>Failed Sign-ins</td><td>{0}</ td></tr>" -f $FailedSignIns.Count$HtmlMsg += "<tr><td>Single-Factor Authentication Sign-ins</td><td>{0}</td></tr>" -f $SingleFactorSignIns.Count$HtmlMsg += "<tr><td>Users using Single-Factor Authentication</td><td>{0}</td></tr>" -f $SingleFactorUsers.Count$HtmlMsg += ("<tr><td>Users using Single-Factor Authentication</td><td>{0}</td></tr>" -f $SingleFactorUsersList)$HtmlMsg += "<tr><td>Applications signed into using Single-Factor Authentication</td><td>{0}</td></tr>" -f $SingleFactorApps.Count$HtmlMsg += ("<tr><td>Applications signed into with Single-Factor Authentication</td><td>{0}</td></tr>" -f $SingleFactorAppsList)$HtmlMsg += "<tr><td>Multi-Factor Authentication Sign-ins</td><td >{0}</td></tr>" -f $MfaSignIns.Count$HtmlMsg += "<tr><td>Multi-Factor Authentication Users</td><td>{0}</td></tr>" -f $MfaUsers.Count$HtmlMsg += "<tr><td>Conditional Access Success Sign-ins</td><td>{0}</td></tr>" -f $CASuccess$HtmlMsg += "<tr><td>Conditional Access Not Applied Sign-ins</td><td>{0}</td></tr>" -f $CANotApplied$HtmlMsg += "<tr><td>Conditional Access Failure Sign-ins</td><td>{0}</td></tr>" -f $CAFailures$htmlMsg += ("<tr><td>Users with Conditional Access Failure Sign-ins</td><td>{0}</td></tr>" -f $CAFailureUsersList)$HtmlMsg += "</table>"$HtmlMsg += "<p><h3>Top ten Applications signed into during the day</h3></p>"$HtmlMsg += $AppHTML$HtmlMsg += "<p><h3>Top five users with successful sign-ins during the day</h3></p>"$HtmlMsg += $UserHTML$HtmlMsg += "<p><h3>Top five users with failed sign-ins during the day</h3></p>"$HtmlMsg += $FailedSignInsHTML$htmlMsg += "<p><h3>Recent Risky Users Identified</h3></p>"$htmlMsg += $Report | ConvertTo-Html -As Table -Fragment$htmlMsg += "</body></html>"# Construct the message body$MsgBody = @{}$MsgBody.Add('Content', "$($HtmlMsg)")$MsgBody.Add('ContentType','html')# Build the parameters to submit the message$Message = @{}$Message.Add('subject', $MsgSubject)$Message.Add('toRecipients', $MsgTo)$Message.Add('body', $MsgBody)$Message.Add("attachments", $MsgAttachments)$EmailParameters = @{}$EmailParameters.Add('message', $Message)$EmailParameters.Add('saveToSentItems', $true)$EmailParameters.Add('isDeliveryReceiptRequested', $true)# Send the messageTry {Send-MgUserMail -UserId $MsgFrom -BodyParameter $EmailParameters -ErrorAction StopWrite-Output ("Daily sign-ins analysis report emailed to {0}" -f $ToRecipient.emailAddress.address)Write-Output "All done!"} Catch {Write-Output "Unable to send email"Write-Output $_.Exception.Message}
Parameters
ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.-LookbackDays1Number of days back to analyze sign-in activity (typically yesterday).-StartDate$Today.AddDays(-1).ToString('yyyy-MM-ddT00:00:00Z')Start of the reporting window.-EndDate$Today.ToString('yyyy-MM-ddT00:00:00Z')End of the reporting window.-DestinationEmailAddress""Email address that receives the generated report.Attribution
Author
Office365itpros