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:
@@ -74,7 +74,18 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Which manifest the engine actually runs against.
|
# Which manifest the engine actually runs against.
|
||||||
$manifestToRun = if ($ShadowMode -and $ShareManifestPath) { $ShareManifestPath } else { $sync.Path }
|
if ($ShadowMode -and $ShareManifestPath) {
|
||||||
|
# Shadow: install from the share exactly as today (no payload resolve).
|
||||||
|
$manifestToRun = $ShareManifestPath
|
||||||
|
} else {
|
||||||
|
# Cutover: stage any http/inline payloads to local files and rewrite the
|
||||||
|
# manifest to point at them, so the UNCHANGED engine installs from local
|
||||||
|
# (no SMB needed for share-less PCs).
|
||||||
|
$manifestToRun = Resolve-ShopdbPayloads -ManifestPath $sync.Path -Config $config
|
||||||
|
if ($manifestToRun -ne $sync.Path) {
|
||||||
|
Write-Log "Resolved http/inline payloads to local files: $manifestToRun"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- INTEGRATION POINT ---------------------------------------------------
|
# --- INTEGRATION POINT ---------------------------------------------------
|
||||||
# Run the engine. Install-FromManifest.ps1 is expected to return (or you
|
# Run the engine. Install-FromManifest.ps1 is expected to return (or you
|
||||||
|
|||||||
@@ -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, `
|
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
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ def populate_entry(entry, entry_dict):
|
|||||||
# RegValue stored as its raw JSON literal so DWord vs string typing survives.
|
# RegValue stored as its raw JSON literal so DWord vs string typing survives.
|
||||||
entry.regvalue = (json.dumps(entry_dict['RegValue'])
|
entry.regvalue = (json.dumps(entry_dict['RegValue'])
|
||||||
if 'RegValue' in entry_dict else None)
|
if 'RegValue' in entry_dict else None)
|
||||||
|
# Payload transport (http/inline share-less delivery); default smb. Kept out
|
||||||
|
# of the scalar map so smb entries stay bare (conditional emit + parity).
|
||||||
|
entry.payloadsource = entry_dict.get('PayloadSource') or 'smb'
|
||||||
|
entry.payloadsha256 = entry_dict.get('PayloadSha256')
|
||||||
|
entry.payloadref = entry_dict.get('PayloadRef')
|
||||||
for key, attr in _FLAG_TO_ATTR.items():
|
for key, attr in _FLAG_TO_ATTR.items():
|
||||||
setattr(entry, attr, bool(entry_dict.get(key)))
|
setattr(entry, attr, bool(entry_dict.get(key)))
|
||||||
# Multi-value filters -> child rows (replace, preserve order).
|
# Multi-value filters -> child rows (replace, preserve order).
|
||||||
|
|||||||
@@ -150,8 +150,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Time fields - Hidden for Recognition (auto-set) -->
|
<!-- Time fields. Recertification hides them (due-date driven); every
|
||||||
<div v-if="!isEmployeeType" class="form-row">
|
other type, including Recognition, can set start/end. Recognition
|
||||||
|
end auto-fills to the next 8 AM reset when left blank. -->
|
||||||
|
<div v-if="!isRecertification" class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="starttime">Start Time *</label>
|
<label for="starttime">Start Time *</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user