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'