Microsoft has some cool tools for Guest user management. Implementing Access Reviews for example is great for ensuring expiration of Guest access when needed. We can also control who can invite Guests and which domains we allow Guests from. When this is all set up we have some really great governance over our B2B strategy in Microsoft 365. Unfortunately, there are a ton of organizations who didn’t have a full governance plan from day 1 and are now in remediation of Guest sprawl.
To help out with this remediation process, I’ve put together a straightforward script (available on GitHub) which will pull all guest users in a tenant, search the logs for the last sign in date/time and also list any apps they’ve logged into. This report is limited by the retention of AAD logs which is 30 days so keep that in mind when running. You’ll need to have an output folder present at c:\temp to export the results.
The only module required for this script is the AzureADPreview PowerShell Module which can be installed using the command “Install-Module AzureADPreview”
<#
Author: Sean McAvinue
Contact: Sean@seanmcavinue., Twitter: @Sean_McAvinue
.SYNOPSIS
Gets guest users last sign in action from AAD logs and exports user and signin list to CSV in C:\temp
#>
azureadpreview\Connect-azuread
##Get all guest users
$guests = Get-AzureADUser -Filter "userType eq 'Guest'" -All $true
##Loop Guest Users
foreach ($guest in $guests) {
##Get logs filtered by current guest
$logs = Get-AzureADAuditSignInLogs -Filter "userprincipalname eq `'$($guest.mail)'" -ALL:$true
##Check if multiple entries and tidy results
if ($logs -is [array]) {
$timestamp = $logs[0].createddatetime
}
else {
$timestamp = $logs.createddatetime
}
##Build Output Object
$object = [PSCustomObject]@{
Userprincipalname = $guest.userprincipalname
Mail = $guest.mail
LastSignin = $timestamp
AppsUsed = (($logs.resourcedisplayname | select -Unique) -join (';'))
}
##Export Results
$object | export-csv C:\temp\GuestUserSignins.csv -NoTypeInformation -Append
Remove-Variable object
}
Thank you for providing this! I had to make some tweaks to this script because:
if ($logs -is [array]) {
$timestamp = $logs[0].createddatetime
}
else {
$timestamp = $logs.createddatetime
}
A lot of times would not return anything, after doing numerous spot checks against the user in Azure Active Directory. I did change this to the following with much better success:
$timestamp = $logs.createddatetime
If ($timestamp -eq $NULL){
$timestamp = “BLANK”}
Else
{$timestamp = $logs[0].createddatetime}
Additionally, I needed to do some spot checks against the creation date to show a customer that accounts that have existed for a while needed cleanup by pulling the creation date. I did this with some modifications:
$exuser = $guest.extensionproperty
$object = [PSCustomObject]@{
Userprincipalname = $guest.userprincipalname
UserState = $guest.UserState
Enabled = $guest.AccountEnabled
Name = $guest.DisplayName
Mail = $guest.mail
CreatedDate = $exuser.createdDateTime
LastSignin = $timestamp
AppsUsed = (($logs.resourcedisplayname | select -Unique) -join (‘;’))
Finally to ensure that attributes were being cleared for the next loop, I updated the end to change Remove-Variable to Clear-Variable:
Clear-Variable object
Clear-Variable logs
Clear-Variable timestamp
Clear-Variable exuser
Your script has been an invaluable part of my Azure Active Directory security audits for clients so I had to share some ways I’ve been able to improve 😁
LikeLike
Nice! I didn’t see this issue but glad you could update and get it working
LikeLike
Chris any chance to share the final version of your script?
LikeLike
If got an error from azureadauditsigninlogs stating: too many requests.
Solved it by adding a 2 second sleep at the foreach loop.
Start-Sleep -s 2
LikeLike
Awesome, there’s definitely room for error handling in here particularly for large environments. Adding a sleep should slow down graph calls and avoid throttling
LikeLike
How on earth did you get this to work?
$logs = Get-AzureADAuditSignInLogs -Filter “userprincipalname eq `’$($guest.mail)'” -ALL:$true
It’s well documented out there that the filter switch can’t handle any variables. Replace it with a hard coded string and it’s fine. Can you confirm this DEFINITELY works? I copied your script and found it didn’t work. I’ve already been trying using my own script for months.
LikeLike
It has always worked for me as long as it’s formatted correctly, for example I just ran this. note the formatting of the variable inside the string.
Get-AzureADAuditSignInLogs -Filter “userprincipalname eq ‘$($user.userprincipalname)'” -ALL:$true
LikeLike
Pingback: Explaining Azure AD External Identities