Keep Track of new ServicePlans in M365 Licenses

Hi All,
During the last couple Months, there have been several M365 Licenses added and activated for all Users. For Example:
- Avatars for Teams
- Copilot (formerly known as Bing Chat Enterprise)
- Clipchamp


I was thinking about, how to Monitor the Licenses in the Tenant for new ServicePlans associated to the Licences.
So i decided to add the ServicePlans to the Script i’ve already had
#Connect-MgGraph
Write-Output "Connect-MgGraph"
Connect-MgGraph -Scopes User.ReadWrite.All, Directory.ReadWrite.All -NoWelcome
###############################################################################
# SKU's an License with MgGraph and PSCustomObject
# https://blog.icewolf.ch/archive/2021/11/29/hinzufugen-und-entfernen-von-m365-lizenzen-mit-microsoft-graph-powershell/
###############################################################################
#Array with all needed Properties using PSCustomObject
Write-Output "Create PSCustomObject"
$ArraySKUS = @()
$SKUS = Get-MgSubscribedSku
Foreach ($SKU in $SKUS)
{
#$ArraySKU = @()
$AppliesTo = $SKU.AppliesTo
$CapabilityStatus = $SKU.CapabilityStatus
$SkuId = $SKU.SkuId
$SkuPartNumber = $SKU.SkuPartNumber
[array]$ServicePlans = $SKU.ServicePlans.ServicePlanName
$ConsumedUnits = $SKU.ConsumedUnits
$Enabled = $SKU.PrepaidUnits.Enabled
$Suspended = $SKU.PrepaidUnits.Suspended
$Warning = $SKU.PrepaidUnits.Warning
$SKUObject = [PSCustomObject]@{
AppliesTo = $AppliesTo
CapabilityStatus = $CapabilityStatus
SkuId = $SkuId
SkuPartNumber = $SkuPartNumber
ServicePlans = $ServicePlans
ConsumedUnits = $ConsumedUnits
Enabled = $Enabled
Suspended = $Suspended
Warning = $Warning
}
$ArraySKUS += $SKUObject
}


The Object contains all Licenses in the Tenant including the amount and consumed Licenses
$ArraySKUS | FT AppliesTo, CapabilityStatus, SkuId, SkuPartNumber, ConsumedUnits, Enabled, Suspended, Warning


The Object now also contains the Service Plans for each SKU
$ArraySKUS | FT AppliesTo, CapabilityStatus, SkuPartNumber, ServicePlans


I’ve exported the Object to an XML File
#Export Object
Write-Output "Export PSCustomObject"
$ArraySKUS | Export-Clixml C:\temp\ArraySKUS.xml


This is how the XML File looks like. For “ENTERPRISEPACK” (M365 E3) i have removed the “VIVAENGAGE_CORE”


Now let’s import the XML and compare it with the ServicePlans Array
#Import Object
$SavedObject = Import-Clixml -Path C:\temp\ArraySKUS.xml
$Output = @()
$INT = 0
Foreach ($Line in $ArraySKUs)
{
$SkuPartNumber = $Line.SkuPartNumber
Write-Output "Working on: $SkuPartNumber" #-ForegroundColor Green
$DifferenceObject = $SavedObject[$INT].ServicePlans
$CompareResult = Compare-Object -ReferenceObject $Line.ServicePlans -DifferenceObject $DifferenceObject #-PassThru
If ($Null -ne $CompareResult)
{
#$CompareResult
Foreach ($Item in $CompareResult)
{
Write-Output "Diffrence found: $($Item.InputObject) $($Item.SideIndicator)" #-ForegroundColor Yellow
switch ($Item.SideIndicator)
{
"<=" {$Description = "Add"}
"=>" {$Description = "Remove"}
Default {$Description = ""}
}
$OutputObject = [PSCustomObject]@{
SkuPartNumber = $SkuPartNumber
ServicePlan = $($Item.InputObject)
SideIndicator = $($Item.SideIndicator)
Description = $Description
}
$Output += $OutputObject
}
}
$INT = $Int + 1
}
The Diffrence has been detected. We have now some Proof of Concept Code.


Script in Azure Automation
I’ve decided to put the Code in an Azure Automation PowerShell Runbook
The Code for the Runbook has been published in my GitHub Repo
###############################################################################
# M365 License Compare
# Compares the Licenses in a M365 Tenant with an exported Object stored on Azure FileShare
# 03.12.2023 - Initial Version - Andres Bohren https://blog.icewolf.ch
###############################################################################
# Required Infrastructure
# - Azure Automation Account with System Assigned Managed Identity
# - Azure Storageaccount with FileShare
# Required Permissions
# - Azure Automation Account Managed Identity must be member of "License Administrator" Role
# - EntraID Application with Application Mail.Send Permissions (for Sending Mail via Microsoft Graph)
# - App can be limited with ApplicationAccessPolicies to specific Mailboxes
# https://blog.icewolf.ch/archive/2021/02/06/limit-microsoft-graph-access-to-specific-exchange-mailboxes/
# Required Modules:
# - Az.Accounts
# - Az.Storage
# - Microsoft.Graph.Authentication
# - Microsoft.Graph.Identity.DirectoryManagement
# Required Automation Account Variables
# - StorageAccountName
# - StorageAccountKey
# - LicenseCompareShare (csv)
# - LicenseCompareFile (ArraySKUS.xml)
# - DelegatedMailAppID (AppID)
# - TenantGuId (TenantID GUID)
# Automation Account Certificate
# - AutomationCertificate for Authentication to Entra AppID for Sending Mail
We need an Azure Storage Account with a FileShare “csv” in my case


We need the Access Key to be able to read and write to the Azure Storage Account


In the Azure Automation Account we need to Add the Modules for the PowerShell Version of the Runbook you intend to use


If not enabled, activate System assigned Managed Identity


Add the System Managed Identity to the “License Administrator” Entra ID Role


Add the Variables to the Azure Automation Account


Upload the Certificate (*.pfx) File to Certificates in the Azure Automation Account. The Certificate is used for Authenticate with the EntraID Application with Application Mail.Send Permissions for sending the Admin Mail


this is how it looks like if we run the Runbook


First Run - nothing to compare


I’ve uploaded the XML to the Azure Storage and removed the “VIVAENGAGE_CORE” in “ENTERPRISEPACK”. Now we can see that there has been added a Package in Comparsion to the XML on the Azure Fileshare.


Run again - nothing changed


As a last step, you can link the Azure Automation Runbook with a Shedule


Summary
Hope this helps you keep Track of new ServicePlans that are added to your Tenant Licenses.
Regards
Andres Bohren

Azure Logo


M365 Logo


PowerShell Logo
