Files
shopdb-flask/plugins/geenforce/client/Invoke-ShopdbEnforce.ps1
cproudlock d894f054ac
All checks were successful
CI / backend (push) Successful in 1m42s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
Add GE-Enforce P4 client kit: fetch + report + shadow mode (reference)
Client-side integration kit for sourcing manifests from shopdb and reporting
results back. Site-neutral reference a site adapts into its GE-Enforce.ps1; the
live dispatcher and engine are NOT touched (they are read-only reference under
projects/pxe). Only the manifest JSON source moves from a share file to shopdb,
plus a result report.

- plugins/geenforce/client/ShopdbEnforceClient.psm1: Sync-ShopdbManifest (GET
  with ETag -> local cache; falls back to last-known-good when shopdb is
  unreachable so a PC is never left unmanaged), Compare-ShopdbShadow (behavioral
  diff vs the on-share manifest), Send-ShopdbReport / New-ShopdbReport (best-
  effort POST /report), Get-ShopdbConfig (BaseUrl + token from
  HKLM:\SOFTWARE\GE\ShopDB).
- plugins/geenforce/client/Invoke-ShopdbEnforce.ps1: orchestrator. Fetches,
  optionally shadow-compares (installs from the share, only logs the diff), runs
  the unchanged engine, and reports. Fail-safe: any error exits 0.
- docs/GE-ENFORCE-CLIENT.md: the fetch + report contracts, config, cache/fail-
  safe behavior, the staged shadow -> read-cutover -> payload-migration runbook,
  and TLS/payload-integrity notes.

The report JSON shape matches the POST /api/geenforce/report contract already
covered by the reporting tests. Nothing here runs the live client; shadow mode
and cutover stay a site decision after Milestone 1 sign-off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:30:08 -04:00

104 lines
4.4 KiB
PowerShell

<#
.SYNOPSIS
Reference orchestrator: source a GE-Enforce manifest from shopdb, run the
UNCHANGED engine against it, and report the result back to shopdb.
This is a thin wrapper around Install-FromManifest.ps1 (the engine). It is a
reference a site adapts into its GE-Enforce.ps1 flow; it is not the live
dispatcher. The engine, detection, self-heal, and SMB payload resolution are
untouched - only the source of the manifest JSON moves from a share file to
shopdb, plus a result report.
.PARAMETER Scope
The imaging pc-type / scope name (e.g. gea-shopfloor-cmm), same value the
dispatcher already resolves from C:\Enrollment\pc-type.txt.
.PARAMETER EnginePath
Path to Install-FromManifest.ps1 (the engine lib, >= 2.6).
.PARAMETER ShareManifestPath
The current on-share manifest for this scope. In shadow mode the engine runs
against THIS (unchanged behavior) and shopdb is only compared + reported. Once
cut over, omit it and the engine runs against the shopdb-sourced manifest.
.PARAMETER ShadowMode
Fetch + compare + report, but install from the share (no behavior change).
.NOTES
Fail-safe: any error exits 0 so a bad web app never blocks or breaks a PC.
Config comes from HKLM:\SOFTWARE\GE\ShopDB (BaseUrl, ApiToken) - see the psm1.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string]$Scope,
[Parameter(Mandatory)] [string]$EnginePath,
[string]$ShareManifestPath,
[switch]$ShadowMode,
[string]$BaseUrl,
[string]$ApiToken,
[string]$LogFile = "C:\Logs\Shopfloor\shopdb-enforce-$(Get-Date -Format yyyyMMdd).log"
)
function Write-Log {
param([string]$Message, [string]$Level = 'INFO')
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message"
try { Add-Content -LiteralPath $LogFile -Value $line -ErrorAction SilentlyContinue } catch {}
Write-Host $line
}
try {
Import-Module (Join-Path $PSScriptRoot 'ShopdbEnforceClient.psm1') -Force
$config = Get-ShopdbConfig -BaseUrl $BaseUrl -ApiToken $ApiToken
if (-not $config) {
Write-Log 'No shopdb BaseUrl/ApiToken configured yet - retry next cycle.' 'WARN'
exit 0
}
$sync = Sync-ShopdbManifest -Scope $Scope -Config $config
if (-not $sync.Path) {
Write-Log "No manifest available for $Scope (shopdb unreachable, no cache)." 'WARN'
exit 0
}
Write-Log "Manifest for $Scope from $($sync.Source) (v$($sync.Version))."
# Shadow mode: compare shopdb vs the share, but install from the share.
if ($ShadowMode -and $ShareManifestPath -and (Test-Path $ShareManifestPath)) {
$diff = Compare-ShopdbShadow -ShopdbManifestPath $sync.Path -ShareManifestPath $ShareManifestPath
if ($diff.Same) {
Write-Log 'Shadow: shopdb manifest matches the share.'
} else {
Write-Log ("Shadow DIFF: shopdb-only=[{0}] share-only=[{1}] orderDiff={2}" -f `
($diff.ShopdbOnly -join ','), ($diff.ShareOnly -join ','), $diff.OrderDiff) 'WARN'
}
}
# Which manifest the engine actually runs against.
$manifestToRun = if ($ShadowMode -and $ShareManifestPath) { $ShareManifestPath } else { $sync.Path }
# --- INTEGRATION POINT ---------------------------------------------------
# Run the engine. Install-FromManifest.ps1 is expected to return (or you
# adapt it to return) a summary carrying Installed/Skipped/Failed/Filtered
# and a Results list of @{Name;Action;SelfHealed;ExitCode;Message}. Wire this
# to your engine's actual return/parse; the shape below is the contract.
Write-Log "Running engine against $manifestToRun"
$summary = & $EnginePath -ManifestPath $manifestToRun -PCType $Scope
if (-not $summary) {
$summary = @{ Installed = 0; Skipped = 0; Failed = 0; Filtered = 0; Results = @() }
}
if (-not $summary.EnforcerVersion) { $summary.EnforcerVersion = '2.6' }
# Report the result (best-effort).
$appliedVersion = 0
if ($sync.Version) { [int]::TryParse($sync.Version, [ref]$appliedVersion) | Out-Null }
$report = New-ShopdbReport -Scope $Scope -AppliedVersion $appliedVersion -Summary $summary
if (Send-ShopdbReport -Config $config -Report $report) {
Write-Log "Reported: installed=$($summary.Installed) skipped=$($summary.Skipped) failed=$($summary.Failed)."
} else {
Write-Log 'Report POST failed (non-fatal).' 'WARN'
}
} catch {
Write-Log "Unhandled error (non-fatal): $($_.Exception.Message)" 'ERROR'
}
exit 0