Dark mode

Dark mode

There are 0 results matching

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: Intune Device Renaming Tool

Published by Vilcu Paul · Jan 23, 2024 · 3 mins read
article card image dark article card image light

Quick Summary

Microsoft Intune provides the capability of renaming device names via the Intune Portal, and also supports bulk renaming, but doesn’t support Linux devices.

We recently encountered a scenario where we needed macOS and Linux devices on-boarded to Intune via the Company Portal to respect a set naming convention using a specific UserAttribute.

To solve this issue, we created a PowerShell script that renames one or more devices and can target a specific Operating System. It supports setting a Prefix or using a User Attribute as prefix, automatic trimming of the name for Windows devices, and full logging.

Prerequisites

What You Need

  • Microsoft Entra Application registration with MS Graph API permissions. See a great example from Laura Kokkarinen.
  • Microsoft Entra synched extension attribute with the value you want to use as prefix (optional).
  • PowerShell Script

Supported Devices

  • Android Enterprise:
    • Corporate-owned work profiles
    • Dedicated devices
    • Fully managed
  • iOS/iPadOS supervised devices with iOS 9.3 and later
  • macOS 10 - Corporate-owned devices
  • Windows - Corporate-owned devices
  • Linux - Corporate-owned devices
  • Corporate-owned co-managed devices that are Microsoft Entra joined
Notes

All rights needed to run this script will get prompted the first time you run it with your privilege account on your Tenant.
You can review the necessary rights and asses if this is acceptable for you.

Notes

  • The rename action does not change the Management Name in the Intune Portal or the Device Name in Company Portal.
  • Hybrid Microsoft Entra devices are not supported, you can use a local device rename script for those and the changes will be propagated everywhere.


Recommendations

  • Run the script with -WhatIf, -Confim and Verbose first to verify that everything works as intended.
  • Use multiple test devices to validate your configuration.
  • History is available in the Endpoint Management event log or %SystemRoot%\Logs\Rename-IntuneDevice\ folder.
  • An Azure Automation Account is recommended for automation, running at a set interval.
Notes

Although the script works in an environment with 15.000 devices, it cannot account for all edge cases. Please test thoroughly before using it in production.


Parameters

TenantID

This parameter specifies the tenant ID.

ApplicationID

This parameter specifies the application (client) ID.

ApplicationSecret

This parameter specifies the application (client) secret.

DeviceName

This parameter specifies the device name to be processed.

Defaults to All.

DeviceOS

This parameter specifies the device OS to be processed.

  • Windows
  • macOS
  • Linux

Defaults to All.

Prefix

This parameter specifies the prefix to use.

Defaults to INTUNE.

PrefixFromUserAttribute

This parameter specifies the user attribute to query and use as the prefix. If no user attribute exists, the device is skipped.


Notes

  • Prefix is always truncated to 6 characters and converted to UPPERCASE.
  • Suffix is always the device serial number converted to UPPERCASE and cannot be changed.
  • Serial Number is always truncated so the Device Name doesn’t exceed 15 characters for Windows OS.
  • PrefixFromUserAttribute output is always truncated to 6 characters and converted to UPPERCASE.


Examples

Example 1

$ConnectionParameters = [hashtable]@{
    TenantID          = '10000000-1000-1000-1000-100000000000'
    ApplicationID     = '12345678-1234-1234-1234-123456789101'
    ApplicationSecret = 'xa21kkjkash3asjjsakdkjdhk'
}
Rename-IntuneDevice.ps1 @ConnectionParameters -DeviceName 'IntuneDevice001' -WhatIf -Verbose

Example 2

$ConnectionParameters = [hashtable]@{
    TenantID          = '10000000-1000-1000-1000-100000000000'
    ApplicationID     = '12345678-1234-1234-1234-123456789101'
    ApplicationSecret = 'xa21kkjkash3asjjsakdkjdhk'
}
Rename-IntuneDevice.ps1 @ConnectionParameters -DeviceOS 'Windows' -Prefix 'TAG'

Example 3

$ConnectionParameters = [hashtable]@{
    TenantID          = '10000000-1000-1000-1000-100000000000'
    ApplicationID     = '12345678-1234-1234-1234-123456789101'
    ApplicationSecret = 'xa21kkjkash3asjjsakdkjdhk'
}
Rename-IntuneDevice.ps1 @ConnectionParameters -DeviceOS 'Windows' -PrefixFromUserAttribute 'extension_16db5763993a4e819bc7dd1824184322_msDS_cloudExtensionAttribute5'

Example 4

$ConnectionParameters = [hashtable]@{
    TenantID          = '10000000-1000-1000-1000-100000000000'
    ApplicationID     = '12345678-1234-1234-1234-123456789101'
    ApplicationSecret = 'xa21kkjkash3asjjsakdkjdhk'
}
Rename-IntuneDevice.ps1 @ConnectionParameters -Confirm

Preview

article card image windows-script-eventlog
Script event log
article card image windows-script-file-log
Script file log

Code

   1<#
   2.SYNOPSIS
   3    Renames an Intune device.
   4.DESCRIPTION
   5    Renames an Intune device according to a specified naming convention.
   6.PARAMETER TenantID
   7    Specifies the tenant ID.
   8.PARAMETER ApplicationID
   9    Specifies the application ID.
  10.PARAMETER ApplicationSecret
  11    Specifies the application secret.
  12.PARAMETER DeviceName
  13    Specifies the device name to be processed.
  14    Default is: 'All'.
  15.PARAMETER DeviceOS
  16    Specifies the device OS to be processed
  17    Valid values are: 'Windows', 'macOS', 'iOS/iPadOS', 'Linux', 'Android', 'All',  .
  18    Default is: 'All'.
  19.PARAMETER Prefix
  20    Specifies the prefix to be used. Please note that it will be truncated to 6 characters and converted to UPPERCASE.
  21    If this parameter is used, the PrefixFromUserAttribute parameter will be ignored.
  22    Default is: 'INTUNE'.
  23.PARAMETER PrefixFromUserAttribute
  24    Specifies the user attribute to be used queried and used as prefix. The result will be truncated to 6 characters.
  25    If this parameter is used, the Prefix parameter will be ignored.
  26.EXAMPLE
  27    Rename-IntuneDevice.ps1 -TenantID $TenantID -ApplicationID $ApplicationID -ApplicationSecret $ApplicationSecret -DeviceName 'IntuneDevice001' -WhatIf -Verbose
  28.EXAMPLE
  29    Rename-IntuneDevice.ps1 -TenantID $TenantID -ApplicationID $ApplicationID -ApplicationSecret $ApplicationSecret -DeviceOS 'macOS' -Prefix 'TAG'
  30.EXAMPLE
  31    Rename-IntuneDevice.ps1 -TenantID $TenantID -ApplicationID $ApplicationID -ApplicationSecret $ApplicationSecret -DeviceOS 'Windows' -PrefixFromUserAttribute 'extension_11db5763783a4e822bd6dsd1826184312_msDS_cloudExtensionAttribute66'
  32.EXAMPLE
  33    Rename-IntuneDevice.ps1 -TenantID $TenantID -ApplicationID $ApplicationID -ApplicationSecret $ApplicationSecret -DeviceOS 'Android' -Confirm
  34.INPUTS
  35    None.
  36.OUTPUTS
  37    None.
  38.NOTES
  39    Created by Ferry Bodijn
  40    Rewritten by Ioan Popovici to add parameters, improve logging and simplify the script. All other functionality remains the same.
  41    v1.0.0 - 2021-09-01
  42
  43    Supports WhatIf and Confirm, see links below for more information.
  44.LINK
  45    https://MEMZ.one/Rename-IntuneDevice
  46.LINK
  47    https://MEMZ.one/Rename-IntuneDevice-CHANGELOG
  48.LINK
  49    https://MEMZ.one/Rename-IntuneDevice-GIT
  50.LINK
  51    https://MEM.Zone/ISSUES
  52.LINK
  53    https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-shouldprocess?view=powershell-7.3#using--whatif
  54.COMPONENT
  55    MSGraph
  56.FUNCTIONALITY
  57    Renames device in Intune.
  58#>
  59
  60##*=============================================
  61##* VARIABLE DECLARATION
  62##*=============================================
  63#region VariableDeclaration
  64
  65## Set script requirements
  66#Requires -Version 5.0
  67
  68## Get script parameters
  69[CmdletBinding(SupportsShouldProcess=$true, DefaultParameterSetName = 'Custom')]
  70Param (
  71    [Parameter(Mandatory = $true, ParameterSetName = 'Custom', HelpMessage = 'Specify the tenant ID', Position = 0)]
  72    [Parameter(Mandatory = $true, ParameterSetName = 'UserAttribute', HelpMessage = 'Enter the tenant ID', Position = 0)]
  73    [ValidateNotNullorEmpty()]
  74    [Alias('Tenant')]
  75    [string]$TenantID,
  76    [Parameter(Mandatory = $true, ParameterSetName = 'Custom', HelpMessage = 'Specify the Application (Client) ID to use.', Position = 1)]
  77    [Parameter(Mandatory = $true, ParameterSetName = 'UserAttribute', HelpMessage = 'Specify the Application (Client) ID to use.', Position = 1)]
  78    [ValidateNotNullorEmpty()]
  79    [Alias('ApplicationClientID')]
  80    [string]$ClientID,
  81    [Parameter(Mandatory = $true, ParameterSetName = 'Custom', HelpMessage = 'Specify the Application (Client) Secret to use.', Position = 2)]
  82    [Parameter(Mandatory = $true, ParameterSetName = 'UserAttribute', HelpMessage = 'Specify the Application (Client) Secret to use.', Position = 2)]
  83    [ValidateNotNullorEmpty()]
  84    [Alias('ApplicationClientSecret')]
  85    [string]$ClientSecret,
  86    [Parameter(Mandatory = $false, ParameterSetName = 'Custom', HelpMessage = 'Specify the device name to be processed. Supports wildcard characters. Default is: All', Position = 3)]
  87    [Parameter(Mandatory = $false, ParameterSetName = 'UserAttribute', HelpMessage = 'Specify the device name to be processed. Supports wildcard characters. Default is: All', Position = 3)]
  88    [ValidateNotNullorEmpty()]
  89    [Alias('Device')]
  90    [string]$DeviceName = 'All',
  91    [Parameter(Mandatory = $false, ParameterSetName = 'Custom', HelpMessage = 'Specify the device OS to be processed. Valid values are: Windows, macOS, Linux, All', Position = 4)]
  92    [Parameter(Mandatory = $false, ParameterSetName = 'UserAttribute', HelpMessage = 'Specify the device OS to be processed. Valid values are: Windows, macOS, Linux, Android, iOS/iPadOS, All', Position = 4)]
  93    [ValidateSet('Windows', 'macOS', 'Linux', 'Android', 'iOS/iPadOS', 'All')]
  94    [string]$DeviceOS = 'All',
  95    [Parameter(Mandatory = $false, ParameterSetName = 'Custom', HelpMessage = 'Specify the prefix to be used. Default isL INTUNE', Position = 5)]
  96    [ValidateNotNullorEmpty()]
  97    [string]$Prefix = 'INTUNE',
  98    [Parameter(Mandatory = $true, ParameterSetName = 'UserAttribute', HelpMessage = 'Specify the user attribute to be used queried and used as prefix. The result will be truncated to 6 characters.', Position = 5)]
  99    [ValidateNotNullorEmpty()]
 100    [Alias('UserAttribute')]
 101    [string]$PrefixFromUserAttribute
 102)
 103
 104## Get script path and name
 105[string]$ScriptName     = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Definition)
 106[string]$ScriptFullName = [System.IO.Path]::GetFullPath($MyInvocation.MyCommand.Definition)
 107
 108## Get Show-Progress steps
 109$ProgressSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Command' -and $_.Content -eq 'Show-Progress' }).Count)
 110$ForEachSteps  = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Keyword' -and $_.Content -eq 'ForEach' }).Count)
 111
 112## Set Show-Progress steps
 113$script:Steps = $ProgressSteps - $ForEachSteps
 114$script:Step  = 1
 115
 116## Set script global variables
 117$script:LoggingOptions   = @('EventLog', 'File', 'Host')
 118$script:LogName          = 'Endpoint Management'
 119$script:LogSource        = $ScriptName
 120$script:LogDebugMessages = $false
 121$script:LogFileDirectory = If ($LogPath) { Join-Path -Path $LogPath -ChildPath $script:LogName } Else { $(Join-Path -Path $Env:WinDir -ChildPath $('\Logs\' + $script:LogName)) }
 122
 123## Initialize script variables
 124If (-not $PSBoundParameters['DeviceName']) { $DeviceName = 'All' }
 125#  Build supported operating systems array
 126[string[]]$SupportedOperatingSystems = @('Windows', 'macOS', 'iOS/iPadOS', 'Linux', 'Android (Corporate-owned work profile)', 'Android (fully managed)', 'Android (dedicated)')
 127
 128#endregion
 129##*=============================================
 130##* END VARIABLE DECLARATION
 131##*=============================================
 132
 133##*=============================================
 134##* FUNCTION LISTINGS
 135##*=============================================
 136#region FunctionListings
 137
 138#region Function Resolve-Error
 139Function Resolve-Error {
 140<#
 141.SYNOPSIS
 142    Enumerate error record details.
 143.DESCRIPTION
 144    Enumerate an error record, or a collection of error record, properties. By default, the details for the last error will be enumerated.
 145.PARAMETER ErrorRecord
 146    The error record to resolve. The default error record is the latest one: $global:Error[0]. This parameter will also accept an array of error records.
 147.PARAMETER Property
 148    The list of properties to display from the error record. Use "*" to display all properties.
 149    Default list of error properties is: Message, FullyQualifiedErrorId, ScriptStackTrace, PositionMessage, InnerException
 150.PARAMETER GetErrorRecord
 151    Get error record details as represented by $_.
 152.PARAMETER GetErrorInvocation
 153    Get error record invocation information as represented by $_.InvocationInfo.
 154.PARAMETER GetErrorException
 155    Get error record exception details as represented by $_.Exception.
 156.PARAMETER GetErrorInnerException
 157    Get error record inner exception details as represented by $_.Exception.InnerException. Will retrieve all inner exceptions if there is more than one.
 158.EXAMPLE
 159    Resolve-Error
 160.EXAMPLE
 161    Resolve-Error -Property *
 162.EXAMPLE
 163    Resolve-Error -Property InnerException
 164.EXAMPLE
 165    Resolve-Error -GetErrorInvocation:$false
 166.NOTES
 167    Unmodified version of the PADT error resolving cmdlet. I did not write the original cmdlet, please do not credit me for it!
 168.LINK
 169    https://psappdeploytoolkit.com
 170#>
 171    [CmdletBinding()]
 172    Param (
 173        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
 174        [AllowEmptyCollection()]
 175        [array]$ErrorRecord,
 176        [Parameter(Mandatory = $false, Position = 1)]
 177        [ValidateNotNullorEmpty()]
 178        [string[]]$Property = ('Message', 'InnerException', 'FullyQualifiedErrorId', 'ScriptStackTrace', 'PositionMessage'),
 179        [Parameter(Mandatory = $false, Position = 2)]
 180        [switch]$GetErrorRecord = $true,
 181        [Parameter(Mandatory = $false, Position = 3)]
 182        [switch]$GetErrorInvocation = $true,
 183        [Parameter(Mandatory = $false, Position = 4)]
 184        [switch]$GetErrorException = $true,
 185        [Parameter(Mandatory = $false, Position = 5)]
 186        [switch]$GetErrorInnerException = $true
 187    )
 188
 189    Begin {
 190        ## If function was called without specifying an error record, then choose the latest error that occurred
 191        If (-not $ErrorRecord) {
 192            If ($global:Error.Count -eq 0) {
 193                #Write-Warning -Message "The `$Error collection is empty"
 194                Return
 195            }
 196            Else {
 197                [array]$ErrorRecord = $global:Error[0]
 198            }
 199        }
 200
 201        ## Allows selecting and filtering the properties on the error object if they exist
 202        [scriptblock]$SelectProperty = {
 203            Param (
 204                [Parameter(Mandatory = $true)]
 205                [ValidateNotNullorEmpty()]
 206                $InputObject,
 207                [Parameter(Mandatory = $true)]
 208                [ValidateNotNullorEmpty()]
 209                [string[]]$Property
 210            )
 211
 212            [string[]]$ObjectProperty = $InputObject | Get-Member -MemberType '*Property' | Select-Object -ExpandProperty 'Name'
 213            ForEach ($Prop in $Property) {
 214                If ($Prop -eq '*') {
 215                    [string[]]$PropertySelection = $ObjectProperty
 216                    Break
 217                }
 218                ElseIf ($ObjectProperty -contains $Prop) {
 219                    [string[]]$PropertySelection += $Prop
 220                }
 221            }
 222            Write-Output -InputObject $PropertySelection
 223        }
 224
 225        #  Initialize variables to avoid error if 'Set-StrictMode' is set
 226        $LogErrorRecordMsg = $null
 227        $LogErrorInvocationMsg = $null
 228        $LogErrorExceptionMsg = $null
 229        $LogErrorMessageTmp = $null
 230        $LogInnerMessage = $null
 231    }
 232    Process {
 233        If (-not $ErrorRecord) { Return }
 234        ForEach ($ErrRecord in $ErrorRecord) {
 235            ## Capture Error Record
 236            If ($GetErrorRecord) {
 237                [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord -Property $Property
 238                $LogErrorRecordMsg = $ErrRecord | Select-Object -Property $SelectedProperties
 239            }
 240
 241            ## Error Invocation Information
 242            If ($GetErrorInvocation) {
 243                If ($ErrRecord.InvocationInfo) {
 244                    [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.InvocationInfo -Property $Property
 245                    $LogErrorInvocationMsg = $ErrRecord.InvocationInfo | Select-Object -Property $SelectedProperties
 246                }
 247            }
 248
 249            ## Capture Error Exception
 250            If ($GetErrorException) {
 251                If ($ErrRecord.Exception) {
 252                    [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.Exception -Property $Property
 253                    $LogErrorExceptionMsg = $ErrRecord.Exception | Select-Object -Property $SelectedProperties
 254                }
 255            }
 256
 257            ## Display properties in the correct order
 258            If ($Property -eq '*') {
 259                #  If all properties were chosen for display, then arrange them in the order the error object displays them by default.
 260                If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg }
 261                If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg }
 262                If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
 263            }
 264            Else {
 265                #  Display selected properties in our custom order
 266                If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
 267                If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg }
 268                If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg }
 269            }
 270
 271            If ($LogErrorMessageTmp) {
 272                $LogErrorMessage = 'Error Record:'
 273                $LogErrorMessage += "`n-------------"
 274                $LogErrorMsg = $LogErrorMessageTmp | Format-List | Out-String
 275                $LogErrorMessage += $LogErrorMsg
 276            }
 277
 278            ## Capture Error Inner Exception(s)
 279            If ($GetErrorInnerException) {
 280                If ($ErrRecord.Exception -and $ErrRecord.Exception.InnerException) {
 281                    $LogInnerMessage = 'Error Inner Exception(s):'
 282                    $LogInnerMessage += "`n-------------------------"
 283
 284                    $ErrorInnerException = $ErrRecord.Exception.InnerException
 285                    $Count = 0
 286
 287                    While ($ErrorInnerException) {
 288                        [string]$InnerExceptionSeperator = '~' * 40
 289
 290                        [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrorInnerException -Property $Property
 291                        $LogErrorInnerExceptionMsg = $ErrorInnerException | Select-Object -Property $SelectedProperties | Format-List | Out-String
 292
 293                        If ($Count -gt 0) { $LogInnerMessage += $InnerExceptionSeperator }
 294                        $LogInnerMessage += $LogErrorInnerExceptionMsg
 295
 296                        $Count++
 297                        $ErrorInnerException = $ErrorInnerException.InnerException
 298                    }
 299                }
 300            }
 301
 302            If ($LogErrorMessage) { $Output = $LogErrorMessage }
 303            If ($LogInnerMessage) { $Output += $LogInnerMessage }
 304
 305            Write-Output -InputObject $Output
 306
 307            If (Test-Path -LiteralPath 'variable:Output') { Clear-Variable -Name 'Output' }
 308            If (Test-Path -LiteralPath 'variable:LogErrorMessage') { Clear-Variable -Name 'LogErrorMessage' }
 309            If (Test-Path -LiteralPath 'variable:LogInnerMessage') { Clear-Variable -Name 'LogInnerMessage' }
 310            If (Test-Path -LiteralPath 'variable:LogErrorMessageTmp') { Clear-Variable -Name 'LogErrorMessageTmp' }
 311        }
 312    }
 313    End {
 314    }
 315}
 316#endregion
 317
 318#region Function Write-Log
 319Function Write-Log {
 320<#
 321.SYNOPSIS
 322    Write messages to a log file in CMTrace.exe compatible format or Legacy text file format.
 323.DESCRIPTION
 324    Write messages to a log file in CMTrace.exe compatible format or Legacy text file format and optionally display in the console.
 325.PARAMETER Message
 326    The message to write to the log file or output to the console.
 327.PARAMETER Severity
 328    Defines message type. When writing to console or CMTrace.exe log format, it allows highlighting of message type.
 329    Options: 1 = Information (default), 2 = Warning (highlighted in yellow), 3 = Error (highlighted in red)
 330.PARAMETER Source
 331    The source of the message being logged. Also used as the event log source.
 332.PARAMETER ScriptSection
 333    The heading for the portion of the script that is being executed. Default is: $script:scriptSection.
 334.PARAMETER LogType
 335    Choose whether to write a CMTrace.exe compatible log file or a Legacy text log file.
 336.PARAMETER LoggingOptions
 337    Choose where to log 'Console', 'File', 'EventLog' or 'None'. You can choose multiple options.
 338.PARAMETER LogFileDirectory
 339    Set the directory where the log file will be saved.
 340.PARAMETER LogFileName
 341    Set the name of the log file.
 342.PARAMETER MaxLogFileSizeMB
 343    Maximum file size limit for log file in megabytes (MB). Default is 10 MB.
 344.PARAMETER LogName
 345    Set the name of the event log.
 346.PARAMETER EventID
 347    Set the event id for the event log entry.
 348.PARAMETER WriteHost
 349    Write the log message to the console.
 350.PARAMETER ContinueOnError
 351    Suppress writing log message to console on failure to write message to log file. Default is: $true.
 352.PARAMETER PassThru
 353    Return the message that was passed to the function
 354.PARAMETER VerboseMessage
 355    Specifies that the message is a debug message. Verbose messages only get logged if -LogDebugMessage is set to $true.
 356.PARAMETER DebugMessage
 357    Specifies that the message is a debug message. Debug messages only get logged if -LogDebugMessage is set to $true.
 358.PARAMETER LogDebugMessage
 359    Debug messages only get logged if this parameter is set to $true in the config XML file.
 360.EXAMPLE
 361    Write-Log -Message "Installing patch MS15-031" -Source 'Add-Patch' -LogType 'CMTrace'
 362.EXAMPLE
 363    Write-Log -Message "Script is running on Windows 8" -Source 'Test-ValidOS' -LogType 'Legacy'
 364.NOTES
 365    Slightly modified version of the PSADT logging cmdlet. I did not write the original cmdlet, please do not credit me for it.
 366.LINK
 367    https://psappdeploytoolkit.com
 368#>
 369    [CmdletBinding()]
 370    Param (
 371        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
 372        [AllowEmptyCollection()]
 373        [Alias('Text')]
 374        [string[]]$Message,
 375        [Parameter(Mandatory = $false, Position = 1)]
 376        [ValidateRange(1, 3)]
 377        [int16]$Severity = 1,
 378        [Parameter(Mandatory = $false, Position = 2)]
 379        [ValidateNotNullorEmpty()]
 380        [string]$Source = $script:LogSource,
 381        [Parameter(Mandatory = $false, Position = 3)]
 382        [ValidateNotNullorEmpty()]
 383        [string]$ScriptSection = $script:ScriptSection,
 384        [Parameter(Mandatory = $false, Position = 4)]
 385        [ValidateSet('CMTrace', 'Legacy')]
 386        [string]$LogType = 'CMTrace',
 387        [Parameter(Mandatory = $false, Position = 5)]
 388        [ValidateSet('Host', 'File', 'EventLog', 'None')]
 389        [string[]]$LoggingOptions = $script:LoggingOptions,
 390        [Parameter(Mandatory = $false, Position = 6)]
 391        [ValidateNotNullorEmpty()]
 392        [string]$LogFileDirectory = $script:LogFileDirectory,
 393        [Parameter(Mandatory = $false, Position = 7)]
 394        [ValidateNotNullorEmpty()]
 395        [string]$LogFileName = $($script:LogSource + '.log'),
 396        [Parameter(Mandatory = $false, Position = 8)]
 397        [ValidateNotNullorEmpty()]
 398        [int]$MaxLogFileSizeMB = 5,
 399        [Parameter(Mandatory = $false, Position = 9)]
 400        [ValidateNotNullorEmpty()]
 401        [string]$LogName = $script:LogName,
 402        [Parameter(Mandatory = $false, Position = 10)]
 403        [ValidateNotNullorEmpty()]
 404        [int32]$EventID = 1,
 405        [Parameter(Mandatory = $false, Position = 11)]
 406        [ValidateNotNullorEmpty()]
 407        [boolean]$ContinueOnError = $false,
 408        [Parameter(Mandatory = $false, Position = 12)]
 409        [switch]$PassThru = $false,
 410        [Parameter(Mandatory = $false, Position = 13)]
 411        [switch]$VerboseMessage = $false,
 412        [Parameter(Mandatory = $false, Position = 14)]
 413        [switch]$DebugMessage = $false,
 414        [Parameter(Mandatory = $false, Position = 15)]
 415        [boolean]$LogDebugMessage = $script:LogDebugMessages
 416    )
 417
 418    Begin {
 419        ## Get the name of this function
 420        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
 421
 422        ## Logging Variables
 423        #  Log file date/time
 424        [string]$LogTime = (Get-Date -Format 'HH:mm:ss.fff').ToString()
 425        [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString()
 426        If (-not (Test-Path -LiteralPath 'variable:LogTimeZoneBias')) { [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes }
 427        [string]$LogTimePlusBias = $LogTime + '-' + $script:LogTimeZoneBias
 428        #  Initialize variables
 429        [boolean]$WriteHost = $false
 430        [boolean]$WriteFile = $false
 431        [boolean]$WriteEvent = $false
 432        [boolean]$DisableLogging = $false
 433        [boolean]$ExitLoggingFunction = $false
 434        If ('Host' -in $LoggingOptions -and -not ($VerboseMessage -or $DebugMessage)) { $WriteHost = $true }
 435        If ('File' -in $LoggingOptions) { $WriteFile = $true }
 436        If ('EventLog' -in $LoggingOptions) { $WriteEvent = $true }
 437        If ('None' -in $LoggingOptions) { $DisableLogging = $true }
 438        #  Check if the script section is defined
 439        [boolean]$ScriptSectionDefined = $(-not [string]::IsNullOrEmpty($ScriptSection))
 440        #  Check if the source is defined
 441        [boolean]$SourceDefined = $(-not [string]::IsNullOrEmpty($Source))
 442        #  Check if the log name is defined
 443        [boolean]$LogNameDefined = $(-not [string]::IsNullOrEmpty($LogName))
 444        #  Check for overlapping log names if the log name does not exist
 445        If ($SourceDefined -and $LogNameDefined) {
 446        #  Check if the event log and event source exist
 447        [boolean]$LogNameNotExists = (-not [System.Diagnostics.EventLog]::Exists($LogName))
 448        [boolean]$LogSourceNotExists = (-not [System.Diagnostics.EventLog]::SourceExists($Source))
 449        #  Check for overlapping log names. The first 8 characters of the log name must be unique.
 450            If ($LogNameNotExists) {
 451                [string[]]$OverLappingLogName = Get-EventLog -List | Where-Object -Property 'Log' -Like  $($LogName.Substring(0,8) + '*') | Select-Object -ExpandProperty 'Log'
 452                If (-not [string]::IsNullOrEmpty($OverLappingLogName)) {
 453                    Write-Warning -Message "Overlapping log names:`n$($OverLappingLogName | Out-String)"
 454                    Write-Warning -Message 'Change the name of your log or use Remove-EventLog to remove the log(s) above!'
 455                }
 456            }
 457        }
 458        Else { Write-Warning -Message 'No Source '$Source' or Log Name '$LogName' defined. Skipping event log logging...' }
 459
 460        ## Create script block for generating CMTrace.exe compatible log entry
 461        [scriptblock]$CMTraceLogString = {
 462            Param (
 463                [string]$lMessage,
 464                [string]$lSource,
 465                [int16]$lSeverity
 466            )
 467            "<![LOG[$lMessage]LOG]!>" + "<time=`"$LogTimePlusBias`" " + "date=`"$LogDate`" " + "component=`"$lSource`" " + "context=`"$([Security.Principal.WindowsIdentity]::GetCurrent().Name)`" " + "type=`"$lSeverity`" " + "thread=`"$PID`" " + "file=`"$Source`">"
 468        }
 469
 470        ## Create script block for writing log entry to the console
 471        [scriptblock]$WriteLogLineToHost = {
 472            Param (
 473                [string]$lTextLogLine,
 474                [int16]$lSeverity
 475            )
 476            If ($WriteHost) {
 477                #  Only output using color options if running in a host which supports colors.
 478                If ($Host.UI.RawUI.ForegroundColor) {
 479                    Switch ($lSeverity) {
 480                        3 { Write-Host -Object $lTextLogLine -ForegroundColor 'Red' -BackgroundColor 'Black' }
 481                        2 { Write-Host -Object $lTextLogLine -ForegroundColor 'Yellow' -BackgroundColor 'Black' }
 482                        1 { Write-Host -Object $lTextLogLine }
 483                    }
 484                }
 485                #  If executing "powershell.exe -File <filename>.ps1 > log.txt", then all the Write-Host calls are converted to Write-Output calls so that they are included in the text log.
 486                Else {
 487                    Write-Output -InputObject $lTextLogLine
 488                }
 489            }
 490        }
 491
 492        ## Create script block for writing log entry to the console as verbose or debug message
 493        [scriptblock]$WriteLogLineToHostAdvanced = {
 494            Param (
 495                [string]$lTextLogLine
 496            )
 497            #  Only output using color options if running in a host which supports colors.
 498            If ($Host.UI.RawUI.ForegroundColor) {
 499                If ($VerboseMessage) {
 500                    Write-Verbose -Message $lTextLogLine
 501                }
 502                Else {
 503                    Write-Debug -Message $lTextLogLine
 504                }
 505            }
 506            #  If executing "powershell.exe -File <filename>.ps1 > log.txt", then all the Write-Host calls are converted to Write-Output calls so that they are included in the text log.
 507            Else {
 508                Write-Output -InputObject $lTextLogLine
 509            }
 510        }
 511
 512        ## Create script block for event writing log entry
 513        [scriptblock]$WriteToEventLog = {
 514            If ($WriteEvent -and $SourceDefined -and $LogNameDefined) {
 515                $EventType = Switch ($Severity) {
 516                    3 { 'Error' }
 517                    2 { 'Warning' }
 518                    1 { 'Information' }
 519                }
 520
 521                If ($LogNameNotExists -and (-not $LogSourceNotExists)) {
 522                    Try {
 523                        #  Delete event source if the log does not exist
 524                        $null = [System.Diagnostics.EventLog]::DeleteEventSource($Source)
 525                        $LogSourceNotExists = $true
 526                    }
 527                    Catch {
 528                        [boolean]$ExitLoggingFunction = $true
 529                        #  If error deleting event source, write message to console
 530                        If (-not $ContinueOnError) {
 531                            Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the event log source [$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 532                        }
 533                    }
 534                }
 535                If ($LogNameNotExists -or $LogSourceNotExists) {
 536                    Try {
 537                        #  Create event log
 538                        $null = New-EventLog -LogName $LogName -Source $Source -ErrorAction 'Stop'
 539                    }
 540                    Catch {
 541                        [boolean]$ExitLoggingFunction = $true
 542                        #  If error creating event log, write message to console
 543                        If (-not $ContinueOnError) {
 544                            Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the event log [$LogName`:$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 545                        }
 546                    }
 547                }
 548                Try {
 549                    #  Write to event log
 550                    Write-EventLog -LogName $LogName -Source $Source -EventId $EventID -EntryType $EventType -Category '0' -Message $EventLogLine -ErrorAction 'Stop'
 551                }
 552                Catch {
 553                    [boolean]$ExitLoggingFunction = $true
 554                    #  If error creating directory, write message to console
 555                    If (-not $ContinueOnError) {
 556                        Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to write to event log [$LogName`:$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 557                    }
 558                }
 559            }
 560        }
 561
 562        ## Exit function if it is a debug message and logging debug messages is not enabled in the config XML file
 563        If (($DebugMessage -or $VerboseMessage) -and (-not $LogDebugMessage)) { [boolean]$ExitLoggingFunction = $true; Return }
 564        ## Exit function if logging to file is disabled and logging to console host is disabled
 565        If (($DisableLogging) -and (-not $WriteHost)) { [boolean]$ExitLoggingFunction = $true; Return }
 566        ## Exit Begin block if logging is disabled
 567        If ($DisableLogging) { Return }
 568
 569        ## Create the directory where the log file will be saved
 570        If (-not (Test-Path -LiteralPath $LogFileDirectory -PathType 'Container')) {
 571            Try {
 572                $null = New-Item -Path $LogFileDirectory -Type 'Directory' -Force -ErrorAction 'Stop' -WhatIf:$false
 573            }
 574            Catch {
 575                [boolean]$ExitLoggingFunction = $true
 576                #  If error creating directory, write message to console
 577                If (-not $ContinueOnError) {
 578                    Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the log directory [$LogFileDirectory]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 579                }
 580                Return
 581            }
 582        }
 583
 584        ## Assemble the fully qualified path to the log file
 585        [string]$LogFilePath = Join-Path -Path $LogFileDirectory -ChildPath $LogFileName
 586    }
 587    Process {
 588
 589        ForEach ($Msg in $Message) {
 590            ## If the message is not $null or empty, create the log entry for the different logging methods
 591            [string]$CMTraceMsg = ''
 592            [string]$ConsoleLogLine = ''
 593            [string]$EventLogLine = ''
 594            [string]$LegacyTextLogLine = ''
 595            If ($Msg) {
 596                #  Create the CMTrace log message
 597                If ($ScriptSectionDefined) { [string]$CMTraceMsg = "[$ScriptSection] :: $Msg" }
 598
 599                #  Create a Console and Legacy "text" log entry
 600                [string]$LegacyMsg = "[$LogDate $LogTime]"
 601                If ($ScriptSectionDefined) { [string]$LegacyMsg += " [$ScriptSection]" }
 602                If ($Source) {
 603                    [string]$EventLogLine = $Msg
 604                    [string]$ConsoleLogLine = "$LegacyMsg [$Source] :: $Msg"
 605                    Switch ($Severity) {
 606                        3 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Error] :: $Msg" }
 607                        2 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Warning] :: $Msg" }
 608                        1 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Info] :: $Msg" }
 609                    }
 610                }
 611                Else {
 612                    [string]$ConsoleLogLine = "$LegacyMsg :: $Msg"
 613                    [string]$EventLogLine = $Msg
 614                    Switch ($Severity) {
 615                        3 { [string]$LegacyTextLogLine = "$LegacyMsg [Error] :: $Msg" }
 616                        2 { [string]$LegacyTextLogLine = "$LegacyMsg [Warning] :: $Msg" }
 617                        1 { [string]$LegacyTextLogLine = "$LegacyMsg [Info] :: $Msg" }
 618                    }
 619                }
 620            }
 621
 622            ## Execute script block to write the log entry to the console as verbose or debug message
 623            & $WriteLogLineToHostAdvanced -lTextLogLine $ConsoleLogLine -lSeverity $Severity
 624
 625            ## Exit function if logging is disabled
 626            If ($ExitLoggingFunction) { Return }
 627
 628            ## Execute script block to create the CMTrace.exe compatible log entry
 629            [string]$CMTraceLogLine = & $CMTraceLogString -lMessage $CMTraceMsg -lSource $Source -lSeverity $lSeverity
 630
 631            ## Choose which log type to write to file
 632            If ($LogType -ieq 'CMTrace') {
 633                [string]$LogLine = $CMTraceLogLine
 634            }
 635            Else {
 636                [string]$LogLine = $LegacyTextLogLine
 637            }
 638
 639            ## Write the log entry to the log file and event log if logging is not currently disabled
 640            If ((-not $ExitLoggingFunction) -and (-not $DisableLogging)) {
 641                If ($WriteFile) {
 642                    ## Write to file log
 643                    Try {
 644                        $LogLine | Out-File -FilePath $LogFilePath -Append -NoClobber -Force -Encoding 'UTF8' -ErrorAction 'Stop' -WhatIf:$false
 645                    }
 646                    Catch {
 647                        If (-not $ContinueOnError) {
 648                            Write-Host -Object "[$LogDate $LogTime] [$ScriptSection] [${CmdletName}] :: Failed to write message [$Msg] to the log file [$LogFilePath]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 649                        }
 650                    }
 651                }
 652                If ($WriteEvent) {
 653                    ## Write to event log
 654                    Try {
 655                        & $WriteToEventLog -lMessage $ConsoleLogLine -lName $LogName -lSource $Source -lSeverity $Severity
 656                    }
 657                    Catch {
 658                        If (-not $ContinueOnError) {
 659                            Write-Host -Object "[$LogDate $LogTime] [$ScriptSection] [${CmdletName}] :: Failed to write message [$Msg] to the log file [$LogFilePath]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 660                        }
 661                    }
 662                }
 663            }
 664
 665            ## Execute script block to write the log entry to the console if $WriteHost is $true and $LogLogDebugMessage is not $true
 666            & $WriteLogLineToHost -lTextLogLine $ConsoleLogLine -lSeverity $Severity
 667        }
 668    }
 669    End {
 670        ## Archive log file if size is greater than $MaxLogFileSizeMB and $MaxLogFileSizeMB > 0
 671        Try {
 672            If ((-not $ExitLoggingFunction) -and (-not $DisableLogging)) {
 673                [IO.FileInfo]$LogFile = Get-ChildItem -LiteralPath $LogFilePath -ErrorAction 'Stop'
 674                [decimal]$LogFileSizeMB = $LogFile.Length / 1MB
 675                If (($LogFileSizeMB -gt $MaxLogFileSizeMB) -and ($MaxLogFileSizeMB -gt 0)) {
 676                    ## Change the file extension to "lo_"
 677                    [string]$ArchivedOutLogFile = [IO.Path]::ChangeExtension($LogFilePath, 'lo_')
 678                    [hashtable]$ArchiveLogParams = @{ ScriptSection = ${CmdletName}; Source = $Source; Severity = 2; LogFileDirectory = $LogFileDirectory; LogFileName = $LogFileName; LogType = $LogType; MaxLogFileSizeMB = 0; ContinueOnError = $ContinueOnError; PassThru = $false }
 679
 680                    ## Log message about archiving the log file
 681                    $ArchiveLogMessage = "Maximum log file size [$MaxLogFileSizeMB MB] reached. Rename log file to [$ArchivedOutLogFile]."
 682                    Write-Log -Message $ArchiveLogMessage @ArchiveLogParams
 683
 684                    ## Archive existing log file from <filename>.log to <filename>.lo_. Overwrites any existing <filename>.lo_ file. This is the same method SCCM uses for log files.
 685                    Move-Item -LiteralPath $LogFilePath -Destination $ArchivedOutLogFile -Force -ErrorAction 'Stop' -WhatIf:$false
 686
 687                    ## Start new log file and Log message about archiving the old log file
 688                    $NewLogMessage = "Previous log file was renamed to [$ArchivedOutLogFile] because maximum log file size of [$MaxLogFileSizeMB MB] was reached."
 689                    Write-Log -Message $NewLogMessage @ArchiveLogParams
 690                }
 691            }
 692        }
 693        Catch {
 694            ## If renaming of file fails, script will continue writing to log file even if size goes over the max file size
 695        }
 696        Finally {
 697            If ($PassThru) { Write-Output -InputObject $Message }
 698        }
 699    }
 700}
 701#endregion
 702
 703#region Function Show-Progress
 704Function Show-Progress {
 705<#
 706.SYNOPSIS
 707    Displays progress info.
 708.DESCRIPTION
 709    Displays progress info and maximizes code reuse by automatically calculating the progress steps.
 710.PARAMETER Actity
 711    Specifies the progress activity. Default: 'Cleaning Up Configuration Manager Client Cache, Please Wait...'.
 712.PARAMETER Status
 713    Specifies the progress status.
 714.PARAMETER CurrentOperation
 715    Specifies the current operation.
 716.PARAMETER Step
 717    Specifies the progress step. Default: $script:Step ++.
 718.PARAMETER Steps
 719    Specifies the progress steps. Default: $script:Steps ++.
 720.PARAMETER ID
 721    Specifies the progress bar id.
 722.PARAMETER Delay
 723    Specifies the progress delay in milliseconds. Default: 0.
 724.PARAMETER Loop
 725    Specifies if the call comes from a loop.
 726.EXAMPLE
 727    Show-Progress -Activity 'Cleaning Up Configuration Manager Client Cache, Please Wait...' -Status 'Cleaning WMI' -Step ($Step++) -Delay 200
 728.INPUTS
 729    None.
 730.OUTPUTS
 731    None.
 732.NOTES
 733    Created by Ioan Popovici.
 734    v2.0.0 - 2021-01-01
 735
 736    This is an private function should tipically not be called directly.
 737    Credit to Adam Bertram.
 738
 739    ## !! IMPORTANT !! ##
 740    #  You need to tokenize the scripts steps at the begining of the script in order for Show-Progress to work:
 741
 742    ## Get script path and name
 743    [string]$ScriptPath = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Definition)
 744    [string]$ScriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Definition)
 745    [string]$ScriptFullName = Join-Path -Path $ScriptPath -ChildPath $ScriptName
 746    #  Get progress steps
 747    $ProgressSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $PSItem.Type -eq 'Command' -and $PSItem.Content -eq 'Show-Progress' }).Count)
 748    $ForEachSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $PSItem.Type -eq 'Keyword' -and $PSItem.Content -eq 'ForEach' }).Count)
 749    #  Set progress steps
 750    $script:Steps = $ProgressSteps - $ForEachSteps
 751    $script:Step = 0
 752.LINK
 753    https://adamtheautomator.com/building-progress-bar-powershell-scripts/
 754.LINK
 755    https://MEM.Zone
 756.LINK
 757    https://MEM.Zone/GIT
 758.LINK
 759    https://MEM.Zone/ISSUES
 760.COMPONENT
 761    Powershell
 762.FUNCTIONALITY
 763    Show Progress
 764#>
 765    [CmdletBinding()]
 766    Param (
 767        [Parameter(Mandatory=$false,Position=0)]
 768        [ValidateNotNullorEmpty()]
 769        [Alias('act')]
 770        [string]$Activity = 'Cleaning Up Configuration Manager Client Cache, Please Wait...',
 771        [Parameter(Mandatory=$true,Position=1)]
 772        [ValidateNotNullorEmpty()]
 773        [Alias('sta')]
 774        [string]$Status,
 775        [Parameter(Mandatory=$false,Position=2)]
 776        [ValidateNotNullorEmpty()]
 777        [Alias('cro')]
 778        [string]$CurrentOperation,
 779        [Parameter(Mandatory=$false,Position=3)]
 780        [ValidateNotNullorEmpty()]
 781        [Alias('pid')]
 782        [int]$ID = 0,
 783        [Parameter(Mandatory=$false,Position=4)]
 784        [ValidateNotNullorEmpty()]
 785        [Alias('ste')]
 786        [int]$Step = $script:Step ++,
 787        [Parameter(Mandatory=$false,Position=5)]
 788        [ValidateNotNullorEmpty()]
 789        [Alias('sts')]
 790        [int]$Steps = $script:Steps,
 791        [Parameter(Mandatory=$false,Position=6)]
 792        [ValidateNotNullorEmpty()]
 793        [Alias('del')]
 794        [string]$Delay = 0,
 795        [Parameter(Mandatory=$false,Position=7)]
 796        [ValidateNotNullorEmpty()]
 797        [Alias('lp')]
 798        [switch]$Loop
 799    )
 800    Begin {
 801
 802
 803    }
 804    Process {
 805        Try {
 806            If ($Step -eq 0) {
 807                $Step ++
 808                $script:Step ++
 809                $Steps ++
 810                $script:Steps ++
 811            }
 812            If ($Steps -eq 0) {
 813                $Steps ++
 814                $script:Steps ++
 815            }
 816
 817            [boolean]$Completed = $false
 818            [int]$PercentComplete = $($($Step / $Steps) * 100)
 819
 820            If ($PercentComplete -ge 100)  {
 821                $PercentComplete = 100
 822                $Completed = $true
 823                $script:CurrentStep ++
 824                $script:Step = $script:CurrentStep
 825                $script:Steps = $script:DefaultSteps
 826            }
 827
 828            ## Debug information
 829            Write-Verbose -Message "Percent Step: $Step"
 830            Write-Verbose -Message "Percent Steps: $Steps"
 831            Write-Verbose -Message "Percent Complete: $PercentComplete"
 832            Write-Verbose -Message "Completed: $Completed"
 833
 834            ##  Show progress
 835            Write-Progress -Activity $Activity -Status $Status -CurrentOperation $CurrentOperation -ID $ID -PercentComplete $PercentComplete -Completed:$Completed
 836            If ($Delay -ne 0) { Start-Sleep -Milliseconds $Delay }
 837        }
 838        Catch {
 839            Throw (New-Object System.Exception("Could not Show progress status [$Status]! $($PSItem.Exception.Message)", $PSItem.Exception))
 840        }
 841    }
 842}
 843#endregion
 844
 845#region Function Get-MSGraphAccessToken
 846Function Get-MSGraphAccessToken {
 847<#
 848.SYNOPSIS
 849    Gets a Microsoft Graph API access token.
 850.DESCRIPTION
 851    Gets a Microsoft Graph API access token,by using an application registered in EntraID.
 852.PARAMETER TenantID
 853    Specifies the tenant ID.
 854.PARAMETER ClientID
 855    Specify the Application Client ID to use.
 856.PARAMETER Secret
 857    Specify the Application Client Secret to use.
 858.PARAMETER Scope
 859    Specify the scope to use.
 860    Default is: 'https://graph.microsoft.com/.default'.
 861.PARAMETER GrantType
 862    Specify the grant type to use.
 863    Default is: 'client_credentials'.
 864.EXAMPLE
 865    Get-MSGraphAccessToken -TenantID $TenantID -ClientID $ClientID -Secret $Secret
 866.INPUTS
 867    None.
 868.OUTPUTS
 869    System.String
 870.NOTES
 871    Created by Ioan Popovici
 872    v1.0.0 - 2024-01-11
 873
 874    This is an private function should tipically not be called directly.
 875.LINK
 876    https://MEM.Zone
 877.LINK
 878    https://MEM.Zone/GIT
 879.LINK
 880    https://MEM.Zone/ISSUES
 881.COMPONENT
 882    MSGraph
 883.FUNCTIONALITY
 884    Invokes the Microsoft Graph API.
 885#>
 886    [CmdletBinding()]
 887    Param (
 888        [Parameter(Mandatory = $true, HelpMessage = 'Specify the tenant ID.', Position = 0)]
 889        [ValidateNotNullorEmpty()]
 890        [Alias('Tenant')]
 891        [string]$TenantID,
 892        [Parameter(Mandatory = $true, HelpMessage = 'Specify the Application (Client) ID to use.', Position = 1)]
 893        [ValidateNotNullorEmpty()]
 894        [Alias('ApplicationClientID')]
 895        [string]$ClientID,
 896        [Parameter(Mandatory = $true, HelpMessage = 'Specify the Application (Client) Secret to use.', Position = 2)]
 897        [ValidateNotNullorEmpty()]
 898        [Alias('ApplicationClientSecret')]
 899        [string]$ClientSecret,
 900        [Parameter(Mandatory = $false, HelpMessage = 'Specify the scope to use.', Position = 3)]
 901        [ValidateNotNullorEmpty()]
 902        [Alias('GrantScope')]
 903        [string]$Scope = 'https://graph.microsoft.com/.default',
 904        [Parameter(Mandatory = $false, HelpMessage = 'Specify the grant type to use.', Position = 4)]
 905        [ValidateNotNullorEmpty()]
 906        [Alias('AccessType')]
 907        [string]$GrantType = 'client_credentials'
 908    )
 909
 910    Begin {
 911
 912        ## Get the name of this function and write verbose header
 913        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
 914
 915        ## Assemble the token body for the API call. You can store the secrets in Azure Key Vault and retrieve them from there.
 916        [hashtable]$Body = @{
 917            client_id     = $ClientID
 918            scope         = $Scope
 919            client_secret = $ClientSecret
 920            grant_type    = $GrantType
 921        }
 922
 923        ## Assembly the URI for the API call
 924        [string]$Uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token"
 925
 926        ## Write Debug information
 927        Write-Debug -Message "Uri: $Uri"
 928        Write-Debug -Message "Body: $($Body | Out-String)"
 929    }
 930    Process {
 931        Try {
 932
 933            ## Get the access token
 934            $Response = Invoke-RestMethod -Method 'POST' -Uri $Uri -ContentType 'application/x-www-form-urlencoded' -Body $Body -UseBasicParsing
 935            $Output = $Response.access_token
 936        }
 937        Catch {
 938            [string]$Message = "Error getting MSGraph API Acces Token for TenantID '{0}' with ClientID '{1}'.`n{2}" -f $TenantID, $ClientID, $(Resolve-Error)
 939            Write-Log -Message $Message -Severity 3 -ScriptSection ${CmdletName} -EventID 666
 940            Write-Error -Message $Message
 941        }
 942        Finally {
 943            Write-Output -InputObject $Output
 944        }
 945    }
 946    End {
 947    }
 948}
 949#endregion
 950
 951#region Function Invoke-MSGraphAPI
 952Function Invoke-MSGraphAPI {
 953<#
 954.SYNOPSIS
 955    Invokes the Microsoft Graph API.
 956.DESCRIPTION
 957    Invokes the Microsoft Graph API with paging support.
 958.PARAMETER Method
 959    Specify the method to use.
 960    Available options are 'GET', 'POST', 'PATCH', 'PUT' and 'DELETE'.
 961    Default is: 'GET'.
 962.PARAMETER Token
 963    Specify the access token to use.
 964.PARAMETER Version
 965    Specify the version of the Microsoft Graph API to use.
 966    Available options are 'Beta' and 'v1.0'.
 967    Default is: 'Beta'.
 968.PARAMETER Resource
 969    Specify the resource to query.
 970    Default is: 'deviceManagement/managedDevices'.
 971.PARAMETER Parameter
 972    Specify the parameter to use. Make sure to use the correct syntax and escape special characters with a backtick.
 973    Default is: $null.
 974.PARAMETER Body
 975    Specify the request body to use.
 976    Default is: $null.
 977.PARAMETER ContentType
 978    Specify the content type to use.
 979    Default is: 'application/json'.
 980.EXAMPLE
 981    Invoke-MSGraphAPI -Method 'GET' -Token $Token -Version 'Beta' -Resource 'deviceManagement/managedDevices' -Parameter "filter=operatingSystem like 'Windows' and deviceName like 'MEM-Zone-PC'"
 982.EXAMPLE
 983    Invoke-MSGraphAPI -Token $Token -Resource 'users'
 984.INPUTS
 985    None.
 986.OUTPUTS
 987    System.Object
 988.NOTES
 989    Created by Ioan Popovici
 990    v1.0.0 - 2024-01-11
 991
 992    This is an private function should tipically not be called directly.
 993.LINK
 994    https://MEM.Zone
 995.LINK
 996    https://MEM.Zone/GIT
 997.LINK
 998    https://MEM.Zone/ISSUES
 999.COMPONENT
1000    MSGraph
1001.FUNCTIONALITY
1002    Invokes the Microsoft Graph API.
1003#>
1004    [CmdletBinding()]
1005    Param (
1006        [Parameter(Mandatory = $false, HelpMessage = 'Specify the method to use.', Position = 0)]
1007        [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')]
1008        [Alias('HTTPMethod')]
1009        [string]$Method = 'GET',
1010        [Parameter(Mandatory = $true, HelpMessage = 'Specify the access token to use.', Position = 1)]
1011        [ValidateNotNullorEmpty()]
1012        [Alias('AccessToken')]
1013        [string]$Token,
1014        [Parameter(Mandatory = $false, HelpMessage = 'Specify the version of the Microsoft Graph API to use.', Position = 2)]
1015        [ValidateSet('Beta', 'v1.0')]
1016        [Alias('GraphVersion')]
1017        [string]$Version = 'Beta',
1018        [Parameter(Mandatory = $true, HelpMessage = 'Specify the resource to query.', Position = 3)]
1019        [ValidateNotNullorEmpty()]
1020        [Alias('APIResource')]
1021        [string]$Resource,
1022        [Parameter(Mandatory = $false, HelpMessage = 'Specify the parameters to use.', Position = 4)]
1023        [ValidateNotNullorEmpty()]
1024        [Alias('QueryParameter')]
1025        [string]$Parameter,
1026        [Parameter(Mandatory = $false, HelpMessage = 'Specify the request body to use.', Position = 5)]
1027        [ValidateNotNullorEmpty()]
1028        [Alias('RequestBody')]
1029        [string]$Body,
1030        [Parameter(Mandatory = $false, HelpMessage = 'Specify the content type to use.', Position = 6)]
1031        [ValidateNotNullorEmpty()]
1032        [Alias('Type')]
1033        [string]$ContentType = 'application/json'
1034    )
1035
1036    Begin {
1037
1038        ## Get the name of this function and write verbose header
1039        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
1040
1041        ## Assemble the URI for the API call
1042        [string]$Uri = "https://graph.microsoft.com/$Version/$Resource"
1043        If (-not [string]::IsNullOrWhiteSpace($Parameter)) { $Uri += "`?`$$Parameter" }
1044
1045        ## Assembly parameters for the API call
1046        [hashtable]$Parameters = @{
1047            'Uri'         = $Uri
1048            'Method'      = $Method
1049            'Headers'     = @{
1050                'Content-Type'  = 'application\json'
1051                'Authorization' = "Bearer $Token"
1052            }
1053            'ContentType' = $ContentType
1054        }
1055        If (-not [string]::IsNullOrWhiteSpace($Body)) { $Parameters.Add('Body', $Body) }
1056
1057        ## Write Debug information
1058        Write-Debug -Message "Uri: $Uri"
1059    }
1060    Process {
1061        Try {
1062
1063            ## Invoke the MSGraph API
1064            $Output = Invoke-RestMethod @Parameters
1065
1066            ## If there are more than 1000 rows, use paging. Only for GET method.
1067            If ($Output.'@odata.nextLink') {
1068                $Output += Do {
1069                    $Parameters.Uri = $OutputPage.'@odata.nextLink'
1070                    $OutputPage = Invoke-RestMethod @Parameters
1071                    $OutputPage
1072                }
1073                Until ([string]::IsNullOrEmpty($OutputPage.'@odata.nextLink'))
1074            }
1075            Write-Verbose -Message "Got '$($Output.Count)' Output pages."
1076        }
1077        Catch {
1078            [string]$Message = "Error invoking MSGraph API version '{0}' for resource '{1}' using '{2}' method.`n{3}" -f $Version, $Resource, $Method, $(Resolve-Error)
1079            Write-Log -Message $Message -Severity 3 -ScriptSection ${CmdletName} -EventID 666
1080            Write-Error -Message $Message
1081        }
1082        Finally {
1083            $Output = $Output.value
1084            Write-Output -InputObject $Output
1085        }
1086    }
1087    End {
1088    }
1089}
1090#endregion
1091
1092#endregion
1093##*=============================================
1094##* END FUNCTION LISTINGS
1095##*=============================================
1096
1097##*=============================================
1098##* SCRIPT BODY
1099##*=============================================
1100#region ScriptBody
1101
1102Try {
1103
1104    ## Set the script section
1105    $script:ScriptSection = 'Main'
1106
1107    ## Write Start verbose message
1108    Write-Log -Message 'Start' -VerboseMessage
1109
1110    ## Get API Token
1111    $Token = Get-MSGraphAccessToken -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret -ErrorAction 'Stop'
1112
1113    ## Get the device information
1114    Write-Verbose -Message "Getting device information, this might take a while..." -Verbose
1115
1116    #  Assemble the Parameter filter value depending if the DeviceOS and DeviceName parameters are specified
1117    $Parameter = If ($DeviceOS -ne 'All') { "filter=startswith(operatingSystem, '$DeviceOS')" }
1118    $Parameter += If ($DeviceName -ne 'All') {
1119        If ($DeviceOS -eq 'All') { "filter=deviceName eq '$DeviceName'" } Else { " and deviceName eq '$DeviceName'" }
1120    }
1121
1122    #  Set the parameters for the API call and add the Parameter parameter as a filter if it is not empty
1123    $Parameters = @{
1124        Token = $Token
1125        Resource = 'deviceManagement/managedDevices'
1126    }
1127    If (-not [string]::IsNullOrWhiteSpace($Parameter)) { $Parameters.Add('Parameter', $Parameter) }
1128
1129    #  Get the device information from the MSGraph API
1130    $Devices = Invoke-MSGraphAPI @Parameters -ErrorAction 'Stop'
1131    Write-Verbose -Message "Retrieved $($Devices.Count) devices."
1132
1133    ## Process devices
1134    ForEach ($Device in $Devices) {
1135
1136        ## Set variables
1137        [int]$RenamedCounter = 0
1138        [string]$Output = ''
1139        [string]$SerialNumber = $Device.serialNumber
1140        [string]$UserPrincipalName = $Device.userPrincipalName
1141        [string]$DeviceName = $Device.deviceName
1142        [string]$DeviceID = $Device.id
1143        [string]$OperatingSystem = $Device.operatingSystem
1144        [int]$DeviceOwnerType = $Device.managedDeviceOwnerType
1145        [boolean]$isSupervised = $Device.isSupervised
1146        #  Initialize the prefix variable with the script parameter value
1147        [string]$Prefix = $PSBoundParameters['Prefix']
1148        #  Convert to CAPS, shorten to 6 characters, convert to upper case and clean Prefix by removing any non-alphanumeric characters
1149        If (-not [string]::IsNullOrWhiteSpace($Prefix)) { $Prefix = $($Prefix.Substring(0, [System.Math]::Min(6, $Prefix.Length))).ToUpper() -replace ('[\W | /_]', '') }
1150
1151        ## Show progress bar
1152        Show-Progress -Status "Processing Devices for Rename --> [$DeviceName]" -Steps $Devices.Count
1153
1154        ## Check for supported device operating system and corporate owned device
1155        If ($OperatingSystem -notin $SupportedOperatingSystems -or $DeviceOwnerType -ne 'company') {
1156            [string]$Message = "Device '$DeviceName' with operating system '$OperatingSystem' and ownership type '$DeviceOwnerType' is not supported. Skipping..."
1157            Write-Warning -Message $Message -Verbose
1158            Write-Log -Message $Message -Severity 3 -EventID 666
1159            #  Skip to next device in the loop
1160            Continue
1161        }
1162        ## Check for supervised iOS/iPadOS device
1163        If ($OperatingSystem -eq 'iOS/iPadOS' -and -not $isSupervised) {
1164            [string]$Message = "Device '$DeviceName' with operating system '$OperatingSystem' is not supervised. Skipping..."
1165            Write-Warning -Message $Message -Verbose
1166            Write-Log -Message $Message -Severity 3 -EventID 666
1167            #  Skip to next device in the loop
1168            Continue
1169        }
1170
1171        ## Get device assigned user attribute information and set the Prefix if specified
1172        If ($PSCmdlet.ParameterSetName -eq 'UserAttribute') {
1173            Try {
1174                $UserInfo = Invoke-MSGraphAPI -Token $Token -Resource 'users' -Parameter "filter=userPrincipalName eq '$UserPrincipalName'" -ErrorAction 'Stop'
1175                    #  Get the user attribute
1176                    [string]$UserAttribute = $UserInfo.$PrefixFromUserAttribute
1177                    #  Convert to CAPS, shorten to 6 characters, convert to upper case and clean UserAttribute by removing any non-alphanumeric characters
1178                    $UserAttribute = $($UserAttribute.Substring(0, [System.Math]::Min(6, $UserAttribute.Length))).ToUpper() -replace ('[\W | /_]', '')
1179                    #  Set the prefix if the user attribute is not empty
1180                    If (-not [string]::IsNullOrEmpty($UserAttribute)) { $Prefix = $UserAttribute } Else { Throw 'User attribute is empty!' }
1181            }
1182            Catch {
1183                [string]$Message = "Error getting user information for device '{0}' with owner '{1}', check if the device has a user assigned. Skipping...`n{2}" -f $DeviceName, $UserPrincipalName, $(Resolve-Error)
1184                Write-Warning -Message $Message -Verbose
1185                Write-Log -Message $Message -Severity 3 -EventID 666
1186                #  Skip to next device in the loop
1187                Continue
1188            }
1189        }
1190
1191        ## Check if the device has a serialnumber and that it's valid
1192        [boolean]$IsValidSerialNumber = If (-not [string]::IsNullOrEmpty($SerialNumber) -and ($SerialNumber -ne 'SystemSerialNumber')) { $true } Else { $false }
1193
1194        ## Clean serialnumber by removing any non-alphanumeric characters
1195        If (-not $IsValidSerialNumber) {
1196            [string]$Message = "Device '$DeviceName' does not have a valid serialnumber. Skipping..."
1197            Write-Warning -Message $Message -Verbose
1198            Write-Log -Message $Message -Severity 3 -EventID 666
1199            Continue
1200        }
1201
1202        ## Remove any non-alphanumeric characters from the serial number and convert to upper case
1203        $SerialNumber = ($SerialNumber -replace ('[\W | /_]', '')).ToUpper()
1204
1205        ## Trim serial number to 15 characters for windows devices
1206        If ($OperatingSystem -eq 'windows') {
1207            $NewDeviceName = -join ($Prefix,'-',$SerialNumber)
1208            $MaxSerialNumberLength = 15 - $Prefix.Length -1
1209            $SerialNumber = $SerialNumber.subString(0, [System.Math]::Min($MaxSerialNumberLength , $NewDeviceName.Length))
1210        }
1211
1212        ## Assemble the new device name
1213        $NewDeviceName = -join ($Prefix,'-',$SerialNumber)
1214
1215        ## Rename device if it has not been alreadu renamed
1216        Try {
1217            If ($DeviceName -ne $NewDeviceName) {
1218                $Parameters = @{
1219                    Method = 'POST'
1220                    Token = $Token
1221                    Resource = "deviceManagement/managedDevices('$DeviceID')/setDeviceName"
1222                    Body = @{ deviceName = $NewDeviceName } | ConvertTo-Json
1223                    ContentType = 'application/json'
1224                    ErrorAction = 'Stop'
1225                }
1226                ##  Rename device with ShouldProcess support
1227                [boolean]$ShouldProcess = $PSCmdlet.ShouldProcess("$DeviceName", "Rename to $NewDeviceName")
1228                If ($ShouldProcess) { Invoke-MSGraphAPI @Parameters }
1229                #  If operation is successful, output the result
1230                $Output = "Device '{0}' renamed to '{1}'." -f $DeviceName, $NewDeviceName
1231                $RenamedCounter++
1232            }
1233            Else {
1234                $Output = "Device '{0}' is already named '{1}'." -f $DeviceName, $NewDeviceName
1235            }
1236        }
1237        Catch {
1238            Write-Log -Message "Error renaming device '$DeviceName' to '$NewDeviceName'.`n$(Resolve-Error)" -Severity 3 -EventID 666
1239            Continue
1240        }
1241        Finally {
1242            Write-Log -Message $Output
1243        }
1244    }
1245}
1246Catch {
1247    Write-Log -Message "Error renaming device.`n$(Resolve-Error)" -Severity 3 -EventID 666
1248}
1249Finally {
1250    Write-Log -Message "Succesully renamed '$RenamedCounter' devices." -EventID 2
1251    Write-Log -Message 'Stop' -VerboseMessage
1252}
1253
1254#endregion
1255##*=============================================
1256##* END SCRIPT BODY
1257##*=============================================

SHARE

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 · 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