Public/Get-GkGuestInventory.ps1
|
function Get-GkGuestInventory { <# .SYNOPSIS Inventory guest (external) accounts with their sponsor, invitation state, last sign-in, and inactivity/age in days. .DESCRIPTION Lists users where userType eq 'Guest' (GET /users) and, for each, reports the invitation state (externalUserState), when it last changed, the account age from createdDateTime, and the inactivity age derived from signInActivity. By default it also resolves each guest's sponsor(s) via GET /users/{id}/sponsors — one call per guest; use -SkipSponsor to skip that on large tenants. Sponsor reads depend on the signed-in admin's directory role in delegated sessions; if a sponsor lookup is denied, the guest is still returned (Sponsors empty) and a single warning is emitted afterward (degrade mode: warn and continue). Reading signInActivity requires Entra ID P1/P2 + AuditLog.Read.All; without it, guests appear as never-signed-in. .PARAMETER InactiveDays Threshold in days used to set the IsStale flag (default 90). Never-signed-in guests are stale. .PARAMETER StaleOnly Return only guests flagged stale (inactive >= InactiveDays, or never signed in). .PARAMETER SkipSponsor Do not resolve sponsors (skips the per-guest /sponsors call). Sponsors will be empty. .PARAMETER AsReport Flatten Sponsors to a single '; '-joined string and add ReportGeneratedUtc for clean export. .EXAMPLE Get-GkGuestInventory All guests with sponsor, invitation state, age, and inactivity. .EXAMPLE Get-GkGuestInventory -StaleOnly -InactiveDays 180 | Sort-Object InactiveDays -Descending Guests inactive for 180+ days (or never signed in), most inactive first. .EXAMPLE Get-GkGuestInventory -SkipSponsor -AsReport | Export-Csv .\guests.csv -NoTypeInformation Fast guest export (no sponsor lookups), sponsors column flattened for CSV. .OUTPUTS PSGraphKit.GuestInventory #> [CmdletBinding()] [OutputType('PSGraphKit.GuestInventory')] param( [ValidateRange(1, 3650)] [int] $InactiveDays = 90, [switch] $StaleOnly, [switch] $SkipSponsor, [switch] $AsReport ) begin { Test-GkConnection -FunctionName 'Get-GkGuestInventory' | Out-Null $now = [datetime]::UtcNow } process { $select = 'id,displayName,userPrincipalName,mail,userType,accountEnabled,externalUserState,externalUserStateChangeDateTime,createdDateTime,signInActivity' $uri = "/users?`$filter=userType eq 'Guest'&`$select=$select&`$top=500" $guests = Invoke-GkGraphRequest -Uri $uri -CallerFunction 'Get-GkGuestInventory' $sponsorFailures = 0 foreach ($g in $guests) { $id = [string](Get-GkDictValue $g 'id') $activity = Get-GkDictValue $g 'signInActivity' $lastInteractive = ConvertTo-GkDateTime (Get-GkDictValue $activity 'lastSignInDateTime') $lastNonInteractive = ConvertTo-GkDateTime (Get-GkDictValue $activity 'lastNonInteractiveSignInDateTime') $lastActivity = $null foreach ($d in @($lastInteractive, $lastNonInteractive)) { if ($null -ne $d -and ($null -eq $lastActivity -or $d -gt $lastActivity)) { $lastActivity = $d } } $neverSignedIn = ($null -eq $lastActivity) $inactive = if ($neverSignedIn) { $null } else { [int][math]::Floor(($now - $lastActivity).TotalDays) } $isStale = $neverSignedIn -or ($inactive -ge $InactiveDays) if ($StaleOnly -and -not $isStale) { continue } $created = ConvertTo-GkDateTime (Get-GkDictValue $g 'createdDateTime') $ageDays = if ($null -ne $created) { [int][math]::Floor(($now - $created).TotalDays) } else { $null } # Sponsors (one call per guest unless skipped). $sponsorNames = @() if (-not $SkipSponsor -and $id) { try { $sponsors = Invoke-GkGraphRequest -Uri "/users/$id/sponsors?`$select=id,displayName" -CallerFunction 'Get-GkGuestInventory' $sponsorNames = @( $sponsors | ForEach-Object { [string](Get-GkDictValue $_ 'displayName') } | Where-Object { $_ } ) } catch { $sponsorFailures++ Write-Verbose "PSGraphKit: sponsor lookup failed for $id : $($_.Exception.Message)" } } $obj = [ordered]@{ PSTypeName = 'PSGraphKit.GuestInventory' DisplayName = [string](Get-GkDictValue $g 'displayName') UserPrincipalName = [string](Get-GkDictValue $g 'userPrincipalName') Mail = [string](Get-GkDictValue $g 'mail') AccountEnabled = [bool](Get-GkDictValue $g 'accountEnabled') InvitationState = [string](Get-GkDictValue $g 'externalUserState') InvitationStateChanged = ConvertTo-GkDateTime (Get-GkDictValue $g 'externalUserStateChangeDateTime') Created = $created GuestAgeDays = $ageDays LastActivity = $lastActivity InactiveDays = $inactive NeverSignedIn = $neverSignedIn IsStale = $isStale Sponsors = if ($AsReport) { $sponsorNames -join '; ' } else { $sponsorNames } SponsorCount = $sponsorNames.Count Id = $id } if ($AsReport) { $obj['ReportGeneratedUtc'] = $now } [pscustomobject]$obj } if ($sponsorFailures -gt 0) { Write-Warning "Sponsor lookup was denied for $sponsorFailures guest(s). In a delegated session, reading /sponsors needs a directory role granting microsoft.directory/users/sponsors/read (e.g. Directory Readers, Guest Inviter, User Administrator). Those guests are returned with no sponsors." } } } |