Checks/Licensing/ARG.LIC.001.ps1

Register-ArgusCheck @{
    Id          = 'ARG.LIC.001'
    Title       = 'Disabled users that still have licences'
    Area        = 'Licensing'
    Category    = 'Licence hygiene'
    Severity    = 'High'
    Description = 'Disabled accounts (`accountEnabled` = false) with at least one licence. Shared, room and equipment mailboxes are excluded using Exchange Online (they are disabled by design and may legitimately need a licence). When Exchange Online is not connected they cannot be told apart, so the result is Investigate.'
    Rationale   = 'A licence on a disabled account is paid for but cannot be used. Leavers are the most common cause.'
    Remediation = @{
        Steps      = "1. Check whether the mailbox must be kept (convert it to a shared mailbox, or apply a litigation hold / retention policy first).`n2. Remove the licences, or remove the user from the licensing group.`n3. Delete the account after your retention period."
        PowerShell = "# Sample only - review before running. Removes all licences from one user.`nConnect-MgGraph -Scopes 'User.ReadWrite.All'`n`$skus = (Get-MgUserLicenseDetail -UserId '<ObjectId>').SkuId`nSet-MgUserLicense -UserId '<ObjectId>' -AddLicenses @() -RemoveLicenses `$skus"
        PortalUrl  = 'https://admin.microsoft.com/'
    }
    References  = @(
        @{ Title = 'Assign or unassign licenses for users in the Microsoft 365 admin center'; Url = 'https://learn.microsoft.com/en-us/microsoft-365/admin/manage/assign-licenses-to-users' }
        @{ Title = 'About shared mailboxes'; Url = 'https://learn.microsoft.com/en-us/microsoft-365/admin/email/about-shared-mailboxes' }
    )
    Requires    = @{
        Services         = @('Graph')
        GraphScopes      = @('User.Read.All')
        DataSets         = @('Users', 'SubscribedSkus')
        OptionalDataSets = @('Mailboxes')
    }
    Tags        = @('Licensing', 'Lifecycle', 'Cost')
    Test        = {
        param($Context)
        $skus = @($Context.Data['SubscribedSkus'])
        $haveMailboxes = $Context.Data.ContainsKey('Mailboxes')
        $resourceMailboxes = @{}
        if ($haveMailboxes) {
            foreach ($mailbox in @($Context.Data['Mailboxes'])) {
                $type = [string](Get-ArgusProperty $mailbox 'RecipientTypeDetails' '')
                $id = [string](Get-ArgusProperty $mailbox 'ExternalDirectoryObjectId' '')
                if ($id -and $type -in 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox') { $resourceMailboxes[$id.ToLowerInvariant()] = $type }
            }
        }

        $disabled = @($Context.Data['Users'] | Where-Object { (Get-ArgusProperty $_ 'accountEnabled') -eq $false })
        $excluded = 0
        $findings = foreach ($user in $disabled) {
            $licences = @(Get-ArgusProperty $user 'assignedLicenses' @())
            if ($licences.Count -eq 0) { continue }
            $id = [string](Get-ArgusProperty $user 'id' '')
            if ($resourceMailboxes.ContainsKey($id.ToLowerInvariant())) { $excluded++; continue }
            [pscustomobject]@{
                DisplayName       = Get-ArgusProperty $user 'displayName' ''
                UserPrincipalName = Get-ArgusProperty $user 'userPrincipalName' ''
                Licences          = @(Get-ArgusSkuName -SkuId @($licences | ForEach-Object { Get-ArgusProperty $_ 'skuId' }) -SubscribedSku $skus)
                UserType          = Get-ArgusProperty $user 'userType' ''
                SyncedFromOnPrem  = [bool](Get-ArgusProperty $user 'onPremisesSyncEnabled' $false)
                ObjectId          = $id
            }
        }
        $findings = @($findings)

        if ($findings.Count -eq 0) {
            return New-ArgusResult -Status Passed -EvaluatedCount $disabled.Count -Summary 'None of the {EvaluatedCount} disabled accounts has a licence.'
        }
        if ($haveMailboxes) {
            $note = if ($excluded) { " ($excluded shared, room or equipment mailboxes excluded)" } else { '' }
            return New-ArgusResult -Status Failed -Findings $findings -EvaluatedCount $disabled.Count `
                -Summary ('{AffectedCount} of {EvaluatedCount} disabled accounts still have licences assigned' + $note + '.')
        }
        New-ArgusResult -Status Investigate -Findings $findings -EvaluatedCount $disabled.Count `
            -Summary '{AffectedCount} of {EvaluatedCount} disabled accounts still have licences assigned. Exchange Online is not connected, so shared, room and equipment mailboxes could not be excluded: review the list.'
    }
}