From 80e9c32faefb3733ac4004ab19155bd5812d80e4 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 1 May 2026 12:15:31 -0400 Subject: [PATCH] Add GE-Enforce v2 architecture doc Captures the full picture of how the manifest engine works, why scripts don't need self-heal entries (run from share), credential context (SYSTEM = computer account, requires Mount-SFLDShare for file-level reads), C:\Enrollment vs SFLD share copy distinction, and update workflows. Written in response to a session that wasted time adding redundant manifest entries because this wasn't documented. Companion to scripts/diagnostics/Capture-LockdownState.ps1 and the auditing script in pxe-images/Audit-SFLDShare.ps1. --- docs/ge-enforce-v2-architecture.md | 317 +++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 docs/ge-enforce-v2-architecture.md diff --git a/docs/ge-enforce-v2-architecture.md b/docs/ge-enforce-v2-architecture.md new file mode 100644 index 0000000..62d659c --- /dev/null +++ b/docs/ge-enforce-v2-architecture.md @@ -0,0 +1,317 @@ +# GE-Enforce v2 architecture + +Ongoing fleet enforcement layer for GE Aerospace shopfloor PCs. Replaces the +v1 `Machine-Enforce.ps1` + `main\machineapps\machineapps-manifest.json` +arrangement that DSC-based deployment retired. + +## What it does + +On every user logon (and on a periodic schedule), every shopfloor PC mounts +the SFLD share with credentials provisioned by Azure DSC, reads the +manifest(s) for its PC type, and runs `Install-FromManifest.ps1` to install, +update, or repair anything whose detection rule fails. The detection-driven +design means a freshly-imaged PC in a known-good state no-ops; an out-of- +date or tampered PC self-heals on the next cycle. + +The same entry-points handle: + +- Initial shopfloor app install on first logon after PXE imaging. +- Drift correction (e.g. user uninstalled UDC, vendor patch tampered with a + config file, manifest version bumped fleet-wide). +- One-shot per-bay actions like UDC data restore from a swap-source PC. + +## Components + +| Piece | Lives at | Role | +|-------|----------|------| +| `GE-Enforce.ps1` | `playbook/shopfloor-setup/common/GE-Enforce.ps1` | Logon dispatcher. Mounts share, finds manifests, calls Install-FromManifest. | +| `Register-GEEnforce.ps1` | `playbook/shopfloor-setup/common/Register-GEEnforce.ps1` | Registers the scheduled task (`GE Shopfloor Machine Apps Enforce`) at imaging time. | +| `Install-FromManifest.ps1` | `playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1` | Manifest interpreter. Iterates entries, runs detection, fires installers. | +| `Mount-SFLDShare` | `playbook/shopfloor-setup/Shopfloor/lib/Restore-EDncReg.ps1` | Reads creds from `HKLM:\SOFTWARE\GE\SFLD\Credentials\*` and `net use`s the share with the SFLD user identity. | +| Manifests | SFLD share `\manifest.json` | Declarative list of entries to enforce. | +| Site config | `playbook/shopfloor-setup/site-config.json` (deployed to `C:\Enrollment\site-config.json`) | Per-PC-type share path overrides + startup/desktop/taskbar layout. | + +## Lifecycle + +``` +Imaging time (PXE) + startnet.cmd xcopies shopfloor-setup -> W:\Enrollment\ + Run-ShopfloorSetup.ps1 runs the imaging-phase scripts + Register-GEEnforce.ps1 registers the scheduled task + +First user logon (post-imaging, post-SFLD-DSC creds delivered) + Scheduled task fires GE-Enforce.ps1 + GE-Enforce reads HKLM:\SOFTWARE\GE\SFLD\Credentials -> Mount-SFLDShare W: + Reads pc-type from C:\Enrollment\pc-type.txt + For each scope (common, then ): + Reads \manifest.json + Calls Install-FromManifest.ps1 -ManifestPath ... -InstallerRoot W:\ + Install-FromManifest iterates each Application entry: + Apply PCTypes filter (skip if entry doesn't apply to this PC) + Apply TargetMachineNumbers filter (skip if applicable) + Run Detection - if installed at expected version/hash/state, skip + Else run installer per Type (MSI/EXE/CMD/PS1/File/INF/Registry) + Log result to C:\Logs\Shopfloor\enforce-YYYYMMDD.log + Unmounts W: + +Every subsequent logon + Same dispatcher fires. No-ops on already-correct state. Self-heals drift. +``` + +## SFLD share layout (v2) + +``` +\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\ + _meta\ + README.md layout doc + manifest-schema.json JSON schema for entries + history\ timestamped backup of each manifest on push + _outputs\ + ntlars-backups\ eDNC INI backups per machine number + logs\ client-uploaded diagnostics + + common\ applies to every PC type + manifest.json + apps\ MSI / EXE / CMD installers + configs\ data files (XML, fonts, txt) + scripts\ PS1 / BAT / CMD helpers + + standard-machine\ Standard PC type, subtype Machine + manifest.json + apps\ configs\ scripts\ + + cmm\ display\ genspect\ keyence\ lab\ waxandtrace\ + per-PC-type +``` + +`_meta`, `_outputs`, and per-PC-type dirs each have `apps/`, `configs/`, +`scripts/` siblings to manifest.json, referenced by relative paths in manifest +entries (`Source: scripts/Foo.ps1` => `\scripts\Foo.ps1`). + +### v1 vs v2 + +v1 used `\shared\dt\shopfloor\main\machineapps\machineapps-manifest.json` as +the single Standard-Machine manifest. v2 reorganized to per-PC-type +manifests so CMM/Keyence/Display PCs get their own scope without filtering +the whole fleet manifest by `PCTypes`. The v1 path may still exist on the +share for backwards compat; v2 GE-Enforce ignores it. Do not edit the v1 +manifest for v2-deployed sites. + +## Manifest entry schema + +```json +{ + "Version": "2.0", + "_comment": "...", + "Applications": [ + { + "_comment": "Per-entry context: install path, version pin reasons, rotation procedure", + "Name": "", + "Type": "", + "Installer": "", + "Source": "", + "Destination": "", + "Script": "", + "Args": "", + "InstallArgs": "", + "DetectionMethod": "", + "DetectionPath": "", + "DetectionName": "", + "DetectionValue": "", + "PCTypes": ["Standard", "CMM", "..."], + "PCSubTypes": ["Machine"], + "TargetMachineNumbers": ["1234", "5678"] + } + ] +} +``` + +### Type semantics (the part that's easy to get wrong) + +| Type | What it does | Local artifact? | Self-heal pattern | +|------|-------------|-----------------|-------------------| +| `MSI` | `msiexec /i ` + InstallArgs | Vendor footprint (uninstall reg key, files) | Detection on uninstall reg DisplayVersion or product File | +| `EXE` | Direct exec of installer EXE | Vendor footprint | Same | +| `CMD` | `cmd /c ` (a wrapper script) | Whatever the wrapper deploys | Same | +| `PS1` | `powershell.exe -File ` - **executes from share, no local copy** | None - runs in place | NOT NEEDED. Update share file = next logon runs new code. No Hash entry required because there's nothing on disk to drift. | +| `File` | `Copy-Item -Source -Destination ` | The file at Destination | **Hash detection IS needed.** Drift = re-copy. | +| `INF` | `pnputil /add-driver` | Driver staged | Detection via PnP | +| `Registry` | Detection-only (no install side; entry is purely a no-op trigger) | Whatever wrote the key | n/a | + +**`Type=PS1` is for scripts that need to RUN every cycle, not be DEPLOYED.** +Common pattern: `DetectionMethod: Always` (always fires). Used for one-shot +cleanups (e.g. `Restore-UDCData.ps1` consumes a swap backup and self-no-ops +when none is waiting). + +If you want a PS1 to ALSO be deployed to disk somewhere, that's a separate +`Type=File` entry copying it - very rarely useful since GE-Enforce already +runs it from the share. + +### Detection methods + +- **`File`** - `Test-Path` on `DetectionPath`. Optional `DetectionName` for property. +- **`Registry`** - `Test-Path` on registry key. With `DetectionName` + `DetectionValue`, must match. +- **`FileVersion`** - PE FileVersion of `DetectionPath` matches `DetectionValue` exactly. +- **`Hash`** - SHA256 of `DetectionPath` matches `DetectionValue`. Case-insensitive. +- **`MarkerFile`** - `Test-Path` on a marker file (engine creates it after a successful PS1 run). For PS1 entries that should NOT re-fire after one success. +- **`ValueMatches`** - Registry value exact match. +- **`Always`** - Never matches; entry fires every cycle. For per-cycle cleanup/check scripts. + +### Filters + +- `PCTypes` - applies only if `C:\Enrollment\pc-type.txt` matches one of the listed values. Skips otherwise. +- `PCSubTypes` - same, against `C:\Enrollment\pc-subtype.txt`. +- `TargetMachineNumbers` - applies only if local `udc_settings.json` MachineNumber is in the list. Used for variant-specific (Fanuc/Okuma/Makino) entries. + +## Credential context (the trap that bit us 2026-05-01) + +GE-Enforce runs as `NT AUTHORITY\SYSTEM` (scheduled task at startup + +logon). SYSTEM authenticates to remote SMB as the **computer account** +(`DOMAIN\HOSTNAME$`), not as a user. + +The SFLD share's ACL grants top-level enumeration to authenticated computers +but file-level reads only to a specific SFLD user. So `Test-Path` on a UNC +path from SYSTEM: + +- **Top-level dir** like `\\tsgwp00525...\shared\dt\shopfloor\backup\udc\` -> True +- **Bay-level subdir** like `...\backup\udc\3207\` -> True (depending on ACL) +- **File-level** like `...\backup\udc\3207\CurrentData.json` -> **`$false`**, indistinguishable from "file not found" + +Symptom: scripts that `Test-Path` raw UNC paths from SYSTEM context silently +log "absent" / "no work" while the files actually exist. Real cause is +access denied returning False. Failure mode is invisible. + +**Always** use `Mount-SFLDShare` (in `Shopfloor\lib\Restore-EDncReg.ps1`) +before file access. It reads creds from `HKLM:\SOFTWARE\GE\SFLD\Credentials\*` +and `net use W: \\... /user: /persistent:no`. Subsequent +file operations on `W:\...` succeed. + +If creds are missing in the registry (e.g. SFLD-DSC bootstrap didn't run), +`Mount-SFLDShare` returns `$false`. Scripts should fail-fast with a clear +ERROR rather than continue with raw-UNC access. + +## C:\Enrollment\shopfloor-setup\ vs SFLD share + +Two separate copies of overlapping content with different roles: + +| Path | Source | Used by | Updated when | +|------|--------|---------|--------------| +| `C:\Enrollment\shopfloor-setup\` | PXE imaging copy from `\\10.9.100.1\enrollment\shopfloor-setup\` | Imaging-flow scripts: `Run-ShopfloorSetup.ps1`, `Stage-Dispatcher.ps1`, `Set-MachineNumber.ps1` -> `Update-MachineNumber.ps1` | Re-image only | +| SFLD share `\\` | Direct upload | GE-Enforce.ps1 / Install-FromManifest.ps1 (every logon) | Direct file upload to share | + +Implication for hot-fixing scripts: a fix to `Restore-UDCData.ps1` needs to +land on the SFLD share (`standard-machine/scripts/Restore-UDCData.ps1`) for +the manifest engine to pick it up. Updating only the PXE enrollment share +helps NEW imaging but not already-imaged PCs. + +## site-config.json paths + +Defines per-PC-type share roots used by Update-MachineNumber.ps1 and +Backup-UDCData.ps1 logic outside the manifest engine: + +| Field (under `pcProfiles.Standard-Machine`) | Used for | +|---------------------------------------------|----------| +| `machineappsSharePath` | v1 legacy machineapps share (mostly unused in v2) | +| `ntlarsBackupSharePath` | per-machine `.reg` backups (eDNC) | +| `udcBackupSharePath` | per-bay live UDC data backup (CurrentData.json + ArchivedData/) | +| `udcSettingsSharePath` | per-bay `udc_settings_.json` (UDC settings overrides) | + +These paths are read by Update-MachineNumber.ps1 placeholder->real +transition logic to mount the right share + restore the right data when a +tech sets a real machine number on a previously-9999 PC. + +## Update workflow + +### Bumping a vendor app version + +1. Drop new `.msi` / `.exe` in `\apps\` on the share. +2. Update the matching manifest entry: `Installer` filename + `DetectionValue`. +3. Save `\manifest.json`. +4. Optional: copy current manifest to `_meta\history\-.json`. +5. Every PC of that scope picks up the change next logon. + +### Rotating a deployed config (eMxInfo.txt, udc_webserver_settings.json) + +1. Overwrite the file in `\configs\` on the share. +2. Recompute SHA256: + ```powershell + (Get-FileHash .\udc_webserver_settings.json -Algorithm SHA256).Hash + ``` +3. Paste into the manifest entry's `DetectionValue`. +4. Save manifest. + +### Hot-fixing a script (Restore-UDCData.ps1, etc.) + +1. Patch script in repo. +2. Copy to `\scripts\.ps1` on the SFLD share. +3. (No manifest edit needed - `Type=PS1` runs from share.) +4. Every PC of that scope runs the new version next logon. + +### Adding a new entry + +1. Decide scope: cross-PC-type goes in `common/`, PC-type-specific in + `/`. Verify the entry isn't already present in either. +2. Pick `Type` based on what the entry is: vendor installer, config file + deploy, or per-cycle script run. +3. For `File` type: include source on the share; pick `DetectionMethod=Hash` + with a precomputed SHA256 so drift triggers re-deploy. +4. For `PS1` type: place script in `\scripts\`; use + `DetectionMethod=Always` for per-cycle, or `MarkerFile` for one-shot. +5. For installer types: provide `Installer` path, `Type`, `InstallArgs`, + detection rule that distinguishes "installed at this version" from + "missing or wrong version". +6. Add a `_comment` field documenting WHY this entry exists, where the + target installs, and how to rotate the version. +7. Save + push. + +### Rolling back + +History snapshots live at `_meta\history\-.json`. Manual revert: +copy old snapshot back over `\manifest.json`. Engine picks it up next +cycle. + +## Logs + +Per-PC log file: `C:\Logs\Shopfloor\enforce-YYYYMMDD.log` + +Format: + +``` +[YYYY-MM-DD HH:MM:SS] [INFO] Mounted \\... as W: +[YYYY-MM-DD HH:MM:SS] [INFO] standard\manifest.json not on share - no type-specific apps +[YYYY-MM-DD HH:MM:SS] [INFO] ---- Processing scope: common ---- +[YYYY-MM-DD HH:MM:SS] [INFO] Manifest lists N app(s) +[YYYY-MM-DD HH:MM:SS] [INFO] ==> Adobe Acrobat Reader DC +[YYYY-MM-DD HH:MM:SS] [INFO] Already installed at expected version - skipping +[YYYY-MM-DD HH:MM:SS] [INFO] ==> WJF Defect Tracker +[YYYY-MM-DD HH:MM:SS] [INFO] msiexec: W:\common\apps\WJF_Defect_Tracker.msi +[YYYY-MM-DD HH:MM:SS] [INFO] verbose log: C:\Logs\Shopfloor\msi-WJF_Defect_Tracker.log +[YYYY-MM-DD HH:MM:SS] [INFO] Exit 0 - SUCCESS +``` + +Per-installer logs go alongside (`msi-*.log`, per-script transcripts). +PS1 entry stdout/stderr surfaces in the main enforce log. + +If detection succeeds first time on a freshly-imaged PC, every entry logs +"Already installed at expected version - skipping" - the no-op path +proves the manifest is in sync with the image. + +## Common audit failures and their cause + +| Symptom | Likely cause | +|---------|-------------| +| `` MSI redeploys every cycle | Detection rule's path or DetectionValue doesn't match the actual install footprint. Verify PE FileVersion is 4-part vs DisplayVersion is 3-part. Verify 32-bit installers go under `WOW6432Node`. | +| Script logs "absent" or "no work" repeatedly while file is visible interactively | Running as SYSTEM with raw-UNC access. Switch to `Mount-SFLDShare`. | +| New script change doesn't take effect | Edited the wrong copy. C:\Enrollment\ is imaging-time; SFLD share is runtime. | +| Manifest entry skipped silently | `PCTypes` / `PCSubTypes` filter excludes this PC. Check `C:\Enrollment\pc-type.txt`. | +| Two PCs racing for the same per-bay backup | Both have the same `udc_settings.json` MachineNumber. Find + decommission the second one. | + +## See also + +- `playbook/shopfloor-setup/_meta/README.md` (on the SFLD share local mirror) - canonical share layout doc +- `pxe-images/Audit-SFLDShare.ps1` - audit script that walks the share and + validates layout + canonical hashes + detection-rule sanity +- `playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1` + - imaging-time progress monitor that watches for the upstream signals + (SFLD reg key, DSCDeployment.log, Consume Credentials task) before + GE-Enforce can even start