Find obsolete guest accounts by activity (v2)
Perform an activity-based analysis of Entra ID guest user accounts and highlight accounts that are not being used. Requires Azure AD Preview and Exchange Online modules.
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([int] $LookbackDays = 90,[string] $StartDate = "Get-Date(Get-Date).AddDays(-$LookbackDays) #For audit log",[string] $EndDate = "Get-Date; $Active = 0; $EmailActive = 0; $AuditRec = 0; $GNo = 0")$ModulesLoaded = Get-Module | Select-Object -ExpandProperty NameIf (!($ModulesLoaded -match "ExchangeOnlineManagement")) {Write-Host "Please connect to the Exchange Online Management module and then restart the script"; break}If (!($ModulesLoaded -match "AzureADPreview")) {Write-Host "Please connect to the Azure AD Preview module and then restart the script"; break}# OK, we seem to be fully connected to Exchange Online and Azure AD# Start by finding all Guest AccountsWrite-Host "Finding Guest Accounts"[array]$Guests = (Get-AzureADUser -Filter "UserType eq 'Guest'" -All $True | Select-Object Displayname, UserPrincipalName, Mail, ObjectId |`Sort-Object DisplayName)If (!($Guests)) { Write-Host "No guest accounts can be found - exiting" ; break }$StartDate2 = Get-Date(Get-Date).AddDays(-$LookbackDays) #For message trace$Report = [System.Collections.Generic.List[Object]]::new() # Create output file for reportClear-Host; $GNo = 0Write-Host $Guests.Count "guest accounts found. Checking their activity..."ForEach ($G in $Guests) {$GNo++$ProgressBar = "Processing guest " + $G.DisplayName + " (" + $GNo + " of " + $Guests.Count + ")"Write-Progress -Activity "Checking Azure Active Directory Guest Accounts for activity" -Status $ProgressBar -PercentComplete ($GNo/$Guests.Count*100)$LastAuditRecord = $Null; $GroupNames = $Null; $LastAuditAction = $Null; $ReviewFlag = $False# Search for audit records for this user[array]$Recs = (Search-UnifiedAuditLog -UserIds $G.Mail, $G.UserPrincipalName -Operations UserLoggedIn, SecureLinkUsed, TeamsSessionStarted -StartDate $StartDate -EndDate $EndDate -ResultSize 1)If ($Recs) { # We found some audit records$LastAuditRecord = $Recs[0].CreationDate; $LastAuditAction = $Recs[0].Operations; $AuditRec++} Else {$LastAuditRecord = "None found"; $LastAuditAction = "N/A"}# Check email tracking logs because guests might receive email through membership of Outlook Groups. Email address must be valid for the check to workIf ($null -ne $G.Mail) {$EmailRecs = (Get-MessageTrace -StartDate $StartDate2 -EndDate $EndDate -Recipient $G.Mail)}If ($EmailRecs.Count -gt 0) {$EmailActive++}# Find what Microsoft 365 Groups the guest belongs to$GroupNames = $Null$Dn = (Get-ExoRecipient -Identity $G.UserPrincipalName).DistinguishedNameIf ($Dn -like "*'*") {$DNNew = "'" + "$($dn.Replace("'","''''"))" + "'"$Cmd = "Get-Recipient -Filter 'Members -eq '$DNnew'' -RecipientTypeDetails GroupMailbox | Select DisplayName, ExternalDirectoryObjectId"$GuestGroups = Invoke-Expression $Cmd} Else {$GuestGroups = (Get-Recipient -Filter "Members -eq '$Dn'" -RecipientTypeDetails GroupMailbox | Select-Object DisplayName, ExternalDirectoryObjectId) }If ($null -ne $GuestGroups) {$GroupNames = $GuestGroups.DisplayName -join ", " }# Figure out the domain the guest is from so that we can report this information$Domain = $G.Mail.Split("@")[1]# Figure out age of guest account in days using the creation date in the extension properties of the guest account$CreationDate = (Get-AzureADUserExtension -ObjectId $G.ObjectId).get_item("createdDateTime")$AccountAge = ($CreationDate | New-TimeSpan).Days# Find if there's been any recent sign on activity$UserLastLogonDate = $Null$UserObjectId = $G.ObjectId$UserLastLogonDate = (Get-AzureADAuditSignInLogs -Top 1 -Filter "userid eq '$UserObjectId' and status/errorCode eq 0").CreatedDateTimeIf ($null -ne $UserLastLogonDate) {$UserLastLogonDate = Get-Date ($UserLastLogonDate) -format g} Else {$UserLastLogonDate = "No recent sign in records found"}# Flag the account for potential deletion if it is more than a year old and isn't a member of any Office 365 Groups.If (($AccountAge -gt 365) -and ($null -eq $GroupNames)) {$ReviewFlag = $True}# Write out report line$ReportLine = [PSCustomObject]@{Guest = $G.MailName = $G.DisplayNameDomain = $DomainInactive = $ReviewFlagCreated = $CreationDateAgeInDays = $AccountAgeEmailCount = $EmailRecs.Count"Last sign-in" = $UserLastLogonDate"Last Audit record" = $LastAuditRecord"Last Audit action" = $LastAuditAction"Member of" = $GroupNamesUPN = $G.UserPrincipalNameObjectId = $G.ObjectId }$Report.Add($ReportLine)# Update Entra ID with details.$ActiveText = "Active"If ($ReviewFlag -eq $True) {$ActiveText = "inactive"}$Text = "Guest account reviewed on " + (Get-Date -format g) + " when account was deemed " + $ActiveTextSet-MailUser -Identity $G.Mail -CustomAttribute1 $Text}# Generate the output files$Report | Sort-Object Name | Export-CSV -NoTypeInformation c:\temp\GuestActivity.csv$Report | Where-Object {$_.Inactive -eq $True} | Select-Object ObjectId, Name, UPN, AgeInDays | Export-CSV -NotypeInformation c:\temp\InActiveGuests.CSVClear-Host$Active = $AuditRec + $EmailActive# Figure out the domains guests come from$Domains = $Report.Domain | Sort-Object$DomainsCount = @{}$Domains | ForEach-Object {$DomainsCount[$_]++}$DomainsCount = $DomainsCount.GetEnumerator() | Sort-Object -Property Value -Descending$DomainNames = $Domains | Sort-Object -Unique$PercentInactive = (($Guests.Count - $Active)/$Guests.Count).toString("P")Write-Host ""Write-Host "Statistics"Write-Host "----------"Write-Host "Guest Accounts " $Guests.CountWrite-Host "Active Guests " $ActiveWrite-Host "Audit Record found " $AuditRecWrite-Host "Active on Email " $EmailActiveWrite-Host "InActive Guests " ($Guests.Count - $Active)Write-Host "Percent inactive guests " $PercentInactiveWrite-Host "Number of guest domains " $DomainsCount.CountWrite-Host ("Domain with most guests {0} ({1})" -f $DomainsCount[0].Name, $DomainsCount[0].Value)Write-Host " "Write-Host "Guests found from domains " ($DomainNames -join ", ")Write-Host " "Write-Host "The output file containing detailed results is in c:\temp\GuestActivity.csv"Write-Host "A CSV file containing the User Principal Names of inactive guest accounts is in c:\temp\InactiveGuests.csv"
Parameters
-LookbackDays90Number of days back to evaluate guest account activity.-StartDate(Get-Date).AddDays(-90)Start of the audit log search window.-EndDate(Get-Date)End of the audit log search window.