Public/Get-MsecEntraLicense.ps1

function Get-MsecEntraLicense {
    <#
    .SYNOPSIS
        Lists the tenant's subscribed licence SKUs as flat rows, with the service
        plans each one turns on.

    .DESCRIPTION
        Calls Microsoft Graph /v1.0/subscribedSkus and projects each SKU to a
        PSCustomObject. This is the tenant's licence inventory: what was bought,
        how much of it is assigned, and which underlying service plans it enables.

        Use this to answer "is this workload even licensed here". That question is
        the difference between a real security gap and a not-applicable one: a
        tenant with no AAD_PREMIUM service plan cannot have Conditional Access at
        all, so an empty CA policy list is expected rather than alarming. The
        derived, question-shaped version of this lives in
        Get-MsecEntraTenantSecuritySetting, which turns these plans into
        capability flags (ConditionalAccessAvailable, IntuneProvisioned, ...).

        Requires the 'Organization.Read.All' application permission. A clearer
        error is raised on the typical 403.

    .EXAMPLE
        Get-MsecEntraLicense | Sort-Object SkuPartNumber

    .EXAMPLE
        # What is actually assigned to someone (bought-but-unused SKUs excluded)?
        Get-MsecEntraLicense | Where-Object Assigned -gt 0

    .EXAMPLE
        # Does this tenant have Entra ID premium (and therefore Conditional Access)?
        Get-MsecEntraLicense | Where-Object ServicePlans -contains 'AAD_PREMIUM'

    .OUTPUTS
        PSCustomObject per subscribed SKU. See .NOTES for the projection.

    .NOTES
        Each row is a [PSCustomObject] with PSTypeName 'MsecEntraLicense', whose
        DefaultDisplayPropertySet (SkuPartNumber, Enabled, Assigned,
        CapabilityStatus) is registered in Msec.psm1 - so Format-Table shows a
        clean 4-column view and the Raw / ServicePlans columns stay accessible
        without cluttering it.

        Projection (Graph field path -> output property):
          skuId -> SkuId
          skuPartNumber -> SkuPartNumber
          appliesTo -> AppliesTo ('User' / 'Company')
          capabilityStatus -> CapabilityStatus ('Enabled' / 'Warning' / 'Suspended')
          prepaidUnits.enabled -> Enabled (units bought and usable)
          prepaidUnits.warning / .suspended -> WarningUnits / SuspendedUnits
          consumedUnits -> Assigned
          servicePlans[].servicePlanName -> ServicePlans (names with provisioningStatus 'Success')
                 (where provisioningStatus eq Success)
          servicePlans[] -> ServicePlanDetail (full objects: name, status, appliesTo)
          <entire SKU object verbatim> -> Raw

        Only service plans whose provisioningStatus is 'Success' land in
        ServicePlans. A plan can be present but PendingProvisioning or Disabled,
        in which case the capability is not actually usable - so filtering on
        ServicePlans is the honest test. ServicePlanDetail keeps every plan
        regardless of status for when you need to see the difference.
    #>

    [CmdletBinding()]
    param()

    Assert-MsecSession

    $path = '/v1.0/subscribedSkus'

    try {
        $skus = @(Invoke-MsecGraphRequest -Path $path -All)
    }
    catch {
        if ($_.Exception.Message -match '403|Forbidden') {
            throw "Forbidden when calling /subscribedSkus. The msec app needs the 'Organization.Read.All' application permission (admin consent required). Re-run New-MsecApp to add and consent it. Original error: $($_.Exception.Message)"
        }
        throw
    }

    # @($null) yields a one-element array containing $null rather than an empty
    # array, which then breaks downstream -contains and .Count checks.
    $arr = { param($v) if ($null -eq $v) { @() } else { @($v) } }

    foreach ($s in $skus) {
        $plans = & $arr $s.servicePlans

        [PSCustomObject]@{
            PSTypeName        = 'MsecEntraLicense'

            SkuId             = $s.skuId
            SkuPartNumber     = $s.skuPartNumber
            AppliesTo         = $s.appliesTo
            CapabilityStatus  = $s.capabilityStatus

            Enabled           = [int] $s.prepaidUnits.enabled
            WarningUnits      = [int] $s.prepaidUnits.warning
            SuspendedUnits    = [int] $s.prepaidUnits.suspended
            Assigned          = [int] $s.consumedUnits

            # Usable plans only - see .NOTES on why provisioningStatus matters.
            ServicePlans      = @($plans |
                                    Where-Object { $_.provisioningStatus -eq 'Success' } |
                                    ForEach-Object { $_.servicePlanName })
            ServicePlanDetail = $plans

            Raw               = $s
        }
    }
}