- Resolve-ShopdbPayloads wrote an absolute local path into the entry, and the engine resolves it as Join-Path InstallerRoot <field>, doubling it (C:\...\payloads\C:\...\payloads\<sha>.ps1 -> PS1 not found). Write the leaf filename instead; the runner already sets InstallerRoot to that payloads dir. - display dispatcher now removes leftover kiosk launchers from prior installs (any Startup .lnk that runs Edge --kiosk, plus .url to a shopdb kiosk page), not just its own, so two kiosks do not fight.
453 lines
20 KiB
PowerShell
453 lines
20 KiB
PowerShell
<#
|
|
.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 Set-ShopdbTls {
|
|
<#
|
|
Force TLS 1.2 for the process-wide ServicePointManager. Windows PowerShell
|
|
5.1 (what runs as SYSTEM on the display image) does not always negotiate
|
|
TLS 1.2 by default, so every network helper calls this first. Mirrors the
|
|
pattern in docs/COLLECTOR-INTEGRATION.md. Best-effort: never throws.
|
|
#>
|
|
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}
|
|
}
|
|
|
|
function Get-ShopdbProperty {
|
|
<#
|
|
Read a property/key from either a hashtable or a PSCustomObject, matching
|
|
any of the given names case-insensitively. Returns $null when absent
|
|
instead of throwing under Set-StrictMode. The engine's per-entry outcomes
|
|
and summary may arrive as either shape, so all normalization goes through
|
|
this.
|
|
#>
|
|
param($InputObject, [Parameter(Mandatory)][string[]]$Names)
|
|
if ($null -eq $InputObject) { return $null }
|
|
if ($InputObject -is [System.Collections.IDictionary]) {
|
|
foreach ($wanted in $Names) {
|
|
foreach ($key in @($InputObject.Keys)) {
|
|
if ($key -is [string] -and $key -ieq $wanted) { return $InputObject[$key] }
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
$properties = $InputObject.PSObject.Properties
|
|
foreach ($wanted in $Names) {
|
|
foreach ($property in $properties) {
|
|
if ($property.Name -ieq $wanted) { return $property.Value }
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
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
|
|
$regBase = Get-ShopdbProperty $props 'BaseUrl'
|
|
$regTok = Get-ShopdbProperty $props 'ApiToken'
|
|
if (-not $BaseUrl -and $regBase) { $BaseUrl = [string]$regBase }
|
|
if (-not $ApiToken -and $regTok) { $ApiToken = [string]$regTok }
|
|
}
|
|
# ApiToken is OPTIONAL: on a vaulted network the server may authorize by
|
|
# source-IP allowlist, so a BaseUrl alone is a valid config. When a token
|
|
# is present it is still sent (and honored) for token-authorized sites.
|
|
if (-not $BaseUrl) { return $null }
|
|
return @{ BaseUrl = $BaseUrl.TrimEnd('/'); ApiToken = $ApiToken }
|
|
}
|
|
|
|
|
|
function New-ShopdbAuthHeaders {
|
|
# Only send X-API-Key when a token is configured; a token-less client
|
|
# relies on the server's IP allowlist.
|
|
param($Config)
|
|
if ($Config.ApiToken) { return @{ 'X-API-Key' = $Config.ApiToken } }
|
|
return @{}
|
|
}
|
|
|
|
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'
|
|
)
|
|
Set-ShopdbTls
|
|
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 = New-ShopdbAuthHeaders $Config
|
|
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) {
|
|
# Validate JSON before overwriting the last-known-good cache: a proxy
|
|
# or IIS error page served as 200 must not clobber the fallback.
|
|
try { $null = ($response.Content | ConvertFrom-Json) }
|
|
catch { throw "manifest response for $Scope was not valid JSON" }
|
|
[System.IO.File]::WriteAllText($manifestPath, $response.Content)
|
|
# PowerShell 7 returns header values as string arrays; 5.1 as scalars.
|
|
# @(...)[0] yields a clean scalar in both.
|
|
$etag = @($response.Headers['ETag'])[0]
|
|
if ($etag) {
|
|
Set-Content -LiteralPath $etagPath -Value $etag -NoNewline
|
|
}
|
|
$version = @($response.Headers['X-Manifest-Version'])[0]
|
|
if ($version) {
|
|
Set-Content -LiteralPath (Join-Path $CacheDir "$Scope.version") -Value $version -NoNewline
|
|
}
|
|
return @{ Path = $manifestPath; Version = $version; Source = 'shopdb' }
|
|
}
|
|
} catch {
|
|
$status = $null
|
|
$exResponse = Get-ShopdbProperty $_.Exception 'Response'
|
|
if ($exResponse) { $status = [int]$exResponse.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' }
|
|
}
|
|
# Nothing cached (e.g. a fresh display): report why so the failure is not
|
|
# silent. Include the HTTP status (401 auth, TLS-trust surfaces as a
|
|
# non-status transport error) and the exception message.
|
|
$errText = $_.Exception.Message
|
|
if ($status) { $errText = "HTTP $status - $errText" }
|
|
return @{ Path = $null; Version = $null; Source = $null; Error = $errText }
|
|
}
|
|
# 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)
|
|
Set-ShopdbTls
|
|
try {
|
|
$body = ($Report | ConvertTo-Json -Depth 6)
|
|
Invoke-RestMethod -Uri "$($Config.BaseUrl)/api/geenforce/report" `
|
|
-Method Post -Headers (New-ShopdbAuthHeaders $Config) `
|
|
-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 whose
|
|
per-entry outcomes carry name/action and optionally selfhealed/exitcode/
|
|
message. The engine emits these in PascalCase (Name/Action/SelfHealed/
|
|
ExitCode/Message); the shopdb report contract is entirely lowercase, so
|
|
this function maps every per-entry key down to lowercase. Case is matched
|
|
case-insensitively, so a caller that already lowercased still works.
|
|
#>
|
|
param([string]$Hostname = $env:COMPUTERNAME,
|
|
[Parameter(Mandatory)][string]$Scope,
|
|
[int]$AppliedVersion,
|
|
[Parameter(Mandatory)][hashtable]$Summary)
|
|
$results = @(foreach ($entry in @($Summary.Results)) {
|
|
if ($null -eq $entry) { continue }
|
|
$mapped = @{
|
|
name = [string](Get-ShopdbProperty -InputObject $entry -Names 'name')
|
|
action = [string](Get-ShopdbProperty -InputObject $entry -Names 'action')
|
|
}
|
|
$selfHealed = Get-ShopdbProperty -InputObject $entry -Names 'selfhealed'
|
|
if ($null -ne $selfHealed) { $mapped['selfhealed'] = [bool]$selfHealed }
|
|
$exitCode = Get-ShopdbProperty -InputObject $entry -Names 'exitcode'
|
|
if ($null -ne $exitCode) { $mapped['exitcode'] = [int]$exitCode }
|
|
$message = Get-ShopdbProperty -InputObject $entry -Names 'message'
|
|
if ($message) { $mapped['message'] = [string]$message }
|
|
$mapped
|
|
})
|
|
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 = $results
|
|
}
|
|
}
|
|
|
|
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'
|
|
)
|
|
Set-ShopdbTls
|
|
$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 (New-ShopdbAuthHeaders $Config) `
|
|
-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](Get-ShopdbProperty $entry 'PayloadSource')
|
|
$sha = [string](Get-ShopdbProperty $entry 'PayloadSha256')
|
|
if (-not $sha -or ($src -ne 'http' -and $src -ne 'inline')) { continue }
|
|
$field = $pathField[[string](Get-ShopdbProperty $entry 'Type')]
|
|
if (-not $field) { continue }
|
|
$ref = Get-ShopdbProperty $entry 'PayloadRef'
|
|
$local = Get-ShopdbPayload -Sha256 $sha -Config $Config -Filename $ref -CacheDir $CacheDir
|
|
if (-not $local) { throw "payload $sha for '$($entry.Name)' could not be fetched/verified" }
|
|
# Write the LEAF filename, not the absolute path: the engine resolves the
|
|
# entry field as Join-Path $InstallerRoot <field>, and the runner sets
|
|
# InstallerRoot to this same payloads dir. Passing an absolute path made
|
|
# the engine double it (InstallerRoot + C:\...\ -> C:\...\C:\...).
|
|
$leaf = Split-Path -Leaf $local
|
|
if ($entry.PSObject.Properties.Name -contains $field) { $entry.$field = $leaf }
|
|
else { $entry | Add-Member -NotePropertyName $field -NotePropertyValue $leaf }
|
|
$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
|
|
}
|
|
|
|
function Merge-ShopdbManifests {
|
|
<#
|
|
Merge the fleet-wide 'common' scope manifest into a pctype (display) scope
|
|
manifest and write the merged result to a sibling file. This mirrors how
|
|
the real GE-Enforce.ps1 applies scopes in order (common first, then the
|
|
pctype), except we produce a single merged manifest for the unchanged
|
|
engine to run once. Merge rules:
|
|
|
|
- Entries are keyed by Name (case-insensitive).
|
|
- common's unique entries come first, then all pctype entries, so
|
|
common's own apps enforce ahead of the pctype's, matching the real
|
|
script's common-then-pctype ordering.
|
|
- On a Name conflict the PCTYPE entry wins (the pctype override replaces
|
|
common's version, and keeps common's slot out of the list).
|
|
|
|
Returns the merged manifest path. If there is no common manifest, returns
|
|
the primary path unchanged. The merged manifest keeps the pctype
|
|
manifest's top-level Version (that is the version the display reports as
|
|
applied).
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)][string]$PrimaryManifestPath,
|
|
[string]$CommonManifestPath
|
|
)
|
|
if (-not $CommonManifestPath -or -not (Test-Path $CommonManifestPath)) { return $PrimaryManifestPath }
|
|
|
|
$primary = Get-Content -LiteralPath $PrimaryManifestPath -Raw | ConvertFrom-Json
|
|
$common = Get-Content -LiteralPath $CommonManifestPath -Raw | ConvertFrom-Json
|
|
|
|
$primaryApps = @($primary.Applications)
|
|
$commonApps = @($common.Applications)
|
|
|
|
$primaryNames = @{}
|
|
foreach ($app in $primaryApps) {
|
|
$name = [string]$app.Name
|
|
if ($name) { $primaryNames[$name.ToLower()] = $true }
|
|
}
|
|
|
|
# common entries the pctype does not override, then all pctype entries.
|
|
$merged = @()
|
|
foreach ($app in $commonApps) {
|
|
$name = [string]$app.Name
|
|
if ($name -and $primaryNames.ContainsKey($name.ToLower())) { continue }
|
|
$merged += $app
|
|
}
|
|
foreach ($app in $primaryApps) { $merged += $app }
|
|
|
|
$primary.Applications = $merged
|
|
$out = [System.IO.Path]::ChangeExtension($PrimaryManifestPath, '.merged.json')
|
|
($primary | ConvertTo-Json -Depth 20) | Set-Content -LiteralPath $out -Encoding UTF8
|
|
return $out
|
|
}
|
|
|
|
function ConvertTo-ShopdbSummary {
|
|
<#
|
|
Normalize whatever the engine returns into a well-formed summary hashtable
|
|
so New-ShopdbReport always has clean input, without assuming the engine was
|
|
fixed.
|
|
|
|
Expected engine contract (what a compliant engine returns):
|
|
@{ Installed=<int>; Skipped=<int>; Failed=<int>; Filtered=<int>;
|
|
EnforcerVersion=<string>;
|
|
Results=@( @{ Name; Action; SelfHealed; ExitCode; Message } ... ) }
|
|
|
|
This adapter tolerates any of:
|
|
- $null / empty -> a zeroed summary.
|
|
- a hashtable or PSCustomObject with those keys (any casing).
|
|
- an array / multiple emitted objects -> the last element that looks
|
|
like a summary (has any count or a Results list) is used.
|
|
- a bare return code (int) or unrecognized object -> a zeroed summary.
|
|
|
|
Counts are coerced to int; a missing EnforcerVersion is left for the caller
|
|
to default. Always returns a hashtable.
|
|
#>
|
|
param($EngineResult, [string]$DefaultEnforcerVersion = '2.6')
|
|
|
|
$zero = @{ Installed = 0; Skipped = 0; Failed = 0; Filtered = 0;
|
|
Results = @(); EnforcerVersion = $DefaultEnforcerVersion }
|
|
|
|
$candidate = $EngineResult
|
|
if ($candidate -is [System.Array]) {
|
|
$picked = $null
|
|
foreach ($item in $candidate) {
|
|
if ($null -eq $item) { continue }
|
|
$looksLikeSummary = $false
|
|
foreach ($key in @('Installed', 'Skipped', 'Failed', 'Filtered', 'Results')) {
|
|
if ($null -ne (Get-ShopdbProperty -InputObject $item -Names $key)) { $looksLikeSummary = $true; break }
|
|
}
|
|
if ($looksLikeSummary) { $picked = $item }
|
|
}
|
|
$candidate = $picked
|
|
}
|
|
|
|
if ($null -eq $candidate) { return $zero }
|
|
# A bare return code (int) or any object without the expected members reads
|
|
# as all-null through Get-ShopdbProperty below, which yields the zeroed
|
|
# summary - exactly the fail-open behavior we want for a non-compliant engine.
|
|
$results = Get-ShopdbProperty -InputObject $candidate -Names 'Results'
|
|
$enforcerVersion = [string](Get-ShopdbProperty -InputObject $candidate -Names 'EnforcerVersion')
|
|
if (-not $enforcerVersion) { $enforcerVersion = $DefaultEnforcerVersion }
|
|
|
|
$toInt = {
|
|
param($value)
|
|
$parsed = 0
|
|
if ($null -ne $value -and [int]::TryParse([string]$value, [ref]$parsed)) { return $parsed }
|
|
return 0
|
|
}
|
|
|
|
return @{
|
|
Installed = (& $toInt (Get-ShopdbProperty -InputObject $candidate -Names 'Installed'))
|
|
Skipped = (& $toInt (Get-ShopdbProperty -InputObject $candidate -Names 'Skipped'))
|
|
Failed = (& $toInt (Get-ShopdbProperty -InputObject $candidate -Names 'Failed'))
|
|
Filtered = (& $toInt (Get-ShopdbProperty -InputObject $candidate -Names 'Filtered', 'PCFiltered'))
|
|
Results = @($results)
|
|
EnforcerVersion = $enforcerVersion
|
|
}
|
|
}
|
|
|
|
Export-ModuleMember -Function Get-ShopdbConfig, Sync-ShopdbManifest, `
|
|
Compare-ShopdbShadow, Send-ShopdbReport, New-ShopdbReport, Read-CachedVersion, `
|
|
Get-ShopdbPayload, Resolve-ShopdbPayloads, Merge-ShopdbManifests, `
|
|
ConvertTo-ShopdbSummary
|