Public/Get-TntLicenseReport.ps1

function Get-TntLicenseReport {
    <#
    .SYNOPSIS
        Retrieves Microsoft 365 license allocation and usage information.

    .DESCRIPTION
        This function connects to Microsoft Graph and retrieves detailed subscription and license
        information for the Microsoft 365 tenant. It provides insights into license allocation,
        consumption, available capacity, and utilization rates across all subscribed SKUs.

    .PARAMETER TenantId
        The Azure AD Tenant ID (GUID) to connect to.

    .PARAMETER ClientId
        The Application (Client) ID of the app registration created for security reporting.

    .PARAMETER ClientSecret
        The client secret for the app registration. Use this for automated scenarios.

    .PARAMETER CertificateThumbprint
        The thumbprint of the certificate to use for authentication instead of client secret.

    .EXAMPLE
        Get-TntLicenseReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret

        Retrieves and displays license allocation information.

    .INPUTS
        None. This function does not accept pipeline input.

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        Returns a structured object containing:
        - Summary: High-level statistics on subscriptions and licenses
        - Licenses: Detailed list of licenses with usage and friendly names

    .NOTES
        Author: Tom de Leeuw
        Website: https://systom.dev
        Module: TenantReports

        Required Permissions:
        - Organization.Read.All (Application)
        - Directory.Read.All (Application)

    .LINK
        https://systom.dev
    #>


    [CmdletBinding(DefaultParameterSetName = 'ClientSecret')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Interactive')]
        [ValidateNotNullOrEmpty()]
        [Alias('Tenant')]
        [string]$TenantId,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Interactive')]
        [ValidatePattern('^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$')]
        [Alias('ApplicationId')]
        [string]$ClientId,

        [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret', ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('Secret', 'ApplicationSecret')]
        [SecureString]$ClientSecret,

        [Parameter(Mandatory = $true, ParameterSetName = 'Certificate', ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('Thumbprint')]
        [string]$CertificateThumbprint,

        # Use interactive authentication (no app registration required).
        [Parameter(Mandatory = $true, ParameterSetName = 'Interactive')]
        [switch]$Interactive
    )

    begin {
        # Load SKU Translation table for retrieving friendly license names
        $SkuHashTable = @{}
        $SkuTable = Get-SkuTranslationTable
        if ($SkuTable) {
            $SkuTable | Group-Object GUID | ForEach-Object {
                $SkuHashTable[$_.Name] = ($_.Group | Select-Object -First 1).Product_Display_Name
            }
        } else {
            Write-Verbose "SKU Translation Table not available."
        }

        Write-Information 'Starting license information retrieval...' -InformationAction Continue
    }

    process {
        try {
            $ConnectionParams = Get-ConnectionParameters -BoundParameters $PSBoundParameters
            $ConnectionInfo = Connect-TntGraphSession @ConnectionParams

            # Retrieve subscription information
            Write-Verbose 'Retrieving subscription and license information...'
            $SubscribedSkus = Get-MgSubscribedSku -All -ErrorAction Stop

            $LicenseData = $SubscribedSkus | ForEach-Object {
                # Translate SKU ID to friendly name
                $ResolvedName = Resolve-SkuName -SkuId $_.SkuId -SkuHashTable $SkuHashTable
                $FriendlyName = if ($ResolvedName -eq $_.SkuId) {
                    Write-Verbose "SKU $($_.SkuId) not found in translation table. Using SkuPartNumber."
                    $_.SkuPartNumber
                } else {
                    $ResolvedName
                }

                # Build license information object
                [PSCustomObject]@{
                    SkuId             = $_.SkuId
                    SkuPartNumber     = $_.SkuPartNumber
                    FriendlyName      = $FriendlyName
                    CapabilityStatus  = $_.CapabilityStatus
                    ConsumedUnits     = $_.ConsumedUnits
                    PrepaidUnits      = $_.PrepaidUnits.Enabled
                    WarningUnits      = $_.PrepaidUnits.Warning
                    SuspendedUnits    = $_.PrepaidUnits.Suspended
                    AvailableUnits    = if ($_.PrepaidUnits.Enabled) {
                        $_.PrepaidUnits.Enabled - $_.ConsumedUnits
                    } else { 0 }
                    Utilization       = if ($_.PrepaidUnits.Enabled -gt 0) {
                        [math]::Round(($_.ConsumedUnits / $_.PrepaidUnits.Enabled) * 100, 2)
                    } else { 0 }
                    ServicePlansCount = $_.ServicePlans.Count
                }
            } | Sort-Object FriendlyName

            # Calculate summary statistics
            $TotalPrepaid = ($LicenseData | Measure-Object -Property PrepaidUnits -Sum).Sum
            $TotalConsumed = ($LicenseData | Measure-Object -Property ConsumedUnits -Sum).Sum
            $TotalAvailable = ($LicenseData | Measure-Object -Property AvailableUnits -Sum).Sum

            $Summary = [PSCustomObject]@{
                TotalSubscriptions     = if ($LicenseData) { $LicenseData.Count } else { 0 }
                ActiveSubscriptions    = if ($LicenseData) { ($LicenseData | Where-Object { $_.CapabilityStatus -eq 'Enabled' }).Count } else { 0 }
                TotalLicensesPurchased = $TotalPrepaid
                TotalLicensesAssigned  = $TotalConsumed
                TotalLicensesAvailable = $TotalAvailable
            }

            # Return report with summary and licenses
            Write-Information "License report completed - $($Summary.TotalSubscriptions) subscriptions found" -InformationAction Continue

            [PSCustomObject] @{
                Summary  = $Summary
                Licenses = $LicenseData
            }
        }
        catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                [System.Exception]::new("Get-TntLicenseReport failed: $($_.Exception.Message)", $_.Exception),
                'GetTntLicenseReportError',
                [System.Management.Automation.ErrorCategory]::OperationStopped,
                $TenantId
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
        finally {
            if ($ConnectionInfo.ShouldDisconnect) {
                Disconnect-TntGraphSession -ConnectionState $ConnectionInfo
            }
        }
    }
}