@trillboards/edge-sdk 0.2.24 → 0.2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/windows/build-windows.ps1 +1 -0
- package/deploy/windows/ci/verify-update-applier.ps1 +168 -27
- package/deploy/windows/enroll-connector.ps1 +169 -0
- package/deploy/windows/installer.nsi +102 -6
- package/deploy/windows/run-connector.ps1 +30 -0
- package/dist/ChannelProber.d.ts +37 -1
- package/dist/ChannelProber.d.ts.map +1 -1
- package/dist/ChannelProber.js +113 -5
- package/dist/ChannelProber.js.map +1 -1
- package/dist/ConnectorCameraClient.d.ts +79 -0
- package/dist/ConnectorCameraClient.d.ts.map +1 -0
- package/dist/ConnectorCameraClient.js +114 -0
- package/dist/ConnectorCameraClient.js.map +1 -0
- package/dist/EdgeAgent.js +1 -1
- package/dist/EnrollClient.d.ts +81 -0
- package/dist/EnrollClient.d.ts.map +1 -0
- package/dist/EnrollClient.js +97 -0
- package/dist/EnrollClient.js.map +1 -0
- package/dist/SetupSessionClient.d.ts +2 -0
- package/dist/SetupSessionClient.d.ts.map +1 -1
- package/dist/SetupSessionClient.js.map +1 -1
- package/dist/cli.d.ts +49 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +332 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/setupMode.d.ts.map +1 -1
- package/dist/setupMode.js +1 -0
- package/dist/setupMode.js.map +1 -1
- package/package.json +1 -1
|
@@ -202,6 +202,7 @@ Copy-Item -Force "$Here\trillboards-hidden-run.vbs" "$Payload\bin\trillboards-hi
|
|
|
202
202
|
Copy-Item -Force "$Here\ensure-sensing.ps1" "$Payload\bin\ensure-sensing.ps1"
|
|
203
203
|
Copy-Item -Force "$Here\run-connector.ps1" "$Payload\bin\run-connector.ps1"
|
|
204
204
|
Copy-Item -Force "$Here\register-task.ps1" "$Payload\bin\register-task.ps1"
|
|
205
|
+
Copy-Item -Force "$Here\enroll-connector.ps1" "$Payload\bin\enroll-connector.ps1"
|
|
205
206
|
Copy-Item -Force "$Here\stop-existing.ps1" "$Payload\bin\stop-existing.ps1"
|
|
206
207
|
Copy-Item -Force "$Here\install-update.ps1" "$Payload\bin\install-update.ps1"
|
|
207
208
|
# The in-field updater. Ships in bin\ so an INSTALLED connector can drive its own
|
|
@@ -67,8 +67,81 @@ $Www = Join-Path $Root 'www'
|
|
|
67
67
|
$Certs = New-Object System.Collections.Generic.List[string]
|
|
68
68
|
$LocalUser = 'tbupd'
|
|
69
69
|
$Failures = 0
|
|
70
|
+
$StartedAt = Get-Date
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
<#
|
|
73
|
+
A SELF-IMPOSED DEADLINE, because the first run of this harness did not fail -
|
|
74
|
+
it ate the whole job. It printed ONE line, ran for 11 minutes, and the JOB hit
|
|
75
|
+
its 15-minute cap, so every other proof in that step lost its budget too and
|
|
76
|
+
the log said only "The operation was canceled" - naming no cause and proving
|
|
77
|
+
nothing. Not one arm had run.
|
|
78
|
+
|
|
79
|
+
A harness that cannot finish must say so ITSELF, at the point it gives up, and
|
|
80
|
+
loudly enough to be actionable. Every phase below also prints its elapsed
|
|
81
|
+
time, because the reason that first failure was undiagnosable was that there
|
|
82
|
+
was no output between "origin listening" and the first arm.
|
|
83
|
+
#>
|
|
84
|
+
$DeadlineMinutes = 8
|
|
85
|
+
|
|
86
|
+
function Elapsed { ((Get-Date) - $script:StartedAt).TotalSeconds }
|
|
87
|
+
function Say([string]$m) { Write-Host ("[update-applier {0,6:N1}s] {1}" -f (Elapsed), $m) }
|
|
88
|
+
|
|
89
|
+
$script:CurrentPhase = 'startup'
|
|
90
|
+
|
|
91
|
+
function Fail-Deadline([string]$Phase, [string]$When) {
|
|
92
|
+
Write-Host "FAIL - the harness exceeded its $($script:DeadlineMinutes)-minute deadline $When`: $Phase"
|
|
93
|
+
Write-Host ' HARNESS BUDGET failure, not a product failure. The arms above ran;'
|
|
94
|
+
Write-Host ' the ones below never started. Read the per-phase timings to find'
|
|
95
|
+
Write-Host ' what got slow before assuming a defect in apply-update.ps1.'
|
|
96
|
+
Cleanup
|
|
97
|
+
exit 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function Assert-WithinDeadline([string]$Phase) {
|
|
101
|
+
$script:CurrentPhase = $Phase
|
|
102
|
+
if (((Get-Date) - $script:StartedAt).TotalMinutes -lt $script:DeadlineMinutes) { return }
|
|
103
|
+
Fail-Deadline $Phase 'before starting'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Remaining-DeadlineSeconds {
|
|
107
|
+
[Math]::Max(0, ($script:DeadlineMinutes * 60) - (Elapsed))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
<#
|
|
111
|
+
Wait for a process, but never past the harness deadline.
|
|
112
|
+
|
|
113
|
+
Assert-WithinDeadline can only sample time BETWEEN phases, so on its own it
|
|
114
|
+
cannot stop a phase that is ALREADY blocked - and that is precisely the
|
|
115
|
+
failure this harness exists to prevent. The first run spent eleven minutes
|
|
116
|
+
inside a single call; the job's 15-minute cap ended it, the harness did not,
|
|
117
|
+
and the log named no cause. A deadline that can only fire between the things
|
|
118
|
+
it is bounding is not a deadline.
|
|
119
|
+
|
|
120
|
+
So every blocking wait goes through here, bounded by the budget REMAINING at
|
|
121
|
+
the moment the wait starts. The deadline therefore stays a whole-harness
|
|
122
|
+
deadline rather than becoming a per-call timeout: eight slow phases cannot
|
|
123
|
+
each consume eight minutes.
|
|
124
|
+
|
|
125
|
+
On expiry we kill the process before reporting, so the cleanup that follows
|
|
126
|
+
is not itself blocked by the thing that hung.
|
|
127
|
+
#>
|
|
128
|
+
function Wait-Bounded {
|
|
129
|
+
param([System.Diagnostics.Process]$Process, [string]$Phase = $script:CurrentPhase)
|
|
130
|
+
$budget = Remaining-DeadlineSeconds
|
|
131
|
+
if ($budget -le 0) {
|
|
132
|
+
try { Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue } catch {}
|
|
133
|
+
Fail-Deadline $Phase 'before starting'
|
|
134
|
+
}
|
|
135
|
+
$exited = $false
|
|
136
|
+
try { $exited = $Process.WaitForExit([int]([Math]::Min(2147483, $budget) * 1000)) } catch { $exited = $true }
|
|
137
|
+
if (-not $exited) {
|
|
138
|
+
try { Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue } catch {}
|
|
139
|
+
try { $Process.WaitForExit(5000) | Out-Null } catch {}
|
|
140
|
+
Fail-Deadline $Phase 'while running'
|
|
141
|
+
}
|
|
142
|
+
try { $Process.Refresh() } catch {}
|
|
143
|
+
return $Process
|
|
144
|
+
}
|
|
72
145
|
function Pass([string]$m) { Write-Host "PASS - $m" }
|
|
73
146
|
function Bad([string]$m) { Write-Host "FAIL - $m"; $script:Failures++ }
|
|
74
147
|
function Die([string]$m) { Write-Host "FAIL - $m"; Cleanup; exit 1 }
|
|
@@ -158,27 +231,70 @@ function Sign-File([string]$Path, $Cert) {
|
|
|
158
231
|
# ---------------------------------------------------------------------------
|
|
159
232
|
# Fixtures
|
|
160
233
|
# ---------------------------------------------------------------------------
|
|
234
|
+
<#
|
|
235
|
+
BUILT ONCE, REUSED. The first version of this harness rebuilt the whole tree
|
|
236
|
+
per arm: 4 trees x (copy the runner's ~110 MB node.exe + Authenticode-sign 12
|
|
237
|
+
scripts + zip the lot). On windows-latest that produced ONE line of output and
|
|
238
|
+
then nothing for 11 minutes, until the job hit its 15-minute cap and was
|
|
239
|
+
cancelled - so not one arm ever ran, and the step looked hung rather than slow
|
|
240
|
+
because there was no progress output between phases at all.
|
|
241
|
+
|
|
242
|
+
Two fixes, and the second is the one that matters for the next person:
|
|
243
|
+
|
|
244
|
+
1. Sign the shipped script set ONCE into a cache and COPY the signed bytes
|
|
245
|
+
into each tree. Authenticode signing is the expensive part, not the file
|
|
246
|
+
copy, and the signatures are identical across trees by construction.
|
|
247
|
+
2. bin\node.exe is a small system PE re-signed with the harness certificate,
|
|
248
|
+
NOT the real 110 MB node. The applier only needs it to be PRESENT and
|
|
249
|
+
VALID; the two arms that actually EXECUTE node ask for -RealNode. Re-signing
|
|
250
|
+
also removes the Microsoft CRL distribution point, so the applier's
|
|
251
|
+
Get-AuthenticodeSignature calls resolve against a self-signed chain with
|
|
252
|
+
nothing to look up instead of reaching for the network 13 times per stage.
|
|
253
|
+
#>
|
|
254
|
+
$SignedCache = $null
|
|
255
|
+
|
|
256
|
+
function Initialize-SignedCache($SignCert) {
|
|
257
|
+
if ($script:SignedCache) { return }
|
|
258
|
+
$script:SignedCache = Join-Path $Root 'signed-cache'
|
|
259
|
+
New-Item -ItemType Directory -Force -Path $script:SignedCache | Out-Null
|
|
260
|
+
$t0 = Get-Date
|
|
261
|
+
foreach ($f in @(& (Join-Path $Here 'list-shipped-scripts.ps1') -Kind signable -AbsolutePath)) {
|
|
262
|
+
$dest = Join-Path $script:SignedCache (Split-Path -Leaf $f)
|
|
263
|
+
Copy-Item -LiteralPath $f -Destination $dest -Force
|
|
264
|
+
Sign-File $dest $SignCert
|
|
265
|
+
}
|
|
266
|
+
# A small, already-PE stub that becomes bin\node.exe. where.exe is ~40 KB and
|
|
267
|
+
# is a real signed PE, so re-signing it with the harness cert succeeds and the
|
|
268
|
+
# result carries OUR chain rather than Microsoft's.
|
|
269
|
+
$stub = Join-Path $script:SignedCache 'node.exe'
|
|
270
|
+
Copy-Item -LiteralPath (Join-Path $env:WINDIR 'System32\where.exe') -Destination $stub -Force
|
|
271
|
+
Sign-File $stub $SignCert
|
|
272
|
+
Say ("signed-script cache built in {0:N1}s" -f ((Get-Date) - $t0).TotalSeconds)
|
|
273
|
+
}
|
|
274
|
+
|
|
161
275
|
<#
|
|
162
276
|
A tree shaped exactly like the shipped connector: bin\ with the real scripts
|
|
163
277
|
(so the applier's "at least 10 signed executables" floor is met by the real
|
|
164
278
|
file set, not a stub), a node.exe, and a cli.js that answers `version` the way
|
|
165
279
|
install-update.ps1 requires.
|
|
166
280
|
#>
|
|
167
|
-
function New-PayloadTree([string]$Dest, $SignCert, [switch]$LogonOnlyRegisterTask, [switch]$WithUnsignedScript) {
|
|
281
|
+
function New-PayloadTree([string]$Dest, $SignCert, [switch]$LogonOnlyRegisterTask, [switch]$WithUnsignedScript, [switch]$RealNode) {
|
|
282
|
+
Initialize-SignedCache $SignCert
|
|
283
|
+
$t0 = Get-Date
|
|
168
284
|
if (Test-Path -LiteralPath $Dest) { Remove-Item -LiteralPath $Dest -Recurse -Force }
|
|
169
285
|
$bin = Join-Path $Dest 'bin'
|
|
170
286
|
$dist = Join-Path $Dest 'lib\edge-sdk\dist'
|
|
171
287
|
New-Item -ItemType Directory -Force -Path $bin, $dist | Out-Null
|
|
172
288
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
289
|
+
Copy-Item -Path (Join-Path $script:SignedCache '*') -Destination $bin -Force
|
|
290
|
+
|
|
176
291
|
if ($LogonOnlyRegisterTask) {
|
|
177
292
|
# A register-task.ps1 that deliberately produces the PRE-headless shape.
|
|
178
293
|
# This is how arm J reproduces, on an elevated runner, the downgrade a
|
|
179
294
|
# non-elevated field caller would suffer: the cause differs, the observable
|
|
180
295
|
# (the task comes back logon-only) is identical, and that observable is what
|
|
181
|
-
# the guard keys on.
|
|
296
|
+
# the guard keys on. Signed, or the payload would be refused by the
|
|
297
|
+
# signature check before it could ever reach install-update.ps1.
|
|
182
298
|
$downgrade = @'
|
|
183
299
|
[CmdletBinding()]
|
|
184
300
|
param([Parameter(Mandatory = $true)][string]$InstallDir, [string]$Mode = "auto-sense")
|
|
@@ -190,29 +306,28 @@ $principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -
|
|
|
190
306
|
Unregister-ScheduledTask -TaskName "TrillboardsEdge" -Confirm:$false -ErrorAction SilentlyContinue
|
|
191
307
|
Register-ScheduledTask -TaskName "TrillboardsEdge" -Action $action -Trigger $trigger -Principal $principal | Out-Null
|
|
192
308
|
'@
|
|
193
|
-
|
|
309
|
+
$rt = Join-Path $bin 'register-task.ps1'
|
|
310
|
+
[System.IO.File]::WriteAllText($rt, $downgrade, (New-Object System.Text.UTF8Encoding($false)))
|
|
311
|
+
Sign-File $rt $SignCert
|
|
194
312
|
}
|
|
195
313
|
if ($WithUnsignedScript) {
|
|
196
314
|
[System.IO.File]::WriteAllText((Join-Path $bin 'rogue.ps1'), "exit 0`r`n", (New-Object System.Text.UTF8Encoding($false)))
|
|
197
315
|
}
|
|
316
|
+
if ($RealNode) {
|
|
317
|
+
# Only the arms that actually RUN the connector need a real interpreter.
|
|
318
|
+
Copy-Item -LiteralPath (Get-Command node).Source -Destination (Join-Path $bin 'node.exe') -Force
|
|
319
|
+
$nodeSig = Get-AuthenticodeSignature -LiteralPath (Join-Path $bin 'node.exe')
|
|
320
|
+
if ($nodeSig.Status -ne 'Valid') { Sign-File (Join-Path $bin 'node.exe') $SignCert }
|
|
321
|
+
}
|
|
198
322
|
|
|
199
|
-
Copy-Item -LiteralPath (Get-Command node).Source -Destination (Join-Path $bin 'node.exe') -Force
|
|
200
323
|
[System.IO.File]::WriteAllText(
|
|
201
324
|
(Join-Path $dist 'cli.js'),
|
|
202
325
|
"if (process.argv[2] === 'version') { console.log('@trillboards/edge-sdk v9.9.9'); process.exit(0); }`r`nsetInterval(function(){}, 1000);`r`n",
|
|
203
326
|
(New-Object System.Text.UTF8Encoding($false)))
|
|
204
|
-
|
|
205
|
-
foreach ($f in @(Get-ChildItem -LiteralPath $bin -File | Where-Object { $_.Name -ne 'rogue.ps1' })) {
|
|
206
|
-
if ($f.Extension -in @('.ps1', '.vbs')) { Sign-File $f.FullName $SignCert }
|
|
207
|
-
}
|
|
208
|
-
# The runner's node.exe is the official OpenJS build and already Valid; sign a
|
|
209
|
-
# copy only if it is not, so the floor check measures signatures rather than
|
|
210
|
-
# the runner image.
|
|
211
|
-
$nodeSig = Get-AuthenticodeSignature -LiteralPath (Join-Path $bin 'node.exe')
|
|
212
|
-
if ($nodeSig.Status -ne 'Valid') { Sign-File (Join-Path $bin 'node.exe') $SignCert }
|
|
327
|
+
Say ("payload tree $(Split-Path -Leaf $Dest) built in {0:N1}s (realNode=$RealNode)" -f ((Get-Date) - $t0).TotalSeconds)
|
|
213
328
|
}
|
|
214
329
|
|
|
215
|
-
function New-InstallTree([string]$Dest) {
|
|
330
|
+
function New-InstallTree([string]$Dest, [switch]$RealNode) {
|
|
216
331
|
if (Test-Path -LiteralPath $Dest) { Remove-Item -LiteralPath $Dest -Recurse -Force }
|
|
217
332
|
$bin = Join-Path $Dest 'bin'
|
|
218
333
|
$dist = Join-Path $Dest 'lib\edge-sdk\dist'
|
|
@@ -220,7 +335,14 @@ function New-InstallTree([string]$Dest) {
|
|
|
220
335
|
foreach ($f in @('stop-existing.ps1', 'register-task.ps1', 'install-update.ps1', 'apply-update.ps1', 'run-connector.ps1', 'trillboards-edge-task.vbs')) {
|
|
221
336
|
Copy-Item -LiteralPath (Join-Path $Deploy $f) -Destination (Join-Path $bin $f) -Force
|
|
222
337
|
}
|
|
223
|
-
|
|
338
|
+
# Same reasoning as the payload tree: only the arms that RUN the connector
|
|
339
|
+
# need a real interpreter, and copying ~110 MB per arm is what blew the job
|
|
340
|
+
# budget the first time this harness ran.
|
|
341
|
+
if ($RealNode) {
|
|
342
|
+
Copy-Item -LiteralPath (Get-Command node).Source -Destination (Join-Path $bin 'node.exe') -Force
|
|
343
|
+
} elseif ($script:SignedCache -and (Test-Path -LiteralPath (Join-Path $script:SignedCache 'node.exe'))) {
|
|
344
|
+
Copy-Item -LiteralPath (Join-Path $script:SignedCache 'node.exe') -Destination (Join-Path $bin 'node.exe') -Force
|
|
345
|
+
}
|
|
224
346
|
[System.IO.File]::WriteAllText((Join-Path $dist 'cli.js'), "setInterval(function(){}, 1000);`r`n", (New-Object System.Text.UTF8Encoding($false)))
|
|
225
347
|
}
|
|
226
348
|
|
|
@@ -242,7 +364,8 @@ function New-Catalog([string]$Path, [string]$Version, [string]$Sha, [long]$Size)
|
|
|
242
364
|
function Invoke-Applier([string]$InstallDir, [string[]]$Args) {
|
|
243
365
|
$ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
244
366
|
$all = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $Applier, '-InstallDir', $InstallDir) + $Args
|
|
245
|
-
$p = Start-Process -FilePath $ps -ArgumentList $all -NoNewWindow -PassThru
|
|
367
|
+
$p = Start-Process -FilePath $ps -ArgumentList $all -NoNewWindow -PassThru
|
|
368
|
+
$null = Wait-Bounded -Process $p
|
|
246
369
|
$updateDir = Join-Path $InstallDir '.auto-update'
|
|
247
370
|
$outcome = $null
|
|
248
371
|
$outcomePath = Join-Path $updateDir 'outcome.json'
|
|
@@ -269,15 +392,18 @@ function Invoke-Applier([string]$InstallDir, [string[]]$Args) {
|
|
|
269
392
|
}
|
|
270
393
|
|
|
271
394
|
function Publish-Payload([string]$TreeDir, [string]$Name) {
|
|
395
|
+
$t0 = Get-Date
|
|
272
396
|
$zip = Join-Path $Www $Name
|
|
273
397
|
if (Test-Path -LiteralPath $zip) { Remove-Item -LiteralPath $zip -Force }
|
|
274
398
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
275
399
|
[System.IO.Compression.ZipFile]::CreateFromDirectory($TreeDir, $zip, [System.IO.Compression.CompressionLevel]::Fastest, $false)
|
|
276
|
-
|
|
400
|
+
$out = [pscustomobject]@{
|
|
277
401
|
Path = $zip
|
|
278
402
|
Bytes = (Get-Item -LiteralPath $zip).Length
|
|
279
403
|
Sha = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
280
404
|
}
|
|
405
|
+
Say ("zipped $Name : {0:N1} MB in {1:N1}s" -f ($out.Bytes / 1MB), ((Get-Date) - $t0).TotalSeconds)
|
|
406
|
+
return $out
|
|
281
407
|
}
|
|
282
408
|
|
|
283
409
|
function Get-Shape([string]$Name) {
|
|
@@ -318,15 +444,18 @@ try {
|
|
|
318
444
|
New-Item -ItemType Directory -Force -Path $Root, $Www | Out-Null
|
|
319
445
|
Start-Origin
|
|
320
446
|
|
|
447
|
+
Say 'minting two throwaway code-signing certificates'
|
|
321
448
|
$goodCert = New-HarnessCert 'Trillboards CI Update Harness'
|
|
322
449
|
$otherCert = New-HarnessCert 'Definitely Not Trillboards'
|
|
323
450
|
$signer = 'Trillboards CI Update Harness'
|
|
324
451
|
$version = '9.9.9'
|
|
452
|
+
Say 'certificates ready'
|
|
325
453
|
|
|
326
454
|
$payloadTree = Join-Path $Root 'payload-src'
|
|
327
455
|
New-PayloadTree -Dest $payloadTree -SignCert $goodCert
|
|
328
456
|
$good = Publish-Payload $payloadTree 'payload.zip'
|
|
329
|
-
Say ("payload: {0} bytes, sha {1}" -f $good.Bytes, $good.Sha)
|
|
457
|
+
Say ("reference payload: {0} bytes, sha {1}" -f $good.Bytes, $good.Sha)
|
|
458
|
+
Assert-WithinDeadline 'building the reference payload'
|
|
330
459
|
|
|
331
460
|
$catalogGood = Join-Path $Www 'catalog-good.ps1'
|
|
332
461
|
New-Catalog $catalogGood $version $good.Sha $good.Bytes
|
|
@@ -336,6 +465,7 @@ try {
|
|
|
336
465
|
|
|
337
466
|
# ---- A: HAPPY PATH. The control for every arm below it -------------------
|
|
338
467
|
Say '--- arm A: happy path (the shared control) ---'
|
|
468
|
+
Assert-WithinDeadline 'arm A: happy path (the shared control)'
|
|
339
469
|
$installA = Join-Path $Root 'install-a'
|
|
340
470
|
New-InstallTree $installA
|
|
341
471
|
$a = Invoke-Applier $installA @('-Stage', '-UpdateUrl', $urlZip, '-CatalogUrl', "$BaseUrl/catalog-good.ps1",
|
|
@@ -349,6 +479,7 @@ try {
|
|
|
349
479
|
|
|
350
480
|
# ---- B: WRONG HASH -------------------------------------------------------
|
|
351
481
|
Say '--- arm B: the payload does not match the signed hash ---'
|
|
482
|
+
Assert-WithinDeadline 'arm B: the payload does not match the signed hash'
|
|
352
483
|
$tampered = Join-Path $Www 'payload-tampered.zip'
|
|
353
484
|
Copy-Item -LiteralPath $good.Path -Destination $tampered -Force
|
|
354
485
|
$bytes = [System.IO.File]::ReadAllBytes($tampered)
|
|
@@ -366,6 +497,7 @@ try {
|
|
|
366
497
|
|
|
367
498
|
# ---- C: SHORT BODY -------------------------------------------------------
|
|
368
499
|
Say '--- arm C: the download is short (what a disk filling mid-download looks like) ---'
|
|
500
|
+
Assert-WithinDeadline 'arm C: the download is short (what a disk filling mid-download looks like)'
|
|
369
501
|
$short = Join-Path $Www 'payload-short.zip'
|
|
370
502
|
# A real byte[] copy, not a PowerShell range slice: `$bytes[0..n]` boxes every
|
|
371
503
|
# element into an Object[], which WriteAllBytes will not take and which is
|
|
@@ -385,6 +517,7 @@ try {
|
|
|
385
517
|
|
|
386
518
|
# ---- D: UNSIGNED CATALOG -------------------------------------------------
|
|
387
519
|
Say '--- arm D: the catalog is not signed at all ---'
|
|
520
|
+
Assert-WithinDeadline 'arm D: the catalog is not signed at all'
|
|
388
521
|
$catalogUnsigned = Join-Path $Www 'catalog-unsigned.ps1'
|
|
389
522
|
New-Catalog $catalogUnsigned $version $good.Sha $good.Bytes
|
|
390
523
|
if ((Get-AuthenticodeSignature -LiteralPath $catalogUnsigned).Status -eq 'Valid') {
|
|
@@ -400,6 +533,7 @@ try {
|
|
|
400
533
|
|
|
401
534
|
# ---- E: WRONG PUBLISHER (the sharpest control) ---------------------------
|
|
402
535
|
Say '--- arm E: the catalog is VALIDLY signed, by somebody else ---'
|
|
536
|
+
Assert-WithinDeadline 'arm E: the catalog is VALIDLY signed, by somebody else'
|
|
403
537
|
$catalogOther = Join-Path $Www 'catalog-other.ps1'
|
|
404
538
|
New-Catalog $catalogOther $version $good.Sha $good.Bytes
|
|
405
539
|
Sign-File $catalogOther $otherCert
|
|
@@ -417,6 +551,7 @@ try {
|
|
|
417
551
|
|
|
418
552
|
# ---- F: SIGNED THEN TAMPERED --------------------------------------------
|
|
419
553
|
Say '--- arm F: the catalog was signed, then edited ---'
|
|
554
|
+
Assert-WithinDeadline 'arm F: the catalog was signed, then edited'
|
|
420
555
|
$catalogEdited = Join-Path $Www 'catalog-edited.ps1'
|
|
421
556
|
Copy-Item -LiteralPath $catalogGood -Destination $catalogEdited -Force
|
|
422
557
|
Add-Content -LiteralPath $catalogEdited -Value '# an edit after signing'
|
|
@@ -431,6 +566,7 @@ try {
|
|
|
431
566
|
|
|
432
567
|
# ---- G: DISK PREFLIGHT ---------------------------------------------------
|
|
433
568
|
Say '--- arm G: the declared payload does not fit on the volume ---'
|
|
569
|
+
Assert-WithinDeadline 'arm G: the declared payload does not fit on the volume'
|
|
434
570
|
$huge = Join-Path $Www 'catalog-huge.ps1'
|
|
435
571
|
New-Catalog $huge $version $good.Sha 900000000000
|
|
436
572
|
Sign-File $huge $goodCert
|
|
@@ -445,6 +581,7 @@ try {
|
|
|
445
581
|
|
|
446
582
|
# ---- H: UNSIGNED SCRIPT INSIDE THE PAYLOAD -------------------------------
|
|
447
583
|
Say '--- arm H: the payload contains an unsigned script ---'
|
|
584
|
+
Assert-WithinDeadline 'arm H: the payload contains an unsigned script'
|
|
448
585
|
$rogueTree = Join-Path $Root 'payload-rogue'
|
|
449
586
|
New-PayloadTree -Dest $rogueTree -SignCert $goodCert -WithUnsignedScript
|
|
450
587
|
$rogue = Publish-Payload $rogueTree 'payload-rogue.zip'
|
|
@@ -468,9 +605,9 @@ try {
|
|
|
468
605
|
)) {
|
|
469
606
|
Say "--- arm $($arm.Name): commit over a RUNNING connector, with a payload that would re-register logon-only ---"
|
|
470
607
|
$installJ = Join-Path $Root "install-$($arm.Name)"
|
|
471
|
-
New-InstallTree $installJ
|
|
608
|
+
New-InstallTree $installJ -RealNode
|
|
472
609
|
$downTree = Join-Path $Root "payload-down-$($arm.Name)"
|
|
473
|
-
New-PayloadTree -Dest $downTree -SignCert $goodCert -LogonOnlyRegisterTask
|
|
610
|
+
New-PayloadTree -Dest $downTree -SignCert $goodCert -LogonOnlyRegisterTask -RealNode
|
|
474
611
|
$down = Publish-Payload $downTree "payload-down-$($arm.Name).zip"
|
|
475
612
|
$catDown = Join-Path $Www "catalog-down-$($arm.Name).ps1"
|
|
476
613
|
New-Catalog $catDown $version $down.Sha $down.Bytes
|
|
@@ -521,8 +658,10 @@ try {
|
|
|
521
658
|
# ---- K: the would-downgrade preflight, from an UNELEVATED caller ---------
|
|
522
659
|
if ($SkipUnelevatedArm) {
|
|
523
660
|
Say '--- arm K SKIPPED by request ---'
|
|
661
|
+
Assert-WithinDeadline 'arm K SKIPPED by request'
|
|
524
662
|
} else {
|
|
525
663
|
Say '--- arm K: an unelevated caller must REFUSE rather than downgrade ---'
|
|
664
|
+
Assert-WithinDeadline 'arm K: an unelevated caller must REFUSE rather than downgrade'
|
|
526
665
|
$installK = Join-Path $Root 'install-k'
|
|
527
666
|
New-InstallTree $installK
|
|
528
667
|
$downTree = Join-Path $Root 'payload-k'
|
|
@@ -563,8 +702,9 @@ try {
|
|
|
563
702
|
$ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
564
703
|
# Materialise the profile once, so the run under test is not measuring
|
|
565
704
|
# first-logon profile creation.
|
|
566
|
-
Start-Process -FilePath $env:ComSpec -ArgumentList '/c', 'exit' -Credential $cred -LoadUserProfile `
|
|
567
|
-
-WorkingDirectory 'C:\' -
|
|
705
|
+
$warm = Start-Process -FilePath $env:ComSpec -ArgumentList '/c', 'exit' -Credential $cred -LoadUserProfile `
|
|
706
|
+
-WorkingDirectory 'C:\' -PassThru -ErrorAction Stop
|
|
707
|
+
$null = Wait-Bounded -Process $warm -Phase 'arm K: materialising the local profile'
|
|
568
708
|
|
|
569
709
|
# Drive the COPY inside the install tree, not the one in the repo
|
|
570
710
|
# workspace: the ACL above covers the install tree, and the runner's
|
|
@@ -572,10 +712,11 @@ try {
|
|
|
572
712
|
$applierCopy = Join-Path $installK 'bin\apply-update.ps1'
|
|
573
713
|
$outK = Join-Path $Root 'k-stdout.txt'
|
|
574
714
|
$errK = Join-Path $Root 'k-stderr.txt'
|
|
575
|
-
$
|
|
715
|
+
$pk = Start-Process -FilePath $ps -Credential $cred -LoadUserProfile -WorkingDirectory 'C:\' -PassThru `
|
|
576
716
|
-RedirectStandardOutput $outK -RedirectStandardError $errK `
|
|
577
717
|
-ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $applierCopy,
|
|
578
718
|
'-InstallDir', $installK, '-Commit', '-ExpectedVersion', $version, '-CurrentVersion', '9.9.8')
|
|
719
|
+
$null = Wait-Bounded -Process $pk
|
|
579
720
|
foreach ($f in @($outK, $errK)) {
|
|
580
721
|
if (Test-Path -LiteralPath $f) {
|
|
581
722
|
$t = Get-Content -Raw -LiteralPath $f
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<#
|
|
2
|
+
enroll-connector.ps1 - UNATTENDED provisioning step for an MDM/Intune/SCCM
|
|
3
|
+
deployment. Invoked by installer.nsi when /ENROLLMENT_TOKEN= was supplied.
|
|
4
|
+
|
|
5
|
+
WHAT IT DOES
|
|
6
|
+
1. Writes the fleet enrollment credential to $InstallDir\trillboards.enrollment
|
|
7
|
+
with an ACL that grants the installing user ONLY (inheritance disabled),
|
|
8
|
+
because a plain %LOCALAPPDATA% file inherits whatever the parent grants
|
|
9
|
+
and this is the one credential in the package that can enroll machines.
|
|
10
|
+
2. Runs `trillboards-edge enroll`, which exchanges it for THIS machine's own
|
|
11
|
+
credential, writes that into trillboards.config.json, and DELETES the
|
|
12
|
+
enrollment file.
|
|
13
|
+
3. Deletes the file itself if the CLI could not (belt and braces - a fleet
|
|
14
|
+
credential left on 200 store PCs is the failure this whole design exists
|
|
15
|
+
to prevent).
|
|
16
|
+
|
|
17
|
+
EXIT CODES - these are the installer's, and they are deliberately NOT fatal to
|
|
18
|
+
the install:
|
|
19
|
+
0 enrolled, or already enrolled (a re-push is idempotent and free)
|
|
20
|
+
1 the credential was rejected: expired, revoked, or every enrollment it was
|
|
21
|
+
issued for is already used. A human must issue a new one.
|
|
22
|
+
6 network/server failure. The credential was NOT spent; the scheduled task
|
|
23
|
+
retries on the next logon.
|
|
24
|
+
|
|
25
|
+
A failed enrollment leaves a correctly INSTALLED connector that is simply not
|
|
26
|
+
yet provisioned, which is recoverable by re-running this script or by the
|
|
27
|
+
ordinary interactive setup. Aborting the whole install instead would leave the
|
|
28
|
+
machine with nothing and no way to retry.
|
|
29
|
+
#>
|
|
30
|
+
[CmdletBinding()]
|
|
31
|
+
param(
|
|
32
|
+
[Parameter(Mandatory = $true)][string]$InstallDir,
|
|
33
|
+
[Parameter(Mandatory = $true)][string]$EnrollmentToken,
|
|
34
|
+
[string]$SiteLabel = "",
|
|
35
|
+
[string]$ApiBase = "",
|
|
36
|
+
# Optional recorder credentials. Passed straight through to the CLI and never
|
|
37
|
+
# written to disk or to the log - only their PRESENCE is logged.
|
|
38
|
+
[string]$NvrUser = "",
|
|
39
|
+
[string]$NvrPass = ""
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
$ErrorActionPreference = "Stop"
|
|
43
|
+
|
|
44
|
+
$logDir = Join-Path $env:LOCALAPPDATA "Trillboards\logs"
|
|
45
|
+
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
|
|
46
|
+
$logPath = Join-Path $logDir "enroll.log"
|
|
47
|
+
|
|
48
|
+
function Write-Log([string]$Message) {
|
|
49
|
+
# NEVER log the token. Only its shape, so support can match it to a portal row
|
|
50
|
+
# without the log becoming a credential store.
|
|
51
|
+
$stamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
52
|
+
Add-Content -Path $logPath -Value "[$stamp] $Message"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
$enrollmentFile = Join-Path $InstallDir "trillboards.enrollment"
|
|
56
|
+
$configPath = Join-Path $InstallDir "trillboards.config.json"
|
|
57
|
+
|
|
58
|
+
function Remove-EnrollmentFile {
|
|
59
|
+
if (Test-Path $enrollmentFile) {
|
|
60
|
+
try {
|
|
61
|
+
Remove-Item -Force $enrollmentFile
|
|
62
|
+
Write-Log "enrollment file removed"
|
|
63
|
+
} catch {
|
|
64
|
+
Write-Log "WARNING: could not remove enrollment file: $($_.Exception.Message)"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
if ([string]::IsNullOrWhiteSpace($EnrollmentToken)) {
|
|
71
|
+
Write-Log "no enrollment token supplied; nothing to do"
|
|
72
|
+
exit 0
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
$tokenShape = if ($EnrollmentToken.Length -gt 11) {
|
|
76
|
+
"$($EnrollmentToken.Substring(0,7))...$($EnrollmentToken.Substring($EnrollmentToken.Length-4))"
|
|
77
|
+
} else { "(short)" }
|
|
78
|
+
$credState = if ([string]::IsNullOrWhiteSpace($NvrUser)) { "absent" } else { "supplied" }
|
|
79
|
+
Write-Log "starting unattended enrollment (credential $tokenShape, site '$SiteLabel', recorder credentials $credState)"
|
|
80
|
+
|
|
81
|
+
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
|
|
82
|
+
# -NoNewline so the CLI's trim() sees exactly the token; ASCII because the key
|
|
83
|
+
# alphabet is hex and a BOM would corrupt the first character.
|
|
84
|
+
[System.IO.File]::WriteAllText($enrollmentFile, $EnrollmentToken, [System.Text.UTF8Encoding]::new($false))
|
|
85
|
+
|
|
86
|
+
# Owner-only ACL with inheritance broken. %LOCALAPPDATA% is already per-user,
|
|
87
|
+
# but this file is the one credential that can enroll further machines, so it
|
|
88
|
+
# does not ride on a directory default that a group policy may widen.
|
|
89
|
+
try {
|
|
90
|
+
$acl = Get-Acl $enrollmentFile
|
|
91
|
+
$acl.SetAccessRuleProtection($true, $false) # break inheritance, drop inherited ACEs
|
|
92
|
+
$me = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
93
|
+
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
94
|
+
$me, "FullControl", "None", "None", "Allow")
|
|
95
|
+
$acl.SetAccessRule($rule)
|
|
96
|
+
Set-Acl -Path $enrollmentFile -AclObject $acl
|
|
97
|
+
Write-Log "enrollment file ACL set to owner-only"
|
|
98
|
+
} catch {
|
|
99
|
+
# Non-fatal: the file is deleted seconds from now. Losing the ACL is worth a
|
|
100
|
+
# log line, not a failed rollout.
|
|
101
|
+
Write-Log "WARNING: could not tighten enrollment file ACL: $($_.Exception.Message)"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
$node = Join-Path $InstallDir "bin\node.exe"
|
|
105
|
+
$cli = Join-Path $InstallDir "lib\edge-sdk\dist\cli.js"
|
|
106
|
+
if (-not (Test-Path $node) -or -not (Test-Path $cli)) {
|
|
107
|
+
# Retryable and the credential is UNSPENT - keep it so the next-logon retry
|
|
108
|
+
# can use it once the install finishes laying the tree down.
|
|
109
|
+
Write-Log "ERROR: connector is not installed at $InstallDir (retryable, credential preserved)"
|
|
110
|
+
exit 6
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
$cliArgs = @($cli, "enroll", "--config", $configPath, "--enrollment-file", $enrollmentFile)
|
|
114
|
+
if (-not [string]::IsNullOrWhiteSpace($SiteLabel)) { $cliArgs += @("--site", $SiteLabel) }
|
|
115
|
+
if (-not [string]::IsNullOrWhiteSpace($ApiBase)) { $cliArgs += @("--api-base", $ApiBase) }
|
|
116
|
+
if (-not [string]::IsNullOrWhiteSpace($NvrUser)) { $cliArgs += @("--nvr-user", $NvrUser) }
|
|
117
|
+
if (-not [string]::IsNullOrWhiteSpace($NvrPass)) { $cliArgs += @("--nvr-pass", $NvrPass) }
|
|
118
|
+
|
|
119
|
+
$oldPath = $env:PATH
|
|
120
|
+
$oldNodePath = $env:NODE_PATH
|
|
121
|
+
try {
|
|
122
|
+
$env:PATH = "$(Join-Path $InstallDir 'bin');$oldPath"
|
|
123
|
+
$env:NODE_PATH = Join-Path $InstallDir "lib\node_modules"
|
|
124
|
+
$output = (& $node @cliArgs 2>&1 | Out-String)
|
|
125
|
+
$code = $LASTEXITCODE
|
|
126
|
+
} finally {
|
|
127
|
+
$env:PATH = $oldPath
|
|
128
|
+
$env:NODE_PATH = $oldNodePath
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# The CLI never prints the credential (see maskConnectorKey), so its output is
|
|
132
|
+
# safe to log verbatim and is the only diagnostic a field failure leaves.
|
|
133
|
+
Write-Log "enroll exit=$code output=$($output.Trim())"
|
|
134
|
+
|
|
135
|
+
# DELETE ONLY ON A TERMINAL OUTCOME.
|
|
136
|
+
#
|
|
137
|
+
# The CLI already deletes the file itself on success. The dangerous case is a
|
|
138
|
+
# RETRYABLE failure: an unconditional delete here would destroy the only
|
|
139
|
+
# persisted copy of the fleet credential on a transient outage during install,
|
|
140
|
+
# leaving that store permanently unprovisioned with nothing to retry from - the
|
|
141
|
+
# opposite of what the exit code says. So the file survives exit 6 and the
|
|
142
|
+
# next-logon retry in run-connector.ps1 picks it up.
|
|
143
|
+
#
|
|
144
|
+
# It IS deleted on a credential REJECTION (exit 1), because that token can
|
|
145
|
+
# never enroll anything again and leaving a dead credential on disk is pure
|
|
146
|
+
# exposure.
|
|
147
|
+
switch ($code) {
|
|
148
|
+
0 { Remove-EnrollmentFile; Write-Log "enrollment complete, cameras registered"; exit 0 }
|
|
149
|
+
5 {
|
|
150
|
+
# Provisioned, but no feeds. The credential is SPENT, so the enrollment
|
|
151
|
+
# file is done - what is missing is recorder credentials, not a retry.
|
|
152
|
+
Remove-EnrollmentFile
|
|
153
|
+
Write-Log "enrolled, but NO camera feeds registered (recorder credentials absent or no recorder found)"
|
|
154
|
+
exit 5
|
|
155
|
+
}
|
|
156
|
+
1 { Remove-EnrollmentFile; Write-Log "enrollment credential REJECTED - a new one must be issued"; exit 1 }
|
|
157
|
+
4 { Remove-EnrollmentFile; Write-Log "no enrollment token was readable"; exit 1 }
|
|
158
|
+
default {
|
|
159
|
+
Write-Log "enrollment deferred (retryable) - credential PRESERVED for the next-logon retry"
|
|
160
|
+
exit 6
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
# An exception here is a local/environmental failure, not a rejected
|
|
165
|
+
# credential, so it is retryable and the token is preserved for the same
|
|
166
|
+
# reason as exit 6 above.
|
|
167
|
+
Write-Log "ERROR (retryable, credential preserved): $($_.Exception.Message)"
|
|
168
|
+
exit 6
|
|
169
|
+
}
|