# 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