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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user