Files
pxe-server/docs/ge-enforce-v2-architecture.md
cproudlock 80e9c32fae 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.
2026-05-01 12:15:31 -04:00

318 lines
15 KiB
Markdown

# 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 `<scope>\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 <pctype>):
Reads <scope>\manifest.json
Calls Install-FromManifest.ps1 -ManifestPath ... -InstallerRoot W:\<scope>
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` => `<scope>\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": "<unique label, also shown in log>",
"Type": "<MSI|EXE|CMD|PS1|File|INF|Registry>",
"Installer": "<path-relative-to-scope>",
"Source": "<for Type=File: path-relative-to-scope>",
"Destination": "<for Type=File: absolute disk path>",
"Script": "<for Type=PS1: path-relative-to-scope>",
"Args": "<optional: extra args>",
"InstallArgs": "<optional: msiexec/exe args>",
"DetectionMethod": "<File|Registry|FileVersion|Hash|MarkerFile|ValueMatches|Always>",
"DetectionPath": "<absolute disk path, or HKLM:\\... for Registry>",
"DetectionName": "<for Registry: value name>",
"DetectionValue": "<expected value or hash>",
"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 <share path>` + 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 <share path>` (a wrapper script) | Whatever the wrapper deploys | Same |
| `PS1` | `powershell.exe -File <share path>` - **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 <share> -Destination <disk>` | 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:<sflduser> <pw> /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 `\<scope>\` | 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_<n>.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 `<scope>\apps\` on the share.
2. Update the matching manifest entry: `Installer` filename + `DetectionValue`.
3. Save `<scope>\manifest.json`.
4. Optional: copy current manifest to `_meta\history\<date>-<scope>.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 `<scope>\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 `<scope>\scripts\<name>.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
`<pctype>/`. 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 `<scope>\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\<date>-<scope>.json`. Manual revert:
copy old snapshot back over `<scope>\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 |
|---------|-------------|
| `<vendor>` 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