Dark mode

Dark mode

There are 0 results matching

article card image dark article card image light

Published by · Jun 25, 2024 tools · 2 mins read

Introducing: macOS JAMF Offboarding Tool

Offboarding macOS Devices from JAMF in Bulk using the JAMF API with a bash script ...

See More
article card image dark article card image light

Published by · Jun 25, 2024 tools · 2 mins read

Introducing: Microsoft Cloud License Automation Tool - Part 1

Automating Microsoft Cloud License Assignment and Reporting with PowerShell and Slack for Enterprise Mobility and Security E3 ...

See More
article card image dark article card image light

Published by · Jun 18, 2024 tools · 2 mins read

Introducing: Configuration Manager Set Implicit Uninstall Flag Tool

Setting Configuration Manager ConfigMgr Implicit Uninstall Flag with PowerShell for Required Application Deployments ...

See More
article card image dark article card image light

Published by · Jun 11, 2024 configmgr · 2 mins read

Configuration Manager Next Maintenance Window SQL Function

Get Next Configuration Manager Maintenance Window from a Schedule Token with Offset Days using an SQL Function. ...

See More
article card image dark article card image light

Published by · Jun 3, 2024 tools · 2 mins read

Introducing: Windows User Rights Assignment Tool - Part 3

Add, Remove, or Replace Windows Rights Assignment with our PowerShell Tool. ...

See More
article card image dark article card image light

Published by · May 28, 2024 tools · 2 mins read

Introducing: Windows User Rights Assignment Tool - Part 2

Get and Report Windows Rights Assignment with our PowerShell Tool. ...

See More
article card image dark article card image light

Published by · May 22, 2024 tools · 1 mins read

Introducing: Windows User Rights Assignment Tool - Part 1

Get Windows Rights Assignment with our PowerShell Tool. ...

See More
article card image dark article card image light

Published by · Apr 11, 2024 tools · 2 mins read

Introducing: Intune Linux Onboarding Tool

Onboard Ubuntu Linux devices to Microsoft Intune using a bash script. Installs prerequisites and starts the user-driven enrollment. ...

See More
article card image dark article card image light

Published by · Apr 11, 2024 tools · 2 mins read

Introducing: Intune macOS Onboarding Tool

Onboard macOS devices to Microsoft Intune using a bash script that initiates the process. Optionally, the script converts mobile accounts, resets the FileVault key, and removes ...

See More
article card image dark article card image light

Published by · Jan 23, 2024 tools · 3 mins read

Introducing: Intune Device Renaming Tool

Rename Intune Devices by setting a Prefix or using a User Attribute as Prefix. Supports Windows, macOS, and Linux ...

See More
article card image dark article card image light

Published by · Dec 8, 2023 intune · 5 mins read

Intune Logs: A Deep Dive into Locations, Interpretation, and Configuration

A Comprehensive Guide to Locations, Interpretation, and Configuration of Intune Logs ...

See More
article card image dark article card image light

Published by · Aug 14, 2023 configmgr · 2 mins read

Configuration Manager Console Extension to show Device Collection Membership with Console Builder

Use the Configuration Manager Console Builder, to add Collection Membership View to the Device Node ...

See More
article card image dark article card image light

Published by · Aug 3, 2023 tools · 3 mins read

Introducing: Configuration Manager SSRS Dashboards

A Configuration Manager Dashboards solution with Reports for Software Updates, Bitlocker and more ...

See More
article card image dark article card image light

Published by · Aug 3, 2023 tools · 2 mins read

Introducing: PowerShell WMI Management Toolkit Module

Streamline your WMI Namespace, Class, and Instance Management with our PowerShell Module ...

See More
article card image dark article card image light

Published by · Jul 14, 2023 configmgr · 1 mins read

Configuration Manager detailed, filterable Port Documentation

Configuration Manager detailed, filterable port documentation as an excel document ...

See More
article card image dark article card image light

Published by · Jul 14, 2023 configmgr · 3 mins read

Configuration Manager PXE TFTP Window Size Bug

Configuration Manager TFTP Block Size and TFTP Window Size Correct Configuration ...

See More
article card image dark article card image light

Published by · Jun 18, 2023 tools · 4 mins read

Introducing: Configuration Manager Client Cache Cleanup Tool

Cleaning the Configuration Manager Client Cache the Right Way with PowerShell and Configuration Baselines ...

See More
article card image dark article card image light

Published by · Jun 18, 2023 tools · 2 mins read

Introducing: Windows Cache Cleanup Tool

Cleaning Windows and Configuration Manager Caches for Configuration Manager Build and Capture Task Sequence or Standalone Use ...

See More
article card image dark article card image light

Published by · Jun 17, 2023 tools · 1 mins read

Introducing: Windows Update Database Reinitialization Tool

Proactively repair corrupted Windows Update Database with Powershell and Configuration Manager ...

See More
article card image dark article card image light

Published by · Mar 31, 2023 tools · 3 mins read

Introducing: Configuration Manager SQL Products Reporting

A Complete SQL Products reporting solution using Configuration Manager ...

See More
article card image dark article card image light

Published by · Jan 28, 2023 configmgr · 1 mins read

Application Detection Method using the Configuration Manager Application Version

Replace hardcoded application version in scripts, with the Configuration Manager Application Version ...

See More
article card image dark article card image light

Published by · Jan 28, 2023 tools · 3 mins read

Introducing: Certificate Management Toolkit

Managing Certificates with Configuration Manager and PowerShell by using just the Public Key ...

See More
article card image dark article card image light

Published by · Jan 7, 2019 reports · 2 mins read

Configuration Manager Device Boundary and Network Information Report

List Device Boundaries and Network Information with Configuration Manager ...

See More
article card image dark article card image light

Published by · Sep 9, 1980 help · 5 mins read

MEM.Zone Blog Publishing Documentation

Publishing Documentation for MEM.Zone ...

See More

We couldn’t find anything related to

“SCCM”

BLOG / tools zone

Introducing: Microsoft Cloud License Automation Tool - Part 1

Published by Paul Vilcu · Jun 25, 2024 · 2 mins read
article card image dark article card image light

Quick Summary

Microsoft has been involved in cloud computing for many years, and its approach to cloud licenses has evolved over time, from its early adoption in 2014 with the introduction of the Cloud Solution Provider (CSP) program it represented a significant step in the new direction.

Today in 2024, we have a plethora of cloud licenses available with various different costs. Balancing the assignment and reporting of these licenses in order not to overpay despite not using them is a challenge which we try to address in this two part blog series.

Part 1 covers reporting and alerting for _Enterprise Mobility and Security E3 Licenses which will enable you supplement additional licenses before they run out.

This tool can be used as a standalone script or integrated into your Azure Automation or Azure Functions.

Prerequisites


Understanding SKUs

Each cloud license from Microsoft has an particular skuId, which can be retrieved Online or using PowerShell.

Online

Microsoft official page for all Product Names and Service Plans, use GUIDs which can lead to some confusion.
As a workaround you can use the CSV version in the Prerequisites section.

PowerShell

Install-Module -Name AzureAD
Connect-AzureAD

Get-AzureADSubscribedSku | Select-Object -Property 'SkuId', 'SkuPartNumber'
Notes

AzureAD PowerShell module is needed in order to be able to run the Get-AzureADSubscribedSku cmdlet.


Parameters

Tenant

Specify the tenant ID

ApplicationClientID & Secret

Specify the Application (Client) ID to use.
Specify the Application (Client) Secret to use.

GUID

Specify the skuID

MinimumAmount

Specify the minimum license threshold before a slack message will be posted.

SlackWebhookURL

Specify the URL of the slack channel to post on.


Preview

article card image microsoft-cloud-license-automation-tool.gif
Microsoft Automate Cloud License Tool

Code

  1<#
  2.SYNOPSIS
  3    Checks the license usage of the specified license.
  4.DESCRIPTION
  5    Checks the license usage of the specified license. If the available amount is under the specified MinimumLicenseThreshold it will send a message to a slack webhook.
  6.PARAMETER TenantID
  7    Specifies the tenant ID.
  8.PARAMETER ClientID
  9    Specifies the application ID.
 10.PARAMETER ClientSecret
 11    Specifies the application secret.
 12.PARAMETER SkuIDs
 13    Specifies the skuId (GUID) of the license you want to check. This parameter is a Array.
 14.PARAMETER MinimumLicenseThreshold
 15    Limit for the minimum value, below this amount it will send a message to slack.
 16.PARAMETER SlackWebhookURI
 17    On what slack channel must the message be posted on.
 18.EXAMPLE
 19    Check-License.ps1 -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret -skuIds $skuIds -minAmount $minAmount -slackWebhookURI $slackWebhookURI
 20.INPUTS
 21    None.
 22.OUTPUTS
 23    None.
 24.NOTES
 25    Created by Ferry Bodijn
 26.LINK
 27    https://MEMZ.one/Get-MSCloudLicenseUsage
 28.LINK
 29    https://MEMZ.one/Get-MSCloudLicenseUsage-CHANGELOG
 30.LINK
 31    https://MEMZ.one/Get-MSCloudLicenseUsage-GIT
 32.LINK
 33    https://MEM.Zone/ISSUES
 34.COMPONENT
 35    MSGraph
 36.FUNCTIONALITY
 37    Get Cloud License Usage.
 38#>
 39
 40## Set script requirements
 41#Requires -Version 5.1
 42
 43##*=============================================
 44##* VARIABLE DECLARATION
 45##*=============================================
 46#region VariableDeclaration
 47
 48## Get script parameters
 49[CmdletBinding(SupportsShouldProcess=$true, DefaultParameterSetName = 'Custom')]
 50Param (
 51    [Parameter(Mandatory = $true, ParameterSetName = 'Custom', HelpMessage = 'Specify the tenant ID', Position = 0)]
 52    [Parameter(Mandatory = $true, ParameterSetName = 'UserAttribute', HelpMessage = 'Enter the tenant ID', Position = 0)]
 53    [ValidateNotNullorEmpty()]
 54    [Alias('Tenant')]
 55    [string]$TenantID,
 56    [Parameter(Mandatory = $true, ParameterSetName = 'Custom', HelpMessage = 'Specify the Application (Client) ID to use.', Position = 1)]
 57    [Parameter(Mandatory = $true, ParameterSetName = 'UserAttribute', HelpMessage = 'Specify the Application (Client) ID to use.', Position = 1)]
 58    [ValidateNotNullorEmpty()]
 59    [Alias('ApplicationClientID')]
 60    [string]$ClientID,
 61    [Parameter(Mandatory = $true, ParameterSetName = 'Custom', HelpMessage = 'Specify the Application (Client) Secret to use.', Position = 2)]
 62    [Parameter(Mandatory = $true, ParameterSetName = 'UserAttribute', HelpMessage = 'Specify the Application (Client) Secret to use.', Position = 2)]
 63    [ValidateNotNullorEmpty()]
 64    [Alias('ApplicationClientSecret')]
 65    [string]$ClientSecret,
 66    [Parameter(Mandatory = $true, ParameterSetName = 'Custom', HelpMessage = 'Specify the skuID that can be found on the site: https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference.', Position = 2)]
 67    [Parameter(Mandatory = $true, ParameterSetName = 'UserAttribute', HelpMessage = 'Specify the skuID that can be found on the site: https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference.', Position = 2)]
 68    [ValidateNotNullorEmpty()]
 69    [Alias('GUID')]
 70    [array]$SkuIds,
 71    [Parameter(Mandatory = $true, ParameterSetName = 'Custom', HelpMessage = 'Specify the minimum amount of licenses before a slack message will be posted.', Position = 2)]
 72    [Parameter(Mandatory = $true, ParameterSetName = 'UserAttribute', HelpMessage = 'Specify the minimum amount of licenses before a slack message will be posted.', Position = 2)]
 73    [ValidateNotNullorEmpty()]
 74    [Alias('MinimumAmount')]
 75    [int]$MinimumLicenseThreshold,
 76    [Parameter(Mandatory = $true, ParameterSetName = 'Custom', HelpMessage = 'Specify the URL of the slack channel to post on.', Position = 2)]
 77    [Parameter(Mandatory = $true, ParameterSetName = 'UserAttribute', HelpMessage = 'Specify the URL of the slack channel to post on.', Position = 2)]
 78    [ValidateNotNullorEmpty()]
 79    [Alias('SlackWebhookURL')]
 80    [string]$SlackWebhookURI
 81)
 82
 83## Set log name and path
 84[string]$ScriptName = 'Get-MSCloudLicense'
 85[string]$ScriptPath = "$ENV:ProgramData"
 86
 87## SkuID examples
 88#SkuId: efccb6f7-5641-4e0e-bd10-b4976e1bf68e  - Enterprise Mobility + Security E3 license
 89#SkuId: 6a0f6da5-0b87-4190-a6ae-9bb5a2b9546a  - Windows 10/11 Enterprise E3
 90
 91#endregion VariableDeclaration
 92##*=============================================
 93##* END VARIABLE DECLARATION
 94##*=============================================
 95
 96##*=============================================
 97##* FUNCTION LISTINGS
 98##*=============================================
 99#region FunctionListings
100
101#region Function Write-Log
102Function Write-Log {
103<#
104.SYNOPSIS
105    Creates a Log entry and appends it to a Log file.
106.DESCRIPTION
107    Creates a Log entry and appends it to a Log file.
108.PARAMETER Severity
109    Specifies the message severity of (Informational, Success or Error).
110    Default is: 1.
111.PARAMETER Message
112    Specifies the log message to append.
113.EXAMPLE
114    Write-Log -Message 'Installation successful.' -Severity 2
115.EXAMPLE
116    Write-Log -Message 'Installation failed!' -Severity 3
117.INPUTS
118    System.Int32Type
119    System.String
120.OUTPUTS
121    .None
122.LINK
123    https://MEM.Zone
124.COMPONENT
125    Get-MSCloudLicense
126.FUNCTIONALITY
127    Creates a Log file entry
128#>
129    Param (
130        [Parameter(Mandatory = $false)]
131        [string]$Message,
132        [int]$Severity = 1
133    )
134    [string]$DeviceName = $env:COMPUTERNAME
135    $Time = Get-Date -Format 'HH:mm:ss'
136    $Date = Get-Date -Format 'yyyy-mm-dd'
137    [string]$FilePath = -join ($ScriptPath,$ScriptName.tx)
138    Switch ($Severity)
139    {
140        1 { $MessageSeverity = "INFORMATIONAL" }
141        2 { $MessageSeverity = "SUCCESS" }
142        3 { $MessageSeverity = "ERROR" }
143
144    }
145    $LogMessage = 'Device: {0}, Severity: {1}, Date: {2}, Time: {3}, Message: {4}`n' -f $DeviceName, $MessageSeverity, $Date, $Time, $Message
146    $LogMessage | Out-File -Append -Encoding 'UTF8' -FilePath $FilePath -Force
147}
148#endregion Function Write-Log
149
150#region Function Get-MSGraphAPItoken
151Function Get-MSGraphAPItoken {
152<#
153.SYNOPSIS
154    Gets a Microsoft Graph API Token.
155.DESCRIPTION
156    Gets a Microsoft Graph API Token, expiresInSec and retrieveTimeUtc, using a TenantID, AppID and AppSecret
157.PARAMETER tenantID
158    Specifies the Azure Tenant ID.
159.PARAMETER applicationID
160    Specifies the Azure Application ID.
161.PARAMETER applicationSecret
162    Specifies the Azure Application Secret.
163.EXAMPLE
164    Get-MSGraphAPItoken -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret
165.INPUTS
166    None.
167.OUTPUTS
168    System.String.
169.NOTES
170    This is an internal script function and should typically not be called directly.
171.LINK
172    https://MEM.Zone
173.LINK
174    https://MEM.Zone/GIT
175.LINK
176    https://MEM.Zone/ISSUES
177.COMPONENT
178    MSGraph
179.FUNCTIONALITY
180    Invokes the Microsoft Graph API.
181#>
182[CmdletBinding()]
183    Param (
184        [Parameter(Mandatory = $true, HelpMessage = 'Specify the tenant ID.', Position = 0)]
185        [ValidateNotNullorEmpty()]
186        [Alias('Tenant')]
187        [string]$TenantID,
188        [Parameter(Mandatory = $true, HelpMessage = 'Specify the Application (Client) ID to use.', Position = 1)]
189        [ValidateNotNullorEmpty()]
190        [Alias('ApplicationClientID')]
191        [string]$ClientID,
192        [Parameter(Mandatory = $true, HelpMessage = 'Specify the Application (Client) Secret to use.', Position = 2)]
193        [ValidateNotNullorEmpty()]
194        [Alias('ApplicationClientSecret')]
195        [string]$ClientSecret,
196        [Parameter(Mandatory = $false, HelpMessage = 'Specify the scope to use.', Position = 3)]
197        [ValidateNotNullorEmpty()]
198        [Alias('GrantScope')]
199        [string]$Scope = 'https://graph.microsoft.com/.default',
200        [Parameter(Mandatory = $false, HelpMessage = 'Specify the grant type to use.', Position = 4)]
201        [ValidateNotNullorEmpty()]
202        [Alias('AccessType')]
203        [string]$GrantType = 'client_credentials'
204    )
205
206    Begin {
207
208        ## Assemble the token body for the API call. You can store the secrets in Azure Key Vault and retrieve them from there.
209        [hashtable]$Body = @{
210            client_id     = $ClientID
211            scope         = $Scope
212            client_secret = $ClientSecret
213            grant_type    = $GrantType
214        }
215
216        ## Assembly the URI for the API call
217        [string]$Uri = -join ('https://login.microsoftonline.com/', $TenantID, '/oauth2/v2.0/token')
218
219        ## Write Debug information
220        Write-Debug -Message "Uri: $Uri"
221        Write-Debug -Message "Body: $($Body | Out-String)"
222    }
223    Process {
224        Try {
225
226            ## Get the access token
227            $Response = Invoke-WebRequest -Method 'Post' -Uri $Uri -ContentType 'application/x-www-form-urlencoded' -Body $Body -UseBasicParsing
228
229            ## Assemble output object
230            $Output = [pscustomobject]@{
231                access_token = $Response.access_token
232                expires_in   = $Response.expires_in
233                granted_on = $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
234            }
235        }
236        Catch {
237
238            ## Write exception to log.
239            #  Note that value__ is not a typo.
240            [string]$StatusCode = $_.Exception.Response.StatusCode.value__
241            [string]$StatusDescription = $_.Exception.Response.StatusDescription
242            #  Assemble the error message
243            [string]$Message = "Error getting MSGraph API Access Token for TenantID '{0}' with ClientID '{1}'.`n Status code {'2'}, Description {'3'}.`n{4}" -f $TenantID, $ClientID, $StatusCode, $StatusDescription, $_.Exception.Message
244            Write-Log -Message $Message -Severity 3
245        }
246        Finally {
247            Write-Output -InputObject $Output
248        }
249    }
250    End {
251    }
252}
253#endregion Function Get-GraphApiToken
254
255#region Function Invoke-MSGraphAPI
256Function Invoke-MSGraphAPI {
257<#
258.SYNOPSIS
259    Invokes the Microsoft Graph API.
260.DESCRIPTION
261    Invokes the Microsoft Graph API with paging support.
262.PARAMETER Method
263    Specify the method to use.
264    Available options are 'GET', 'POST', 'PATCH', 'PUT' and 'DELETE'.
265    Default is: 'GET'.
266.PARAMETER Token
267    Specify the access token to use.
268.PARAMETER Version
269    Specify the version of the Microsoft Graph API to use.
270    Available options are 'Beta' and 'v1.0'.
271    Default is: 'Beta'.
272.PARAMETER Resource
273    Specify the resource to query.
274    Default is: 'deviceManagement/managedDevices'.
275.PARAMETER Parameter
276    Specify the parameter to use. Make sure to use the correct syntax and escape special characters with a backtick.
277    Default is: $null.
278.PARAMETER Body
279    Specify the request body to use.
280    Default is: $null.
281.PARAMETER ContentType
282    Specify the content type to use.
283    Default is: 'application/json'.
284.EXAMPLE
285    Invoke-MSGraphAPI -Method 'GET' -Token $Token -Version 'Beta' -Resource 'deviceManagement/managedDevices' -Parameter "filter=operatingSystem like 'Windows' and deviceName like 'MEM-Zone-PC'"
286.EXAMPLE
287    Invoke-MSGraphAPI -Token $Token -Resource 'users'
288.INPUTS
289    None.
290.OUTPUTS
291    System.Object
292.NOTES
293    Created by Ioan Popovici
294    v1.0.0 - 2024-01-11
295    Changes made by Ferry Bodijn
296    v1.0.1 - 2024-03-07 Made some changes to make the function work properly
297    v1.0.2 - 2024-04-22 Sometimes there is no output.value. The correct data is already returned. Fixed this.
298    v1.0.3 - 2024-06-19 - Review Paging and overall functionality
299
300    This is an private function should typically not be called directly.
301.LINK
302    https://MEM.Zone
303.LINK
304    https://MEM.Zone/GIT
305.LINK
306    https://MEM.Zone/ISSUES
307.COMPONENT
308    MSGraph
309.FUNCTIONALITY
310    Invokes the Microsoft Graph API.
311#>
312    [CmdletBinding()]
313    Param (
314        [Parameter(Mandatory = $false, HelpMessage = 'Specify the method to use.', Position = 0)]
315        [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')]
316        [Alias('HTTPMethod')]
317        [string]$Method = 'GET',
318        [Parameter(Mandatory = $true, HelpMessage = 'Specify the access token to use.', Position = 1)]
319        [ValidateNotNullorEmpty()]
320        [Alias('AccessToken')]
321        [string]$Token,
322        [Parameter(Mandatory = $false, HelpMessage = 'Specify the version of the Microsoft Graph API to use.', Position = 2)]
323        [ValidateSet('Beta', 'v1.0')]
324        [Alias('GraphVersion')]
325        [string]$Version = 'Beta',
326        [Parameter(Mandatory = $true, HelpMessage = 'Specify the resource to query.', Position = 3)]
327        [ValidateNotNullorEmpty()]
328        [Alias('APIResource')]
329        [string]$Resource,
330        [Parameter(Mandatory = $false, HelpMessage = 'Specify the parameters to use.', Position = 4)]
331        [ValidateNotNullorEmpty()]
332        [Alias('QueryParameter')]
333        [string]$Parameter,
334        [Parameter(Mandatory = $false, HelpMessage = 'Specify the request body to use.', Position = 5)]
335        [ValidateNotNullorEmpty()]
336        [Alias('RequestBody')]
337        [string]$Body,
338        [Parameter(Mandatory = $false, HelpMessage = 'Specify the content type to use.', Position = 6)]
339        [ValidateNotNullorEmpty()]
340        [Alias('Type')]
341        [string]$ContentType = 'application/json'
342    )
343
344    Begin {
345
346        ## Assemble the URI for the API call
347        [string]$Uri = "https://graph.microsoft.com/$Version/$Resource"
348        If (-not [string]::IsNullOrWhiteSpace($Parameter)) { $Uri += "`?`$$Parameter" }
349
350        ## Assembly parameters for the API call
351        [hashtable]$Parameters = @{
352            'Uri'         = $Uri
353            'Method'      = $Method
354            'Headers'     = @{
355                'Content-Type'  = 'application\json'
356                'Authorization' = "Bearer $Token"
357            }
358            'ContentType' = $ContentType
359        }
360        If (-not [string]::IsNullOrWhiteSpace($Body)) { $Parameters.Add('Body', $Body) }
361
362        ## Write Debug information
363        Write-Debug -Message "Uri: $Uri"
364    }
365    Process {
366        Try {
367
368            ## Invoke the MSGraph API
369            $Output = Invoke-RestMethod @Parameters
370
371            ## If there are more than 1000 rows, use paging. Only for GET method.
372            If ($Output.'@odata.nextLink') {
373                $Output += Do {
374                    $Parameters.Uri = $OutputPage.'@odata.nextLink'
375                    $OutputPage = Invoke-RestMethod @Parameters
376                    $OutputPage
377                }
378                Until ([string]::IsNullOrEmpty($OutputPage.'@odata.nextLink'))
379            }
380            Write-Verbose -Message "Got '$($Output.Count)' Output pages."
381        }
382        Catch {
383            [string]$Message = "Error invoking MSGraph API version '{0}' for resource '{1}' using '{2}' method.`n{3}" -f $Version, $Resource, $Method
384            Write-Log -Message $Message -Severity 3
385            Write-Error -Message $Message
386        }
387        Finally {
388            $Output = If ($Output.value) { $Output.value } Else { $Output }
389            Write-Output -InputObject $Output
390        }
391    }
392    End {
393    }
394}
395#endregion Function Invoke-MSGraphAPI
396
397#region function Send-SlackMessage
398Function Send-SlackMessage {
399
400    [CmdletBinding()]
401    Param (
402        [Parameter(Mandatory = $true, HelpMessage = 'Specify the header.', Position = 0)]
403        [Alias('Title')]
404        [string]$Header,
405        [Parameter(Mandatory = $true, HelpMessage = 'Total licenses.', Position = 1)]
406        [Alias('Total')]
407        [string]$AvailableLicenses,
408        [Parameter(Mandatory = $true, HelpMessage = 'Used licenses.', Position = 2)]
409        [Alias('Used')]
410        [string]$UsedLicenses,
411        [Parameter(Mandatory = $true, HelpMessage = 'Licenses remaining.', Position = 3)]
412        [Alias('Remaining')]
413        [string]$RemainingLicenses,
414        [Parameter(Mandatory = $true, HelpMessage = 'Slack webhook URL', Position = 4)]
415        [Alias('WebhookURI')]
416        [string]$slackWebhookURI
417    )
418
419    ## Assemble the notification payload
420    [string]$Body =
421@"
422{
423    "blocks": [
424        {
425            "type": "header",
426            "text": {
427                "type": "plain_text",
428                "text": ":alert: $Header :alert:",
429                "emoji": true
430            }
431        },
432        {
433            "type": "divider"
434        },
435        {
436            "type": "section",
437            "text": {
438                "type": "mrkdwn",
439                "text": "*Available Licenses:* $AvailableLicenses"
440            }
441        },
442        {
443            "type": "section",
444            "text": {
445                "type": "mrkdwn",
446                "text": "*Used Licenses:* $UsedLicenses"
447            }
448        },
449        {
450            "type": "section",
451            "text": {
452                "type": "mrkdwn",
453                "text": "*Remaining Licenses: $RemainingLicenses*"
454            }
455        },
456        {
457            "type": "section",
458            "text": {
459                "type": "plain_text",
460                "text": "Please check if more licenses are required!",
461                "emoji": true
462            }
463        }
464    ]
465}
466"@
467
468    ## Post to slack
469    Start-Sleep -Seconds 1
470    Try {
471        $SlackNotify = Invoke-RestMethod -uri $SlackWebhookURI -Method 'POST' -Body $Body -ContentType 'application/json'
472
473        If ($SlackNotify -ne 'ok') { $Output = "Could not send Slack message! '$SlackNotify'" } Else { $Output = 'Slack Message Sent!' }
474    }
475    Catch {
476        Write-Error -Message "Error Sending Slack Message. $($_.Exception.Message)"
477    }
478    Finally {
479        Write-Output -InputObject $Output
480    }
481}
482#endregion Function Send-SlackMessage
483
484#endregion FunctionListings
485##*=============================================
486##* END FUNCTION LISTINGS
487##*=============================================
488
489##*=============================================
490##* SCRIPT BODY
491##*=============================================
492#region ScriptBody
493
494#First remove old log:
495If (Test-Path "$ScriptPath\$ScriptName.txt" -PathType 'Leaf') { Remove-Item -Path "$ScriptPath\$ScriptName.txt" -Force -Confirm:$false -ErrorAction 'SilentlyContinue' }
496
497## Write Start verbose message
498Write-Log -Message "Start '$ScriptName'" -Severity 1
499
500## Get API Token
501$AccessToken = Get-MSGraphApiToken -TenantID $TenantID ClientID $ClientID ClientSecret $ClientSecret -ErrorAction 'Stop'
502$Token = $AccessToken.access_token
503
504
505## Get all licenses from the tenant
506$AvailableLicenses = Invoke-MSGraphAPI -Method 'GET' -Version 'v1.0' -Token $Token -Resource 'subscribedSkus' -ErrorAction 'Stop'
507
508## Get information from 'https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-service-plan-reference'
509Try {
510
511    #  Fetch the content as bytes
512    $Response = Invoke-WebRequest -Uri "https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv" -Method 'GET' -ErrorAction 'Stop'
513
514    #  Convert the content from a byte array to a string, assuming it's UTF8 encoded
515    $Utf8NoBom = New-Object 'System.Text.UTF8Encoding' $false
516    [string]$TranslationTable = $Utf8NoBom.GetString($Response.Content) | ConvertFrom-Csv
517}
518Catch {
519    $Output = "Could not get the translation table!"
520    Write-Log -Message $Output -Severity 3
521    Write-Verbose -Message $Output -Verbose
522}
523
524## Get each license information with the SkuID and check if it is lower then the specific value in $minAmount
525Foreach ($SkuID in $SkuIDs) {
526
527    ## Write verbose message
528    Write-Verbose = "Checking license: {0}" -f $SkuID
529
530    ## Calculate Token Expiry time
531    $TokenExpiryTime = $AccessToken.granted_on.ToUniversalTime().AddSeconds($AccessToken.expires_in)
532
533    ## If token expires in 5 minutes then generate new token
534    If ($TokenExpiryTime.AddMinutes(-5) -lt [DateTime]::UtcNow) {
535
536        ## Regenerate token
537        $AccessToken = Get-MSGraphApiToken -TenantID $TenantID ClientID $ClientID ClientSecret $ClientSecret -ErrorAction 'Stop'
538        $Token = $AccessToken.access_token
539    }
540
541    $SkuIdLicense = $AvailableLicenses | Where-Object { $PsItem.skuId -eq $SkuId }
542    $ResolvedSkuName = ($TranslationTable | Where-Object { $PSItem.GUID -eq $SkuID_license.skuId } | Sort-Object -Property 'Product_Display_Name' -Unique).Product_Display_Name
543
544    [int]$AvailableLicenses = $SkuIDLicense.prepaidUnits.enabled
545    [int]$UsedLicenses = $SkuIDLicense.consumedUnits
546    [int]$RemainingLicenses = $AvailableLicenses - $UsedLicenses
547
548    If ($RemainingLicenses -lt $MinimumLicenseThreshold) {
549        [string]$Header = "License: $ResolvedSkuName"
550        Send-SlackMessage -Header $Header -AvailableLicenses $AvailableLicenses -UsedLicenses $UsedLicenses -RemainingLicenses $RemainingLicenses -SlackWebhookURI $SlackWebhookURI
551        $Output = "The license: {0} is below the amount of: '{1}'. Message to slack has been send." -f $ResolvedSkuName, $MinimumLicenseThreshold
552        Write-Log -Message $Output -Severity 2
553    }
554    Else {
555        $Output = "There are {0} {1} licenses left. So it is not below: '{2}'." -f $RemainingLicenses, $ResolvedSkuName, $MinimumLicenseThreshold
556        Write-Log -Message $Output -Severity 1
557    }
558    Write-Verbose -Message $Output -Verbose
559}
560#endregion ScriptBody
561##*=============================================
562##* END SCRIPT BODY
563##*=============================================

SHARE

article card image dark article card image light

Published by · Jun 18, 2023 tools · 2 mins read

Introducing: Windows Cache Cleanup Tool

Cleaning Windows and Configuration Manager Caches for Configuration Manager Build and Capture Task Sequence or Standalone Use ...

See More
article card image dark article card image light

Published by · Jun 17, 2023 tools · 1 mins read

Introducing: Windows Update Database Reinitialization Tool

Proactively repair corrupted Windows Update Database with Powershell and Configuration Manager ...

See More