Collectors/StaleAccounts.ps1

@{
    Name           = 'StaleAccounts'
    FileName       = 'staleAccounts'
    ApiVersion     = 'v1.0'
    RequiredScopes = @('User.Read.All', 'AuditLog.Read.All')
    Description    = 'Enabled users without recent sign-ins and never-signed-in guests (signInActivity; requires Entra ID P1).'
    Collect        = {
        $thresholdDays = 90
        $users = @(Invoke-TLGraphRequest -Uri '/v1.0/users?$select=id,userPrincipalName,displayName,accountEnabled,userType,createdDateTime,assignedLicenses,signInActivity&$top=999' -All)
        $utcNow = [DateTime]::UtcNow
        $threshold = $utcNow.AddDays(-1 * $thresholdDays)

        $staleUsers = 0
        $neverSignedInGuests = 0
        $signInBlockCandidates = 0
        foreach ($user in $users) {
            $lastSignIn = $null
            if ($user.PSObject.Properties['signInActivity'] -and $user.signInActivity) {
                foreach ($property in @('lastSignInDateTime', 'lastNonInteractiveSignInDateTime')) {
                    if ($user.signInActivity.PSObject.Properties[$property] -and $user.signInActivity.$property) {
                        $candidate = ([DateTime]$user.signInActivity.$property).ToUniversalTime()
                        if ($null -eq $lastSignIn -or $candidate -gt $lastSignIn) { $lastSignIn = $candidate }
                    }
                }
            }
            $created = $null
            if ($user.PSObject.Properties['createdDateTime'] -and $user.createdDateTime) {
                $created = ([DateTime]$user.createdDateTime).ToUniversalTime()
            }

            # Stale: enabled account, no sign-in within the threshold - brand-new
            # accounts that never signed in yet are not counted.
            $isStale = $false
            if ($user.accountEnabled) {
                if ($null -ne $lastSignIn) { $isStale = $lastSignIn -lt $threshold }
                elseif ($null -ne $created) { $isStale = $created -lt $threshold }
            }
            if ($isStale) { $staleUsers++ }

            $neverSignedInGuest = ([string]$user.userType -eq 'Guest') -and ($null -eq $lastSignIn)
            if ($neverSignedInGuest) { $neverSignedInGuests++ }

            # Shared-mailbox pattern: enabled member account, no license, never
            # signed in - sign-in should be blocked on such accounts.
            $isUnlicensed = @($user.assignedLicenses).Count -eq 0
            $signInBlockCandidate = [bool]($user.accountEnabled -and $isUnlicensed -and
                ([string]$user.userType -ne 'Guest') -and ($null -eq $lastSignIn))
            if ($signInBlockCandidate) { $signInBlockCandidates++ }

            Add-Member -InputObject $user -NotePropertyName '_tlLastSignInUtc' -NotePropertyValue $(if ($lastSignIn) { $lastSignIn.ToString('o') } else { $null }) -Force
            Add-Member -InputObject $user -NotePropertyName '_tlStale' -NotePropertyValue $isStale -Force
            Add-Member -InputObject $user -NotePropertyName '_tlNeverSignedInGuest' -NotePropertyValue $neverSignedInGuest -Force
            Add-Member -InputObject $user -NotePropertyName '_tlSignInBlockCandidate' -NotePropertyValue $signInBlockCandidate -Force
        }

        @{
            thresholdDays = $thresholdDays
            summary       = [ordered]@{
                totalEvaluated           = @($users).Count
                staleUserCount           = $staleUsers
                neverSignedInGuestCount  = $neverSignedInGuests
                signInBlockCandidateCount = $signInBlockCandidates
            }
            users         = $users
        }
    }
}