One of my clients needed a script that would pull all the License and Add-Ons information from PowerShell and put it into a CSV that could be filtered by license or add-on. So I created this script to do just that.
It had a few revisions, but I do have final one here for usage. As always, if you have suggestions on making it better, please feel free to contact me. I am working on updating my repository on GitHub @LanceL218 and will have these scripts up there soon as well!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | <# .SYNOPSIS Get an M365 License Report of all members of a Commercial or Government Tenant M365 License Distribution. It will tell you what licneses they have based on if they have a mailbox enabled on their account. The report will be in CSV Format and will output to a predetermined directory based on the Parameters set in the script. .NOTES Name: Get-M365LicenseInfo Author: Lance Lingerfelt Version: 1.0 Modify Date: 2023-08-24 Parameter Values: $ReportsPath sets the path to a default directory and will create the directory if needed later in the script. Setting a value is NOT mandatory but script will setup a directory of C:\LDLNETScripts\Reports $TenantEXO must use one of the following values: O365USGovGCCHigh or O365USGovDoD or O365Default (for commercial) $TenantAAD must use one of the following values: AzureUSGovernment or AzureCloud (for commercial) .EXAMPLE Connect to a Government Cloud Tenant in the Script .\Get-M365LicenseInfo.ps1 -TenantEXO O365USGovGCCHigh -TenantAAD AzureUSGovernment Connect to Default Commerical Tenant in the Script .\Get-M365LicenseInfo.ps1 -TenantEXO O365Default -TenantAAD AzureCloud #> [CmdletBinding(SupportsShouldProcess = $true)] Param( [Parameter(Mandatory = $false)] [string] $ReportsPath = "C:\LDLNETScripts\Reports", [Parameter(Mandatory = $true)] [ValidateSet("O365USGovGCCHigh","O365USGovDoD", "O365Default")] $TenantEXO, [Parameter(Mandatory = $true)] [ValidateSet("AzureUSGovernment","AzureCloud")] $TenantAAD ) # ================================================ # DO NOT MODIFY BEGIN # ================================================ $ErrorActionPreference = 'SilentlyContinue' $Date = Get-Date -Format "MM/dd/yyyy" # Set Logging Configuration $Log = [PSCustomObject]@{ Path = "C:\MatlockScripts\Logs\Get-M365LicenseInfo" Name = "$($Date).log" } # ================================================ # DO NOT MODIFY END # ================================================ # ================================================ # SCRIPT BEGIN # ================================================ # Create New Logger Instance if Enabled if ($PSCmdlet.ShouldProcess("Create New Logger Instance", $Log.Path)) { # Import Logger Module try { if ( -not (Get-Module -Name PoShLog -ListAvailable) ) { Install-Module -Name PoShLog -Scope CurrentUser -Force } else { Import-Module -Name PoShLog -Force } } catch { Write-Host -Object "Unable to import logger module. Error: $($_.Exception.Message)" exit 1 } # Create New Logger Instance. Verbose logging level. Log to file and console. Start Logger. New-Logger | ` Set-MinimumLevel -Value Verbose | ` Add-SinkFile -Path "$($Log.Path)\$($Log.Name)" -OutputTemplate ` '{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}' -RollingInterval Day | ` Add-SinkConsole | ` Start-Logger # Log Start of Script Write-VerboseLog "Start of Script." } if ($PSCmdlet.ShouldProcess("Create New Exchange Online Instance", $Log.Path)) { # Import ExchangeOnlineManagement Module try { if ( -not (Get-Module -Name ExchangeOnlineManagement -ListAvailable) ) { Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser -Force } else { Import-Module -Name ExchangeOnlineManagement -Force } } catch { Write-Host -Object "Unable to import ExchangeOnlineManagement module. Error: $($_.Exception.Message)" exit 1 } if ($PSCmdlet.ShouldProcess("Create New AzureAD Instance", $Log.Path)) { # Import AzureAD Module try { if ( -not (Get-Module -Name AzureAD -ListAvailable) ) { Install-Module -Name AzureAD -Scope CurrentUser -Force } else { Import-Module -Name AzureAD -Force } } catch { Write-Host -Object "Unable to import AzureAD module. Error: $($_.Exception.Message)" exit 1 } } # Connect to EXO via ExchangeOnlineManagement Module (GCCHigh) Write-VerboseLog "Connecting Exchange Online" #Connect-ExchangeOnline -ShowBanner:$false -ExchangeEnvironmentName O365USGovGCCHigh Connect-ExchangeOnline -ShowBanner:$false -ExchangeEnvironmentName $TenantEXO # Connect to AzureAD via AzureAD Module (GCCHigh) Write-VerboseLog "Connecting AzureAD" #Connect-AzureAD -AzureEnvironmentName AzureUSGovernment Connect-AzureAD -AzureEnvironmentName $TenantAAD # Get all users with a mailbox Write-VerboseLog "Getting User Mailbox List" $users = Get-Mailbox -resultsize Unlimited # Initialize an array to store the results $licenseInfo = @() # Loop through each user to get their M365 license information Write-VerboseLog "Getting License Info For Each Mailbox User" foreach ($user in $users) { $userPrincipalName = $user.UserPrincipalName Write-Host "Checking [$userPrincipalName]" -ForegroundColor Green # Retrieve the user's M365 license information $userLicense = Get-AzureADUserLicenseDetail -ObjectId $user.ExternalDirectoryObjectId foreach ($License in $userLicense) { foreach ($SKU in $License) { Write-Host "Found License [$($SKU.SkuPartNumber)]" -ForegroundColor Cyan $licenseInfo += [PSCustomObject]@{ DisplayName = $user.DisplayName UPN = $userPrincipalName License = $SKU.SkuPartNumber Services = "" } foreach ($ServicePlan in $SKU) { foreach ($Service in $ServicePlan.ServicePlans) { Write-Host "Found Service [$($Service.ServicePlanName)]" -ForegroundColor White $licenseInfo += [PSCustomObject]@{ DisplayName = $user.DisplayName UPN = $userPrincipalName License = "" Services = $Service.ServicePlanName } } } } } } } #Create Report Path if not there if (Test-Path $ReportsPath) { #Do Nothing } else { New-Item -Type Directory -Path $ReportsPath } #Export the Results $licenseInfo | Export-Csv $ReportsPath\M365_User_Licenses_$(Get-Date -Format yyyyMMddThhmmss).csv -notypeinformation # Disconnect from Microsoft 365 PowerShell session Disconnect-AzureAD -Confirm:$False Disconnect-ExchangeOnline -Confirm:$False Write-VerboseLog "End of Script" # ================================================ # SCRIPT END # ================================================ |
It turns out a report that looks like this:
Doing the output in this manner allowed me to be able to do filtering on the License and on the Add-Ons. I think that is a better solution for folks needing to know who has what. What I would like to do next with the script is a switch parameter that would rename the SKU and Add-Ons to more palatable names so that you could give it to a C-Suite to review and they not be confused, but that would be A LOT more code. It’s something to do though!
THANKS FOR VIEWING!
TIME TO START GETTING READY FOR MS GRAPH POWERSHELL!
About Lance Lingerfelt
Lance Lingerfelt is an M365 Specialist and Evangelist with over 20 years of experience in the Information Technology field. Having worked in enterprise environments to small businesses, he is able to adapt and provide the best IT Training and Consultation possible. With a focus on AI, the M365 Stack, and Healthcare, he continues to give back to the community with training, public speaking events, and this blog.