Public/Get-InactiveLicensedUsers.ps1

function Get-InactiveLicensedUsers {
    <#
    .SYNOPSIS
        Identifies licensed users who have not signed in within the specified threshold.
    .DESCRIPTION
        Finds Microsoft 365 licensed users who are inactive (no sign-in), disabled but
        still licensed, guests with licenses, or users who have never signed in. Each
        user's total license cost is calculated to quantify waste.
    .PARAMETER DaysInactive
        Number of days without sign-in to consider a user inactive. Default: 90.
    .PARAMETER IncludeGuests
        Include guest (external) users in the analysis.
    .PARAMETER IncludeDisabled
        Include disabled user accounts in the analysis. Default: included automatically
        as DISABLED WITH LICENSE findings.
    .EXAMPLE
        Get-InactiveLicensedUsers -DaysInactive 60 -IncludeGuests
        Finds all inactive licensed users (including guests) with no sign-in in 60 days.
    .EXAMPLE
        Get-InactiveLicensedUsers | Measure-Object -Property LicenseCost -Sum
        Calculates the total monthly cost of inactive licensed users.
    .OUTPUTS
        PSCustomObject with properties: UserPrincipalName, DisplayName, AccountEnabled,
        UserType, AssignedLicenses, LicenseCost, LastSignIn, DaysSinceSignIn, Finding
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 365)]
        [int]$DaysInactive = 90,

        [Parameter(Mandatory = $false)]
        [switch]$IncludeGuests,

        [Parameter(Mandatory = $false)]
        [switch]$IncludeDisabled
    )

    Test-GraphConnection

    $friendlyNames = Get-LicenseFriendlyName
    $cutoffDate    = (Get-Date).AddDays(-$DaysInactive)

    Write-Verbose "Retrieving licensed users with sign-in activity..."
    try {
        $filter = "assignedLicenses/`$count ne 0"
        $users  = Get-MgUser -All -Property 'Id,DisplayName,UserPrincipalName,SignInActivity,AssignedLicenses,AccountEnabled,UserType' `
                             -Filter $filter -ErrorAction Stop
    }
    catch {
        throw "Failed to retrieve users: $_"
    }

    # Build a SkuId-to-SkuPartNumber mapping from subscribed SKUs
    try {
        $subscribedSkus = Get-MgSubscribedSku -All -ErrorAction Stop
    }
    catch {
        Write-Warning "Could not retrieve subscribed SKUs for cost lookup: $_"
        $subscribedSkus = @()
    }

    $skuIdToPartNumber = @{}
    foreach ($sku in $subscribedSkus) {
        $skuIdToPartNumber[$sku.SkuId] = $sku.SkuPartNumber
    }

    $results = foreach ($user in $users) {
        # Filter guests unless requested
        if ($user.UserType -eq 'Guest' -and -not $IncludeGuests) { continue }

        # Get sign-in data
        $lastSignIn    = $user.SignInActivity.LastSignInDateTime
        $daysSinceSign = if ($lastSignIn) {
            [math]::Round(((Get-Date) - [datetime]$lastSignIn).TotalDays, 0)
        } else {
            -1
        }

        # Calculate total license cost for this user
        $userLicenseNames = @()
        $totalCost        = 0
        foreach ($license in $user.AssignedLicenses) {
            $partNumber = $skuIdToPartNumber[$license.SkuId]
            if ($partNumber) {
                $lookup = $friendlyNames[$partNumber]
                if ($lookup) {
                    $userLicenseNames += $lookup.FriendlyName
                    $totalCost += $lookup.EstimatedMonthlyCost
                } else {
                    $userLicenseNames += $partNumber
                }
            } else {
                $userLicenseNames += $license.SkuId
            }
        }
        $assignedLicenseList = $userLicenseNames -join '; '
        $totalCost = [math]::Round($totalCost, 2)

        # Determine finding
        $finding = $null

        # Priority 1: Disabled account with active license
        if (-not $user.AccountEnabled) {
            $finding = 'DISABLED WITH LICENSE'
        }
        # Priority 2: Guest with license
        elseif ($user.UserType -eq 'Guest') {
            $finding = 'GUEST WITH LICENSE'
        }
        # Priority 3: Never signed in
        elseif ($daysSinceSign -eq -1) {
            $finding = 'NEVER SIGNED IN'
        }
        # Priority 4: Inactive beyond threshold
        elseif ($daysSinceSign -ge $DaysInactive) {
            $finding = 'INACTIVE LICENSED USER'
        }

        # Only emit results with findings
        if ($finding) {
            $lastSignInDisplay = if ($lastSignIn) { ([datetime]$lastSignIn).ToString('yyyy-MM-dd') } else { 'Never' }

            [PSCustomObject]@{
                UserPrincipalName = $user.UserPrincipalName
                DisplayName       = $user.DisplayName
                AccountEnabled    = $user.AccountEnabled
                UserType          = $user.UserType
                AssignedLicenses  = $assignedLicenseList
                LicenseCost       = $totalCost
                LastSignIn        = $lastSignInDisplay
                DaysSinceSignIn   = if ($daysSinceSign -ge 0) { $daysSinceSign } else { 'Never' }
                Finding           = $finding
            }
        }
    }

    return $results | Sort-Object LicenseCost -Descending
}