@trillboards/edge-sdk 0.2.16 → 0.2.18
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/macos/scripts/setup-litertlm.sh +35 -10
- package/deploy/windows/README.md +17 -4
- 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/e2e/test-headless-principal.ps1 +632 -0
- package/deploy/windows/ensure-sensing.ps1 +91 -1
- package/deploy/windows/register-litertlm-task.ps1 +114 -15
- package/deploy/windows/register-task.ps1 +162 -12
- package/deploy/windows/setup-litertlm.ps1 +78 -32
- 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/CloudSensingManager.d.ts +13 -4
- package/dist/CloudSensingManager.d.ts.map +1 -1
- package/dist/CloudSensingManager.js +6 -0
- package/dist/CloudSensingManager.js.map +1 -1
- package/dist/EdgeAgent.js +1 -1
- package/dist/LiteRtVlmProvider.d.ts +108 -45
- package/dist/LiteRtVlmProvider.d.ts.map +1 -1
- package/dist/LiteRtVlmProvider.js +124 -84
- package/dist/LiteRtVlmProvider.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/localSenseE2eHarness.d.ts +17 -4
- package/dist/localSenseE2eHarness.d.ts.map +1 -1
- package/dist/localSenseE2eHarness.js +11 -1
- package/dist/localSenseE2eHarness.js.map +1 -1
- package/dist/sensingRunMode.d.ts.map +1 -1
- package/dist/sensingRunMode.js +49 -31
- package/dist/sensingRunMode.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|