Public/Get-M365Guest.ps1

function Get-M365Guest {
    <#
    .SYNOPSIS
        Returns guest (external) accounts with domain, state and last sign-in.
    .DESCRIPTION
        Reads guest users via Microsoft Graph and derives the external domain, invitation state and
        days since last sign-in. Read-only.
    .PARAMETER InactiveDays
        Mark guests stale if not signed in for this many days. Default: 90.
    .PARAMETER StaleOnly
        Return only stale guests.
    .EXAMPLE
        Get-M365Guest -StaleOnly
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param([int]$InactiveDays = 90, [switch]$StaleOnly)

    Assert-TTGraph
    Write-TTLog -Level INFO -Message "Reading guest accounts ..."
    $cutoff = (Get-Date).AddDays(-$InactiveDays)

    $guests = Get-MgUser -All -Filter "userType eq 'Guest'" -Property 'id,displayName,userPrincipalName,mail,createdDateTime,externalUserState,signInActivity' -ErrorAction Stop

    foreach ($g in $guests) {
        $mail = if ($g.Mail) { $g.Mail } else { $g.UserPrincipalName }
        $domain = if ($mail -match '@') { ($mail -split '@')[-1] } else { '' }
        $domain = $domain -replace '#EXT#.*$', ''
        $last = $g.SignInActivity.LastSignInDateTime
        $never = $null -eq $last
        $days = if ($never) { $null } else { [math]::Floor(((Get-Date) - [datetime]$last).TotalDays) }
        $stale = $never -or ([datetime]$last -lt $cutoff)
        if ($StaleOnly -and -not $stale) { continue }

        [pscustomobject]@{
            DisplayName = $g.DisplayName
            Mail        = $g.Mail
            Domain      = $domain
            State       = $g.ExternalUserState
            Created     = $g.CreatedDateTime
            LastSignIn  = if ($never) { $null } else { [datetime]$last }
            DaysInactive = $days
            Stale       = $stale
            NeverSignedIn = $never
            Id          = $g.Id
        }
    }
}