Entra ID inactive guest account cleanup with exclusion groups script

Entra ID inactive guest account cleanup with exclusion groups script

The script below will help you to automate your inactive guest account cleanups in Entra ID (Azure AD).

The script handles nested groups in your main YOURGROUPNAMEHERE group, it will exclude any user or group it finds. The reason why adding your users to groups is to work with Entra ID access reviews to keep the exclusions to a healthy minimum.

In the end the script does a final check of the guests accounts it can find and does a compare for a final sum sanity check.

You only need to add logic to send yourself an email (or whatever) and output the stringbuilder.

# Parameters
$ClientID = "YourClientIDHere"
$AppId = "YourAppIdHere"
$ThumbPrint = "CertThumbprintHere"
$Tenant = "YourTenantPrimaryURLHere"

# Import modules
Import-Module Microsoft.Graph.Users
Import-Module Microsoft.Graph.Groups
Import-Module ExchangeOnlineManagement

# Connect with Azure AD and Exchange Online
Connect-MgGraph -ApplicationId "$AppId" -CertificateThumbprint "$ThumbPrint" -Tenant "$Tenant" -NoWelcome

# Connect with Exchange Online
Connect-ExchangeOnline -CertificateThumbPrint "ThumbPrint" -AppID "$AppId" -Organization "$Tenant"

### VARIABLES ###
$DeductDays = "89"

$Guests = (Get-MgUser -Filter "UserType eq 'Guest'" -All -Property Displayname, Id, UserPrincipalName, Mail, RefreshTokensValidFromDateTime, SignInActivity  | Select-Object Displayname, Id, UserPrincipalName, Mail, RefreshTokensValidFromDateTime, @{n="LastSignInDateTime";e={$_.SignInActivity.LastSignInDateTime}}) | Sort-Object
$totalGuests = $Guests.Count

Write-Host "$totalGuests guest accounts found. Checking their recent activity..."

$StartDate = (Get-Date).AddDays(-$DeductDays)
$StartDate2 = (Get-Date).AddDays(-10) # Message trace can only go back 10 days.
$Report = [System.Collections.Generic.List[Object]]::new() # Create output file for report
$counter = 0
$EndDate = (Get-Date);
$Active = 0
$EmailActive = 0
$Inactive = 0
$AuditRec = 0
###

# Loop over every currently existing guest and check their (in)activity
foreach ($guest in $Guests) 
{
    $counter++

    $displayname = $guest.DisplayName
        
    Write-Host ""
    Write-Host "Checking $DisplayName | guest $counter of $totalGuests"  
         
    $LastAuditAction = $Null; $LastAuditRecord = $Null
        
    # Search for audit records for this user
    $Recs = (Search-UnifiedAuditLog -UserIds $guest.Mail, $guest.UserPrincipalName -Operations UserLoggedIn, SecureLinkUsed, TeamsSessionStarted -StartDate $StartDate -EndDate $EndDate -ResultSize 1)
            
    if ($Null -ne $Recs.CreationDate) 
    {
        # We found some audit logs
        $LastAuditRecord = $Recs[0].CreationDate; 
        $LastAuditAction = $Recs[0].Operations;

        $AuditRec++

        Write-Host "Last audit record for $DisplayName on $LastAuditRecord"
    }
            
    else
    { 
        # Check message trace data because guests might receive email through membership of Outlook Groups. Email address must be valid for the check to work
        If ($Null -ne $guest.Mail)
        {
            #get all email records
            $EmailRecs = (Get-MessageTrace –StartDate $StartDate2 –EndDate $EndDate -Recipient $guest.Mail)
        }

        $displayname= $guest.DisplayName
        $RefreshTokensValidFromDateTime = $guest.SignInActivity
            
        Write-Host "No audit records found in the last $DeductDays days for $DisplayName ; account last logged in on $RefreshTokensValidFromDateTime"
            
        # Write out report line     
        $ReportLine = [PSCustomObject]@{
            Guest            = $guest.Mail
            Name             = $guest.DisplayName
            ObjectID         = $guest.Id
            Created          = $guest.RefreshTokensValidFromDateTime
            EmailCount       = $EmailRecs.Count
            LastConnectOn    = $LastAuditRecord
            LastConnect      = $LastAuditAction
        } 
        $Report.Add($ReportLine)
    }      

    Write-Host ""
}
###

# Every user that did not do a login in the last $DeductDays days are in the $report

$reportCount = $report.Count
$activeUsers = $totalGuests - $reportCount
$userGroup = ""

# Create a $removedusers stringbuilder
$removedusers = New-Object System.Text.StringBuilder
$removedusers.Append("Hi, <br></p>")
$removedusers.Append("<p>A total of <b>$totalGuests</b> guest accounts were processsed, of which <b>" + $activeUsers + "</b> are active.</p>")
$removedusers.Append("<b>" + $reportCount + "</b> haven't signed in the last $DeductDays days.</p>")
$removedusers.Append("<br> ")
$removedusers.Append("<p>Below you can find the full list of these <b>inactive guests accounts.</b></p>")

# creation of html table
$RemovedUsers.Append("<style> tr:nth-child(even) { background-color: #f2f2f2; } tr:hover {background-color: #dbf0f8;} </style>")
$removedusers.Append("<table style='width:100%%' class='InactiveGuestAccountsTable' id='InactiveGuestAccountsTable'>")
    $removedusers.Append("<tr>")
        $removedusers.Append("<th>#</th>")
        $removedusers.Append("<th>Name</th>")
        $removedusers.Append("<th>Mail</th>")
        $removedusers.Append("<th>Group membership</th>")
        $removedusers.Append("<th>Action</th>")
    $removedusers.Append("</tr>")

    # Go over each line in the report and check the $DeductDays days marker
    $lineExcludedCounter = 0
    $lineRemovedCounter = 0
    $lineCounter = 0
    
    # YOURGROUPNAMEHERE = "YOURGROUPIDHERE"
    $group = Get-MgGroup -GroupId "YOURGROUPIDHERE"
    $members = Get-MgGroupMember -GroupId $group.Id -All

    foreach($line in $report)
    {
        $lineCounter++

        # Get today minus the deductdays
        $DeductDate = (get-date).adddays(-$DeductDays)
    
        # Check if users haven't connect less then the $deductdate
        if($line.LastConnectOn -lt $DeductDate)
        {
            Write-Host $line.ObjectID
            Write-Host $line.Name
            
            # Groupcounter will be used to indicated whether the user is in the YOURGROUPNAMEHERE-Excluded_Members group. 
            $GroupCounter = 0
            $userGroup = ""
                        
            foreach ($member in $members)
            {
                Write-Host "Checking member = $member"
                
                # If found object is a user
                if ($member.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.user")
                {
                    Write-Host "Member is a direct part of the guest group (not good)"
                    
                    if($member.Id -eq $line.ObjectID)
                    {
                        $lineExcludedCounter++
            
                        $user = Get-MgUser -UserId $member.Id
                        
                        Write-Host "Excluded direct member found"
                        Write-Host $member.Id
                        Write-Host $member.AdditionalProperties.displayName
                        
                        $userGroup = "<span style='background-color: #FFA500;'><b>[WARNING]</b></span> Direct member detected in <a href='https://portal.azure.com/?feature.msaljs=true#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/Members/groupId/YOURGROUPIDHERE/menuId/'>YOURGROUPNAMEHERE</a>"
            
                        Write-Host "Direct member found = $($user.DisplayName)"
                        
                        $GroupCounter = 666 # User can't be removed
                        
                        break
                    }
                }
            
                elseif ($member.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.group")
                {
                    Write-Host "Member is part of a group"
                    
                    $nestedGroup = Get-MgGroup -GroupId $member.Id
                    $nestedMembers = Get-MgGroupMember -GroupId $nestedGroup.Id -All
        
                    foreach ($nestedMember in $nestedMembers)
                    {
                        Write-Host "Going over nested member = $nestedMember.AdditionalProperties.displayName"
                        
                        if ($nestedMember.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.user")
                        {
                            Write-Host "Nested member is a user"
                            
                            if($nestedMember.Id -eq $line.ObjectID)
                            {
                                Write-Host "If the nested member is the same as in the parent foreach"
                                
                                $lineExcludedCounter++
            
                                $nestedUser = Get-MgUser -UserId $nestedMember.Id
            
                                Write-Host "Excluded nested member found"
                                Write-Host $nestedUser.id
                                Write-Host $nestedUser.DisplayName
                                            
                                $userGroup = "Nested member -> <a href='https://portal.azure.com/?feature.msaljs=true#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/Members/groupId/$($nestedGroup.Id)/menuId/'>$($nestedGroup.DisplayName)</a>"
                                 
                                Write-Host "Excluded nested member found = $($nestedGroup.DisplayName)"
                                Write-Host "$userGroup"
                                            
                                $GroupCounter = 666
                                
                                break
                            } 
                        }
                    }
                }
            }

            if($GroupCounter -eq 0)
            {
                Write-Host "Member is NOT protected, removal started for $line.guest"
                
                try
                {
                    $lineRemovedCounter++

                    Remove-MgUser -UserId $line.ObjectID
                            
                    Write-Host $line.guest " has been removed successfully"
                    Write-Host "$line.guest"

                    $removedusers.Append("<tr>")
                        $removedusers.Append("<td>" + $lineCounter + "</td>")
                        $removedusers.Append("<td>" + $line.Name + "</td>")
                        $removedusers.Append("<td>" + $line.Guest + "</td>")
                        $removedusers.Append("<td>Standard guest account</td>")
                        $removedusers.Append("<td style='background-color: #FF0000;'>Removed</td>")
                    $removedusers.Append("</tr>")
                }
                catch
                {
                    Write-Host "$line.guest could not be removed!"
                    Write-Host "$_"

                    $removedusers.Append("<tr>")
                        $removedusers.Append("<td>" + $lineCounter + "</td>")
                        $removedusers.Append("<td>" + $line.Name + "</td>")
                        $removedusers.Append("<td>" + $line.Guest + "</td>")
                        $removedusers.Append("<td>" + $userGroup + "</td>")
                        $removedusers.Append("<td style='background-color: #FF0000;'>Could not be removed!</td>")
                    $removedusers.Append("</tr>")
                }
            }

            elseif ($GroupCounter -eq 666)
            {
                if($NULL -ne $line.LastConnectOn)
                {
                    Write-Host $line.LastConnectOn
                    $lastConnectOn = [datetime]::parseexact($line.LastConnectOn, 'MM/dd/yyyy HH:mm:ss', $null).ToString('yyyy-MM-dd')
                }
                else
                {
                    $lastConnectOn = "User has not logged in for over $DeductDays days"
                }
                
                if($NULL -eq $userGroup)
                {
                    $userGroup = "Not excluded user"
                }
                    
                $removedusers.Append("<tr>")
                    $removedusers.Append("<td>" + $lineCounter + "</td>")
                    $removedusers.Append("<td>" + $line.Name + "</td>")
                    $removedusers.Append("<td>" + $line.Guest + "</td>")
                    $removedusers.Append("<td>" + $userGroup + "</td>")
                    $removedusers.Append("<td style='background-color: #90EE90;'>Excluded</td>")
                $removedusers.Append("</tr>")

                Write-Host "User $guest Excluded Guest Member"
            }
        }
    }
    
    $excludedUsersCounter = $reportCount - $lineRemovedCounter
    
    $sumcheck = ( ($activeUsers + $lineRemovedCounter) + $excludedUsersCounter)
    Write-Host "sum = $sumcheck"
    
    $sumCheckMessage = ""
    if($sumCheck -eq $totalGuests)
    {
        $sumCheckMessage = "<span style='color: #90EE90'>MATCHES = $sumCheck</span>"
    }
    else
    {
        $sumCheckMessage = "<span style='color: #FF0000'>DOES NOT MATCH = $sumCheck</span>"
    }
    
    $removedusers.Append("<caption style='caption-side:bottom'>")
        $removedusers.Append("<ul>")
            $removedusers.Append("<li><b>Active:</b> $activeUsers of $totalGuests are active.</li>")
            $removedusers.Append("<li><b>Removed:</b> $lineRemovedCounter of $reportCount inactive guest accounts were removed.</li>")
            $removedusers.Append("<li><b>Excluded:</b> $excludedUsersCounter of $reportCount inactive guest accounts were excluded.</li>")
            $removedusers.Append("<li><b>Control:</b> The SUM of the Active ($activeUsers) + Removed ($lineRemovedCounter) + Excluded ($excludedUsersCounter) must be equal to the total Guests ($totalGuests) => $sumCheckMessage</li>")
        $removedusers.Append("</ul>")
    $removedusers.Append("</caption>")

$removedusers.Append("</table>")
$removedusers.Append("Direct members group link = <a href='https://portal.azure.com/?feature.msaljs=true#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/Members/groupId/YOURGROUPIDHERE/menuId/'>YOURGROUPNAMEHERE</a>")
$removedusers.Append("<br> ")
$removedusers.Append("<p>Kind regards,<br><br>NAMEHERE</p>")
###

# Ticketing system API or mail messaging logic here
$subjectDate = get-date -Format "yyyy-MM"
$subject = $subjectDate + "Your Monthly Guest Cleanup"

# variable with stringbuilder information
$removedusers.ToString()

Write-Host "Ending script"

Example output

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *