AutopilotGroupTagger.ps1

<#PSScriptInfo
 
.VERSION 0.7.0
.GUID 63c8809e-5c8a-4ddc-82a4-29706992802f
.AUTHOR Nick Benton
.COMPANYNAME
.COPYRIGHT GPL
.TAGS Graph Intune Windows Autopilot GroupTags
.LICENSEURI https://github.com/ennnbeee/AutopilotGroupTagger/blob/main/LICENSE
.PROJECTURI https://github.com/ennnbeee/AutopilotGroupTagger
.ICONURI https://raw.githubusercontent.com/ennnbeee/AutopilotGroupTagger/refs/heads/main/img/agt-icon.png
.EXTERNALMODULEDEPENDENCIES Microsoft.Graph.Authentication
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
v0.7.0 - Updated to support re-running of the script and other bug fixes
v0.6.0 - Supports unblocking of Autopilot devices
v0.5.0 - Now supports PowerShell 7 on macOS, removal of Group Tags, and Dynamic Group creation
v0.4.5 - Function rework to support PowerShell gallery requirements
v0.4.4 - Added 'WhatIf' mode, and updated user experience of output of the progress of Group Tag updates
v0.4.3 - Improvements to user interface and error handling
v0.4.2 - Bug fixes and improvements
v0.4.1 - Updated authentication and module detection
v0.4.0 - Configured to run on PowerShell 5
v0.3.0 - Updated logic around Autopilot device selection
v0.2.0 - Included functionality to update group tags based on Purchase order
v0.1.0 - Initial release
 
.PRIVATEDATA
#>


<#
.SYNOPSIS
Autopilot GroupTagger - Update Autopilot Device Group Tags in bulk.
 
.DESCRIPTION
The Autopilot GroupTagger script is designed to allow for bulk updating of Autopilot device group tags in Microsoft Intune.
The script will connect to the Microsoft Graph API and retrieve all Autopilot devices, then allow for bulk updating of group tags based on various criteria.
 
.PARAMETER whatIf
Switch to enable WhatIf mode to simulate changes.
 
.PARAMETER createGroups
Switch to enable the creation of dynamic groups based on Group Tags.
 
.PARAMETER groupPrefix
Provide the prefix to be used for the creation of dynamic groups. Default is 'AGT-Autopilot-'.
 
.PARAMETER tenantId
Provide the Id of the Entra ID tenant to connect to.
 
.PARAMETER appId
Provide the Id of the Entra App registration to be used for authentication.
 
.PARAMETER appSecret
Provide the App secret to allow for authentication to graph
 
.EXAMPLE
Interactive Authentication
.\AutopilotGroupTagger.ps1
 
.EXAMPLE
Pass through Authentication
.\AutopilotGroupTagger.ps1 -tenantId '437e8ffb-3030-469a-99da-e5b527908099'
 
.EXAMPLE
App Authentication
.\AutopilotGroupTagger.ps1 -tenantId '437e8ffb-3030-469a-99da-e5b527908099' -appId '799ebcfa-ca81-4e72-baaf-a35126464d67' -appSecret 'g708Q~uof4xo9dU_1EjGQIuUr0UyBHNZmY2mcdy6'
 
#>


[CmdletBinding(DefaultParameterSetName = 'Default')]

param(

    [Parameter(Mandatory = $false, HelpMessage = 'Switch to enable the creation of dynamic groups based on Group Tags')]
    [switch]$createGroups,

    [Parameter(Mandatory = $false, HelpMessage = 'Provide the prefix to be used for the creation of dynamic groups')]
    [String]$groupPrefix = 'AGT-Autopilot-',

    [Parameter(Mandatory = $false, HelpMessage = 'Provide the Id of the Entra ID tenant to connect to')]
    [ValidateLength(36, 36)]
    [String]$tenantId,

    [Parameter(Mandatory = $false, ParameterSetName = 'appAuth', HelpMessage = 'Provide the Id of the Entra App registration to be used for authentication')]
    [ValidateLength(36, 36)]
    [String]$appId,

    [Parameter(Mandatory = $true, ParameterSetName = 'appAuth', HelpMessage = 'Provide the App secret to allow for authentication to graph')]
    [ValidateNotNullOrEmpty()]
    [String]$appSecret,

    [Parameter(Mandatory = $false, HelpMessage = 'WhatIf mode to simulate changes')]
    [switch]$whatIf

)

#region Functions
function Test-JSONData {

    <#
    .SYNOPSIS
    Validates JSON data format.
 
    .DESCRIPTION
    The Test-JSONData function checks if the provided JSON string is in a valid format.
 
    .PARAMETER JSON
    Specifies the JSON string to validate.
 
    .EXAMPLE
    Test-JSONData -JSON '{"key": "value"}'
    #>


    param (
        $JSON
    )

    try {
        $TestJSON = ConvertFrom-Json $JSON -ErrorAction Stop
        $TestJSON | Out-Null
        $validJson = $true
    }
    catch {
        $validJson = $false
        Write-Error $_.Exception.Message
        break
    }
    if (!$validJson) {
        Write-Error $_.Exception.Message
        break
    }
}
function Connect-ToGraph {
    <#
    .SYNOPSIS
    Authenticates to the Graph API via the Microsoft.Graph.Authentication module.
 
    .DESCRIPTION
    The Connect-ToGraph cmdlet is a wrapper cmdlet that helps authenticate to the Intune Graph API using the Microsoft.Graph.Authentication module. It leverages an Azure AD app ID and app secret for authentication or user-based auth.
 
    .PARAMETER tenantId
    Specifies the tenantId from Entra ID to which to authenticate.
 
    .PARAMETER appId
    Specifies the Azure AD app ID (GUID) for the application that will be used to authenticate.
 
    .PARAMETER appSecret
    Specifies the Azure AD app secret corresponding to the app ID that will be used to authenticate.
 
    .PARAMETER scopes
    Specifies the user scopes for interactive authentication.
 
    .EXAMPLE
    Connect-ToGraph -tenantId $tenantId -appId $appId -appSecret $appSecret
 
    #>


    [cmdletbinding()]
    param
    (
        [Parameter(Mandatory = $false)] [string]$tenantId,
        [Parameter(Mandatory = $false)] [string]$appId,
        [Parameter(Mandatory = $false)] [string]$appSecret,
        [Parameter(Mandatory = $false)] [string[]]$scopes
    )

    process {
        Import-Module Microsoft.Graph.Authentication
        $version = (Get-Module microsoft.graph.authentication | Select-Object -ExpandProperty Version).major

        if ($AppId -ne '') {
            $body = @{
                grant_type    = 'client_credentials';
                client_id     = $appId;
                client_secret = $appSecret;
                scope         = 'https://graph.microsoft.com/.default';
            }

            $response = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Body $body
            $accessToken = $response.access_token

            if ($version -eq 2) {
                Write-Host 'Version 2 module detected'
                $accessTokenFinal = ConvertTo-SecureString -String $accessToken -AsPlainText -Force
            }
            else {
                Write-Host 'Version 1 Module Detected'
                Select-MgProfile -Name Beta
                $accessTokenFinal = $accessToken
            }
            $graph = Connect-MgGraph -AccessToken $accessTokenFinal
            Write-Host "Connected to Intune tenant $TenantId using app-based authentication (Azure AD authentication not supported)"
        }
        else {
            if ($version -eq 2) {
                Write-Host 'Version 2 module detected'
            }
            else {
                Write-Host 'Version 1 Module Detected'
                Select-MgProfile -Name Beta
            }
            $graph = Connect-MgGraph -Scopes $scopes -TenantId $tenantId
            Write-Host "Connected to Intune tenant $($graph.TenantId)"
        }
    }
}
function Get-AutopilotDevice() {

    <#
    .SYNOPSIS
    This function is used to get autopilot devices via the Graph API REST interface
 
    .DESCRIPTION
    The function connects to the Graph API Interface and gets any autopilot devices
 
    .EXAMPLE
    Get-AutopilotDevice
    Returns any autopilot devices
 
    #>


    $graphApiVersion = 'Beta'
    $Resource = 'deviceManagement/windowsAutopilotDeviceIdentities'

    try {

        $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
        $graphResults = Invoke-MgGraphRequest -Uri $uri -Method Get -OutputType PSObject

        $results = @()
        $results += $graphResults.value

        $pages = $graphResults.'@odata.nextLink'
        while ($null -ne $pages) {

            $additional = Invoke-MgGraphRequest -Uri $pages -Method Get -OutputType PSObject

            if ($pages) {
                $pages = $additional.'@odata.nextLink'
            }
            $results += $additional.value
        }
        $results
    }
    catch {
        Write-Error $_.Exception.Message
        break
    }
}
function Set-AutopilotDevice() {

    <#
    .SYNOPSIS
    This function is used to set autopilot devices properties via the Graph API REST interface
 
    .DESCRIPTION
    The function connects to the Graph API Interface and sets autopilot device properties
 
    .PARAMETER Id
    Specifies the Id of the autopilot device to update.
 
    .PARAMETER groupTag
    Specifies the group tag to assign to the autopilot device.
 
    .PARAMETER unblock
    Specifies whether to unblock the autopilot device.
 
    .EXAMPLE
    Set-AutopilotDevice -Id <Id> -groupTag <groupTag> -unblock $true
    Updates the specified autopilot device with the provided group tag and unblock status.
 
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'high')]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Id,

        [Parameter(Mandatory = $false)]
        [string]$groupTag,

        [Parameter(Mandatory = $false)]
        [bool]$unblock
    )

    process {
        $graphApiVersion = 'Beta'
        if ($groupTag) {
            $Resource = "deviceManagement/windowsAutopilotDeviceIdentities/$Id/updateDeviceProperties"
        }
        elseif ($unblock -eq $true) {
            $Resource = "deviceManagement/windowsAutopilotDeviceIdentities/$Id//allowNextEnrollment"
        }

        if ($PSCmdlet.ShouldProcess('Autopilot Device', 'Update')) {
            try {
                $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
                if ($groupTag) {
                    $Autopilot = New-Object -TypeName psobject
                    $Autopilot | Add-Member -MemberType NoteProperty -Name 'groupTag' -Value $groupTag

                    $JSON = $Autopilot | ConvertTo-Json -Depth 3

                    Invoke-MgGraphRequest -Uri $uri -Method Post -Body $JSON -ContentType 'application/json'
                }
                else {
                    Invoke-MgGraphRequest -Uri $uri -Method Post
                }
            }
            catch {
                Write-Error $_.Exception.Message
                break
            }
        }
        elseif ($WhatIfPreference.IsPresent) {
            Write-Output "Autopilot Device $Id would have been updated"
        }
        else {
            Write-Output "Autopilot Device $Id was not updated"
        }
    }

}
function Get-EntraIDObject() {

    <#
    .SYNOPSIS
    This function is used to get Entra ID objects
 
    .DESCRIPTION
    The function connects to the Graph API Interface and gets any Entra ID objects
 
    .PARAMETER user
    Specifies whether to retrieve user objects.
 
    .PARAMETER device
    Specifies whether to retrieve device objects.
 
    .PARAMETER os
    Specifies the operating system of the device to retrieve.
 
    .EXAMPLE
    Get-EntraIDObject -device -os Windows
    Returns any Windows Entra ID objects
 
    #>


    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param
    (

        [parameter(Mandatory = $false)]
        [switch]$user,

        [parameter(Mandatory = $false, ParameterSetName = 'devices')]
        [switch]$device,

        [parameter(Mandatory = $true, ParameterSetName = 'devices')]
        [ValidateSet('Windows', 'iOS', 'Android', 'macOS')]
        [string]$os

    )

    $graphApiVersion = 'beta'
    if ($user) {
        $Resource = "users?`$filter=userType eq 'member' and accountEnabled eq true"
    }
    elseif ($device) {
        switch ($os) {
            'iOS' {
                $Resource = "devices?`$filter=operatingSystem eq 'iOS'"
            }
            'Android' {
                $Resource = "devices?`$filter=operatingSystem eq 'Android'"
            }
            'macOS' {
                $Resource = "devices?`$filter=operatingSystem eq 'macOS'"
            }
            'Windows' {
                $Resource = "devices?`$filter=operatingSystem eq 'Windows'"
            }
        }
    }
    try {

        $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource"
        $graphResults = Invoke-MgGraphRequest -Uri $uri -Method Get -OutputType PSObject

        $results = @()
        $results += $graphResults.value

        $pages = $graphResults.'@odata.nextLink'
        while ($null -ne $pages) {

            $additional = Invoke-MgGraphRequest -Uri $pages -Method Get -OutputType PSObject

            if ($pages) {
                $pages = $additional.'@odata.nextLink'
            }
            $results += $additional.value
        }
        $results
    }
    catch {
        Write-Error $_.Exception.Message
        break
    }
}
function Get-ManagedDevice() {

    <#
    .SYNOPSIS
    This function is used to get Intune device objects
 
    .DESCRIPTION
    The function connects to the Graph API Interface and gets any Intune device objects
 
    .PARAMETER os
    Specifies the operating system of the device to retrieve.
 
    .EXAMPLE
    Get-ManagedDevice -os Windows
    Returns any Windows Intune device objects
 
    #>


    [cmdletbinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateSet('Windows', 'iOS', 'Android', 'macOS')]
        [string]$os
    )

    $graphApiVersion = 'beta'
    switch ($os) {
        'iOS' {
            $Resource = "deviceManagement/managedDevices?`$filter=operatingSystem eq 'iOS'"
        }
        'Android' {
            $Resource = "deviceManagement/managedDevices?`$filter=operatingSystem eq 'Android'"
        }
        'macOS' {
            $Resource = "deviceManagement/managedDevices?`$filter=operatingSystem eq 'macOS'"
        }
        'Windows' {
            $Resource = "deviceManagement/managedDevices?`$filter=operatingSystem eq 'Windows'"
        }
    }
    try {

        $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource"
        $graphResults = Invoke-MgGraphRequest -Uri $uri -Method Get -OutputType PSObject

        $results = @()
        $results += $graphResults.value

        $pages = $graphResults.'@odata.nextLink'
        while ($null -ne $pages) {

            $additional = Invoke-MgGraphRequest -Uri $pages -Method Get -OutputType PSObject

            if ($pages) {
                $pages = $additional.'@odata.nextLink'
            }
            $results += $additional.value
        }
        $results
    }
    catch {
        Write-Error $Error[0].ErrorDetails.Message
        break
    }
}
function Get-MDMGroup() {

    <#
    .SYNOPSIS
    This function is used to get Entra ID groups
 
    .DESCRIPTION
    The function connects to the Graph API Interface and gets any Entra ID groups
 
    .PARAMETER groupName
    Specifies the name of the group to retrieve.
 
    .EXAMPLE
    Get-MDMGroup -groupName 'Windows Intune Devices'
 
    #>


    [cmdletbinding()]

    param
    (
        [parameter(Mandatory = $true)]
        [string]$groupName
    )

    $graphApiVersion = 'beta'
    $Resource = 'groups'

    try {
        $searchTerm = 'search="displayName:' + $groupName + '"'
        $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?$searchTerm"
        (Invoke-MgGraphRequest -Uri $uri -Method Get -OutputType PSObject -Headers @{ConsistencyLevel = 'eventual' }).Value
    }
    catch {
        Write-Error $_.Exception.Message
        break
    }
}
function New-MDMGroup() {

    <#
    .SYNOPSIS
    This function is used to create Entra ID groups
 
    .DESCRIPTION
    The function connects to the Graph API Interface and creates a new Entra ID group
 
    .PARAMETER JSON
    Specifies the JSON representation of the group to create.
 
    .EXAMPLE
    New-MDMGroup -JSON $JSONData
 
    #>


    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'low')]

    param
    (
        [Parameter(Mandatory = $true)]
        $JSON
    )

    process {

        $graphApiVersion = 'beta'
        $Resource = 'groups'

        if ($PSCmdlet.ShouldProcess('Entra Group', 'Create')) {
            try {
                Test-JsonData -Json $JSON
                $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
                Invoke-MgGraphRequest -Uri $uri -Method Post -Body $JSON -ContentType 'application/json'
            }
            catch {
                Write-Error $_.Exception.Message
                break
            }
        }
        elseif ($WhatIfPreference.IsPresent) {
            Write-Output 'Entra Group would have been created'
        }
        else {
            Write-Output 'Entra Group was not created'
        }
    }
}
function Read-YesNoChoice {
    <#
    .SYNOPSIS
    Prompt the user for a Yes No choice.
 
    .DESCRIPTION
    Prompt the user for a Yes No choice and returns 0 for no and 1 for yes.
 
    .PARAMETER Title
    Title for the prompt
 
    .PARAMETER Message
    Message for the prompt
 
    .PARAMETER DefaultOption
    Specifies the default option if nothing is selected
 
    .INPUTS
    None. You cannot pipe objects to Read-YesNoChoice.
 
    .OUTPUTS
    Int. Read-YesNoChoice returns an Int, 0 for no and 1 for yes.
 
    .EXAMPLE
    PS> $choice = Read-YesNoChoice -Title "Please Choose" -Message "Yes or No?"
 
    Please Choose
    Yes or No?
    [N] No [Y] Yes [?] Help (default is "N"): y
    PS> $choice
    1
 
    .EXAMPLE
    PS> $choice = Read-YesNoChoice -Title "Please Choose" -Message "Yes or No?" -DefaultOption 1
 
    Please Choose
    Yes or No?
    [N] No [Y] Yes [?] Help (default is "Y"):
    PS> $choice
    1
 
    .LINK
    Online version: https://www.chriscolden.net/2024/03/01/yes-no-choice-function-in-powershell/
    #>


    param (
        [Parameter(Mandatory = $true)][String]$Title,
        [Parameter(Mandatory = $true)][String]$Message,
        [Parameter(Mandatory = $false)][Int]$DefaultOption = 0
    )

    $No = New-Object System.Management.Automation.Host.ChoiceDescription '&No', 'No'
    $Yes = New-Object System.Management.Automation.Host.ChoiceDescription '&Yes', 'Yes'
    $Options = [System.Management.Automation.Host.ChoiceDescription[]]($No, $Yes)

    return $host.ui.PromptForChoice($Title, $Message, $Options, $DefaultOption)
}
#endregion Functions

#region variables
$modules = @('Microsoft.Graph.Authentication', 'Microsoft.PowerShell.ConsoleGuiTools')
$requiredScopes = @('Device.Read.All', 'DeviceManagementServiceConfig.ReadWrite.All', 'DeviceManagementManagedDevices.Read.All', 'Group.ReadWrite.All')
[String[]]$scopes = $requiredScopes -join ', '
$rndWait = Get-Random -Minimum 1 -Maximum 2
$continueScript = ''
#endregion variables

#region intro
Write-Host '
 _______ __ __ __ __
| _ |.--.--.| |_.-----.-----.|__| |.-----.| |_'
 -ForegroundColor Cyan -NoNewline
Write-Host '
| || | || _| _ | _ || | || _ || _|'
 -ForegroundColor DarkCyan -NoNewline
Write-Host '
|___|___||_____||____|_____| __||__|__||_____||____|
                           |__|
'
 -ForegroundColor blue
Write-Host '
 _______ _______
| __|.----.-----.--.--.-----.|_ _|.---.-.-----.-----.-----.----.
| | || _| _ | | | _ | | | | _ | _ | _ | -__| _|
|_______||__| |_____|_____| __| |___| |___._|___ |___ |_____|__|
                          |__| |_____|_____|
'
 -ForegroundColor Green

Write-Host 'AutopilotGroupTagger - Update Autopilot devices in bulk.' -ForegroundColor Green
Write-Host 'Nick Benton - oddsandendpoints.co.uk' -NoNewline;
Write-Host ' | Version' -NoNewline; Write-Host ' 0.7.0 Public Preview' -ForegroundColor Yellow -NoNewline
Write-Host ' | Last updated: ' -NoNewline; Write-Host '2025-10-08' -ForegroundColor Magenta
Write-Host "`nIf you have any feedback, please open an issue at https://github.com/ennnbeee/AutopilotGroupTagger/issues" -ForegroundColor Cyan
Start-Sleep -Seconds $rndWait
#endregion intro

#region preflight
if ($PSVersionTable.PSVersion.Major -eq 5) {
    Write-Host "`nWARNING: PowerShell 5 is not supported, use PowerShell 7.2 or later." -ForegroundColor Yellow
    exit
}
#endregion preflight

#region module check
foreach ($module in $modules) {
    Write-Host "Checking for $module PowerShell module..." -ForegroundColor Cyan
    if (!(Get-Module -Name $module -ListAvailable)) {
        Install-Module -Name $module -Scope CurrentUser -AllowClobber
    }
    Write-Host "`nPowerShell Module $module found." -ForegroundColor Green
    if (!([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "*$module*")) {
        Import-Module -Name $module -Force
    }
}
#endregion module check

#region app auth
try {
    if (!$tenantId) {
        Write-Host 'Connecting using interactive authentication' -ForegroundColor Yellow
        Connect-MgGraph -Scopes $scopes -NoWelcome -ErrorAction Stop
    }
    else {
        if ((!$appId -and !$appSecret) -or ($appId -and !$appSecret) -or (!$appId -and $appSecret)) {
            Write-Host 'Missing App Details, connecting using user authentication' -ForegroundColor Yellow
            Connect-ToGraph -tenantId $tenantId -Scopes $scopes -ErrorAction Stop
        }
        else {
            Write-Host 'Connecting using App authentication' -ForegroundColor Yellow
            Connect-ToGraph -tenantId $tenantId -appId $appId -appSecret $appSecret -ErrorAction Stop
        }
    }
    $context = Get-MgContext
    Write-Host "`nSuccessfully connected to Microsoft Graph tenant $($context.TenantId)." -ForegroundColor Green
}
catch {
    Write-Error $_.Exception.Message
    exit
}
#endregion app auth

#region scopes
$currentScopes = $context.Scopes
# Validate required permissions
$missingScopes = $requiredScopes | Where-Object { $_ -notin $currentScopes }
if ($missingScopes.Count -gt 0) {
    Write-Host 'WARNING: The following scope permissions are missing:' -ForegroundColor Red
    $missingScopes | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
    Write-Host "`nPlease ensure these permissions are granted to the app registration for full functionality." -ForegroundColor Yellow
    exit
}
Write-Host "`nAll required scope permissions are present." -ForegroundColor Green
#endregion scopes

#region script
do {

    #region discovery
    Start-Sleep -Seconds 2  # Delay to allow for Graph API to catch up
    Write-Host ''
    Write-Host 'Getting all Entra ID Windows computer objects...' -ForegroundColor Cyan
    $entraDevices = Get-EntraIDObject -device -os Windows
    $entraDevicesHash = @{}
    foreach ($entraDevice in $entraDevices) {
        $entraDevicesHash[$entraDevice.deviceid] = $entraDevice
    }
    Write-Host "Found $($entraDevices.Count) Windows devices and associated IDs from Entra ID." -ForegroundColor Green

    Write-Host ''
    Write-Host 'Getting all Windows Intune devices...' -ForegroundColor Cyan
    $intuneDevices = Get-ManagedDevice -os Windows
    $intuneDevicesHash = @{}
    foreach ($intuneDevice in $intuneDevices) {
        $intuneDevicesHash[$intuneDevice.id] = $intuneDevice
    }
    Write-Host "Found $($intuneDevices.Count) Windows device objects and associated IDs from Microsoft Intune." -ForegroundColor Green
    Write-Host ''

    Write-Host 'Getting all Windows Autopilot devices...' -ForegroundColor Cyan
    $apDevices = Get-AutopilotDevice
    $autopilotDevices = @()
    foreach ($apDevice in $apDevices) {
        # Details of Entra ID device object
        $entraObject = $entraDevicesHash[$apDevice.azureAdDeviceId]
        # Details of Intune device object
        #$intuneObject = $intuneDevicesHash[$apDevice.managedDeviceId]

        $autopilotDevices += [PSCustomObject]@{
            'displayName'              = $entraObject.displayName
            'serialNumber'             = $apDevice.serialNumber
            'manufacturer'             = $apDevice.manufacturer
            'model'                    = $apDevice.model
            'enrolmentState'           = $apDevice.enrollmentState
            'enrolmentProfile'         = $entraObject.enrollmentProfileName
            'enrolmentType'            = $entraObject.enrollmentType
            'groupTag'                 = $apDevice.groupTag
            'purchaseOrder'            = $apDevice.purchaseOrderIdentifier
            'Id'                       = $apDevice.Id
            'userlessEnrollmentStatus' = $apDevice.userlessEnrollmentStatus
        }
    }
    $autopilotDevicesHash = @{}
    foreach ($autopilotDevice in $autopilotDevices) {
        $autopilotDevicesHash[$autopilotDevice.id] = $autopilotDevice
    }
    Write-Host "Found $($autopilotDevices.Count) Windows Autopilot Devices from Microsoft Intune." -ForegroundColor Green
    #endregion discovery

    #region choices
    $choice = ''
    $autopilotUpdateDevices = @()
    while ($autopilotUpdateDevices.Count -eq 0) {
        if ($whatIf) {
            Write-Host "`nWhatIf mode enabled, no changes will be made." -ForegroundColor Magenta
        }
        if ($createGroups) {
            Write-Host "`nDynamic Groups will be created based on Group Tags" -ForegroundColor Green
        }
        Write-Host "`nPlease Choose one of the Group Tag options below:" -ForegroundColor White
        Write-Host "`n (1) Update All Autopilot Devices Group Tags" -ForegroundColor Cyan
        Write-Host "`n (2) Update All Autopilot Devices with Empty Group Tags" -ForegroundColor Cyan
        Write-Host "`n (3) Update All Autopilot Devices with a specific Group Tag" -ForegroundColor Cyan
        Write-Host "`n (4) Update All selected Manufacturers of Autopilot Device Group Tags" -ForegroundColor Cyan
        Write-Host "`n (5) Update All selected Models of Autopilot Device Group Tags" -ForegroundColor Cyan
        Write-Host "`n (6) Update All Autopilot Devices with a specific Purchase Order" -ForegroundColor Cyan
        Write-Host "`n (7) Update a selection of Autopilot Devices Group Tags interactively" -ForegroundColor Cyan
        Write-Host "`n (8) Update Autopilot Devices Group Tags using exported data" -ForegroundColor Cyan
        Write-Host "`n (a) Unblock All Autopilot Devices" -ForegroundColor Magenta
        Write-Host "`n (b) Unblock All blocked Autopilot Devices" -ForegroundColor Magenta
        Write-Host "`n (c) Unblock All selected Manufacturers of Autopilot Device" -ForegroundColor Magenta
        Write-Host "`n (d) Unblock All selected Models of Autopilot Device" -ForegroundColor Magenta
        Write-Host "`n (X) EXIT SCRIPT`n" -ForegroundColor Red
        $choice = Read-Host -Prompt 'Please select an option from the provided list, then press enter'
        while ( $choice -notin @('1', '2', '3', '4', '5', '6', '7', '8', 'a', 'b', 'c', 'd', 'X')) {
            $choice = Read-Host -Prompt 'Please select an option from the provided list, then press enter'
        }
        if ($choice -eq 'X') {
            exit
        }
        if ($choice -eq '1' -or $choice -eq 'a') {
            #All AutoPilot Devices
            $autopilotUpdateDevices = $autopilotDevices
        }
        if ($choice -eq '2') {
            #All AutoPilot Devices with Empty Group Tags
            $autopilotUpdateDevices = $autopilotDevices | Where-Object { ($null -eq $_.groupTag) -or ($_.groupTag) -eq '' }
            if ($autopilotUpdateDevices.count -eq 0) {
                Start-Sleep -Seconds $rndWait
                Write-Host 'No Autopilot Devices with Empty Group Tags found.' -ForegroundColor Yellow
                Write-Host 'Please select another option.' -ForegroundColor Yellow
                Start-Sleep -Seconds $rndWait
            }
        }
        if ($choice -eq '3') {
            # GroupTag prompts
            $confirmGroupTags = 0
            while ($confirmGroupTags -ne 1) {
                while ($autopilotGroupTags.count -eq 0) {
                    $autopilotGroupTags = @($autopilotDevices | Select-Object -Property groupTag -Unique | Out-ConsoleGridView -Title 'Select GroupTags of Autopilot Devices to Update' -OutputMode Multiple)
                }
                Write-Host "`nThe following Group Tag(s) were selected:`n" -ForegroundColor Cyan
                $autopilotGroupTags.groupTag
                $confirmGroupTags = Read-YesNoChoice -Title 'Please confirm Group Tag(s) selection' -Message 'Are these the correct Group Tag(s) to update?' -DefaultOption 1
                if ($confirmGroupTags -eq 0) {
                    Write-Host "`nPlease re-select the Group Tags to update" -ForegroundColor Yellow
                    $autopilotGroupTags = $null
                }
                $autopilotUpdateDevices = $autopilotDevices | Where-Object { $_.groupTag -in $autopilotGroupTags.groupTag }
            }
        }
        if ($choice -eq '4') {
            # Manufacturer prompts
            $confirmManufacturers = 0
            while ($confirmManufacturers -ne 1) {
                while ($autopilotManufacturers.count -eq 0) {
                    $autopilotManufacturers = @($autopilotDevices | Select-Object -Property manufacturer -Unique | Out-ConsoleGridView -Title 'Select Manufacturer of Autopilot Devices to Update' -OutputMode Multiple)
                }
                Write-Host "`nThe following Autopilot Device Manufacturer(s) were selected:`n" -ForegroundColor Cyan
                $autopilotManufacturers.manufacturer
                $confirmManufacturers = Read-YesNoChoice -Title 'Please confirm the Autopilot Device Manufacturer(s)' -Message 'Are these the correct Manufacturer(s) to update?' -DefaultOption 1
                if ($confirmManufacturers -eq 0) {
                    Write-Host "`nPlease re-select the Manufacturer(s) to update" -ForegroundColor Yellow
                    $autopilotManufacturers = $null
                }
                $autopilotUpdateDevices = $autopilotDevices | Where-Object { $_.manufacturer -in $autopilotManufacturers.manufacturer }
            }
        }
        if ($choice -eq '5') {
            # Model prompts
            $confirmModels = 0
            while ($confirmModels -ne 1) {
                while ($autopilotModels.count -eq 0) {
                    $autopilotModels = @($autopilotDevices | Select-Object -Property model -Unique | Out-ConsoleGridView -Title 'Select Models of Autopilot Devices to Update' -OutputMode Multiple)
                }
                Write-Host "`nThe following Autopilot Device Model(s) were selected:`n" -ForegroundColor Cyan
                $autopilotModels.model
                $confirmModels = Read-YesNoChoice -Title 'Please confirm the Autopilot Device Model(s)' -Message 'Are these the correct Model(s) to update?' -DefaultOption 1
                if ($confirmModels -eq 0) {
                    Write-Host "`nPlease re-select the Models to update" -ForegroundColor Yellow
                    $autopilotModels = $null
                }
                $autopilotUpdateDevices = $autopilotDevices | Where-Object { $_.model -in $autopilotModels.model }
            }
        }
        if ($choice -eq '6') {
            # Purchase Order prompts
            $confirmPOs = 0
            while ($confirmPOs -ne 1) {
                while ($autopilotPOs.count -eq 0) {
                    $autopilotPOs = @($autopilotDevices | Select-Object -Property purchaseOrder -Unique | Out-ConsoleGridView -Title 'Select Purchase Order of Autopilot Devices to Update' -OutputMode Multiple)
                }
                Write-Host "`nThe following Autopilot Device Purchase Order(s) were selected:`n" -ForegroundColor Cyan
                $autopilotPOs.purchaseOrder
                $confirmPOs = Read-YesNoChoice -Title 'Please confirm Autopilot Device Purchase Order(s)' -Message 'Are these the correct Purchase Order(s) to update?' -DefaultOption 1
                if ($confirmPOs -eq 0) {
                    Write-Host "`nPlease re-select the Purchase Order(s) to update" -ForegroundColor Yellow
                    $autopilotPOs = $null
                }
                $autopilotUpdateDevices = $autopilotDevices | Where-Object { $_.purchaseOrder -in $autopilotPOs.purchaseOrder }
            }
        }
        if ($choice -eq '7') {
            while ($autopilotUpdateDevices.count -eq 0) {
                $autopilotUpdateDevices = @($autopilotDevices | Out-ConsoleGridView -Title 'Select Autopilot Devices to Update' -OutputMode Multiple)
            }
        }
        if ($choice -eq '8') {
            # Report
            $autopilotDevices | Export-Csv -Path '.\AutopilotDevices.csv' -NoTypeInformation -Force
            Write-Host "`nExported All Autopilot Device(s) to AutopilotDevices.csv" -ForegroundColor Cyan
            while ($autopilotUpdateDevices.count -eq 0 -or ($autopilotUpdateDevices.groupTag | Measure-Object -Maximum).Maximum.length -gt 512) {
                if (($autopilotUpdateDevices.groupTag | Measure-Object -Maximum).Maximum.length -gt 512) {
                    Write-Host "`nOne or more Group Tags are greater than 512 characters." -ForegroundColor Red
                }
                Write-Warning -Message 'Please update the Group Tags on device(s) in AutopilotDevices.csv and save the file before continuing' -WarningAction Inquire
                $autopilotImportDevices = Import-Csv -Path .\AutopilotDevices.csv
                $autopilotUpdateDevices = @()
                foreach ($autopilotImportDevice in $autopilotImportDevices) {
                    $apObject = $autopilotDevicesHash[$autopilotImportDevice.Id]
                    if ($autopilotImportDevice.groupTag -ne $apObject.groupTag) {
                        $autopilotUpdateDevices += $autopilotImportDevice
                    }
                }
            }
        }
        if ($choice -eq 'b') {
            # Unblock All blocked Autopilot Devices
            $autopilotUpdateDevices = $autopilotDevices | Where-Object { $_.userlessEnrollmentStatus -ne 'allowed' }
            if ($autopilotUpdateDevices.count -eq 0) {
                Start-Sleep -Seconds $rndWait
                Write-Host 'No Autopilot Devices are currently blocked.' -ForegroundColor Yellow
                Write-Host 'Please select another option.' -ForegroundColor Yellow
                Start-Sleep -Seconds $rndWait
            }
        }
        if ($choice -eq 'c') {
            # Manufacturer prompts
            $confirmManufacturers = 0
            while ($confirmManufacturers -ne 1) {
                while ($autopilotManufacturers.count -eq 0) {
                    $autopilotManufacturers = @($autopilotDevices | Select-Object -Property manufacturer -Unique | Out-ConsoleGridView -Title 'Select Manufacturer of Autopilot Devices to unblock' -OutputMode Multiple)
                }
                Write-Host "`nThe following Autopilot Device Manufacturer(s) were selected:`n" -ForegroundColor Cyan
                $autopilotManufacturers.manufacturer
                $confirmManufacturers = Read-YesNoChoice -Title 'Please confirm the Autopilot Device Manufacturer(s)' -Message 'Are these the correct Manufacturer(s) to unblock?' -DefaultOption 1
                if ($confirmManufacturers -eq 0) {
                    Write-Host "`nPlease re-select the Manufacturer(s) to unblock" -ForegroundColor Yellow
                    $autopilotManufacturers = $null
                }
                $autopilotUpdateDevices = $autopilotDevices | Where-Object { $_.manufacturer -in $autopilotManufacturers.manufacturer }
            }
        }
        if ($choice -eq 'd') {
            # Model prompts
            $confirmModels = 0
            while ($confirmModels -ne 1) {
                while ($autopilotModels.count -eq 0) {
                    $autopilotModels = @($autopilotDevices | Select-Object -Property model -Unique | Out-ConsoleGridView -Title 'Select Models of Autopilot Devices to unblock' -OutputMode Multiple)
                }
                Write-Host "`nThe following Autopilot Device Model(s) were selected:`n" -ForegroundColor Cyan
                $autopilotModels.model
                $confirmModels = Read-YesNoChoice -Title 'Please confirm the Autopilot Device Model(s)' -Message 'Are these the correct Model(s) to unblock?' -DefaultOption 1
                if ($confirmModels -eq 0) {
                    Write-Host "`nPlease re-select the Models to unblock" -ForegroundColor Yellow
                    $autopilotModels = $null
                }
                $autopilotUpdateDevices = $autopilotDevices | Where-Object { $_.model -in $autopilotModels.model }
            }
        }
    }
    #endregion choices

    #region group tag prompt
    if ($choice -notin @('a', 'b', 'c', 'd')) {
        if ($choice -ne '8') {
            #group tags have a maximum of 512 characters
            $confirmGroupTag = 0
            while ($confirmGroupTag -ne 1) {
                Write-Host 'Press Enter to select an empty Group Tag value which will remove the Group Tag from the Autopilot Device(s).' -ForegroundColor Yellow
                Write-Host ''
                [string]$groupTagNew = Read-Host "Please enter the *NEW* group tag you wish to apply to the $($autopilotUpdateDevices.Count) Autopilot device(s)"
                while ($groupTagNew.length -gt 512) {
                    [string]$groupTagNew = Read-Host "Please enter the *NEW* group tag you wish to apply to the $($autopilotUpdateDevices.Count) Autopilot device(s) but with less than 512 characters"
                }
                Write-Host ''
                Write-Host 'The following Autopilot Device Group Tag was entered:' -ForegroundColor Cyan
                Write-Host ''
                $groupTagNew
                if ($groupTagNew -eq '' -or $null -eq $groupTagNew) {
                    Write-Host 'An empty Group Tag value will remove the Group Tag from the Autopilot Device(s).' -ForegroundColor red
                    Write-Host ''
                }
                $confirmGroupTag = Read-YesNoChoice -Title 'Please confirm the Autopilot Device Group Tag' -Message 'Is this the correct Group Tag to use?' -DefaultOption 1
                if ($confirmGroupTag -eq 0) {
                    Write-Host ''
                    Write-Host 'Please re-enter a *NEW* Group Tag' -ForegroundColor Yellow
                    Write-Host ''
                    $groupTagNew = $null
                }
            }
        }
    }
    #endregion group tag prompt

    #region Autopilot device update
    Write-Host "`nThe following $($autopilotUpdateDevices.Count) Autopilot device(s) are in scope to be updated:" -ForegroundColor Yellow
    $autopilotUpdateDevices | Format-Table -Property displayName, serialNumber, manufacturer, model, purchaseOrder, userlessEnrollmentStatus -AutoSize

    if ($whatIf) {
        Write-Host "`nWhatIf mode enabled, no changes will be made." -ForegroundColor Magenta
    }

    Write-Warning -Message "You are about to update $($autopilotUpdateDevices.Count) Autopilot device(s)." -WarningAction Inquire

    $progressCount = 0
    $progressTotal = $($autopilotUpdateDevices.Count)
    $progressActivity = 'Updating Autopilot Device(s) with '
    $Host.PrivateData.ProgressBackgroundColor = $Host.UI.RawUI.BackgroundColor
    $host.PrivateData.ProgressForegroundColor = 'green'
    Write-Progress -Activity $progressActivity -Status 'Starting' -PercentComplete 0

    foreach ($autopilotUpdateDevice in $autopilotUpdateDevices) {
        Start-Sleep -Seconds $rndWait
        if ($choice -eq '8') {
            $groupTagNew = $($autopilotUpdateDevice.groupTag)
        }

        #Write-Host "Updating Autopilot Group Tag with Serial Number: $($autopilotUpdateDevice.serialNumber) to '$groupTagNew'." -ForegroundColor Cyan
        $progressCount++
        $progressComplete = (($progressCount / $progressTotal) * 100)
        $progressStatus = "serial number: $($autopilotUpdateDevice.serialNumber)"
        Write-Progress -Activity $progressActivity -Status $progressStatus -PercentComplete $progressComplete
        if (!$whatIf) {
            if ($choice -notin @('a', 'b', 'c', 'd')) {
                Set-AutopilotDevice -id $autopilotUpdateDevice.id -groupTag $groupTagNew -Confirm:$false
            }
            else {
                Set-AutopilotDevice -id $autopilotUpdateDevice.id -unblock:$true -Confirm:$false
            }

        }
        #Write-Host "Updated Autopilot Group Tag with Serial Number: $($autopilotUpdateDevice.serialNumber) to '$groupTagNew'." -ForegroundColor Green
    }

    Write-Progress -Activity $progressActivity -Status 'Complete' -PercentComplete 100
    Write-Host "Successfully updated $($autopilotUpdateDevices.Count) Autopilot device(s)" -ForegroundColor Green
    #endregion Autopilot device update

    #region Group Creation
    if ($choice -notin @('a', 'b', 'c', 'd')) {
        if ($createGroups) {
            $groupTagsArray = @()
            if ($choice -eq '8') {
                foreach ($autopilotUpdateDevice in $autopilotUpdateDevices) {
                    $groupTagsArray += $($autopilotUpdateDevice.groupTag)
                }
            }
            else {
                $groupTagsArray += $groupTagNew
            }
            $groupTagsArray = $groupTagsArray | Select-Object -Unique
            $groupsArray = @()
            foreach ($groupTagArray in $groupTagsArray) {
                $groupRule = "(device.devicePhysicalIds -any _ -eq `\`"[OrderID]:$groupTagArray`\`")"
                $groupsArray += [pscustomobject]@{displayName = "$($groupPrefix + $groupTagArray)"; description = "All Autopilot Devices with Group Tag '$groupTagArray' created by AutopilotGroupTagger"; rule = "$groupRule" }
            }
            Write-Host "`nThe following $($groupsArray.Count) group(s) will be created:" -ForegroundColor Yellow
            $groupsArray | Select-Object -Property displayName, rule, description | Format-Table -AutoSize

            Write-Warning -Message "You are about to create $($groupsArray.Count) new group(s) in Microsoft Entra ID. Please confirm you want to continue." -WarningAction Inquire

            foreach ($group in $groupsArray) {
                Start-Sleep -Seconds $rndWait
                $groupName = $($group.displayName)
                if ($groupName.length -gt 120) {
                    #shrinking group name to less than 120 characters
                    $groupName = $groupName[0..120] -join ''
                }

                if (!(Get-MDMGroup -groupName $groupName)) {
                    Write-Host "`nCreating Group $groupName with rule $($group.rule)" -ForegroundColor Cyan
                    $groupJSON = @"
{
    "description": "$($group.description)",
    "displayName": "$groupName",
    "groupTypes": [
        "DynamicMembership"
    ],
    "mailEnabled": false,
    "mailNickname": "$groupName",
    "securityEnabled": true,
    "membershipRule": "$($group.rule)",
    "membershipRuleProcessingState": "On"
}
"@

                    if ($whatIf) {
                        Write-Host 'WhatIf mode enabled, no changes will be made.' -ForegroundColor Magenta
                        continue
                    }
                    else {
                        New-MDMGroup -JSON $groupJSON | Out-Null
                    }
                    Write-Host "Group $($group.displayName) created successfully." -ForegroundColor Green
                }
                else {
                    Write-Host "Group $($group.displayName) already exists, skipping creation." -ForegroundColor Yellow
                    continue
                }
            }
            Write-Host "Successfully created $($groupsArray.Count) new group(s) in Microsoft Entra ID." -ForegroundColor Green
        }
    }

    #endregion Group Creation

    $continueScript = Read-YesNoChoice -Title 'Continue AutopilotGroupTagger' -Message 'Do you want to update additional Autopilot device(s)?' -DefaultOption 0
    Clear-Host
}

until ($continueScript -eq '0')
#endregion script