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 Get-MSCloudLicenseUsage.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-MSGraphAPIAccessToken
151Function Get-MSGraphAPIAccessToken {
152<#
153.SYNOPSIS
154 Gets a Microsoft Graph API access token.
155.DESCRIPTION
156 Gets a Microsoft Graph API access token, by using an application registered in EntraID.
157.PARAMETER TenantID
158 Specifies the tenant ID.
159.PARAMETER ClientID
160 Specify the Application Client ID to use.
161.PARAMETER Secret
162 Specify the Application Client Secret to use.
163.PARAMETER Scope
164 Specify the scope to use.
165 Default is: 'https://graph.microsoft.com/.default'.
166.PARAMETER GrantType
167 Specify the grant type to use.
168 Default is: 'client_credentials'.
169.EXAMPLE
170 Get-MSGraphAPIAccessToken -TenantID $TenantID -ClientID $ClientID -Secret $Secret -Scope 'https://graph.microsoft.com/.default' -GrantType 'client_credentials'
171.EXAMPLE
172 Get-MSGraphAPIAccessToken -TenantID $TenantID -ClientID $ClientID -Secret $Secret
173.INPUTS
174 None.
175.OUTPUTS
176 System.String
177.NOTES
178 Created by Ioan Popovici
179 v1.0.0 - 2024-01-11
180.LINK
181 https://MEMZ.one/Invoke-MSGraphAPI
182.LINK
183 https://MEMZ.one/Invoke-MSGraphAPI-CHANGELOG
184.LINK
185 https://MEMZ.one/Invoke-MSGraphAPI-GIT
186.LINK
187 https://MEM.Zone/ISSUES
188.COMPONENT
189 MSGraph
190.FUNCTIONALITY
191 Gets a Microsoft Graph API Access Token.
192#>
193[CmdletBinding()]
194 Param (
195 [Parameter(Mandatory = $true, HelpMessage = 'Specify the tenant ID.', Position = 0)]
196 [ValidateNotNullorEmpty()]
197 [Alias('Tenant')]
198 [string]$TenantID,
199 [Parameter(Mandatory = $true, HelpMessage = 'Specify the Application (Client) ID to use.', Position = 1)]
200 [ValidateNotNullorEmpty()]
201 [Alias('ApplicationClientID')]
202 [string]$ClientID,
203 [Parameter(Mandatory = $true, HelpMessage = 'Specify the Application (Client) Secret to use.', Position = 2)]
204 [ValidateNotNullorEmpty()]
205 [Alias('ApplicationClientSecret')]
206 [string]$ClientSecret,
207 [Parameter(Mandatory = $false, HelpMessage = 'Specify the scope to use.', Position = 3)]
208 [ValidateNotNullorEmpty()]
209 [Alias('GrantScope')]
210 [string]$Scope = 'https://graph.microsoft.com/.default',
211 [Parameter(Mandatory = $false, HelpMessage = 'Specify the grant type to use.', Position = 4)]
212 [ValidateNotNullorEmpty()]
213 [Alias('AccessType')]
214 [string]$GrantType = 'client_credentials'
215 )
216
217 Begin {
218
219 ## Assemble the token body for the API call. You can store the secrets in Azure Key Vault and retrieve them from there.
220 [hashtable]$Body = @{
221 client_id = $ClientID
222 scope = $Scope
223 client_secret = $ClientSecret
224 grant_type = $GrantType
225 }
226
227 ## Assembly the URI for the API call
228 [string]$Uri = -join ('https://login.microsoftonline.com/', $TenantID, '/oauth2/v2.0/token')
229
230 ## Write Debug information
231 Write-Debug -Message "Uri: $Uri"
232 Write-Debug -Message "Body: $($Body | Out-String)"
233 }
234 Process {
235 Try {
236
237 ## Get the access token
238 $Response = Invoke-WebRequest -Method 'Post' -Uri $Uri -ContentType 'application/x-www-form-urlencoded' -Body $Body -UseBasicParsing
239
240 ## Assemble output object
241 $Output = [pscustomobject]@{
242 access_token = $Response.access_token
243 expires_in = $Response.expires_in
244 granted_on = $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
245 }
246 }
247 Catch {
248
249 ## Write exception to log.
250 # Note that value__ is not a typo.
251 [string]$StatusCode = $PsItem.Exception.Response.StatusCode.value__
252 [string]$StatusDescription = $PSItem.Exception.Response.StatusDescription
253 # Assemble the error message
254 [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, $PSItem.Exception.Message
255 Write-Log -Message $Message -Severity 3
256 }
257 Finally {
258 Write-Output -InputObject $Output
259 }
260 }
261 End {
262 }
263}
264#endregion
265
266#region Function Invoke-MSGraphAPI
267Function Invoke-MSGraphAPI {
268<#
269.SYNOPSIS
270 Invokes the Microsoft Graph API.
271.DESCRIPTION
272 Invokes the Microsoft Graph API with paging support.
273.PARAMETER Method
274 Specify the method to use.
275 Available options are 'GET', 'POST', 'PATCH', 'PUT' and 'DELETE'.
276 Default is: 'GET'.
277.PARAMETER Token
278 Specify the access token to use.
279.PARAMETER Version
280 Specify the version of the Microsoft Graph API to use.
281 Available options are 'Beta' and 'v1.0'.
282 Default is: 'Beta'.
283.PARAMETER Resource
284 Specify the resource to query.
285 Default is: 'deviceManagement/managedDevices'.
286.PARAMETER Parameter
287 Specify the parameter to use. Make sure to use the correct syntax and escape special characters with a backtick.
288 Default is: $null.
289.PARAMETER Body
290 Specify the request body to use.
291 Default is: $null.
292.PARAMETER ContentType
293 Specify the content type to use.
294 Default is: 'application/json'.
295.EXAMPLE
296 Invoke-MSGraphAPI -Method 'GET' -Token $Token -Version 'Beta' -Resource 'deviceManagement/managedDevices' -Parameter "filter=operatingSystem like 'Windows' and deviceName like 'MEM-Zone-PC'"
297.EXAMPLE
298 Invoke-MSGraphAPI -Token $Token -Resource 'users'
299.INPUTS
300 None.
301.OUTPUTS
302 System.Object
303.NOTES
304 Created by Ioan Popovici
305 v1.0.0 - 2024-01-11
306.LINK
307 https://MEMZ.one/Invoke-MSGraphAPI
308.LINK
309 https://MEMZ.one/Invoke-MSGraphAPI-CHANGELOG
310.LINK
311 https://MEMZ.one/Invoke-MSGraphAPI-GIT
312.LINK
313 https://MEM.Zone/ISSUES
314.COMPONENT
315 MSGraph
316.FUNCTIONALITY
317 Invokes the Microsoft Graph API.
318#>
319 [CmdletBinding()]
320 Param (
321 [Parameter(Mandatory = $false, HelpMessage = 'Specify the method to use.', Position = 0)]
322 [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')]
323 [Alias('HTTPMethod')]
324 [string]$Method = 'GET',
325 [Parameter(Mandatory = $true, HelpMessage = 'Specify the access token to use.', Position = 1)]
326 [ValidateNotNullorEmpty()]
327 [Alias('AccessToken')]
328 [string]$Token,
329 [Parameter(Mandatory = $false, HelpMessage = 'Specify the version of the Microsoft Graph API to use.', Position = 2)]
330 [ValidateSet('Beta', 'v1.0')]
331 [Alias('GraphVersion')]
332 [string]$Version = 'Beta',
333 [Parameter(Mandatory = $true, HelpMessage = 'Specify the resource to query.', Position = 3)]
334 [ValidateNotNullorEmpty()]
335 [Alias('APIResource')]
336 [string]$Resource,
337 [Parameter(Mandatory = $false, HelpMessage = 'Specify the parameters to use.', Position = 4)]
338 [ValidateNotNullorEmpty()]
339 [Alias('QueryParameter')]
340 [string]$Parameter,
341 [Parameter(Mandatory = $false, HelpMessage = 'Specify the request body to use.', Position = 5)]
342 [ValidateNotNullorEmpty()]
343 [Alias('RequestBody')]
344 [string]$Body,
345 [Parameter(Mandatory = $false, HelpMessage = 'Specify the content type to use.', Position = 6)]
346 [ValidateNotNullorEmpty()]
347 [Alias('Type')]
348 [string]$ContentType = 'application/json'
349 )
350
351 Begin {
352
353 ## Assemble the URI for the API call
354 [string]$Uri = "https://graph.microsoft.com/$Version/$Resource"
355 If (-not [string]::IsNullOrWhiteSpace($Parameter)) { $Uri += "`?`$$Parameter" }
356
357 ## Assembly parameters for the API call
358 [hashtable]$Parameters = @{
359 'Uri' = $Uri
360 'Method' = $Method
361 'Headers' = @{
362 'Content-Type' = 'application\json'
363 'Authorization' = "Bearer $Token"
364 }
365 'ContentType' = $ContentType
366 }
367 If (-not [string]::IsNullOrWhiteSpace($Body)) { $Parameters.Add('Body', $Body) }
368
369 ## Write Debug information
370 Write-Debug -Message "Uri: $Uri"
371 }
372 Process {
373 Try {
374
375 ## Invoke the MSGraph API
376 $Output = Invoke-RestMethod @Parameters
377
378 ## If there are more than 1000 rows, use paging. Only for GET method.
379 If (-not [string]::IsNullOrEmpty($Output.'@odata.nextLink')) {
380 # Assign the nextLink to the Uri
381 $Parameters.Uri = $Output.'@odata.nextLink'
382 [array]$Output += Do {
383 # Invoke the MSGraph API
384 $OutputPage = Invoke-RestMethod @Parameters
385 # Assign the nextLink to the Uri
386 $Parameters.Uri = $OutputPage.'@odata.nextLink'
387 # Write Debug information
388 Write-Debug -Message "Parameters:`n$($Parameters | Out-String)"
389 # Return the OutputPage
390 $OutputPage
391 }
392 Until ([string]::IsNullOrEmpty($OutputPage.'@odata.nextLink'))
393 }
394 Write-Verbose -Message "Got '$($Output.Count)' Output pages."
395 }
396 Catch {
397 [string]$Message = "Error invoking MSGraph API version '{0}' for resource '{1}' using '{2}' method.`n{3}" -f $Version, $Resource, $Method, $Error[0].Exception.Message
398 Write-Log -Message $Message -Severity 3
399 Write-Error -Message $Message
400 }
401 Finally {
402 $Output = If ($Output.value) { $Output.value } Else { $Output }
403 Write-Output -InputObject $Output
404 }
405 }
406 End {
407 }
408}
409#endregion Function Invoke-MSGraphAPI
410
411#region function Send-SlackMessage
412Function Send-SlackMessage {
413
414 [CmdletBinding()]
415 Param (
416 [Parameter(Mandatory = $true, HelpMessage = 'Specify the header.', Position = 0)]
417 [Alias('Title')]
418 [string]$Header,
419 [Parameter(Mandatory = $true, HelpMessage = 'Total licenses.', Position = 1)]
420 [Alias('Total')]
421 [string]$AvailableLicenses,
422 [Parameter(Mandatory = $true, HelpMessage = 'Used licenses.', Position = 2)]
423 [Alias('Used')]
424 [string]$UsedLicenses,
425 [Parameter(Mandatory = $true, HelpMessage = 'Licenses remaining.', Position = 3)]
426 [Alias('Remaining')]
427 [string]$RemainingLicenses,
428 [Parameter(Mandatory = $true, HelpMessage = 'Slack webhook URL', Position = 4)]
429 [Alias('WebhookURI')]
430 [string]$slackWebhookURI
431 )
432
433 ## Assemble the notification payload
434 [string]$Body =
435@"
436{
437 "blocks": [
438 {
439 "type": "header",
440 "text": {
441 "type": "plain_text",
442 "text": ":alert: $Header :alert:",
443 "emoji": true
444 }
445 },
446 {
447 "type": "divider"
448 },
449 {
450 "type": "section",
451 "text": {
452 "type": "mrkdwn",
453 "text": "*Available Licenses:* $AvailableLicenses"
454 }
455 },
456 {
457 "type": "section",
458 "text": {
459 "type": "mrkdwn",
460 "text": "*Used Licenses:* $UsedLicenses"
461 }
462 },
463 {
464 "type": "section",
465 "text": {
466 "type": "mrkdwn",
467 "text": "*Remaining Licenses: $RemainingLicenses*"
468 }
469 },
470 {
471 "type": "section",
472 "text": {
473 "type": "plain_text",
474 "text": "Please check if more licenses are required!",
475 "emoji": true
476 }
477 }
478 ]
479}
480"@
481
482 ## Post to slack
483 Start-Sleep -Seconds 1
484 Try {
485 $SlackNotify = Invoke-RestMethod -uri $SlackWebhookURI -Method 'POST' -Body $Body -ContentType 'application/json'
486
487 If ($SlackNotify -ne 'ok') { $Output = "Could not send Slack message! '$SlackNotify'" } Else { $Output = 'Slack Message Sent!' }
488 }
489 Catch {
490 Write-Error -Message "Error Sending Slack Message. $($_.Exception.Message)"
491 }
492 Finally {
493 Write-Output -InputObject $Output
494 }
495}
496#endregion Function Send-SlackMessage
497
498#endregion FunctionListings
499##*=============================================
500##* END FUNCTION LISTINGS
501##*=============================================
502
503##*=============================================
504##* SCRIPT BODY
505##*=============================================
506#region ScriptBody
507
508#First remove old log:
509If (Test-Path "$ScriptPath\$ScriptName.txt" -PathType 'Leaf') { Remove-Item -Path "$ScriptPath\$ScriptName.txt" -Force -Confirm:$false -ErrorAction 'SilentlyContinue' }
510
511## Write Start verbose message
512Write-Log -Message "Start '$ScriptName'" -Severity 1
513
514## Get API Token
515$AccessToken = Get-MSGraphAPIAccessToken -TenantID $TenantID ClientID $ClientID ClientSecret $ClientSecret -ErrorAction 'Stop'
516$Token = $AccessToken.access_token
517
518
519## Get all licenses from the tenant
520$AvailableLicenses = Invoke-MSGraphAPI -Method 'GET' -Version 'v1.0' -Token $Token -Resource 'subscribedSkus' -ErrorAction 'Stop'
521
522## Get information from 'https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-service-plan-reference'
523Try {
524
525 # Fetch the content as bytes
526 $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'
527
528 # Convert the content from a byte array to a string, assuming it's UTF8 encoded
529 $Utf8NoBom = New-Object 'System.Text.UTF8Encoding' $false
530 [string]$TranslationTable = $Utf8NoBom.GetString($Response.Content) | ConvertFrom-Csv
531}
532Catch {
533 $Output = "Could not get the translation table!"
534 Write-Log -Message $Output -Severity 3
535 Write-Verbose -Message $Output -Verbose
536}
537
538## Get each license information with the SkuID and check if it is lower then the specific value in $minAmount
539Foreach ($SkuID in $SkuIDs) {
540
541 ## Write verbose message
542 Write-Verbose = "Checking license: {0}" -f $SkuID
543
544 ## Calculate Token Expiry time
545 $TokenExpiryTime = $AccessToken.granted_on.ToUniversalTime().AddSeconds($AccessToken.expires_in)
546
547 ## If token expires in 5 minutes then generate new token
548 If ($TokenExpiryTime.AddMinutes(-5) -lt [DateTime]::UtcNow) {
549
550 ## Regenerate token
551 $AccessToken = Get-MSGraphAPIAccessToken -TenantID $TenantID ClientID $ClientID ClientSecret $ClientSecret -ErrorAction 'Stop'
552 $Token = $AccessToken.access_token
553 }
554
555 $SkuIdLicense = $AvailableLicenses | Where-Object { $PsItem.skuId -eq $SkuId }
556 $ResolvedSkuName = ($TranslationTable | Where-Object { $PSItem.GUID -eq $SkuID_license.skuId } | Sort-Object -Property 'Product_Display_Name' -Unique).Product_Display_Name
557
558 [int]$AvailableLicenses = $SkuIDLicense.prepaidUnits.enabled
559 [int]$UsedLicenses = $SkuIDLicense.consumedUnits
560 [int]$RemainingLicenses = $AvailableLicenses - $UsedLicenses
561
562 If ($RemainingLicenses -lt $MinimumLicenseThreshold) {
563 [string]$Header = "License: $ResolvedSkuName"
564 Send-SlackMessage -Header $Header -AvailableLicenses $AvailableLicenses -UsedLicenses $UsedLicenses -RemainingLicenses $RemainingLicenses -SlackWebhookURI $SlackWebhookURI
565 $Output = "The license: {0} is below the amount of: '{1}'. Message to slack has been send." -f $ResolvedSkuName, $MinimumLicenseThreshold
566 Write-Log -Message $Output -Severity 2
567 }
568 Else {
569 $Output = "There are {0} {1} licenses left. So it is not below: '{2}'." -f $RemainingLicenses, $ResolvedSkuName, $MinimumLicenseThreshold
570 Write-Log -Message $Output -Severity 1
571 }
572 Write-Verbose -Message $Output -Verbose
573}
574#endregion ScriptBody
575##*=============================================
576##* END SCRIPT BODY
577##*=============================================