2024-05-17 18:55:02 +05:30
<#
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Name : Find & Export Microsoft 365 User License Assignment Paths using PowerShell
2025-08-21 10:16:47 +05:30
Version : 2.0
2024-05-17 18:55:02 +05:30
website : o365reports . com
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
Script Highlights :
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
1 . The script uses MS Graph PowerShell and installs MS Graph PowerShell SDK ( if not installed already ) upon your confirmation .
2 . The script can be executed with MFA enabled account too .
3 . Exports directly assigned licenses alone .
4 . Exports group-based license assignments alone .
5 . Helps to identify users with license assignment errors .
6 . Converts SKU name into user-friendly name .
7 . Produces a list of disabled service plans for the assigned license .
8 . Exports report results as a CSV file .
9 . The script is scheduler friendly .
10 . It can be executed with certificate-based authentication ( CBA ) too .
For detailed Script execution : https : / / o365reports . com / 2024 / 05 / 14 / find-export -microsoft - 365 -user -license -assignment -paths -using -powershell /
2025-08-21 10:16:47 +05:30
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
Change Log :
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
V1 . 0 ( May 17 , 2024 ) – File created .
V2 . 0 ( Aug 21 , 2025 ) – Upgraded from the Graph beta module to the Graph module , implemented error handling for retrieving expired trial subscriptions , and upgraded to use new LicenseFriendlyName . csv for license mapping .
2024-05-17 18:55:02 +05:30
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
#>
Param
(
[ switch ] $ShowDirectlyAssignedLicenses ,
[ switch ] $ShowGrpBasedLicenses ,
[ switch ] $DisabledUsersOnly ,
[ Switch ] $FindUsersWithLicenseAssignmentErrors ,
[ switch ] $CreateSession ,
[ string ] $TenantId ,
[ string ] $ClientId ,
[ string ] $CertificateThumbprint
)
Function Connect_MgGraph
{
#Check for module installation
2025-08-21 10:16:47 +05:30
$Module = Get-Module -Name microsoft . graph -ListAvailable
2024-05-17 18:55:02 +05:30
if ( $Module . count -eq 0 )
{
Write-Host Microsoft Graph PowerShell SDK is not available -ForegroundColor yellow
$Confirm = Read-Host Are you sure you want to install module ? [ Y] Yes [N ] No
if ( $Confirm -match " [yY] " )
{
Write-host " Installing Microsoft Graph PowerShell module... "
2025-08-21 10:16:47 +05:30
Install-Module Microsoft . Graph -Repository PSGallery -Scope CurrentUser -AllowClobber -Force
2024-05-17 18:55:02 +05:30
}
else
{
2025-08-21 10:16:47 +05:30
Write-Host " Microsoft Graph PowerShell module is required to run this script. Please install module using 'Install-Module Microsoft.Graph' cmdlet. "
2024-05-17 18:55:02 +05:30
Exit
}
}
#Disconnect Existing MgGraph session
if ( $CreateSession . IsPresent )
{
Disconnect-MgGraph
}
Write-Host Connecting to Microsoft Graph . . .
if ( ( $TenantId -ne " " ) -and ( $ClientId -ne " " ) -and ( $CertificateThumbprint -ne " " ) )
{
Connect-MgGraph -TenantId $TenantId -AppId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome
}
else
{
Connect-MgGraph -Scopes " User.Read.All " , " AuditLog.read.All " -NoWelcome
}
}
Function Convert-FrndlyName {
param (
[ Parameter ( Mandatory = $true ) ]
[ Array ] $InputIds
)
2025-08-21 10:16:47 +05:30
$EasyName = $FriendlyNameHash [ $InputIds ]
2024-05-17 18:55:02 +05:30
if ( ! ( $EasyName ) )
2025-08-21 10:16:47 +05:30
{ $NamePrint = $InputIds }
2024-05-17 18:55:02 +05:30
else
{ $NamePrint = $EasyName }
return $NamePrint
}
2025-08-21 10:16:47 +05:30
2024-05-17 18:55:02 +05:30
Connect_MgGraph
2025-08-21 10:16:47 +05:30
2024-05-17 18:55:02 +05:30
$Location = Get-Location
$ExportCSV = " $Location \M365Users_LicenseAssignmentPath_Report_ $( ( Get-Date -format yyyy-MMM -dd -ddd ` hh-mm -ss ` tt ) . ToString ( ) ) .csv "
$ExportResult = " "
2025-08-21 10:16:47 +05:30
$ExportResults = @ ( )
2024-05-17 18:55:02 +05:30
#Get license in the organization and saving it as hash table
$SKUHashtable = @ { }
2025-08-21 10:16:47 +05:30
Get-MgSubscribedSku – All | foreach {
2024-05-17 18:55:02 +05:30
$SKUHashtable [ $_ . skuid ] = $_ . Skupartnumber
}
2025-08-21 10:16:47 +05:30
#Get Licenses
$FriendlyNameHash = @ { }
Import-Csv -Path . \ LicenseFriendlyName . csv -ErrorAction Stop | ForEach-Object {
$FriendlyNameHash [ $_ . String_Id ] = $_ . Product_Display_Name
}
2024-05-17 18:55:02 +05:30
#Get friendly name of Service plan from external file
$ServicePlanHash = @ { }
Import-Csv -Path . \ ServicePlansFrndlyName . csv | ForEach-Object {
$ServicePlanHash [ $_ . ServicePlanId ] = $_ . ServicePlanFriendlyNames
}
$GroupNameHash = @ { }
#Process users
$RequiredProperties = @ ( 'UserPrincipalName' , 'DisplayName' , 'EmployeeId' , 'CreatedDateTime' , 'AccountEnabled' , 'Department' , 'JobTitle' , 'LicenseAssignmentStates' , 'AssignedLicenses' , 'SigninActivity' )
2025-08-21 10:16:47 +05:30
$Count = 0
Get-MgUser -All -Property $RequiredProperties | ForEach-Object {
2024-05-17 18:55:02 +05:30
$Count + +
$Print = 1
$DirectlyAssignedLicense = @ ( )
$GroupBasedLicense = @ ( )
$DirectlyAssignedLicense_FrndlyName = @ ( )
$GroupBasedLicense_FrndlyName = @ ( )
$UPN = $_ . UserPrincipalName
Write-Progress -Activity " `n Processing user: $Count - $UPN "
$DisplayName = $_ . DisplayName
$AccountEnabled = $_ . AccountEnabled
$Department = $_ . Department
$JobTitle = $_ . JobTitle
$LastSignIn = $_ . SignInActivity . LastSignInDateTime
if ( $LastSignIn -eq $null )
{
$LastSignIn = " Never Logged In "
$InactiveDays = " - "
}
else
{
$InactiveDays = ( New-TimeSpan -Start $LastSignIn ) . Days
}
$LicenseAssignmentStates = $_ . LicenseAssignmentStates
if ( $AccountEnabled -eq $true )
{
$AccountStatus = 'Enabled'
}
else
{
$AccountStatus = 'Disabled'
}
foreach ( $License in $licenseAssignmentStates )
{
2025-08-21 10:16:47 +05:30
$SkuName = $SkuHashtable [ $License . SkuId ] #When activated trial ends, it will not be available in the Get-MgBetaSubscribedSKU. In that case, respective SKU name will be empty.
if ( $SkuName -ne $null )
{
$FriendlyName = Convert-FrndlyName -InputIds $SkuName
}
else
{
$FriendlyName = $License . SkuId
}
2024-05-17 18:55:02 +05:30
$DisabledPlans = $License . DisabledPlans
$ServicePlanNames = @ ( )
if ( $DisabledPlans . count -ne 0 )
{
foreach ( $DisabledPlan in $DisabledPlans )
{
$ServicePlanName = $ServicePlanHash [ $DisabledPlan ]
if ( ! ( $ServicePlanName ) )
{ $NamePrint = $DisabledPlan }
else
{ $NamePrint = $ServicePlanName }
$ServicePlanNames + = $NamePrint
}
}
$DisabledPlans = $ServicePlanNames -join " , "
$State = $License . State
2025-08-21 10:16:47 +05:30
$LicenseError = $License . Error
2024-05-17 18:55:02 +05:30
#Filter for users with license assignment errors
if ( $FindUsersWithLicenseAssignmentErrors . IsPresent -and ( $State -eq " Active " ) )
{
$Print = 0
}
if ( $License . AssignedByGroup -eq $null )
{
$LicenseAssignmentPath = " Directly assigned "
$GroupName = " NA "
#Filter for group based license assignment
if ( $ShowGrpBasedLicenses . IsPresent )
{
$Print = 0
}
}
else
{
$LicenseAssignmentPath = " Inherited from group "
#Filter for directly assigned licenses
if ( $ShowDirectlyAssignedLicenses . IsPresent )
{
$Print = 0
}
$AssignedByGroup = $License . AssignedByGroup
# Check Id-Name pair already exist in hash table
if ( $GroupNameHash . ContainsKey ( $AssignedByGroup ) )
{
$GroupName = $GroupNameHash [ $AssignedByGroup ]
}
else
{
2025-08-21 10:16:47 +05:30
$GroupName = ( Get-MgGroup -GroupId $AssignedByGroup ) . DisplayName
2024-05-17 18:55:02 +05:30
$GroupNameHash [ $AssignedByGroup ] = $GroupName
}
}
if ( $Print -eq 1 )
{
2025-08-21 10:16:47 +05:30
$ExportResult = [ PSCustomObject ] @ { 'Display Name' = $DisplayName ; 'UPN' = $UPN ; 'License Assignment Path' = $LicenseAssignmentPath ; 'Sku Name' = $SkuName ; 'Sku_FriendlyName' = $FriendlyName ; 'Disabled Plans' = $DisabledPlans ; 'Assigned via(group name)' = $GroupName ; 'State' = $State ; 'Error' = $LicenseError ; 'Last Signin Time' = $LastSignIn ; 'Inactive Days' = $InactiveDays ; 'Account Status' = $AccountStatus ; 'Department' = $Department ; 'Job Title' = $JobTitle }
2024-05-17 18:55:02 +05:30
$ExportResult | Export-Csv -Path $ExportCSV -Notype -Append
}
}
}
2025-08-21 10:16:47 +05:30
Disconnect-MgGraph | Out-Null
2024-05-17 18:55:02 +05:30
#Open output file after execution
2025-08-21 10:16:47 +05:30
Write-Host ` nScript executed successfully .
2024-05-17 18:55:02 +05:30
if ( ( Test-Path -Path $ExportCSV ) -eq " True " )
{
Write-Host ` n ~ ~ Script prepared by AdminDroid Community ~ ~ ` n -ForegroundColor Green
2025-08-21 10:16:47 +05:30
Write-Host " ~~ Check out " -NoNewline -ForegroundColor Green ; Write-Host " admindroid.com " -ForegroundColor Yellow -NoNewline ; Write-Host " to access 3,000+ reports and 450+ management actions across your Microsoft 365 environment. ~~ " -ForegroundColor Green ` n ` n
2024-05-17 18:55:02 +05:30
$Prompt = New-Object -ComObject wscript . shell
$UserInput = $Prompt . popup ( " Do you want to open output file? " , ` 0 , " Open Output File " , 4 )
if ( $UserInput -eq 6 )
{
Invoke-Item " $ExportCSV "
}
2025-08-21 10:16:47 +05:30
Write-Host ` nDetailed report available in : -NoNewline -ForegroundColor Yellow
Write-Host $ExportCSV
2024-05-17 18:55:02 +05:30
}
else
{
Write-Host " No user found " -ForegroundColor Red
2025-08-21 10:16:47 +05:30
}