Cmdlet/Get-MSOLUserLicenseReport.ps1

# Generates an MSOL User License Report

Function Get-MSOLUserLicenseReport {
    <#
  
    .SYNOPSIS
    Generates a comprehensive license report.
 
    .DESCRIPTION
    Generates a license report on a all users or a specified group of users.
    By Default it will generate a new report file each time it is run.
 
    Report includes the following:
    * All licenses assigned to each user provided.
    * State of all plans inside of each license assignment
    * Where the user is getting the license from Group name(s) and/or Explict.
     
    .PARAMETER Users
    Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property.
 
    .PARAMETER OverWrite
    If specified it will Over Write the current output CSV file instead of generating a new one.
 
    .PARAMETER LogFile
    File to log all actions taken by the function.
     
    .PARAMETER IncludeDeletedUsers
    Includes deleted users if doing a report of all users.
    Will not work if -Users is specified
 
    .OUTPUTS
    Log file showing all actions taken by the function.
    CSV file named License_Report_YYYYMMDD_X.csv that contains the report.
 
    .EXAMPLE
    Get-MSOLUserLicenseReport -LogFile C:\temp\report.log
 
    Creates a new License_Report_YYYYMMDD_X.csv file with the license report in the c:\temp directory for all users.
 
    .EXAMPLE
    Get-MSOLUserLicenseReport -LogFile C:\temp\report.log -Users $SalesUsers -Overwrite
 
    OverWrites the existing c:\temp\License_Report_YYYYMMDD.csv file with a license report for all users in $SalesUsers.
     
    #>

    
    param 
    (
        [array]$Users,
        [Parameter(Mandatory)]
        [string]$LogFile,
        [switch]$OverWrite = $false,
        [switch]$IncludeDeletedUsers = $false
    )
    
    # Make sure we have a valid log file path
    Test-LogPath -LogFile $LogFile

    # Make sure we have the connection to MSOL
    Test-MSOLServiceConnection
    
    Write-Log "Generating Sku and Plan Report"

    # Get all of the availible SKUs
    $AllSku = Get-MsolAccountSku
    Write-Log ("Found " + $AllSku.count + " SKUs in the tenant")
    
    # Make sure out plan array is null
    [array]$Plans = $null

    # Build a list of all of the plans from all of the SKUs
    foreach ($Sku in $AllSku) {
        $SKU.servicestatus.serviceplan | ForEach-Object { [array]$Plans = $Plans + $_.servicename }
    }

    # Need just the unique plans
    $Plans = $Plans | Select-Object -Unique | Sort-Object
    Write-Log ("Found " + $Plans.count + " Unique plans in the tenant")

    # Make sure the output array is null
    $Output = $null

    # Create a false SKU object so we populate the first entry in the array with all needed values so we get a good CSV export
    # Basically a cheat so we can easily use export-csv we will remove this entry from the actual output
    $Object = $Null
    $Object = New-Object -TypeName PSobject
    $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value "BASELINE_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value "BASELINE_IGNORE@contoso.com"
    $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value "BASELINELOCATION_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name IsDeleted -Value "BASELINEDELETED_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value "BASELINESKU_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name CommonName -Value "BASELINESKUNAME_IGNORE"
    $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "BASELINEINHERIT_IGNORE"

    # Populate a value into all of the plan names
    foreach ($value in $Plans) {

        $Object | Add-Member -MemberType NoteProperty -Name $value -Value "---"
    }

    # Create the output list array
    $Output = New-Object System.Collections.ArrayList

    # Populate in the generic object to the list array
    $Output.Add($Object) | Out-Null

    # Make sure our UserToProcess array is created and is null
    [array]$UserToProcess = $null

    # See if our user array is null and pull all users if needed
    if ($null -eq $Users) {

        Write-Log "Getting all users in the tenant."
        Write-Log "This can take some time."

        # Get all of the users in the tenant
        [array]$UserToProcess = Get-MsolUser -All
        Write-log ("Found " + $UserToProcess.count + " users in the tenant")

        # Gather the deleted users as well if we want them
        if ($IncludeDeletedUsers){
            [array]$UserToProcess += Get-MsolUser -ReturnDeletedUsers -All
            Write-Log ("Found " + $UserToProcess.count + " users and deleted users in tenant")
        }

    }
    
    # Gather just the users provided
    else {
        
        # Make user our Users object is valid
        [array]$Users = Test-UserObject -ToTest $Users

        Write-Log "Gathering License information for provided users"
        $i = 0
        
        # Get the data for each user to use in generating the report
        foreach ($account in $Users) {
            $i++
            [array]$UserToProcess = [array]$UserToProcess + (Get-MsolUser -UserPrincipalName $Account.UserPrincipalName)
            if (!($i % 100)) { Write-log ("Gathered Data for " + $i + " Users") }
        }

        Write-log ("Found " + $UserToProcess.count + " users to report on")
    }

    # Now that we have all of the user objects we need to start processing them and building our output object

    # Setup a counter for informing the user of progress
    $i = 0
    
    # Null out the group cache array
    # Array holds group names and GUIDs so we do not have a lookup a group more than once
    [array]$GroupCache = $Null

    # Pull in our SkuID to Common Name map
    $SkuArray = Get-Content (join-path (Split-path (((get-module MSOLLicenseManagement)[0]).path) -Parent) "SkuMap.json") | convertfrom-json

    # Process each user
    Write-Log "Generating Report"
    foreach ($UserObject in $UserToProcess) {
    
        # Increase the counter
        $i++
        
        # Output every 100 users for progress
        if (!($i % 100)) { Write-log ("Finished " + $i + " Users") }
        
        # If no license is assigned we need to create that object
        if ($UserObject.isLicensed -eq $false) {
        
            $Object = $null
        
            # Create our object and populate in our values
            $Object = New-Object -TypeName PSobject
            $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value $UserObject.displayname
            $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $UserObject.userprincipalname
            $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value $UserObject.UsageLocation
            $Object | Add-Member -MemberType NoteProperty -Name IsDeleted -value ([bool]$UserObject.SoftDeletionTimestamp)
            $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value "UNASSIGNED"
            $Object | Add-Member -MemberType NoteProperty -Name CommonName -Value "UNASSIGNED"
            $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "Explicit"
        
            # Add the object to the output array
            #[array]$Output = $Output + $Object
            $Output.Add($Object) | Out-Null
        }
        
        #If we have a license then add that information
        else {
            # We can have more than one license on a user so we need to do each one seperatly
            foreach ($license in $UserObject.licenses) {
                $Object = $null
                
                # Get the common name of the licnese from our skuid.json
                $CommonName = Get-SKUCommonName -Skuid ((($license.accountskuid).split(":"))[-1]) -SkuArray $SkuArray
                
                # Create our object and populate in our values
                $Object = New-Object -TypeName PSobject
                $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value $UserObject.displayname
                $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $UserObject.userprincipalname
                $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value $UserObject.UsageLocation
                $Object | Add-Member -MemberType NoteProperty -Name IsDeleted -value ([bool]$UserObject.SoftDeletionTimestamp)
                $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value $license.accountskuid
                $Object | Add-Member -MemberType NoteProperty -Name CommonName -Value $CommonName

                # Add each of the plans for the license in along with its status
                foreach ($value in $license.servicestatus) {
                
                    $Object | Add-Member -MemberType NoteProperty -Name $value.serviceplan.servicename -Value $value.ProvisioningStatus
                
                }
                
                # Get the inherited from / status of the license based on GBL
                # If there are no groups in GroupsAssigningLicense then it is explicitly assigned
                if ($license.GroupsAssigningLicense.count -le 0) {
                    
                    $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "Explicit"                
                
                }
                # If it is populated then we need to process ALL values
                else {
                
                    # Null out our assigned by string
                    [string]$assignedby = $null
                    
                    # Process each group entry and work to resolve them
                    foreach ($entry in $license.GroupsAssigningLicense) {
                    
                        # If the GUID = the ObjectID then it is direct assigned
                        if ($entry.guid -eq $UserObject.objectid) { [string]$assignedby = $assignedby + "Explicit;" }
                        
                        # If it doesn't match the objectid of the user then it is a group and we need to resolve it
                        else {
                        
                            # if groupcache isn't populated yet then we know we need to look it up
                            if ($null -eq $GroupCache) {
                                [array]$GroupCache = $GroupCache + (Get-MsolGroup -objectid $entry.guid | Select-Object -Property objectid, displayname)
                            }
                            # If not null check if we have a cache hit
                            else {
                                # If the guid is in the groupcache then no action needed
                                if ($GroupCache.objectid -contains $entry.guid) { }
                                
                                # If we have a cache miss then we need to populate it into the cache
                                else { [array]$GroupCache = $GroupCache + (Get-MsolGroup -objectid $entry.guid | Select-Object -Property objectid, displayname) }
                            }
                            
                            # Add the group information from the cache to the output
                            [string]$assignedby = $assignedby + ($GroupCache | Where-Object { $_.objectid -eq $entry.guid }).DisplayName + ";"
                        }
                    
                    }
                
                    # Add the value of assignment to the object
                    $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value ($assignedby.trimend(";"))
                }
                
                
                # Add the object created from the user information to the output array
                #[array]$Output = $Output + $Object
                $Output.Add($Object) | Out-Null
            }
        }
    }
    
    # Now that we have all of our output objects in an array we need to output them to a file
    # If overwrite has been set then just take the file name as the date and force overwrite the file
    if ($OverWrite) {
        # Build our file name
        $path = Join-path (Split-Path $LogFile -Parent) ("License_Report_" + [string](Get-Date -UFormat %Y%m%d) + ".csv")
    }
    
    # Default behavior will be to create a new incremental file name
    else {
        # Build our file name
        $RootPath = Join-path (Split-Path $LogFile -Parent) ("License_Report_" + [string](Get-Date -UFormat %Y%m%d))
        $TryPath = $RootPath + "*"
        
        # Find any existing files that start with our planed file name
        $FilesInPath = Get-ChildItem $TryPath

        # Found files so we need to increment our file name with _X
        if ($FilesInPath.count -gt 0) {
            $Path = $RootPath + "_" + ($FilesInPath.count) + ".csv"
        }
        # Didn't find anything so we are good with the base file name
        else {
            $Path = $RootPath + ".csv"
        }
    }

    Write-Log ("Exporting report to " + $path)

    # Export everything to the CSV file
    $Output | Export-Csv $path -Force -NoTypeInformation
        
    # Pull it back in so we can remove our fake object
    $Temp = Import-Csv $path
    $Temp | Where-Object { $_.UserPrincipalName -ne "BASELINE_IGNORE@contoso.com" } | Export-Csv $path -Force -NoTypeInformation
    
    Write-log "Report generation finished"
}