Add three-stage imaging snapshot tool + runbook

scripts/diagnostics/Capture-LockdownState.ps1 captures Windows endpoint
state at three lifecycle checkpoints so the deltas isolate which phase
delivered (or failed to deliver) each component:
  - pre-category   - PPKG-enrolled, no Intune category yet
  - post-category  - category-driven assignments arrived, pre-lockdown
  - post-lockdown  - kiosk + autologon + AppLocker fully landed

Bumped from the previous 2-stage (pre/post) version. Legacy 'pre'/'post'
aliases preserved.

Captures additions driven by the SFLD-DSC v2.0.2 post-mortem:
  - IMECache file listing (catches missing sastoken.txt)
  - DSCDeployment.log + version.txt copied from C:\pc\
  - SFLD\DSC payload listing
  - C:\Logs\BPRT\ runtime state (criticalChecks.json, packageInfo.json)
  - C:\WCDApps\ deploy verification
  - Windows\Provisioning\Diagnostics copy
  - Tasks-RunHistory.csv with LastRunTime + LastTaskResult per task
  - DeviceManagement-Events.csv (MDM 429s, AAD token failures)
  - Provisioning-Events.csv (PPKG runtime errors)
  - MDM-Certificates.csv (enrollment cert health)

scripts/diagnostics/snapshot-runbook.txt: step-by-step ops guide
covering when to fire each stage, where output lands, how to ship it
back via image-upload share, and which files to compare first when
diffing.
This commit is contained in:
cproudlock
2026-05-01 08:53:52 -04:00
parent a72db8af5e
commit 1ae5bdce57
2 changed files with 417 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
#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-<stage>-<timestamp>\.
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
# <AppGUID>_<contentVersion>; 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"