Back to script library
Entra / Microsoft 365 · Users & guests

Get Graph user statistics report

A sample script showing how to gather user activity information from the Graph and assemble it into one report.

Connect & set up

Run these once per session. All scopes are read-only unless the script makes changes.

# Review required modules and connection steps before running.
# Connect to Microsoft Graph or Exchange Online as needed for this script.

Run it

The main script. Copy it, or download the .ps1 and run it from your console.

param(
[string] $TenantId = "",
[string] $AppId = ""
)
function Get-GraphData {
# Based on https://danielchronlund.com/2018/11/19/fetch-data-from-microsoft-graph-with-powershell-paging-support/
# GET data from Microsoft Graph.
param (
[parameter(Mandatory = $true)]
$AccessToken,
[parameter(Mandatory = $true)]
$Uri
)
# Check if authentication was successful.
if ($AccessToken) {
$Headers = @{
'Content-Type' = "application\json"
'Authorization' = "Bearer $AccessToken"
'ConsistencyLevel' = "eventual" }
# Create an empty array to store the result.
$QueryResults = @()
# Invoke REST method and fetch data until there are no pages left.
do {
$Results = ""
$StatusCode = ""
do {
try {
$Results = Invoke-RestMethod -Headers $Headers -Uri $Uri -UseBasicParsing -Method "GET" -ContentType "application/json"
$StatusCode = $Results.StatusCode
} catch {
$StatusCode = $_.Exception.Response.StatusCode.value__
if ($StatusCode -eq 429) {
Write-Warning "Got throttled by Microsoft. Sleeping for 45 seconds..."
Start-Sleep -Seconds 45
}
else {
Write-Error $_.Exception
}
}
} while ($StatusCode -eq 429)
if ($Results.value) {
$QueryResults += $Results.value
}
else {
$QueryResults += $Results
}
$uri = $Results.'@odata.nextlink'
} until (!($uri))
# Return the result.
$QueryResults
}
else {
Write-Error "No Access Token"
}
}
Clear-Host
# Define the values applicable for the application used to connect to the Graph (change these for your tenant)
$AppSecret = 't_rkvIn1oZ1cNceUBvJ2or1lrrIsb*:='
$OutputCSV = "c:\temp\Office365TenantUsage.csv"
$StartTime1 = Get-Date
# Construct URI and body needed for authentication
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
client_id = $AppId
scope = "https://graph.microsoft.com/.default"
client_secret = $AppSecret
grant_type = "client_credentials" }
# Get OAuth 2.0 Token
$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
# Unpack Access Token
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
# Base URL
$headers = @{Authorization = "Bearer $token"}
Write-Host "Fetching Teams user activity data from the Graph..."
# The Graph returns information in CSV format. We convert it to allow the data to be more easily processed by PowerShell
# Get Teams Usage Data - the replace parameter is there to remove three odd leading characters () in the CSV data returned by the
$TeamsUserReportsURI = "https://graph.microsoft.com/v1.0/reports/getTeamsUserActivityUserDetail(period='D180')"
[array]$TeamsUserData = (Invoke-RestMethod -Uri $TeamsUserReportsURI -Headers $Headers -Method Get -ContentType "application/json") -Replace "...Report Refresh Date", "Report Refresh Date" | ConvertFrom-Csv
Write-Host "Fetching OneDrive for Business user activity data from the Graph..."
# Get OneDrive for Business data
$OneDriveUsageUri = "https://graph.microsoft.com/v1.0/reports/getOneDriveUsageAccountDetail(period='D180')"
[array]$OneDriveData = (Get-GraphData -Uri $OneDriveUsageUri -AccessToken $Token) -Replace "...Report Refresh Date", "Report Refresh Date" | ConvertFrom-Csv
Write-Host "Fetching Exchange Online user activity data from the Graph..."
# Get Exchange Activity Data
$EmailReportsUri = "https://graph.microsoft.com/v1.0/reports/getEmailActivityUserDetail(period='D180')"
[array]$EmailData = (Get-GraphData -Uri $EmailReportsUri -AccessToken $Token) -Replace "...Report Refresh Date", "Report Refresh Date" | ConvertFrom-Csv
# Get Exchange Storage Data
$MailboxUsageReportsUri = "https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail(period='D180')"
[array]$MailboxUsage = (Get-GraphData -Uri $MailboxUsageReportsUri -AccessToken $Token) -Replace "...Report Refresh Date", "Report Refresh Date" | ConvertFrom-Csv
Write-Host "Fetching SharePoint Online user activity data from the Graph..."
# Get SharePoint usage data
$SPOUsageReportsUri = "https://graph.microsoft.com/v1.0/reports/getSharePointActivityUserDetail(period='D180')"
[array]$SPOUsage = (Get-GraphData -Uri $SPOUsageReportsUri -AccessToken $Token) -Replace "...Report Refresh Date", "Report Refresh Date" | ConvertFrom-Csv
Write-Host "Fetching Yammer user activity data from the Graph..."
# Get Yammer usage data
$YammerUsageReportsUri = "https://graph.microsoft.com/v1.0/reports/getYammerActivityUserDetail(period='D180')"
[array]$YammerUsage = (Get-GraphData -Uri $YammerUsageReportsUri -AccessToken $Token) -Replace "...Report Refresh Date", "Report Refresh Date" | ConvertFrom-Csv
# Create hash table for user sign in data
$UserSignIns = @{}
# And hash table for the output data
$DataTable = @{}
# Get User sign in data
Write-Host "Fetching user sign-in data from the Graph..."
$Uri = "https://graph.microsoft.com/V1.0/users?`$select=displayName,userPrincipalName, mail, id, CreatedDateTime,signInActivity,UserType&`$top=999"
[array]$SignInData = Get-GraphData -Uri $Uri -AccessToken $Token
# Filter out guest member sign-in records
[array]$UserSignInData = $SignInData | Where-Object {$_.UserType -eq "Member"} | Sort-Object UserPrincipalName -Unique
# Update the user sign in hash table
ForEach ($U in $UserSignInData) {
If ($U.SignInActivity.LastSuccessfulSignInDateTime) {
$LastSignInDate = Get-Date($U.SignInActivity.LastSuccessfulSignInDateTime) -format 'dd-MMM-yyyy HH:mm'
$UserSignIns.Add([String]$U.UserPrincipalName, $LastSignInDate)
} Else {
$UserSignIns.Add([String]$U.UserPrincipalName,$Null)
}
}
$StartTime2 = Get-Date
Write-Host "Processing activity data fetched from the Graph..."
# Process Teams Data
ForEach ($T in $TeamsUserData) {
If ([string]::IsNullOrEmpty($T."Last Activity Date")) {
$TeamsLastActivity = "No activity"
$TeamsDaysSinceActive = "N/A" }
Else {
$TeamsLastActivity = Get-Date($T."Last Activity Date") -format "dd-MMM-yyyy"
$TeamsDaysSinceActive = (New-TimeSpan($TeamsLastActivity)).Days }
$ReportLine = [PSCustomObject] @{
TeamsUPN = $T."User Principal Name"
TeamsLastActive = $TeamsLastActivity
TeamsDaysSinceActive = $TeamsDaysSinceActive
TeamsReportDate = Get-Date($T."Report Refresh Date") -format "dd-MMM-yyyy"
TeamsLicense = $T."Assigned Products"
TeamsChannelChats = $T."Team Chat Message Count"
TeamsPrivateChats = $T."Private Chat Message Count"
TeamsCalls = $T."Call Count"
TeamsMeetings = $T."Meeting Count"
TeamsRecordType = "Teams"}
$DataTable[$T."User Principal Name"] = $ReportLine}
# Process Exchange Data
ForEach ($E in $EmailData) {
$ExoDaysSinceActive = $Null
If ([string]::IsNullOrEmpty($E."Last Activity Date")) {
$ExoLastActivity = "No activity"
$ExoDaysSinceActive = "N/A" }
Else {
$ExoLastActivity = Get-Date($E."Last Activity Date") -format "dd-MMM-yyyy"
$ExoDaysSinceActive = (New-TimeSpan($ExoLastActivity)).Days }
$ReportLine = [PSCustomObject] @{
ExoUPN = $E."User Principal Name"
ExoDisplayName = $E."Display Name"
ExoLastActive = $ExoLastActivity
ExoDaysSinceActive = $ExoDaysSinceActive
ExoReportDate = Get-Date($E."Report Refresh Date") -format "dd-MMM-yyyy"
ExoSendCount = [int]$E."Send Count"
ExoReadCount = [int]$E."Read Count"
ExoReceiveCount = [int]$E."Receive Count"
ExoIsDeleted = $E."Is Deleted"
ExoRecordType = "Exchange Activity"}
[Array]$ExistingData = $DataTable[$E."User Principal Name"]
[Array]$NewData = $ExistingData + $ReportLine
$DataTable[$E."User Principal Name"] = $NewData }
ForEach ($M in $MailboxUsage) {
If ([string]::IsNullOrEmpty($M."Last Activity Date")) {
$ExoLastActivity = "No activity" }
Else {
$ExoLastActivity = Get-Date($M."Last Activity Date") -format "dd-MMM-yyyy"
$ExoDaysSinceActive = (New-TimeSpan($ExoLastActivity)).Days }
$ReportLine = [PSCustomObject] @{
MbxUPN = $M."User Principal Name"
MbxDisplayName = $M."Display Name"
MbxLastActive = $ExoLastActivity
MbxDaysSinceActive = $ExoDaysSinceActive
MbxReportDate = Get-Date($M."Report Refresh Date") -format "dd-MMM-yyyy"
MbxQuotaUsed = [Math]::Round($M."Storage Used (Byte)"/1GB,2)
MbxItems = [int]$M."Item Count"
MbxRecordType = "Exchange Storage"}
[Array]$ExistingData = $DataTable[$M."User Principal Name"]
[Array]$NewData = $ExistingData + $ReportLine
$DataTable[$M."User Principal Name"] = $NewData }
# SharePoint data
ForEach ($S in $SPOUsage) {
If ([string]::IsNullOrEmpty($S."Last Activity Date")) {
$SPOLastActivity = "No activity"
$SPODaysSinceActive = "N/A" }
Else {
$SPOLastActivity = Get-Date($S."Last Activity Date") -format "dd-MMM-yyyy"
$SPODaysSinceActive = (New-TimeSpan ($SPOLastActivity)).Days }
$ReportLine = [PSCustomObject] @{
SPOUPN = $S."User Principal Name"
SPOLastActive = $SPOLastActivity
SPODaysSinceActive = $SPODaysSinceActive
SPOViewedEdited = [int]$S."Viewed or Edited File Count"
SPOSyncedFileCount = [int]$S."Synced File Count"
SPOSharedExt = [int]$S."Shared Externally File Count"
SPOSharedInt = [int]$S."Shared Internally File Count"
SPOVisitedPages = [int]$S."Visited Page Count"
SPORecordType = "SharePoint Usage"}
[Array]$ExistingData = $DataTable[$S."User Principal Name"]
[Array]$NewData = $ExistingData + $ReportLine
$DataTable[$S."User Principal Name"] = $NewData }
# OneDrive for Business data
ForEach ($O in $OneDriveData) {
$OneDriveLastActivity = $Null
If ([string]::IsNullOrEmpty($O."Last Activity Date")) {
$OneDriveLastActivity = "No activity"
$OneDriveDaysSinceActive = "N/A" }
Else {
$OneDriveLastActivity = Get-Date($O."Last Activity Date") -format "dd-MMM-yyyy"
$OneDriveDaysSinceActive = (New-TimeSpan($OneDriveLastActivity)).Days }
$ReportLine = [PSCustomObject] @{
ODUPN = $O."Owner Principal Name"
ODDisplayName = $O."Owner Display Name"
ODLastActive = $OneDriveLastActivity
ODDaysSinceActive = $OneDriveDaysSinceActive
ODSite = $O."Site URL"
ODFileCount = [int]$O."File Count"
ODStorageUsed = [Math]::Round($O."Storage Used (Byte)"/1GB,4)
ODQuota = [Math]::Round($O."Storage Allocated (Byte)"/1GB,2)
ODRecordType = "OneDrive Storage"}
[Array]$ExistingData = $DataTable[$O."Owner Principal Name"]
[Array]$NewData = $ExistingData + $ReportLine
$DataTable[$O."Owner Principal Name"] = $NewData }
# Yammer Data
ForEach ($Y in $YammerUsage) {
If ([string]::IsNullOrEmpty($Y."Last Activity Date")) {
$YammerLastActivity = "No activity"
$YammerDaysSinceActive = "N/A" }
Else {
$YammerLastActivity = Get-Date($Y."Last Activity Date") -format "dd-MMM-yyyy"
$YammerDaysSinceActive = (New-TimeSpan ($YammerLastActivity)).Days
}
$ReportLine = [PSCustomObject] @{
YUPN = $Y."User Principal Name"
YDisplayName = $Y."Display Name"
YLastActive = $YammerLastActivity
YDaysSinceActive = $YammerDaysSinceActive
YPostedCount = [int]$Y."Posted Count"
YReadCount = [int]$Y."Read Count"
YLikedCount = [int]$Y."Liked Count"
YRecordType = "Yammer Usage"
}
[Array]$ExistingData = $DataTable[$Y."User Principal Name"]
[Array]$NewData = $ExistingData + $ReportLine
$DataTable[$Y."User Principal Name"] = $NewData }
Clear-Host
# Create set of users that we've collected data for - each of these users will be in the $DataTable with some information.
[System.Collections.ArrayList]$Users = @()
ForEach ($UserPrincipalName in $DataTable.Keys) {
If ($DataTable[$UserPrincipalName]) { #Info exists in datatable
$obj = [PSCustomObject]@{
UPN = $UserPrincipalName}
$Users.add($obj) | Out-Null }
}
$StartTime3 = Get-Date
# Set up progress bar
$ProgressDelta = 100/($Users.Count); $PercentComplete = 0; $UserNumber = 0
$OutData = [System.Collections.Generic.List[Object]]::new() # Create merged output file
# Process each user to extract Exchange, Teams, OneDrive, SharePoint, and Yammer statistics for their activity
ForEach ($UserPrincipalName in $Users) {
$U = $UserPrincipalName.UPN
$UserNumber++
$CurrentStatus = $U + " ["+ $UserNumber +"/" + $Users.Count + "]"
Write-Progress -Activity "Extracting information for user" -Status $CurrentStatus -PercentComplete $PercentComplete
$PercentComplete += $ProgressDelta
$UserData = $DataTable[$U] # Extract data for the user - everything is in a single keyed access to the hash table
# Process Exchange Data
[string]$ExoUPN = (Out-String -InputObject $UserData.ExoUPN).Trim()
[string]$ExoLastActive = (Out-String -InputObject $UserData.ExoLastActive).Trim()
If ([string]::IsNullOrEmpty($ExoUPN) -or $ExoLastActive -eq "No Activity") {
$ExoDaysSinceActive = "N/A"
$ExoLastActive = "No Activity"
} Else {
[string]$ExoLastActive = (Out-String -InputObject $UserData.ExoLastActive).Trim()
[string]$ExoDaysSinceActive = (Out-String -InputObject $UserData.ExoDaysSinceActive).Trim()
}
# Parse OneDrive for Business usage data
[string]$ODUPN = (Out-String -InputObject $UserData.ODUPN).Trim()
[string]$ODLastActive = (Out-String -InputObject $UserData.ODLastActive).Trim() # Possibility of a second OneDrive account for some users.
If (($ODLastActive -Like "*No Activity*") -or ([string]::IsNullOrEmpty($ODLastActive))) {$ODLastActive = "No Activity"} # this is a hack until I figure out a better way to handle the situation
If ($null -eq [string]::IsNullOrEmpty($ODUPN) -or $ODLastActive -eq "No Activity") {
[string]$ODDaysSinceActive = "N/A"
[string]$ODLastActive = "No Activity"
$ODFiles = 0
$ODStorage = 0
$ODQuota = 1024
} Else {
[string]$ODDaysSinceActive = (Out-String -InputObject $UserData.ODDaysSinceActive).Trim()
[string]$ODLastActive = (Out-String -InputObject $UserData.ODLastActive).Trim()
[string]$ODFiles = (Out-String -InputObject $UserData.ODFileCount).Trim()
[string]$ODStorage = (Out-String -InputObject $UserData.ODStorageUsed).Trim()
[string]$ODQuota = (Out-String -InputObject $UserData.ODQuota).Trim()
}
# Parse Yammer usage data; Yammer isn't used everywhere, so make sure that we record zero data
[string]$YUPN = (Out-String -InputObject $UserData.YUPN).Trim()
[string]$YammerLastActive = (Out-String -InputObject $UserData.YLastActive).Trim()
If (([string]::IsNullOrEmpty($YUPN) -or ($YammerLastActive -eq "No Activity"))) {
[string]$YammerLastActive = "No Activity"
[string]$YammerDaysSinceActive = "N/A"
$YammerPosts = 0
$YammerReads = 0
$YammerLikes = 0
} Else {
$YammerDaysSinceActive = (Out-String -InputObject $UserData.YDaysSinceActive).Trim()
$YammerPosts = (Out-String -InputObject $UserData.YPostedCount).Trim()
$YammerReads = (Out-String -InputObject $UserData.YReadCount).Trim()
$YammerLikes = (Out-String -InputObject $UserData.YLikedCount).Trim()
}
If ($UserData.TeamsDaysSinceActive -gt 0) {
[string]$TeamsDaysSinceActive = (Out-String -InputObject $UserData.TeamsDaysSinceActive).Trim()
[string]$TeamsLastActive = (Out-String -InputObject $UserData.TeamsLastActive).Trim()
} Else {
[string]$TeamsDaysSinceActive = "N/A"
[string]$TeamsLastActive = "No Activity"
}
If ($UserData.SPODaysSinceActive -gt 0) {
[string]$SPODaysSinceActive = (Out-String -InputObject $UserData.SPODaysSinceActive).Trim()
[string]$SPOLastActive = (Out-String -InputObject $UserData.SPOLastActive).Trim()
} Else {
[string]$SPODaysSinceActive = "N/A"
[string]$SPOLastActive = "No Activity"
}
# Fetch the sign in data if available
$LastAccountSignIn = $Null; $DaysSinceSignIn = 0
$LastAccountSignIn = $UserSignIns.Item($U)
If ($null -eq $LastAccountSignIn) {
$LastAccountSignIn = "No sign in data found"; $DaysSinceSignIn = "N/A"
} Else {
$DaysSinceSignIn = (New-TimeSpan($LastAccountSignIn)).Days
}
# Figure out if the account is used
[int]$ExoDays = 365; [int]$TeamsDays = 365; [int]$SPODays = 365; [int]$ODDays = 365; [int]$YammerDays = 365
# Base is 2 if someuse uses the five workloads because the Graph is usually 2 days behind, but we have some N/A values for days used
If ($ExoDaysSinceActive -ne "N/A") {$ExoDays = $ExoDaysSinceActive -as [int]}
If ($TeamsDaysSinceActive -eq "N/A") {$TeamsDays = 365} Else {$TeamsDays = $TeamsDaysSinceActive -as [int]}
If ($SPODaysSinceActive -eq "N/A") {$SPODays = 365} Else {$SPODays = $SPODaysSinceActive -as [int]}
If ($ODDaysSinceActive -eq "N/A") {$ODDays = 365} Else {$ODDays = $ODDaysSinceActive -as [int]}
If ($YammerDaysSinceActive -eq "N/A") {$YammerDays = 365} Else {$YammerDays = $YammerDaysSinceActive -as [int]}
# Average days per workload used...
$AverageDaysSinceUse = [Math]::Round((($ExoDays + $TeamsDays + $SPODays + $ODDays + $YammerDays)/5),2)
Switch ($AverageDaysSinceUse) { # Figure out if account is used
({$PSItem -le 8}) { $AccountStatus = "Heavy usage" }
({$PSItem -ge 9 -and $PSItem -le 50} ) { $AccountStatus = "Moderate usage" }
({$PSItem -ge 51 -and $PSItem -le 120} ) { $AccountStatus = "Poor usage" }
({$PSItem -ge 121 -and $PSItem -le 300 } ) { $AccountStatus = "Review account" }
default { $AccountStatus = "Account unused" }
} # End Switch
# And an override if someone has been active in just one workload in the last 14 days
[int]$DaysCheck = 14 # Set this to your chosen value if you want to use a different period.
If (($ExoDays -le $DaysCheck) -or ($TeamsDays -le $DaysCheck) -or ($SPODays -le $DaysCheck) -or ($ODDays -le $DaysCheck) -or ($YammerDays -le $DaysCheck)) {
$AccountStatus = "Account in use"}
If ((![string]::IsNullOrEmpty($ExoUPN))) {
# Build a line for the report file with the collected data for all workloads and write it to the list
$OutLine = [PSCustomObject] @{
UPN = $U
DisplayName = (Out-String -InputObject $UserData.ExoDisplayName).Trim()
Status = $AccountStatus
LastSignIn = $LastAccountSignIn
DaysSinceSignIn = $DaysSinceSignIn
EXOLastActive = $ExoLastActive
EXODaysSinceActive = $ExoDaysSinceActive
EXOQuotaUsed = (Out-String -InputObject $UserData.MbxQuotaUsed).Trim()
EXOItems = (Out-String -InputObject $UserData.MbxItems).Trim()
EXOSendCount = (Out-String -InputObject $UserData.ExoSendCount).Trim()
EXOReadCount = (Out-String -InputObject $UserData.ExoReadCount).Trim()
EXOReceiveCount = (Out-String -InputObject $UserData.ExoReceiveCount).Trim()
TeamsLastActive = $TeamsLastActive
TeamsDaysSinceActive = $TeamsDays
TeamsChannelChat = (Out-String -InputObject $UserData.TeamsChannelChats).Trim()
TeamsPrivateChat = (Out-String -InputObject $UserData.TeamsPrivateChats).Trim()
TeamsMeetings = (Out-String -InputObject $UserData.TeamsMeetings).Trim()
TeamsCalls = (Out-String -InputObject $UserData.TeamsCalls).Trim()
SPOLastActive = $SPOLastActive
SPODaysSinceActive = $SPODays
SPOViewedEditedFiles = (Out-String -InputObject $UserData.SPOViewedEdited).Trim()
SPOSyncedFiles = (Out-String -InputObject $UserData.SPOSyncedFileCount).Trim()
SPOSharedExtFiles = (Out-String -InputObject $UserData.SPOSharedExt).Trim()
SPOSharedIntFiles = (Out-String -InputObject $UserData.SPOSharedInt).Trim()
SPOVisitedPages = (Out-String -InputObject $UserData.SPOVisitedPages).Trim()
OneDriveLastActive = $ODLastActive
OneDriveDaysSinceActive = $ODDaysSinceActive
OneDriveFiles = $ODFiles
OneDriveStorage = $ODStorage
OneDriveQuota = $ODQuota
YammerLastActive = $YammerLastActive
YammerDaysSinceActive = $YammerDaysSinceActive
YammerPosts = $YammerPosts
YammerReads = $YammerReads
YammerLikes = $YammerLikes
License = (Out-String -InputObject $UserData.TeamsLicense).Trim()
OneDriveSite = (Out-String -InputObject $UserData.ODSite).Trim()
IsDeleted = (Out-String -InputObject $UserData.ExoIsDeleted).Trim()
EXOReportDate = (Out-String -InputObject $UserData.ExoReportDate).Trim()
TeamsReportDate = (Out-String -InputObject $UserData.TeamsReportDate).Trim()
UsageFigure = $AverageDaysSinceUse }
$OutData.Add($OutLine) }
} #End processing user data
Clear-Host
$StartTime4 = Get-Date
$GraphTime = $StartTime2 - $StartTime1
$PrepTime = $StartTime3 - $StartTime2
$ReportTime = $StartTime4 - $StartTime3
$ScriptTime = $StartTime4 - $StartTime1
$AccountsPerMinute = [math]::Round(($Outdata.count/($ScriptTime.TotalSeconds/60)),2)
$GraphElapsed = $GraphTime.Minutes.ToString() + ":" + $GraphTime.Seconds.ToString()
$PrepElapsed = $PrepTime.Minutes.ToString() + ":" + $PrepTime.Seconds.ToString()
$ReportElapsed = $ReportTime.Minutes.ToString() + ":" + $ReportTime.Seconds.ToString()
$ScriptElapsed = $ScriptTime.Minutes.ToString() + ":" + $ScriptTime.Seconds.ToString()
Write-Host " "
Write-Host "Statistics for Graph Report Script V2.0"
Write-Host "---------------------------------------"
Write-Host "Time to fetch data from Microsoft Graph:" $GraphElapsed
Write-Host "Time to prepare date for processing: " $PrepElapsed
Write-Host "Time to create report from data: " $ReportElapsed
Write-Host "Total time for script: " $ScriptElapsed
Write-Host "Total accounts processed: " $Outdata.count
Write-Host "Accounts processsed per minute: " $AccountsPerMinute
Write-Host " "
Write-Host "Output CSV file available in " $OutputCSV
$OutData | Sort-Object {$_.ExoLastActive -as [DateTime]} -Descending | Out-GridView
$OutData | Sort-Object $AccountStatus | Export-CSV $OutputCSV -NoTypeInformation

Parameters

ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.
-AppId""Application (client) ID for the app registration used to connect.
Attribution