diff --git a/scripts/diagnostics/Capture-LockdownState.ps1 b/scripts/diagnostics/Capture-LockdownState.ps1 new file mode 100644 index 0000000..d03d3e9 --- /dev/null +++ b/scripts/diagnostics/Capture-LockdownState.ps1 @@ -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--\. + 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" diff --git a/scripts/diagnostics/snapshot-runbook.txt b/scripts/diagnostics/snapshot-runbook.txt new file mode 100644 index 0000000..16c19ef --- /dev/null +++ b/scripts/diagnostics/snapshot-runbook.txt @@ -0,0 +1,121 @@ +SFLD imaging-lifecycle snapshot runbook +======================================== + +Run all three snapshots on the imaged device (elevated PowerShell). Each +captures registry, files, logs, scheduled-task state, and event logs at a +distinct lifecycle checkpoint so the deltas between them isolate which +phase delivered (or failed to deliver) each component. + +Prereq: device is in supportuser auto-logon, just finished PPKG bulk +enrollment, and is enrolled to Intune but no device category assigned yet. + +---------------------------------------- +0. Map share + stage script (run once, at the start) +---------------------------------------- + + net use Z: \\10.9.100.1\image-upload /user:pxe-upload pxe /persistent:no + Copy-Item Z:\Capture-LockdownState.ps1 C:\Windows\Temp\ + Set-ExecutionPolicy -Scope Process Bypass -Force + +---------------------------------------- +1. Snapshot BEFORE assigning device category +---------------------------------------- + +State: + - PPKG ran, enrolled to Intune + - Device sitting in SupportUser, no category assigned in portal yet + - Win32Apps + DSC profiles tied to category have NOT delivered + + C:\Windows\Temp\Capture-LockdownState.ps1 -Stage pre-category + +Output: C:\ProgramData\state-pre-category-\ + +---------------------------------------- +2. Assign device category in Intune portal +---------------------------------------- + + - Intune portal -> Devices -> Windows -> [this device] -> Properties + - Set Device category to MAIN (or whichever is correct) + - Wait ~5-10 min for sync (or force sync via Settings -> Accounts -> + Access work or school -> Info -> Sync) + +---------------------------------------- +3. Snapshot AFTER category, BEFORE lockdown +---------------------------------------- + +State: + - Category lands, dynamic-group membership evaluates + - SFLD-DSC Win32App (or whatever the category-driven config is) has had + a chance to download, install, write registry, schedule its task + - Lockdown has NOT yet flipped Winlogon DefaultUserName from SupportUser + to ShopFloor (i.e., still in tech / setup mode) + + C:\Windows\Temp\Capture-LockdownState.ps1 -Stage post-category + +Output: C:\ProgramData\state-post-category-\ + +---------------------------------------- +4. Wait for lockdown to finish landing +---------------------------------------- + +Watch for the two terminal signals (per Monitor-IntuneProgress.ps1 +notes): + - HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon + DefaultUserName flips from "SupportUser" to "ShopFloor" + - AssignedAccess kiosk profile becomes active + +---------------------------------------- +5. Snapshot AFTER lockdown +---------------------------------------- + + C:\Windows\Temp\Capture-LockdownState.ps1 -Stage post-lockdown + +Output: C:\ProgramData\state-post-lockdown-\ + +---------------------------------------- +6. Copy all three snapshots back to PXE +---------------------------------------- + + Get-ChildItem 'C:\ProgramData\state-*' -Directory | + Where-Object Name -match '^state-(pre-category|post-category|post-lockdown)-' | + ForEach-Object { + robocopy $_.FullName "Z:\$($_.Name)" /E /NFL /NDL /NJH /NJS + } + net use Z: /delete /y + +The three folders land at \\10.9.100.1\image-upload\state-*-\. +On the workstation: pull from /home/pxe/image-upload/ on the PXE server +(scp or local mount) and diff against any prior baseline (e.g. the +4/15 v1.3.1 working snapshot at pxe-images/state-post-lockdown-20260415-154705/). + +---------------------------------------- +What each diff reveals +---------------------------------------- + +pre-category -> post-category + - Which Win32Apps Intune assigned via the category + - Whether SFLD-DSC bootstrap actually ran (DSCDeployment.log, + HKLM:\SOFTWARE\GE\SFLD\Credentials\baseVersion) + - Whether sastoken.txt was present in the IMECache (IMECache-Files.csv) + - Scheduled task SFLD-ApplyDSCConfig - was it created? Did it run? + What was its last result code? (Tasks-RunHistory.csv) + - Outbound MDM events: 429 throttles, AAD failures + (DeviceManagement-Events.csv) + +post-category -> post-lockdown + - Lockdown DSC delta: AssignedAccess kiosk config, AppLocker rules, + Winlogon flip, autologon change + - Final registry state for HKLM:\SOFTWARE\GE\SFLD\Credentials\* + + HKLM:\SOFTWARE\GE\SFLD\DSC (Site, Environment, Function, SasToken) + - Final PolicyManager state (which Intune profiles fully landed) + +---------------------------------------- +Key files to look at first when comparing +---------------------------------------- + +SFLD.reg -> creds + DSC values landed? +IMECache-Files.csv -> sastoken.txt present in Win32App content? +DSCDeployment.log -> bootstrap version + warnings +Tasks-RunHistory.csv -> SFLD-ApplyDSCConfig LastRunTime + LastTaskResult +DeviceManagement-Events.csv -> 429s, AAD token failures, sync stalls +GE-WOW6432.reg -> baseVersion (1.3.1 = working, 2.0.2 = broken)