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) # post-reboot DSCInstall.log to finish)
# ============================================================================ # ============================================================================
function Get-LockdownState { function Get-LockdownState {
# Machine-level signals that the kiosk/lockdown baseline has finished # Lockdown is applied by an Intune Remediation script (not the PPKG
# being applied. Both are HKLM/SAM changes pushed by MDM PolicyCSP after # directly). The remediation runs Autologon.exe to configure ShopFloor
# DSCInstall.log finishes, so they land independently of which user is # autologon and writes two IME logs under
# currently logged in. See pre/post state diff 2026-04-15 for rationale. # C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\:
$wl = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' # Autologon_Remediation.log - "Autologon set for ShopFloor user ..."
$defUser = Read-RegValue $wl 'DefaultUserName' # Autologon_Detection.log - "... matches the expected value: 1"
$autoUser = ($defUser -eq 'ShopFloor') #
$adminRenamed = [bool](Get-LocalUser -Name 'SFLDAdmin' -ErrorAction SilentlyContinue) # 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 @{ return @{
AutologonShopfloor = $autoUser RemediationApplied = $remDone
AdminRenamed = $adminRenamed DetectionPassed = $detDone
Complete = ($autoUser -and $adminRenamed) Complete = ($remDone -and $detDone)
} }
} }
@@ -514,89 +537,156 @@ function Build-QRCodeText {
# ============================================================================ # ============================================================================
# Renderer # 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 { function Format-Snapshot {
param($Snap, $LastSync, $NextRetrigger) param($Snap, $LastSync, $NextRetrigger)
function Mk { param([bool]$ok) if ($ok) { '[v]' } else { '[ ]' } }
$lines = @() $lines = @()
$lines += "========================================"
$lines += " Intune Lockdown Progress"
if ($Snap.Function) {
$lines += " Function: $($Snap.Function)"
}
$lines += "========================================"
$lines += "" $lines += ""
$lines += " Phase 1: Identity" $lines += " GE Aerospace -- Shopfloor Device Setup"
$lines += " $(Mk $Snap.Phase1.AzureAdJoined) Azure AD joined" $lines += ""
$lines += " $(Mk $Snap.Phase1.IntuneEnrolled) Intune enrolled" if ($Snap.Function) {
$lines += " $(Mk $Snap.Phase1.EmTaskExists) EnterpriseMgmt sync tasks" $lines += " Category: $($Snap.Function)"
$lines += " $(Mk $Snap.Phase1.PoliciesArriving) Policies arriving" }
$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) { if (-not $skipDsc) {
$lines += "" # Phase 2
$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.
$phase1Done = ($Snap.Phase1.AzureAdJoined -and $Snap.Phase1.IntuneEnrolled) $phase1Done = ($Snap.Phase1.AzureAdJoined -and $Snap.Phase1.IntuneEnrolled)
$phase2Done = ($Snap.Phase2.SfldRoot -and $Snap.Phase2.FunctionOk -and $Snap.Phase2.SasTokenOk) $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) { if ($phase1Done -and -not $phase2Done) {
$lines += " --> ACTION: assign device category in Intune portal" $p2Action = ' >> Assign device category in Intune portal'
$lines += " (main / cmm / displaypcs / waxtrace)"
} }
$lines += ""
$lines += " Phase 3: DSC deployment + install" # Phase 3
$lines += " $(Mk $Snap.Phase3.DeployLogExists) DSCDeployment.log present" $p3Status = Get-PhaseStatus @(
$lines += " $(Mk $Snap.Phase3.DeployComplete) Pre-reboot deployment complete" @{ Ok = $Snap.Phase3.DeployComplete; Failed = $false },
$lines += " $(Mk $Snap.Phase3.InstallLogExists) DSCInstall.log present" @{ Ok = $Snap.Phase3.InstallComplete; Failed = $false }
$lines += " $(Mk $Snap.Phase3.InstallComplete) Post-reboot install complete" )
$lines += "" $lines += $null
$lines += " Phase 4: Custom scripts (auto-discovered)" $p3Index = $lines.Count - 1
if (-not $Snap.Phase4 -or $Snap.Phase4.Count -eq 0) {
$lines += " (no Install-*.log files yet in C:\Logs\SFLD)" # Phase 4
} else { $p4HasFailed = $false
$p4AllDone = $true
$p4AnyStarted = $false
if ($Snap.Phase4 -and $Snap.Phase4.Count -gt 0) {
foreach ($s in $Snap.Phase4) { foreach ($s in $Snap.Phase4) {
$mark = switch ($s.Status) { if ($s.Status -eq 'failed') { $p4HasFailed = $true }
'done' { '[v]' } if ($s.Status -ne 'done') { $p4AllDone = $false }
'running' { '[.]' } if ($s.Status -ne 'pending') { $p4AnyStarted = $true }
'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"
} }
} } else { $p4AllDone = $false }
$lines += "" $p4Status = if ($p4HasFailed) { 'FAILED' }
$lines += " Phase 5: SFLD credentials" elseif ($p4AllDone) { 'COMPLETE' }
$lines += " $(Mk $Snap.Phase5.ConsumeCredsTask) Consume Credentials task scheduled" elseif ($p4AnyStarted) { 'IN PROGRESS' }
$lines += " $(Mk $Snap.Phase5.CredsPopulated) Share creds present in HKLM" else { 'WAITING' }
$lines += "" $lines += $null
$lines += " Phase 6: Lockdown" $p4Index = $lines.Count - 1
$lines += " $(Mk $Snap.Phase6.AutologonShopfloor) Winlogon autologon = ShopFloor"
$lines += " $(Mk $Snap.Phase6.AdminRenamed) Administrator renamed -> SFLDAdmin" # Phase 5
} else { $p5Status = Get-PhaseStatus @(
$lines += "" @{ Ok = $Snap.Phase5.ConsumeCredsTask; Failed = $false },
$lines += " (DSC phases not applicable for $pcType)" @{ 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 += "" $lines += ""
$sinceSync = ((Get-Date) - $LastSync).TotalSeconds $sinceSync = ((Get-Date) - $LastSync).TotalSeconds
$untilNext = ($NextRetrigger - (Get-Date)).TotalSeconds $untilNext = ($NextRetrigger - (Get-Date)).TotalSeconds
$lines += " Sync: triggered $(Format-Age $sinceSync) ago | next re-trigger in $(Format-Age $untilNext)" $lines += " Last sync: $(Format-Age $sinceSync) ago | Next: $(Format-Age $untilNext)"
return $lines
# --- 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 Clear-Host
Write-Host "=== Monitor running - transcript: $transcriptPath ===" -ForegroundColor DarkGray Format-Snapshot -Snap $snap -LastSync $lastSync -NextRetrigger $nextRetrigger
foreach ($l in (Format-Snapshot -Snap $snap -LastSync $lastSync -NextRetrigger $nextRetrigger)) {
Write-Host $l
}
Write-Host "" Write-Host ""
Write-Host $qrText Write-Host $qrText