Keep Track of new ServicePlans in M365 Licenses

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