From db55bd772a5e5d737c67b60139ea803f3f3e63bf Mon Sep 17 00:00:00 2001 From: cproudlock Date: Thu, 16 Apr 2026 07:35:22 -0400 Subject: [PATCH] sync_intune: professional UI, IME-based lockdown detection UI overhaul: Replaced the 30+ line checkbox-per-sub-item view with a clean 6-line phase summary styled for GE Aerospace branding. Each phase shows one colored status tag: [COMPLETE] green, [IN PROGRESS] cyan, [WAITING] gray, [FAILED] red. Action hint for Phase 2 (device category assignment) in yellow. QR code + Device ID below. Phase 6 lockdown detection: Replaced DefaultUserName + admin-rename checks (which pass at PPKG time, way too early) with Intune Remediation log artifacts: - Autologon_Remediation.log: "Autologon set for ShopFloor" - Autologon_Detection.log: "matches the expected value: 1" These only exist after the Intune Remediation cycle actually fires post-enrollment, making Phase 6 a true end-of-chain signal. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Shopfloor/lib/Monitor-IntuneProgress.ps1 | 247 ++++++++++++------ 1 file changed, 167 insertions(+), 80 deletions(-) diff --git a/playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1 b/playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1 index b51a408..8fd49e8 100644 --- a/playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1 @@ -300,18 +300,41 @@ function Get-CustomScriptStatuses { # post-reboot DSCInstall.log to finish) # ============================================================================ function Get-LockdownState { - # Machine-level signals that the kiosk/lockdown baseline has finished - # being applied. Both are HKLM/SAM changes pushed by MDM PolicyCSP after - # DSCInstall.log finishes, so they land independently of which user is - # currently logged in. See pre/post state diff 2026-04-15 for rationale. - $wl = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' - $defUser = Read-RegValue $wl 'DefaultUserName' - $autoUser = ($defUser -eq 'ShopFloor') - $adminRenamed = [bool](Get-LocalUser -Name 'SFLDAdmin' -ErrorAction SilentlyContinue) + # Lockdown is applied by an Intune Remediation script (not the PPKG + # directly). The remediation runs Autologon.exe to configure ShopFloor + # autologon and writes two IME logs under + # C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\: + # Autologon_Remediation.log - "Autologon set for ShopFloor user ..." + # Autologon_Detection.log - "... matches the expected value: 1" + # + # These are the TRUE end-of-chain signals. The DefaultUserName flip and + # admin rename that we previously checked land at PPKG time (too early); + # the IME logs only appear after Intune enrollment + category + DSC + + # Remediation cycle, which is the actual lockdown completion. + $imeLogs = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs' + + $remLog = Join-Path $imeLogs 'Autologon_Remediation.log' + $remDone = $false + if (Test-Path $remLog) { + try { + $content = Get-Content $remLog -Raw -ErrorAction Stop + $remDone = ($content -match 'Autologon set for ShopFloor') + } catch {} + } + + $detLog = Join-Path $imeLogs 'Autologon_Detection.log' + $detDone = $false + if (Test-Path $detLog) { + try { + $content = Get-Content $detLog -Raw -ErrorAction Stop + $detDone = ($content -match 'matches the expected value:\s*1') + } catch {} + } + return @{ - AutologonShopfloor = $autoUser - AdminRenamed = $adminRenamed - Complete = ($autoUser -and $adminRenamed) + RemediationApplied = $remDone + DetectionPassed = $detDone + Complete = ($remDone -and $detDone) } } @@ -514,89 +537,156 @@ function Build-QRCodeText { # ============================================================================ # Renderer # ============================================================================ +function Get-PhaseStatus { + param([hashtable[]]$Checks) + $total = $Checks.Count + $passed = ($Checks | Where-Object { $_.Ok }).Count + $failed = ($Checks | Where-Object { $_.Failed }).Count + if ($failed -gt 0) { return 'FAILED' } + if ($passed -eq $total) { return 'COMPLETE' } + if ($passed -gt 0) { return 'IN PROGRESS' } + return 'WAITING' +} + +function Format-StatusTag { + param([string]$Status) + switch ($Status) { + 'COMPLETE' { Write-Host ('[COMPLETE]'.PadLeft(14)) -ForegroundColor Green -NoNewline } + 'IN PROGRESS' { Write-Host ('[IN PROGRESS]'.PadLeft(14)) -ForegroundColor Cyan -NoNewline } + 'WAITING' { Write-Host ('[WAITING]'.PadLeft(14)) -ForegroundColor DarkGray -NoNewline } + 'FAILED' { Write-Host ('[FAILED]'.PadLeft(14)) -ForegroundColor Red -NoNewline } + default { Write-Host ('[UNKNOWN]'.PadLeft(14)) -ForegroundColor Yellow -NoNewline } + } +} + function Format-Snapshot { param($Snap, $LastSync, $NextRetrigger) - function Mk { param([bool]$ok) if ($ok) { '[v]' } else { '[ ]' } } - $lines = @() - $lines += "========================================" - $lines += " Intune Lockdown Progress" - if ($Snap.Function) { - $lines += " Function: $($Snap.Function)" - } - $lines += "========================================" $lines += "" - $lines += " Phase 1: Identity" - $lines += " $(Mk $Snap.Phase1.AzureAdJoined) Azure AD joined" - $lines += " $(Mk $Snap.Phase1.IntuneEnrolled) Intune enrolled" - $lines += " $(Mk $Snap.Phase1.EmTaskExists) EnterpriseMgmt sync tasks" - $lines += " $(Mk $Snap.Phase1.PoliciesArriving) Policies arriving" + $lines += " GE Aerospace -- Shopfloor Device Setup" + $lines += "" + if ($Snap.Function) { + $lines += " Category: $($Snap.Function)" + } + $lines += " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + $lines += "" + $lines += " ============================================" + + # Phase 1 + $p1Status = Get-PhaseStatus @( + @{ Ok = $Snap.Phase1.AzureAdJoined; Failed = $false }, + @{ Ok = $Snap.Phase1.IntuneEnrolled; Failed = $false }, + @{ Ok = $Snap.Phase1.EmTaskExists; Failed = $false }, + @{ Ok = $Snap.Phase1.PoliciesArriving; Failed = $false } + ) + $lines += $null # placeholder - rendered with color below + $p1Index = $lines.Count - 1 if (-not $skipDsc) { - $lines += "" - $lines += " Phase 2: SFLD configuration" - $lines += " $(Mk $Snap.Phase2.SfldRoot) SFLD reg key" - $lines += " $(Mk $Snap.Phase2.FunctionOk) Function set" - $lines += " $(Mk $Snap.Phase2.SasTokenOk) DSC SAS token configured" - - # Phase-1-done-but-Phase-2-stuck is the classic "tech needs to go - # set device category in Intune portal" state. Surface it loud - # rather than leaving the user staring at empty checkboxes. + # Phase 2 $phase1Done = ($Snap.Phase1.AzureAdJoined -and $Snap.Phase1.IntuneEnrolled) $phase2Done = ($Snap.Phase2.SfldRoot -and $Snap.Phase2.FunctionOk -and $Snap.Phase2.SasTokenOk) + $p2Status = Get-PhaseStatus @( + @{ Ok = $Snap.Phase2.SfldRoot; Failed = $false }, + @{ Ok = $Snap.Phase2.FunctionOk; Failed = $false }, + @{ Ok = $Snap.Phase2.SasTokenOk; Failed = $false } + ) + $lines += $null + $p2Index = $lines.Count - 1 + + $p2Action = $null if ($phase1Done -and -not $phase2Done) { - $lines += " --> ACTION: assign device category in Intune portal" - $lines += " (main / cmm / displaypcs / waxtrace)" + $p2Action = ' >> Assign device category in Intune portal' } - $lines += "" - $lines += " Phase 3: DSC deployment + install" - $lines += " $(Mk $Snap.Phase3.DeployLogExists) DSCDeployment.log present" - $lines += " $(Mk $Snap.Phase3.DeployComplete) Pre-reboot deployment complete" - $lines += " $(Mk $Snap.Phase3.InstallLogExists) DSCInstall.log present" - $lines += " $(Mk $Snap.Phase3.InstallComplete) Post-reboot install complete" - $lines += "" - $lines += " Phase 4: Custom scripts (auto-discovered)" - if (-not $Snap.Phase4 -or $Snap.Phase4.Count -eq 0) { - $lines += " (no Install-*.log files yet in C:\Logs\SFLD)" - } else { + + # Phase 3 + $p3Status = Get-PhaseStatus @( + @{ Ok = $Snap.Phase3.DeployComplete; Failed = $false }, + @{ Ok = $Snap.Phase3.InstallComplete; Failed = $false } + ) + $lines += $null + $p3Index = $lines.Count - 1 + + # Phase 4 + $p4HasFailed = $false + $p4AllDone = $true + $p4AnyStarted = $false + if ($Snap.Phase4 -and $Snap.Phase4.Count -gt 0) { foreach ($s in $Snap.Phase4) { - $mark = switch ($s.Status) { - 'done' { '[v]' } - 'running' { '[.]' } - 'failed' { '[!]' } - 'pending' { '[ ]' } - default { '[?]' } - } - $detail = switch ($s.Status) { - 'done' { "done $(Format-Age $s.Age) ago" } - 'running' { "running, last update $(Format-Age $s.Age) ago" } - 'failed' { "FAILED $(Format-Age $s.Age) ago" } - 'pending' { "pending" } - default { "" } - } - $name = $s.Name.PadRight(22) - $lines += " $mark $name $detail" + if ($s.Status -eq 'failed') { $p4HasFailed = $true } + if ($s.Status -ne 'done') { $p4AllDone = $false } + if ($s.Status -ne 'pending') { $p4AnyStarted = $true } } - } - $lines += "" - $lines += " Phase 5: SFLD credentials" - $lines += " $(Mk $Snap.Phase5.ConsumeCredsTask) Consume Credentials task scheduled" - $lines += " $(Mk $Snap.Phase5.CredsPopulated) Share creds present in HKLM" - $lines += "" - $lines += " Phase 6: Lockdown" - $lines += " $(Mk $Snap.Phase6.AutologonShopfloor) Winlogon autologon = ShopFloor" - $lines += " $(Mk $Snap.Phase6.AdminRenamed) Administrator renamed -> SFLDAdmin" - } else { - $lines += "" - $lines += " (DSC phases not applicable for $pcType)" + } else { $p4AllDone = $false } + $p4Status = if ($p4HasFailed) { 'FAILED' } + elseif ($p4AllDone) { 'COMPLETE' } + elseif ($p4AnyStarted) { 'IN PROGRESS' } + else { 'WAITING' } + $lines += $null + $p4Index = $lines.Count - 1 + + # Phase 5 + $p5Status = Get-PhaseStatus @( + @{ Ok = $Snap.Phase5.ConsumeCredsTask; Failed = $false }, + @{ Ok = $Snap.Phase5.CredsPopulated; Failed = $false } + ) + $lines += $null + $p5Index = $lines.Count - 1 + + # Phase 6 + $p6Status = Get-PhaseStatus @( + @{ Ok = $Snap.Phase6.RemediationApplied; Failed = $false }, + @{ Ok = $Snap.Phase6.DetectionPassed; Failed = $false } + ) + $lines += $null + $p6Index = $lines.Count - 1 } + $lines += " ============================================" $lines += "" $sinceSync = ((Get-Date) - $LastSync).TotalSeconds $untilNext = ($NextRetrigger - (Get-Date)).TotalSeconds - $lines += " Sync: triggered $(Format-Age $sinceSync) ago | next re-trigger in $(Format-Age $untilNext)" - return $lines + $lines += " Last sync: $(Format-Age $sinceSync) ago | Next: $(Format-Age $untilNext)" + + # --- Render with color --- + # Lines are printed manually so phase rows get colored status tags. + # $lines entries that are $null are phase-row placeholders rendered + # inline with Format-StatusTag. + for ($i = 0; $i -lt $lines.Count; $i++) { + if ($null -eq $lines[$i]) { + # Phase row: print label then colored tag + if ($i -eq $p1Index) { + Write-Host ' 1. Intune Registration ' -NoNewline; Format-StatusTag $p1Status; Write-Host '' + } + elseif (-not $skipDsc -and $i -eq $p2Index) { + Write-Host ' 2. Device Configuration ' -NoNewline; Format-StatusTag $p2Status; Write-Host '' + if ($p2Action) { Write-Host $p2Action -ForegroundColor Yellow } + } + elseif (-not $skipDsc -and $i -eq $p3Index) { + Write-Host ' 3. Software Deployment ' -NoNewline; Format-StatusTag $p3Status; Write-Host '' + } + elseif (-not $skipDsc -and $i -eq $p4Index) { + Write-Host ' 4. Application Install ' -NoNewline; Format-StatusTag $p4Status; Write-Host '' + } + elseif (-not $skipDsc -and $i -eq $p5Index) { + Write-Host ' 5. Credential Setup ' -NoNewline; Format-StatusTag $p5Status; Write-Host '' + } + elseif (-not $skipDsc -and $i -eq $p6Index) { + Write-Host ' 6. Lockdown ' -NoNewline; Format-StatusTag $p6Status; Write-Host '' + } + } else { + Write-Host $lines[$i] + } + } + + if ($skipDsc) { + Write-Host '' + Write-Host " (Phases 2-6 not applicable for $pcType)" -ForegroundColor DarkGray + } + + # Return empty - we rendered directly via Write-Host for color support. + return @() } # ============================================================================ @@ -792,10 +882,7 @@ try { } Clear-Host - Write-Host "=== Monitor running - transcript: $transcriptPath ===" -ForegroundColor DarkGray - foreach ($l in (Format-Snapshot -Snap $snap -LastSync $lastSync -NextRetrigger $nextRetrigger)) { - Write-Host $l - } + Format-Snapshot -Snap $snap -LastSync $lastSync -NextRetrigger $nextRetrigger Write-Host "" Write-Host $qrText