Files
shopdb-flask/docs/GE-ENFORCE-CLIENT.md
cproudlock 4995456136 docs: take one site's name, hosts and paths off the public wiki
The publishability gate caught internal tooling names and developer paths but
nothing site-specific, so roughly sixty leaks reached the wiki: the site name in
ten documents, real fleet hostnames in the collector and GE-Enforce examples, an
internal database name through the whole import guide, imaging-share paths, and
a maintainer's username as the Deciders line of every ADR and inside a generated
curl example.

None of it is a security matter on an air-gapped fleet. It matters because these
pages are read by engineers at other plants, and a document that names one site
throughout reads as that site's notes rather than a product's documentation -
which is exactly what it then gets treated as.

Examples now use neutral hostnames, the site is "the reference site" where the
distinction carries meaning, and ADRs are decided by "ShopDB maintainers". The
gate carries all of these patterns, so the next one fails a build.

Two documents leave docs/ because they were never written for an outside reader.
PROJECT-REVIEW.md is an internal health memo pinned to a commit from July, whose
headline finding (an untracked playbook) has since been fixed - it is history,
and git holds it. PILOT-DEPLOY.md is one site's own cutover runbook, complete
with a "re-measure before publishing" placeholder; it moves next to the loader
it belongs to, in scripts/site_imports/wjf/.

ADR-015 is AMENDED rather than rewritten. Its enforcement section still said
report-only and its backlog still listed hardcodes that are now cleared, which
left the record contradicting itself. The amendment says what changed and why
the report-only period ended; the original text stays, because what the decision
looked like when it was taken is the part worth keeping.

Also corrects llms.txt's response envelope, which had errors at the top level
and pagination at meta.total. Both are nested one deeper, so anything written
against that description read undefined on every error it tried to handle.
2026-08-14 15:38:27 -04:00

177 lines
8.1 KiB
Markdown

# GE-Enforce client integration (shopdb manifest source + reporting)
This is the client-side contract for the GE-Enforce manifest-store plugin: how a
PC sources its install manifest from shopdb instead of a share file, and how it
reports its enforcement result back. It pairs with the plugin proposal in
`docs/proposals/ge-enforce-plugin.md`.
The reference kit lives in `plugins/geenforce/client/`:
- `ShopdbEnforceClient.psm1` - fetch (with ETag + last-known-good cache),
shadow compare, and report helpers.
- `Invoke-ShopdbEnforce.ps1` - a reference orchestrator that fetches a manifest,
runs the UNCHANGED engine against it, and reports the result.
These are site-neutral references, not the live dispatcher. A site adapts them
into its GE-Enforce.ps1 flow. The engine (`Install-FromManifest.ps1`),
detection, self-heal, and SMB payload resolution are untouched - only the source
of the manifest JSON moves, plus a result report.
## What does NOT change
- The engine and its four filters, all detection methods, self-heal, marker
files, and SMB payload staging.
- Payload transport for `smb` rows: the client still mounts the share and
resolves `apps/...` paths exactly as today. Only the manifest JSON source moves.
- The fail-safe posture: any error exits 0. A PC is never blocked or broken
because shopdb is unreachable.
## Configuration
Registry (provisioned by Azure DSC, same channel as the SFLD credentials):
```
HKLM:\SOFTWARE\GE\ShopDB
BaseUrl https://shopdb.<site>.geaerospace.net
ApiToken <a geenforce.fetch (+ geenforce.report) managed service token>
```
Mint the token in shopdb: Settings > API Tokens, scopes `geenforce.fetch` and
`geenforce.report`. It is a service token (owner must hold those permissions).
## Fetch contract
```
GET /api/geenforce/manifest?pctype=<scope>[&phase=runtime]
X-API-Key: <token>
If-None-Match: <cached ETag> (optional)
```
- `200` - body is the full published manifest JSON for the scope (fat client:
the engine filters locally, exactly as today). Response headers carry `ETag`
and `X-Manifest-Version`. Cache the body + ETag + version.
- `304` - your cached copy is current; use it.
- `404` - no such scope, or the scope has no published version yet.
- Network failure - enforce from the last-known-good cached manifest (the kit
does this automatically) and log a warning.
The served manifest is always the current PUBLISHED snapshot, never a live draft
being edited in shopdb, so a half-finished edit can never reach a PC.
## Report contract
Each enforcement cycle, POST the result (best-effort; a failed report never
fails the cycle):
```
POST /api/geenforce/report
X-API-Key: <token>
Content-Type: application/json
{
"hostname": "CMMPC01",
"scopename": "gea-shopfloor-cmm",
"appliedversion": 3, // the published version you actually ran
"enforcerversion": "2.6",
"counts": { "installed": 1, "skipped": 3, "failed": 0, "filtered": 2 },
"results": [
{ "name": "PC-DMIS 2019 R2", "action": "installed", "selfhealed": true },
{ "name": "Protect Viewer", "action": "skipped" },
{ "name": "eDNC", "action": "failed", "exitcode": 1603,
"message": "MSI 1603" }
]
}
```
- `appliedversion` lets shopdb show which PCs received the latest manifest
(`receivedlatest` in the fleet view).
- `action` per entry: `installed` (fired - a self-heal when it should already be
present), `skipped` (detected present), `failed`, `filtered`. `selfhealed`
marks a drift correction.
- shopdb keeps the latest report per (hostname, scope, phase) plus history, and
surfaces it under GE-Enforce > Enforcement Reports.
The engine already computes these counts (`installed/skipped/failed/pcFiltered`
at the end of its main loop) and knows each entry's action; shape them into the
`results` list at the call site (`New-ShopdbReport` in the kit takes a summary
with `Installed/Skipped/Failed/Filtered` + a `Results` list).
The engine emits per-entry outcomes in PascalCase (`Name/Action/SelfHealed/
ExitCode/Message`); `New-ShopdbReport` maps every per-entry key down to the
lowercase names above (`name/action/selfhealed/exitcode/message`) before POST,
so the entire wire contract shopdb reads is lowercase. `ConvertTo-ShopdbSummary`
first normalizes whatever the engine returns (a well-formed summary, a bare
return code, `$null`, or several emitted objects) into the count/results shape
`New-ShopdbReport` expects, so a not-yet-compliant engine still produces a valid
report.
## Common-scope inheritance (opt-in, OFF by default)
By default a PC enforces its `-Scope` ALONE. Pass `-IncludeCommon` to also fetch
the fleet-wide `common` scope and merge it on top, mirroring the real
GE-Enforce.ps1 (which applies `common\manifest.json` first, then the pctype's).
When enabled, `Invoke-ShopdbEnforce.ps1` fetches `common` in addition to
`-Scope` and merges it via `Merge-ShopdbManifests`:
- entries are keyed by `Name` (case-insensitive);
- common's unique entries come first, then all pctype entries (common enforces
ahead of the pctype, as on the share);
- on a `Name` conflict the pctype entry wins (its override replaces common's).
Common is fetched over the same fail-safe path (ETag + last-known-good cache).
`-CommonScope <name>` inherits a different fleet scope; a run whose `-Scope`
already is the common scope does not merge itself.
Displays do NOT use this: the `gea-shopfloor-display` scope is self-sufficient,
so the display scheduled task omits `-IncludeCommon`. Common-merge exists for a
future share-less non-display PC that genuinely needs the fleet-wide entries
(which would first require repackaging common's SMB payloads as http/inline).
The three display subtypes (Dashboard, Lobby, 3D Print Room), selected by
`C:\Enrollment\display-type.txt`, carry their shared policy inside the display
scope itself, not via common.
## Fail-safe is observable, not silent
Any error still exits 0 - a bad web app never blocks or breaks a PC. But a fresh
display with an EMPTY cache (first boot, shopdb unreachable or the token
rejected with 401 / a TLS-trust failure) would otherwise enforce nothing
*silently*. When no manifest and no cache are available, the kit:
- writes a Windows Application event-log entry (source `ShopdbEnforce`, event id
1001, type Error) naming the scope and the reason (HTTP status or transport
error), and
- fires a best-effort report ping (counts `failed: 1`, a single
`(manifest-fetch)` result carrying the reason) so the miss surfaces under
GE-Enforce > Enforcement Reports.
The cycle still exits 0; the signal just makes the no-enforcement state visible.
## Cutover (safe, staged)
1. **Configure** the registry values on a canary PC; mint the token.
2. **Shadow mode**: run `Invoke-ShopdbEnforce.ps1 -ShadowMode -ShareManifestPath
<current share manifest>`. It installs from the SHARE (no behavior change),
fetches the shopdb manifest, logs any diff, and reports. Watch for zero diffs
across one PC of every pctype for ~20 cycles.
3. **Read cutover**: drop `-ShadowMode`. The engine now runs against the
shopdb-sourced manifest; payloads still come from the share. Rollback is a
one-line revert to the share-sourced call. Keep exporting manifests from
shopdb to the share (GE-Enforce > Manifests > Export to Share) so the
share stays a break-glass copy.
4. **Payload migration** (optional, later): move small scripts/configs to
`http`/`inline` payloads, verified by `payloadsha256`. Big MSIs stay on SMB.
Do not cut a fleet over before the shadow diffs are clean. Preinstall
(`phase=preinstall`) stays share-sourced until its own cutover is planned - it
runs before enrollment provisions a token.
## Security notes
- The client runs as SYSTEM, so shopdb's TLS certificate must be in the machine
trust store (air-gapped/self-signed sites provision the CA via the same DSC
step as the token).
- `http`/`inline` payloads are verified against `payloadsha256` before running,
independent of how the entry detects install state. This is the real integrity
guarantee and holds even over plain HTTP inside a trusted segment.
- The token is a scoped service token: it can fetch manifests and report, and
nothing else.