Files
shopdb-flask/docs/GE-ENFORCE-CLIENT.md
cproudlock 9d65ef103d geenforce: display-readiness batch (server hardening, PS client wiring, display scope)
Get GE-Enforce closer to running on credential-less Intune/Entra display PCs
that pull manifest + payloads over HTTPS instead of SMB.

Server (plugins/geenforce/api/routes.py):
- Rate-limit + 512MB served-size ceiling on GET /payload/<sha256> (reuses the
  login limiter's cache pattern, config-overridable via GEENFORCE_PAYLOAD_*).
- New tests: payload hardening, manifestblobs model-vs-migration parity, and a
  report-contract test locking the lowercase per-entry report keys.

PS client (plugins/geenforce/client/):
- Fix New-ShopdbReport per-entry key casing to lowercase (name/action/selfhealed/
  exitcode/message) to match what the server reads; the engine emits PascalCase.
- Enforce TLS 1.2 in the network functions.
- Fetch + merge the fleet-wide common scope alongside the pctype scope
  (pctype wins on conflict; -NoCommon opt-out).
- Normalize whatever the engine returns into a well-formed summary.
- Make the empty-cache fail-safe observable: event-log entry + report ping
  instead of a silent exit 0.

Manifest (plugins/geenforce/seed_display_scope.py + docs/GE-ENFORCE-DISPLAY.md):
- Seed a gea-shopfloor-display scope: 4 Edge kiosk drift-heal registry entries
  + 1 data-driven dispatcher (Dashboard/Lobby/3DPrintRoom via display-type.txt).
  Kiosk EXEs stay image-baked; the manifest heals policy/config drift only.
- Documents the common SMB-payload audit (entries needing http/inline before a
  share-less display can inherit common).

Migration registry (shopdb/plugins/alembic_template.py + test):
- Register the pre-existing manifestblobs and the new printersupplyalerts tables
  in PLUGIN_TABLE_OWNERS; update EXPECTED_HEAD_REVISION for geenforce (0002blobs),
  printers (0002supplyalerts), and printedparts (0004txnrev) which had drifted.
2026-07-23 08:16:38 -04:00

7.7 KiB

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": "WJCMM01",
  "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

Every PC inherits the fleet-wide common scope on top of its own pctype scope, mirroring the real GE-Enforce.ps1 (which applies common\manifest.json first, then the pctype's). Invoke-ShopdbEnforce.ps1 fetches the common scope in addition to -Scope and merges it in 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). Pass -NoCommon to enforce the pctype alone, or -CommonScope <name> to inherit a different fleet scope. A run whose -Scope already is the common scope does not merge itself. This is how the three display subtypes (Dashboard, Lobby, 3D Print Room), selected by C:\Enrollment\display-type.txt, pick up shared policy without duplicating it per subtype.

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.