#Requires -RunAsAdministrator <# .SYNOPSIS Captures endpoint state at three points in the imaging lifecycle so they can be diffed pairwise. .DESCRIPTION Run at three checkpoints: 1) After PPKG enrollment, BEFORE the tech assigns a device category in Intune (the device is enrolled but no category-driven policy/apps have been delivered): .\Capture-LockdownState.ps1 -Stage pre-category 2) After the device category is assigned and the device has synced (apps + policies tied to that category have arrived/started, but lockdown has not yet flipped the user from SupportUser to ShopFloor): .\Capture-LockdownState.ps1 -Stage post-category 3) After lockdown DSC has fully landed (Winlogon DefaultUserName flipped, AssignedAccess kiosk active, SFLD creds populated): .\Capture-LockdownState.ps1 -Stage post-lockdown Output lands under C:\ProgramData\state--\. Copy the whole folder back to the workstation (\\10.9.100.1\image-upload or dump to pxe-images manually) and diff. Diffing tips: pre-category -> post-category : what the category-driven Intune assignments brought in (the SFLD-DSC Win32App + its registry writes + scheduled tasks) post-category -> post-lockdown : what the lockdown DSC profile finished (kiosk shell, autologon flip, AppLocker rules, etc.) .PARAMETER Stage One of 'pre-category', 'post-category', 'post-lockdown'. Controls the output folder name. Legacy 'pre' / 'post' values are still accepted and map to 'pre-category' / 'post-lockdown' respectively. .EXAMPLE .\Capture-LockdownState.ps1 -Stage pre-category #> param( [Parameter(Mandatory = $true)] [ValidateSet('pre-category', 'post-category', 'post-lockdown', 'pre', 'post')] [string]$Stage ) # Map legacy values to new names if ($Stage -eq 'pre') { $Stage = 'pre-category' } if ($Stage -eq 'post') { $Stage = 'post-lockdown' } $ErrorActionPreference = 'Continue' $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $out = "C:\ProgramData\state-$Stage-$stamp" New-Item -ItemType Directory -Path $out -Force | Out-Null function Step($name, [scriptblock]$block) { Write-Host "[$Stage] $name ..." try { & $block } catch { Write-Warning " $name failed: $_" } } # --- MDM policy store: the subkeys that change are the clearest lockdown signal Step "export PolicyManager registry" { reg export 'HKLM\SOFTWARE\Microsoft\PolicyManager\current\device' "$out\PolicyManager.reg" /y | Out-Null } # --- MDM enrollment and provisioning sessions (category-targeted profiles land here) Step "export Enrollments + Provisioning Sessions" { reg export 'HKLM\SOFTWARE\Microsoft\Enrollments' "$out\Enrollments.reg" /y | Out-Null reg export 'HKLM\SOFTWARE\Microsoft\Provisioning\Sessions' "$out\ProvisioningSessions.reg" /y 2>$null | Out-Null reg export 'HKLM\SOFTWARE\Microsoft\Provisioning\OMADM\Accounts' "$out\OMADMAccounts.reg" /y 2>$null | Out-Null } # --- GE-specific state (DSC, credentials, anything under GE\SFLD) Step "export GE\SFLD registry" { reg export 'HKLM\SOFTWARE\GE\SFLD' "$out\SFLD.reg" /y 2>$null | Out-Null reg export 'HKLM\SOFTWARE\WOW6432Node\GE' "$out\GE-WOW6432.reg" /y 2>$null | Out-Null } # --- Intune Management Extension Win32App state (what apps are assigned + detection results) Step "export IME Win32Apps registry" { reg export 'HKLM\SOFTWARE\Microsoft\IntuneManagementExtension' "$out\IME.reg" /y 2>$null | Out-Null } # --- Shell / Winlogon (kiosk replaces Shell value here, autologon values, etc.) Step "export Winlogon" { reg export 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' "$out\Winlogon.reg" /y | Out-Null } # --- AppLocker / software restriction (lockdown commonly adds rules here) Step "export AppLocker + SRP" { reg export 'HKLM\SOFTWARE\Policies\Microsoft\Windows\SrpV2' "$out\AppLocker.reg" /y 2>$null | Out-Null reg export 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers' "$out\SRP.reg" /y 2>$null | Out-Null } # --- WDAC / Code Integrity policies Step "export CI / WDAC" { reg export 'HKLM\SYSTEM\CurrentControlSet\Control\CI' "$out\CI.reg" /y 2>$null | Out-Null if (Test-Path 'C:\Windows\System32\CodeIntegrity\CiPolicies\Active') { Copy-Item 'C:\Windows\System32\CodeIntegrity\CiPolicies\Active\*.cip' "$out\" -Force -EA SilentlyContinue } } # --- Shell Launcher / Assigned Access config (Win11 kiosk feature) Step "export AssignedAccess + ShellLauncher" { reg export 'HKLM\SOFTWARE\Microsoft\Windows\AssignedAccessConfiguration' "$out\AssignedAccess.reg" /y 2>$null | Out-Null reg export 'HKLM\SOFTWARE\Microsoft\ShellLauncher' "$out\ShellLauncher.reg" /y 2>$null | Out-Null } # --- Local users and groups (kiosk accounts get created) Step "snapshot local users and groups" { try { Get-LocalUser | Select Name, Enabled, LastLogon, Description, SID | Export-Csv "$out\LocalUsers.csv" -NoTypeInformation } catch {} try { Get-LocalGroupMember Administrators -EA 0 | Select Name, PrincipalSource, ObjectClass | Export-Csv "$out\Admins.csv" -NoTypeInformation } catch {} } # --- Scheduled tasks (lockdown often registers a monitor/re-enforcer task) Step "snapshot scheduled tasks" { Get-ScheduledTask | Select-Object TaskPath, TaskName, State, @{n='Actions';e={($_.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }) -join '; '}}, @{n='Triggers';e={($_.Triggers | ForEach-Object { $_.CimClass.CimClassName }) -join ','}} | Sort-Object TaskPath, TaskName | Export-Csv "$out\Tasks.csv" -NoTypeInformation } # --- Services Step "snapshot services" { Get-Service | Select Name, DisplayName, Status, StartType | Sort Name | Export-Csv "$out\Services.csv" -NoTypeInformation } # --- Installed apps (64 + 32 bit Uninstall hives) Step "snapshot installed apps" { @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') | ForEach-Object { Get-ItemProperty $_ -EA 0 } | Where-Object DisplayName | Select DisplayName, DisplayVersion, Publisher, InstallDate | Sort DisplayName -Unique | Export-Csv "$out\InstalledApps.csv" -NoTypeInformation } # --- Desktop contents (lockdown strips desktop icons on kiosk) Step "snapshot desktops" { Get-ChildItem -Path 'C:\Users\Public\Desktop' -ErrorAction SilentlyContinue | Select Name, Length, LastWriteTime | Export-Csv "$out\PublicDesktop.csv" -NoTypeInformation Get-ChildItem -Path 'C:\Users\SupportUser\Desktop' -ErrorAction SilentlyContinue | Select Name, Length, LastWriteTime | Export-Csv "$out\SupportUserDesktop.csv" -NoTypeInformation } # --- Relevant logs Step "copy Intune + Enrollment logs" { $imeDir = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs' $enrollDir = 'C:\Logs' if (Test-Path $imeDir) { New-Item "$out\IME-Logs" -ItemType Directory -Force | Out-Null Copy-Item "$imeDir\*.log" "$out\IME-Logs\" -Force -EA SilentlyContinue } if (Test-Path $enrollDir) { New-Item "$out\GE-Logs" -ItemType Directory -Force | Out-Null Copy-Item "$enrollDir\*.log" "$out\GE-Logs\" -Recurse -Force -EA SilentlyContinue } } # --- dsregcmd status (AAD / Entra join state) Step "dsregcmd /status" { dsregcmd /status 2>&1 | Out-File "$out\dsregcmd.txt" } # --- SFLD credential manager YAML (identifies the Intune category) Step "snapshot SFLD CredentialManager dir" { $cmDir = 'C:\ProgramData\SFLD\CredentialManager' if (Test-Path $cmDir) { Get-ChildItem $cmDir -Recurse | Select FullName, Length, LastWriteTime | Export-Csv "$out\SFLD-CredentialManager.csv" -NoTypeInformation } } # --- SFLD DSC payload directory (where the DSC bootstrap drops scripts/creds) Step "snapshot SFLD DSC dir" { $dscDir = 'C:\ProgramData\SFLD\DSC' if (Test-Path $dscDir) { Get-ChildItem $dscDir -Recurse -Force | Select FullName, Length, LastWriteTime | Export-Csv "$out\SFLD-DSC-Files.csv" -NoTypeInformation } } # --- DSCDeployment.log (sits at C:\pc\, not under C:\Logs - written by the # SFLD-DSC Win32App bootstrap. Critical for diagnosing version drift, # missing sastoken.txt, fallback-path failures.) Step "copy DSCDeployment + version artifacts" { foreach ($p in 'C:\pc\DSCDeployment.log','C:\pc\version.txt') { if (Test-Path $p) { Copy-Item $p "$out\" -Force -EA SilentlyContinue } } } # --- IMECache file listing per Win32App content folder. Each subdir is # _; the file list tells us which content # payloads landed correctly (e.g. sastoken.txt presence/absence). # Avoid copying .intunewin blobs (huge, encrypted) - just list. Step "snapshot IMECache file listing" { $imeContent = 'C:\Windows\IMECache' if (Test-Path $imeContent) { Get-ChildItem $imeContent -Recurse -File -Force -EA SilentlyContinue | Select FullName, Length, LastWriteTime | Export-Csv "$out\IMECache-Files.csv" -NoTypeInformation } } # --- BPRT (PPKG bulk enrollment) runtime state. Setup-WCD.ps1 dumps # packageInfo.json + criticalChecks.json here; their values prove # whether PPKG parse + critical checks passed. Step "copy BPRT runtime state" { $bprtDir = 'C:\Logs\BPRT' if (Test-Path $bprtDir) { New-Item "$out\BPRT" -ItemType Directory -Force | Out-Null Copy-Item "$bprtDir\*" "$out\BPRT\" -Recurse -Force -EA SilentlyContinue } } # --- C:\WCDApps state (where Setup-WCD.ps1 deploys helper scripts) Step "snapshot WCDApps dir" { $wcd = 'C:\WCDApps' if (Test-Path $wcd) { Get-ChildItem $wcd -Recurse -File -Force -EA SilentlyContinue | Select FullName, Length, LastWriteTime | Export-Csv "$out\WCDApps-Files.csv" -NoTypeInformation } } # --- Windows Provisioning Diagnostics (Microsoft-side PPKG runtime logs) Step "copy Windows Provisioning Diagnostics" { $pdiag = 'C:\Windows\Provisioning\Diagnostics' if (Test-Path $pdiag) { New-Item "$out\Provisioning-Diagnostics" -ItemType Directory -Force | Out-Null Copy-Item "$pdiag\*" "$out\Provisioning-Diagnostics\" -Recurse -Force -EA SilentlyContinue } } # --- Scheduled task last-run details (need LastRunTime + LastTaskResult, not just State) Step "snapshot scheduled task run history" { Get-ScheduledTask | ForEach-Object { $info = $_ | Get-ScheduledTaskInfo -EA SilentlyContinue [pscustomobject]@{ TaskPath = $_.TaskPath TaskName = $_.TaskName State = $_.State LastRunTime = $info.LastRunTime LastTaskResult = $info.LastTaskResult NextRunTime = $info.NextRunTime NumberOfMissedRuns = $info.NumberOfMissedRuns } } | Sort TaskPath, TaskName | Export-Csv "$out\Tasks-RunHistory.csv" -NoTypeInformation } # --- DeviceManagement event log (outbound MDM sync errors - 429 throttles, # AAD token failures, policy-pull stalls. Not visible in IME log alone.) Step "export DeviceManagement event log" { Get-WinEvent -LogName 'Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider/Admin' ` -MaxEvents 500 -EA SilentlyContinue | Select TimeCreated, Id, LevelDisplayName, Message | Export-Csv "$out\DeviceManagement-Events.csv" -NoTypeInformation } # --- Provisioning event log (PPKG application errors, runtime issues) Step "export Provisioning event log" { Get-WinEvent -LogName 'Microsoft-Windows-Provisioning-Diagnostics-Provider/Admin' ` -MaxEvents 500 -EA SilentlyContinue | Select TimeCreated, Id, LevelDisplayName, Message | Export-Csv "$out\Provisioning-Events.csv" -NoTypeInformation } # --- MDM device certificate (confirms enrollment is healthy at the cert layer) Step "snapshot MDM certificates" { Get-ChildItem Cert:\LocalMachine\My -EA SilentlyContinue | Where-Object { $_.Subject -match 'MS-Organization|MS-Device|Microsoft Intune' -or $_.Issuer -match 'Microsoft Intune' } | Select Subject, Issuer, Thumbprint, NotBefore, NotAfter | Export-Csv "$out\MDM-Certificates.csv" -NoTypeInformation } Write-Host "" Write-Host "Snapshot complete: $out" Write-Host "" Write-Host "Next: copy that folder to the workstation. Easiest:" Write-Host " net use Z: \\10.9.100.1\image-upload /user:pxe-upload pxe /persistent:no" Write-Host " robocopy `"$out`" `"Z:\state-$Stage-$stamp`" /E /NFL /NDL /NJH /NJS" Write-Host " net use Z: /delete /y"