Monitor and report license availability within the Microsoft 365 Tenant

When Microsoft 365 licenses are distributed automatically via groups, it can happen that there are no available licenses left and an employee is not assigned a license. By default, administrators do not receive notifications when this occurs. With the power of Graph API, PowerShell and Azure Automation you can check the available licenses and create a custom notification via email. This script queries the subscribedSkus API in Graph. To schedule the script to run daily an Azure Automation Runbook will be used.

Prerequisites:

  • Azure Subscription
  • Account within the enough permissions to create an App Registration and consent permissions
  • PowerShell script – Check_MS365_licenses.ps1


Create the App Registration

Firstly, create a new App Registration so the script is able to read the SKU’s via the Graph API.

Go to Azure Active Directory and go to App Registrations.

Click New Registration

Fill in a name for the App Registration and leave the rest of the options as the default settings.  

Click Register

Copy the Application (client) ID and Directory (tenant) ID and save them for later use in the script.

Go to API Permissions and set the following permissions for the Graph API

Mail.Send - Application
Organization.Read.All - Application
User.Read - Delegated

Grant Admin consent afterwards


Now create a secret via Certificates & secrets

Click New client secret

Fill in a name for client secret and choose the expiry date. Click Add to add the secret the the app registration

Copy the Value and save it for later use in the script.

Change the PowerShell script

Now open the PowerShell script and change the following values with the values that were copied earlier:

    $MailSender 
    $MailReceiver 
    $AppID 
    $AppSecret
    $TenantID 
  • Optionally change the Freeunits value to accommodate the situation
  • Optionally change the $FilteredObjects variable to filter SKU’s


Save the PowerShell script to upload into a Azure Automation Runbook later on.


Schedule the script via Azure Automation

Now create an Azure Automation Account to schedule the script to run daily.

Go to the Azure Portal and go to Automation Accounts.

Click Create

Select the subscription and choose a Resource Group or create a new one. Give the Automation a name and click Review + Create, after the review click Create

Wait until the resource is created select your newly created automation account.

Now create a Runbook and upload the script there

Go to Runbooks

Click Create a runbook

Fill in a name for the Runbook and choose Runbook Type : Powershell and Runtime version 5.1, then click Create

After the creation the runbook, the script editor will be opened. Copy the PowerShell script and paste it into the script editor.

Click Save, then click Publish

Click Yes to Proceed

Now go back to the Runbook overview and click Start to execute the script.

If everything is configured correctly and if there are SKU’s below 10 free licenses, a mail message should arrive in the mailbox of the $MailReceiver set in the PowerShell script.

To schedule this check on a daily basis, select your newly create Runbook. Then go to Schedules.

Click Add a schedule

Click Schedule and then Add a Schedule

Fill in a name for the schedule and choose a time when the script has to execute. Then click Create.

Leave the parameters and run settings Default:Azure and click OK

Everything done now! The script should now be scheduled and will report via E-mail if there are any licensing issues within your MS365 Tenant.

<# 
.SYNOPSIS
    This script queries the Graph API to check for low availability of MS365 Licences.

.DESCRIPTION 
    This script queries the Graph API to check if there are still enough available licenses within the MS365 Tenant. If the availability is below x licenses it will send an e-mail with a warning. 

    This script must be used in combination with an Application Registration within Azure AD. The permissions needed for the App Registration are:

    Mail.Send - Application
    Organization.Read.All - Application
    User.Read - Delegated

    Variables that should be changed within the Script:
        $MailSender
        $MailReceiver
        $AppID
        $AppSecret
        $TenantID
        $Filteredobjects for SKU's you want to filter
 
.NOTES 
    Filename: Check_MS365_licenses.ps1
    Author: Remco van Diermen
    Version: 1.0

.COMPONENT 
    Built in Powershell v5.1

.LINK 
    https://www.remcovandiermen.nl
     
#>


Function AlertMail

{

#From which e-mailaddress is the mail sent from
$MailSender = "<SENDER MAIL ADDRESS>"

#From which e-mailaddress is the mail sent to
$MailReceiver = "<RECEIVER MAIL ADDRESS>"

$Attachment= "$env:TEMP\SKU.csv"
$FileName=(Get-Item -Path $Attachment).name
$base64string = [Convert]::ToBase64String([IO.File]::ReadAllBytes($Attachment))

#Connect to GRAPH API
$tokenBody = @{
    Grant_Type    = "client_credentials"
    Scope         = "https://graph.microsoft.com/.default"
    Client_Id     = $AppId
    Client_Secret = $AppSecret
}
$tokenResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $tokenBody
$headers = @{
    "Authorization" = "Bearer $($tokenResponse.access_token)"
    "Content-type"  = "application/json"
}

#Send Mail    
$URLsend = "https://graph.microsoft.com/v1.0/users/$MailSender/sendMail"
$BodyJsonsend = @"
                    {
                        "message": {
                          "subject": "You have Microsoft 365 License Warning(s)",
                          "body": {
                            "contentType": "HTML",
                            "content": "There are licensing warnings present in your Microsoft 365 Tenant. <br>
                            Please review the attachment <br>
                            
                            "
                          },
                          "toRecipients":[ 
                            {
                              "emailAddress": {
                                "address": "$MailReceiver"
                              }
                            }
                          ]
                        ,
                        "attachments": [
                            {
                              "@odata.type": "#microsoft.graph.fileAttachment",
                              "name": "$FileName",
                              "contentType": "text/plain",
                              "contentBytes": "$base64string"
                            }
                          ]
                        },
                        "saveToSentItems": "false"
                      }
"@

Invoke-RestMethod -Method POST -Uri $URLsend -Headers $headers -Body $BodyJsonsend
write-Output "Warnings Found, E-mail was sent"
}


# Define AppId, secret and scope, your tenant name and endpoint URL
$AppId = "<APP ID FOR APP REGISTRATION>"
$TenantId = "<TENANT ID>"
$AppSecret = '<APP REGISTRATION SECRET>'
$Scope = "https://graph.microsoft.com/.default"

$Url = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"


# Add System.Web for urlencode
Add-Type -AssemblyName System.Web

# Create body
$Body = @{
    client_id = $AppId
	client_secret = $AppSecret
	scope = $Scope
	grant_type = 'client_credentials'
}

# Splat the parameters for Invoke-Restmethod for cleaner code
$PostSplat = @{
    ContentType = 'application/x-www-form-urlencoded'
    Method = 'POST'
    # Create string by joining bodylist with '&'
    Body = $Body
    Uri = $Url
}

# Request the token!
$Request = Invoke-RestMethod @PostSplat

# Create header
$Header = @{
    Authorization = "$($Request.token_type) $($Request.access_token)"
}

$Uri = "https://graph.microsoft.com/v1.0/subscribedSkus"

# Fetch all security alerts
$SKURequest = Invoke-RestMethod -Uri $Uri -Headers $Header -Method Get -ContentType "application/json"

$SKUS = $SKURequest.Value
$Report = [System.Collections.Generic.List[Object]]::new() 

Foreach ($SKU in $SKUS) {  

$FilteredObjects = @("VISIOCLIENT","PROJECTPREMIUM", "MCOMEETADV")
$CompareFilter = Compare-Object -ReferenceObject $SKU.skuPartNumber -DifferenceObject $FilteredObjects -IncludeEqual | where-object{$_.sideindicator -eq "=="}

    If (!$CompareFilter)

    {


    If (($SKU.capabilityStatus -ne "Enabled") -or ($SKU.consumedUnits -eq 0 )) 
        {
            write-output "Skipping" }
    Else 
        { 
         
            $ReportLine  = [PSCustomObject] @{          
        
            skuPartNumber = $SKU.skuPartNumber
            prepaidUnits = $SKU.prepaidUnits.enabled
            ComsumedUnits = $SKU.consumedUnits
            freeunits = ($SKU.prepaidUnits.enabled - $SKU.consumedUnits)
          }

    <# Add this part if you want to query on percentages

    $percentage = ($Reportline.freeunits / $reportline.prepaidUnits).tostring("P")

    If ([int]$percentage.trim("%") -le 10)
        {        
            $Report.Add($ReportLine) 
        }
    #>

    # Add this part if you want to query on values
   

    If ($Reportline.freeunits -le 10)
        {        
            $Report.Add($ReportLine) 
        }

    
        }

    }

}


#Create Report from array
$Report | Export-CSV "$env:TEMP\SKU.csv" -notypeinformation 

# Checking if there is a attachment to send via mail
$Attachment= "$env:TEMP\SKU.csv"
$QueryFile = Get-item -Path $Attachment -erroraction Silentlycontinue 

If ($QueryFile.length -gt 0)

    {
        AlertMail
    }
Else
    {
        exit
    }




You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *