diff --git a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 index 800b60f..a7fbf2a 100644 --- a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 +++ b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 @@ -131,6 +131,12 @@ $skipInBaseline = @( '06-OrganizeDesktop.ps1', '07-TaskbarLayout.ps1', '08-EdgeDefaultBrowser.ps1', + # Machine number flow: split into two scripts registered as scheduled + # tasks by Register-CheckMachineNumberTask.ps1. Prompt runs as the + # logged-in user (GUI), Apply runs as SYSTEM (privileged writes). + # Neither should run in baseline pass. + 'Prompt-MachineNumber.ps1', + 'Apply-MachineNumber.ps1', 'Check-MachineNumber.ps1', 'Configure-PC.ps1' ) diff --git a/playbook/shopfloor-setup/Shopfloor/Apply-MachineNumber.ps1 b/playbook/shopfloor-setup/Shopfloor/Apply-MachineNumber.ps1 new file mode 100644 index 0000000..8987a70 --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/Apply-MachineNumber.ps1 @@ -0,0 +1,115 @@ +# Apply-MachineNumber.ps1 - SYSTEM-context worker for the two-task machine +# number flow. Triggered on-demand by Prompt-MachineNumber.ps1 via +# `schtasks /run /tn "WT-Apply-MachineNumber"`. Reads the requested number +# from a file the GUI script wrote, invokes Update-MachineNumber as SYSTEM +# (full HKLM + ProgramData access), writes a result JSON for the GUI to +# display, then cleans up. +# +# Why SYSTEM: +# The eDNC reg key (HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\ +# General\MachineNo) and UDC settings JSON live in HKLM + ProgramData +# respectively - both require admin to write. The OLD design granted +# BUILTIN\Users SetValue + Modify via 02-MachineNumberACLs.ps1, but that +# was fragile (timing race with eDNC install, ACL silently failed on +# some bays) AND a security hole (any user could mess with the machine +# identity). Two-task design: GUI gathers input as logged-in user, SYSTEM +# does the actual write. +# +# Files: +# C:\Logs\SFLD\machine-number-request.txt - input, single line, new number +# C:\Logs\SFLD\machine-number-result.json - output, status fields for GUI +# C:\Logs\SFLD\Apply-MachineNumber.log - transcript + +$ErrorActionPreference = 'Continue' + +$logDir = 'C:\Logs\SFLD' +if (-not (Test-Path -LiteralPath $logDir)) { + try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP } +} +$transcript = Join-Path $logDir 'Apply-MachineNumber.log' +try { Start-Transcript -Path $transcript -Append -Force | Out-Null } catch {} + +$requestFile = Join-Path $logDir 'machine-number-request.txt' +$resultFile = Join-Path $logDir 'machine-number-result.json' + +function Write-Result { + param([hashtable]$Body) + $Body | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $resultFile -Encoding ascii -Force +} + +Write-Host "Apply-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" + +try { + if (-not (Test-Path -LiteralPath $requestFile)) { + Write-Warning "No request file at $requestFile - nothing to apply." + Write-Result @{ Status = 'NoRequest'; Errors = @("request file missing: $requestFile") } + exit 0 + } + $newNumber = (Get-Content -LiteralPath $requestFile -First 1 -ErrorAction Stop).Trim() + Write-Host "Requested new machine number: $newNumber" + + if ($newNumber -notmatch '^\d+$') { + Write-Warning "Request is not digits-only: '$newNumber'" + Write-Result @{ Status = 'BadInput'; Requested = $newNumber; Errors = @("Not digits only: '$newNumber'") } + Remove-Item -LiteralPath $requestFile -Force -ErrorAction SilentlyContinue + exit 1 + } + + # Dot-source the shared helper. Update-MachineNumber.ps1 now has + # -ErrorAction Stop on the writes so failures actually throw. + . "$PSScriptRoot\lib\Get-PCProfile.ps1" + . "$PSScriptRoot\lib\Update-MachineNumber.ps1" + + $site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' } + $mnResult = Update-MachineNumber -NewNumber $newNumber -Site $site + + $resultBody = @{ + Status = if ($mnResult.Errors.Count -eq 0) { 'OK' } else { 'PartialErrors' } + Requested = $newNumber + Site = $site + UdcUpdated = [bool]$mnResult.UdcUpdated + EdncUpdated = [bool]$mnResult.EdncUpdated + OldUdc = $mnResult.OldUdc + OldEdnc = $mnResult.OldEdnc + UdcSettingsRestored = [bool]$mnResult.UdcSettingsRestored + UdcRestored = [bool]$mnResult.UdcRestored + MTConnectUpdated = $mnResult.MTConnectUpdated + MachineNumberTxtUpdated = [bool]$mnResult.MachineNumberTxtUpdated + Errors = $mnResult.Errors + AppliedAt = (Get-Date -Format 'o') + AppliedAs = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + } + Write-Result -Body $resultBody + + Write-Host "Update-MachineNumber result:" + Write-Host " UdcUpdated = $($mnResult.UdcUpdated)" + Write-Host " EdncUpdated = $($mnResult.EdncUpdated)" + Write-Host " Errors = $($mnResult.Errors.Count)" + if ($mnResult.Errors) { $mnResult.Errors | ForEach-Object { Write-Host " FAILED: $_" } } + + Remove-Item -LiteralPath $requestFile -Force -ErrorAction SilentlyContinue + + # On clean success, also unregister the Prompt logon task. Prompt itself + # tries to self-unregister but it runs as a Limited user (BUILTIN\Users) + # and silently fails on Unregister-ScheduledTask (no delete right on a + # SYSTEM-registered task). We're running as SYSTEM here, so we can. + # Idempotent if Prompt already unregistered itself somehow. + if ($mnResult.Errors.Count -eq 0 -and $mnResult.EdncUpdated) { + try { + if (Get-ScheduledTask -TaskName 'Prompt Machine Number' -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName 'Prompt Machine Number' -Confirm:$false -ErrorAction Stop + Write-Host "Unregistered 'Prompt Machine Number' task (SYSTEM cleanup)." + } + } catch { + Write-Host "Could not unregister 'Prompt Machine Number': $_" + } + } + + Write-Host "Apply-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +} catch { + Write-Warning "Apply threw: $_" + Write-Result @{ Status = 'Exception'; Errors = @("$_") } +} finally { + try { Stop-Transcript | Out-Null } catch {} +} diff --git a/playbook/shopfloor-setup/Shopfloor/Check-MachineNumber.ps1 b/playbook/shopfloor-setup/Shopfloor/Check-MachineNumber.ps1 index 26fe439..94a9484 100644 --- a/playbook/shopfloor-setup/Shopfloor/Check-MachineNumber.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/Check-MachineNumber.ps1 @@ -93,13 +93,24 @@ if ($mnResult.EdncUpdated) { $results += "eDNC updated to $new" } foreach ($err in $mnResult.Errors) { $results += $err -replace '^', 'FAILED: ' } # --- Show result --- -$summary = ($results -join "`n") + "`n`nTo apply eDNC changes, restart any running DncMain.exe." +$summary = ($results -join "`n") + "`n`nTo apply eDNC changes, restart any running DncMain.exe.`n`nFull log: C:\Logs\SFLD\Check-MachineNumber.log" +# Force the MessageBox to topmost + take focus so it isn't hidden behind +# other windows. Without this, the result dialog can render off-screen or +# behind the FormTracePak / DNC windows and the tech misses it. +$tmpForm = New-Object System.Windows.Forms.Form +$tmpForm.TopMost = $true +$tmpForm.WindowState = 'Minimized' +$tmpForm.ShowInTaskbar = $false +$tmpForm.Opacity = 0 +$tmpForm.Show() [System.Windows.Forms.MessageBox]::Show( + $tmpForm, $summary, "Machine Number Updated", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information ) | Out-Null +$tmpForm.Close() # --- Unregister task on success --- Write-Host "Results: $($results -join '; ')" diff --git a/playbook/shopfloor-setup/Shopfloor/Prompt-MachineNumber.ps1 b/playbook/shopfloor-setup/Shopfloor/Prompt-MachineNumber.ps1 new file mode 100644 index 0000000..4d708ff --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/Prompt-MachineNumber.ps1 @@ -0,0 +1,200 @@ +# Prompt-MachineNumber.ps1 - User-context GUI script for the two-task +# machine number flow. Triggered AtLogOn for any BUILTIN\Users member. +# +# Flow: +# 1. Read current UDC + eDNC values (read-only - no privileges needed). +# 2. If neither is 9999, unregister self and exit (this PC is set up). +# 3. Show InputBox for new machine number. +# 4. Write number to C:\Logs\SFLD\machine-number-request.txt. +# 5. Trigger the SYSTEM-context Apply-MachineNumber task via +# schtasks /run. SYSTEM has full HKLM + ProgramData access so the +# actual write happens with proper privileges - the prompted user +# never needs HKLM write rights (security improvement over the old +# 02-MachineNumberACLs.ps1 ACL-grant hack). +# 6. Poll for C:\Logs\SFLD\machine-number-result.json (30s timeout). +# 7. Show result MessageBox. Unregister self on success. +# +# Why this script doesn't do the writes itself: GUI is required (InputBox), +# but GUI requires user-context (SYSTEM can't render to user desktop on +# modern Windows). The user-context dialog gathers input; the SYSTEM task +# does privileged writes. + +# --- Transcript --- +$logDir = 'C:\Logs\SFLD' +if (-not (Test-Path -LiteralPath $logDir)) { + try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP } +} +$transcript = Join-Path $logDir 'Prompt-MachineNumber.log' +try { Start-Transcript -Path $transcript -Append -Force | Out-Null } catch {} +Write-Host "Prompt-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" + +. "$PSScriptRoot\lib\Get-PCProfile.ps1" +. "$PSScriptRoot\lib\Update-MachineNumber.ps1" + +Add-Type -AssemblyName Microsoft.VisualBasic +Add-Type -AssemblyName System.Windows.Forms + +$taskName = 'Prompt Machine Number' +$applyTaskName = 'Apply Machine Number' +$requestFile = Join-Path $logDir 'machine-number-request.txt' +$resultFile = Join-Path $logDir 'machine-number-result.json' +$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' } + +# --- Read current values (read-only, no perms needed) --- +$currentMN = Get-CurrentMachineNumber +$currentUdc = $currentMN.Udc +$currentEdnc = $currentMN.Ednc +Write-Host "UDC machine number: $(if ($currentUdc) { $currentUdc } else { '(not found)' })" +Write-Host "eDNC machine number: $(if ($currentEdnc) { $currentEdnc } else { '(not found)' })" + +if ($currentUdc -ne '9999' -and $currentEdnc -ne '9999') { + Write-Host "Machine number is set (not 9999). Unregistering Prompt task and exiting." + try { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue } catch {} + try { Stop-Transcript | Out-Null } catch {} + exit 0 +} + +Write-Host "Placeholder 9999 detected - showing prompt." + +# --- Show prompt --- +$promptLines = @() +$promptLines += "The machine number on this PC is still set to the" +$promptLines += "placeholder value (9999). Please enter the correct" +$promptLines += "machine number for this workstation." +$promptLines += "" +if ($currentUdc) { $promptLines += "Current UDC: $currentUdc" } +if ($currentEdnc) { $promptLines += "Current eDNC: $currentEdnc" } +$promptLines += "" +$promptLines += "Enter the new Machine Number:" +$prompt = $promptLines -join "`n" +$new = [Microsoft.VisualBasic.Interaction]::InputBox($prompt, "Set Machine Number", "") + +if ([string]::IsNullOrWhiteSpace($new)) { + Write-Host "User cancelled. Will prompt again next logon." + try { Stop-Transcript | Out-Null } catch {} + exit 0 +} +$new = $new.Trim() + +if ($new -notmatch '^\d+$') { + Write-Host "Invalid input: '$new' (not digits only). Showing error and re-prompting next logon." + [System.Windows.Forms.MessageBox]::Show( + "Machine number must be digits only.`n`nYou entered: '$new'`n`nThe prompt will appear again at next logon.", + "Invalid Machine Number", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Error + ) | Out-Null + try { Stop-Transcript | Out-Null } catch {} + exit 0 +} + +# --- Hand off to SYSTEM task --- +# Clean any stale request / result files first so we read fresh ones. +Remove-Item -LiteralPath $requestFile, $resultFile -Force -ErrorAction SilentlyContinue + +try { + Set-Content -LiteralPath $requestFile -Value $new -Encoding ascii -Force -ErrorAction Stop +} catch { + [System.Windows.Forms.MessageBox]::Show( + "Could not write request file at $requestFile`n`n$_`n`nThe prompt will appear again at next logon.", + "Machine Number Request Failed", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Error + ) | Out-Null + try { Stop-Transcript | Out-Null } catch {} + exit 1 +} + +Write-Host "Wrote $requestFile with '$new'. Triggering SYSTEM apply task..." +& schtasks.exe /run /tn $applyTaskName 2>&1 | ForEach-Object { Write-Host " schtasks: $_" } + +# --- Wait for result --- +$deadline = (Get-Date).AddSeconds(60) +$result = $null +while ((Get-Date) -lt $deadline) { + if (Test-Path -LiteralPath $resultFile) { + try { + $result = Get-Content -LiteralPath $resultFile -Raw -ErrorAction Stop | ConvertFrom-Json + break + } catch { Start-Sleep -Milliseconds 200 } + } + Start-Sleep -Milliseconds 500 +} + +# Make the result MessageBox topmost so it shows above the FormTracePak / +# DNC windows and isn't missed. +$tmpForm = New-Object System.Windows.Forms.Form +$tmpForm.TopMost = $true +$tmpForm.WindowState = 'Minimized' +$tmpForm.ShowInTaskbar = $false +$tmpForm.Opacity = 0 +$tmpForm.Show() + +if (-not $result) { + Write-Host "Timed out waiting for SYSTEM apply task to produce result file ($resultFile)." + [System.Windows.Forms.MessageBox]::Show( + $tmpForm, + "Timed out waiting for the SYSTEM update task to complete.`n`nCheck:`n C:\Logs\SFLD\Apply-MachineNumber.log`n C:\Logs\SFLD\Prompt-MachineNumber.log`n`nThe prompt will appear again at next logon.", + "Machine Number Update Timed Out", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Warning + ) | Out-Null + $tmpForm.Close() + try { Stop-Transcript | Out-Null } catch {} + exit 1 +} + +# Build summary from result JSON +$lines = @() +$lines += "Requested: $($result.Requested)" +$lines += "" +if ($result.UdcUpdated) { $lines += "UDC updated to $($result.Requested)" } else { $lines += "UDC: not updated (UDC may not be installed)" } +if ($result.EdncUpdated) { $lines += "eDNC updated to $($result.Requested)" } else { $lines += "eDNC: not updated" } +if ($result.UdcSettingsRestored) { $lines += "UDC settings restored from SFLD" } +if ($result.UdcRestored) { $lines += "UDC live data restored from SFLD" } +if ($result.MachineNumberTxtUpdated) { $lines += "machine-number.txt updated" } +if ($result.MTConnectUpdated -and $result.MTConnectUpdated.Count -gt 0) { + $lines += "" + $lines += "MTConnect Devices.xml updates:" + $result.MTConnectUpdated | ForEach-Object { $lines += " - $_" } +} +if ($result.Errors -and $result.Errors.Count -gt 0) { + $lines += "" + $lines += "FAILURES:" + $result.Errors | ForEach-Object { $lines += " - $_" } +} +$lines += "" +$lines += "Status: $($result.Status)" +$lines += "Logs: C:\Logs\SFLD\Apply-MachineNumber.log" +$lines += " C:\Logs\SFLD\Prompt-MachineNumber.log" +$lines += "" +$lines += "To apply eDNC changes, restart any running DncMain.exe." +$summary = $lines -join "`n" + +$icon = if ($result.Status -eq 'OK') { [System.Windows.Forms.MessageBoxIcon]::Information } else { [System.Windows.Forms.MessageBoxIcon]::Warning } +[System.Windows.Forms.MessageBox]::Show( + $tmpForm, + $summary, + "Machine Number Update Result", + [System.Windows.Forms.MessageBoxButtons]::OK, + $icon +) | Out-Null +$tmpForm.Close() + +# Clean up result file for the next round. +Remove-Item -LiteralPath $resultFile -Force -ErrorAction SilentlyContinue + +# Only unregister the Prompt task on full success (no errors AND eDNC +# updated to the requested value). If anything failed, leave it registered +# for next logon retry. +if ($result.Status -eq 'OK' -and $result.EdncUpdated) { + Write-Host "All updates succeeded. Unregistering Prompt task." + try { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue } catch {} +} else { + Write-Host "Some updates failed or skipped. Prompt task stays registered for next logon retry." +} + +Write-Host "Prompt-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +try { Stop-Transcript | Out-Null } catch {} +exit 0 diff --git a/playbook/shopfloor-setup/Shopfloor/Register-CheckMachineNumberTask.ps1 b/playbook/shopfloor-setup/Shopfloor/Register-CheckMachineNumberTask.ps1 index 146fc7e..5e635b1 100644 --- a/playbook/shopfloor-setup/Shopfloor/Register-CheckMachineNumberTask.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/Register-CheckMachineNumberTask.ps1 @@ -1,17 +1,27 @@ -# Register-CheckMachineNumberTask.ps1 - Register the "Check Machine Number" -# logon scheduled task at imaging time. Mirrors Register-MapSfldShare.ps1. +# Register-CheckMachineNumberTask.ps1 - Register the two-task machine +# number flow at imaging time: # -# The task fires at every interactive logon for BUILTIN\Users (so the -# ShopFloor end-user, who is the auto-logon principal post-lockdown, -# triggers it). Check-MachineNumber.ps1 reads the current UDC + eDNC -# machine numbers, and: -# - If neither is 9999, unregisters the task and exits (one-shot). -# - If either is 9999, pops an InputBox forcing the user to type the -# real number; on success calls Update-MachineNumber.ps1 which pulls -# the per-machine NTLARS .reg + UDC settings JSON + UDC data backup -# from the SFLD share and applies them. +# 1. "Prompt Machine Number" - AtLogOn, BUILTIN\Users, Limited. +# Shows InputBox + writes new number to a request file, then triggers +# the SYSTEM task via schtasks /run. # -# Idempotent: safe to re-run. Existing task is overwritten. +# 2. "Apply Machine Number" - on-demand only (no trigger), SYSTEM, +# RunLevel Highest. Reads the request file, calls Update-MachineNumber +# with full HKLM + ProgramData access, writes a result JSON, removes +# the request file. No GUI - the Prompt task polls the result file +# and displays the dialog. +# +# Replaces the old single-task design that ran as the logged-in user with +# pre-granted BUILTIN\Users HKLM ACLs (02-MachineNumberACLs.ps1). That +# approach was fragile (timing race with eDNC install, silent ACL skip) +# and a security hole (any user could write to the machine-identity reg +# key). With SYSTEM doing the actual writes, no ACL grants needed. +# +# Idempotent: safe to re-run. Existing tasks are overwritten. +# +# File kept named Register-CheckMachineNumberTask.ps1 (rather than +# Register-MachineNumberTasks.ps1) so Run-ShopfloorSetup's existing +# discovery doesn't need editing. $ErrorActionPreference = 'Continue' @@ -28,9 +38,19 @@ function Write-RegLog { Write-RegLog '=== Register-CheckMachineNumberTask start ===' -$taskName = 'Check Machine Number' +$promptTaskName = 'Prompt Machine Number' +$applyTaskName = 'Apply Machine Number' +$oldTaskName = 'Check Machine Number' # legacy, removed below -# Only arm the task if the bay was imaged with the 9999 placeholder. If +# Clean up the legacy single-task name from prior imaging cycles. +try { + if (Get-ScheduledTask -TaskName $oldTaskName -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName $oldTaskName -Confirm:$false -ErrorAction Stop + Write-RegLog "Unregistered legacy task '$oldTaskName'" + } +} catch { Write-RegLog "Could not unregister legacy '$oldTaskName': $_" } + +# Only arm the tasks if the bay was imaged with the 9999 placeholder. If # the tech entered a real machine number during PXE imaging it's already # in C:\Enrollment\machine-number.txt; no prompt needed on first logon. $mnFile = 'C:\Enrollment\machine-number.txt' @@ -41,63 +61,87 @@ if (Test-Path -LiteralPath $mnFile) { } Write-RegLog "Imaging-time machine-number.txt = '$mnAtImaging'" if ($mnAtImaging -ne '9999') { - Write-RegLog "Machine number is real ('$mnAtImaging' != 9999). Not registering task." - # Clean up any stale task from a prior 9999-imaging cycle on the same disk. - try { - if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction Stop - Write-RegLog "Unregistered stale task '$taskName'" - } - } catch {} + Write-RegLog "Machine number is real ('$mnAtImaging' != 9999). Not registering tasks." + foreach ($t in @($promptTaskName, $applyTaskName)) { + try { + if (Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction Stop + Write-RegLog "Unregistered stale task '$t'" + } + } catch {} + } Write-RegLog '=== Register-CheckMachineNumberTask end (no-op) ===' exit 0 } -# Resolve the script path. Prefer the staged shopfloor-setup tree on C: +# Resolve script paths. Prefer the staged shopfloor-setup tree on C: # (where Run-ShopfloorSetup ran from); fall back to the same dir as this # Register script if invoked standalone. -$checkScript = Join-Path $PSScriptRoot 'Check-MachineNumber.ps1' -if (-not (Test-Path -LiteralPath $checkScript)) { - $checkScript = 'C:\Enrollment\shopfloor-setup\Shopfloor\Check-MachineNumber.ps1' +function Resolve-Script { + param([string]$LeafName) + $p = Join-Path $PSScriptRoot $LeafName + if (Test-Path -LiteralPath $p) { return $p } + $p = "C:\Enrollment\shopfloor-setup\Shopfloor\$LeafName" + if (Test-Path -LiteralPath $p) { return $p } + return $null } -if (-not (Test-Path -LiteralPath $checkScript)) { - Write-RegLog "Check-MachineNumber.ps1 not found at $checkScript - cannot register" - exit 1 -} -Write-RegLog "Check-MachineNumber.ps1 at: $checkScript" +$promptScript = Resolve-Script 'Prompt-MachineNumber.ps1' +$applyScript = Resolve-Script 'Apply-MachineNumber.ps1' +if (-not $promptScript) { Write-RegLog "Prompt-MachineNumber.ps1 not found - cannot register"; exit 1 } +if (-not $applyScript) { Write-RegLog "Apply-MachineNumber.ps1 not found - cannot register"; exit 1 } +Write-RegLog "Prompt script: $promptScript" +Write-RegLog "Apply script: $applyScript" + +# --- Prompt task (user-context, GUI) --- try { $action = New-ScheduledTaskAction ` -Execute 'powershell.exe' ` - -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$checkScript`"" - + -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$promptScript`"" $trigger = New-ScheduledTaskTrigger -AtLogOn - - # Run as the logged-in user (needs GUI for InputBox), NOT SYSTEM. - # Group SID S-1-5-32-545 = BUILTIN\Users; catches ShopFloor + any - # support / admin user that logs in interactively. - $principal = New-ScheduledTaskPrincipal ` - -GroupId 'S-1-5-32-545' ` - -RunLevel Limited - - $settings = New-ScheduledTaskSettingsSet ` - -AllowStartIfOnBatteries ` - -DontStopIfGoingOnBatteries ` - -StartWhenAvailable ` - -ExecutionTimeLimit (New-TimeSpan -Minutes 5) - - Register-ScheduledTask ` - -TaskName $taskName ` - -Action $action ` - -Trigger $trigger ` - -Principal $principal ` - -Settings $settings ` - -Force ` - -ErrorAction Stop | Out-Null - - Write-RegLog "Registered scheduled task '$taskName' (AtLogOn, BUILTIN\Users, Limited)" + # Group SID S-1-5-32-545 = BUILTIN\Users (catches ShopFloor + support/admin + # users that log in interactively). RunLevel Limited - no elevation; the + # actual writes happen in the SYSTEM Apply task below. + $principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Limited + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5) + Register-ScheduledTask -TaskName $promptTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force -ErrorAction Stop | Out-Null + Write-RegLog "Registered scheduled task '$promptTaskName' (AtLogOn, BUILTIN\Users, Limited)" } catch { - Write-RegLog "FAILED to register '$taskName': $_" + Write-RegLog "FAILED to register '$promptTaskName': $_" + exit 1 +} + +# --- Apply task (SYSTEM, on-demand) --- +try { + $action = New-ScheduledTaskAction ` + -Execute 'powershell.exe' ` + -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$applyScript`"" + # No trigger - the Prompt task starts this via schtasks /run /tn. + $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 10) + Register-ScheduledTask -TaskName $applyTaskName -Action $action -Principal $principal -Settings $settings -Force -ErrorAction Stop | Out-Null + Write-RegLog "Registered scheduled task '$applyTaskName' (on-demand, SYSTEM, Highest)" + + # Default SDDL on a SYSTEM-owned task only grants Admins + SYSTEM + # FullAccess - BUILTIN\Users can't see or run it via schtasks /run. + # Add an ACE granting BUILTIN\Users GenericRead + GenericExecute so the + # user-context Prompt task can trigger this Apply task on demand. They + # still can't modify/delete it - only read+execute. + try { + $svc = New-Object -ComObject Schedule.Service + $svc.Connect() + $taskObj = $svc.GetFolder('\').GetTask($applyTaskName) + # GenericRead = 0x80000000 (GR), GenericExecute = 0x20000000 (GX) + # BU = BUILTIN\Users + $newSd = 'O:BAG:BAD:(A;;FA;;;BA)(A;;FA;;;SY)(A;;GRGX;;;BU)' + # SetSecurityDescriptor flag 0 = default, persists DACL change. + $taskObj.SetSecurityDescriptor($newSd, 0) + Write-RegLog "Granted BUILTIN\Users GR+GX on '$applyTaskName' (so Limited users can schtasks /run)" + } catch { + Write-RegLog "FAILED to set task SDDL on '$applyTaskName': $_ (Limited users may not be able to trigger Apply)" + } +} catch { + Write-RegLog "FAILED to register '$applyTaskName': $_" exit 1 } diff --git a/playbook/shopfloor-setup/Shopfloor/lib/Update-MachineNumber.ps1 b/playbook/shopfloor-setup/Shopfloor/lib/Update-MachineNumber.ps1 index 1f6b334..fc728c8 100644 --- a/playbook/shopfloor-setup/Shopfloor/lib/Update-MachineNumber.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/lib/Update-MachineNumber.ps1 @@ -244,11 +244,16 @@ function Update-MachineNumber { Start-Sleep -Seconds 1 # --- Update UDC settings JSON --- + # -ErrorAction Stop on the WRITE so PermissionDenied / IO errors become + # terminating and actually hit the catch block. Without this, the cmdlet + # writes a non-terminating error (visible in transcript) but flow + # continues + $out.UdcUpdated is set to $true, leading the dialog to + # report "UDC updated" when the file write actually failed. if (Test-Path $script:UdcSettingsPath) { try { - $json = Get-Content $script:UdcSettingsPath -Raw | ConvertFrom-Json + $json = Get-Content $script:UdcSettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json $json.GeneralSettings.MachineNumber = $NewNumber - $json | ConvertTo-Json -Depth 99 | Set-Content -Path $script:UdcSettingsPath -Encoding UTF8 + $json | ConvertTo-Json -Depth 99 | Set-Content -Path $script:UdcSettingsPath -Encoding UTF8 -ErrorAction Stop $out.UdcUpdated = $true } catch { $out.Errors += "UDC update failed: $_" @@ -256,9 +261,15 @@ function Update-MachineNumber { } # --- Update eDNC registry --- + # Same -ErrorAction Stop reasoning as above. Set-ItemProperty's + # PermissionDenied is non-terminating by default; without -ErrorAction + # Stop, the catch block never fires and $out.EdncUpdated=$true gets set + # despite the write failing. This is the bug that made the 13:35:39 + # tech run on FGY07FZ3 report "eDNC updated to 3005 / All updates + # succeeded" while the actual reg value stayed at 9999. if (Test-Path $script:EdncRegPath) { try { - Set-ItemProperty -Path $script:EdncRegPath -Name MachineNo -Value $NewNumber -Type String -Force + Set-ItemProperty -Path $script:EdncRegPath -Name MachineNo -Value $NewNumber -Type String -Force -ErrorAction Stop $out.EdncUpdated = $true } catch { $out.Errors += "eDNC update failed: $_" diff --git a/playbook/shopfloor-setup/gea-shopfloor-collections/02-MachineNumberACLs.ps1 b/playbook/shopfloor-setup/gea-shopfloor-collections/02-MachineNumberACLs.ps1 index 96f1a5d..ce841f8 100644 --- a/playbook/shopfloor-setup/gea-shopfloor-collections/02-MachineNumberACLs.ps1 +++ b/playbook/shopfloor-setup/gea-shopfloor-collections/02-MachineNumberACLs.ps1 @@ -1,81 +1,29 @@ -# 02-MachineNumberACLs.ps1 - Pre-grant write access on the UDC settings -# file and eDNC registry key so that STANDARD (non-admin) users can update -# the machine number via the Check-MachineNumber logon task without -# elevation or a UAC prompt. +# 02-MachineNumberACLs.ps1 - NO-OP (deprecated 2026-05-24). # -# Runs during imaging as admin (type-specific Standard phase, after -# 01-eDNC.ps1 has installed DnC). Only touches Standard PCs. +# This script used to grant BUILTIN\Users SetValue on the eDNC reg key +# and Modify on the UDC ProgramData dir so the logged-in user could +# update machine number from the Check-MachineNumber logon dialog without +# elevation. # -# What gets opened up (narrow scope, not blanket admin): -# - C:\ProgramData\UDC\udc_settings.json -> BUILTIN\Users : Modify -# - HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General -# -> BUILTIN\Users : SetValue +# That design had two flaws: +# 1. Security hole - any logged-in user could overwrite the machine- +# identity reg key. +# 2. Fragile - ACL grants raced with eDNC install timing on some bays; +# the OpenSubKey call returned null + the grant was silently skipped, +# leaving Check-MachineNumber unable to update the bay (yet the old +# Update-MachineNumber.ps1 reported success anyway because +# Set-ItemProperty's PermissionDenied is non-terminating). +# +# Replaced by the two-task design in Register-CheckMachineNumberTask.ps1: +# - "Prompt Machine Number" : user-context GUI, no privileges +# - "Apply Machine Number" : SYSTEM-context worker, full HKLM access +# +# Left as a no-op so Stage-Dispatcher / Run-ShopfloorSetup discovery +# patterns don't have to be updated. Existing bays' ACL grants are still +# present and harmless (the SYSTEM Apply task ignores them). -# --- Transcript --- $logDir = 'C:\Logs\SFLD' if (-not (Test-Path $logDir)) { try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch {} } try { Start-Transcript -Path (Join-Path $logDir '02-MachineNumberACLs.log') -Append -Force | Out-Null } catch {} - -# --- Skip on Timeclock sub-type (no UDC/eDNC to grant ACLs for) --- -$subtypeFile = 'C:\Enrollment\pc-subtype.txt' -if (Test-Path $subtypeFile) { - $subtype = (Get-Content $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim() - if ($subtype -eq 'Timeclock') { - Write-Host "02-MachineNumberACLs: skipped (Standard-Timeclock)" - try { Stop-Transcript | Out-Null } catch {} - return - } -} - -Write-Host "02-MachineNumberACLs.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" -Write-Host "" -Write-Host "Setting ACLs for standard-user machine number access..." - -# --- UDC settings directory --- -# Set ACL on the DIRECTORY (not the file) with inheritance so that -# udc_settings.json inherits the permission whenever UDC.exe creates it. -# UDC_Setup.exe is killed by KillAfterDetection before UDC.exe writes the -# JSON, so the file doesn't exist at this point. Directory-level ACL with -# ContainerInherit + ObjectInherit covers any file created inside later. -$udcDir = 'C:\ProgramData\UDC' -if (Test-Path -LiteralPath $udcDir) { - try { - $acl = Get-Acl -LiteralPath $udcDir - $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( - 'BUILTIN\Users', 'Modify', - ([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor - [System.Security.AccessControl.InheritanceFlags]::ObjectInherit), - [System.Security.AccessControl.PropagationFlags]::None, - 'Allow') - $acl.AddAccessRule($rule) - Set-Acl -LiteralPath $udcDir -AclObject $acl -ErrorAction Stop - Write-Host " UDC dir: BUILTIN\Users granted Modify (inherited) on $udcDir" - } catch { - Write-Warning " Failed to set ACL on $udcDir : $_" - } -} else { - Write-Host " UDC dir not found at $udcDir - skipping (UDC not installed?)" -ForegroundColor DarkGray -} - -# --- eDNC registry key --- -$ednRegPathWin = 'SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General' -try { - $regKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($ednRegPathWin, $true) - if ($regKey) { - $regSec = $regKey.GetAccessControl() - $rule = New-Object System.Security.AccessControl.RegistryAccessRule( - 'BUILTIN\Users', 'SetValue', 'Allow') - $regSec.AddAccessRule($rule) - $regKey.SetAccessControl($regSec) - $regKey.Close() - Write-Host " eDNC reg: BUILTIN\Users granted SetValue on HKLM:\$ednRegPathWin" - } else { - Write-Host " eDNC registry key not found - skipping (eDNC not installed?)" -ForegroundColor DarkGray - } -} catch { - Write-Warning " Failed to set ACL on HKLM:\$ednRegPathWin : $_" -} - -Write-Host "ACL setup complete." +Write-Host "02-MachineNumberACLs.ps1: no-op (replaced by SYSTEM Apply task - see Register-CheckMachineNumberTask.ps1)" try { Stop-Transcript | Out-Null } catch {}