diff --git a/plugins/geenforce/client/Invoke-ShopdbEnforce.ps1 b/plugins/geenforce/client/Invoke-ShopdbEnforce.ps1 index 4128a82..e67adef 100644 --- a/plugins/geenforce/client/Invoke-ShopdbEnforce.ps1 +++ b/plugins/geenforce/client/Invoke-ShopdbEnforce.ps1 @@ -74,7 +74,18 @@ try { } # 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 --------------------------------------------------- # Run the engine. Install-FromManifest.ps1 is expected to return (or you diff --git a/plugins/geenforce/client/ShopdbEnforceClient.psm1 b/plugins/geenforce/client/ShopdbEnforceClient.psm1 index a84d2d5..c9d26f0 100644 --- a/plugins/geenforce/client/ShopdbEnforceClient.psm1 +++ b/plugins/geenforce/client/ShopdbEnforceClient.psm1 @@ -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 diff --git a/plugins/geenforce/importer.py b/plugins/geenforce/importer.py index 684f588..67588d6 100644 --- a/plugins/geenforce/importer.py +++ b/plugins/geenforce/importer.py @@ -48,6 +48,11 @@ def populate_entry(entry, entry_dict): # RegValue stored as its raw JSON literal so DWord vs string typing survives. entry.regvalue = (json.dumps(entry_dict['RegValue']) 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(): setattr(entry, attr, bool(entry_dict.get(key))) # Multi-value filters -> child rows (replace, preserve order). diff --git a/plugins/notifications/frontend/views/NotificationForm.vue b/plugins/notifications/frontend/views/NotificationForm.vue index 3073381..139fd7e 100644 --- a/plugins/notifications/frontend/views/NotificationForm.vue +++ b/plugins/notifications/frontend/views/NotificationForm.vue @@ -150,8 +150,10 @@ /> - -
+ +