From 8dceb8812fcf1807ddcdb7eaaba70a85561d7834 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sun, 12 Jul 2026 23:02:54 -0400 Subject: [PATCH] Add GE-Enforce agent deployment: Install-GEEnforce.ps1 + deploy doc Closes the "how do sites actually deploy GE-Enforce" gap (esp. OOBE-ppkg sites without a PXE/WinPE step). Site-neutral + imaging-path independent. - plugins/geenforce/client/Install-GEEnforce.ps1: a bootstrap that writes the PC's identity (C:\Enrollment\pc-type.txt is what determines the PC type; plus machine-number/cmm version/cmm id/site-config as needed), sets the shopdb BaseUrl + token in HKLM:\SOFTWARE\GE\ShopDB, deploys the client kit, optionally copies the engine from -EngineSource, and registers the SYSTEM scheduled task (at logon + every N min). Idempotent; fails loud (installer, not the fail-safe runtime). Engine is REFERENCED not vendored - it belongs to the GE-Enforce framework; the script warns if absent but still labels the PC. - docs/GE-ENFORCE-DEPLOY.md: the deploy contract - the three things a PC needs (client, identity, credential), the identity table (what determines PC type, no auto-detection - the provisioner supplies it; shopdb cannot set it at imaging), and how to invoke per path (PXE step, OOBE ppkg via ProvisioningCommands, Intune, manual), the engine boundary, and verification. - Cross-linked from docs/GE-ENFORCE.md. Co-Authored-By: Claude Opus 4.8 --- docs/GE-ENFORCE-DEPLOY.md | 135 +++++++++++++ docs/GE-ENFORCE.md | 2 + .../geenforce/client/Install-GEEnforce.ps1 | 180 ++++++++++++++++++ 3 files changed, 317 insertions(+) create mode 100644 docs/GE-ENFORCE-DEPLOY.md create mode 100644 plugins/geenforce/client/Install-GEEnforce.ps1 diff --git a/docs/GE-ENFORCE-DEPLOY.md b/docs/GE-ENFORCE-DEPLOY.md new file mode 100644 index 0000000..3d9012f --- /dev/null +++ b/docs/GE-ENFORCE-DEPLOY.md @@ -0,0 +1,135 @@ +# Deploying the GE-Enforce agent on a PC + +This is the deploy contract: what has to be laid down on a PC so GE-Enforce runs, +and how to do it regardless of imaging path (PXE, OOBE provisioning package, +Intune, or by hand). It complements `docs/GE-ENFORCE.md` (concepts) and +`docs/GE-ENFORCE-CLIENT.md` (the fetch/report contract). + +The reference installer is `plugins/geenforce/client/Install-GEEnforce.ps1`. It +is site-neutral: you pass the PC's identity in, it writes the files/registry the +engine reads and registers the enforcement task. + +--- + +## 1. What "deploying GE-Enforce" means + +A PC needs three things present before enforcement works. HOW they get there is +up to your imaging path; WHAT they are is fixed: + +1. **The GE-Enforce client** - the engine (`Install-FromManifest.ps1`), the + shopdb client kit (`ShopdbEnforceClient.psm1`, `Invoke-ShopdbEnforce.ps1`), + and a scheduled task (at logon + periodic) that runs as SYSTEM. +2. **Identity** in `C:\Enrollment` - so the PC knows what it is (see section 2). +3. **A credential** - the SFLD share credential (for a share-sourced manifest) + and/or the shopdb service token (for the fetch/report client). This is what + gates enforcement actually starting; until it exists the task exits 0 and + retries. + +`Install-GEEnforce.ps1` lays down 1 and 2, and can write the shopdb token for 3. +The engine itself is the GE-Enforce framework's, not shopdb's - point the +installer at your copy with `-EngineSource`, or place it under the install root +first (see section 5). + +--- + +## 2. Identity: how a PC determines its PC type (and bay) + +There is NO auto-detection. The provisioner supplies the values; the engine only +reads files. This is the core of "set the PC up to know its type." + +| Value | Written to | Purpose | Required? | +|---|---|---|---| +| **PC type** | `C:\Enrollment\pc-type.txt` (first line) | picks the manifest scope (`gea-shopfloor-`) | YES | +| Machine (bay) number | `C:\Enrollment\machine-number.txt` (fallback; DNC registry `MachineNo` wins) | per-bay gates | only for bay-gated entries | +| CMM version | `C:\Enrollment\cmm\version.txt` | `_CmmVersion` gating (CMM PCs) | CMM only | +| CMM bay id | `C:\Enrollment\cmm\cmmid.txt` | CMM bay identity | CMM only | +| Share root + site | `C:\Enrollment\site-config.json` | where manifests/payloads live | for share-sourced | +| shopdb URL + token | `HKLM:\SOFTWARE\GE\ShopDB` (BaseUrl, ApiToken) | fetch/report client | for shopdb client | + +Valid `pc-type` values are the manifest scope names +(`gea-shopfloor-cmm`, `-collections`, `-nocollections`, `-common`, `-keyence`, +`-genspect`, `-heattreat`, `-partmarker`, `-waxtrace`) or a legacy alias the +engine maps (`Standard`, `CMM`, ...). + +**shopdb cannot set the type at imaging** - a PC is not known to shopdb until it +enrolls and reports. If you want the value to come from an asset system, pre-map +asset-tag / hostname -> PC type in your provisioning and feed it to the +installer. + +--- + +## 3. Running it, per imaging path + +`Install-GEEnforce.ps1` is the same in every case; only how you invoke it differs. + +### PXE / imaging step (identity known at image time) +Run it as an imaging step after the OS lays down, passing the type the operator +selected: + +``` +powershell -ExecutionPolicy Bypass -File Install-GEEnforce.ps1 ` + -PCType gea-shopfloor-cmm -MachineNumber 0615 -CmmVersion 2019 ` + -ShareRoot \\server\share\dt\shopfloor -Site "West Jefferson" ` + -ShopdbUrl https://shopdb.site.geaerospace.net -ShopdbToken shopdb_pat_xxx ` + -EngineSource \\server\share\dt\shopfloor\common +``` + +### OOBE provisioning package (ppkg) +Sites that apply a ppkg during OOBE (no PXE/WinPE step) embed the installer + the +client kit in the ppkg and run it from a `CommandLine` / `ProvisioningCommands` +action. Supply the PC type from a ppkg variable, a first-boot prompt, or an +asset lookup: + +``` +powershell -ExecutionPolicy Bypass -File Install-GEEnforce.ps1 -PCType %PCTYPE% ... +``` + +Timing is forgiving: the scheduled task is fail-safe, so if OOBE finishes before +Intune/DSC provisions the credential, enforcement simply waits and starts once +the credential lands. There is no ordering trap. + +### Intune / manual +Same script as a Win32 app / remediation, or run by hand on an existing PC to +retrofit it. `-NoTask` provisions identity + kit without registering the task. + +--- + +## 4. What the installer does (idempotent) + +1. Writes the `C:\Enrollment` identity files (section 2). +2. Writes `HKLM:\SOFTWARE\GE\ShopDB` (BaseUrl + token) if provided. +3. Copies the client kit (the two files shipped next to it) to `-InstallRoot` + (default `C:\ProgramData\GE-Enforce`). +4. If `-EngineSource` is given, copies `GE-Enforce.ps1` + `lib\Install-FromManifest.ps1`. +5. Registers the scheduled task (SYSTEM, at logon + every `-IntervalMinutes`) to + run `Invoke-ShopdbEnforce.ps1 -Scope -EnginePath `. + +Re-running it updates identity/config and re-registers the task in place. + +--- + +## 5. The engine boundary + +shopdb ships the **manifest store + client kit + this installer**, not the +GE-Enforce **engine** (`Install-FromManifest.ps1`) or dispatcher - those live in +the GE-Enforce framework. So one of: + +- pass `-EngineSource ` pointing at a folder that has `GE-Enforce.ps1` and + `lib\Install-FromManifest.ps1` (e.g. your share's `common` dir), or +- place the engine under `\lib\Install-FromManifest.ps1` yourself + before enforcement runs. + +The installer warns if the engine is missing but still provisions identity so a +PC is at least correctly labelled. Use engine lib >= 2.6 (required for the +`_CmmVersion` gate). + +--- + +## 6. Verify a provisioned PC + +- `Get-Content C:\Enrollment\pc-type.txt` -> the expected scope. +- `Get-ItemProperty HKLM:\SOFTWARE\GE\ShopDB` -> BaseUrl + ApiToken set. +- `Get-ScheduledTask GE-Enforce` -> Ready. +- Trigger it once and check the client log + (`C:\Logs\Shopfloor\shopdb-enforce-*.log`), then confirm the PC appears under + **GE-Enforce > Enforcement Reports** in shopdb with the right PC type. diff --git a/docs/GE-ENFORCE.md b/docs/GE-ENFORCE.md index a584699..f7abe0d 100644 --- a/docs/GE-ENFORCE.md +++ b/docs/GE-ENFORCE.md @@ -324,4 +324,6 @@ All in GE-Enforce > Manifests. No PowerShell, no editing JSON on the share. - Behavioral parity gate (proves the shopdb model round-trips the real manifests): `plugins/geenforce/parity.py` + `flask geenforce parity`. - Client kit + contract: `plugins/geenforce/client/`, `docs/GE-ENFORCE-CLIENT.md`. +- Agent deployment (per PC, any imaging path): `docs/GE-ENFORCE-DEPLOY.md` + + `plugins/geenforce/client/Install-GEEnforce.ps1`. - Design + cutover plan: `docs/proposals/ge-enforce-plugin.md`. diff --git a/plugins/geenforce/client/Install-GEEnforce.ps1 b/plugins/geenforce/client/Install-GEEnforce.ps1 new file mode 100644 index 0000000..2f4b6e6 --- /dev/null +++ b/plugins/geenforce/client/Install-GEEnforce.ps1 @@ -0,0 +1,180 @@ +<# +.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 "West Jefferson" ` + -ShopdbUrl https://shopdb.site.geaerospace.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 +}