Files
shopdb-flask/plugins/geenforce/client/Install-GEEnforce.ps1
cproudlock 035419fa51 ADR-015: stop shipping one site's values, and make the rule a gate
The scanner has been reporting the same count for weeks, which is what a rule
that only prints becomes. It now FAILS the build, and it looks where the leaks
actually were: PowerShell, the installer, the seeds, generated JSON, the
frontend - case-insensitively, across plugins, shopdb, scripts, deploy, tools.
A line that is deliberate declares itself with an ADR-015-OK marker and a
reason, so the claim is visible in review instead of tolerated in silence.

What it found, fixed here:

- The shadow client wrote one site's ShopDB URL into HKLM whenever the registry
  disagreed. At the site it was written for that reads as healing drift;
  anywhere else it overwrites the site's own address on every enforce cycle,
  and the site cannot win because the cycle repeats. The bay's value now wins,
  an explicit -BaseUrl seeds it, and with neither there is nothing honest to
  write, so it says so and skips.
- The kiosk dispatcher fell back to one plant's host when HKLM was unset, so a
  kiosk elsewhere quietly opened a server it has no business reaching. The
  fallback is now this site's site_base_url, baked in at seed time, and the
  dispatcher refuses rather than guessing when neither is set. Its legacy
  shortcut matcher derives the host from that URL instead of naming one.
- The OpenAPI generator hardcoded a production hostname into every spec it
  generated, which then published to a public wiki. The relative mount is the
  only server it can honestly name; a site passes its own by environment.
- Placeholders and examples in the UI and the client help offered real internal
  subnets and a real production URL. They now use documentation ranges.

Both publication gates - the export scrub and the docs publishability test -
carry the site patterns, which neither did. One plant's hostname, FQDN and
internal networks are out of the documentation and the generated specs.

Comments naming the reference site are reworded rather than deleted: the
reasoning is worth keeping, the plant name is not what makes it true.
2026-08-14 13:47:39 -04:00

181 lines
7.8 KiB
PowerShell

<#
.SYNOPSIS
Provision a PC for GE-Enforce + shopdb: write its identity (so it knows its PC
type and bay), deploy the shopdb client kit, and register the enforcement
scheduled task. Site-neutral and imaging-path independent - run it from a PXE
step, an OOBE provisioning package (ppkg), Intune, or by hand.
This is the "set up the PC to determine its PC type and such" step. There is no
auto-detection: the caller supplies the type (and, where relevant, the bay
machine number and CMM version), and this script writes the files/registry the
GE-Enforce engine reads.
This script does NOT contain the GE-Enforce engine (Install-FromManifest.ps1)
or dispatcher - those are the GE-Enforce framework's. Point at your copy with
-EngineSource, or ensure it is already present under -InstallRoot\lib.
.PARAMETER PCType
The imaging PC type = the manifest scope this PC runs. One of the scope names
(e.g. gea-shopfloor-cmm, gea-shopfloor-collections) or a legacy alias the
engine maps. Written to C:\Enrollment\pc-type.txt. REQUIRED.
.PARAMETER MachineNumber
The bay machine number (for TargetMachineNumbers gates). Written to
machine-number.txt as a fallback; the DNC registry MachineNo wins if present.
.PARAMETER CmmVersion
CMM bays only: the resolved PC-DMIS version (2016/2019/2026) for _CmmVersion
gating. Written to C:\Enrollment\cmm\version.txt. Omit for non-CMM PCs.
.PARAMETER CmmId
CMM bays only: the bay id. Written to C:\Enrollment\cmm\cmmid.txt.
.PARAMETER ShareRoot
The GE-Enforce share root (UNC), written into site-config.json as
shopfloorShareRoot.
.PARAMETER Site
Site name, written into site-config.json.
.PARAMETER ShopdbUrl
shopdb base URL (scheme + host) for the fetch/report client. Written to
HKLM:\SOFTWARE\GE\ShopDB\BaseUrl.
.PARAMETER ShopdbToken
A geenforce.fetch (+ geenforce.report) managed service token. Written to
HKLM:\SOFTWARE\GE\ShopDB\ApiToken. (In many sites Azure DSC provisions this
instead - omit here if so.)
.PARAMETER EngineSource
Optional path (folder or share) containing the GE-Enforce engine to copy in:
expects GE-Enforce.ps1 and lib\Install-FromManifest.ps1. If omitted, the engine
is assumed already present under -InstallRoot.
.PARAMETER InstallRoot
Where the client kit + engine live on the PC. Default C:\ProgramData\GE-Enforce.
.PARAMETER TaskName
Scheduled task name. Default 'GE-Enforce'.
.PARAMETER IntervalMinutes
How often the enforcement task repeats. Default 5.
.PARAMETER NoTask
Provision identity + kit only; do not register the scheduled task.
.EXAMPLE
.\Install-GEEnforce.ps1 -PCType gea-shopfloor-cmm -MachineNumber 0615 `
-CmmVersion 2019 -ShareRoot \\server\share\dt\shopfloor -Site "Main Plant" `
-ShopdbUrl https://shopdb.example.net -ShopdbToken shopdb_pat_xxx `
-EngineSource \\server\share\dt\shopfloor\common
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string]$PCType,
[string]$MachineNumber,
[string]$CmmVersion,
[string]$CmmId,
[string]$ShareRoot,
[string]$Site,
[string]$ShopdbUrl,
[string]$ShopdbToken,
[string]$EngineSource,
[string]$InstallRoot = 'C:\ProgramData\GE-Enforce',
[string]$EnrollmentRoot = 'C:\Enrollment',
[string]$TaskName = 'GE-Enforce',
[int]$IntervalMinutes = 5,
[switch]$NoTask
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Write-TextFile {
param([string]$Path, [string]$Value)
$dir = Split-Path -Parent $Path
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
Set-Content -LiteralPath $Path -Value $Value -Encoding ascii -NoNewline
Write-Host " wrote $Path"
}
try {
Write-Host "GE-Enforce provisioning: PCType=$PCType"
# --- 1. Identity: what the PC is (what the engine reads) -----------------
Write-TextFile (Join-Path $EnrollmentRoot 'pc-type.txt') $PCType
if ($MachineNumber) { Write-TextFile (Join-Path $EnrollmentRoot 'machine-number.txt') $MachineNumber }
if ($CmmVersion) { Write-TextFile (Join-Path $EnrollmentRoot 'cmm\version.txt') $CmmVersion }
if ($CmmId) { Write-TextFile (Join-Path $EnrollmentRoot 'cmm\cmmid.txt') $CmmId }
$siteConfig = @{}
if ($ShareRoot) { $siteConfig['shopfloorShareRoot'] = $ShareRoot }
if ($Site) { $siteConfig['site'] = $Site }
if ($siteConfig.Count) {
$siteConfigPath = Join-Path $EnrollmentRoot 'site-config.json'
$dir = Split-Path -Parent $siteConfigPath
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
($siteConfig | ConvertTo-Json) | Set-Content -LiteralPath $siteConfigPath -Encoding ascii
Write-Host " wrote $siteConfigPath"
}
# --- 2. shopdb client registry config ------------------------------------
if ($ShopdbUrl -or $ShopdbToken) {
$regPath = 'HKLM:\SOFTWARE\GE\ShopDB'
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
if ($ShopdbUrl) { Set-ItemProperty -Path $regPath -Name BaseUrl -Value $ShopdbUrl }
if ($ShopdbToken) { Set-ItemProperty -Path $regPath -Name ApiToken -Value $ShopdbToken }
Write-Host " wrote HKLM:\SOFTWARE\GE\ShopDB"
}
# --- 3. Deploy the client kit (ships alongside this script) --------------
if (-not (Test-Path $InstallRoot)) { New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null }
foreach ($file in 'ShopdbEnforceClient.psm1', 'Invoke-ShopdbEnforce.ps1') {
$src = Join-Path $PSScriptRoot $file
if (Test-Path $src) {
Copy-Item -Path $src -Destination (Join-Path $InstallRoot $file) -Force
Write-Host " deployed $file"
} else {
Write-Warning "client kit file not found next to this script: $file"
}
}
# --- 4. Engine (referenced, not vendored) --------------------------------
if ($EngineSource) {
Copy-Item -Path (Join-Path $EngineSource 'GE-Enforce.ps1') `
-Destination (Join-Path $InstallRoot 'GE-Enforce.ps1') -Force -ErrorAction SilentlyContinue
$libDir = Join-Path $InstallRoot 'lib'
if (-not (Test-Path $libDir)) { New-Item -ItemType Directory -Path $libDir -Force | Out-Null }
Copy-Item -Path (Join-Path $EngineSource 'lib\Install-FromManifest.ps1') `
-Destination (Join-Path $libDir 'Install-FromManifest.ps1') -Force -ErrorAction SilentlyContinue
Write-Host " copied engine from $EngineSource"
}
$enginePath = Join-Path $InstallRoot 'lib\Install-FromManifest.ps1'
if (-not (Test-Path $enginePath)) {
Write-Warning "engine not present at $enginePath - provide -EngineSource or place it there before enforcement runs."
}
# --- 5. Scheduled task: run the enforcement client as SYSTEM -------------
if (-not $NoTask) {
$runner = Join-Path $InstallRoot 'Invoke-ShopdbEnforce.ps1'
$arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$runner`" -Scope `"$PCType`" -EnginePath `"$enginePath`""
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arguments
$triggers = @(
(New-ScheduledTaskTrigger -AtLogOn),
(New-ScheduledTaskTrigger -Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes))
)
$principal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $triggers `
-Principal $principal -Settings $settings -Force | Out-Null
Write-Host " registered task '$TaskName' (at logon + every $IntervalMinutes min)"
}
Write-Host "GE-Enforce provisioning complete."
exit 0
} catch {
Write-Error "GE-Enforce provisioning FAILED: $($_.Exception.Message)"
exit 1
}