Summary
This article explains how to retrieve all active Microsoft 365 subscriptions in your tenant using Microsoft Graph PowerShell and map the technical SKU identifiers to friendly product names. The result is a clear CSV export that includes subscription status, license counts, and lifecycle details.
The quick and dirty version of this script, that does not map the Product Names, looks like this:
Connect-MgGraph -Scopes "Directory.Read.All" Get-MgDirectorySubscription | Select-Object SkuPartNumber, NextLifecycleDateTime
Purpose
The script connects to Microsoft Graph with Directory.Read.All permissions, retrieves subscription data, and matches the SkuPartNumber to a friendly name based on the official Microsoft SKU reference file. The output helps administrators quickly understand which products are active, when they were created, and how many licenses are available.
Prerequisites
- PowerShell 5.1 or later (or PowerShell 7)
- Microsoft Graph PowerShell SDK installed
Run:Install-Module Microsoft.Graph -Scope CurrentUser - Sign-in credentials with permission to read directory data
How to Run
- Open PowerShell as Administrator.
- Copy and paste the full script below into a new file named Get-TenantSubscriptionsFriendlyNames.ps1.
- Run the script:
.\Get-TenantSubscriptionsFriendlyNames.ps1 - When prompted, sign in to Microsoft Graph.
- After completion, check the output CSV file in the same folder — it will be named like:
2025-10-28_14-35-02_SubscriptionReport_FriendlyNames.csv
Output
The exported file includes the following columns:
- SkuPartNumber
- FriendlyProductName
- SkuId
- CreatedDateTime
- NextLifecycleDateTime
- Status
- TotalLicenses
The file uses a semicolon (;) as delimiter for easier import into Excel.
PowerShell Script
<#
.SYNOPSIS
Retrieves Microsoft Graph subscription data for the tenant and translates SKU identifiers to friendly product names.
.DESCRIPTION
This script connects to Microsoft Graph using the required scope (Directory.Read.All),
retrieves subscription/license data from the tenant (including SkuPartNumber, SkuId, CreatedDateTime,
NextLifecycleDateTime, Status, TotalLicenses),
downloads a SKU-to-friendly-name mapping file, builds a lookup table,
maps each subscription record to a friendly product name (if available),
and produces console output and an exported CSV report with a timestamped filename.
.PARAMETER mappingUrl
The URL of the CSV file containing the mapping of String_ID / SkuPartNumber to friendly Product_Display_Name.
Default value: https://raw.githubusercontent.com/InfoSecGeoff/365SKUTranslator/main/365Products.csv
.PARAMETER exportDirectory
The directory where the resulting CSV report will be saved.
Default value: the current directory.
.INPUTS
None. The script does not accept piped input.
.OUTPUTS
System.Management.Automation.PSCustomObject[] — An array of custom objects, each containing:
SkuPartNumber, FriendlyProductName, SkuId, CreatedDateTime,
NextLifecycleDateTime, Status, TotalLicenses
Also produces a CSV file at the location specified.
.EXAMPLE
PS C:\> .\Get-TenantSubscriptionsFriendlyNames.ps1
Runs the script with default mapping URL and default export path. Produces console output and a CSV file
named “2025-10-28_14-35-02_SubscriptionReport_FriendlyNames.csv” (with current timestamp) in the current directory.
.EXAMPLE
PS C:\> .\.\Get-TenantSubscriptionsFriendlyNames.ps1 -mappingUrl "https://example.com/myMapping.csv" -exportDirectory "C:\Reports"
Runs the script using a custom mapping URL and exports the report to the given directory.
.NOTES
Author: YourNameHere
Date: 2025-10-28
Version: 1.0
Requirements: Microsoft Graph PowerShell module installed, account with Directory.Read.All permission,
PowerShell version 3.0 or higher. If Invoke-WebRequest is unavailable, the script will
fallback to using System.Net.WebClient.DownloadString for downloading the mapping file.
#>
Param (
[string]$mappingUrl = "https://raw.githubusercontent.com/InfoSecGeoff/365SKUTranslator/main/365Products.csv",
[string]$exportDirectory = "."
)
# Import Graph module if needed
# clImport-Module Microsoft.Graph -ErrorAction SilentlyContinue
Write-Host "=> Connecting to Microsoft Graph…" -ForegroundColor Cyan
Connect-MgGraph -Scopes "Directory.Read.All"
Write-Host "✔ Connected." -ForegroundColor Green
Write-Host "=> Retrieving subscription data…" -ForegroundColor Cyan
$skuData = Get-MgDirectorySubscription -Property SkuPartNumber, SkuId, CreatedDateTime, NextLifecycleDateTime, Status, TotalLicenses |
Select-Object SkuPartNumber, SkuId, CreatedDateTime, NextLifecycleDateTime, Status, TotalLicenses
Write-Host ("✔ Retrieved {0} subscription records." -f $skuData.Count) -ForegroundColor Green
Write-Host "=> Downloading SKU mapping file…" -ForegroundColor Cyan
$csvContent = $null
if (Get-Command -Name Invoke-WebRequest -ErrorAction SilentlyContinue) {
try {
$response = Invoke-WebRequest -Uri $mappingUrl -UseBasicParsing -ErrorAction Stop
$csvContent = $response.Content
Write-Host "✔ Mapping file downloaded (Invoke-WebRequest)." -ForegroundColor Green
}
catch {
Write-Host "WARNING: Invoke-WebRequest failed: $_. Trying fallback download…" -ForegroundColor Yellow
}
}
if (-not $csvContent) {
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webClient = New-Object System.Net.WebClient
$csvContent = $webClient.DownloadString($mappingUrl)
Write-Host "✔ Mapping file downloaded (WebClient fallback)." -ForegroundColor Green
}
catch {
Write-Host "ERROR: Could not download mapping file via fallback: $_" -ForegroundColor Red
exit 1
}
}
try {
$mapping = $csvContent | ConvertFrom-Csv
Write-Host ("✔ Mapping rows: {0}" -f $mapping.Count) -ForegroundColor Green
}
catch {
Write-Host "ERROR: Could not parse CSV content: $_" -ForegroundColor Red
exit 1
}
Write-Host "=> Building lookup table…" -ForegroundColor Cyan
$mapByStringId = @{}
foreach ($m in $mapping) {
if ($m.String_ID) {
$mapByStringId[$m.String_ID] = $m.Product_Display_Name
}
}
Write-Host ("✔ Lookup table contains {0} keys." -f $mapByStringId.Keys.Count) -ForegroundColor Green
Write-Host "=> Processing and mapping subscription data…" -ForegroundColor Cyan
$counter = 0
$total = $skuData.Count
$report = $skuData | ForEach-Object {
$counter++
Write-Progress -Activity "Mapping SKUs" -Status ("Processing {0}/{1}" -f $counter, $total) -PercentComplete (($counter/$total)*100)
$skuPart = $_.SkuPartNumber
$skuId = $_.SkuId
$friendly = if ($mapByStringId.ContainsKey($skuPart)) { $mapByStringId[$skuPart] } else { "Unknown product for $skuPart" }
[PSCustomObject]@{
SkuPartNumber = $skuPart
FriendlyProductName = $friendly
SkuId = $skuId
CreatedDateTime = $_.CreatedDateTime
NextLifecycleDateTime = $_.NextLifecycleDateTime
Status = $_.Status
TotalLicenses = $_.TotalLicenses
}
}
Write-Progress -Activity "Mapping SKUs" -Completed
Write-Host "✔ Processing complete." -ForegroundColor Green
Write-Host "=> Displaying report:" -ForegroundColor Cyan
$report | Format-Table -AutoSize
# Generate timestamp and export filename
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$exportFilename = "{0}_SubscriptionReport_FriendlyNames.csv" -f $timestamp
$exportPath = Join-Path -Path $exportDirectory -ChildPath $exportFilename
Write-Host ("=> Exporting report to '{0}'…" -f $exportPath) -ForegroundColor Cyan
$report | Export-Csv -Path $exportPath -NoTypeInformation -Delimiter ";"
Write-Host "✔ Export done." -ForegroundColor Green
Write-Host "=== Script finished successfully ===" -ForegroundColor Magenta
Troubleshooting
- If
Invoke-WebRequestfails, the script automatically uses a .NET WebClient fallback. - If no data is retrieved, confirm your Graph permissions include
Directory.Read.All. - If you encounter “Unknown product” entries, the SKU may not yet exist in the Microsoft mapping file.