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