Public/Remove-M365StaleGuests.ps1

function Remove-M365StaleGuests {
    <#
    .SYNOPSIS
        Finds and removes inactive guest accounts (with -WhatIf).
    .DESCRIPTION
        Lists guest accounts (userType = Guest) that have not signed in for a given number of
        days (or never) and removes them via Microsoft Graph. Every deletion goes through
        ShouldProcess: with -WhatIf NOTHING is deleted (dry run), only shown. Returns one
        object per (would-be) removed guest. Requires User.ReadWrite.All (write).
    .PARAMETER InactiveDays
        Threshold in days. Default: 90.
    .PARAMETER IncludeNeverSignedIn
        Also include guests that have never signed in.
    .PARAMETER WhatIf
        Dry run - only show what would be removed.
    .EXAMPLE
        Remove-M365StaleGuests -InactiveDays 180 -WhatIf
    .EXAMPLE
        Remove-M365StaleGuests -InactiveDays 180 -IncludeNeverSignedIn
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType([pscustomobject])]
    param(
        [int]$InactiveDays = 90,
        [switch]$IncludeNeverSignedIn
    )

    Assert-TTGraph

    $cutoff = (Get-Date).AddDays(-$InactiveDays)
    Write-TTLog -Level INFO -Message "Searching for stale guests (threshold: $InactiveDays days)."

    $props = 'id,displayName,userPrincipalName,mail,createdDateTime,signInActivity'
    $guests = Get-MgUser -All -Filter "userType eq 'Guest'" -Property $props -ErrorAction Stop |
        Select-Object DisplayName, UserPrincipalName, Mail, CreatedDateTime, Id,
            @{ N = 'LastSignIn'; E = { $_.SignInActivity.LastSignInDateTime } }

    foreach ($g in $guests) {
        $last = $g.LastSignIn
        $never = $null -eq $last

        if ($never) {
            if (-not $IncludeNeverSignedIn) { continue }
            $daysInactive = $null
        }
        else {
            if ([datetime]$last -ge $cutoff) { continue }
            $daysInactive = [math]::Floor(((Get-Date) - [datetime]$last).TotalDays)
        }

        $removed = $false
        if ($PSCmdlet.ShouldProcess($g.UserPrincipalName, "Remove stale guest (last sign-in: $(if ($never) { 'never' } else { $last }))")) {
            try {
                Remove-MgUser -UserId $g.Id -ErrorAction Stop
                Write-TTLog -Level ACTION -Message "Removed stale guest '$($g.UserPrincipalName)' ($($g.Id))."
                $removed = $true
            }
            catch { Write-TTLog -Level WARN -Message "Could not remove '$($g.UserPrincipalName)': $_" }
        }

        [pscustomobject]@{
            DisplayName       = $g.DisplayName
            UserPrincipalName = $g.UserPrincipalName
            Mail              = $g.Mail
            LastSignIn        = if ($never) { $null } else { [datetime]$last }
            DaysInactive      = $daysInactive
            NeverSignedIn     = $never
            Removed           = $removed
            WhatIf            = [bool]$WhatIfPreference
            Id                = $g.Id
        }
    }
}