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>
This commit is contained in:
126
docs/GE-ENFORCE-CLIENT.md
Normal file
126
docs/GE-ENFORCE-CLIENT.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# 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 Settings > 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).
|
||||||
|
|
||||||
|
## 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 (Settings > Imaging PC Types > 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.
|
||||||
103
plugins/geenforce/client/Invoke-ShopdbEnforce.ps1
Normal file
103
plugins/geenforce/client/Invoke-ShopdbEnforce.ps1
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<#
|
||||||
|
.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
|
||||||
162
plugins/geenforce/client/ShopdbEnforceClient.psm1
Normal file
162
plugins/geenforce/client/ShopdbEnforceClient.psm1
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Client-side helpers for sourcing GE-Enforce manifests from shopdb and reporting
|
||||||
|
results back. Site-neutral reference kit - deploy alongside GE-Enforce; do NOT
|
||||||
|
hardcode any site here.
|
||||||
|
|
||||||
|
This module does NOT replace Install-FromManifest.ps1 (the engine). It only
|
||||||
|
changes where the manifest JSON comes from (shopdb HTTP instead of a share
|
||||||
|
file) and adds a result report. The engine, detection, self-heal, and SMB
|
||||||
|
payload resolution stay exactly as they are.
|
||||||
|
|
||||||
|
Resilience mirrors GE-Enforce: any failure is non-fatal (fail-safe). If shopdb
|
||||||
|
is unreachable the client enforces from the last-known-good cached manifest and
|
||||||
|
a PC is never left unmanaged because the web app is down.
|
||||||
|
|
||||||
|
Config (params override registry): HKLM:\SOFTWARE\GE\ShopDB
|
||||||
|
BaseUrl e.g. https://shopdb.example.geaerospace.net
|
||||||
|
ApiToken a geenforce.fetch (+ geenforce.report) managed service token,
|
||||||
|
provisioned the same way as SFLD creds (Azure DSC).
|
||||||
|
#>
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
|
function Get-ShopdbConfig {
|
||||||
|
param([string]$BaseUrl, [string]$ApiToken)
|
||||||
|
$regPath = 'HKLM:\SOFTWARE\GE\ShopDB'
|
||||||
|
if ((-not $BaseUrl -or -not $ApiToken) -and (Test-Path $regPath)) {
|
||||||
|
$props = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue
|
||||||
|
if (-not $BaseUrl -and $props.BaseUrl) { $BaseUrl = $props.BaseUrl }
|
||||||
|
if (-not $ApiToken -and $props.ApiToken) { $ApiToken = $props.ApiToken }
|
||||||
|
}
|
||||||
|
if (-not $BaseUrl -or -not $ApiToken) { return $null }
|
||||||
|
return @{ BaseUrl = $BaseUrl.TrimEnd('/'); ApiToken = $ApiToken }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sync-ShopdbManifest {
|
||||||
|
<#
|
||||||
|
Fetch the current published manifest for a scope into a local cache, using
|
||||||
|
an ETag so an unchanged manifest is a cheap 304. On any network error, fall
|
||||||
|
back to the last-known-good cached copy. Returns:
|
||||||
|
@{ Path; Version; Source } where Source is
|
||||||
|
'shopdb' | 'cache-304' | 'cache-lastgood' | $null (nothing available)
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)] [string]$Scope,
|
||||||
|
[Parameter(Mandatory)] [hashtable]$Config,
|
||||||
|
[string]$CacheDir = 'C:\ProgramData\ShopDB\geenforce'
|
||||||
|
)
|
||||||
|
if (-not (Test-Path $CacheDir)) { New-Item -ItemType Directory -Path $CacheDir -Force | Out-Null }
|
||||||
|
$manifestPath = Join-Path $CacheDir "$Scope.json"
|
||||||
|
$etagPath = Join-Path $CacheDir "$Scope.etag"
|
||||||
|
$headers = @{ 'X-API-Key' = $Config.ApiToken }
|
||||||
|
if (Test-Path $etagPath) { $headers['If-None-Match'] = (Get-Content -LiteralPath $etagPath -Raw).Trim() }
|
||||||
|
|
||||||
|
$uri = "$($Config.BaseUrl)/api/geenforce/manifest?pctype=$([uri]::EscapeDataString($Scope))"
|
||||||
|
try {
|
||||||
|
$response = Invoke-WebRequest -Uri $uri -Headers $headers -UseBasicParsing `
|
||||||
|
-TimeoutSec 30 -ErrorAction Stop
|
||||||
|
if ($response.StatusCode -eq 200) {
|
||||||
|
[System.IO.File]::WriteAllText($manifestPath, $response.Content)
|
||||||
|
if ($response.Headers.ETag) {
|
||||||
|
Set-Content -LiteralPath $etagPath -Value $response.Headers.ETag -NoNewline
|
||||||
|
}
|
||||||
|
$version = $null
|
||||||
|
if ($response.Headers['X-Manifest-Version']) { $version = $response.Headers['X-Manifest-Version'] }
|
||||||
|
if ($version) {
|
||||||
|
Set-Content -LiteralPath (Join-Path $CacheDir "$Scope.version") -Value $version -NoNewline
|
||||||
|
}
|
||||||
|
return @{ Path = $manifestPath; Version = $version; Source = 'shopdb' }
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$status = $null
|
||||||
|
if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode }
|
||||||
|
if ($status -eq 304 -and (Test-Path $manifestPath)) {
|
||||||
|
return @{ Path = $manifestPath; Version = (Read-CachedVersion $CacheDir $Scope); Source = 'cache-304' }
|
||||||
|
}
|
||||||
|
# Network/other failure: fall back to last-known-good.
|
||||||
|
if (Test-Path $manifestPath) {
|
||||||
|
return @{ Path = $manifestPath; Version = (Read-CachedVersion $CacheDir $Scope); Source = 'cache-lastgood' }
|
||||||
|
}
|
||||||
|
return @{ Path = $null; Version = $null; Source = $null }
|
||||||
|
}
|
||||||
|
# 304 without exception (some PS versions) -> use cache.
|
||||||
|
if (Test-Path $manifestPath) {
|
||||||
|
return @{ Path = $manifestPath; Version = (Read-CachedVersion $CacheDir $Scope); Source = 'cache-304' }
|
||||||
|
}
|
||||||
|
return @{ Path = $null; Version = $null; Source = $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-CachedVersion {
|
||||||
|
param([string]$CacheDir, [string]$Scope)
|
||||||
|
$verPath = Join-Path $CacheDir "$Scope.version"
|
||||||
|
if (Test-Path $verPath) { return (Get-Content -LiteralPath $verPath -Raw).Trim() }
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-ShopdbShadow {
|
||||||
|
<#
|
||||||
|
Shadow-mode comparison: do the shopdb manifest and the on-share manifest
|
||||||
|
select the same ordered entry names? Returns @{ Same; ShopdbOnly; ShareOnly;
|
||||||
|
OrderDiff }. Behavioral, not byte, comparison.
|
||||||
|
#>
|
||||||
|
param([Parameter(Mandatory)][string]$ShopdbManifestPath,
|
||||||
|
[Parameter(Mandatory)][string]$ShareManifestPath)
|
||||||
|
$shopdb = (Get-Content -LiteralPath $ShopdbManifestPath -Raw | ConvertFrom-Json)
|
||||||
|
$share = (Get-Content -LiteralPath $ShareManifestPath -Raw | ConvertFrom-Json)
|
||||||
|
$shopdbNames = @($shopdb.Applications | ForEach-Object { $_.Name })
|
||||||
|
$shareNames = @($share.Applications | ForEach-Object { $_.Name })
|
||||||
|
return @{
|
||||||
|
Same = (($shopdbNames -join '|') -eq ($shareNames -join '|'))
|
||||||
|
ShopdbOnly = @($shopdbNames | Where-Object { $_ -notin $shareNames })
|
||||||
|
ShareOnly = @($shareNames | Where-Object { $_ -notin $shopdbNames })
|
||||||
|
OrderDiff = (($shopdbNames -join '|') -ne ($shareNames -join '|'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-ShopdbReport {
|
||||||
|
<#
|
||||||
|
POST an enforcement report to shopdb. Best-effort: never throws, returns
|
||||||
|
$true on success. Report is a hashtable matching POST /api/geenforce/report.
|
||||||
|
#>
|
||||||
|
param([Parameter(Mandatory)][hashtable]$Config,
|
||||||
|
[Parameter(Mandatory)][hashtable]$Report)
|
||||||
|
try {
|
||||||
|
$body = ($Report | ConvertTo-Json -Depth 6)
|
||||||
|
Invoke-RestMethod -Uri "$($Config.BaseUrl)/api/geenforce/report" `
|
||||||
|
-Method Post -Headers @{ 'X-API-Key' = $Config.ApiToken } `
|
||||||
|
-ContentType 'application/json' -Body $body -TimeoutSec 30 -ErrorAction Stop | Out-Null
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ShopdbReport {
|
||||||
|
<#
|
||||||
|
Build a report payload from an engine summary. `Summary` is expected to
|
||||||
|
carry Installed/Skipped/Failed/Filtered counts and a Results list of
|
||||||
|
@{ Name; Action; SelfHealed; ExitCode; Message }. Shape the engine's own
|
||||||
|
per-entry outcomes into this at the call site.
|
||||||
|
#>
|
||||||
|
param([string]$Hostname = $env:COMPUTERNAME,
|
||||||
|
[Parameter(Mandatory)][string]$Scope,
|
||||||
|
[int]$AppliedVersion,
|
||||||
|
[Parameter(Mandatory)][hashtable]$Summary)
|
||||||
|
return @{
|
||||||
|
hostname = $Hostname
|
||||||
|
scopename = $Scope
|
||||||
|
appliedversion = $AppliedVersion
|
||||||
|
enforcerversion = $Summary.EnforcerVersion
|
||||||
|
counts = @{
|
||||||
|
installed = [int]$Summary.Installed
|
||||||
|
skipped = [int]$Summary.Skipped
|
||||||
|
failed = [int]$Summary.Failed
|
||||||
|
filtered = [int]$Summary.Filtered
|
||||||
|
}
|
||||||
|
results = @($Summary.Results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Get-ShopdbConfig, Sync-ShopdbManifest, `
|
||||||
|
Compare-ShopdbShadow, Send-ShopdbReport, New-ShopdbReport, Read-CachedVersion
|
||||||
Reference in New Issue
Block a user