Public/Get-UnderutilizedLicenses.ps1

function Get-UnderutilizedLicenses {
    <#
    .SYNOPSIS
        Identifies users with expensive licenses who are not using premium features.
    .DESCRIPTION
        Analyzes each licensed user's actual service usage against their assigned license
        tier. Identifies E5 users who only use E3 features, E3 users who only use E1
        features, and users with no recent sign-in activity. Provides specific downgrade
        recommendations with estimated monthly savings.
    .PARAMETER DaysInactive
        Number of days without sign-in to flag as inactive. Default: 90.
    .EXAMPLE
        Get-UnderutilizedLicenses -DaysInactive 60
        Finds users with expensive licenses who haven't used premium features in 60 days.
    .EXAMPLE
        Get-UnderutilizedLicenses | Where-Object Finding -like 'DOWNGRADE*' | Export-Csv downgrade-candidates.csv
        Exports downgrade candidates to CSV for review.
    .OUTPUTS
        PSCustomObject with properties: UserPrincipalName, DisplayName, AssignedLicense,
        LastSignIn, DaysSinceSignIn, ServicesUsed, RecommendedLicense, MonthlySavings, Finding
    #>

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

    Test-GraphConnection

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

    # E5-only service plan names (features that justify the E5 price)
    $e5OnlyServices = @(
        'MCOEV'                              # Phone System
        'MICROSOFTBOOKINGS'                  # Bookings (E5 bundled)
        'POWER_BI_PRO'                       # Power BI Pro (included in E5)
        'INFORMATION_BARRIERS'               # Information Barriers
        'CONTENT_EXPLORER'                   # Content Explorer
        'MIP_S_CLP2'                         # Azure Information Protection P2
        'M365_ADVANCED_AUDITING'             # Advanced Auditing
        'COMMUNICATIONS_DLP'                 # Communications DLP
        'CUSTOMER_KEY'                       # Customer Key
        'DATA_INVESTIGATIONS'                # Data Investigations
        'PREMIUM_ENCRYPTION'                 # Premium Encryption
        'MICROSOFTENDPOINTDLP'              # Endpoint DLP
        'INSIDER_RISK_MANAGEMENT'            # Insider Risk Management
        'INSIDER_RISK'                       # Insider Risk
        'ML_CLASSIFICATION'                  # Trainable Classifiers
        'RECORDS_MANAGEMENT'                 # Records Management
        'EQUIVIO_ANALYTICS'                  # eDiscovery Premium
        'PAM_ENTERPRISE'                     # Privileged Access Management
        'SAFEDOCS'                           # Safe Documents
        'CDS_O365_F1'                        # Common Data Service
    )

    # E3 features beyond E1
    $e3BeyondE1Services = @(
        'OFFICESUBSCRIPTION'                 # Microsoft 365 Apps (desktop)
        'EXCHANGE_S_ENTERPRISE'              # Exchange Plan 2
        'SHAREPOINTENTERPRISE'               # SharePoint Plan 2
        'RMS_S_ENTERPRISE'                   # Azure Rights Management
        'MCOSTANDARD'                        # Skype for Business / Teams
        'INTUNE_A'                           # Intune
    )

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

    $results = foreach ($user in $users) {
        # Skip unlicensed users (belt and suspenders)
        if (-not $user.AssignedLicenses -or $user.AssignedLicenses.Count -eq 0) { continue }

        # Determine the user's highest-tier license
        $userSkuIds     = $user.AssignedLicenses | Select-Object -ExpandProperty SkuId
        $highestLicense = $null
        $licenseCost    = 0

        # Get license details to check service plans
        try {
            $licenseDetails = Get-MgUserLicenseDetail -UserId $user.Id -ErrorAction Stop
        }
        catch {
            Write-Warning "Could not retrieve license details for $($user.UserPrincipalName): $_"
            continue
        }

        # Identify the most expensive license
        $skuLookup = @{}
        foreach ($detail in $licenseDetails) {
            $lookup = $friendlyNames[$detail.SkuPartNumber]
            $cost = if ($lookup) { $lookup.EstimatedMonthlyCost } else { 0 }
            $skuLookup[$detail.SkuPartNumber] = @{
                Cost          = $cost
                FriendlyName  = if ($lookup) { $lookup.FriendlyName } else { $detail.SkuPartNumber }
                ServicePlans  = $detail.ServicePlans
            }
            if ($cost -gt $licenseCost) {
                $licenseCost    = $cost
                $highestLicense = $detail.SkuPartNumber
            }
        }

        # Only analyze users with premium licenses ($20+/mo)
        if ($licenseCost -lt 20) { continue }

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

        # Check which services are actually enabled and in use
        $enabledServices = @()
        foreach ($detail in $licenseDetails) {
            foreach ($plan in $detail.ServicePlans) {
                if ($plan.ProvisioningStatus -eq 'Success') {
                    $enabledServices += $plan.ServicePlanName
                }
            }
        }
        $enabledServices = $enabledServices | Select-Object -Unique

        # Determine services used (simplified: enabled = potentially used)
        $servicesUsedList = @()
        if ($enabledServices -match 'EXCHANGE')    { $servicesUsedList += 'Exchange' }
        if ($enabledServices -match 'SHAREPOINT')  { $servicesUsedList += 'SharePoint' }
        if ($enabledServices -match 'TEAMS|MCO')   { $servicesUsedList += 'Teams' }
        if ($enabledServices -match 'OFFICESUB')   { $servicesUsedList += 'Office Apps' }
        if ($enabledServices -match 'POWER_BI')    { $servicesUsedList += 'Power BI' }
        if ($enabledServices -match 'INTUNE')      { $servicesUsedList += 'Intune' }
        $servicesUsed = $servicesUsedList -join ', '

        # Determine finding and recommendation
        $finding           = $null
        $recommendedLicense = $null
        $monthlySavings    = 0

        # User has not signed in at all
        if ($daysSinceSign -eq -1 -or $daysSinceSign -gt $DaysInactive) {
            $finding            = 'NO SIGN-IN'
            $recommendedLicense = 'Remove License'
            $monthlySavings     = $licenseCost
        }
        else {
            # Check E5 users for downgrade to E3
            $isE5 = $highestLicense -match 'SPE_E5|Microsoft_365_E5|ENTERPRISEPREMIUM'
            if ($isE5) {
                $usesE5Features = $false
                foreach ($svc in $e5OnlyServices) {
                    if ($enabledServices -contains $svc) {
                        $usesE5Features = $true
                        break
                    }
                }
                if (-not $usesE5Features) {
                    $finding            = 'DOWNGRADE CANDIDATE'
                    $recommendedLicense = 'Microsoft 365 E3'
                    $e3Cost = 36.00
                    $monthlySavings     = [math]::Round($licenseCost - $e3Cost, 2)
                }
            }

            # Check E3 users for downgrade to E1
            $isE3 = $highestLicense -match 'SPE_E3|Microsoft_365_E3|ENTERPRISEPACK'
            if ($isE3 -and -not $finding) {
                $usesE3Features = $false
                foreach ($svc in $e3BeyondE1Services) {
                    if ($enabledServices -contains $svc) {
                        $usesE3Features = $true
                        break
                    }
                }
                if (-not $usesE3Features) {
                    $finding            = 'DOWNGRADE CANDIDATE'
                    $recommendedLicense = 'Office 365 E1'
                    $e1Cost = 8.00
                    $monthlySavings     = [math]::Round($licenseCost - $e1Cost, 2)
                }
            }
        }

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

            [PSCustomObject]@{
                UserPrincipalName  = $user.UserPrincipalName
                DisplayName        = $user.DisplayName
                AssignedLicense    = $highestInfo.FriendlyName
                LastSignIn         = $lastSignInDisplay
                DaysSinceSignIn    = if ($daysSinceSign -ge 0) { $daysSinceSign } else { 'Never' }
                ServicesUsed       = $servicesUsed
                RecommendedLicense = $recommendedLicense
                MonthlySavings     = $monthlySavings
                Finding            = $finding
            }
        }
    }

    return $results | Sort-Object MonthlySavings -Descending
}