@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,632 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
test-headless-principal.ps1 -- proves the Trillboards connector's scheduled
|
|
4
|
+
task runs with NO interactive session, which is what makes it survive an
|
|
5
|
+
unattended reboot.
|
|
6
|
+
|
|
7
|
+
.DESCRIPTION
|
|
8
|
+
The defect this guards: register-task.ps1 used to register `-AtLogOn` +
|
|
9
|
+
`-LogonType Interactive`, so sensing only ran while one specific user was
|
|
10
|
+
interactively logged on. After a Windows Update reboot on a store back-office
|
|
11
|
+
PC the connector stayed down until somebody signed in -- days, silently, with
|
|
12
|
+
the machine up and no error surfaced anywhere.
|
|
13
|
+
|
|
14
|
+
EVERY assertion here carries a CONTROL that must fail. That is not ceremony:
|
|
15
|
+
a sibling probe for the "no visible window" property once reported 0 windows
|
|
16
|
+
for the fix AND for a deliberately broken control, because a hosted runner has
|
|
17
|
+
no desktop for EnumWindows to walk. It passed on hardware incapable of
|
|
18
|
+
observing the property. So each arm below states what makes it VACUOUS, and
|
|
19
|
+
fails loudly instead of passing when the control does not fire.
|
|
20
|
+
|
|
21
|
+
WHICH ARM COVERS THE PRODUCT'S OWN PATH. Arm B, and only Arm B: it runs the
|
|
22
|
+
real register-task.ps1 as the account that owns the install, unelevated, which
|
|
23
|
+
is exactly what installer.nsi (RequestExecutionLevel user) does. Arm C is a
|
|
24
|
+
SYNTHETIC differential over the principal, so its registration context is not
|
|
25
|
+
part of what it measures -- whether a task can run without a session is decided
|
|
26
|
+
by its principal, not by who wrote it.
|
|
27
|
+
|
|
28
|
+
WHO CAN CREATE AN S4U TASK -- the full rule, measured over three runs, because
|
|
29
|
+
two intermediate readings of it were wrong:
|
|
30
|
+
|
|
31
|
+
as the account ITSELF, ELEVATED works (Arm A, run 31823057985)
|
|
32
|
+
as the account itself, UNELEVATED refused (Arm B, run 31836760844)
|
|
33
|
+
by an elevated admin FOR ANOTHER account refused (run 31838558271), and
|
|
34
|
+
refused even when that account is
|
|
35
|
+
itself an Administrators member
|
|
36
|
+
|
|
37
|
+
So it is BOTH: the caller must be elevated AND must be the account itself. The
|
|
38
|
+
cross-user direction needs SeTcbPrivilege, which an elevated admin does not
|
|
39
|
+
hold, and `Start-Process -Credential` cannot help because it yields a FILTERED
|
|
40
|
+
token. That is why Arm C reaches its subject through a Password-logon task
|
|
41
|
+
with RunLevel Highest -- the only vehicle that produces an elevated,
|
|
42
|
+
session-less process running AS the subject, which can then register S4U for
|
|
43
|
+
itself the same way the product does.
|
|
44
|
+
|
|
45
|
+
Arm A -- SHAPE (uses the real register-task.ps1).
|
|
46
|
+
Asserts the registered task XML carries a LOGON-FREE trigger (<BootTrigger>
|
|
47
|
+
or <Repetition>) and a non-interactive <LogonType>. Control: the pre-fix
|
|
48
|
+
registration (AtLogOn + InteractiveToken) is built inline and the SAME
|
|
49
|
+
predicate must REJECT it.
|
|
50
|
+
|
|
51
|
+
Arm B -- THE UNELEVATED FLOOR (real register-task.ps1, standard NON-admin).
|
|
52
|
+
Asserts what a standard user actually gets. Headless costs ONE elevation on
|
|
53
|
+
Windows: S4U performs a batch logon and Task Scheduler grants
|
|
54
|
+
SeBatchLogonRight only to an administrator CALLER, so a standard user is
|
|
55
|
+
refused S4U even for itself. This arm pins the floor -- the ladder still
|
|
56
|
+
lands, the repeating trigger survives, and it completes PROMPTLESSLY -- and
|
|
57
|
+
asserts the platform rule itself, so a future Windows that allowed
|
|
58
|
+
unelevated S4U would turn this arm red rather than leave a dead rung.
|
|
59
|
+
|
|
60
|
+
Arm C -- RUNTIME, and this is the load-bearing one.
|
|
61
|
+
A second local account, in Administrators and NEVER logged on, registers the
|
|
62
|
+
same marker action twice FOR ITSELF -- once with the pre-fix Interactive
|
|
63
|
+
principal, once with S4U -- and both are started. The principal is the ONLY
|
|
64
|
+
variable. The subject is an elevated admin because that is the only account
|
|
65
|
+
Windows lets create an S4U task, and it is the state a real customer is in
|
|
66
|
+
after accepting the single UAC prompt.
|
|
67
|
+
The Interactive arm must NOT produce its marker; the S4U arm must.
|
|
68
|
+
VACUOUS if the bootstrap never runs, if it is not elevated, if the
|
|
69
|
+
Interactive control also runs, or if the control could not register at all
|
|
70
|
+
(then "it did not run" would say nothing about logon type).
|
|
71
|
+
|
|
72
|
+
Arm D -- THE TASK CAN FIND ITS OWN STATE.
|
|
73
|
+
The S4U marker records the identity, profile root and session id the task
|
|
74
|
+
actually ran with, plus a real outbound HTTPS attempt. A headless task that
|
|
75
|
+
resolved %USERPROFILE% to systemprofile would boot, find no device token and
|
|
76
|
+
no Gemma model, and fail while looking alive -- worse than the outage. This
|
|
77
|
+
arm is why the principal is S4U and not SYSTEM.
|
|
78
|
+
|
|
79
|
+
Arm F -- NO PROMPT A USER CANNOT MEET. ensure-sensing.ps1 owns the single UAC
|
|
80
|
+
prompt, and must not show it to a standard user: UAC would demand credentials
|
|
81
|
+
they cannot supply and -Wait would hang the wizard-close path.
|
|
82
|
+
|
|
83
|
+
Arm E -- MIGRATION. Existing installs carry a logon-only task and there is no
|
|
84
|
+
auto-updater, so ensure-sensing.ps1 repairs the shape in place. Starts from a
|
|
85
|
+
genuinely pre-fix registration and asserts it becomes headless, then asserts
|
|
86
|
+
the repair is idempotent (a second run must not re-register).
|
|
87
|
+
|
|
88
|
+
WHAT THIS CANNOT PROVE. The UAC prompt itself. A hosted runner has no one to
|
|
89
|
+
click it, so the admin-capable-but-unelevated path -- the population for whom
|
|
90
|
+
one click turns 'Interactive + repeat' into 'S4U + boot + repeat' -- is
|
|
91
|
+
exercised only at its two ENDS: Arm A proves an already-elevated caller gets
|
|
92
|
+
the headless shape, and Arm F proves a standard user is never asked. The
|
|
93
|
+
middle, `Start-Process -Verb RunAs` actually being accepted, is unobserved.
|
|
94
|
+
|
|
95
|
+
A hosted runner cannot reboot mid-job either, so Task
|
|
96
|
+
Scheduler actually firing <BootTrigger> or the <Repetition> at boot is
|
|
97
|
+
asserted STRUCTURALLY (the element is in the registered definition), never
|
|
98
|
+
observed. Nor is the repetition INTERVAL waited out -- a 5-minute wait per run
|
|
99
|
+
would buy nothing the structural assertion does not already give. What IS
|
|
100
|
+
observed at runtime is the property that made every trigger useless before:
|
|
101
|
+
the task running to completion with its owning user not logged on.
|
|
102
|
+
|
|
103
|
+
.OUTPUTS exit 0 = passed; exit 1 = failed (including "failed because vacuous").
|
|
104
|
+
#>
|
|
105
|
+
[CmdletBinding()]
|
|
106
|
+
param(
|
|
107
|
+
# Skip the arms that need to create a local account (B/C/D). Off by default;
|
|
108
|
+
# they are the point of this file.
|
|
109
|
+
[switch]$SkipLocalAccountArms
|
|
110
|
+
)
|
|
111
|
+
$ErrorActionPreference = 'Stop'
|
|
112
|
+
|
|
113
|
+
$Here = Split-Path -Parent $MyInvocation.MyCommand.Path # deploy\windows\e2e
|
|
114
|
+
$Deploy = (Resolve-Path (Join-Path $Here '..')).Path # deploy\windows
|
|
115
|
+
|
|
116
|
+
function Log ($m) { Write-Host "[headless-principal] $m" }
|
|
117
|
+
function Pass ($m) { Write-Host "PASS - $m" }
|
|
118
|
+
function Die ($m) { Write-Host "FAIL - $m"; exit 1 }
|
|
119
|
+
|
|
120
|
+
$TempRoot = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP }
|
|
121
|
+
$ProbeRoot = Join-Path $TempRoot 'tb-headless-probe'
|
|
122
|
+
$MarkerDir = Join-Path $ProbeRoot 'markers'
|
|
123
|
+
$RealTask = 'TrillboardsEdge'
|
|
124
|
+
$ControlTask = 'TbHeadlessProbeControl'
|
|
125
|
+
$FixTask = 'TbHeadlessProbeFix'
|
|
126
|
+
$ProbeUser = 'tbheadless' # standard user -> Arm B/F (the unelevated floor)
|
|
127
|
+
$AdminUser = 'tbheadlessadm' # ADMIN, never logged on -> Arm C/D (post-elevation)
|
|
128
|
+
$ProbePass = 'Pr0be!Headless#2026'
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------- the predicate
|
|
131
|
+
# One definition of "headless", used by BOTH the fix and the control. If this
|
|
132
|
+
# ever gets weakened, the control stops failing and the arm reports vacuous.
|
|
133
|
+
# "Headless" is TWO independent properties and both are required:
|
|
134
|
+
# 1. a trigger that fires with nobody logged on, and
|
|
135
|
+
# 2. a principal that does not need an interactive session.
|
|
136
|
+
# Boot and repetition both satisfy (1). They are not interchangeable in latency
|
|
137
|
+
# -- boot is ~30s, repetition is up to one interval -- but they are
|
|
138
|
+
# interchangeable in the property under test, and only ONE of them is available
|
|
139
|
+
# to a non-admin: Microsoft restricts boot triggers to the Administrators group,
|
|
140
|
+
# and installer.nsi:33 is RequestExecutionLevel user.
|
|
141
|
+
function Test-HeadlessDefinition([string]$Xml) {
|
|
142
|
+
$hasBootTrigger = $Xml -match '<BootTrigger>'
|
|
143
|
+
$hasRepetition = $Xml -match '<Repetition>'
|
|
144
|
+
$logonType = if ($Xml -match '<LogonType>([^<]+)</LogonType>') { $Matches[1] } else { '' }
|
|
145
|
+
# InteractiveToken is the one logon type that REQUIRES a live interactive
|
|
146
|
+
# session. An ABSENT <LogonType> is treated as not-headless on purpose: it is
|
|
147
|
+
# the shape a service-account principal exports, and this product must not run
|
|
148
|
+
# as one (see Arm D -- SYSTEM re-roots every homedir-derived path). Fail closed.
|
|
149
|
+
$nonInteractive = ($logonType -ne '') -and ($logonType -ne 'InteractiveToken')
|
|
150
|
+
[pscustomobject]@{
|
|
151
|
+
Headless = (($hasBootTrigger -or $hasRepetition) -and $nonInteractive)
|
|
152
|
+
BootTrigger = $hasBootTrigger
|
|
153
|
+
Repetition = $hasRepetition
|
|
154
|
+
LogonType = $logonType
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function Remove-ProbeTask([string]$Name) {
|
|
159
|
+
Unregister-ScheduledTask -TaskName $Name -Confirm:$false -ErrorAction SilentlyContinue
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function Get-TaskShape([string]$Name) {
|
|
163
|
+
Test-HeadlessDefinition (Export-ScheduledTask -TaskName $Name)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Register the PRE-FIX shape (AtLogOn + Interactive) for the CURRENT user. This
|
|
167
|
+
# is the control, and it is also how Arm E manufactures a genuine legacy install.
|
|
168
|
+
function Register-PreFixTask([string]$Name, $Action) {
|
|
169
|
+
$me = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
170
|
+
$t = New-ScheduledTaskTrigger -AtLogOn -User $me
|
|
171
|
+
$p = New-ScheduledTaskPrincipal -UserId $me -LogonType Interactive -RunLevel Limited
|
|
172
|
+
Remove-ProbeTask $Name
|
|
173
|
+
Register-ScheduledTask -TaskName $Name -Action $Action -Trigger $t -Principal $p | Out-Null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
if (Test-Path $ProbeRoot) { Remove-Item -Recurse -Force $ProbeRoot -ErrorAction SilentlyContinue }
|
|
178
|
+
New-Item -ItemType Directory -Force -Path $MarkerDir | Out-Null
|
|
179
|
+
|
|
180
|
+
# =========================================================== ARM A -- SHAPE
|
|
181
|
+
Log 'Arm A: registered-task shape, via the real register-task.ps1'
|
|
182
|
+
|
|
183
|
+
$installDir = Join-Path $ProbeRoot 'install'
|
|
184
|
+
$bin = Join-Path $installDir 'bin'
|
|
185
|
+
New-Item -ItemType Directory -Force -Path $bin, (Join-Path $installDir 'logs') | Out-Null
|
|
186
|
+
foreach ($f in @('trillboards-edge-task.vbs', 'run-connector.ps1', 'register-task.ps1', 'ensure-sensing.ps1', 'trillboards-edge.cmd', 'trillboards-hidden-run.vbs')) {
|
|
187
|
+
Copy-Item -Force (Join-Path $Deploy $f) (Join-Path $bin $f)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
Remove-ProbeTask $RealTask
|
|
191
|
+
& (Join-Path $bin 'register-task.ps1') -InstallDir $installDir | Out-Null
|
|
192
|
+
$fixShape = Get-TaskShape $RealTask
|
|
193
|
+
Log (" fix -> BootTrigger=$($fixShape.BootTrigger) Repetition=$($fixShape.Repetition) LogonType=$($fixShape.LogonType)")
|
|
194
|
+
|
|
195
|
+
$noopAction = New-ScheduledTaskAction -Execute (Join-Path $env:WINDIR 'System32\wscript.exe') -Argument 'noop'
|
|
196
|
+
Register-PreFixTask $ControlTask $noopAction
|
|
197
|
+
$ctlShape = Get-TaskShape $ControlTask
|
|
198
|
+
Log (" control -> BootTrigger=$($ctlShape.BootTrigger) Repetition=$($ctlShape.Repetition) LogonType=$($ctlShape.LogonType)")
|
|
199
|
+
|
|
200
|
+
if ($ctlShape.Headless) {
|
|
201
|
+
Die "Arm A is VACUOUS: the pre-fix shape (AtLogOn + Interactive) satisfied the headless predicate. The predicate is broken, not the fix."
|
|
202
|
+
}
|
|
203
|
+
if (-not $fixShape.Headless) {
|
|
204
|
+
Die "register-task.ps1 did NOT register a headless task (BootTrigger=$($fixShape.BootTrigger), Repetition=$($fixShape.Repetition), LogonType='$($fixShape.LogonType)')."
|
|
205
|
+
}
|
|
206
|
+
$state = (Get-ScheduledTask -TaskName $RealTask).State
|
|
207
|
+
if ("$state" -eq 'Disabled') { Die "TrillboardsEdge registered Disabled -- it would never fire at boot." }
|
|
208
|
+
Pass "shape: BootTrigger present, LogonType=$($fixShape.LogonType), state=$state; pre-fix control correctly rejected"
|
|
209
|
+
Remove-ProbeTask $ControlTask
|
|
210
|
+
|
|
211
|
+
# ======================================================== ARM E -- MIGRATION
|
|
212
|
+
# Before touching accounts, because it only needs the current user.
|
|
213
|
+
Log 'Arm E: ensure-sensing.ps1 repairs a legacy logon-only registration'
|
|
214
|
+
Set-Content -LiteralPath (Join-Path $installDir 'trillboards.config.json') -Encoding ASCII -Value '{"cameras":[]}'
|
|
215
|
+
Remove-ProbeTask $RealTask
|
|
216
|
+
Register-PreFixTask $RealTask $noopAction
|
|
217
|
+
$beforeShape = Get-TaskShape $RealTask
|
|
218
|
+
Log (" before repair -> BootTrigger=$($beforeShape.BootTrigger) Repetition=$($beforeShape.Repetition) LogonType=$($beforeShape.LogonType)")
|
|
219
|
+
if ($beforeShape.Headless) {
|
|
220
|
+
Die 'Arm E is VACUOUS: the manufactured legacy task was already headless, so the repair had nothing to do.'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
$global:LASTEXITCODE = 0
|
|
224
|
+
& (Join-Path $bin 'ensure-sensing.ps1') -InstallDir $installDir
|
|
225
|
+
if ($LASTEXITCODE -ne 0) { Die "ensure-sensing.ps1 exited $LASTEXITCODE while repairing a legacy task." }
|
|
226
|
+
$afterShape = Get-TaskShape $RealTask
|
|
227
|
+
Log (" after repair -> BootTrigger=$($afterShape.BootTrigger) Repetition=$($afterShape.Repetition) LogonType=$($afterShape.LogonType)")
|
|
228
|
+
if (-not $afterShape.Headless) {
|
|
229
|
+
Die "ensure-sensing.ps1 did NOT migrate the legacy task (BootTrigger=$($afterShape.BootTrigger), Repetition=$($afterShape.Repetition), LogonType='$($afterShape.LogonType)'). Existing installs would stay logon-only forever -- there is no auto-updater."
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Idempotence: a healthy install must not be re-registered on every wizard
|
|
233
|
+
# close. Compare the WHOLE exported definition -- a re-registration restamps
|
|
234
|
+
# <RegistrationInfo><Date>, and starting a task does not change its XML (run
|
|
235
|
+
# times live in ScheduledTaskInfo, not the definition). Deliberately NOT an
|
|
236
|
+
# [xml] cast: Export-ScheduledTask returns a string declaring encoding="UTF-16"
|
|
237
|
+
# with no byte-order mark, and casting that throws "There is no Unicode byte
|
|
238
|
+
# order mark. Cannot switch to Unicode."
|
|
239
|
+
$definitionBefore = Export-ScheduledTask -TaskName $RealTask
|
|
240
|
+
$global:LASTEXITCODE = 0
|
|
241
|
+
& (Join-Path $bin 'ensure-sensing.ps1') -InstallDir $installDir
|
|
242
|
+
if ($LASTEXITCODE -ne 0) { Die "ensure-sensing.ps1 exited $LASTEXITCODE on an already-healthy task." }
|
|
243
|
+
$definitionAfter = Export-ScheduledTask -TaskName $RealTask
|
|
244
|
+
if ($definitionBefore -ne $definitionAfter) {
|
|
245
|
+
$d1 = if ($definitionBefore -match '<Date>([^<]+)</Date>') { $Matches[1] } else { 'unknown' }
|
|
246
|
+
$d2 = if ($definitionAfter -match '<Date>([^<]+)</Date>') { $Matches[1] } else { 'unknown' }
|
|
247
|
+
Die "ensure-sensing.ps1 re-registered an ALREADY-headless task ($d1 -> $d2). That churns a healthy install and interrupts sensing on every wizard close."
|
|
248
|
+
}
|
|
249
|
+
Pass "migration: logon-only -> headless in place, and the repair is idempotent"
|
|
250
|
+
Remove-ProbeTask $RealTask
|
|
251
|
+
|
|
252
|
+
if ($SkipLocalAccountArms) {
|
|
253
|
+
Log 'Arm B/C/D skipped by request.'
|
|
254
|
+
exit 0
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# ------------------------------------------------ a user who is NOT logged on
|
|
258
|
+
Log "Arm B/C/D: creating standard (non-admin) local user '$ProbeUser'"
|
|
259
|
+
$secure = ConvertTo-SecureString $ProbePass -AsPlainText -Force
|
|
260
|
+
# -Admin puts the account in Administrators, which is how it comes to hold
|
|
261
|
+
# SeBatchLogonRight -- the right S4U needs and the one a standard user cannot
|
|
262
|
+
# be granted without an elevated caller.
|
|
263
|
+
function New-ProbeAccount([string]$Name, [switch]$Admin) {
|
|
264
|
+
Get-LocalUser -Name $Name -ErrorAction SilentlyContinue | Remove-LocalUser -ErrorAction SilentlyContinue
|
|
265
|
+
New-LocalUser -Name $Name -Password $secure -AccountNeverExpires -PasswordNeverExpires `
|
|
266
|
+
-UserMayNotChangePassword -Description 'Trillboards headless-principal probe' | Out-Null
|
|
267
|
+
$groupSid = if ($Admin) { 'S-1-5-32-544' } else { 'S-1-5-32-545' }
|
|
268
|
+
Add-LocalGroupMember -Group (Get-LocalGroup -SID $groupSid).Name -Member $Name -ErrorAction SilentlyContinue
|
|
269
|
+
}
|
|
270
|
+
# Members of Users ONLY. Never Administrators -- Arm B would be meaningless.
|
|
271
|
+
New-ProbeAccount -Name $ProbeUser
|
|
272
|
+
|
|
273
|
+
# Both probe accounts must READ the scripts and WRITE the markers. Everyone
|
|
274
|
+
# (S-1-1-0) rather than Users, because the Arm C/D account is an admin and is
|
|
275
|
+
# not necessarily in Users.
|
|
276
|
+
& icacls.exe $ProbeRoot /grant '*S-1-1-0:(OI)(CI)RX' /T | Out-Null
|
|
277
|
+
& icacls.exe $MarkerDir /grant '*S-1-1-0:(OI)(CI)M' /T | Out-Null
|
|
278
|
+
|
|
279
|
+
$cred = New-Object System.Management.Automation.PSCredential($ProbeUser, $secure)
|
|
280
|
+
$psExe = Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
281
|
+
|
|
282
|
+
# Run something AS the probe user and return the exit code. Everything the
|
|
283
|
+
# product does, it does as this user.
|
|
284
|
+
function Invoke-AsProbeUser([string[]]$ScriptArgs, [string]$Tag) {
|
|
285
|
+
$out = Join-Path $ProbeRoot "$Tag-stdout.txt"
|
|
286
|
+
$err = Join-Path $ProbeRoot "$Tag-stderr.txt"
|
|
287
|
+
$p = Start-Process -FilePath $psExe -Credential $cred -LoadUserProfile -WorkingDirectory $bin `
|
|
288
|
+
-ArgumentList (@('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File') + $ScriptArgs) `
|
|
289
|
+
-RedirectStandardOutput $out -RedirectStandardError $err -PassThru -Wait
|
|
290
|
+
foreach ($pair in @(@('stdout', $out), @('stderr', $err))) {
|
|
291
|
+
if (Test-Path $pair[1]) {
|
|
292
|
+
$text = (Get-Content -Raw $pair[1])
|
|
293
|
+
if ($text -and $text.Trim()) { Log " [$Tag $($pair[0])] $($text.Trim())" }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return $p.ExitCode
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# Materialise the profile ONCE so %USERPROFILE% under the task is a real
|
|
300
|
+
# profile. On a customer machine this is always true: the installing user has
|
|
301
|
+
# logged on. Without it Arm D would be measuring first-logon profile creation.
|
|
302
|
+
Log ' materialising the probe user profile'
|
|
303
|
+
Start-Process -FilePath $env:ComSpec -ArgumentList '/c', 'exit' -Credential $cred -LoadUserProfile `
|
|
304
|
+
-WorkingDirectory 'C:\' -Wait -ErrorAction Stop
|
|
305
|
+
$probeProfile = (Get-CimInstance Win32_UserProfile -ErrorAction SilentlyContinue |
|
|
306
|
+
Where-Object { $_.LocalPath -like "*\$ProbeUser" } | Select-Object -First 1).LocalPath
|
|
307
|
+
Log " probe user profile: $probeProfile"
|
|
308
|
+
|
|
309
|
+
# Prove the premise of the whole differential: this user has NO session.
|
|
310
|
+
# `query user` exits 1 and writes to stderr when there are no sessions at all,
|
|
311
|
+
# which under $ErrorActionPreference='Stop' becomes a NativeCommandError. That
|
|
312
|
+
# case is the EXPECTED one here, so it must not abort the probe.
|
|
313
|
+
$sessions = ''
|
|
314
|
+
try { $sessions = (& query.exe user 2>&1 | Out-String) }
|
|
315
|
+
catch { $sessions = "query user reported no sessions: $($_.Exception.Message)" }
|
|
316
|
+
Log " query user ->`n$sessions"
|
|
317
|
+
if ($sessions -match [regex]::Escape($ProbeUser)) {
|
|
318
|
+
Die "Arm C premise broken: '$ProbeUser' HAS an interactive session, so the Interactive control could legitimately run."
|
|
319
|
+
}
|
|
320
|
+
Pass "premise: '$ProbeUser' has no interactive session"
|
|
321
|
+
|
|
322
|
+
# ============================================== ARM B -- THE UNELEVATED FLOOR
|
|
323
|
+
# This arm asserts what a standard user actually gets, and it is NOT headless.
|
|
324
|
+
# Measured here, run 31836760844: S4U is refused to a standard user registering
|
|
325
|
+
# for ITSELF ("Access is denied") because S4U performs a BATCH logon and Task
|
|
326
|
+
# Scheduler grants SeBatchLogonRight only when the CALLER is an administrator.
|
|
327
|
+
# Boot triggers are Administrators-only for the same reason. So headless
|
|
328
|
+
# operation costs exactly one elevation on Windows -- that is a platform rule,
|
|
329
|
+
# not something the ladder can code around, and ensure-sensing.ps1 is where the
|
|
330
|
+
# single UAC prompt lives.
|
|
331
|
+
#
|
|
332
|
+
# What MUST hold unelevated: the ladder still lands, the task still exists, it
|
|
333
|
+
# carries the repeating trigger (strictly better than the pre-fix logon-only
|
|
334
|
+
# shape -- it revives a connector that died while somebody was logged on), and
|
|
335
|
+
# the script neither hangs nor throws. A standard user must never be shown a
|
|
336
|
+
# UAC credential prompt they cannot satisfy, so this must also complete
|
|
337
|
+
# PROMPTLESSLY -- a hang here would be an install that never finishes.
|
|
338
|
+
Log 'Arm B: the real register-task.ps1, run by the NON-admin user (unelevated floor)'
|
|
339
|
+
Remove-ProbeTask $RealTask
|
|
340
|
+
$armBStart = [DateTime]::UtcNow
|
|
341
|
+
$armBExit = Invoke-AsProbeUser @(('"' + (Join-Path $bin 'register-task.ps1') + '"'), '-InstallDir', ('"' + $installDir + '"')) 'armB'
|
|
342
|
+
$armBSeconds = ([DateTime]::UtcNow - $armBStart).TotalSeconds
|
|
343
|
+
Log (" exit code: $armBExit elapsed: {0:N1}s" -f $armBSeconds)
|
|
344
|
+
if ($armBExit -ne 0) {
|
|
345
|
+
Die "Arm B: register-task.ps1 exited $armBExit for a standard user. An unelevated install must still produce a working task."
|
|
346
|
+
}
|
|
347
|
+
if (-not (Get-ScheduledTask -TaskName $RealTask -ErrorAction SilentlyContinue)) {
|
|
348
|
+
Die 'Arm B: a standard non-admin user could NOT register the connector task at all. The installer is RequestExecutionLevel user, so this would break every install.'
|
|
349
|
+
}
|
|
350
|
+
$armBShape = Get-TaskShape $RealTask
|
|
351
|
+
Log " non-admin registration -> BootTrigger=$($armBShape.BootTrigger) Repetition=$($armBShape.Repetition) LogonType=$($armBShape.LogonType)"
|
|
352
|
+
if (-not $armBShape.Repetition) {
|
|
353
|
+
Die "Arm B: the unelevated floor lost the repeating trigger (BootTrigger=$($armBShape.BootTrigger), LogonType='$($armBShape.LogonType)'). That is the only start path a standard user can create, and without it this rung is no better than the pre-fix shape."
|
|
354
|
+
}
|
|
355
|
+
# Documenting the platform rule as an ASSERTION, so that if a future Windows
|
|
356
|
+
# build ever does allow unelevated S4U this arm goes red and tells us, instead
|
|
357
|
+
# of the ladder silently carrying a rung nobody needs any more.
|
|
358
|
+
if ($armBShape.Headless) {
|
|
359
|
+
Die "Arm B: an unelevated standard user registered a HEADLESS task. That contradicts the measurement this design is built on (S4U refused unelevated). Re-check whether the elevation prompt in ensure-sensing.ps1 is still needed."
|
|
360
|
+
}
|
|
361
|
+
Pass "unelevated floor: standard user gets 'Interactive + repeat' promptlessly in $([Math]::Round($armBSeconds,1))s; headless correctly requires elevation"
|
|
362
|
+
Remove-ProbeTask $RealTask
|
|
363
|
+
|
|
364
|
+
# ======================================= ARM F -- NO PROMPT A USER CANNOT MEET
|
|
365
|
+
# ensure-sensing.ps1 asks for elevation, and it must NOT ask a standard user:
|
|
366
|
+
# UAC would show an over-the-shoulder CREDENTIAL prompt they cannot satisfy,
|
|
367
|
+
# and with -Wait that is an install that hangs on the wizard-close path.
|
|
368
|
+
# Deliberately runs against the task ARM B just created, which the probe user
|
|
369
|
+
# OWNS and which is non-headless ('Interactive + repeat') -- exactly the state
|
|
370
|
+
# that arms the elevation branch. Manufacturing a pre-fix task as the ELEVATED
|
|
371
|
+
# runner instead would test nothing real: the probe user cannot replace a task
|
|
372
|
+
# it does not own, so register-task.ps1 would fail on an ownership error
|
|
373
|
+
# ("Cannot create a file when that file already exists") that no customer hits,
|
|
374
|
+
# because on a customer machine the task belongs to the installing user.
|
|
375
|
+
Log 'Arm F: ensure-sensing.ps1 as the NON-admin user must not prompt or hang'
|
|
376
|
+
$armFStart = [DateTime]::UtcNow
|
|
377
|
+
$armFExit = Invoke-AsProbeUser @(('"' + (Join-Path $bin 'ensure-sensing.ps1') + '"'), '-InstallDir', ('"' + $installDir + '"')) 'armF'
|
|
378
|
+
$armFSeconds = ([DateTime]::UtcNow - $armFStart).TotalSeconds
|
|
379
|
+
Log (" exit code: $armFExit elapsed: {0:N1}s" -f $armFSeconds)
|
|
380
|
+
if ($armFSeconds -gt 60) {
|
|
381
|
+
Die "Arm F: ensure-sensing.ps1 took $([Math]::Round($armFSeconds,1))s for a standard user -- that is the signature of a UAC prompt waiting for credentials nobody can supply."
|
|
382
|
+
}
|
|
383
|
+
if (-not (Get-ScheduledTask -TaskName $RealTask -ErrorAction SilentlyContinue)) {
|
|
384
|
+
Die 'Arm F: ensure-sensing.ps1 left a standard user with NO task.'
|
|
385
|
+
}
|
|
386
|
+
Pass "no unsatisfiable prompt: ensure-sensing.ps1 completed for a standard user in $([Math]::Round($armFSeconds,1))s"
|
|
387
|
+
|
|
388
|
+
# The elevation branch is gated on [Environment]::UserInteractive, and the ONLY
|
|
389
|
+
# caller that can reach it is trillboards-setup.ps1's Resume-BackgroundSensing,
|
|
390
|
+
# which launches ensure-sensing.ps1 with -NonInteractive, CreateNoWindow and
|
|
391
|
+
# WindowStyle Hidden. If any of those flipped UserInteractive to False the
|
|
392
|
+
# prompt would never fire and the fix would reach nobody -- silently, which is
|
|
393
|
+
# this bug's whole signature. Measured as a DIFFERENTIAL on the -NonInteractive
|
|
394
|
+
# flag so the answer does not depend on the runner's own baseline: whatever the
|
|
395
|
+
# baseline is, the flag must not change it.
|
|
396
|
+
$uiProbe = Join-Path $ProbeRoot 'ui-probe.ps1'
|
|
397
|
+
Set-Content -LiteralPath $uiProbe -Encoding ASCII -Value @'
|
|
398
|
+
param([string]$OutFile)
|
|
399
|
+
Set-Content -LiteralPath $OutFile -Encoding ASCII -Value "userInteractive=$([Environment]::UserInteractive)"
|
|
400
|
+
'@
|
|
401
|
+
function Measure-UserInteractive([bool]$NonInteractive) {
|
|
402
|
+
$out = Join-Path $MarkerDir ("ui-" + $NonInteractive + ".txt")
|
|
403
|
+
if (Test-Path $out) { Remove-Item -Force $out }
|
|
404
|
+
$flag = if ($NonInteractive) { ' -NonInteractive' } else { '' }
|
|
405
|
+
$si = New-Object System.Diagnostics.ProcessStartInfo
|
|
406
|
+
$si.FileName = $psExe
|
|
407
|
+
$si.Arguments = ('-NoProfile' + $flag + ' -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}" -OutFile "{1}"' -f $uiProbe, $out)
|
|
408
|
+
$si.UseShellExecute = $false
|
|
409
|
+
$si.CreateNoWindow = $true
|
|
410
|
+
$si.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
|
411
|
+
$p = [System.Diagnostics.Process]::Start($si)
|
|
412
|
+
$p.WaitForExit()
|
|
413
|
+
if (-not (Test-Path $out)) { return 'no-result' }
|
|
414
|
+
((Get-Content -LiteralPath $out -Raw).Trim() -split '=', 2)[1]
|
|
415
|
+
}
|
|
416
|
+
$uiPlain = Measure-UserInteractive $false
|
|
417
|
+
$uiNonInt = Measure-UserInteractive $true
|
|
418
|
+
Log " UserInteractive: without -NonInteractive = $uiPlain ; with -NonInteractive = $uiNonInt"
|
|
419
|
+
if ($uiPlain -eq 'no-result' -or $uiNonInt -eq 'no-result') {
|
|
420
|
+
Die 'Could not measure [Environment]::UserInteractive under the wizard launch flags.'
|
|
421
|
+
}
|
|
422
|
+
if ($uiPlain -ne $uiNonInt) {
|
|
423
|
+
Die "-NonInteractive CHANGES [Environment]::UserInteractive ($uiPlain -> $uiNonInt). trillboards-setup.ps1 launches ensure-sensing.ps1 with that flag, so the elevation branch would be unreachable from the only path that calls it."
|
|
424
|
+
}
|
|
425
|
+
Pass "the wizard's launch flags do not gate the elevation branch (-NonInteractive leaves UserInteractive=$uiNonInt)"
|
|
426
|
+
Remove-ProbeTask $RealTask
|
|
427
|
+
|
|
428
|
+
# ========================================================= ARM C -- RUNTIME
|
|
429
|
+
$markerScript = Join-Path $ProbeRoot 'probe-marker.ps1'
|
|
430
|
+
Set-Content -LiteralPath $markerScript -Encoding ASCII -Value @'
|
|
431
|
+
param([string]$OutFile)
|
|
432
|
+
$net = 'not-attempted'
|
|
433
|
+
for ($i = 0; $i -lt 3; $i++) {
|
|
434
|
+
try {
|
|
435
|
+
$r = Invoke-WebRequest -Uri 'https://api.trillboards.com/health' -UseBasicParsing -TimeoutSec 20
|
|
436
|
+
$net = "transport-ok:$($r.StatusCode)"; break
|
|
437
|
+
} catch {
|
|
438
|
+
# Any HTTP status proves TCP+TLS completed under this token, which is the
|
|
439
|
+
# thing S4U is suspected of breaking. Only a transport error is a real miss.
|
|
440
|
+
if ($_.Exception.Response) { $net = "transport-ok:$([int]$_.Exception.Response.StatusCode)"; break }
|
|
441
|
+
$net = "transport-fail:$($_.Exception.Message)"
|
|
442
|
+
Start-Sleep -Seconds 3
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
$lines = @(
|
|
446
|
+
"identity=$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)",
|
|
447
|
+
"userProfile=$env:USERPROFILE",
|
|
448
|
+
"localAppData=$env:LOCALAPPDATA",
|
|
449
|
+
"sessionId=$((Get-Process -Id $PID).SessionId)",
|
|
450
|
+
"network=$net"
|
|
451
|
+
)
|
|
452
|
+
Set-Content -LiteralPath $OutFile -Encoding ASCII -Value $lines
|
|
453
|
+
'@
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
$controlMarker = Join-Path $MarkerDir 'control.txt'
|
|
457
|
+
$fixMarker = Join-Path $MarkerDir 'fix.txt'
|
|
458
|
+
|
|
459
|
+
# Arm C needs an S4U task owned by somebody who is NOT logged on, and Arm B
|
|
460
|
+
# just proved a standard user cannot create one. The subject here is therefore
|
|
461
|
+
# a second local account that IS in Administrators -- which is exactly the
|
|
462
|
+
# population that gets headless in the field, because they are the ones who
|
|
463
|
+
# can accept the UAC prompt. Members of Administrators hold SeBatchLogonRight
|
|
464
|
+
# by default, so no policy has to be touched to reach this state.
|
|
465
|
+
#
|
|
466
|
+
# An EARLIER revision granted SeBatchLogonRight with `secedit /configure
|
|
467
|
+
# /areas USER_RIGHTS` instead. Do not do that: /configure REPLACES the whole
|
|
468
|
+
# user-rights policy with the contents of the .inf, and `secedit /export`
|
|
469
|
+
# emits only the explicitly-configured rights, so re-applying it stripped
|
|
470
|
+
# defaults machine-wide. The visible symptom was the CONTROL arm suddenly
|
|
471
|
+
# failing to register at all ("Access is denied") on a runner where it had
|
|
472
|
+
# worked minutes earlier -- i.e. the probe broke the host it was measuring.
|
|
473
|
+
Log "Arm C/D: creating a local ADMIN user '$AdminUser' who is never logged on"
|
|
474
|
+
New-ProbeAccount -Name $AdminUser -Admin
|
|
475
|
+
$adminCred = New-Object System.Management.Automation.PSCredential($AdminUser, (ConvertTo-SecureString $ProbePass -AsPlainText -Force))
|
|
476
|
+
Start-Process -FilePath $env:ComSpec -ArgumentList '/c', 'exit' -Credential $adminCred -LoadUserProfile `
|
|
477
|
+
-WorkingDirectory 'C:\' -Wait -ErrorAction Stop
|
|
478
|
+
if ((& query.exe user 2>&1 | Out-String) -match [regex]::Escape($AdminUser)) {
|
|
479
|
+
Die "Arm C premise broken: '$AdminUser' HAS an interactive session."
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# HOW AN S4U TASK CAN BE CREATED AT ALL, measured rather than assumed:
|
|
483
|
+
# as the account ITSELF, ELEVATED -> works (Arm A does exactly this)
|
|
484
|
+
# as the account itself, UNELEVATED -> refused (Arm B, run 31836760844)
|
|
485
|
+
# by an elevated admin FOR ANOTHER account -> refused, even when the target
|
|
486
|
+
# is itself an Administrators
|
|
487
|
+
# member (run 31838558271)
|
|
488
|
+
# The cross-user direction needs SeTcbPrivilege, which an elevated admin does
|
|
489
|
+
# not hold. And `Start-Process -Credential` yields a FILTERED token, so it
|
|
490
|
+
# cannot produce an elevated process as somebody else either.
|
|
491
|
+
#
|
|
492
|
+
# The one remaining vehicle is a task. A Password-logon task runs ELEVATED
|
|
493
|
+
# (RunLevel Highest) and WITHOUT a session, so it can act as $AdminUser and
|
|
494
|
+
# register the two arms FOR ITSELF -- which is the only direction Windows
|
|
495
|
+
# permits, and the same direction the product uses.
|
|
496
|
+
$registrar = Join-Path $ProbeRoot 'probe-register-arms.ps1'
|
|
497
|
+
Set-Content -LiteralPath $registrar -Encoding ASCII -Value @'
|
|
498
|
+
param([string]$MarkerScript, [string]$ControlTask, [string]$ControlMarker,
|
|
499
|
+
[string]$FixTask, [string]$FixMarker, [string]$ResultFile)
|
|
500
|
+
$me = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
501
|
+
$elevated = (New-Object System.Security.Principal.WindowsPrincipal(
|
|
502
|
+
[System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole(
|
|
503
|
+
[System.Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
504
|
+
$psExe = Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
505
|
+
function Register-Arm([string]$Name, [string]$LogonType, [string]$Marker) {
|
|
506
|
+
try {
|
|
507
|
+
$a = New-ScheduledTaskAction -Execute $psExe -Argument ('-NoProfile -ExecutionPolicy Bypass -File "' + $MarkerScript + '" -OutFile "' + $Marker + '"')
|
|
508
|
+
$t = New-ScheduledTaskTrigger -AtStartup
|
|
509
|
+
$p = New-ScheduledTaskPrincipal -UserId $me -LogonType $LogonType -RunLevel Limited
|
|
510
|
+
$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
|
511
|
+
Unregister-ScheduledTask -TaskName $Name -Confirm:$false -ErrorAction SilentlyContinue
|
|
512
|
+
Register-ScheduledTask -TaskName $Name -Action $a -Trigger $t -Principal $p -Settings $s -ErrorAction Stop | Out-Null
|
|
513
|
+
return 'registered'
|
|
514
|
+
} catch {
|
|
515
|
+
return "REFUSED:$($_.Exception.Message)"
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
$lines = @(
|
|
519
|
+
"identity=$me",
|
|
520
|
+
"elevated=$elevated",
|
|
521
|
+
"control=$(Register-Arm $ControlTask 'Interactive' $ControlMarker)",
|
|
522
|
+
"fix=$(Register-Arm $FixTask 'S4U' $FixMarker)"
|
|
523
|
+
)
|
|
524
|
+
Set-Content -LiteralPath $ResultFile -Encoding ASCII -Value $lines
|
|
525
|
+
'@
|
|
526
|
+
& icacls.exe $registrar /grant '*S-1-1-0:RX' | Out-Null
|
|
527
|
+
|
|
528
|
+
$resultFile = Join-Path $MarkerDir 'registrar.txt'
|
|
529
|
+
$bootstrapTask = 'TbHeadlessProbeBootstrap'
|
|
530
|
+
Log "Arm C: bootstrapping an ELEVATED, session-less process as '$AdminUser' to register both arms for itself"
|
|
531
|
+
Remove-ProbeTask $bootstrapTask
|
|
532
|
+
$bootArgs = ('-NoProfile -ExecutionPolicy Bypass -File "{0}" -MarkerScript "{1}" -ControlTask {2} -ControlMarker "{3}" -FixTask {4} -FixMarker "{5}" -ResultFile "{6}"' -f
|
|
533
|
+
$registrar, $markerScript, $ControlTask, $controlMarker, $FixTask, $fixMarker, $resultFile)
|
|
534
|
+
Register-ScheduledTask -TaskName $bootstrapTask `
|
|
535
|
+
-Action (New-ScheduledTaskAction -Execute $psExe -Argument $bootArgs) `
|
|
536
|
+
-Trigger (New-ScheduledTaskTrigger -AtStartup) `
|
|
537
|
+
-User $AdminUser -Password $ProbePass -RunLevel Highest | Out-Null
|
|
538
|
+
Start-ScheduledTask -TaskName $bootstrapTask
|
|
539
|
+
$bootDeadline = [DateTime]::UtcNow.AddSeconds(90)
|
|
540
|
+
while (([DateTime]::UtcNow -lt $bootDeadline) -and -not (Test-Path $resultFile)) { Start-Sleep -Seconds 3 }
|
|
541
|
+
Remove-ProbeTask $bootstrapTask
|
|
542
|
+
|
|
543
|
+
if (-not (Test-Path $resultFile)) {
|
|
544
|
+
Die 'Arm C is VACUOUS: the Password-logon bootstrap task never ran, so no S4U task could be created and nothing below can be concluded.'
|
|
545
|
+
}
|
|
546
|
+
$reg = @{}
|
|
547
|
+
foreach ($line in (Get-Content -LiteralPath $resultFile)) {
|
|
548
|
+
$kv = $line -split '=', 2
|
|
549
|
+
if ($kv.Count -eq 2) { $reg[$kv[0]] = $kv[1] }
|
|
550
|
+
}
|
|
551
|
+
# The bootstrap running at all is itself a result: a non-interactive principal
|
|
552
|
+
# executed with nobody logged on. Arm C then isolates S4U specifically.
|
|
553
|
+
Log " bootstrap ran as: $($reg['identity']) elevated=$($reg['elevated'])"
|
|
554
|
+
Log " control registration: $($reg['control'])"
|
|
555
|
+
Log " fix registration: $($reg['fix'])"
|
|
556
|
+
if ($reg['elevated'] -ne 'True') {
|
|
557
|
+
Die "Arm C is VACUOUS: the bootstrap was not elevated ($($reg['elevated'])), and S4U cannot be registered without elevation, so a refusal below would say nothing about the principal."
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if ($reg['control'] -notmatch 'registered$') {
|
|
561
|
+
Die "Arm C is VACUOUS: the Interactive control could not be registered at all ($($reg['control'])), so 'the control did not run' would say nothing about logon type."
|
|
562
|
+
}
|
|
563
|
+
if ($reg['fix'] -notmatch 'registered$') {
|
|
564
|
+
Die "Arm C: S4U was refused to an ELEVATED account registering for ITSELF ($($reg['fix'])) -- the one direction Windows is supposed to permit, and the one register-task.ps1 uses. The runtime differential cannot be established on this host, so nothing below it would mean anything. Do NOT weaken this into a pass."
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
foreach ($n in @($ControlTask, $FixTask)) {
|
|
568
|
+
try { Start-ScheduledTask -TaskName $n -ErrorAction Stop; Log " started $n" }
|
|
569
|
+
catch { Log " Start-ScheduledTask $n threw: $($_.Exception.Message)" }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
# Poll both. The fix should land in seconds; give the control the SAME full
|
|
573
|
+
# budget so "it did not run" cannot be confused with "it was slow".
|
|
574
|
+
$deadline = [DateTime]::UtcNow.AddSeconds(90)
|
|
575
|
+
while ([DateTime]::UtcNow -lt $deadline) {
|
|
576
|
+
if ((Test-Path $fixMarker) -and (Test-Path $controlMarker)) { break }
|
|
577
|
+
Start-Sleep -Seconds 3
|
|
578
|
+
}
|
|
579
|
+
$controlRan = Test-Path $controlMarker
|
|
580
|
+
$fixRan = Test-Path $fixMarker
|
|
581
|
+
|
|
582
|
+
foreach ($n in @($ControlTask, $FixTask)) {
|
|
583
|
+
$info = Get-ScheduledTaskInfo -TaskName $n -ErrorAction SilentlyContinue
|
|
584
|
+
if ($info) { Log (" {0}: LastTaskResult=0x{1:X} LastRunTime={2}" -f $n, $info.LastTaskResult, $info.LastRunTime) }
|
|
585
|
+
}
|
|
586
|
+
Log " control marker present: $controlRan"
|
|
587
|
+
Log " fix marker present: $fixRan"
|
|
588
|
+
|
|
589
|
+
if ($controlRan) {
|
|
590
|
+
Die 'Arm C is VACUOUS: the Interactive-principal task RAN for a user with no interactive session. This host cannot observe the property, so a green fix arm would prove nothing.'
|
|
591
|
+
}
|
|
592
|
+
if (-not $fixRan) {
|
|
593
|
+
Die 'The S4U task did NOT run for a user with no interactive session -- the headless principal does not work here.'
|
|
594
|
+
}
|
|
595
|
+
Pass 'runtime: S4U ran with nobody logged on; the pre-fix Interactive principal did NOT (same action, same user, same trigger)'
|
|
596
|
+
|
|
597
|
+
# ============================================================ ARM D -- STATE
|
|
598
|
+
$marker = @{}
|
|
599
|
+
foreach ($line in (Get-Content -LiteralPath $fixMarker)) {
|
|
600
|
+
$kv = $line -split '=', 2
|
|
601
|
+
if ($kv.Count -eq 2) { $marker[$kv[0]] = $kv[1] }
|
|
602
|
+
}
|
|
603
|
+
Log ' S4U task reported:'
|
|
604
|
+
foreach ($k in $marker.Keys) { Log " $k = $($marker[$k])" }
|
|
605
|
+
|
|
606
|
+
if ($marker['identity'] -notmatch [regex]::Escape($AdminUser)) {
|
|
607
|
+
Die "Arm D: the S4U task ran as '$($marker['identity'])', not as '$AdminUser'."
|
|
608
|
+
}
|
|
609
|
+
if ([string]::IsNullOrWhiteSpace($marker['userProfile']) -or $marker['userProfile'] -match 'systemprofile') {
|
|
610
|
+
Die "Arm D: USERPROFILE under the headless task is '$($marker['userProfile'])'. The connector resolves its device identity, data dir and the ~2.4 GB Gemma model through this root; systemprofile means it would start and then find nothing."
|
|
611
|
+
}
|
|
612
|
+
if ($marker['userProfile'] -notmatch [regex]::Escape($AdminUser)) {
|
|
613
|
+
Die "Arm D: USERPROFILE '$($marker['userProfile'])' is not the owning user's profile."
|
|
614
|
+
}
|
|
615
|
+
if ($marker['network'] -notmatch '^transport-ok') {
|
|
616
|
+
Die "Arm D: outbound HTTPS under the S4U token did not complete: $($marker['network']). S4U grants no NETWORK credentials; this asserts that plain outbound TLS is unaffected."
|
|
617
|
+
}
|
|
618
|
+
Pass "state: USERPROFILE=$($marker['userProfile']) (not systemprofile), sessionId=$($marker['sessionId']), outbound HTTPS $($marker['network'])"
|
|
619
|
+
|
|
620
|
+
Write-Host ''
|
|
621
|
+
Write-Host 'headless-principal E2E PASSED'
|
|
622
|
+
exit 0
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
foreach ($n in @($RealTask, $ControlTask, $FixTask, 'TbHeadlessProbeBootstrap')) { Remove-ProbeTask $n }
|
|
626
|
+
foreach ($account in @($ProbeUser, $AdminUser)) {
|
|
627
|
+
Get-LocalUser -Name $account -ErrorAction SilentlyContinue | Remove-LocalUser -ErrorAction SilentlyContinue
|
|
628
|
+
Get-CimInstance Win32_UserProfile -ErrorAction SilentlyContinue |
|
|
629
|
+
Where-Object { $_.LocalPath -like "*\$account" } |
|
|
630
|
+
Remove-CimInstance -ErrorAction SilentlyContinue
|
|
631
|
+
}
|
|
632
|
+
}
|