sweep: pre-existing drift + matrix UDC entry + ignore 142MB EXE

Bundles drift left uncommitted from prior sessions and the UDC matrix
verify entry added today.

Drift items (all per session-progress.md, completed in earlier sessions
but never staged):

- playbook/check-bios.cmd (deleted, moved to BIOS/check-bios.cmd)
- playbook/migrate-to-wifi.ps1 (made no-op 2026-04-24 after the dnsmasq
  no-gateway fix removed the wired-NIC race that motivated it)
- playbook/preinstall/oracle/Install-Oracle11r2.cmd (post-OUI .ora copy
  added 2026-04-24)
- playbook/preinstall/oracle/tnsnames.ora (live tnsnames, 469 KB,
  deployed alongside the wrapper 2026-04-24)
- playbook/pxe_server_setup.yml (dnsmasq dhcp-option=3,6 commented,
  Oracle .ora deploy task added 2026-04-24)
- playbook/shopfloor-setup/BIOS/{check-bios.cmd, models.txt} (BIOS
  detection refinements)
- playbook/shopfloor-setup/Shopfloor/Force-Lockdown.bat
- playbook/shopfloor-setup/Shopfloor/Monitor-IntuneProgress.ps1
- playbook/shopfloor-setup/Shopfloor/SetShopfloorAutoLogon.bat (new)
- playbook/shopfloor-setup/Shopfloor/09-Install-PrinterInstallerMap.ps1
  (new, places PrinterInstallerMap.exe + Public Desktop shortcut at
  imaging time; manifest entry self-heals on tamper)
- playbook/shopfloor-setup/Shopfloor/lib/Show-IntuneDeviceQR.ps1 (new,
  standalone QR rendering for site that wanted just that piece)
- playbook/shopfloor-setup/gea-shopfloor-collections/{Install-eMxInfo.cmd.template,
  Restore-UDCData.ps1} (these were uncommitted in pre-rename Standard/;
  git mv didn't catch them because they were untracked at the time)
- docs/shopfloor-machine-imaging-guide.md (operator-facing how-to)

Matrix:
- common.test/matrix.json: add UDC verify entry to gea-shopfloor-collections
  row. Surfaces UDC silent-install issue (item H pending) instead of
  letting it pass silently.

.gitignore:
- PrinterInstallerMap.exe (142 MB) excluded. Track via LFS or stage on
  PXE server only - too big for regular git history. Untouched on disk
  so existing local copy still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-05-04 08:49:43 -04:00
parent 64169819b3
commit ce3fbf5a28
17 changed files with 13413 additions and 294 deletions

View File

@@ -303,34 +303,91 @@ function Get-LockdownState {
# 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.
# IME logs are append-only - they never rotate or clear. Earlier logic
# used `-match` against the whole raw file, which stayed true forever
# once success was ever recorded, even if the policy later changed and
# the current state drifted. The fix below:
# (a) Walks lines from END to find the MOST RECENT outcome per log
# (latest attempt wins, stale successes are ignored)
# (b) Cross-checks Winlogon registry for ground truth - catches the
# case where IME logs say success but the reg state got rolled
# back. All three must agree for Complete to be true.
$imeLogs = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs'
# --- Remediation: scan from tail for the LATEST result line
$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')
$lines = Get-Content $remLog -ErrorAction Stop
for ($i = $lines.Count - 1; $i -ge 0; $i--) {
$ln = $lines[$i]
if ($ln -match 'Autologon set for ShopFloor') {
$remDone = $true
break
}
if ($ln -match 'Autologon could not be set|Remediation failed|FATAL|Exception') {
$remDone = $false
break
}
}
} catch {}
}
# --- Detection: scan from tail for the LATEST "matches the expected value: <N>"
$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')
$lines = Get-Content $detLog -ErrorAction Stop
for ($i = $lines.Count - 1; $i -ge 0; $i--) {
if ($lines[$i] -match 'matches the expected value:\s*(\d+)') {
$detDone = ($matches[1] -eq '1')
break
}
}
} catch {}
}
# --- Ground truth: does Winlogon actually reflect ShopFloor autologon?
# This catches the case where IME logs report historical success but the
# reg state has since been changed back (manual tech intervention, policy
# rollback, GPO override, etc).
$registryMatches = $false
try {
$wl = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -ErrorAction Stop
$registryMatches = (
[string]$wl.AutoAdminLogon -eq '1' -and
$wl.DefaultUserName -and
$wl.DefaultUserName -like 'ShopFloor*'
)
} catch {}
# --- Manual override marker written by Force-Lockdown.bat.
# When a tech runs sfld_autologon.ps1 directly (vendor-documented escape
# hatch when the Intune push hasn't applied within ~30 minutes), only
# Winlogon flips - the IME Remediation log never gets a new success
# entry because Detection now passes and Remediation isn't invoked.
# Without this marker, the strict (rem AND det AND reg) gate stays
# false forever even though the device is fully locked down.
$forceMarker = 'C:\Enrollment\force-lockdown-applied.txt'
$forceApplied = (Test-Path -LiteralPath $forceMarker)
return @{
RemediationApplied = $remDone
DetectionPassed = $detDone
Complete = ($remDone -and $detDone)
RegistryMatches = $registryMatches
ForceApplied = $forceApplied
# Two paths to Complete:
# 1. Normal Intune-driven: all three signals present (closes the
# append-only-log false-positive AND rollback-without-log case).
# 2. Manual escape hatch: marker file + ground-truth registry
# match. Marker is the audit-trail substitute for the missing
# Remediation log entry; registry is still the gate.
Complete = (
($remDone -and $detDone -and $registryMatches) -or
($forceApplied -and $registryMatches)
)
}
}
@@ -580,11 +637,21 @@ function Format-Snapshot {
@{ Ok = $Snap.Phase1.PoliciesArriving; Failed = $false }
)
# Phase 6 / Lockdown (shared by both flows, rendered last)
$p6Status = Get-PhaseStatus @(
@{ Ok = $Snap.Phase6.RemediationApplied; Failed = $false },
@{ Ok = $Snap.Phase6.DetectionPassed; Failed = $false }
)
# Phase 6 / Lockdown (shared by both flows, rendered last).
# RegistryMatches is the ground-truth check - if it flips to false after a
# historical success, something rolled back Winlogon and lockdown has drifted.
# If the manual Force-Lockdown marker is present, treat the row as COMPLETE
# to match the Get-LockdownState gate (avoids the table reading IN PROGRESS
# while the script is about to advance to setup-complete on this same tick).
if ($Snap.Phase6.Complete) {
$p6Status = 'COMPLETE'
} else {
$p6Status = Get-PhaseStatus @(
@{ Ok = $Snap.Phase6.RemediationApplied; Failed = $false },
@{ Ok = $Snap.Phase6.DetectionPassed; Failed = $false },
@{ Ok = $Snap.Phase6.RegistryMatches; Failed = $false }
)
}
if (-not $skipDsc) {
# ---- Standard / CMM / etc. (DSC flow) ----
@@ -600,24 +667,16 @@ function Format-Snapshot {
@{ Ok = $Snap.Phase3.DeployComplete; Failed = $false },
@{ Ok = $Snap.Phase3.InstallComplete; Failed = $false }
)
$p4HasFailed = $false; $p4AllDone = $true; $p4AnyStarted = $false
$p4HasScripts = ($Snap.Phase4 -and $Snap.Phase4.Count -gt 0)
if ($p4HasScripts) {
foreach ($s in $Snap.Phase4) {
if ($s.Status -eq 'failed') { $p4HasFailed = $true }
if ($s.Status -ne 'done') { $p4AllDone = $false }
if ($s.Status -ne 'pending') { $p4AnyStarted = $true }
}
} else {
# No scripts discovered. If DSC install is already complete,
# there are simply no custom scripts for this image type --
# that's COMPLETE, not WAITING.
$p4AllDone = $Snap.Phase3.InstallComplete
}
$p4Status = if ($p4HasFailed) { 'FAILED' } elseif ($p4AllDone) { 'COMPLETE' } elseif ($p4AnyStarted) { 'IN PROGRESS' } else { 'WAITING' }
# Phase 4 (per-script Application Install) was retired 2026-04-28:
# apps now deploy via the GE-Enforce manifest engine off
# \\tsgwp00525\...\machineapps\, gated on AESFMA WiFi + S: mount
# which both happen post-reboot. The monitor's responsibility ends
# at "DSC bootstrap + creds + lockdown landed, time to reboot."
# The manifest engine takes over from there on the next ShopFloor
# logon.
$p5Done = ($Snap.Phase5.ConsumeCredsTask -and $Snap.Phase5.CredsPopulated)
$p5Status = Get-PhaseStatus @(
$p4Done = ($Snap.Phase5.ConsumeCredsTask -and $Snap.Phase5.CredsPopulated)
$p4Status = Get-PhaseStatus @(
@{ Ok = $Snap.Phase5.ConsumeCredsTask; Failed = $false },
@{ Ok = $Snap.Phase5.CredsPopulated; Failed = $false }
)
@@ -629,12 +688,11 @@ function Format-Snapshot {
}
Write-Host ' 2. Device Configuration ' -NoNewline; Format-StatusTag $p2Status; Write-Host ''
Write-Host ' 3. Software Deployment ' -NoNewline; Format-StatusTag $p3Status; Write-Host ''
Write-Host ' 4. Application Install ' -NoNewline; Format-StatusTag $p4Status; Write-Host ''
Write-Host ' 5. Credential Setup ' -NoNewline; Format-StatusTag $p5Status; Write-Host ''
if ($p5Done -and $p6Status -ne 'COMPLETE') {
Write-Host ' 4. Credential Setup ' -NoNewline; Format-StatusTag $p4Status; Write-Host ''
if ($p4Done -and $p6Status -ne 'COMPLETE') {
Write-Host ' >> Initiate ARTS Lockdown request' -ForegroundColor Yellow
}
Write-Host ' 6. Lockdown ' -NoNewline; Format-StatusTag $p6Status; Write-Host ''
Write-Host ' 5. Lockdown ' -NoNewline; Format-StatusTag $p6Status; Write-Host ''
} else {
# ---- Display (no DSC, no credentials) ----
# Phases: 1 Registration -> 2 Device Configuration -> 3 Lockdown
@@ -863,17 +921,21 @@ try {
Write-Host ""
Write-Host $qrText
# Final state: every phase landed. The gate is intentionally strict
# because each piece is needed for the device to function:
# - DscInstallComplete: device-config.yaml apps + custom scripts ran
# - CredsPopulated: SFLD share creds in HKLM (Machine-Enforce,
# Acrobat-Enforce, CMM-Enforce all need these)
# - LockdownComplete: kiosk policy baseline + Winlogon flipped to
# ShopFloor autologon + admin renamed
# Final state: every phase landed. Apps are no longer in the gate -
# they deploy via the GE-Enforce manifest engine post-reboot, gated
# on AESFMA WiFi + S: drive mount which only happen after we hand
# off to the ShopFloor autologon session. So this gate is now:
# - CredsPopulated: SFLD share creds in HKLM (Machine-Enforce,
# Acrobat-Enforce, CMM-Enforce, manifest
# engine all need these)
# - LockdownComplete: kiosk policy baseline + Winlogon flipped to
# ShopFloor autologon + admin renamed
# DscInstallComplete used to be required, but device-config.yaml
# is becoming a no-op as apps migrate to the manifest engine; a
# 403 on the YAML download (e.g. expired SAS token) shouldn't
# block the imaging workflow if creds + lockdown are otherwise OK.
# Display PCs skip this whole branch via $skipDsc above.
if ($snap.DscInstallComplete -and
$snap.Phase5.CredsPopulated -and
$snap.LockdownComplete) {
if ($snap.Phase5.CredsPopulated -and $snap.LockdownComplete) {
Invoke-SetupComplete
}
@@ -912,8 +974,7 @@ try {
# Intune Remediation script mid-execution and delay lockdown.
$waitingForLockdownOnly = $false
if (-not $skipDsc) {
$waitingForLockdownOnly = ($snap.DscInstallComplete -and
$snap.Phase5.CredsPopulated -and
$waitingForLockdownOnly = ($snap.Phase5.CredsPopulated -and
-not $snap.LockdownComplete)
} else {
$waitingForLockdownOnly = ($snap.Phase1.AzureAdJoined -and

View File

@@ -0,0 +1,207 @@
# Show-IntuneDeviceQR.ps1 - Standalone version of the QR-code panel from
# Monitor-IntuneProgress.ps1, extracted for use at sites that only need the
# device-ID display (no DSC monitoring, no Intune sync triggers, no reboot
# orchestration).
#
# Polls dsregcmd /status every 15s until the device reports an Azure AD
# DeviceId, then renders that ID as a half-block QR code in the console
# along with the literal text. Stays open until the user presses a key.
#
# REQUIREMENTS
# - Windows PowerShell 5.1 (or PS 7) with .NET Framework available
# - QRCoder.dll alongside this script (or at one of the fallback paths
# below). Single ~75 KB native dependency, no internet needed.
#
# USAGE
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File Show-IntuneDeviceQR.ps1
#
# Or double-click via a one-line .bat:
# powershell.exe -NoProfile -ExecutionPolicy Bypass -NoExit -File "%~dp0Show-IntuneDeviceQR.ps1"
[CmdletBinding()]
param(
[int]$PollSeconds = 15,
[string]$QRCoderDllPath = ''
)
$ErrorActionPreference = 'Continue'
# ----------------------------------------------------------------------------
# Locate QRCoder.dll. Search order:
# 1. -QRCoderDllPath if explicitly passed
# 2. Next to this script ($PSScriptRoot\QRCoder.dll)
# 3. Current working directory
# 4. WJ canonical staging path (kept for cross-site portability)
# ----------------------------------------------------------------------------
function Resolve-QRCoderDll {
param([string]$Override)
$candidates = @()
if ($Override) { $candidates += $Override }
if ($PSScriptRoot) { $candidates += (Join-Path $PSScriptRoot 'QRCoder.dll') }
$candidates += (Join-Path (Get-Location) 'QRCoder.dll')
$candidates += 'C:\Enrollment\shopfloor-setup\Shopfloor\QRCoder.dll'
foreach ($c in $candidates) {
if ($c -and (Test-Path -LiteralPath $c)) { return (Resolve-Path -LiteralPath $c).Path }
}
return $null
}
# ----------------------------------------------------------------------------
# Get the AAD DeviceId from dsregcmd. Returns $null until the device is
# Azure AD joined AND dsregcmd has populated the field.
# ----------------------------------------------------------------------------
function Get-AadDeviceId {
try {
$dsreg = dsregcmd /status 2>&1
} catch {
return $null
}
$line = $dsreg | Select-String 'DeviceId' | Select-Object -First 1
if (-not $line) { return $null }
$parts = $line.ToString().Split(':')
if ($parts.Count -lt 2) { return $null }
$id = $parts[1].Trim()
if ([string]::IsNullOrWhiteSpace($id)) { return $null }
return $id
}
# ----------------------------------------------------------------------------
# Half-block QR renderer. Each output character is 1 module wide, 2 modules
# tall, using:
# U+2588 = both top and bottom modules set
# U+2580 = top only
# U+2584 = bottom only
# space = neither
# Cuts QR height in half vs full-block rendering. Adds a 4-module quiet
# zone manually since QRCoder's ModuleMatrix excludes it by default.
# ----------------------------------------------------------------------------
function Format-QrHalfBlocks {
param([string]$Payload, [string]$DllPath, [int]$IndentSpaces = 8)
Add-Type -Path $DllPath
$gen = New-Object QRCoder.QRCodeGenerator
$data = $gen.CreateQrCode($Payload, [QRCoder.QRCodeGenerator+ECCLevel]::L)
$matrix = $data.ModuleMatrix
$size = $matrix.Count
$pad = 4
$total = $size + 2 * $pad
$upper = [char]0x2580
$lower = [char]0x2584
$full = [char]0x2588
$left = ' ' * $IndentSpaces
$lines = New-Object System.Collections.Generic.List[string]
for ($y = 0; $y -lt $total; $y += 2) {
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append($left)
for ($x = 0; $x -lt $total; $x++) {
$mx = $x - $pad
$my1 = $y - $pad
$my2 = $y + 1 - $pad
$top = ($my1 -ge 0 -and $my1 -lt $size -and $mx -ge 0 -and $mx -lt $size -and $matrix[$my1].Get($mx))
$bot = ($my2 -ge 0 -and $my2 -lt $size -and $mx -ge 0 -and $mx -lt $size -and $matrix[$my2].Get($mx))
if ($top -and $bot) { [void]$sb.Append($full) }
elseif ($top) { [void]$sb.Append($upper) }
elseif ($bot) { [void]$sb.Append($lower) }
else { [void]$sb.Append(' ') }
}
$lines.Add($sb.ToString())
}
return $lines
}
# ----------------------------------------------------------------------------
# Bigger console so the QR + text frame fits in one viewport.
# ----------------------------------------------------------------------------
try {
$rui = $Host.UI.RawUI
$maxH = $rui.MaxPhysicalWindowSize.Height
$targetWindow = [Math]::Min(58, [int]$maxH)
$targetBuffer = [Math]::Max($targetWindow, 200)
$bs = $rui.BufferSize
if ($bs.Height -lt $targetBuffer) { $bs.Height = $targetBuffer; $rui.BufferSize = $bs }
$ws = $rui.WindowSize
if ($ws.Height -lt $targetWindow) { $ws.Height = $targetWindow; $rui.WindowSize = $ws }
} catch {}
# ----------------------------------------------------------------------------
# Wait for QRCoder.dll
# ----------------------------------------------------------------------------
$dllPath = Resolve-QRCoderDll -Override $QRCoderDllPath
# Mark-of-the-Web: when files arrive via SMB / zip download / email,
# Windows attaches a Zone.Identifier alternate data stream that flags the
# file as "from another computer". Add-Type then refuses to load the DLL
# with "Operation is not supported" / "Could not load file or assembly".
# Unblock-File silently strips the ADS. Best-effort: errors are non-fatal
# (file may already be unblocked, or the user may not have write access).
if ($dllPath) {
try { Unblock-File -LiteralPath $dllPath -ErrorAction SilentlyContinue } catch {}
}
if ($PSCommandPath) {
try { Unblock-File -LiteralPath $PSCommandPath -ErrorAction SilentlyContinue } catch {}
}
if (-not $dllPath) {
Write-Host ""
Write-Host "ERROR: QRCoder.dll not found." -ForegroundColor Red
Write-Host "Searched (in order):"
if ($QRCoderDllPath) { Write-Host " - $QRCoderDllPath (-QRCoderDllPath)" }
if ($PSScriptRoot) { Write-Host " - $(Join-Path $PSScriptRoot 'QRCoder.dll')" }
Write-Host " - $(Join-Path (Get-Location) 'QRCoder.dll')"
Write-Host " - C:\Enrollment\shopfloor-setup\Shopfloor\QRCoder.dll"
Write-Host ""
Write-Host "Drop QRCoder.dll alongside this script and re-run." -ForegroundColor Yellow
exit 2
}
# ----------------------------------------------------------------------------
# Poll loop - retry every $PollSeconds until DeviceId appears
# ----------------------------------------------------------------------------
Write-Host ""
Write-Host " Waiting for Azure AD device registration..." -ForegroundColor Cyan
Write-Host " Polling dsregcmd every $PollSeconds s. Press Ctrl+C to abort." -ForegroundColor DarkGray
Write-Host ""
$attempt = 0
while ($true) {
$attempt++
$deviceId = Get-AadDeviceId
if ($deviceId) { break }
Write-Host (" [{0}] attempt {1,3} no DeviceId yet, retrying in {2}s..." -f (Get-Date -Format 'HH:mm:ss'), $attempt, $PollSeconds) -ForegroundColor DarkGray
Start-Sleep -Seconds $PollSeconds
}
# ----------------------------------------------------------------------------
# Render
# ----------------------------------------------------------------------------
Clear-Host
Write-Host ""
Write-Host " GE Aerospace -- Intune Device ID" -ForegroundColor Cyan
Write-Host " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor DarkGray
Write-Host ""
Write-Host " Device ID: $deviceId" -ForegroundColor Green
Write-Host ""
try {
$qrLines = Format-QrHalfBlocks -Payload $deviceId -DllPath $dllPath
foreach ($l in $qrLines) { Write-Host $l }
} catch {
Write-Host " (QR generation failed: $($_.Exception.Message))" -ForegroundColor Red
}
Write-Host ""
Write-Host " Scan with phone camera or Intune admin app." -ForegroundColor DarkGray
Write-Host ""
Write-Host " Press any key to close..." -ForegroundColor Yellow
try { [void][Console]::ReadKey($true) } catch { [void](Read-Host) }
exit 0