PrepareOOBE.ps1
|
<#PSScriptInfo .VERSION 1.9 .GUID b28d9ccb-b2e5-4c78-8270-2a53c80ab0f6 .AUTHOR admin.descampd .COMPANYNAME .COPYRIGHT .TAGS .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .PRIVATEDATA #> <# .DESCRIPTION 1.9 #> Param() # Version : 1.7 # Changelog: # 1.0 - Initial release # 1.1 - Added force-removal of locked CCM directories via robocopy # 1.2 - Added MDM/co-management enrollment cleanup for Autopilot ZTID # 1.3 - Added CloudDomainJoin and OOBE state purge # 1.4 - Verbose logging, dsregcmd status capture, cleanup summary # 1.5 - Added NGC, Cloud AP token cache, Autopilot provisioning cache, AAD cert wipe # 1.6 - Merged Register-Autopilot.ps1; post-cleanup re-registration with fresh hash # 1.7 - Delete existing Autopilot records before fresh re-registration; delete stale Intune device [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Read GroupTag from Task Sequence environment while TS is still active. # Microsoft.SMS.TSEnvironment COM object is only available during TS execution. $DropperGroupTag = '' try { $tsEnv = New-Object -ComObject Microsoft.SMS.TSEnvironment -ErrorAction Stop $DropperGroupTag = $tsEnv.Value('GroupTag') } catch {} if (-not $DropperGroupTag) { $DropperGroupTag = [System.Environment]::GetEnvironmentVariable('GroupTag') } # Persist GroupTag to registry so the post-action script can retrieve it. # The COM object is gone by the time SMSTSPostAction runs. $RegBase = 'HKLM:\SOFTWARE\BekaertDeslee\Autopilot' try { if (-not (Test-Path $RegBase)) { New-Item -Path $RegBase -Force -ErrorAction Stop | Out-Null } Set-ItemProperty -Path $RegBase -Name 'GroupTag' -Value $DropperGroupTag -Force -ErrorAction Stop Write-Output "GroupTag '$DropperGroupTag' persisted to registry: $RegBase" } catch { Write-Warning "Could not persist GroupTag to registry: $_" } $ScriptBlock = @' $LogPath = 'C:\Windows\Temp\PrepareOOBE.log' $Script:Removed = 0 $Script:NotFound = 0 $Script:Failed = 0 # App credentials for Microsoft Graph (Autopilot re-registration) $TenantId = 'd955e7b6-1e6a-4009-91b7-98bff9ff36e6' $ClientId = '12913116-625c-4a6e-bf98-f768311eb87e' $ClientSecret = 'Ycw8Q~zifppCMFbIohu7Z~8puK~3BVMq53aEwb0K' # ---------------------------------------------------------------- function Write-Log { param([string]$Message, [string]$Level = 'Info') $Stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' Add-Content -Path $LogPath -Value "[$Stamp] [$Level] $Message" -ErrorAction SilentlyContinue $Color = switch ($Level) { 'Warning' { 'Yellow' } 'Error' { 'Red' } 'Success' { 'Green' } default { if ($Message -match '^\s*\[REMOVED\]') { 'Green' } elseif ($Message -match '^\s*\[FAILED\]') { 'Red' } elseif ($Message -match '^\s*\[KILLED\]') { 'Magenta' } elseif ($Message -match '^\s*\[NOT FOUND\]') { 'DarkGray' } elseif ($Message -match '^\s*\[FOUND\]') { 'Cyan' } elseif ($Message -match '^\s*\[WARNING\]') { 'Yellow' } else { 'White' } } } Write-Host "[$Stamp] [$Level] $Message" -ForegroundColor $Color } function Write-Step { param([string]$Title) $Sep = '-' * 60 Write-Log '' Write-Host '' Write-Host $Sep -ForegroundColor DarkCyan Write-Host " $Title" -ForegroundColor Cyan Write-Host $Sep -ForegroundColor DarkCyan Add-Content -Path $LogPath -Value $Sep -ErrorAction SilentlyContinue Add-Content -Path $LogPath -Value " $Title" -ErrorAction SilentlyContinue Add-Content -Path $LogPath -Value $Sep -ErrorAction SilentlyContinue } function Remove-RegKey { param([string]$Path) if (Test-Path $Path) { $children = (Get-ChildItem $Path -Recurse -ErrorAction SilentlyContinue).Count Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue if (-not (Test-Path $Path)) { Write-Log " [REMOVED] $Path ($children sub-items)" $Script:Removed++ } else { Write-Log " [FAILED] $Path - still present after removal" -Level Warning $Script:Failed++ } } else { Write-Log " [NOT FOUND] $Path" $Script:NotFound++ } } function Remove-LockedDirectory { param([string]$Path) if (-not (Test-Path $Path)) { Write-Log " [NOT FOUND] $Path" $Script:NotFound++ return } $FileCount = (Get-ChildItem $Path -Recurse -Force -ErrorAction SilentlyContinue).Count Write-Log " [FOUND] $Path ($FileCount files/folders) - starting robocopy wipe" $Empty = "$env:TEMP\__empty_robocopy" New-Item -ItemType Directory -Path $Empty -Force -ErrorAction SilentlyContinue | Out-Null $RoboResult = Start-Process 'robocopy.exe' ` -ArgumentList "`"$Empty`" `"$Path`" /MIR /NFL /NDL /NJH /NJS /R:1 /W:1" ` -Wait -NoNewWindow -PassThru -ErrorAction SilentlyContinue Write-Log " [ROBOCOPY] Exit code: $($RoboResult.ExitCode) (<=7 = success)" Remove-Item $Empty -Force -Recurse -ErrorAction SilentlyContinue Start-Process 'takeown.exe' -ArgumentList "/f `"$Path`" /r /d y" -Wait -NoNewWindow -ErrorAction SilentlyContinue | Out-Null Start-Process 'icacls.exe' -ArgumentList "`"$Path`" /grant administrators:F /t" -Wait -NoNewWindow -ErrorAction SilentlyContinue | Out-Null Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue if (Test-Path $Path) { Write-Log " [FAILED] $Path still present after forced removal" -Level Warning $Script:Failed++ } else { Write-Log " [REMOVED] $Path" $Script:Removed++ } } function Get-GraphToken { $Response = Invoke-RestMethod ` -Method POST ` -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" ` -ContentType 'application/x-www-form-urlencoded' ` -Body @{ grant_type = 'client_credentials' client_id = $ClientId client_secret = $ClientSecret scope = 'https://graph.microsoft.com/.default' } ` -ErrorAction Stop return $Response.access_token } function Invoke-GraphRequest { param( [Parameter(Mandatory)][string]$Method, [Parameter(Mandatory)][string]$Uri, [Parameter(Mandatory)][string]$Token, [object]$Body, [int]$MaxRetries = 5 ) $Headers = @{ Authorization = "Bearer $Token" } $Attempt = 0 while ($true) { try { $Params = @{ Method = $Method; Uri = $Uri; Headers = $Headers; ErrorAction = 'Stop' } if ($null -ne $Body) { $Params['Body'] = if ($Body -is [string]) { $Body } else { $Body | ConvertTo-Json -Depth 10 } $Params['ContentType'] = 'application/json' } return Invoke-RestMethod @Params } catch { $Attempt++ $StatusCode = $_.Exception.Response.StatusCode.value__ if ($Attempt -ge $MaxRetries) { throw } if ($StatusCode -in 429, 503, 504) { $Wait = [Math]::Min(60, [Math]::Pow(2, $Attempt)) Write-Log " Graph throttled (HTTP $StatusCode) - retrying in ${Wait}s ($Attempt/$MaxRetries)" -Level Warning Start-Sleep -Seconds $Wait } elseif ($StatusCode -eq 404) { return $null } else { throw } } } } # ================================================================ Write-Log '================================================================' Write-Log ' PrepareOOBE v1.7 started' Write-Log " Computer : $env:COMPUTERNAME" Write-Log " User : $env:USERNAME" Write-Log " Time : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" Write-Log '================================================================' # ================================================================ Write-Step 'PRE-FLIGHT: dsregcmd /status' # ================================================================ $DsregBefore = & dsregcmd.exe /status 2>&1 $DsregBefore | Where-Object { $_ -match 'AzureAdJoined|WorkplaceJoined|DomainJoined|TenantName|DeviceId|MDMUrl|EnterpriseJoined' } | ForEach-Object { Write-Log " $($_.Trim())" } # ================================================================ Write-Step 'STEP 1 - Initial dsregcmd /leave' # ================================================================ Write-Log ' Running dsregcmd /leave' $LeaveOut = & dsregcmd.exe /leave 2>&1 $LeaveOut | ForEach-Object { Write-Log " dsregcmd: $($_.ToString().Trim())" } Write-Log " dsregcmd /leave exit code: $LASTEXITCODE" # ================================================================ Write-Step 'STEP 2 - Kill CCM processes' # ================================================================ $Processes = @('CcmExec','ccmsetup','CMRcService','CmRcService','smsexec','smsswd', 'SensorWDService','SensorManagedProvider','ccmrestart','SCNotification') foreach ($ProcName in $Processes) { $procs = Get-Process -Name $ProcName -ErrorAction SilentlyContinue if ($procs) { $ids = $procs.Id -join ', ' $procs | Stop-Process -Force -ErrorAction SilentlyContinue Write-Log " [KILLED] $ProcName (PID: $ids)" } else { Write-Log " [NOT FOUND] Process: $ProcName" } } # ================================================================ Write-Step 'STEP 3 - Stop and remove CCM services' # ================================================================ foreach ($SvcName in @('CcmExec','ccmsetup','smstsmgr','CmRcService')) { $svc = Get-Service -Name $SvcName -ErrorAction SilentlyContinue if ($svc) { Write-Log " [FOUND] Service $SvcName (Status: $($svc.Status))" Stop-Service -Name $SvcName -Force -ErrorAction SilentlyContinue $ScResult = & sc.exe delete $SvcName 2>&1 Write-Log " [SC DELETE] $SvcName - $ScResult" } else { Write-Log " [NOT FOUND] Service: $SvcName" } } # ================================================================ Write-Step 'STEP 4 - Uninstall SCCM client' # ================================================================ if (Test-Path 'C:\Windows\ccmsetup\ccmsetup.exe') { Write-Log ' [FOUND] ccmsetup.exe - starting /Uninstall' Start-Process -FilePath 'C:\Windows\ccmsetup\ccmsetup.exe' -ArgumentList '/Uninstall' -NoNewWindow -ErrorAction SilentlyContinue $TimeoutSec = 300 $Elapsed = 0 $Interval = 5 Write-Log " Polling for uninstall completion (timeout: ${TimeoutSec}s)..." do { Start-Sleep -Seconds $Interval $Elapsed += $Interval $SvcGone = -not (Get-Service -Name 'CcmExec' -ErrorAction SilentlyContinue) $ProcGone = -not (Get-Process -Name 'ccmsetup' -ErrorAction SilentlyContinue) $ExeGone = -not (Test-Path 'C:\Windows\ccmsetup\ccmsetup.exe') Write-Log " [${Elapsed}s] ServiceGone=$SvcGone | ProcessGone=$ProcGone | ExeGone=$ExeGone" } until (($SvcGone -and $ProcGone) -or $Elapsed -ge $TimeoutSec) if ($SvcGone -and $ProcGone) { Write-Log " [DONE] CCM uninstall confirmed after ${Elapsed}s" } else { Write-Log " [WARNING] CCM uninstall timed out after ${TimeoutSec}s - continuing" -Level Warning } } else { Write-Log ' [NOT FOUND] ccmsetup.exe - skipping uninstall' } # ================================================================ Write-Step 'STEP 5 - Remove CCM WMI namespaces' # ================================================================ foreach ($ns in @('root\ccm','root\ccm\Policy','root\ccm\SoftwareMeteringAgent','root\SmsDm','root\cimv2\SMS')) { try { Get-WmiObject -Namespace $ns -Class '__Namespace' -ErrorAction Stop | Out-Null ([wmiclass]"\\.\$($ns):__Namespace").Delete() | Out-Null Write-Log " [REMOVED] WMI namespace: $ns" $Script:Removed++ } catch { Write-Log " [NOT FOUND] WMI namespace: $ns" $Script:NotFound++ } } # ================================================================ Write-Step 'STEP 6 - Remove CCM scheduled tasks' # ================================================================ $CcmTasks = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskPath -match 'Configuration Manager' -or $_.TaskName -match 'CCM|SMS|SCCM' } if ($CcmTasks) { foreach ($Task in $CcmTasks) { Unregister-ScheduledTask -TaskName $Task.TaskName -TaskPath $Task.TaskPath -Confirm:$false -ErrorAction SilentlyContinue Write-Log " [REMOVED] Task: $($Task.TaskPath)$($Task.TaskName)" $Script:Removed++ } } else { Write-Log ' [NOT FOUND] No CCM/SMS scheduled tasks' $Script:NotFound++ } # ================================================================ Write-Step 'STEP 7 - Remove SCCM registry keys' # ================================================================ @( 'HKLM:\SOFTWARE\Microsoft\CCM', 'HKLM:\SOFTWARE\Microsoft\CCMSetup', 'HKLM:\SOFTWARE\Microsoft\SMS', 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\SMS', 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\CCM', 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\SMS', 'HKLM:\SYSTEM\CurrentControlSet\Services\CcmExec', 'HKLM:\SYSTEM\CurrentControlSet\Services\ccmsetup', 'HKLM:\SYSTEM\CurrentControlSet\Services\smstsmgr', 'HKLM:\SYSTEM\CurrentControlSet\Services\CmRcService' ) | ForEach-Object { Remove-RegKey $_ } # ================================================================ Write-Step 'STEP 8 - Remove SCCM directories (robocopy wipe)' # ================================================================ @('C:\Windows\CCM','C:\Windows\CCMCache','C:\Windows\CCMSetup','C:\MININT') | ForEach-Object { Remove-LockedDirectory $_ } if (Test-Path 'C:\Windows\SMSCFG.ini') { Remove-Item 'C:\Windows\SMSCFG.ini' -Force -ErrorAction SilentlyContinue Write-Log ' [REMOVED] C:\Windows\SMSCFG.ini' $Script:Removed++ } else { Write-Log ' [NOT FOUND] C:\Windows\SMSCFG.ini' $Script:NotFound++ } $MifFiles = Get-ChildItem -Path 'C:\Windows' -Filter 'sms*.mif' -ErrorAction SilentlyContinue if ($MifFiles.Count -gt 0) { $MifFiles | Remove-Item -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] $($MifFiles.Count) SMS .mif file(s)" $Script:Removed += $MifFiles.Count } else { Write-Log ' [NOT FOUND] No SMS .mif files' $Script:NotFound++ } # ================================================================ Write-Step 'STEP 9 - Remove MDM / co-management enrollment state' # ================================================================ $EnrollmentRoot = 'HKLM:\SOFTWARE\Microsoft\Enrollments' if (Test-Path $EnrollmentRoot) { $Guids = Get-ChildItem $EnrollmentRoot -ErrorAction SilentlyContinue Write-Log " [FOUND] $($Guids.Count) MDM enrollment GUID(s):" foreach ($g in $Guids) { $etype = (Get-ItemProperty "$($g.PSPath)" -Name 'EnrollmentType' -ErrorAction SilentlyContinue).EnrollmentType $upn = (Get-ItemProperty "$($g.PSPath)" -Name 'UPN' -ErrorAction SilentlyContinue).UPN Write-Log " $($g.PSChildName) | Type=$etype | UPN=$upn" Remove-Item -Path $g.PSPath -Recurse -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] Enrollment GUID: $($g.PSChildName)" $Script:Removed++ } } else { Write-Log " [NOT FOUND] $EnrollmentRoot" $Script:NotFound++ } @( 'HKLM:\SOFTWARE\Microsoft\Enrollments', 'HKLM:\SOFTWARE\Microsoft\EnterpriseResourceManager', 'HKLM:\SOFTWARE\Microsoft\PolicyManager\current', 'HKLM:\SOFTWARE\Microsoft\PolicyManager\device', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MDMCommon', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MDM', 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\MDM', 'HKLM:\SOFTWARE\Microsoft\CCM\CoManagementHandler', 'HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin', 'HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo', 'HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\TenantInfo', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CloudExperienceHost', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\OOBE', 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\UnattendSettings' ) | ForEach-Object { Remove-RegKey $_ } Write-Log ' Checking MDM enrollment scheduled tasks...' $MdmTaskCount = 0 foreach ($TaskPath in @('\Microsoft\Windows\EnterpriseMgmt\','\Microsoft\Windows\EnterpriseMgmtNoncritical\')) { $tasks = Get-ScheduledTask -TaskPath $TaskPath -ErrorAction SilentlyContinue foreach ($t in $tasks) { Unregister-ScheduledTask -TaskName $t.TaskName -TaskPath $t.TaskPath -Confirm:$false -ErrorAction SilentlyContinue Write-Log " [REMOVED] MDM task: $($t.TaskPath)$($t.TaskName)" $MdmTaskCount++ $Script:Removed++ } } if ($MdmTaskCount -eq 0) { Write-Log ' [NOT FOUND] No MDM enrollment scheduled tasks' } Write-Log ' Checking MDM device certificates in LocalMachine\My...' $MdmCerts = Get-ChildItem 'Cert:\LocalMachine\My' -ErrorAction SilentlyContinue | Where-Object { $_.Issuer -match 'MS-Organization-Access|Microsoft Intune MDM Device CA|MDM' } if ($MdmCerts) { foreach ($cert in $MdmCerts) { Remove-Item -Path "Cert:\LocalMachine\My\$($cert.Thumbprint)" -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] Cert: $($cert.Subject) | Issuer: $($cert.Issuer) | Thumb: $($cert.Thumbprint)" $Script:Removed++ } } else { Write-Log ' [NOT FOUND] No MDM certificates in LocalMachine\My' $Script:NotFound++ } # ================================================================ Write-Step 'STEP 10 - Final dsregcmd /leave after full cleanup' # ================================================================ Write-Log ' Running dsregcmd /leave (post-cleanup flush)' $LeaveOut2 = & dsregcmd.exe /leave 2>&1 $LeaveOut2 | ForEach-Object { Write-Log " dsregcmd: $($_.ToString().Trim())" } Write-Log " Exit code: $LASTEXITCODE" # ================================================================ Write-Step 'STEP 11 - Remove AAD / ZTID identity artifacts' # ================================================================ Write-Log ' Removing NGC (Windows Hello) credential folders' @( 'C:\Windows\ServiceProfiles\LocalService\AppData\Local\Microsoft\NGC', 'C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\NGC', 'C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\NGC' ) | ForEach-Object { Remove-LockedDirectory $_ } Write-Log ' Removing Cloud AP plugin token cache' @( 'C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\CloudAPPlugin', 'C:\Windows\ServiceProfiles\LocalService\AppData\Local\Microsoft\Windows\CloudAPPlugin', 'C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\CloudAPPlugin' ) | ForEach-Object { Remove-LockedDirectory $_ } Write-Log ' Removing Autopilot provisioning cache' @( 'C:\Windows\Provisioning\Autopilot', 'C:\ProgramData\Microsoft\Windows\DeviceManagementProvisioning' ) | ForEach-Object { Remove-LockedDirectory $_ } Write-Log ' Removing identity store / workplace join registry entries' @( 'HKLM:\SOFTWARE\Microsoft\IdentityStore\Cache', 'HKLM:\SOFTWARE\Microsoft\IdentityStore\LogonCache', 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CDJ' ) | ForEach-Object { Remove-RegKey $_ } Remove-LockedDirectory 'C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\IdentityCache' Write-Log ' Removing AAD device identity certificates' $AadCertStores = @('Cert:\LocalMachine\My','Cert:\LocalMachine\Root','Cert:\LocalMachine\CA') foreach ($Store in $AadCertStores) { $Certs = Get-ChildItem $Store -ErrorAction SilentlyContinue | Where-Object { $_.Issuer -match 'MS-Organization-Access|MS-Organization-P2P-Access|Microsoft Intune MDM Device CA|AAD|AzureAD|EnterpriseRegistration' } foreach ($Cert in $Certs) { Remove-Item -Path "$Store\$($Cert.Thumbprint)" -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] Cert [$Store]: $($Cert.Subject) | $($Cert.Thumbprint)" $Script:Removed++ } if (-not $Certs) { Write-Log " [NOT FOUND] No AAD certs in $Store" $Script:NotFound++ } } # ================================================================ Write-Step 'STEP 12 - Remove TS post action, unattend, setup remnants' # ================================================================ Write-Log ' Removing SMSTSPostAction registry value' Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\SMS\Task Sequence' -Name 'SMSTSPostAction' -ErrorAction SilentlyContinue @('C:\Windows\Panther\Unattend.xml','C:\Windows\Panther\unattend') | ForEach-Object { if (Test-Path $_) { Remove-Item -Path $_ -Recurse -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] $_" $Script:Removed++ } else { Write-Log " [NOT FOUND] $_" $Script:NotFound++ } } Remove-ItemProperty -Path 'HKLM:\SYSTEM\Setup' -Name 'CmdLine' -ErrorAction SilentlyContinue Remove-ItemProperty -Path 'HKLM:\SYSTEM\Setup' -Name 'SetupType' -ErrorAction SilentlyContinue Write-Log ' Cleared HKLM:\SYSTEM\Setup CmdLine and SetupType' $TagPath = 'C:\Windows\Setup\Scripts\DisableCMDRequest.TAG' if (Test-Path $TagPath) { Remove-Item -Path $TagPath -Force -ErrorAction SilentlyContinue Write-Log ' [REMOVED] DisableCMDRequest.TAG' $Script:Removed++ } else { Write-Log ' [NOT FOUND] DisableCMDRequest.TAG' $Script:NotFound++ } # ================================================================ Write-Step 'POST-FLIGHT - dsregcmd /status after full cleanup' # ================================================================ $DsregAfter = & dsregcmd.exe /status 2>&1 $DsregAfter | Where-Object { $_ -match 'AzureAdJoined|WorkplaceJoined|DomainJoined|TenantName|DeviceId|MDMUrl|EnterpriseJoined' } | ForEach-Object { Write-Log " $($_.Trim())" } # ================================================================ Write-Step 'CLEANUP SUMMARY' # ================================================================ Write-Log " Items removed : $($Script:Removed)" Write-Log " Items not found : $($Script:NotFound)" Write-Log " Items failed : $($Script:Failed)" # ================================================================ Write-Step 'FINAL STEP - Re-register device in Autopilot with post-cleanup hash' # ================================================================ # GroupTag was written to registry by the dropper during TS execution. $GroupTag = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\BekaertDeslee\Autopilot' -Name 'GroupTag' -ErrorAction SilentlyContinue).GroupTag if (-not $GroupTag) { Write-Log ' [WARNING] GroupTag not found in registry - skipping re-registration. ZTID may not work.' -Level Warning } else { Write-Log " GroupTag: $GroupTag" try { Write-Log ' Authenticating to Microsoft Graph...' $Token = Get-GraphToken Write-Log ' Token acquired.' # Collect hardware info post-cleanup. # The hash captured NOW matches what OOBE will present - this is the definitive ZTID fix. Write-Log ' Collecting hardware info from WMI (post-cleanup)...' $Serial = (Get-WmiObject -Class Win32_BIOS -ErrorAction Stop).SerialNumber.Trim() $HashObj = Get-WmiObject -Namespace 'root/cimv2/mdm/dmmap' -Class 'MDM_DevDetail_Ext01' ` -Filter "InstanceID='Ext' AND ParentID='./DevDetail'" -ErrorAction Stop if (-not $HashObj) { throw 'MDM_DevDetail_Ext01 WMI query returned no results.' } $Hash = $HashObj.DeviceHardwareData Write-Log " Serial : $Serial" Write-Log " Hash len: $($Hash.Length) chars" if ($Hash.Length -lt 100) { throw "Hardware hash too short ($($Hash.Length) chars)" } $Escaped = $Serial -replace "'", "''" $SearchUri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities?`$filter=contains(serialNumber,'$Escaped')" # Find any existing Autopilot records and delete them first. # Registering a fresh record with the post-cleanup hash ensures OOBE # presents the same hash that Autopilot has on file. $Existing = @((Invoke-GraphRequest -Method GET -Uri $SearchUri -Token $Token).value) if ($Existing.Count -gt 0) { Write-Log " Found $($Existing.Count) existing Autopilot record(s) - deleting before fresh re-registration" foreach ($rec in $Existing) { Write-Log " Deleting: id=$($rec.id) | serial=$($rec.serialNumber) | tag='$($rec.groupTag)'" try { Invoke-GraphRequest -Method DELETE ` -Uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities/$($rec.id)" ` -Token $Token | Out-Null Write-Log " [REMOVED] Autopilot record $($rec.id)" } catch { Write-Log " [WARNING] Could not delete Autopilot record $($rec.id): $_" -Level Warning } } Write-Log ' Waiting 15s for deletion to propagate...' Start-Sleep -Seconds 15 } else { Write-Log ' No existing Autopilot record found - registering fresh.' } # Register fresh with post-cleanup hardware hash Write-Log " Registering fresh Autopilot record (GroupTag: $GroupTag)..." $RegBody = @{ serialNumber = $Serial hardwareIdentifier = $Hash groupTag = $GroupTag productKey = '' } # odata.type added as separate step to avoid single-quote collision with here-string $RegBody['@odata.type'] = '#microsoft.graph.importedWindowsAutopilotDeviceIdentity' Invoke-GraphRequest -Method POST ` -Uri 'https://graph.microsoft.com/beta/deviceManagement/importedWindowsAutopilotDeviceIdentities' ` -Token $Token ` -Body $RegBody | Out-Null Write-Log ' [OK] Device queued for Autopilot registration' # Trigger sync try { Invoke-GraphRequest -Method POST ` -Uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotSettings/sync' ` -Token $Token | Out-Null Write-Log ' Autopilot sync triggered.' } catch { Write-Log " [WARNING] Sync trigger failed (non-critical): $_" -Level Warning } # Wait for deploymentProfileAssignmentStatus = assignedInSync (up to 10 min) $Verified = $false for ($i = 1; $i -le 10; $i++) { Write-Log " Waiting 60s for profile assignment (attempt $i/10)..." Start-Sleep -Seconds 60 try { $Token = Get-GraphToken } catch { Write-Log " Token refresh failed: $_" -Level Warning } $CheckDevices = @((Invoke-GraphRequest -Method GET -Uri $SearchUri -Token $Token).value) if ($CheckDevices.Count -gt 0) { $ApStatus = $CheckDevices[0].deploymentProfileAssignmentStatus $ApTag = $CheckDevices[0].groupTag Write-Log " GroupTag=$ApTag | ProfileAssignment=$ApStatus" if ($ApTag -eq $GroupTag -and $ApStatus -in 'assignedInSync','assignedOutOfSync') { Write-Log ' [OK] Profile assigned - proceeding to Intune cleanup and sysprep' -Level Success $Verified = $true break } } else { Write-Log " Device not yet visible in Autopilot (attempt $i)" } } if (-not $Verified) { Write-Log ' [WARNING] Profile not assignedInSync after 10 min - continuing anyway' -Level Warning } # Delete stale Intune managed device record. # Without this the device re-enrolls into the old co-managed record # and inherits ConfigMgr compliance authority instead of Intune-only. Write-Log ' Checking for stale Intune managed device record...' try { $Token = Get-GraphToken $MdmSearchUri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=serialNumber eq '$Escaped'" $MdmDevices = @((Invoke-GraphRequest -Method GET -Uri $MdmSearchUri -Token $Token).value) if ($MdmDevices.Count -gt 0) { foreach ($mdmDev in $MdmDevices) { Write-Log " Deleting Intune managed device: $($mdmDev.deviceName) [$($mdmDev.id)] (state: $($mdmDev.managementState))" Invoke-GraphRequest -Method DELETE ` -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$($mdmDev.id)" ` -Token $Token | Out-Null Write-Log " [REMOVED] Intune managed device: $($mdmDev.deviceName)" } } else { Write-Log ' [NOT FOUND] No stale Intune managed device record.' } } catch { Write-Log " [WARNING] Intune device cleanup failed (non-critical): $_" -Level Warning } } catch { Write-Log " [WARNING] Autopilot re-registration failed: $_ - continuing to sysprep" -Level Warning } } # ================================================================ Write-Step 'SYSPREP' # ================================================================ Write-Log ' Launching: sysprep.exe /oobe /reboot /quiet' $Sysprep = Start-Process ` -FilePath 'C:\Windows\System32\Sysprep\Sysprep.exe' ` -ArgumentList '/oobe /reboot /quiet' ` -NoNewWindow -Wait -PassThru Write-Log " Sysprep exit code: $($Sysprep.ExitCode)" if ($Sysprep.ExitCode -ne 0) { Write-Log ' Sysprep FAILED - dumping Panther log' -Level Warning $PantherLog = 'C:\Windows\System32\Sysprep\Panther\setupact.log' if (Test-Path $PantherLog) { Get-Content $PantherLog -Tail 40 -ErrorAction SilentlyContinue | ForEach-Object { Add-Content -Path $LogPath -Value " [Panther] $_" -ErrorAction SilentlyContinue } } else { Write-Log " Panther log not found at $PantherLog" -Level Warning } } else { Write-Log ' Sysprep completed successfully - rebooting into OOBE' } Write-Log 'PrepareOOBE finished' '@ New-Item -ItemType Directory -Path 'C:\Windows\Temp' -Force -ErrorAction SilentlyContinue | Out-Null New-Item -ItemType File -Path 'C:\Windows\Temp\PrepareOOBE.ps1' -Value $ScriptBlock -Force | Out-Null Write-Output 'PrepareOOBE.ps1 created at C:\Windows\Temp\PrepareOOBE.ps1' |