Get-EntraProductsAndServicePlans.ps1
<#PSScriptInfo
.VERSION 1.1.1 .GUID 66d8b653-3887-4839-941b-37d6f4f459ca .AUTHOR Erlend Westervik .COMPANYNAME .COPYRIGHT .TAGS Entra, Azure, EntraID, Entra ID, Licence, Licences, Product, ID, GUID, Service, Plan, O365, Office 365, Service plan, Licensing, CSV, Identifiers, Product names, Microsoft Entra, Microsoft .LICENSEURI .PROJECTURI https://github.com/erlwes/EntraLicenseIdToProductName .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES Version: 1.0.0 - Original published version (EntraLicenseIDToProductName) Version: 1.1.0 - Re-write script to download CSV-file from same page, now that this has become avaliable, using new property names from CSV, rather than from HTLM-table (no spaces - yay!) Version: 1.1.1 - Extend on logic to only check for new versions and download if nessasary + more parameters for searching directly, rather than using filters in gridview. This uses regexp with ignore case + hacked support for negate match by starting string with "!" #> <# .DESCRIPTION Get product names from GUID or visa-versa. Search all products, service plans and lists Microsoft online service products and provide their various ID values. .PARAMETER GUID Specifies the guid of the licence you want to look up. .EXAMPLE .\Get-EntraProductsAndServicePlans.ps1 -GUID '06ebc4ee-1bb5-47dd-8120-11324bc54e06' -ProductOnly .EXAMPLE .\Get-EntraProductsAndServicePlans.ps1 | Out-GridView .EXAMPLE .\Get-EntraProductsAndServicePlans.ps1 -ProductDisplayName "^Microsoft 365 E5$" | Select-Object -ExpandProperty Service_Plans_Included_Friendly_Names .EXAMPLE .\Get-EntraProductsAndServicePlans.ps1 -ProductDisplayName "(faculty|students)" .EXAMPLE .\Get-EntraProductsAndServicePlans.ps1 | Where-Object {$_.Service_Plans_Included_Friendly_Names -match 'Microsoft Entra ID P2'} | select Product_Display_Name .EXAMPLE .\Get-EntraProductsAndServicePlans.ps1 -ForceDownload -VerboseLogging #> [CmdletBinding(DefaultParameterSetName = 'Default')] Param( [Parameter(ParameterSetName='1')] [regex]$GUID, [Parameter(ParameterSetName='2')] [regex]$ProductDisplayName, [Parameter(ParameterSetName='3')] [regex]$ServicePlanNames, [Parameter(ParameterSetName='4')] [switch]$ForceDownload, [Parameter(ParameterSetName='1')][Parameter(ParameterSetName='2')][Parameter(ParameterSetName='3')][Parameter(ParameterSetName='4')][Parameter(ParameterSetName='Default')] [switch]$VerboseLogging, [Parameter(ParameterSetName='1')][Parameter(ParameterSetName='2')][Parameter(ParameterSetName='3')][Parameter(ParameterSetName='Default')] [switch]$ProductOnly, [Parameter(ParameterSetName='1')][Parameter(ParameterSetName='2')][Parameter(ParameterSetName='3')][Parameter(ParameterSetName='Default')] [string]$PathLocalStore = "$PSScriptRoot\Product names and service plan identifiers for licensing.csv", [Parameter(ParameterSetName='Default')] [switch]$Dummy = $false ) # Function to parse HTML in PS-core on Windows function ParseHtml($string) { $unicode = [System.Text.Encoding]::Unicode.GetBytes($string) $html = New-Object -Com 'HTMLFile' if ($html.PSObject.Methods.Name -Contains 'IHTMLDocument2_Write') { $html.IHTMLDocument2_Write($unicode) } else { $html.write($Unicode) } $html.Close() $html } # Function for console-logging Function Write-Console { param( [ValidateSet(0, 1, 2, 3, 4)] [int]$Level, [Parameter(Mandatory=$true)] [string]$Message ) $Message = $Message.Replace("`r",'').Replace("`n",' ') switch ($Level) { 0 { $Status = 'Info' ;$FGColor = 'White' } 1 { $Status = 'Success' ;$FGColor = 'Green' } 2 { $Status = 'Warning' ;$FGColor = 'Yellow' } 3 { $Status = 'Error' ;$FGColor = 'Red' } 4 { $Status = 'Highlight' ;$FGColor = 'Gray' } Default { $Status = '' ;$FGColor = 'Black' } } if ($VerboseLogging) { Write-Host "$((Get-Date).ToString()) " -ForegroundColor 'DarkGray' -NoNewline Write-Host "$Status" -ForegroundColor $FGColor -NoNewline if ($level -eq 4) { Write-Host ("`t " + $Message) -ForegroundColor 'Cyan' } else { Write-Host ("`t " + $Message) -ForegroundColor 'White' } } if ($Level -eq 3) { $LogErrors += $Message } } Write-Console -Level 0 "Start" Write-Console -Level 0 "CSV: Location '$PathLocalStore' is used" # If -ForceDownload is set, set ReDownload to true also if ($ForceDownload) { $ReDownload = $true } # If CSV-file already exist, inspect it if (Test-Path $PathLocalStore) { Write-Console -Level 0 "CSV: File exist (Test-Path)" # If download is forced - delete the old file if ($ReDownload) { Write-Console -Level 0 "CSV: -ForceDownload parameter is used - will download CSV" try { Remove-Item $PathLocalStore -Confirm:$false -Force Write-Console -Level 1 "CSV: File deleted (Remove-Item)" } catch { Write-Console -Level 3 "CSV: Failed to delete file (Remove-Item). Error: $($_.Exception.Message)" } } # Else, compare todays date vs. date on csv-file to see if its more than 1d old else { [datetime]$LocalDataTimestamp = (Get-Item $PathLocalStore).LastWriteTime Write-Console -Level 0 "CSV: Local file dated '$LocalDataTimestamp'" $DaysSinceDownload = ((Get-Date) - (Get-Date $LocalDataTimestamp)).Totaldays if ($DaysSinceDownload -lt 1) { $LocalCopyCouldBeOutDated = $false Write-Console -Level 0 "CSV: Local file is less than 1 day old ($DaysSinceDownload). No need to check for newer versions online" } else { $LocalCopyCouldBeOutDated = $true Write-Console -Level 0 "CSV: Local file is more than 1 day old ($DaysSinceDownload). Will check for newer versions online" } } } # Else, we need to download it else { Write-Console -Level 0 "CSV: File not found ($PathLocalStore)" $ReDownload = $true } #If local copy more than 1 day old, or if the -ReDownload parameter is set (from -ForceDownload or otherwise), load licensing service plan reference from learn.microsoft.com if ($LocalCopyCouldBeOutDated -or $ReDownload) { # Do the webrequest try { $WR = Invoke-WebRequest -Uri 'https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference' -ErrorAction Stop Write-Console -Level 1 "Invoke-WebRequest: 'https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference' (StatusCode: $($WR.StatusCode))" } catch { Write-Console -Level 3 "Invoke-WebRequest: Failed. Error: $($_.Exception.Message)" } #Parse the web response using diffent methods depending on PowerShell version if ($host.version.Major -gt 5) { Write-Console -Level 0 -Message "Parse HTML - PowerShell Core detected ($($host.version.Major).$($host.version.Minor)). Parsing HTML using 'ParseHTLM function'" $document = ParseHtml $WR.Content $DownloadURL = $document.getElementsByTagName('a') | Select-Object -ExpandProperty href | Where-Object {$_-Match "\/download\.microsoft\.com\/download" -and $_ -match 'licensing.csv'} $OnlineDataTimestamp = (($document.getElementsByTagName('p') | Select-Object -ExpandProperty innerText | Where-Object {$_ -match "This information was last updated on"}) -split "`n")[0] -replace "This information was last updated on " -replace "\." } else { Write-Console -Level 0 -Message "Parse HTML - Windows PowerShell detected ($($host.version.Major).$($host.version.Minor)). Using built in 'parsedHtml'" $DownloadURL = $WR.ParsedHtml.getElementsByTagName('a') | Select-Object -ExpandProperty href | Where-Object {$_-Match "\/download\.microsoft\.com\/download" -and $_ -match 'licensing.csv'} $OnlineDataTimestamp = (($WR.ParsedHtml.getElementsByTagName('p') | Select-Object -ExpandProperty innerText | Where-Object {$_ -match "This information was last updated on"}) -split "`n")[0] -replace "This information was last updated on " -replace "\." } # If re-download parameter is not used, compare local version vs. online to see if the local copy still needs updating if (!$ReDownload) { $DaysSinceLastUpdate = ($LocalDataTimestamp - (Get-Date $OnlineDataTimestamp)).Totaldays #Negative values means that the online version is newer. if ($DaysSinceLastUpdate -le 0) { $LocalCopyIsOutDated = $true Write-Console -Level 2 -Message "Compare versions - Online version is newer than local copy of CSV. Will re-download." } else { $LocalCopyIsOutDated = $false Write-Console -Level 0 -Message "Compare versions - Local version not older than online copy of CSV. Will use local copy." (Get-Item $PathLocalStore).LastWriteTime = Get-Date Write-Console -Level 0 -Message "Get-Item - Updated 'LastWriteTime' on '$PathLocalStore' to todays date ($(Get-Date)), so that next intra-day runs can skip online checks." } } # If re-download parameter is used, or if ($ReDownload -or $LocalCopyIsOutDated) { try { Invoke-WebRequest -Uri $DownloadURL -OutFile "C:\Script\Product names and service plan identifiers for licensing.csv" -ErrorAction Stop Write-Console -Level 1 "Invoke-WebRequest: Downloaded ($PathLocalStore)" } catch { Write-Console -Level 3 "Invoke-WebRequest: Failed to download ($PathLocalStore). Error: $($_.Exception.Message)" } } } # Import CSV (same location regardless if it was must redownloaded or cache was used, so import it) if (!$ForceDownload) { try { $Lookup = Import-Csv '.\Product names and service plan identifiers for licensing.csv' -Delimiter ',' -Encoding UTF8 -ErrorAction Stop Write-Console -Level 1 'CSV: Imported (Import-Csv)' } catch { Write-Console -Level 3 "CSV: Failed to import (Import-Csv). Error: $($_.Exception.Message)" } } # Download forced? Dont display results if ($ForceDownload) { Write-Console -Level 0 "End" Break } # Lookup on GUID elseif ($GUID) { $GUID = "(?i)$GUID" Write-Console -Level 0 "Lookup: Finding results with 'GUID' matching regexp '$GUID'" $Result = $Lookup | Where-Object {$_.GUID -match $GUID} } # Lookup on product name elseif ($ProductDisplayName) { $ProductDisplayName = "(?i)$ProductDisplayName" if ($ProductDisplayName -match "!") { $ProductDisplayName = $ProductDisplayName -replace "!" Write-Console -Level 0 "Lookup: Finding results with 'Product_Display_Name' not matching regexp '$ProductDisplayName'" $Result = $Lookup | Where-Object {$_.Product_Display_Name -notmatch $ProductDisplayName} } else { Write-Console -Level 0 "Lookup: Finding results with 'Product_Display_Name' matching regexp '$ProductDisplayName'" $Result = $Lookup | Where-Object {$_.Product_Display_Name -match $ProductDisplayName} } } # Lookup on product name elseif ($ServicePlanNames) { $ServicePlanNames = "(?i)$ServicePlanNames" if ($ServicePlanNames -match "!") { $ServicePlanNames = $ServicePlanNames -replace "!" Write-Console -Level 0 "Lookup: Finding results with 'Service_Plan_Name' -or 'Service_Plans_Included_Friendly_Names' not matching regexp '$ServicePlanNames'" $Result = $Lookup | Where-Object {$_.Service_Plan_Name -notmatch $ServicePlanNames -or $_.Service_Plans_Included_Friendly_Names -match $ServicePlanNames} } else { Write-Console -Level 0 "Lookup: Finding results with 'Service_Plan_Name' -or 'Service_Plans_Included_Friendly_Names' matching regexp '$ServicePlanNames'" $Result = $Lookup | Where-Object {$_.Service_Plan_Name -match $ServicePlanNames -or $_.Service_Plans_Included_Friendly_Names -match $ServicePlanNames} } } # If no "search"-parameters are provided, return everything elseif (!$GUID -and !$ProductDisplayName -and !$ServicePlanNames) { $Result = $Lookup } # If -ProductOnly is specified, dont return service plan properties if ($ProductOnly) { $Result = $Result | Select-Object Product_Display_Name, String_Id, GUID -Unique } Write-Console -Level 0 "End" # Return the results Return $Result |