<# .SYNOPSIS Generates an Azure license report, including license usage data, and sends it via email. .DESCRIPTION This PowerShell script connects to the Microsoft Graph API, retrieves available licenses and licensed users, and calculates the assigned and available licenses. It then generates an HTML report and sends it to the specified recipients. A summary report is also generated and saved to a file. .PARAMETER None This script does not require any parameters. .EXAMPLE .\Azure-License-Report.ps1 .NOTES This script is intended for use in a test or production environment. Make sure to test the script in a non-production environment before running it in production. Author: D.de Kooker - info@dcomputers.nl Version: 1.0 DISCLAIMER: Use scripts at your own risk, if there is anything I can help you with I will try but I do not take responsibility for the way that anyone else uses my scripts. Sharing is caring. Share your knowledge with the world so that everybody can learn from it. .LINK The latest version can Always be found on my GIT page on the link below: https://git.dcomputers.nl/Dcomputers/PowershellScripts #> #region Global script settings and variables #General $Version = "v1.0" $logfilelocation = "$($MyInvocation.MyCommand.Path | Split-Path -Parent)\Logs" $logfilename = "$(Get-Date -Format yyyyMMddHHmmss)-Azure-Licensing-Report.log" $summaryfilename = "$(Get-Date -Format yyyyMMddHHmmss)-Azure-Licensing-Summary.txt" #Azure Enterprise app configuration $STR_TenantID = "" $STR_AppID = "" $STR_ClientSecret = "" #Email report settings $STR_SMTPServer = "" $STR_SMTPServerPort = "" $STR_SMTPUsername = "" $STR_SMTPPassword = "" $STR_EmailSubject= "Azure License report - $(Get-Date -Format "dd-MM-yyyy")" $STR_SMTPFromaddress = "Servicedesk ICT " $STR_Receivers = "servicedesk@contoso.com,systemengineer1@contoso.com" #List of commaseperated emailaddresses #endregion #region functions function SendMailv2 ($To,$Subject,$Body){ $SMTPClient = New-Object Net.Mail.SmtpClient($STR_SMTPServer, $STR_SMTPServerPort) # $SMTPClient.EnableSsl = $true $SMTPClient.Credentials = New-Object System.Net.NetworkCredential($STR_SMTPUsername, $STR_SMTPPassword); $SMTPMessage = New-Object System.Net.Mail.MailMessage($STR_SMTPFromaddress,$To,$Subject,$Body) $SMTPMessage.IsBodyHTML = $true $SMTPClient.Send($SMTPMessage) } function Initiate-Log { # Get current user and session information $username = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $computerName = $env:COMPUTERNAME $sessionID = $pid $date = Get-Date -Format "yyyy-MM-dd HH:mm:ss" # Write log header $logHeader = "[$date] Log initiated by $username on $computerName (Session ID: $sessionID)" Add-Content -Path $logfilelocation\$logfilename -Value "**********************" Add-Content -Path $logfilelocation\$logfilename -Value "LogFile initiation" Add-Content -Path $logfilelocation\$logfilename -Value "Start time: $date" Add-Content -Path $logfilelocation\$logfilename -Value "Username: $username" Add-Content -Path $logfilelocation\$logfilename -Value "Machine: $computerName" Add-Content -Path $logfilelocation\$logfilename -Value "Process ID: $sessionID" Add-Content -Path $logfilelocation\$logfilename -Value "Script Version: $Version" Add-Content -Path $logfilelocation\$logfilename -Value "Script Source: https://git.dcomputers.nl/Dcomputers/PowershellScripts" Add-Content -Path $logfilelocation\$logfilename -Value "**********************" } function Write-Log { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [string]$Message, [Parameter(Mandatory=$false)] [ValidateSet("INFO", "WARNING", "ERROR")] [string]$Level = "INFO" ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logmessage = "[$timestamp] [$Level] $Message" Add-Content -Path $logfilelocation\$logfilename -Value $logmessage } function Write-Summary { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [string]$Message ) Add-Content -Path $logfilelocation\$summaryfilename -Value $Message } #endregion #region prerequisites check #Create log directory if not present and initiate logfile if (!(test-path $logfilelocation)) {mkdir $logfilelocation} Initiate-Log #Check if the required Powershell Modules are available $modules = @("Microsoft.Graph") foreach ($module in $modules) { if (!(Get-Module -Name $module -ListAvailable)) { Write-Host "The $module module is not installed. Please install it and try again." Write-Log -Message "The $module module is not installed. Please install it and try again." -Level ERROR exit 1 } } #Setup MSGraph connection $ClientSecretPass = ConvertTo-SecureString -String $STR_ClientSecret -AsPlainText -Force $ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $STR_AppID, $ClientSecretPass Connect-MgGraph -TenantId $STR_TenantID -ClientSecretCredential $ClientSecretCredential Write-Log -Message "Connected to MsGraph API" -Level INFO #Download the latest product name translation file if (Test-Path "$($MyInvocation.MyCommand.Path | Split-Path -Parent)\ProductNames.csv") { Remove-Item "$($MyInvocation.MyCommand.Path | Split-Path -Parent)\ProductNames.csv" -Force } $uri = "https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv" Invoke-WebRequest $uri -OutFile "$($MyInvocation.MyCommand.Path | Split-Path -Parent)\ProductNames.csv" $ReadableNames = Import-Csv "$($MyInvocation.MyCommand.Path | Split-Path -Parent)\ProductNames.csv" #endregion #region collect license usage and generate a readable report Write-Log -Message "Collecting license usage data" -Level INFO # Get all available licenses and licensed users $availableLicenses = Get-MgSubscribedSku | Where-Object {$_.CapabilityStatus -eq "Enabled"} Write-Log -Message "Retrieved available licenses" -Level INFO $licensedUsers = Get-MgUser -Filter 'assignedLicenses/$count ne 0' -ConsistencyLevel eventual -CountVariable licensedUserCount -All Write-Log -Message "Retrieved licensed users" -Level INFO # Initialize an empty array to store the license usage data $licenseUsageData = @() $userLicenses = @() # Loop through each available license foreach ($licensedUser in $licensedUsers) { $userLicenses += Get-MgUserLicenseDetail -UserId $licensedUser.Id } foreach ($availableLicense in $availableLicenses) { # Get the license details $licenseDetails = Get-MgSubscribedSku -SubscribedSkuId $availableLicense.Id Write-Log -Message "Retrieved license details for $($availableLicense.SkuPartNumber)" -Level INFO # Get the total licenses available $totalLicenses = $licenseDetails.PrePaidUnits.Enabled # Get the assigned licenses $assignedLicenses = 0 foreach ($userLicense in $userLicenses) { if ($userLicense.SkuId -eq $availableLicense.SkuId) { $assignedLicenses++ } } # Calculate the available licenses $availableLicensesCount = $totalLicenses - $assignedLicenses # Calculate license status if ($availableLicensesCount -le 1) { $licenseStatus = "ERROR" } elseif ($availableLicensesCount -le ($totalLicenses * 0.05)) { $licenseStatus = "WARNING" } else { $licenseStatus = "OK" } # Create a custom object to store the license usage data $licenseUsageObject = [PSCustomObject]@{ LicenseDisplayName = $($ReadableNames | Where-Object {$_.String_Id -eq $($availableLicense.SkuPartNumber)} | Select-Object -ExpandProperty Product_Display_Name)[0] LicenseSKU = $availableLicense.SkuPartNumber AssignedLicenses = $assignedLicenses AvailableLicenses = $availableLicensesCount TotalLicenses = $totalLicenses LicenseStatus = $licenseStatus } # Add the custom object to the array $licenseUsageData += $licenseUsageObject Write-Log -Message "Added license usage data for $($availableLicense.SkuPartNumber)" -Level INFO } # Create an HTML report $htmlReport = @"

Azure Product license report - $(Get-Date -Format "dd-MM-yyyy - HH:mm")

Script version: $Version

"@ foreach ($licenseUsageObject in $($licenseUsageData | Sort-Object -Property LicenseDisplayName)) { $htmlReport += @" $(switch ($licenseUsageObject.LicenseStatus) { 'ERROR' {""} 'WARNING' {""} default {""} } ) "@ } $htmlReport += @"
License Name License SKU Assigned Licenses Available Licenses Total Licenses Status
$($licenseUsageObject.LicenseDisplayName) $($licenseUsageObject.LicenseSKU) $($licenseUsageObject.AssignedLicenses) $($licenseUsageObject.AvailableLicenses) $($licenseUsageObject.TotalLicenses)$($licenseUsageObject.LicenseStatus)$($licenseUsageObject.LicenseStatus)$($licenseUsageObject.LicenseStatus)

This is an automated report.

"@ #endregion #region send reports and generate summary report # Send the report via email $licenseStatus = "OK" switch ($licenseUsageData | Select-Object -ExpandProperty LicenseStatus) { { $_ -contains "ERROR" } { $licenseStatus = "ERROR"; break } { $_ -contains "WARNING" } { $licenseStatus = "WARNING"; break } } if ($licenseStatus -ne "OK") { $subject = "$($LicenseStatus): $STR_EmailSubject" } else { $subject = "$STR_EmailSubject" } $body = $htmlReport SendMailv2 -To $STR_Receivers -Subject $subject -Body $body # write summary Write-Summary "Azure Product license report Summary:" Write-Summary "---------------------------" Write-Summary "Report date: $(Get-Date -Format "dd-MM-yyy HH:mm:ss")" foreach ($license in $($licenseUsageData | Sort-Object -Property LicenseDisplayName)){ Write-Summary "******************" Write-Summary "License Name: $($license.LicenseDisplayName)" Write-Summary "License SKU: $($license.LicenseSKU)" Write-Summary "Assigned Licenses: $($license.AssignedLicenses)" Write-Summary "Available Licenses: $($license.AvailableLicenses)" Write-Summary "Total Licenses: $($license.TotalLicenses)" Write-Summary "Status: $($license.LicenseStatus)" } Write-Summary "---------------------------"