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.
171 lines
7.7 KiB
Markdown
171 lines
7.7 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": "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.
|