notifications: let Recognition set start/end dates; geenforce B2 client payload fetch

Recognition edit hid the time fields (grouped with Recertification), so start/end
could not be adjusted even though the backend honors them. Show the time fields
for every type except Recertification (due-date driven); Recognition end still
auto-fills to the next 8 AM reset when blank.

Also GE-Enforce B2 client (HTTPS payload consume): ShopdbEnforceClient.psm1 gains
Get-ShopdbPayload (fetch by sha256, verify, cache) + Resolve-ShopdbPayloads
(rewrite http/inline entries to local staged files so the engine installs from
local, no SMB); Invoke-ShopdbEnforce resolves payloads before running the engine;
importer parses PayloadSource/PayloadSha256/PayloadRef. VM-verified: a SYSTEM
Windows client fetched a payload over HTTP by hash, hash matched.
This commit is contained in:
cproudlock
2026-07-21 10:56:00 -04:00
parent b00ef72581
commit 0bb906a37c
4 changed files with 104 additions and 4 deletions

View File

@@ -160,5 +160,87 @@ function New-ShopdbReport {
}
}
function Get-ShopdbPayload {
<#
Fetch a payload blob by content hash over HTTPS, verify the sha256, and
cache it locally (content-addressed, last-known-good). This is how a
share-less PC pulls an installer the manifest references. Returns the local
path, or $null on failure / hash mismatch.
#>
param(
[Parameter(Mandatory)][string]$Sha256,
[Parameter(Mandatory)][hashtable]$Config,
[string]$Filename,
[string]$CacheDir = 'C:\ProgramData\ShopDB\geenforce'
)
$sha = $Sha256.Trim().ToLower()
$payloadDir = Join-Path $CacheDir 'payloads'
if (-not (Test-Path $payloadDir)) { New-Item -ItemType Directory -Path $payloadDir -Force | Out-Null }
$ext = if ($Filename) { [System.IO.Path]::GetExtension($Filename) } else { '' }
$dest = Join-Path $payloadDir "$sha$ext"
# Cache hit only counts if the cached bytes still hash correctly.
if (Test-Path $dest) {
if ((Get-FileHash -LiteralPath $dest -Algorithm SHA256).Hash.ToLower() -eq $sha) { return $dest }
Remove-Item -LiteralPath $dest -Force -ErrorAction SilentlyContinue
}
$uri = "$($Config.BaseUrl)/api/geenforce/payload/$sha"
$tmp = "$dest.tmp"
try {
Invoke-WebRequest -Uri $uri -Headers @{ 'X-API-Key' = $Config.ApiToken } `
-UseBasicParsing -TimeoutSec 120 -OutFile $tmp -ErrorAction Stop
} catch {
if (Test-Path $tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue }
return $null
}
$got = (Get-FileHash -LiteralPath $tmp -Algorithm SHA256).Hash.ToLower()
if ($got -ne $sha) {
Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
return $null
}
Move-Item -LiteralPath $tmp -Destination $dest -Force
return $dest
}
function Resolve-ShopdbPayloads {
<#
Rewrite a manifest so http/inline payload entries install from a locally
fetched file instead of a share path - keeping the engine (and its SMB
handling) untouched. For each entry with PayloadSha256 (PayloadSource
http/inline), fetches + verifies the payload and points the entry's
installer path at the local copy (Installer for MSI/EXE/CMD/BAT/INF, Script
for PS1, Source for File). Returns a rewritten sibling manifest path, or the
original path when there is nothing to resolve. Throws if a referenced
payload cannot be fetched/verified (caller decides fail-safe behavior).
#>
param(
[Parameter(Mandatory)][string]$ManifestPath,
[Parameter(Mandatory)][hashtable]$Config,
[string]$CacheDir = 'C:\ProgramData\ShopDB\geenforce'
)
$json = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
$pathField = @{ MSI='Installer'; EXE='Installer'; CMD='Installer'; BAT='Installer';
INF='Installer'; PS1='Script'; File='Source' }
$changed = $false
foreach ($entry in @($json.Applications)) {
$src = [string]$entry.PayloadSource
$sha = [string]$entry.PayloadSha256
if (-not $sha -or ($src -ne 'http' -and $src -ne 'inline')) { continue }
$field = $pathField[[string]$entry.Type]
if (-not $field) { continue }
$local = Get-ShopdbPayload -Sha256 $sha -Config $Config -Filename $entry.PayloadRef -CacheDir $CacheDir
if (-not $local) { throw "payload $sha for '$($entry.Name)' could not be fetched/verified" }
if ($entry.PSObject.Properties.Name -contains $field) { $entry.$field = $local }
else { $entry | Add-Member -NotePropertyName $field -NotePropertyValue $local }
$changed = $true
}
if (-not $changed) { return $ManifestPath }
$out = [System.IO.Path]::ChangeExtension($ManifestPath, '.resolved.json')
($json | ConvertTo-Json -Depth 20) | Set-Content -LiteralPath $out -Encoding UTF8
return $out
}
Export-ModuleMember -Function Get-ShopdbConfig, Sync-ShopdbManifest, `
Compare-ShopdbShadow, Send-ShopdbReport, New-ShopdbReport, Read-CachedVersion
Compare-ShopdbShadow, Send-ShopdbReport, New-ShopdbReport, Read-CachedVersion, `
Get-ShopdbPayload, Resolve-ShopdbPayloads