# GE-Enforce.ps1 - Unified shopfloor enforcer. # # Replaces the per-type enforcers (CMM-Enforce, Keyence-Enforce, # Machine-Enforce, Common-Enforce, Acrobat-Enforce). Runs as a single SYSTEM # scheduled task with multiple triggers (at-logon, periodic 5-min, and the # three shift-change windows). # # On each invocation: # 1. Reads C:\Enrollment\pc-type.txt and pc-subtype.txt. # 2. Mounts the tsgwp00525 SFLD share using creds from HKLM. # 3. Processes common\manifest.json (always, with per-entry PCTypes filter). # 4. Processes \manifest.json if it exists (enforces type-specific # apps like CMM, Keyence, Display, Keyence, etc.). # 5. Processes -\manifest.json if it exists # (the "standard-machine" case). # # Graceful degradation: # - pc-type.txt missing -> log + exit 0 (PC is pre-imaging) # - SFLD creds missing -> log + exit 0 (Azure DSC hasn't provisioned yet) # - Share unreachable -> log + exit 0 (off-network, retry next cycle) # - Per-entry install failure -> log + continue to next entry # # Always exits 0 so the scheduled task "last run result" stays clean. Truth # is in the log: C:\Logs\Shopfloor\enforce-YYYYMMDD.log. $ErrorActionPreference = 'Continue' $installRoot = 'C:\Program Files\GE\Shopfloor' $libPath = Join-Path $installRoot 'lib\Install-FromManifest.ps1' $logDir = 'C:\Logs\Shopfloor' $logFile = Join-Path $logDir ('enforce-{0}.log' -f (Get-Date -Format 'yyyyMMdd')) $driveLetter = 'W:' # distinct from S: (shopfloor share), T:/U: (legacy enforcers) if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } function Write-EnforceLog { param([string]$Message, [string]$Level = 'INFO') $line = '[{0}] [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message Write-Host $line Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue } Write-EnforceLog '================================================================' Write-EnforceLog "=== GE-Enforce session start (PID $PID, user $env:USERNAME) ===" Write-EnforceLog '================================================================' # --- Log retention prune --- # Drops *.log files older than $retentionDays from the shopfloor log roots. # Cheap (flat dir scan, no recursion). Runs every cycle. Today's # enforce-YYYYMMDD.log is never touched (LastWriteTime = now). $retentionDays = 30 $prunedCount = 0 foreach ($root in @('C:\Logs\Shopfloor', 'C:\Logs\SFLD', 'C:\Logs\Keyence')) { if (-not (Test-Path $root)) { continue } $cutoff = (Get-Date).AddDays(-$retentionDays) Get-ChildItem -Path $root -Filter '*.log' -File -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $cutoff } | ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop; $prunedCount++ } catch {} } } if ($prunedCount -gt 0) { Write-EnforceLog "Pruned $prunedCount log file(s) older than $retentionDays days" } # --- pc-type --- $pcTypeFile = 'C:\Enrollment\pc-type.txt' $pcSubTypeFile = 'C:\Enrollment\pc-subtype.txt' if (-not (Test-Path $pcTypeFile)) { Write-EnforceLog "pc-type.txt not found - PC is pre-imaging, nothing to enforce" exit 0 } $pcType = (Get-Content -LiteralPath $pcTypeFile -First 1 -ErrorAction SilentlyContinue).Trim() $pcSubType = if (Test-Path $pcSubTypeFile) { (Get-Content -LiteralPath $pcSubTypeFile -First 1 -ErrorAction SilentlyContinue).Trim() } else { '' } Write-EnforceLog "PCType: $pcType$(if ($pcSubType) { " / $pcSubType" })" # --- site-config --- $siteConfigFile = 'C:\Enrollment\site-config.json' if (-not (Test-Path $siteConfigFile)) { Write-EnforceLog "site-config.json not found at $siteConfigFile" 'ERROR' exit 0 } try { $siteConfig = Get-Content -LiteralPath $siteConfigFile -Raw | ConvertFrom-Json } catch { Write-EnforceLog "site-config.json parse failed: $_" 'ERROR' exit 0 } $shopfloorShareRoot = $siteConfig.shopfloorShareRoot if (-not $shopfloorShareRoot) { # Fallback: derive from commonAppsSharePath if the new field isn't set. $capp = $siteConfig.common.commonAppsSharePath if ($capp) { # \\...\shared\dt\shopfloor\common\apps -> \\...\shared\dt\shopfloor $shopfloorShareRoot = ($capp -replace '\\common\\apps$', '') } } if (-not $shopfloorShareRoot) { Write-EnforceLog 'No shopfloorShareRoot derivable from site-config - nothing to enforce' 'ERROR' exit 0 } Write-EnforceLog "Shopfloor share root: $shopfloorShareRoot" # --- SFLD credential lookup (written by Azure DSC) --- function Get-SFLDCredential { param([string]$ServerName) $basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials' if (-not (Test-Path $basePath)) { return $null } foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) { $props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue if (-not $props -or -not $props.TargetHost) { continue } if ($props.TargetHost -eq $ServerName -or $props.TargetHost -like "$ServerName.*" -or $ServerName -like "$($props.TargetHost).*") { return @{ Username = $props.Username Password = $props.Password TargetHost = $props.TargetHost KeyName = $entry.PSChildName } } } return $null } $serverName = ($shopfloorShareRoot -replace '^\\\\', '') -split '\\' | Select-Object -First 1 $cred = Get-SFLDCredential -ServerName $serverName if (-not $cred -or -not $cred.Username -or -not $cred.Password) { Write-EnforceLog "No SFLD credential for $serverName yet (Azure DSC has not provisioned it) - will retry next cycle" exit 0 } Write-EnforceLog "Credential: $($cred.KeyName) (user: $($cred.Username))" # --- Mount --- & net use $driveLetter /delete /y 2>$null | Out-Null $netResult = & net use $driveLetter $shopfloorShareRoot /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1 if ($LASTEXITCODE -ne 0) { Write-EnforceLog "net use failed (exit $LASTEXITCODE): $netResult" 'WARN' Write-EnforceLog 'Share unreachable - probably off-network. Will retry at next cycle.' exit 0 } Write-EnforceLog "Mounted $shopfloorShareRoot as $driveLetter" try { if (-not (Test-Path $libPath)) { Write-EnforceLog "Install-FromManifest.ps1 not found at $libPath" 'ERROR' return } # --- Process manifests in order: common, per-type, per-type+subtype --- $targets = @() $commonRoot = Join-Path $driveLetter 'common' $commonManifest = Join-Path $commonRoot 'manifest.json' if (Test-Path $commonManifest) { $targets += [pscustomobject]@{ Label = 'common'; Manifest = $commonManifest; Root = $commonRoot } } else { Write-EnforceLog "common\manifest.json missing - skipping common scope" 'WARN' } $typeDir = $pcType.ToLower() if ($typeDir) { $typeRoot = Join-Path $driveLetter $typeDir $typeManifest = Join-Path $typeRoot 'manifest.json' if (Test-Path $typeManifest) { $targets += [pscustomobject]@{ Label = $pcType; Manifest = $typeManifest; Root = $typeRoot } } else { Write-EnforceLog "$typeDir\manifest.json not on share - no type-specific apps for $pcType" } } if ($pcSubType) { $stDir = ($pcType + '-' + $pcSubType).ToLower() $stRoot = Join-Path $driveLetter $stDir $stManifest = Join-Path $stRoot 'manifest.json' if (Test-Path $stManifest) { $targets += [pscustomobject]@{ Label = "$pcType-$pcSubType"; Manifest = $stManifest; Root = $stRoot } } } $scopeResults = @() foreach ($t in $targets) { Write-EnforceLog "---- Processing scope: $($t.Label) ----" Write-EnforceLog " Manifest: $($t.Manifest)" Write-EnforceLog " Root: $($t.Root)" & $libPath -ManifestPath $t.Manifest -InstallerRoot $t.Root -LogFile $logFile -PCType $pcType -PCSubType $pcSubType $rc = $LASTEXITCODE Write-EnforceLog " Install-FromManifest returned $rc" $scopeResults += [pscustomobject]@{ Label = $t.Label; ExitCode = $rc } } # ------------------------------------------------------------------ # Status write-back to _outputs/logs//status.json on share. # Consumed by the management app / fleet dashboard to answer # "which PCs checked in, when, and what version did each install." # Writes under the mounted drive using SFLD creds; gracefully # continues if the share path is not writable. # ------------------------------------------------------------------ try { # Live NetBIOS name from kernel - not $env:COMPUTERNAME, which is # cached in the process env block and goes stale after a post-image # rename on Intune-managed PCs. $hostname = [System.Environment]::MachineName $statusDir = Join-Path (Join-Path $driveLetter '_outputs') (Join-Path 'logs' $hostname) if (-not (Test-Path $statusDir)) { New-Item -Path $statusDir -ItemType Directory -Force -ErrorAction Stop | Out-Null } $statusFile = Join-Path $statusDir 'status.json' # Walk each processed manifest and pull current DetectionValue per entry. $installedVersions = @{} foreach ($t in $targets) { try { $cfg = Get-Content -LiteralPath $t.Manifest -Raw | ConvertFrom-Json foreach ($a in $cfg.Applications) { if (-not $a.DetectionPath) { continue } $key = "$($t.Label)/$($a.Name)" $val = $null switch ($a.DetectionMethod) { 'Registry' { if ((Test-Path $a.DetectionPath) -and $a.DetectionName) { $p = Get-ItemProperty -Path $a.DetectionPath -Name $a.DetectionName -ErrorAction SilentlyContinue if ($p) { $val = "$($p.$($a.DetectionName))" } } } 'FileVersion' { if (Test-Path $a.DetectionPath) { $val = (Get-Item $a.DetectionPath).VersionInfo.FileVersion } } 'File' { if (Test-Path $a.DetectionPath) { $val = 'present' } } 'Hash' { if (Test-Path $a.DetectionPath) { $val = (Get-FileHash -Path $a.DetectionPath -Algorithm SHA256).Hash } } 'MarkerFile' { if (Test-Path $a.DetectionPath) { $val = 'marker present' } } 'Always' { $val = 'n/a (Always)' } 'pnputil' { $val = 'pnputil-managed' } } $installedVersions[$key] = $val } } catch { Write-EnforceLog " status: manifest introspection for $($t.Label) failed: $_" 'WARN' } } $status = [ordered]@{ hostname = $hostname lastCheckIn = (Get-Date).ToUniversalTime().ToString('o') pcType = $pcType pcSubType = $pcSubType enforcerVersion = '2.0' shopfloorShareRoot = $shopfloorShareRoot scopesProcessed = $scopeResults installedVersions = $installedVersions } | ConvertTo-Json -Depth 6 Set-Content -Path $statusFile -Value $status -Encoding ascii -ErrorAction Stop Write-EnforceLog "Status written to $statusFile" } catch { Write-EnforceLog "Status write-back failed (share may be read-only for this user): $_" 'WARN' } } finally { & net use $driveLetter /delete /y 2>$null | Out-Null Write-EnforceLog "Unmounted $driveLetter" Write-EnforceLog '=== GE-Enforce session end ===' } exit 0