├── LICENSE ├── README.md ├── remove_syncml.xml ├── install_syncml.xml └── UEMGoldMaster.ps1 /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Omnissa, LLC. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UEM Managed Goldmaster Scripts 2 | 3 | This powershell script allows you to easily use Workspace ONE Unified Endpoint Management (UEM) to bring a Gold Master image up the required state. 4 | 5 | The install_syncml.xml and remove_syncml.xml files are custom CSPs for setting the Windows Update settings. Place these in a profile in UEM, and modify the target version as needed. 6 | 7 | ## Requirements 8 | - A Workspace ONE UEM Admin Account that can use basic auth to authenticate into the APIs. 9 | - The corresponding Workspace ONE UEM API key. 10 | - Access to the gold image you want to bring up to date. 11 | 12 | ## Description 13 | When run, the script will: 14 | 1. Enroll the device to UEM using the given credentials. 15 | 2. Ensure all apps and profiles are on the device (there may be reboots in this step as app installs require). 16 | 3. Run windows update as much as is needed to get the device to the desired update state (there may be reboots in this step as Windows update requires). 17 | 4. Unenroll the device, leaving the apps on there. 18 | 5. Uninstall UEM 19 | 6. Delete the device from UEM console 20 | 21 | Every one of the above steps is optional, and can be skipped with one of the inbuilt -SkipX flags (i.e. -SkipCleanup doesn't delete the enrollment from the Workspace ONE UEM console). 22 | 23 | For example, to run use the following (replacing values with correct environment details): 24 | 25 | ``` 26 | .\GoldMasterEnrollmentPoc.ps1 ` 27 | -ApiUsername "uem-api-username" ` 28 | -ApiPassword "uem-api-password" ` 29 | -UemUrl https://yourUemAddress.com ` 30 | -TenantCode "uem-tenant-code" ` 31 | -EnrollmentUrl https://yourUemEnrollmentUrl.com ` 32 | -EnrollmentUsername "uem-enrollment-username" ` 33 | -EnrollmentPassword "uem-enrollment-password" ` 34 | -EnrollmentOG "uem-enrollment-og" ` 35 | -AgentMsiPath "C:\path\to\AirwatchAgent.msi" 36 | ``` 37 | 38 | If you don't want to enter username and password directly into the script, it also supports using a pscredential object (pass the result of Get-Credential as -ApiCredential). 39 | 40 | To control windows updates, we recommend setting custom windows desktop profiles with syncml similar to the provided install.xml and uninstall.xml files (editing as needed for different feature updates and risk tollerance levels). These files are not meant to be exhaustive, and as long as you set a windows update policy that you're happy with then the script will work with it. 41 | 42 | ## Parameters 43 | -ApiUsername 44 | API/Administrator username into UEM. Necessary if -ApiCredential isn't passed in. 45 | 46 | -ApiPassword 47 | API/Administrator password into UEM. Necessary if -ApiCredential isn't passed in. 48 | 49 | -ApiCredential 50 | API/Administrator credential into UEM. 51 | 52 | -UemUrl 53 | UEM URL to use. 54 | 55 | -TenantCode 56 | UEM API Tenant Code. 57 | 58 | -EnrollmentUrl 59 | Enrollment URL if different to the UEM one, defaults to the -UemUrl value 60 | 61 | -EnrollmentUsername 62 | Enrollment user username 63 | 64 | -EnrollmentPassword 65 | Enrollment user password 66 | 67 | -EnrollmentOG 68 | Enrollment organizational group 69 | 70 | -AgentMsiPath 71 | Path to the AirwatchAgent.msi. Defaults to "AirwatchAgent.msi" in the current directory. 72 | 73 | -SkipEnroll [] 74 | Don't enroll the device. 75 | 76 | -SkipUpdate [] 77 | Don't check for apps or profiles, and don't apply windows updates. 78 | 79 | -SkipUnenroll [] 80 | Don't unenroll the device. (Also sets -SkipUninstall and -SkipCleanup). 81 | 82 | -SkipUninstall [] 83 | Skip uninstall. This leaves the Workspace ONE Unified Endpoint Management agent on the device in an unenrolled state. 84 | 85 | -SkipCleanup [] 86 | Skip the cleanup where the device is deleted from the Workspace ONE Unified Endpoint Management console. -------------------------------------------------------------------------------- /remove_syncml.xml: -------------------------------------------------------------------------------- 1 | 2 | 1 3 | 4 | 5 | ./Vendor/MSFT/Policy/Config/Update/TargetReleaseVersion 6 | 7 | 8 | chr 9 | 10 | 11 | 12 | 13 | 14 | 2 15 | 16 | 17 | ./Vendor/MSFT/Policy/Config/Update/ProductVersion 18 | 19 | 20 | chr 21 | 22 | 23 | 24 | 25 | 26 | 3 27 | 28 | 29 | ./Device/Vendor/MSFT/Policy/Config/Update/AllowAutoUpdate 30 | 31 | 32 | int 33 | text/plain 34 | 35 | 1 36 | 37 | 38 | 39 | 4 40 | 41 | 42 | ./Vendor/MSFT/Policy/Config/Update/AllowUpdateService 43 | 44 | 45 | int 46 | 47 | 1 48 | 49 | 50 | 51 | 5 52 | 53 | 54 | ./Device/Vendor/MSFT/Policy/Config/Update/AllowMUUpdateService 55 | 56 | 57 | int 58 | text/plain 59 | 60 | 1 61 | 62 | 63 | 64 | 6 65 | 66 | 67 | ./Device/Vendor/MSFT/Policy/Config/Update/BranchReadinessLevel 68 | 69 | 70 | int 71 | text/plain 72 | 73 | 16 74 | 75 | 76 | 77 | 7 78 | 79 | 80 | ./Device/Vendor/MSFT/Policy/Config/Update/DeferQualityUpdatesPeriodInDays 81 | 82 | 83 | int 84 | text/plain 85 | 86 | 3 87 | 88 | 89 | 90 | 8 91 | 92 | 93 | ./Device/Vendor/MSFT/Policy/Config/Update/DeferFeatureUpdatesPeriodInDays 94 | 95 | 96 | int 97 | text/plain 98 | 99 | 0 100 | 101 | 102 | 103 | 9 104 | 105 | 106 | ./Device/Vendor/MSFT/Policy/Config/Update/ConfigureFeatureUpdateUninstallPeriod 107 | 108 | 109 | int 110 | text/plain 111 | 112 | 14 113 | 114 | 115 | 116 | 10 117 | 118 | 119 | ./Device/Vendor/MSFT/Policy/Config/Update/ExcludeWUDriversInQualityUpdate 120 | 121 | 122 | int 123 | text/plain 124 | 125 | 0 126 | 127 | 128 | 129 | 11 130 | 131 | 132 | ./Vendor/MSFT/Policy/Config/Update/AllowNonMicrosoftSignedUpdate 133 | 134 | 135 | int 136 | 137 | 0 138 | 139 | 140 | 141 | 12 142 | 143 | 144 | ./Vendor/MSFT/Policy/Config/System/AllowBuildPreview 145 | 146 | 147 | int 148 | 149 | 0 150 | 151 | 152 | 153 | 13 154 | 155 | 156 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DODownloadMode 157 | 158 | 159 | int 160 | text/plain 161 | 162 | 163 | 164 | 165 | 166 | 14 167 | 168 | 169 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DOMaxCacheAge 170 | 171 | 172 | int 173 | text/plain 174 | 175 | 176 | 177 | 178 | 179 | 15 180 | 181 | 182 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DOMinFileSizeToCache 183 | 184 | 185 | int 186 | text/plain 187 | 188 | 189 | 190 | 191 | 192 | 16 193 | 194 | 195 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DODelayBackgroundDownloadFromHttp 196 | 197 | 198 | int 199 | text/plain 200 | 201 | 202 | 203 | 204 | 205 | 17 206 | 207 | 208 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DODelayForegroundDownloadFromHttp 209 | 210 | 211 | int 212 | text/plain 213 | 214 | 215 | 216 | 217 | 218 | 18 219 | 220 | 221 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DOMinBackgroundQoS 222 | 223 | 224 | int 225 | text/plain 226 | 227 | 228 | 229 | 230 | 231 | -------------------------------------------------------------------------------- /install_syncml.xml: -------------------------------------------------------------------------------- 1 | 2 | 1 3 | 4 | 5 | ./Vendor/MSFT/Policy/Config/Update/TargetReleaseVersion 6 | 7 | 8 | chr 9 | 10 | 2004 11 | 12 | 13 | 14 | 2 15 | 16 | 17 | ./Vendor/MSFT/Policy/Config/Update/ProductVersion 18 | 19 | 20 | chr 21 | 22 | Windows 10 23 | 24 | 25 | 26 | 3 27 | 28 | 29 | ./Device/Vendor/MSFT/Policy/Config/Update/AllowAutoUpdate 30 | 31 | 32 | int 33 | text/plain 34 | 35 | 1 36 | 37 | 38 | 39 | 4 40 | 41 | 42 | ./Vendor/MSFT/Policy/Config/Update/AllowUpdateService 43 | 44 | 45 | int 46 | 47 | 1 48 | 49 | 50 | 51 | 5 52 | 53 | 54 | ./Device/Vendor/MSFT/Policy/Config/Update/AllowMUUpdateService 55 | 56 | 57 | int 58 | text/plain 59 | 60 | 1 61 | 62 | 63 | 64 | 6 65 | 66 | 67 | ./Device/Vendor/MSFT/Policy/Config/Update/BranchReadinessLevel 68 | 69 | 70 | int 71 | text/plain 72 | 73 | 16 74 | 75 | 76 | 77 | 7 78 | 79 | 80 | ./Device/Vendor/MSFT/Policy/Config/Update/DeferQualityUpdatesPeriodInDays 81 | 82 | 83 | int 84 | text/plain 85 | 86 | 0 87 | 88 | 89 | 90 | 8 91 | 92 | 93 | ./Device/Vendor/MSFT/Policy/Config/Update/DeferFeatureUpdatesPeriodInDays 94 | 95 | 96 | int 97 | text/plain 98 | 99 | 0 100 | 101 | 102 | 103 | 9 104 | 105 | 106 | ./Device/Vendor/MSFT/Policy/Config/Update/ConfigureFeatureUpdateUninstallPeriod 107 | 108 | 109 | int 110 | text/plain 111 | 112 | 14 113 | 114 | 115 | 116 | 10 117 | 118 | 119 | ./Device/Vendor/MSFT/Policy/Config/Update/ExcludeWUDriversInQualityUpdate 120 | 121 | 122 | int 123 | text/plain 124 | 125 | 0 126 | 127 | 128 | 129 | 11 130 | 131 | 132 | ./Vendor/MSFT/Policy/Config/Update/AllowNonMicrosoftSignedUpdate 133 | 134 | 135 | int 136 | 137 | 0 138 | 139 | 140 | 141 | 12 142 | 143 | 144 | ./Vendor/MSFT/Policy/Config/System/AllowBuildPreview 145 | 146 | 147 | int 148 | 149 | 0 150 | 151 | 152 | 153 | 13 154 | 155 | 156 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DODownloadMode 157 | 158 | 159 | int 160 | text/plain 161 | 162 | 2 163 | 164 | 165 | 166 | 14 167 | 168 | 169 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DOMaxCacheAge 170 | 171 | 172 | int 173 | text/plain 174 | 175 | 0 176 | 177 | 178 | 179 | 15 180 | 181 | 182 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DOMinFileSizeToCache 183 | 184 | 185 | int 186 | text/plain 187 | 188 | 1 189 | 190 | 191 | 192 | 16 193 | 194 | 195 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DODelayBackgroundDownloadFromHttp 196 | 197 | 198 | int 199 | text/plain 200 | 201 | 3600 202 | 203 | 204 | 205 | 17 206 | 207 | 208 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DODelayForegroundDownloadFromHttp 209 | 210 | 211 | int 212 | text/plain 213 | 214 | 60 215 | 216 | 217 | 218 | 18 219 | 220 | 221 | ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DOMinBackgroundQoS 222 | 223 | 224 | int 225 | text/plain 226 | 227 | 64 228 | 229 | 230 | 231 | -------------------------------------------------------------------------------- /UEMGoldMaster.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Enrolls the machine to Workspace ONE Unified Endpoint Management, installs all apps and Windows updates, then unenrolls the machine, leaving those apps and updates installed. 4 | .NOTES 5 | Created: April 2022 6 | Created by: Max Fox 7 | Updated by: Max Fox 8 | Organization: Omnissa, LLC. 9 | Filename: UEMGoldMaster.ps1 10 | .DESCRIPTION 11 | Enrolls the machine to Workspace ONE Unified Endpoint Management. Then makes sure that all assigned apps and profiles are on the device, before installing all available windows updates. After this, it unenrolls the device, leaving the previously installed apps and updates on the device, so that we can seal the device as a Gold Master image. After this the device will be deleted from the Workspace ONE Unified Endpoint Management console. 12 | .EXAMPLE 13 | .\UEMGoldMaster.ps1 -ApiUsername administrator -ApiPassword verysecurepassword -UemUrl https://cnuemurl.com -TenantCode VGhpcyBpcyBhIGJhc2U2NCBzdHJpbmcgaXNuJ3QgaXQ= -EnrollmentUrl https://dsuemurl.com -EnrollmentUsername enrollmentuser -EnrollmentPassword verysecurepassword -EnrollmentOG gldmstr -AgentMsiPath C:\Recovery\OEM\AirwatchAgent.msi 14 | #> 15 | 16 | 17 | param ( 18 | # API/Administrator username into UEM. Necessary if -ApiCredential isn't passed in. 19 | [Parameter(Mandatory=$false)] 20 | [string]$ApiUsername = $null, 21 | # API/Administrator password into UEM. Necessary if -ApiCredential isn't passed in. 22 | [Parameter(Mandatory=$false)] 23 | [String]$ApiPassword = $null, 24 | # API/Administrator credential into UEM. 25 | [Parameter(Mandatory=$false)] 26 | [pscredential]$ApiCredential = $null, 27 | # UEM URL to use. 28 | [Parameter(Mandatory=$true)] 29 | [string]$UemUrl, 30 | # UEM API Tenant Code. 31 | [Parameter(Mandatory=$true)] 32 | [string]$TenantCode, 33 | # Enrollment URL if different to the UEM one, defaults to the -UemUrl value 34 | [Parameter(Mandatory=$false)] 35 | [string]$EnrollmentUrl=$UemUrl, 36 | # Enrollment user username 37 | [Parameter(Mandatory=$true)] 38 | [string]$EnrollmentUsername, 39 | # Enrollment user password 40 | [Parameter(Mandatory=$true)] 41 | [string]$EnrollmentPassword, 42 | # Enrollment organizational group 43 | [Parameter(Mandatory=$true)] 44 | [string]$EnrollmentOG, 45 | # Path to the AirwatchAgent.msi. Defaults to "AirwatchAgent.msi" in the current directory. 46 | [Parameter(Mandatory=$false)] 47 | [string]$AgentMsiPath=(Join-Path -Path (Get-Location) -ChildPath "AirwatchAgent.msi"), 48 | # Don't enroll the device. 49 | [switch]$SkipEnroll, 50 | # Don't check for apps or profiles, and don't apply windows updates. 51 | [switch]$SkipUpdate, 52 | # Don't unenroll the device. (Also sets -SkipUninstall and -SkipCleanup). 53 | [switch]$SkipUnenroll, 54 | # Skip uninstall. This leaves the Workspace ONE Unified Endpoint Management agent on the device in an unenrolled state. 55 | [switch]$SkipUninstall, 56 | # Skip the cleanup where the device is deleted from the Workspace ONE Unified Endpoint Management console. 57 | [switch]$SkipCleanup 58 | ) 59 | 60 | $ErrorActionPreference = "Stop" 61 | 62 | $MaxRetriesOnSuccessfulApiCalls = 30 63 | 64 | class UemApiConnection { 65 | [string]$BaseUrl 66 | [string]$ApiTenantCode 67 | [pscredential]$ApiCredential 68 | [System.Collections.IDictionary]$BaseApiHeaders 69 | 70 | UemApiConnection( 71 | [string]$baseUrl, 72 | [string]$apiTenantCode, 73 | [string]$apiUsername, 74 | [string]$apiPassword 75 | ) { 76 | $this.BaseUrl = $baseUrl 77 | $this.ApiTenantCode = $apiTenantCode 78 | $secureStringApiPassword = ConvertTo-SecureString -AsPlainText -String $apiPassword -Force 79 | $this.ApiCredential = New-Object System.Management.Automation.PSCredential ($apiUsername, $secureStringApiPassword) 80 | $this.BaseApiHeaders = @{ 81 | "aw-tenant-code" = $this.ApiTenantCode 82 | "Accept" = "application/json" 83 | } 84 | } 85 | 86 | UemApiConnection( 87 | [string]$baseUrl, 88 | [string]$apiTenantCode, 89 | [pscredential]$apiCredential 90 | ) { 91 | $this.BaseUrl = $baseUrl 92 | $this.ApiTenantCode = $apiTenantCode 93 | $this.ApiCredential = $apiCredential 94 | $this.BaseApiHeaders = @{ 95 | "aw-tenant-code" = $this.ApiTenantCode 96 | "Accept" = "application/json" 97 | } 98 | } 99 | } 100 | 101 | function Invoke-RestMethodWithRetry { 102 | param ( 103 | [Microsoft.PowerShell.Commands.WebRequestMethod]$Method, 104 | [uri]$Uri, 105 | [pscredential]$Credential, 106 | [System.Collections.IDictionary]$Headers, 107 | [object]$Body=$null, 108 | [int]$MaxAttempts = 5, 109 | [int]$RetryInterval = 120, 110 | [int[]]$AdditionalTransientStatusCodes 111 | ) 112 | 113 | $attempts = 0 114 | while ($attempts -lt $MaxAttempts) { 115 | try { 116 | $response = Invoke-RestMethod -Method $Method -Uri $Uri -Credential $Credential -Headers $Headers -Body $Body 117 | return $response 118 | } catch { 119 | $errorResponse = $_.Exception.Response 120 | 121 | if ($errorResponse) { 122 | $statusCode = $errorResponse.StatusCode.value__ 123 | 124 | if (($statusCode -ge 400 -and $statusCode -lt 500) -and $statusCode -ne 408 -and $AdditionalTransientStatusCodes -notcontains $statusCode) { 125 | # Status Code is a 4xx code that isn't 408 or any given transient code. Unrecoverable. 126 | throw $_.Exception 127 | } 128 | } 129 | } 130 | 131 | $attempts++ 132 | Start-Sleep -Seconds (60 * [Math]::Pow(2, $attempts)) 133 | } 134 | } 135 | 136 | function Get-HttpResponseReason { 137 | param ( 138 | $response 139 | ) 140 | if ($response.StatusDescription) { 141 | return $response.StatusDescription 142 | } elseif ($response.ReasonPhrase) { 143 | return $response.ReasonPhrase 144 | } else { 145 | return "Unknown" 146 | } 147 | } 148 | 149 | 150 | function Get-UemAgentInstallInfo { 151 | $installInfo = Get-WmiObject -Class Win32_Product -Filter "Name = 'Workspace ONE Intelligent Hub Installer'" 152 | return $installInfo 153 | } 154 | 155 | function Install-UemAgent { 156 | param ( 157 | [Parameter(Mandatory=$true)] 158 | [string]$agentMsiPath, 159 | [Parameter(Mandatory=$true)] 160 | [string]$enrollmentUrl, 161 | [Parameter(Mandatory=$true)] 162 | [string]$enrollmentOG, 163 | [Parameter(Mandatory=$true)] 164 | [string]$enrollmentUsername, 165 | [Parameter(Mandatory=$true)] 166 | [string]$enrollmentPassword 167 | ) 168 | # install/enroll WS1 UEM. 169 | $installInfo = Get-UemAgentInstallInfo 170 | if ($installInfo -and $installInfo.InstallState -eq 5) { 171 | Write-Host "UEM agent is already installed, continuing" 172 | } 173 | else { 174 | if (-not (Test-Path -Path $agentMsiPath)) { 175 | Write-Error "Cannot find ${agentMsiPath}" 176 | } 177 | Write-Host "Installing UEM agent" 178 | msiexec /i $AgentMsiPath /qn /L*V (Join-Path -Path $PSScriptRoot -ChildPath "AirwatchAgent.msi.log") ENROLL=Y SERVER=$enrollmentUrl LGNAME=$enrollmentOG USERNAME=$enrollmentUsername PASSWORD=$enrollmentPassword 179 | } 180 | } 181 | 182 | function Get-EnrollmentInfo { 183 | param ( 184 | [Parameter(Mandatory=$true)] 185 | [UemApiConnection]$apiConnection, 186 | [Parameter(Mandatory=$true)] 187 | [string]$serialNumber, 188 | [Parameter(Mandatory=$false)] 189 | [ValidateSet("Enrolled", "Unenrolled", IgnoreCase=$true)] 190 | [string]$expectedStatus = "Enrolled" 191 | ) 192 | 193 | $enrollmentFound = $false 194 | $attemptCount = 0 195 | 196 | $id = $null 197 | $uuid = $null 198 | $udid = $null 199 | $status = $null 200 | 201 | Add-Type -AssemblyName System.Web 202 | $url = "$($apiConnection.BaseUrl)/API/mdm/devices?searchby=SerialNumber&id=$([System.Web.HttpUtility]::UrlEncode($serialNumber))" 203 | while (-not $enrollmentFound -and $attemptCount -lt $MaxRetriesOnSuccessfulApiCalls) { 204 | $response = $null 205 | try { 206 | $response = Invoke-RestMethodWithRetry -Method Get -Uri $url -Credential $apiConnection.ApiCredential -Headers $apiConnection.BaseApiHeaders -AdditionalTransientStatusCodes @(404) 207 | } catch { 208 | $errorResponse = $_.Exception.Response 209 | $reason = Get-HttpResponseReason $errorResponse 210 | Write-Error "Enrollment not found. Reason: $reason" 211 | } 212 | 213 | if ($response -and $response.EnrollmentStatus -eq $expectedStatus) { 214 | $enrollmentFound = $true 215 | $udid = $response.Udid 216 | $uuid = $response.Uuid 217 | $id = $response.Id.Value 218 | $status = $response.EnrollmentStatus 219 | Write-Host "Device found as $($expectedStatus.ToLowerInvariant())." 220 | Write-Host "ID = ${id}" 221 | Write-Host "UUID = ${uuid}" 222 | Write-Host "UDID = ${udid}" 223 | } else { 224 | $attemptCount++ 225 | Write-Host "Device is not $($expectedStatus.ToLowerInvariant()) yet, retrying in 2 minutes..." 226 | Start-Sleep -Seconds 120 # poll only once every 2 mins 227 | } 228 | } 229 | if ($enrollmentFound) { 230 | return @{ 231 | Status = $status 232 | ID = $id; 233 | UUID = $uuid; 234 | UDID = $udid; 235 | } 236 | } else { 237 | Write-Error "Enrollment still not found in the desired state after ${attemptCount} attempts." 238 | } 239 | } 240 | 241 | function Test-UemAppsInstalled { 242 | param ( 243 | [Parameter(Mandatory=$true)] 244 | [UemApiConnection]$apiConnection, 245 | [Parameter(Mandatory=$true)] 246 | [string]$deviceUuid 247 | ) 248 | 249 | $appsUrl = "$($apiConnection.BaseUrl)/API/mdm/devices/$([System.Web.HttpUtility]::UrlEncode($deviceUuid))/apps/search" 250 | $appsComplete = $false 251 | 252 | $pendingAppNames = @() 253 | $pendingAppsCount = 0 254 | 255 | $attemptCount = 0 256 | 257 | while (-not $appsComplete -and $attemptCount -lt $MaxRetriesOnSuccessfulApiCalls) { 258 | $appsResponse = $null 259 | 260 | try { 261 | $appsResponse = Invoke-RestMethodWithRetry -Method Get -Uri $appsUrl -Credential $apiConnection.ApiCredential -Headers $apiConnection.BaseApiHeaders 262 | } catch { 263 | $errorResponse = $_.Exception.Response 264 | $reason = Get-HttpResponseReason $errorResponse 265 | Write-Error "Error getting apps information: $reason" 266 | } 267 | 268 | if ($appsResponse) { 269 | $assignedApps = $appsResponse.app_items | Where-Object { $_.assignment_status -eq "Assigned" } 270 | $installedApps = $assignedApps | Where-Object { $_.installed_status -eq "Installed" } 271 | $pendingApps = $assignedApps | Where-Object { $_.installed_status -ne "Installed" } 272 | $assignedAppsCount = @($assignedApps).Count 273 | $installedAppsCount = @($installedApps).Count 274 | $pendingAppsCount = @($pendingApps).Count 275 | $pendingAppNames = $pendingApps.name 276 | if ($assignedAppsCount -eq 0 -and $pendingAppsCount -eq 0) { 277 | Write-Host "No apps found... skipping..." 278 | } else { 279 | Write-Host "Installed ${installedAppsCount}/${assignedAppsCount} apps..." 280 | } 281 | if ($pendingAppsCount -eq 0) { 282 | $appsComplete = $true 283 | } 284 | } else { 285 | Write-Host "No apps found... skipping..." 286 | $appsComplete = $true 287 | } 288 | 289 | $attemptCount++ 290 | if (-not $appsComplete) { 291 | Write-Host "Sleeping for 2 minutes to try and install remaining $pendingAppsCount app(s)..." 292 | Start-Sleep -Seconds 120 # poll only once every 2 mins 293 | } 294 | } 295 | 296 | if (-not $appsComplete) { 297 | Write-Host "The following $pendingAppsCount apps failed to install in time:`n$pendingAppNames" 298 | } 299 | 300 | return $appsComplete 301 | } 302 | 303 | function Test-UemProfilesInstalled { 304 | param ( 305 | [Parameter(Mandatory=$true)] 306 | [UemApiConnection]$apiConnection, 307 | [Parameter(Mandatory=$true)] 308 | [int32]$deviceId 309 | ) 310 | 311 | $profilesUrl = "$($apiConnection.BaseUrl)/API/mdm/devices/${deviceId}/profiles" 312 | $profilesComplete = $false 313 | 314 | $pendingProfileNames = @{} 315 | $pendingProfilesCount = 0 316 | 317 | $attemptCount = 0 318 | 319 | while (-not $profilesComplete -and $attemptCount -lt $MaxRetriesOnSuccessfulApiCalls) { 320 | $profilesResponse = $null 321 | 322 | try { 323 | $profilesResponse = Invoke-RestMethodWithRetry -Method Get -Uri $profilesUrl -Credential $apiConnection.ApiCredential -Headers $apiConnection.BaseApiHeaders 324 | } catch { 325 | $errorResponse = $_.Exception.Response 326 | $reason = Get-HttpResponseReason $errorResponse 327 | Write-Error "Error getting profiles information: $reason" 328 | } 329 | 330 | if ($profilesResponse) { 331 | $assignedProfiles = $profilesResponse.DeviceProfiles | Where-Object { $_.AssignmentType -eq 1 } 332 | $installedProfiles = $assignedProfiles | Where-Object { $_.Status -eq 3 } 333 | $pendingProfiles = $assignedProfiles | Where-Object { $_.Status -ne 3 } 334 | $pendingProfileNames = $pendingProfiles.Name 335 | $assignedProfilesCount = @($assignedProfiles).Count 336 | $installedProfilesCount = @($installedProfiles).Count 337 | $pendingProfilesCount = @($pendingProfiles).Count 338 | if ($assignedProfilesCount -eq 0 -and $pendingProfilesCount -eq 0) { 339 | Write-Host "No profiles found... skipping..." 340 | } else { 341 | Write-Host "Installed ${installedProfilesCount}/${assignedProfilesCount} profile(s)..." 342 | } 343 | if ($pendingProfilesCount -eq 0) { 344 | $profilesComplete = $true 345 | } 346 | } else { 347 | Write-Host "No profiles found... skipping..." 348 | $profilesComplete = $true 349 | } 350 | 351 | $attemptCount++ 352 | if (-not $profilesComplete) { 353 | Write-Host "Sleeping for 2 minutes to try and install remaining $pendingProfilesCount profile(s)..." 354 | Start-Sleep -Seconds 120 # poll only once every 2 mins 355 | } 356 | } 357 | 358 | if (-not $profilesComplete) { 359 | Write-Host "The following $pendingProfilesCount profiles failed to install in time:`n$pendingProfileNames" 360 | } 361 | 362 | return $profilesComplete 363 | } 364 | 365 | function Install-WindowsUpdates { 366 | Install-PackageProvider -Name Nuget -MinimumVersion 2.8.5.208 -Force | Out-Null 367 | if (-not (Get-Module -ListAvailable -Name PSWindowsUpdate)) { 368 | Install-Module PSWindowsUpdate -Force -MinimumVersion 2.2.0.2 | Out-Null 369 | } 370 | Import-Module -Name "PSwindowsUpdate" -MinimumVersion 2.2.0.2 | Out-Null 371 | $updates = Get-WindowsUpdate 372 | 373 | if ($null -eq $updates -or $updates.Count -eq 0) { 374 | Write-Host "No updates to install." 375 | return 376 | } 377 | 378 | Write-Host "Installing the following updates:" 379 | $updates | ForEach-Object { 380 | Write-Host ([string]$_.Title) 381 | } 382 | 383 | $rebootRequired = $updates.RebootRequired -contains $true 384 | 385 | if ($rebootRequired) { 386 | Write-Host "Reboot will be required for updates to finish" 387 | } 388 | 389 | Install-WindowsUpdate -AcceptAll -Install -AutoReboot | Out-Null 390 | 391 | return $rebootRequired 392 | } 393 | 394 | function Add-KeepAppsPolicy { 395 | 396 | $deployCmdPath = Join-Path -Path $env:ProgramFiles -ChildPath "\Omnissa\SfdAgent\Omnissa.Hub.SfdAgent.DeployCmd.exe" 397 | if (-not (Test-Path -Path $deployCmdPath)) { 398 | Write-Error "Unable to find Omnissa.Hub.SfdAgent.DeployCmd.exe" 399 | } 400 | 401 | $keepAppsString = '{"policies":{"enterprise_wipe_options":{"keep_app":true,"keep_appdata":true}}}' 402 | 403 | $policyPath = Join-Path -Path $env:TEMP -ChildPath "sfdpolicy.json" 404 | 405 | Set-Content -Path $policyPath -Value $keepAppsString 406 | 407 | & $deployCmdPath /addpolicy $policyPath | Out-Null 408 | 409 | $regItem = Get-ItemProperty -Path "HKLM:\SOFTWARE\AirWatchMDM\AppDeploymentAgent\Policy\{00000000-0000-0000-0000-000000000000}" 410 | if (-not $regItem.PolicyJson -or 411 | $regItem.PolicyJson.Trim() -ne $keepAppsString -or 412 | -not $regItem.KeepApps -or 413 | -not $regItem.KeepAppData) { 414 | Write-Error "SFD policy to keep apps wasn't correctly applied" 415 | } 416 | 417 | Remove-Item $policyPath 418 | } 419 | 420 | function Remove-Enrollment { 421 | 422 | Add-KeepAppsPolicy 423 | 424 | $awProcessCommandsPath = "$(${env:ProgramFiles(x86)})\Airwatch\AgentUI\AWProcessCommands.exe" 425 | 426 | if (-not (Test-Path $awProcessCommandsPath)) { 427 | Write-Error "Unable to find AWProcessCommands.exe" 428 | return $false 429 | } 430 | 431 | & $awProcessCommandsPath Unenroll 432 | return $true 433 | } 434 | 435 | function Remove-UemAgent { 436 | Write-Host "Checking if uninstall is required" 437 | $installInfo = Get-UemAgentInstallInfo 438 | 439 | if ($installInfo -and $installInfo.InstallState -eq 5) { 440 | Write-Host "Uninstall required" 441 | $installInfo.Uninstall() | Out-Null 442 | } else { 443 | Write-Host "Uninstall not required" 444 | } 445 | } 446 | 447 | function Remove-EnrollmentFromUem { 448 | param ( 449 | [Parameter(Mandatory=$true)] 450 | [UemApiConnection]$apiConnection, 451 | [Parameter(Mandatory=$true)] 452 | [int32]$deviceId 453 | ) 454 | 455 | Write-Host "Deleting enrollment from UEM console." 456 | 457 | try { 458 | $deleteUrl = "$($apiConnection.BaseUrl)/API/mdm/devices?id=${deviceId}&searchby=DeviceId" 459 | Invoke-RestMethodWithRetry -Method Delete -Uri $deleteUrl -Credential $apiConnection.ApiCredential -Headers $apiConnection.BaseApiHeaders 460 | } catch { 461 | $errorResponse = $_.Exception.Response 462 | $reason = Get-HttpResponseReason $errorResponse 463 | Write-Error "Error deleting enrollment from UEM console: $reason" 464 | } 465 | } 466 | 467 | $x = $PSScriptRoot 468 | $serialNumber = (Get-WmiObject Win32_BIOS).SerialNumber 469 | Write-Host "Serial Number: `"$serialNumber`"" 470 | 471 | [UemApiConnection]$uemApiConnection = $null 472 | 473 | if ($ApiCredential) { 474 | $uemApiConnection = [UemApiConnection]::new($UemUrl, $TenantCode, $ApiCredential) 475 | } elseif ($ApiUsername -and $ApiPassword) { 476 | $uemApiConnection = [UemApiConnection]::new($UemUrl, $TenantCode, $ApiUsername, $ApiPassword) 477 | } else { 478 | Write-Error "Please supply an API credential (either using Get-Credentails, or supply a username and password)" 479 | } 480 | 481 | $enrollmentInfo = $null 482 | 483 | 484 | if (-not $SkipEnroll) { 485 | Install-UemAgent -agentMsiPath $AgentMsiPath -enrollmentUrl $EnrollmentUrl -enrollmentOG $EnrollmentOG -enrollmentUsername $EnrollmentUsername -enrollmentPassword $EnrollmentPassword 486 | } 487 | 488 | if (-not $SkipUpdate) { 489 | $enrollmentInfo = Get-EnrollmentInfo -apiConnection $uemApiConnection -serialNumber $serialNumber -expectedStatus "Enrolled" 490 | $profilesInstalled = Test-UemProfilesInstalled -apiConnection $uemApiConnection -deviceId $enrollmentInfo.ID 491 | $appsInstalled = Test-UemAppsInstalled -apiConnection $uemApiConnection -deviceUuid $enrollmentInfo.UUID 492 | 493 | if ((-not $appsInstalled) -or (-not $profilesInstalled)) { 494 | Write-Error "All apps and profiles are not installed." 495 | } 496 | 497 | $rebootRequired = Install-WindowsUpdates 498 | 499 | if ($rebootRequired) { 500 | Write-Host "Reboot is required, rebooting..." 501 | Restart-Computer 502 | } 503 | } 504 | 505 | if (-not $SkipUnenroll) { 506 | $unenrolled = Remove-Enrollment 507 | 508 | if (-not $unenrolled) { 509 | Write-Error "Unable to unenroll the device" 510 | } 511 | 512 | if (-not $SkipUninstall) { 513 | Remove-UemAgent 514 | } 515 | 516 | if (-not $SkipCleanup) { 517 | $enrollmentInfo = Get-EnrollmentInfo -apiConnection $uemApiConnection -serialNumber $serialNumber -expectedStatus "Unenrolled" 518 | Remove-EnrollmentFromUem -apiConnection $uemApiConnection -deviceId $enrollmentInfo.ID 519 | } 520 | } 521 | --------------------------------------------------------------------------------