fix(shopfloor): S: mapper review fixes - headless task, drift guard, pre-flight, re-seed
Independent review in the win11 VM found four things worth fixing before the fleet sync: - The task action ran powershell -WindowStyle Hidden, which on Win11 (Windows Terminal as console host) flashes a window and steals foreground for ~0.5 s on every fire. Action is now conhost.exe --headless powershell.exe ..., verified silent. - If the share copy of Map-SfldShare.ps1 changes without the manifest DetectionValue being bumped, the engine calls the registrar every cycle. The registrar now takes an idempotent fast path (staged hash matches, task present with our action -> re-stamp and exit, no task churn) and logs one WARN per hash when -ExpectedHash disagrees with the share file. - Replacing a wrong or unreadable mapping now pre-flights the new mapping on a spare letter and keeps the old S: when that fails (except on 1219, where the old session is what blocks the new one). - Credential Manager is re-seeded whenever the HKLM credential changes (password rotation) and after every repair, instead of once per profile. Set-State no longer wipes the stored hash on suspect writes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011boHjE3EeexwszdiKDZ7iX
This commit is contained in:
@@ -11,7 +11,10 @@
|
||||
|
||||
param(
|
||||
[int]$IntervalMinutes = 3,
|
||||
[string]$InstallRoot = 'C:\Program Files\GE\SfldShare'
|
||||
[string]$InstallRoot = 'C:\Program Files\GE\SfldShare',
|
||||
# SHA256 the manifest entry detects on. When it differs from the share
|
||||
# file the manifest was not bumped with the script; warn loudly once.
|
||||
[string]$ExpectedHash = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
@@ -32,8 +35,6 @@ function Write-RegLog {
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-MapSfldShare start ==='
|
||||
|
||||
# Self-contained PC types never get S: (Display kiosk: local accounts, no
|
||||
# SFLD creds, mapping could only fail). Gate here as well as at the call
|
||||
# site - see the 579C144 note in the previous version.
|
||||
@@ -53,6 +54,36 @@ if ($pcType -and ($selfContainedTypes -contains $pcType)) {
|
||||
# --- stage the script ----------------------------------------------------
|
||||
$src = Join-Path $PSScriptRoot 'lib\Map-SfldShare.ps1'
|
||||
if (-not (Test-Path $src)) { Write-RegLog "Map-SfldShare.ps1 not found at $src - cannot register"; exit 1 }
|
||||
$srcHash = (Get-FileHash -Path $src -Algorithm SHA256).Hash
|
||||
$stampKey = 'HKLM:\SOFTWARE\GE\SfldShare'
|
||||
|
||||
# Manifest / share drift guard. The GE-Enforce entry detects on
|
||||
# MapperHash == DetectionValue, so a share edit without a DetectionValue bump
|
||||
# makes the engine call this every cycle. Say so once per hash, and take the
|
||||
# fast path below so that loop costs nothing and never churns the task.
|
||||
if ($ExpectedHash -and ($ExpectedHash -ne $srcHash)) {
|
||||
$warned = (Get-ItemProperty -Path $stampKey -Name DriftWarnedHash -EA SilentlyContinue).DriftWarnedHash
|
||||
if ($warned -ne $srcHash) {
|
||||
Write-RegLog "WARN: share Map-SfldShare.ps1 is $srcHash but the manifest DetectionValue is $ExpectedHash - bump DetectionValue in common/manifest.json or the engine re-runs this every cycle"
|
||||
if (-not (Test-Path $stampKey)) { New-Item -Path $stampKey -Force | Out-Null }
|
||||
Set-ItemProperty -Path $stampKey -Name DriftWarnedHash -Value $srcHash -Type String -EA SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Fast path: staged copy already matches the share and the task exists with
|
||||
# our action -> nothing to (re)do. Re-stamp and leave (no Register -Force,
|
||||
# so a live task is never torn down for no reason).
|
||||
$taskNow = Get-ScheduledTask -TaskName $taskName -EA SilentlyContinue
|
||||
if ($taskNow -and (Test-Path $mapScript) -and
|
||||
((Get-FileHash -Path $mapScript -Algorithm SHA256).Hash -eq $srcHash) -and
|
||||
($taskNow.Actions[0].Execute -like '*conhost.exe') -and
|
||||
($taskNow.Actions[0].Arguments -like "*$mapScript*")) {
|
||||
if (-not (Test-Path $stampKey)) { New-Item -Path $stampKey -Force | Out-Null }
|
||||
Set-ItemProperty -Path $stampKey -Name MapperHash -Value $srcHash -Type String -EA SilentlyContinue
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-MapSfldShare start ==='
|
||||
if (-not (Test-Path $InstallRoot)) { New-Item -Path $InstallRoot -ItemType Directory -Force | Out-Null }
|
||||
Copy-Item -Path $src -Destination $mapScript -Force
|
||||
Write-RegLog "Staged $src -> $mapScript"
|
||||
@@ -69,9 +100,14 @@ if (Get-ScheduledTask -TaskName $legacyTask -EA SilentlyContinue) {
|
||||
|
||||
# --- task ------------------------------------------------------------------
|
||||
try {
|
||||
# conhost --headless, not powershell -WindowStyle Hidden: on Win11 the
|
||||
# default console host is Windows Terminal, which ignores the hosted
|
||||
# app's Hidden style and flashes a window + steals foreground for
|
||||
# ~0.5 s on every fire (measured in the VM). Headless conhost shows
|
||||
# nothing and moves focus nowhere.
|
||||
$ps = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$action = New-ScheduledTaskAction -Execute $ps `
|
||||
-Argument ('-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}"' -f $mapScript)
|
||||
$action = New-ScheduledTaskAction -Execute "$env:SystemRoot\System32\conhost.exe" `
|
||||
-Argument ('--headless "{0}" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{1}"' -f $ps, $mapScript)
|
||||
|
||||
$ns = 'Root/Microsoft/Windows/TaskScheduler'
|
||||
$triggers = New-Object System.Collections.ArrayList
|
||||
|
||||
@@ -59,15 +59,23 @@ function Get-State {
|
||||
Key = $k
|
||||
FailCount = [int]($p.FailCount)
|
||||
LastState = [string]$p.LastState
|
||||
Seeded = [int]($p.CredSeeded)
|
||||
CredHash = [string]$p.CredHash # SHA256(user + password) last seeded
|
||||
}
|
||||
}
|
||||
function Set-State {
|
||||
param([string]$Key, [int]$FailCount, [string]$LastState, [int]$Seeded = -1)
|
||||
param([string]$Key, [int]$FailCount, [string]$LastState, [string]$CredHash = '')
|
||||
Set-ItemProperty -Path $Key -Name FailCount -Value $FailCount -Type DWord -EA SilentlyContinue
|
||||
Set-ItemProperty -Path $Key -Name LastState -Value $LastState -Type String -EA SilentlyContinue
|
||||
Set-ItemProperty -Path $Key -Name LastRun -Value (Get-Date -Format 's') -Type String -EA SilentlyContinue
|
||||
if ($Seeded -ge 0) { Set-ItemProperty -Path $Key -Name CredSeeded -Value $Seeded -Type DWord -EA SilentlyContinue }
|
||||
# only overwrite the seeded-credential hash when the caller supplies one
|
||||
if ($CredHash) { Set-ItemProperty -Path $Key -Name CredHash -Value $CredHash -Type String -EA SilentlyContinue }
|
||||
}
|
||||
function Get-CredHash {
|
||||
param([string]$User, [string]$Password)
|
||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||
$b = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes("$User`n$Password"))
|
||||
$sha.Dispose()
|
||||
return ([BitConverter]::ToString($b) -replace '-', '')
|
||||
}
|
||||
|
||||
# --- probes -------------------------------------------------------------
|
||||
@@ -147,8 +155,34 @@ public static class GeCredStore {
|
||||
|
||||
# --- mapping ------------------------------------------------------------
|
||||
function New-Mapping {
|
||||
param([string]$Drive, [string]$Unc, [string]$User, [string]$Password, [string]$HostName)
|
||||
param([string]$Drive, [string]$Unc, [string]$User, [string]$Password, [string]$HostName, [bool]$PreFlight = $false)
|
||||
$letter = $Drive.TrimEnd(':')
|
||||
|
||||
# When a mapping (wrong or unreadable) is being replaced, prove the new
|
||||
# one can be made BEFORE deleting the old: map the target to a spare
|
||||
# letter, then drop it. A failed pre-flight leaves the old mapping in
|
||||
# place. 1219 is the exception - it means the old mapping's own session
|
||||
# is what blocks the new identity, so deleting it is the only way.
|
||||
if ($PreFlight) {
|
||||
$used = @((Get-PSDrive -PSProvider FileSystem -EA SilentlyContinue).Name) + @((Get-SmbMapping -EA SilentlyContinue).LocalPath | ForEach-Object { $_.TrimEnd(':') })
|
||||
$spare = $null
|
||||
foreach ($c in [char[]]'ZYXVUTQPONMLKJIHG') { if ($used -notcontains [string]$c) { $spare = "${c}:"; break } }
|
||||
if ($spare) {
|
||||
try {
|
||||
New-SmbMapping -LocalPath $spare -RemotePath $Unc -UserName $User -Password $Password -Persistent $false -ErrorAction Stop | Out-Null
|
||||
$ok = Test-DriveReadable -Drive $spare -TimeoutSec 8
|
||||
& net use $spare /delete /y 2>&1 | Out-Null
|
||||
if ($ok -ne 'OK') { return "pre-flight via $spare mapped but unreadable ($ok) - old $Drive left in place" }
|
||||
} catch {
|
||||
$m = $_.Exception.Message
|
||||
if ($m -notmatch '1219|more than one user name|Multiple connections') {
|
||||
return "pre-flight via $spare failed: $m - old $Drive left in place"
|
||||
}
|
||||
Write-MapLog "pre-flight hit 1219 - the existing $Drive session blocks the new identity; replacing it" 'WARN'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& net use $Drive /delete /y 2>&1 | Out-Null
|
||||
Remove-Item -Path "HKCU:\Network\$letter" -Recurse -Force -EA SilentlyContinue
|
||||
try {
|
||||
@@ -200,11 +234,16 @@ foreach ($e in $entries) {
|
||||
$target = if ($mapping) { [string]$mapping.RemotePath } else { '' }
|
||||
$wrong = $present -and ($target.TrimEnd('\') -ne $e.Unc)
|
||||
|
||||
# Seed Credential Manager once per profile (idempotent, cheap).
|
||||
if ($st.Seeded -ne 1) {
|
||||
# Seed Credential Manager whenever the HKLM credential differs from what
|
||||
# this profile last seeded (first run, or a password rotation), and again
|
||||
# after every repair. Keeps Windows' own logon-time restore authenticating.
|
||||
$credHash = Get-CredHash -User $e.User -Password $e.Pass
|
||||
if ($st.CredHash -ne $credHash) {
|
||||
$rc = Set-StoredDomainCredential -Target $e.Host -User $e.User -Password $e.Pass
|
||||
if ($rc -eq 0) { Write-MapLog "Seeded Credential Manager for $($e.Host) as $($e.User)"; Set-State -Key $st.Key -FailCount $st.FailCount -LastState $st.LastState -Seeded 1 }
|
||||
else { Write-MapLog "CredWrite for $($e.Host) failed (win32 $rc)" 'WARN' }
|
||||
if ($rc -eq 0) {
|
||||
Write-MapLog "Seeded Credential Manager for $($e.Host) as $($e.User)$(if ($st.CredHash) { ' (credential changed)' })"
|
||||
Set-State -Key $st.Key -FailCount $st.FailCount -LastState $st.LastState -CredHash $credHash
|
||||
} else { Write-MapLog "CredWrite for $($e.Host) failed (win32 $rc)" 'WARN' }
|
||||
}
|
||||
|
||||
$probe = 'SKIPPED'
|
||||
@@ -239,8 +278,8 @@ foreach ($e in $entries) {
|
||||
continue
|
||||
}
|
||||
|
||||
Write-MapLog "Repairing $($e.Drive) -> $($e.Unc)"
|
||||
$err = New-Mapping -Drive $e.Drive -Unc $e.Unc -User $e.User -Password $e.Pass -HostName $e.Host
|
||||
Write-MapLog "Repairing $($e.Drive) -> $($e.Unc)$(if ($present) { ' (pre-flight first, existing mapping kept on failure)' })"
|
||||
$err = New-Mapping -Drive $e.Drive -Unc $e.Unc -User $e.User -Password $e.Pass -HostName $e.Host -PreFlight $present
|
||||
if ($err) {
|
||||
Write-MapLog "Repair FAILED $($e.Drive): $err" 'ERROR'
|
||||
Set-State -Key $st.Key -FailCount $st.FailCount -LastState 'failed'
|
||||
@@ -250,7 +289,11 @@ foreach ($e in $entries) {
|
||||
$verify = Test-DriveReadable -Drive $e.Drive -TimeoutSec $ProbeTimeoutSec
|
||||
if ($verify -eq 'OK') {
|
||||
Write-MapLog "Repaired $($e.Drive) -> $($e.Unc) (verified readable)"
|
||||
Set-State -Key $st.Key -FailCount 0 -LastState 'healthy'
|
||||
# re-seed after every repair so the stored credential matches what
|
||||
# just proved to work
|
||||
$rc = Set-StoredDomainCredential -Target $e.Host -User $e.User -Password $e.Pass
|
||||
if ($rc -ne 0) { Write-MapLog "CredWrite after repair failed (win32 $rc)" 'WARN' }
|
||||
Set-State -Key $st.Key -FailCount 0 -LastState 'healthy' -CredHash $credHash
|
||||
} else {
|
||||
Write-MapLog "Mapped $($e.Drive) but verify failed: $verify" 'ERROR'
|
||||
Set-State -Key $st.Key -FailCount $st.FailCount -LastState 'failed'
|
||||
|
||||
Reference in New Issue
Block a user