#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-Report.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 } else { Import-Module $module } } #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")" Write-Summary $($licenseUsageData | Sort-Object -Property LicenseDisplayName) Write-Summary "---------------------------"