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) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-04-16 07:35:22 -04:00
parent a4de11814d
commit db55bd772a

View File

@@ -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