@trillboards/edge-sdk 0.2.16 → 0.2.17
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/THIRD-PARTY-NOTICES.md +51 -0
- package/deploy/macos/build-macos-pkg.sh +35 -5
- package/deploy/windows/build-windows.ps1 +25 -2
- package/deploy/windows/ci/list-shipped-scripts.ps1 +58 -0
- package/deploy/windows/ci/tb-canary.ps1 +18 -0
- package/deploy/windows/ci/tb-canary.vbs +17 -0
- package/deploy/windows/ci/verify-allsigned-enforcement.ps1 +608 -0
- package/deploy/windows/register-litertlm-task.ps1 +3 -3
- package/deploy/windows/register-task.ps1 +3 -3
- package/deploy/windows/stop-existing.ps1 +2 -2
- package/deploy/windows/trillboards-hidden-run.vbs +3 -3
- package/deploy/windows/trillboards-setup.ps1 +2 -2
- package/dist/EdgeAgent.js +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/package.json +1 -1
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -47,3 +47,54 @@ SOFTWARE.
|
|
|
47
47
|
Frigate™ is a trademark of Frigate, Inc. Its use here is nominative — to
|
|
48
48
|
identify the upstream project this code was ported from. Trillboards is not
|
|
49
49
|
affiliated with, endorsed by, or sponsored by Frigate, Inc.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## FFmpeg
|
|
54
|
+
|
|
55
|
+
The Windows and macOS connector installers bundle a prebuilt static
|
|
56
|
+
`ffmpeg` binary. RTSP capture goes through it — `RTSPCameraProvider` spawns it
|
|
57
|
+
per frame — so it is redistributed as part of the product, not merely used at
|
|
58
|
+
build time.
|
|
59
|
+
|
|
60
|
+
**Which binary.** The Windows x64 build pinned at
|
|
61
|
+
`deploy/windows/build-windows.ps1` is
|
|
62
|
+
`eugeneware/ffmpeg-static` release `b6.0`, asset `ffmpeg-win32-x64`, SHA-256
|
|
63
|
+
`e9fd5e711debab9d680955fc1e38a2c1160fd280b144476cc3f62bc43ef49db1`. That
|
|
64
|
+
project republishes upstream Windows x64 builds from
|
|
65
|
+
[gyan.dev](https://www.gyan.dev/ffmpeg/builds/).
|
|
66
|
+
|
|
67
|
+
**License: GPL v3.** Confirmed two independent ways rather than assumed. The
|
|
68
|
+
upstream builder states "All builds are 64-bit, static and licensed as GPLv3",
|
|
69
|
+
and the binary reports its own configuration as
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
--enable-gpl --enable-version3 --enable-static ... --enable-libx264
|
|
73
|
+
--enable-libx265 --enable-libxvid --enable-libvidstab --enable-librubberband
|
|
74
|
+
--enable-libvmaf --enable-libopencore-amrnb --enable-libopencore-amrwb
|
|
75
|
+
--enable-libvo-amrwbenc ...
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`--enable-gpl` plus `--enable-version3` makes the combined work GPL v3.
|
|
79
|
+
`--enable-nonfree` is **absent**, so the build is redistributable; a `nonfree`
|
|
80
|
+
build would not be, and swapping the pin for one would make shipping it
|
|
81
|
+
unlawful. Check that flag before ever changing the pin.
|
|
82
|
+
|
|
83
|
+
**Corresponding Source (GPL v3 §6).** Trillboards conveys this object code and
|
|
84
|
+
will provide the complete Corresponding Source for the exact binary above, on
|
|
85
|
+
any medium customarily used for software interchange, to anyone who asks at
|
|
86
|
+
**legal@trillboards.com**, for as long as we offer spare parts or customer
|
|
87
|
+
support for this product. The upstream sources are FFmpeg
|
|
88
|
+
(<https://ffmpeg.org/download.html>, <https://git.ffmpeg.org/ffmpeg.git>) and
|
|
89
|
+
the build recipe published by the builder above.
|
|
90
|
+
|
|
91
|
+
**Our modification, stated plainly (GPL v3 §5(a)).** Trillboards redistributes
|
|
92
|
+
this binary unmodified except for one thing: we Authenticode-sign it with our
|
|
93
|
+
own code-signing certificate (publisher `Aethercorp Inc`) before bundling it.
|
|
94
|
+
Upstream ships it unsigned, which makes it an EDR and antivirus false-positive
|
|
95
|
+
magnet and the one unsigned executable in an otherwise fully signed bundle. The
|
|
96
|
+
signature is appended to the PE certificate table; no byte of the program image
|
|
97
|
+
is altered, its version and configuration strings are untouched, and it imposes
|
|
98
|
+
no restriction on anyone — a recipient remains free to strip the signature,
|
|
99
|
+
replace the binary, or rebuild it from the Corresponding Source above. Nothing
|
|
100
|
+
in the product requires `bin\ffmpeg.exe` to be signed in order to run.
|
|
@@ -37,11 +37,41 @@
|
|
|
37
37
|
# air-gapped CI)
|
|
38
38
|
#
|
|
39
39
|
# --universal builds ONE fat arm64+x86_64 .pkg (node + ffmpeg lipo'd from both
|
|
40
|
-
# arches; bundled fsevents.node is already universal)
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
40
|
+
# arches; bundled fsevents.node is already universal). --arch builds a
|
|
41
|
+
# single-arch .pkg.
|
|
42
|
+
#
|
|
43
|
+
# THE RELEASE ARTIFACT IS `--arch arm64`, NOT `--universal`. This comment used to
|
|
44
|
+
# say the opposite — that universal "installs + runs on every Mac" and "is the
|
|
45
|
+
# RELEASE artifact" — and that sentence is why PR #12228 tried to switch the
|
|
46
|
+
# release lane to it. Do not repeat that. A universal .pkg makes `node` and
|
|
47
|
+
# `ffmpeg` fat, and changes nothing about the constraint that actually decides
|
|
48
|
+
# this: `litert_lm_api` is published ONLY for `macosx_12_0_arm64`. There is no
|
|
49
|
+
# macOS x86_64 build to lipo against, and the installed `liblitert-lm.dylib` is
|
|
50
|
+
# arm64-thin. scripts/setup-litertlm.sh:151,156 already encodes that —
|
|
51
|
+
#
|
|
52
|
+
# if [ "$HOST_OS" = "Darwin" ] && [ "$HOST_ARCH" != "arm64" ]; then
|
|
53
|
+
# [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "local AI requires Apple silicon"
|
|
54
|
+
#
|
|
55
|
+
# — and postinstall sets TRILLBOARDS_REQUIRE_BUNDLED_MODEL=1, so on an Intel Mac
|
|
56
|
+
# that refusal is an abort, not a downgrade.
|
|
57
|
+
#
|
|
58
|
+
# The arm64-thin binaries in the shipped .pkg are therefore a SYMPTOM OF THE
|
|
59
|
+
# CORRECT BUILD, not a defect. What the two options actually do on an Intel Mac:
|
|
60
|
+
#
|
|
61
|
+
# --arch arm64 (today) : Installer.app refuses up front. Nothing is written.
|
|
62
|
+
# --universal : the installer ACCEPTS, lays down 2.27 GB, then dies in
|
|
63
|
+
# postinstall — and there is no uninstaller to clean up.
|
|
64
|
+
#
|
|
65
|
+
# Failing before the download beats failing after it. Ship universal only when
|
|
66
|
+
# upstream ships an x86_64 litert-lm AND the arch gate above is relaxed in the
|
|
67
|
+
# same change; the `macos-release-arch-lockstep` guard below fails the build if
|
|
68
|
+
# those two ever disagree.
|
|
69
|
+
#
|
|
70
|
+
# The one true thing the old comment said, kept because it still binds: a browser
|
|
71
|
+
# cannot reliably detect a Mac's CPU arch (Safari reports "MacIntel" even on
|
|
72
|
+
# Apple Silicon), so the portal cannot hand out an arch-specific download. That
|
|
73
|
+
# is an argument for ONE artifact at one URL — it is not an argument that the
|
|
74
|
+
# artifact can run everywhere, and conflating those is the error above.
|
|
45
75
|
#
|
|
46
76
|
# ffmpeg: a pinned static darwin build (eugeneware/ffmpeg-static b6.0 — the same
|
|
47
77
|
# upstream the ffmpeg-static npm package ships) is downloaded at build time and
|
|
@@ -35,6 +35,15 @@
|
|
|
35
35
|
.PARAMETER NodeVersion Pinned Node runtime (default v22.22.0, matches macOS).
|
|
36
36
|
.PARAMETER Offline Skip downloads; copy the host node/ffmpeg (air-gapped CI).
|
|
37
37
|
.PARAMETER SkipNsis Stage only; do not compile the NSIS .exe (for unit checks).
|
|
38
|
+
.PARAMETER FfmpegPath Stage THIS ffmpeg.exe instead of downloading one. The
|
|
39
|
+
release workflow fetches the pinned build, verifies its
|
|
40
|
+
SHA-256, Authenticode-signs it, and passes the signed
|
|
41
|
+
copy here — signing changes the bytes, so the hash gate
|
|
42
|
+
has to run before it, not after.
|
|
43
|
+
.PARAMETER PrintFfmpegPin
|
|
44
|
+
Print "<download-url> <sha256>" and exit. The release
|
|
45
|
+
workflow reads the pin from here rather than restating
|
|
46
|
+
it, so the two can never drift.
|
|
38
47
|
|
|
39
48
|
.OUTPUTS
|
|
40
49
|
Prints the inner installer path on the last line:
|
|
@@ -44,7 +53,9 @@
|
|
|
44
53
|
param(
|
|
45
54
|
[string]$NodeVersion = "v22.22.0",
|
|
46
55
|
[switch]$Offline,
|
|
47
|
-
[switch]$SkipNsis
|
|
56
|
+
[switch]$SkipNsis,
|
|
57
|
+
[string]$FfmpegPath = "",
|
|
58
|
+
[switch]$PrintFfmpegPin
|
|
48
59
|
)
|
|
49
60
|
|
|
50
61
|
$ErrorActionPreference = "Stop"
|
|
@@ -55,6 +66,9 @@ Set-StrictMode -Version Latest
|
|
|
55
66
|
# verified after download; refuse to bundle an unverified binary.
|
|
56
67
|
$FfmpegStaticTag = if ($env:FFMPEG_STATIC_TAG) { $env:FFMPEG_STATIC_TAG } else { "b6.0" }
|
|
57
68
|
$FfmpegSha256Win = if ($env:FFMPEG_SHA256_WIN) { $env:FFMPEG_SHA256_WIN } else { "e9fd5e711debab9d680955fc1e38a2c1160fd280b144476cc3f62bc43ef49db1" }
|
|
69
|
+
$FfmpegUrl = "https://github.com/eugeneware/ffmpeg-static/releases/download/$FfmpegStaticTag/ffmpeg-win32-x64"
|
|
70
|
+
|
|
71
|
+
if ($PrintFfmpegPin) { Write-Output "$FfmpegUrl $FfmpegSha256Win"; exit 0 }
|
|
58
72
|
|
|
59
73
|
# ─── Locate ourselves + the edge-sdk package root ─────────────────────────────
|
|
60
74
|
$Here = Split-Path -Parent $MyInvocation.MyCommand.Path # deploy\windows
|
|
@@ -152,8 +166,17 @@ if ($Offline) {
|
|
|
152
166
|
$hostFf = (Get-Command ffmpeg -ErrorAction SilentlyContinue).Source
|
|
153
167
|
if (-not $hostFf) { Die "offline mode: no host ffmpeg on PATH to copy." }
|
|
154
168
|
Copy-Item -Force $hostFf $FfmpegBin
|
|
169
|
+
} elseif ($FfmpegPath) {
|
|
170
|
+
# Supplied by the release workflow AFTER it verified the pinned SHA-256 and
|
|
171
|
+
# Authenticode-signed the binary. Re-checking the hash here would fail by
|
|
172
|
+
# construction: the signature is appended to the PE certificate table, so the
|
|
173
|
+
# signed file cannot hash to the unsigned pin. The pin still gates the build —
|
|
174
|
+
# just one step earlier, in the caller, against the bytes it downloaded.
|
|
175
|
+
if (-not (Test-Path -LiteralPath $FfmpegPath)) { Die "-FfmpegPath does not exist: $FfmpegPath" }
|
|
176
|
+
Log "Staging supplied ffmpeg: $FfmpegPath"
|
|
177
|
+
Copy-Item -Force -LiteralPath $FfmpegPath -Destination $FfmpegBin
|
|
155
178
|
} else {
|
|
156
|
-
$url =
|
|
179
|
+
$url = $FfmpegUrl
|
|
157
180
|
Log "Downloading pinned static ffmpeg: $url"
|
|
158
181
|
Invoke-WebRequest -Uri $url -OutFile "$Scratch\ffmpeg.exe" -UseBasicParsing
|
|
159
182
|
$actual = (Get-FileHash -Algorithm SHA256 "$Scratch\ffmpeg.exe").Hash.ToLower()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Emit the scripts that actually ship inside the connector installer, DERIVED
|
|
4
|
+
from build-windows.ps1 rather than restated.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
"What ships" is decided by the `Copy-Item -Force "$Here\<file>"
|
|
8
|
+
"$Payload\bin\..."` calls in build-windows.ps1 - nothing else. A hand-kept
|
|
9
|
+
list beside it is a list that goes stale the first time someone adds a script
|
|
10
|
+
and forgets, and the failure is silent: the new file ships unsigned and only a
|
|
11
|
+
customer under GPO AllSigned finds out.
|
|
12
|
+
|
|
13
|
+
Consumers: the release workflow (which files to Authenticode-sign) and the
|
|
14
|
+
AllSigned enforcement harness (which files to prove).
|
|
15
|
+
|
|
16
|
+
Note deploy/windows/install-service.ps1 is deliberately absent: it is the
|
|
17
|
+
legacy service path, is not copied into the payload, and does not ship.
|
|
18
|
+
|
|
19
|
+
.PARAMETER Kind
|
|
20
|
+
ps1 | vbs | cmd | signable (ps1 + vbs) | all
|
|
21
|
+
|
|
22
|
+
.PARAMETER AbsolutePath
|
|
23
|
+
Emit full paths into deploy/windows instead of bare file names.
|
|
24
|
+
#>
|
|
25
|
+
[CmdletBinding()]
|
|
26
|
+
param(
|
|
27
|
+
[ValidateSet('ps1', 'vbs', 'cmd', 'signable', 'all')][string]$Kind = 'all',
|
|
28
|
+
[switch]$AbsolutePath
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
$ErrorActionPreference = 'Stop'
|
|
32
|
+
Set-StrictMode -Version Latest
|
|
33
|
+
|
|
34
|
+
$windowsDir = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
|
35
|
+
$build = Join-Path $windowsDir 'build-windows.ps1'
|
|
36
|
+
if (-not (Test-Path -LiteralPath $build)) { throw "build-windows.ps1 not found next to $PSScriptRoot" }
|
|
37
|
+
|
|
38
|
+
$text = [IO.File]::ReadAllText($build)
|
|
39
|
+
$matched = [regex]::Matches($text, 'Copy-Item\s+-Force\s+"\$Here\\([^"\\]+)"\s+"\$Payload\\bin\\')
|
|
40
|
+
$names = @($matched | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique)
|
|
41
|
+
|
|
42
|
+
# Derivation floor. If the Copy-Item shape in build-windows.ps1 ever changes,
|
|
43
|
+
# this fails loudly here instead of silently signing nothing downstream.
|
|
44
|
+
if ($names.Count -lt 12) {
|
|
45
|
+
throw "only $($names.Count) staged script(s) derived from build-windows.ps1 - the Copy-Item pattern changed; fix this derivation before trusting any signing step"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
$selected = switch ($Kind) {
|
|
49
|
+
'ps1' { $names | Where-Object { $_ -like '*.ps1' } }
|
|
50
|
+
'vbs' { $names | Where-Object { $_ -like '*.vbs' } }
|
|
51
|
+
'cmd' { $names | Where-Object { $_ -like '*.cmd' } }
|
|
52
|
+
'signable' { $names | Where-Object { $_ -like '*.ps1' -or $_ -like '*.vbs' } }
|
|
53
|
+
default { $names }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
foreach ($n in ($selected | Sort-Object)) {
|
|
57
|
+
if ($AbsolutePath) { Write-Output (Join-Path $windowsDir $n) } else { Write-Output $n }
|
|
58
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# tb-canary.ps1 - CI only. Never staged into the installer payload.
|
|
2
|
+
#
|
|
3
|
+
# The AllSigned enforcement proof needs something it can actually RUN and watch
|
|
4
|
+
# be blocked or admitted. The shipped scripts stop scheduled tasks and move
|
|
5
|
+
# install trees, so they are proved through PowerShell's policy gate without
|
|
6
|
+
# being executed; this stands in for them at execution time, and the release
|
|
7
|
+
# workflow signs it in the SAME Trusted Signing call as the shipped scripts so
|
|
8
|
+
# it carries identical signature provenance.
|
|
9
|
+
#
|
|
10
|
+
# Keep it ASCII and keep it trivial: anything it does other than write a marker
|
|
11
|
+
# would make a 'blocked' result ambiguous.
|
|
12
|
+
# [CmdletBinding()] on purpose: it lets the harness validate its non-executing
|
|
13
|
+
# policy probe against this same file, because an unknown named parameter then
|
|
14
|
+
# fails to bind instead of falling through to $args and running the body.
|
|
15
|
+
[CmdletBinding()]
|
|
16
|
+
param([string]$MarkerPath = "")
|
|
17
|
+
if ($MarkerPath) { [IO.File]::WriteAllText($MarkerPath, "ran") }
|
|
18
|
+
exit 0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Option Explicit
|
|
2
|
+
|
|
3
|
+
' tb-canary.vbs - CI only. Never staged into the installer payload.
|
|
4
|
+
'
|
|
5
|
+
' The .vbs half of the AllSigned enforcement proof. WSH has its own gate
|
|
6
|
+
' (TrustPolicy=2, "run only signed and trusted scripts"), which PowerShell's
|
|
7
|
+
' execution policy does not cover, so the .vbs enforcement is measured
|
|
8
|
+
' separately. Signed in the same Trusted Signing call as the shipped scripts.
|
|
9
|
+
|
|
10
|
+
Dim fso, args, f
|
|
11
|
+
Set args = WScript.Arguments
|
|
12
|
+
If args.Count > 0 Then
|
|
13
|
+
Set fso = CreateObject("Scripting.FileSystemObject")
|
|
14
|
+
Set f = fso.CreateTextFile(args(0), True)
|
|
15
|
+
f.WriteLine "ran"
|
|
16
|
+
f.Close
|
|
17
|
+
End If
|
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Prove, on a real Windows host, that the shipped scripts survive a machine-wide
|
|
4
|
+
`AllSigned` policy - and prove the assertion FAILS against unsigned scripts
|
|
5
|
+
first, so a green result cannot mean "the policy never applied".
|
|
6
|
+
|
|
7
|
+
.DESCRIPTION
|
|
8
|
+
The product's runtime invocation sites all pass `-ExecutionPolicy Bypass`.
|
|
9
|
+
That sets only the *Process* scope, and PowerShell resolves policy
|
|
10
|
+
MachinePolicy > UserPolicy > Process > CurrentUser > LocalMachine - so a GPO
|
|
11
|
+
of `AllSigned` (routine on a managed corporate estate) overrides every one of
|
|
12
|
+
them. This script sets that GPO for real (HKLM\SOFTWARE\Policies\...) and
|
|
13
|
+
measures what happens.
|
|
14
|
+
|
|
15
|
+
It is built the way the desktop-less window probe should have been built (see
|
|
16
|
+
.claude/lessons/2026-08-14-a-hosted-windows-runner-has-no-desktop-...md): every
|
|
17
|
+
positive assertion is paired with a control that MUST fire. Four controls:
|
|
18
|
+
|
|
19
|
+
A unsigned -> must be BLOCKED (policy is really in force)
|
|
20
|
+
B signed, publisher untrusted -> must be BLOCKED (we measure trust, not bytes)
|
|
21
|
+
C signed + publisher trusted -> must be ALLOWED <- the deliverable
|
|
22
|
+
D signed then tampered -> must be BLOCKED (we measure THIS signature)
|
|
23
|
+
|
|
24
|
+
If A, B or D does not fire, the harness reports VACUOUS and exits non-zero.
|
|
25
|
+
A pass therefore cannot be produced by a policy that failed to apply, by a
|
|
26
|
+
cert store that trusts everything, or by a gate that ignores the signature.
|
|
27
|
+
|
|
28
|
+
Shipped scripts are probed WITHOUT executing them. Their bodies stop scheduled
|
|
29
|
+
tasks and move install trees, so the probe invokes each one with an unknown
|
|
30
|
+
named parameter: PowerShell checks the execution policy when it LOADS the file,
|
|
31
|
+
before it binds parameters, so a [CmdletBinding()] script gives up the gate's
|
|
32
|
+
verdict and then dies on a binding error with its body never entered. Two of
|
|
33
|
+
the shipped scripts do not declare [CmdletBinding()] - an unknown argument
|
|
34
|
+
would fall through to positional binding and RUN them - so those assert the two
|
|
35
|
+
inputs to the same decision instead (signature Valid, publisher trusted).
|
|
36
|
+
Which route each file took is printed, never inferred.
|
|
37
|
+
|
|
38
|
+
NB `(Get-Command <path> -CommandType ExternalScript).ScriptBlock` looks like it
|
|
39
|
+
should work and does NOT: command discovery does not run the policy check, and
|
|
40
|
+
it reports 'allowed' for a file the policy really blocks. Measured, not
|
|
41
|
+
assumed - the probe validates itself against the unsigned canary before it is
|
|
42
|
+
trusted on anything.
|
|
43
|
+
|
|
44
|
+
Execution itself is proven on canaries carrying the IDENTICAL signature
|
|
45
|
+
provenance (in -AlreadySigned mode the canaries are signed by the production
|
|
46
|
+
certificate in the same signing call as the shipped files).
|
|
47
|
+
|
|
48
|
+
.PARAMETER ScriptPath Shipped .ps1 files to prove.
|
|
49
|
+
.PARAMETER VbsPath Shipped .vbs files to prove.
|
|
50
|
+
.PARAMETER CmdPath Shipped .cmd files. Recorded, not gated - see below.
|
|
51
|
+
.PARAMETER CanaryPs1 An already-signed .ps1 that is safe to EXECUTE.
|
|
52
|
+
.PARAMETER CanaryVbs An already-signed .vbs that is safe to EXECUTE.
|
|
53
|
+
.PARAMETER SelfSign Hermetic mode: mint a throwaway code-signing cert and
|
|
54
|
+
sign COPIES of the inputs. Used by the per-PR lane,
|
|
55
|
+
where no Azure Trusted Signing credential exists.
|
|
56
|
+
.PARAMETER AlreadySigned Release mode: the inputs are already Authenticode
|
|
57
|
+
signed (by Azure Trusted Signing) and are proved
|
|
58
|
+
as-is, with the unsigned control derived by stripping
|
|
59
|
+
the signature block from a copy.
|
|
60
|
+
|
|
61
|
+
.NOTES
|
|
62
|
+
.cmd/.bat have no Authenticode SIP and cmd.exe has no execution policy, so a
|
|
63
|
+
batch file can be neither signed nor blocked by AllSigned. This harness proves
|
|
64
|
+
that rather than assuming it, so the claim in the runbook stays measured.
|
|
65
|
+
#>
|
|
66
|
+
[CmdletBinding()]
|
|
67
|
+
param(
|
|
68
|
+
[string[]]$ScriptPath = @(),
|
|
69
|
+
[string[]]$VbsPath = @(),
|
|
70
|
+
[string[]]$CmdPath = @(),
|
|
71
|
+
[string]$CanaryPs1 = "",
|
|
72
|
+
[string]$CanaryVbs = "",
|
|
73
|
+
[switch]$SelfSign,
|
|
74
|
+
[switch]$AlreadySigned
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
$ErrorActionPreference = 'Stop'
|
|
78
|
+
Set-StrictMode -Version Latest
|
|
79
|
+
|
|
80
|
+
if (-not ($SelfSign -xor $AlreadySigned)) {
|
|
81
|
+
Write-Error 'Choose exactly one of -SelfSign or -AlreadySigned.'
|
|
82
|
+
exit 2
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
$PolicyKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell'
|
|
86
|
+
$WshKey = 'HKLM:\SOFTWARE\Microsoft\Windows Script Host\Settings'
|
|
87
|
+
$Work = Join-Path ([IO.Path]::GetTempPath()) ("tb-allsigned-" + [Guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
88
|
+
$failures = New-Object System.Collections.Generic.List[string]
|
|
89
|
+
$addedThumbprints = New-Object System.Collections.Generic.List[string]
|
|
90
|
+
|
|
91
|
+
function Say($m) { Write-Host "[allsigned] $m" }
|
|
92
|
+
function Fail($m) { $script:failures.Add($m); Write-Host "[allsigned] FAIL: $m" -ForegroundColor Red }
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Signature-block handling. Both PowerShell and WSH append the Authenticode
|
|
96
|
+
# blob as trailing COMMENT lines, so an unsigned twin is the file up to the
|
|
97
|
+
# begin marker. Byte-exact on the retained prefix, which is what lets the
|
|
98
|
+
# "content unchanged by signing" assertion below be meaningful.
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# The comment prefix differs per SIP - PowerShell writes
|
|
101
|
+
# `# SIG # Begin signature block` and WSH writes `'' SIG '' Begin signature
|
|
102
|
+
# block` - so cut on the part they SHARE and walk back to the start of that
|
|
103
|
+
# line. Hardcoding either prefix silently fails to strip the other, and a
|
|
104
|
+
# "unsigned" control that is still signed does not fire, which is the one
|
|
105
|
+
# outcome this harness must never produce.
|
|
106
|
+
function Copy-WithoutSignature([string]$Source, [string]$Destination) {
|
|
107
|
+
$text = [IO.File]::ReadAllText($Source)
|
|
108
|
+
$idx = $text.IndexOf('Begin signature block', [StringComparison]::Ordinal)
|
|
109
|
+
$found = ($idx -ge 0)
|
|
110
|
+
if ($found) {
|
|
111
|
+
$lineStart = $text.LastIndexOf("`n", $idx)
|
|
112
|
+
$cut = if ($lineStart -ge 0) { $lineStart + 1 } else { 0 }
|
|
113
|
+
$text = $text.Substring(0, $cut)
|
|
114
|
+
}
|
|
115
|
+
[IO.File]::WriteAllText($Destination, $text, (New-Object System.Text.UTF8Encoding($false)))
|
|
116
|
+
return $found
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# The gate probes.
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
# PS 5.1 turns any stderr from a NATIVE command into an ErrorRecord, and with
|
|
124
|
+
# $ErrorActionPreference = 'Stop' that becomes terminating. A blocked script
|
|
125
|
+
# prints "cannot be loaded" to stderr, so the harness would die the moment a
|
|
126
|
+
# control fired correctly - the probes must run with Continue.
|
|
127
|
+
function Invoke-Native([string]$Exe, [string[]]$Arguments) {
|
|
128
|
+
$prev = $ErrorActionPreference
|
|
129
|
+
$ErrorActionPreference = 'Continue'
|
|
130
|
+
try { & $Exe @Arguments *> $null } finally { $ErrorActionPreference = $prev }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Actually EXECUTES a canary and reports whether it got to run. The canary
|
|
134
|
+
# writes a marker; the marker, not the exit code, is the evidence.
|
|
135
|
+
function Test-ScriptExecution([string]$Path) {
|
|
136
|
+
$marker = Join-Path $Work ("ran-" + [Guid]::NewGuid().ToString('N').Substring(0, 6) + ".txt")
|
|
137
|
+
$ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
138
|
+
Invoke-Native $ps @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $Path, '-MarkerPath', $marker)
|
|
139
|
+
if (Test-Path -LiteralPath $marker) { return 'allowed' }
|
|
140
|
+
return 'blocked'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function Test-VbsExecution([string]$Path) {
|
|
144
|
+
$marker = Join-Path $Work ("vbs-" + [Guid]::NewGuid().ToString('N').Substring(0, 6) + ".txt")
|
|
145
|
+
$cscript = Join-Path $env:SystemRoot 'System32\cscript.exe'
|
|
146
|
+
Invoke-Native $cscript @('//nologo', '//B', $Path, $marker)
|
|
147
|
+
if (Test-Path -LiteralPath $marker) { return 'allowed' }
|
|
148
|
+
return 'blocked'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function Test-CmdExecution([string]$Path) {
|
|
152
|
+
$marker = Join-Path $Work ("cmd-" + [Guid]::NewGuid().ToString('N').Substring(0, 6) + ".txt")
|
|
153
|
+
Invoke-Native $env:ComSpec @('/c', $Path, $marker)
|
|
154
|
+
if (Test-Path -LiteralPath $marker) { return 'allowed' }
|
|
155
|
+
return 'blocked'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# A DIRECT policy probe for scripts that declare [CmdletBinding()]: an unknown
|
|
159
|
+
# named parameter is a terminating binding error, and PowerShell checks the
|
|
160
|
+
# execution policy when it LOADS the file, before it binds. So the gate's verdict
|
|
161
|
+
# is observable without the body ever running.
|
|
162
|
+
#
|
|
163
|
+
# Restricted to [CmdletBinding()] scripts on purpose. Without it, an unrecognised
|
|
164
|
+
# named argument falls through to positional binding or $args and the script
|
|
165
|
+
# RUNS - and two of the shipped scripts register scheduled tasks.
|
|
166
|
+
function Test-ScriptPolicyByBinding([string]$Path) {
|
|
167
|
+
$probe = @'
|
|
168
|
+
$out = $args[1]
|
|
169
|
+
try {
|
|
170
|
+
& $args[0] -TbAllSignedProbeUnbindable
|
|
171
|
+
[IO.File]::WriteAllText($out, 'allowed')
|
|
172
|
+
} catch {
|
|
173
|
+
if (($_.Exception -is [System.Management.Automation.PSSecurityException]) -or
|
|
174
|
+
($_.FullyQualifiedErrorId -like 'UnauthorizedAccess*')) {
|
|
175
|
+
[IO.File]::WriteAllText($out, 'blocked')
|
|
176
|
+
} else {
|
|
177
|
+
[IO.File]::WriteAllText($out, 'allowed')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
'@
|
|
181
|
+
$id = [Guid]::NewGuid().ToString('N').Substring(0, 8)
|
|
182
|
+
$probeFile = Join-Path $Work "bind-$id.txt"
|
|
183
|
+
$resultFile = Join-Path $Work "bindverdict-$id.txt"
|
|
184
|
+
[IO.File]::WriteAllText($probeFile, $probe, (New-Object System.Text.UTF8Encoding($false)))
|
|
185
|
+
$ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
186
|
+
$inline = '& ([scriptblock]::Create([IO.File]::ReadAllText(''' + $probeFile + '''))) ''' + $Path + ''' ''' + $resultFile + ''''
|
|
187
|
+
Invoke-Native $ps @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', $inline)
|
|
188
|
+
if (-not (Test-Path -LiteralPath $resultFile)) { return 'indeterminate(no-verdict)' }
|
|
189
|
+
return ([IO.File]::ReadAllText($resultFile)).Trim()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function Test-DeclaresCmdletBinding([string]$Path) {
|
|
193
|
+
return ([IO.File]::ReadAllText($Path)) -match '(?m)^\s*\[CmdletBinding\(\)\]'
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# StrictMode makes a missing property a terminating error, so registry values
|
|
197
|
+
# that may not exist have to be read through the property bag.
|
|
198
|
+
function Get-RegValue([string]$Key, [string]$Name) {
|
|
199
|
+
if (-not (Test-Path $Key)) { return $null }
|
|
200
|
+
$item = Get-ItemProperty -Path $Key -ErrorAction SilentlyContinue
|
|
201
|
+
if (-not $item) { return $null }
|
|
202
|
+
if ($item.PSObject.Properties.Name -notcontains $Name) { return $null }
|
|
203
|
+
return $item.$Name
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function Add-CertToStore([Security.Cryptography.X509Certificates.X509Certificate2]$Cert, [string]$StoreName) {
|
|
207
|
+
# The PUBLIC certificate only. Nothing here reads, prints or persists a
|
|
208
|
+
# private key, a password or a token - the signing material never leaves the
|
|
209
|
+
# Trusted Signing service.
|
|
210
|
+
$store = New-Object Security.Cryptography.X509Certificates.X509Store($StoreName, 'LocalMachine')
|
|
211
|
+
$store.Open('ReadWrite')
|
|
212
|
+
$store.Add($Cert)
|
|
213
|
+
$store.Close()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function Remove-CertFromStore([string]$Thumbprint, [string]$StoreName) {
|
|
217
|
+
$store = New-Object Security.Cryptography.X509Certificates.X509Store($StoreName, 'LocalMachine')
|
|
218
|
+
$store.Open('ReadWrite')
|
|
219
|
+
foreach ($c in @($store.Certificates | Where-Object { $_.Thumbprint -eq $Thumbprint })) { $store.Remove($c) }
|
|
220
|
+
$store.Close()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# Set up
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
New-Item -ItemType Directory -Force -Path $Work | Out-Null
|
|
227
|
+
|
|
228
|
+
$priorPolicy = $null
|
|
229
|
+
$priorEnableScripts = $null
|
|
230
|
+
$hadPolicyKey = Test-Path $PolicyKey
|
|
231
|
+
$priorTrustPolicy = $null
|
|
232
|
+
$priorUseWinSafer = $null
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
# --- canaries -------------------------------------------------------------
|
|
236
|
+
# Committed next to this harness so both modes prove the same bytes. In
|
|
237
|
+
# -AlreadySigned mode the release workflow has already signed them in the same
|
|
238
|
+
# Trusted Signing call as the shipped scripts; in -SelfSign mode they are
|
|
239
|
+
# signed below with the throwaway certificate.
|
|
240
|
+
if (-not $CanaryPs1) { $CanaryPs1 = Join-Path $PSScriptRoot 'tb-canary.ps1' }
|
|
241
|
+
if (-not $CanaryVbs) { $CanaryVbs = Join-Path $PSScriptRoot 'tb-canary.vbs' }
|
|
242
|
+
if (-not (Test-Path -LiteralPath $CanaryPs1)) { Write-Error "canary not found: $CanaryPs1"; exit 2 }
|
|
243
|
+
if (-not (Test-Path -LiteralPath $CanaryVbs)) { Write-Error "canary not found: $CanaryVbs"; exit 2 }
|
|
244
|
+
$canaryPs1Src = Join-Path $Work 'tb-canary.ps1'
|
|
245
|
+
$canaryVbsSrc = Join-Path $Work 'tb-canary.vbs'
|
|
246
|
+
Copy-Item -LiteralPath $CanaryPs1 -Destination $canaryPs1Src -Force
|
|
247
|
+
Copy-Item -LiteralPath $CanaryVbs -Destination $canaryVbsSrc -Force
|
|
248
|
+
|
|
249
|
+
# --- assemble the signed / unsigned pairs ---------------------------------
|
|
250
|
+
$signedDir = Join-Path $Work 'signed'
|
|
251
|
+
$unsignedDir = Join-Path $Work 'unsigned'
|
|
252
|
+
New-Item -ItemType Directory -Force -Path $signedDir, $unsignedDir | Out-Null
|
|
253
|
+
|
|
254
|
+
$subjects = @()
|
|
255
|
+
foreach ($p in @($ScriptPath + $VbsPath)) {
|
|
256
|
+
if (-not (Test-Path -LiteralPath $p)) { throw "input not found: $p" }
|
|
257
|
+
$name = Split-Path -Leaf $p
|
|
258
|
+
$signed = Join-Path $signedDir $name
|
|
259
|
+
$unsigned = Join-Path $unsignedDir $name
|
|
260
|
+
Copy-Item -LiteralPath $p -Destination $signed -Force
|
|
261
|
+
$stripped = Copy-WithoutSignature -Source $p -Destination $unsigned
|
|
262
|
+
# In -AlreadySigned mode the input MUST carry a block to strip. If it does
|
|
263
|
+
# not, the "unsigned" control is not unsigned and would not fire.
|
|
264
|
+
if ($AlreadySigned -and -not $stripped) { Fail "$name : no signature block found to strip - the unsigned control would be vacuous" }
|
|
265
|
+
$subjects += [pscustomobject]@{
|
|
266
|
+
Name = $name; Signed = $signed; Unsigned = $unsigned
|
|
267
|
+
Kind = ([IO.Path]::GetExtension($p).TrimStart('.').ToLower())
|
|
268
|
+
# Only [CmdletBinding()] scripts can be probed without risking execution.
|
|
269
|
+
Probeable = (([IO.Path]::GetExtension($p) -ieq '.ps1') -and (Test-DeclaresCmdletBinding $unsigned))
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
foreach ($c in @(@{ n = 'tb-canary.ps1'; s = $canaryPs1Src }, @{ n = 'tb-canary.vbs'; s = $canaryVbsSrc })) {
|
|
273
|
+
$signed = Join-Path $signedDir $c.n
|
|
274
|
+
$unsigned = Join-Path $unsignedDir $c.n
|
|
275
|
+
Copy-Item -LiteralPath $c.s -Destination $signed -Force
|
|
276
|
+
$stripped = Copy-WithoutSignature -Source $c.s -Destination $unsigned
|
|
277
|
+
if ($AlreadySigned -and -not $stripped) { Fail "$($c.n): no signature block found to strip - the unsigned control would be vacuous" }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# --- sign (hermetic mode only) -------------------------------------------
|
|
281
|
+
if ($SelfSign) {
|
|
282
|
+
Say 'Minting a throwaway code-signing certificate (hermetic mode).'
|
|
283
|
+
$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject 'CN=Trillboards CI AllSigned Harness' `
|
|
284
|
+
-CertStoreLocation 'Cert:\CurrentUser\My' -NotAfter (Get-Date).AddDays(2)
|
|
285
|
+
$pub = New-Object Security.Cryptography.X509Certificates.X509Certificate2(, $cert.RawData)
|
|
286
|
+
# A self-signed cert must be in Root for the chain to build at all; without
|
|
287
|
+
# this its Status is UnknownError and control B would fire for the wrong
|
|
288
|
+
# reason.
|
|
289
|
+
Add-CertToStore -Cert $pub -StoreName 'Root'
|
|
290
|
+
$addedThumbprints.Add('Root|' + $pub.Thumbprint)
|
|
291
|
+
foreach ($f in @(Get-ChildItem -LiteralPath $signedDir -File)) {
|
|
292
|
+
$r = Set-AuthenticodeSignature -FilePath $f.FullName -Certificate $cert -HashAlgorithm SHA256
|
|
293
|
+
if ($r.Status -ne 'Valid') { throw "self-signing produced $($r.Status) for $($f.Name)" }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# --- what signing did to the file, measured ------------------------------
|
|
298
|
+
# The signer does NOT merely append: measured on windows-latest,
|
|
299
|
+
# Set-AuthenticodeSignature rewrites the whole file, and PS 5.1 re-emits it
|
|
300
|
+
# with a UTF-8 BOM and CRLF line endings. That is fine - it happens before
|
|
301
|
+
# packaging - but it is worth knowing, because it is the same mechanism that
|
|
302
|
+
# makes a post-signing edit so dangerous: whoever rewrites the file last wins,
|
|
303
|
+
# and the signature dies silently.
|
|
304
|
+
#
|
|
305
|
+
# So the assertion here is on the TEXT, normalised for the signer's own
|
|
306
|
+
# re-encoding. A genuine content change (mojibake from a codepage round-trip,
|
|
307
|
+
# a truncated line) still fails. The exact byte-identity claim belongs to the
|
|
308
|
+
# release lane, which compares the SHIPPED file to the SIGNED file - both
|
|
309
|
+
# post-signing, so no normalisation is needed or allowed there.
|
|
310
|
+
function Get-NormalisedText([string]$Path) {
|
|
311
|
+
$t = [IO.File]::ReadAllText($Path)
|
|
312
|
+
if ($t.Length -gt 0 -and $t[0] -eq [char]0xFEFF) { $t = $t.Substring(1) }
|
|
313
|
+
return ($t -replace "`r`n", "`n") -replace "`r", "`n"
|
|
314
|
+
}
|
|
315
|
+
foreach ($s in $subjects) {
|
|
316
|
+
$originalNoSig = Join-Path $Work ('orig-' + $s.Name)
|
|
317
|
+
$null = Copy-WithoutSignature -Source $s.Signed -Destination $originalNoSig
|
|
318
|
+
# TrimEnd: the retained prefix stops at the start of the signature block's
|
|
319
|
+
# line, so it carries one newline the original does not. That is the block's
|
|
320
|
+
# separator, not script content.
|
|
321
|
+
$before = (Get-NormalisedText $s.Unsigned).TrimEnd()
|
|
322
|
+
$after = (Get-NormalisedText $originalNoSig).TrimEnd()
|
|
323
|
+
if ($before -ne $after) {
|
|
324
|
+
Fail "$($s.Name): signing changed the script TEXT, not just encoding and the appended block"
|
|
325
|
+
}
|
|
326
|
+
$exact = ((Get-FileHash -Algorithm SHA256 -LiteralPath $s.Unsigned).Hash -eq
|
|
327
|
+
(Get-FileHash -Algorithm SHA256 -LiteralPath $originalNoSig).Hash)
|
|
328
|
+
if (-not $exact) { Say (" note: signer re-encoded " + $s.Name + " (BOM/line endings); text is unchanged") }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# --- every signed input must be pure ASCII --------------------------------
|
|
332
|
+
# These files ship WITHOUT a BOM, so Windows PowerShell 5.1 decodes them with
|
|
333
|
+
# the machine's ANSI codepage. The Authenticode digest is taken over the
|
|
334
|
+
# DECODED text, so one non-ASCII byte makes a signature that verifies on the
|
|
335
|
+
# signing machine and fails on a customer machine in another locale. The
|
|
336
|
+
# existing guard in windowsHiddenSupervisor.test.ts covers string literals
|
|
337
|
+
# only, because that is all that breaks PARSING - comments break SIGNING.
|
|
338
|
+
foreach ($s in $subjects) {
|
|
339
|
+
$bytes = [IO.File]::ReadAllBytes($s.Unsigned)
|
|
340
|
+
if ($bytes | Where-Object { $_ -gt 127 }) {
|
|
341
|
+
Fail "$($s.Name): contains non-ASCII bytes and ships without a BOM - its signature is locale-dependent"
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# --- turn the GPO on ------------------------------------------------------
|
|
346
|
+
if ($hadPolicyKey) {
|
|
347
|
+
$priorPolicy = Get-RegValue $PolicyKey 'ExecutionPolicy'
|
|
348
|
+
$priorEnableScripts = Get-RegValue $PolicyKey 'EnableScripts'
|
|
349
|
+
} else {
|
|
350
|
+
New-Item -Path $PolicyKey -Force | Out-Null
|
|
351
|
+
}
|
|
352
|
+
Set-ItemProperty -Path $PolicyKey -Name 'EnableScripts' -Value 1 -Type DWord
|
|
353
|
+
Set-ItemProperty -Path $PolicyKey -Name 'ExecutionPolicy' -Value 'AllSigned' -Type String
|
|
354
|
+
|
|
355
|
+
$ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
356
|
+
# No embedded double quotes: the native command-line parser strips them and
|
|
357
|
+
# PowerShell then reads a bare `=` as an assignment (InvalidLeftHandSide).
|
|
358
|
+
$listed = & $ps -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command 'Get-ExecutionPolicy -List | Out-String'
|
|
359
|
+
Say ("Scopes seen by a child with -ExecutionPolicy Bypass:`r`n" + ((@($listed) -join [Environment]::NewLine).Trim()))
|
|
360
|
+
$effective = ((& $ps -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command 'Get-ExecutionPolicy') | Out-String).Trim()
|
|
361
|
+
Say "Effective policy inside -ExecutionPolicy Bypass: $effective"
|
|
362
|
+
if ($effective -ne 'AllSigned') {
|
|
363
|
+
Fail "MachinePolicy did not take effect (effective policy is '$effective') - every result below would be meaningless"
|
|
364
|
+
throw 'VACUOUS: the AllSigned machine policy did not apply.'
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
# =========================================================================
|
|
368
|
+
# CONTROL A - unsigned must be BLOCKED
|
|
369
|
+
#
|
|
370
|
+
# The canary is EXECUTED (`powershell -File`), which is unambiguously
|
|
371
|
+
# policy-checked; that result is mandatory. The non-executing Get-Command
|
|
372
|
+
# probe used on the shipped scripts is validated against the same unsigned
|
|
373
|
+
# canary first: if it does not report 'blocked' there, the technique does not
|
|
374
|
+
# observe the gate and the shipped-file assertions fall back to the
|
|
375
|
+
# signature + trusted-publisher decision PowerShell itself makes. Which mode
|
|
376
|
+
# was used is printed, never inferred.
|
|
377
|
+
# =========================================================================
|
|
378
|
+
Say '--- control A: unsigned under MachinePolicy=AllSigned ---'
|
|
379
|
+
$canaryUnsignedPs1 = Join-Path $unsignedDir 'tb-canary.ps1'
|
|
380
|
+
$rc = Test-ScriptExecution $canaryUnsignedPs1
|
|
381
|
+
Say " A canary EXECUTION (unsigned) $rc"
|
|
382
|
+
if ($rc -ne 'blocked') { Fail 'VACUOUS: an unsigned canary EXECUTED under MachinePolicy=AllSigned' }
|
|
383
|
+
|
|
384
|
+
# Validate the non-executing probe against that same unsigned canary before
|
|
385
|
+
# trusting it on the shipped scripts. Measured on windows-latest, the
|
|
386
|
+
# Get-Command/.ScriptBlock route reports 'allowed' for a file the policy really
|
|
387
|
+
# does block - command discovery does NOT run the policy check - so it is not
|
|
388
|
+
# used. The unbindable-parameter route does observe the gate.
|
|
389
|
+
$probeOnCanary = Test-ScriptPolicyByBinding $canaryUnsignedPs1
|
|
390
|
+
$policyProbeUsable = ($probeOnCanary -eq 'blocked')
|
|
391
|
+
Say (" A non-executing probe on the same unsigned canary: $probeOnCanary" +
|
|
392
|
+
$(if ($policyProbeUsable) { ' -> probe IS gated; shipped scripts get the direct assertion' }
|
|
393
|
+
else { ' -> probe is NOT gated; shipped scripts fall back to the signature/trust decision' }))
|
|
394
|
+
|
|
395
|
+
foreach ($s in ($subjects | Where-Object { $_.Kind -eq 'ps1' })) {
|
|
396
|
+
if (-not ($policyProbeUsable -and $s.Probeable)) { continue }
|
|
397
|
+
$r = Test-ScriptPolicyByBinding $s.Unsigned
|
|
398
|
+
Say (" A {0,-30} {1}" -f $s.Name, $r)
|
|
399
|
+
if ($r -ne 'blocked') { Fail "control A did not fire for $($s.Name): $r" }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
# =========================================================================
|
|
403
|
+
# CONTROL B - signed but publisher not trusted must be BLOCKED
|
|
404
|
+
# =========================================================================
|
|
405
|
+
Say '--- control B: signed, publisher NOT yet in TrustedPublisher ---'
|
|
406
|
+
$rc = Test-ScriptExecution (Join-Path $signedDir 'tb-canary.ps1')
|
|
407
|
+
Say " B canary EXECUTION (signed, untrusted publisher) $rc"
|
|
408
|
+
if ($rc -ne 'blocked') { Fail 'control B did not fire: a signature from an UNTRUSTED publisher was executed under AllSigned' }
|
|
409
|
+
|
|
410
|
+
foreach ($s in ($subjects | Where-Object { $_.Kind -eq 'ps1' })) {
|
|
411
|
+
$sig = Get-AuthenticodeSignature -LiteralPath $s.Signed
|
|
412
|
+
if ($sig.Status -ne 'Valid') { Fail "$($s.Name): Authenticode status is $($sig.Status), expected Valid"; continue }
|
|
413
|
+
# Only the production signature can carry a countersignature - the hermetic
|
|
414
|
+
# mode signs with a throwaway cert and no timestamp authority. In production
|
|
415
|
+
# this is not optional: the Trusted Signing leaf is issued for ~72 hours
|
|
416
|
+
# (measured on the live artifact: notBefore Aug 12 20:09:38, notAfter Aug 15
|
|
417
|
+
# 20:09:38), so an untimestamped signature is dead within days.
|
|
418
|
+
if ($AlreadySigned -and -not $sig.TimeStamperCertificate) {
|
|
419
|
+
Fail "$($s.Name): signed but NOT RFC3161 timestamped - the Trusted Signing leaf lives ~3 days, so this stops validating almost immediately"
|
|
420
|
+
}
|
|
421
|
+
if ($policyProbeUsable -and $s.Probeable) {
|
|
422
|
+
$r = Test-ScriptPolicyByBinding $s.Signed
|
|
423
|
+
Say (" B {0,-30} {1}" -f $s.Name, $r)
|
|
424
|
+
if ($r -ne 'blocked') { Fail "control B did not fire for $($s.Name): untrusted publisher was admitted ($r)" }
|
|
425
|
+
} else {
|
|
426
|
+
Say (" B {0,-30} Authenticode={1} timestamped={2}" -f $s.Name, $sig.Status, [bool]$sig.TimeStamperCertificate)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# =========================================================================
|
|
431
|
+
# trust the publisher - PUBLIC certificate, taken from the signed file
|
|
432
|
+
# =========================================================================
|
|
433
|
+
$signerCert = (Get-AuthenticodeSignature -LiteralPath (Join-Path $signedDir 'tb-canary.ps1')).SignerCertificate
|
|
434
|
+
if (-not $signerCert) { throw 'could not read a signer certificate off the signed canary' }
|
|
435
|
+
Say ("Trusting publisher: " + $signerCert.Subject)
|
|
436
|
+
Add-CertToStore -Cert $signerCert -StoreName 'TrustedPublisher'
|
|
437
|
+
$addedThumbprints.Add('TrustedPublisher|' + $signerCert.Thumbprint)
|
|
438
|
+
|
|
439
|
+
# =========================================================================
|
|
440
|
+
# ASSERTION C - the shipped scripts are admitted. This is the deliverable.
|
|
441
|
+
# =========================================================================
|
|
442
|
+
Say '--- assertion C: shipped scripts under MachinePolicy=AllSigned ---'
|
|
443
|
+
$rc = Test-ScriptExecution (Join-Path $signedDir 'tb-canary.ps1')
|
|
444
|
+
Say " C canary EXECUTION (signed, trusted) $rc"
|
|
445
|
+
if ($rc -ne 'allowed') { Fail 'a signed, trusted canary did not execute under AllSigned' }
|
|
446
|
+
|
|
447
|
+
foreach ($s in ($subjects | Where-Object { $_.Kind -eq 'ps1' })) {
|
|
448
|
+
if ($policyProbeUsable -and $s.Probeable) {
|
|
449
|
+
$r = Test-ScriptPolicyByBinding $s.Signed
|
|
450
|
+
Say (" C {0,-30} {1} (policy gate, body not run)" -f $s.Name, $r)
|
|
451
|
+
if ($r -ne 'allowed') { Fail "$($s.Name) is STILL blocked under AllSigned after signing: $r" }
|
|
452
|
+
} else {
|
|
453
|
+
# The two inputs to PowerShell's AllSigned decision, asserted directly.
|
|
454
|
+
$sig = Get-AuthenticodeSignature -LiteralPath $s.Signed
|
|
455
|
+
$trusted = $sig.SignerCertificate -and ($sig.SignerCertificate.Thumbprint -eq $signerCert.Thumbprint)
|
|
456
|
+
Say (" C {0,-30} Valid={1} publisher-trusted={2}" -f $s.Name, ($sig.Status -eq 'Valid'), $trusted)
|
|
457
|
+
if ($sig.Status -ne 'Valid') { Fail "$($s.Name): Authenticode status is $($sig.Status)" }
|
|
458
|
+
if (-not $trusted) { Fail "$($s.Name): signed by a publisher other than the one proved to run under AllSigned" }
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# =========================================================================
|
|
464
|
+
# E - is publisher trust DURABLE, or does it die with the 72-hour leaf?
|
|
465
|
+
#
|
|
466
|
+
# This is the question that decides whether "the admin trusts our publisher
|
|
467
|
+
# once" is a real enterprise story or a weekly chore. The Trusted Signing leaf
|
|
468
|
+
# is reissued about every 72 hours, so if AllSigned matches the LEAF in
|
|
469
|
+
# TrustedPublisher, every release needs a fresh trust deployment. If it accepts
|
|
470
|
+
# an issuer in the chain, one GPO covers every future release.
|
|
471
|
+
#
|
|
472
|
+
# Measured, not asserted, and not a pass/fail: it is a property of Windows, not
|
|
473
|
+
# of our code. Recorded here so the answer is a CI artifact rather than folklore.
|
|
474
|
+
# =========================================================================
|
|
475
|
+
Say '--- E: does trusting the ISSUER (not the 72h leaf) satisfy AllSigned? ---'
|
|
476
|
+
$issuer = $null
|
|
477
|
+
try {
|
|
478
|
+
$chain = New-Object Security.Cryptography.X509Certificates.X509Chain
|
|
479
|
+
$chain.ChainPolicy.RevocationMode = 'NoCheck'
|
|
480
|
+
$null = $chain.Build($signerCert)
|
|
481
|
+
if ($chain.ChainElements.Count -gt 1) { $issuer = $chain.ChainElements[1].Certificate }
|
|
482
|
+
} catch { Say " E could not build the certificate chain: $($_.Exception.Message)" }
|
|
483
|
+
|
|
484
|
+
if (-not $issuer) {
|
|
485
|
+
Say ' E no issuer above the leaf (self-signed harness cert) - not measurable in this mode'
|
|
486
|
+
} else {
|
|
487
|
+
Remove-CertFromStore -Thumbprint $signerCert.Thumbprint -StoreName 'TrustedPublisher'
|
|
488
|
+
Add-CertToStore -Cert $issuer -StoreName 'TrustedPublisher'
|
|
489
|
+
$addedThumbprints.Add('TrustedPublisher|' + $issuer.Thumbprint)
|
|
490
|
+
$rIssuer = Test-ScriptExecution (Join-Path $signedDir 'tb-canary.ps1')
|
|
491
|
+
Say (" E issuer trusted instead of leaf: " + $issuer.Subject)
|
|
492
|
+
Say (" E signed canary EXECUTION with ISSUER trusted: $rIssuer")
|
|
493
|
+
if ($rIssuer -eq 'allowed') {
|
|
494
|
+
Say ' E => publisher trust is DURABLE: one GPO deployment of the issuer covers every future release.'
|
|
495
|
+
} else {
|
|
496
|
+
Say ' E => publisher trust binds the LEAF, which rotates about every 72 hours. A one-time trust'
|
|
497
|
+
Say ' E deployment does NOT survive a release; an AllSigned estate needs a publisher rule that'
|
|
498
|
+
Say ' E matches on subject (AppLocker/WDAC) or a per-release trust push.'
|
|
499
|
+
}
|
|
500
|
+
# Put the leaf back so any later assertion sees the state assertion C proved.
|
|
501
|
+
Remove-CertFromStore -Thumbprint $issuer.Thumbprint -StoreName 'TrustedPublisher'
|
|
502
|
+
Add-CertToStore -Cert $signerCert -StoreName 'TrustedPublisher'
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# =========================================================================
|
|
506
|
+
# CONTROL D - tamper with a signed file, it must go back to BLOCKED
|
|
507
|
+
# =========================================================================
|
|
508
|
+
Say '--- control D: signed then tampered ---'
|
|
509
|
+
# Prepending a comment leaves the script functionally identical and still
|
|
510
|
+
# breaks the digest, so a 'blocked' here can only come from the signature.
|
|
511
|
+
$tampered = Join-Path $Work 'tampered.ps1'
|
|
512
|
+
Copy-Item -LiteralPath (Join-Path $signedDir 'tb-canary.ps1') -Destination $tampered -Force
|
|
513
|
+
$t = [IO.File]::ReadAllText($tampered)
|
|
514
|
+
[IO.File]::WriteAllText($tampered, ("# tampered`r`n" + $t), (New-Object System.Text.UTF8Encoding($false)))
|
|
515
|
+
$r = Test-ScriptExecution $tampered
|
|
516
|
+
Say " D tampered canary EXECUTION $r"
|
|
517
|
+
if ($r -ne 'blocked') { Fail "control D did not fire: a TAMPERED signed script ran under AllSigned ($r)" }
|
|
518
|
+
|
|
519
|
+
# =========================================================================
|
|
520
|
+
# VBScript - a different gate (WSH TrustPolicy), same shape
|
|
521
|
+
# =========================================================================
|
|
522
|
+
if ($VbsPath.Count -gt 0 -or (Test-Path -LiteralPath (Join-Path $signedDir 'tb-canary.vbs'))) {
|
|
523
|
+
Say '--- VBScript: WSH TrustPolicy=2 (run only signed, trusted scripts) ---'
|
|
524
|
+
if (-not (Test-Path $WshKey)) { New-Item -Path $WshKey -Force | Out-Null }
|
|
525
|
+
$priorTrustPolicy = Get-RegValue $WshKey 'TrustPolicy'
|
|
526
|
+
$priorUseWinSafer = Get-RegValue $WshKey 'UseWINSAFER'
|
|
527
|
+
# UseWINSAFER=1 makes WSH defer to software restriction policy and IGNORE
|
|
528
|
+
# TrustPolicy entirely - the exact way this probe could silently measure
|
|
529
|
+
# nothing.
|
|
530
|
+
Set-ItemProperty -Path $WshKey -Name 'UseWINSAFER' -Value 0 -Type DWord
|
|
531
|
+
Set-ItemProperty -Path $WshKey -Name 'TrustPolicy' -Value 2 -Type DWord
|
|
532
|
+
|
|
533
|
+
$rv = Test-VbsExecution (Join-Path $unsignedDir 'tb-canary.vbs')
|
|
534
|
+
Say " A vbs canary EXECUTION (unsigned) $rv"
|
|
535
|
+
if ($rv -ne 'blocked') { Fail 'VACUOUS: an unsigned .vbs executed under WSH TrustPolicy=2' }
|
|
536
|
+
|
|
537
|
+
$rv = Test-VbsExecution (Join-Path $signedDir 'tb-canary.vbs')
|
|
538
|
+
Say " C vbs canary EXECUTION (signed) $rv"
|
|
539
|
+
if ($rv -ne 'allowed') { Fail 'a signed, trusted .vbs did not execute under WSH TrustPolicy=2' }
|
|
540
|
+
|
|
541
|
+
foreach ($s in ($subjects | Where-Object { $_.Kind -eq 'vbs' })) {
|
|
542
|
+
$sig = Get-AuthenticodeSignature -LiteralPath $s.Signed
|
|
543
|
+
Say (" C {0,-30} Authenticode={1}" -f $s.Name, $sig.Status)
|
|
544
|
+
if ($sig.Status -ne 'Valid') { Fail "$($s.Name): Authenticode status is $($sig.Status), expected Valid" }
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
# =========================================================================
|
|
549
|
+
# .cmd - measured, not assumed
|
|
550
|
+
# =========================================================================
|
|
551
|
+
if ($CmdPath.Count -gt 0) {
|
|
552
|
+
Say '--- .cmd: no Authenticode SIP, and no execution policy to block it ---'
|
|
553
|
+
$probeCmd = Join-Path $Work 'tb-canary.cmd'
|
|
554
|
+
[IO.File]::WriteAllText($probeCmd, "@echo off`r`n> %1 echo ran`r`n", (New-Object System.Text.ASCIIEncoding))
|
|
555
|
+
$signable = 'no'
|
|
556
|
+
try {
|
|
557
|
+
$c = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
558
|
+
if ($c) {
|
|
559
|
+
$res = Set-AuthenticodeSignature -FilePath $probeCmd -Certificate $c -ErrorAction Stop
|
|
560
|
+
if ($res.Status -eq 'Valid') { $signable = 'YES' }
|
|
561
|
+
} else { $signable = 'not-attempted(no local cert)' }
|
|
562
|
+
} catch { $signable = 'no (' + $_.Exception.Message.Split([char]10)[0].Trim() + ')' }
|
|
563
|
+
Say " .cmd Authenticode signable: $signable"
|
|
564
|
+
|
|
565
|
+
$rcmd = Test-CmdExecution $probeCmd
|
|
566
|
+
Say " .cmd EXECUTION under MachinePolicy=AllSigned: $rcmd"
|
|
567
|
+
if ($rcmd -ne 'allowed') {
|
|
568
|
+
Fail 'unexpected: a .cmd was blocked under AllSigned - the runbook claim that AllSigned does not gate batch files is wrong'
|
|
569
|
+
}
|
|
570
|
+
foreach ($p in $CmdPath) {
|
|
571
|
+
Say (" .cmd shipped, unsignable by design: " + (Split-Path -Leaf $p))
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
} finally {
|
|
576
|
+
# Restore both policies. Ephemeral CI does not need this; a developer box does.
|
|
577
|
+
try {
|
|
578
|
+
if ($hadPolicyKey) {
|
|
579
|
+
if ($null -ne $priorPolicy) { Set-ItemProperty -Path $PolicyKey -Name 'ExecutionPolicy' -Value $priorPolicy -Type String }
|
|
580
|
+
else { Remove-ItemProperty -Path $PolicyKey -Name 'ExecutionPolicy' -ErrorAction SilentlyContinue }
|
|
581
|
+
if ($null -ne $priorEnableScripts) { Set-ItemProperty -Path $PolicyKey -Name 'EnableScripts' -Value $priorEnableScripts -Type DWord }
|
|
582
|
+
else { Remove-ItemProperty -Path $PolicyKey -Name 'EnableScripts' -ErrorAction SilentlyContinue }
|
|
583
|
+
} else {
|
|
584
|
+
Remove-Item -Path $PolicyKey -Recurse -Force -ErrorAction SilentlyContinue
|
|
585
|
+
}
|
|
586
|
+
if ($null -ne $priorTrustPolicy) { Set-ItemProperty -Path $WshKey -Name 'TrustPolicy' -Value $priorTrustPolicy -Type DWord -ErrorAction SilentlyContinue }
|
|
587
|
+
elseif (Test-Path $WshKey) { Remove-ItemProperty -Path $WshKey -Name 'TrustPolicy' -ErrorAction SilentlyContinue }
|
|
588
|
+
if ($null -ne $priorUseWinSafer) { Set-ItemProperty -Path $WshKey -Name 'UseWINSAFER' -Value $priorUseWinSafer -Type DWord -ErrorAction SilentlyContinue }
|
|
589
|
+
elseif (Test-Path $WshKey) { Remove-ItemProperty -Path $WshKey -Name 'UseWINSAFER' -ErrorAction SilentlyContinue }
|
|
590
|
+
foreach ($entry in $addedThumbprints) {
|
|
591
|
+
$parts = $entry.Split('|')
|
|
592
|
+
Remove-CertFromStore -Thumbprint $parts[1] -StoreName $parts[0]
|
|
593
|
+
}
|
|
594
|
+
} catch {
|
|
595
|
+
Write-Host "[allsigned] cleanup warning: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
596
|
+
}
|
|
597
|
+
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if ($failures.Count -gt 0) {
|
|
601
|
+
Write-Host ''
|
|
602
|
+
Write-Host "[allsigned] $($failures.Count) failure(s):" -ForegroundColor Red
|
|
603
|
+
foreach ($f in $failures) { Write-Host " - $f" -ForegroundColor Red }
|
|
604
|
+
exit 1
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
Say 'PASSED - unsigned is blocked, untrusted-publisher is blocked, tampered is blocked, and every shipped script is admitted.'
|
|
608
|
+
exit 0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<#
|
|
2
|
-
register-litertlm-task.ps1
|
|
2
|
+
register-litertlm-task.ps1 - register the per-user Scheduled Task that runs the
|
|
3
3
|
LiteRT-LM on-device VLM server (Gemma 4 E2B) at logon, with crash-restart.
|
|
4
|
-
Per-user + Interactive logon type
|
|
4
|
+
Per-user + Interactive logon type -> NO admin/UAC (mirrors the macOS
|
|
5
5
|
com.trillboards.litertlm LaunchAgent: KeepAlive=true + ThrottleInterval=30).
|
|
6
6
|
The connector's LiteRtVlmProvider talks to the OpenAI-compatible endpoint on
|
|
7
7
|
127.0.0.1:<Port>. Invoked by setup-litertlm.ps1.
|
|
@@ -14,7 +14,7 @@ $ErrorActionPreference = "Stop"
|
|
|
14
14
|
|
|
15
15
|
$TaskName = "TrillboardsLiteRtLm"
|
|
16
16
|
$UserId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
17
|
-
# litert-lm serve --host 127.0.0.1 --port <port> (loopback only
|
|
17
|
+
# litert-lm serve --host 127.0.0.1 --port <port> (loopback only - the connector
|
|
18
18
|
# is local; never expose the model server off-box).
|
|
19
19
|
$argument = 'serve --host 127.0.0.1 --port ' + $Port
|
|
20
20
|
$Action = New-ScheduledTaskAction -Execute $LitertLmPath -Argument $argument
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<#
|
|
2
|
-
register-task.ps1
|
|
2
|
+
register-task.ps1 - register the per-user Scheduled Task that runs the
|
|
3
3
|
Trillboards local-first connector at logon (with crash-restart). Per-user +
|
|
4
4
|
Interactive logon type so NO admin/UAC is required (mirrors the home-domain,
|
|
5
5
|
no-sudo macOS LaunchAgent). Invoked by the NSIS installer post-copy.
|
|
@@ -25,7 +25,7 @@ $Action = New-ScheduledTaskAction -Execute $wscript -Argument $argument -Working
|
|
|
25
25
|
$Trigger = New-ScheduledTaskTrigger -AtLogOn -User $UserId
|
|
26
26
|
$Settings = New-ScheduledTaskSettingsSet -RestartCount 99 -RestartInterval (New-TimeSpan -Minutes 1) `
|
|
27
27
|
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable `
|
|
28
|
-
-ExecutionTimeLimit ([TimeSpan]::Zero) # no time limit
|
|
28
|
+
-ExecutionTimeLimit ([TimeSpan]::Zero) # no time limit - sensing runs continuously
|
|
29
29
|
$Principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Limited
|
|
30
30
|
|
|
31
31
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
@@ -33,7 +33,7 @@ Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Se
|
|
|
33
33
|
-Principal $Principal -Description "Trillboards Edge - CCTV $Mode connector" | Out-Null
|
|
34
34
|
|
|
35
35
|
# VERIFY the registration landed instead of trusting Register-ScheduledTask's
|
|
36
|
-
# silence
|
|
36
|
+
# silence - a silently-missing task is exactly how a real 16-camera install
|
|
37
37
|
# ended up paired with zero frames (the pairing helper then claimed success).
|
|
38
38
|
# Callers (installer.nsi, trillboards-pair.cmd's self-repair) branch on our
|
|
39
39
|
# exit code, so fail LOUDLY here.
|
|
@@ -15,7 +15,7 @@ $rootPrefix = $root + '\'
|
|
|
15
15
|
# DISABLE BEFORE STOP. register-task.ps1 registers the task with
|
|
16
16
|
# `-RestartCount 99 -RestartInterval 1 minute`, so Stop-ScheduledTask ends the
|
|
17
17
|
# instance as a FAILURE and Task Scheduler relaunches node.exe about a minute
|
|
18
|
-
# later
|
|
18
|
+
# later - right inside the installer's bin\ + lib\ move. That is the "reinstall
|
|
19
19
|
# hit a locked node.exe" the field test saw. Disabling suppresses the restart
|
|
20
20
|
# AND the AtLogOn trigger for the whole swap window; install-update.ps1 owns
|
|
21
21
|
# re-enabling it on every exit path, and the uninstaller deletes the task.
|
|
@@ -25,7 +25,7 @@ Start-Sleep -Milliseconds 750
|
|
|
25
25
|
|
|
26
26
|
function Get-TrillboardsEdgeProcesses {
|
|
27
27
|
# The process that actually holds bin\node.exe open is node.exe itself, and
|
|
28
|
-
# its command line names NONE of these scripts
|
|
28
|
+
# its command line names NONE of these scripts - it names the cli.js entry.
|
|
29
29
|
# Win32_Process.ExecutablePath is empty across a 32/64-bit boundary, and then
|
|
30
30
|
# the marker list is the only matcher left, so it has to cover the runtime.
|
|
31
31
|
$markers = @(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Option Explicit
|
|
2
2
|
|
|
3
|
-
' trillboards-hidden-run.vbs
|
|
3
|
+
' trillboards-hidden-run.vbs - run a command with NO console window, ever.
|
|
4
4
|
'
|
|
5
5
|
' cmd's `start "" prog.exe` allocates a NEW console (conhost) for the child
|
|
6
6
|
' BEFORE the child can hide itself, so `powershell.exe -WindowStyle Hidden`
|
|
7
|
-
' still flashes
|
|
7
|
+
' still flashes - and on a slow logon it is a black window the customer sees
|
|
8
8
|
' and closes. wscript.exe is a GUI-subsystem host: WScript.Shell.Run with
|
|
9
9
|
' window style 0 (SW_HIDE) creates the process with a hidden console from the
|
|
10
10
|
' very first instruction. This is the same mechanism trillboards-edge-task.vbs
|
|
@@ -19,7 +19,7 @@ Option Explicit
|
|
|
19
19
|
Dim shell, index, command, argument
|
|
20
20
|
|
|
21
21
|
If WScript.Arguments.Count < 1 Then
|
|
22
|
-
' Nothing to run. Exit quietly
|
|
22
|
+
' Nothing to run. Exit quietly - a hidden helper must never pop a dialog.
|
|
23
23
|
WScript.Quit 2
|
|
24
24
|
End If
|
|
25
25
|
|
|
@@ -290,8 +290,8 @@ function Stop-SetupProcess {
|
|
|
290
290
|
function Resume-BackgroundSensing {
|
|
291
291
|
# The wizard is the ONE window a customer can see, and closing it
|
|
292
292
|
# taskkill /T /F's the whole pairing tree. If that happens between "config
|
|
293
|
-
# written" and "scheduled task started"
|
|
294
|
-
# stop-existing.ps1 has the task disabled
|
|
293
|
+
# written" and "scheduled task started" - or during an upgrade, while
|
|
294
|
+
# stop-existing.ps1 has the task disabled - sensing would stay down with
|
|
295
295
|
# nothing to retry it. Closing a window must never be what stops sensing.
|
|
296
296
|
$installDir = Split-Path -Parent $BinDir
|
|
297
297
|
$ensure = Join-Path $BinDir 'ensure-sensing.ps1'
|
package/dist/EdgeAgent.js
CHANGED
|
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
|
|
|
44
44
|
const CommandRouter_1 = require("./CommandRouter");
|
|
45
45
|
const config_1 = require("./config");
|
|
46
46
|
const dataDir_1 = require("./dataDir");
|
|
47
|
-
const SDK_VERSION = '0.2.
|
|
47
|
+
const SDK_VERSION = '0.2.17';
|
|
48
48
|
class EdgeAgent extends events_1.EventEmitter {
|
|
49
49
|
constructor(config, deps) {
|
|
50
50
|
super();
|
package/dist/diagnose.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface DiagnosticReport {
|
|
|
21
21
|
warnings: number;
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
declare const SDK_VERSION = "0.2.
|
|
24
|
+
declare const SDK_VERSION = "0.2.17";
|
|
25
25
|
declare function checkNodeVersion(): CheckResult;
|
|
26
26
|
declare function checkPlatform(): CheckResult;
|
|
27
27
|
declare function checkMemory(): CheckResult;
|
package/dist/diagnose.js
CHANGED
|
@@ -59,7 +59,7 @@ const modelIntegrity_1 = require("./modelIntegrity");
|
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
// SDK version — mirrors package.json; kept in sync by CI
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
|
-
const SDK_VERSION = '0.2.
|
|
62
|
+
const SDK_VERSION = '0.2.17';
|
|
63
63
|
exports._SDK_VERSION = SDK_VERSION;
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
65
|
// ANSI helpers (no external deps)
|