@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.
@@ -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) that installs + runs on
41
- # every Mac. This is the RELEASE artifact: a browser can't reliably detect a
42
- # Mac's CPU arch (Safari reports "MacIntel" even on Apple Silicon), so the portal
43
- # cannot hand out an arch-specific download. --arch builds a single-arch .pkg
44
- # (faster, for local testing).
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 = "https://github.com/eugeneware/ffmpeg-static/releases/download/$FfmpegStaticTag/ffmpeg-win32-x64"
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 — register the per-user Scheduled Task that runs the
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 → NO admin/UAC (mirrors the macOS
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 — the connector
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 — register the per-user Scheduled Task that runs the
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 — sensing runs continuously
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 — a silently-missing task is exactly how a real 16-camera install
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 — right inside the installer's bin\ + lib\ move. That is the "reinstall
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 — it names the cli.js entry.
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 — run a command with NO console window, ever.
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 — and on a slow logon it is a black window the customer sees
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 — a hidden helper must never pop a dialog.
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" — or during an upgrade, while
294
- # stop-existing.ps1 has the task disabled — sensing would stay down with
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.16';
47
+ const SDK_VERSION = '0.2.17';
48
48
  class EdgeAgent extends events_1.EventEmitter {
49
49
  constructor(config, deps) {
50
50
  super();
@@ -21,7 +21,7 @@ export interface DiagnosticReport {
21
21
  warnings: number;
22
22
  };
23
23
  }
24
- declare const SDK_VERSION = "0.2.16";
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.16';
62
+ const SDK_VERSION = '0.2.17';
63
63
  exports._SDK_VERSION = SDK_VERSION;
64
64
  // ---------------------------------------------------------------------------
65
65
  // ANSI helpers (no external deps)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trillboards/edge-sdk",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "description": "Composable cross-platform Edge AI SDK for audience sensing on DOOH/CTV devices",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",