mcp-pacemaker 2.0.2 → 2.1.1
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/CHANGELOG.md +171 -1
- package/CONTRIBUTING.md +46 -13
- package/README.md +109 -524
- package/THIRD_PARTY_NOTICES.txt +1 -28
- package/autostart/windows/account-task.ps1 +167 -0
- package/autostart/windows/inspect-task.ps1 +42 -0
- package/autostart/windows/inspect-worker-process-sessions.ps1 +155 -0
- package/autostart/windows/legacy-access.ps1 +359 -0
- package/autostart/windows/legacy-task.ps1 +301 -0
- package/autostart/windows/register-task.ps1 +11 -2
- package/bin/account-task-adapter.mjs +47 -0
- package/bin/account-upgrade-controller.mjs +198 -0
- package/bin/account-upgrade-worker.mjs +141 -0
- package/bin/account-worker-session.mjs +215 -0
- package/bin/cli-dispatch.mjs +154 -0
- package/bin/cli-main.mjs +1063 -0
- package/bin/cli-selection.mjs +43 -0
- package/bin/cli.mjs +7 -876
- package/bin/dependency-lock.json +642 -0
- package/bin/legacy-1.3.json +32 -0
- package/bin/legacy-access.mjs +73 -0
- package/bin/legacy-installation.mjs +273 -0
- package/bin/legacy-task.mjs +76 -0
- package/bin/legacy-upgrade.mjs +527 -0
- package/bin/managed-runtime.mjs +170 -0
- package/bin/managed-state.mjs +257 -0
- package/bin/managed-upgrade.mjs +581 -0
- package/bin/mcp-bridge.mjs +150 -4
- package/bin/pooling-batches.mjs +13 -0
- package/bin/pooling-writer.mjs +4 -0
- package/bin/prewarm.mjs +59 -25
- package/bin/runtime-context.mjs +100 -0
- package/bin/service-control.mjs +32 -4
- package/bin/stable-launcher.mjs +54 -0
- package/bin/task-only-controller.mjs +226 -0
- package/bin/task-scope.mjs +65 -0
- package/bin/task-transaction-protocol.mjs +173 -0
- package/bin/top.mjs +22 -10
- package/bin/update-check.mjs +13 -2
- package/bin/upgrade-autostart.mjs +47 -0
- package/bin/upgrade-capability.json +9 -0
- package/bin/upgrade-dependencies.mjs +183 -0
- package/bin/upgrade-package.mjs +205 -0
- package/bin/windows-legacy/LegacyProcessBroker.build.json +42 -0
- package/bin/windows-legacy/LegacyProcessBroker.exe +0 -0
- package/bin/windows-legacy/src/AssemblyInfo.cs +8 -0
- package/bin/windows-legacy/src/LegacyNative.cs +832 -0
- package/bin/windows-legacy/src/LegacyProcessBroker.cs +961 -0
- package/bin/windows-legacy-process.mjs +248 -0
- package/bin/windows-lifetime/ProcessLifetimeHelper.build.json +38 -0
- package/bin/windows-lifetime/ProcessLifetimeHelper.exe +0 -0
- package/bin/windows-lifetime/src/AssemblyInfo.cs +8 -0
- package/bin/windows-lifetime/src/ProcessLifetimeHelper.cs +468 -0
- package/bin/windows-process-lifetime.mjs +218 -0
- package/bin/windows-task-channel/TaskChannelGuard.build.json +45 -0
- package/bin/windows-task-channel/TaskChannelGuard.exe +0 -0
- package/bin/windows-task-channel/src/AssemblyInfo.cs +8 -0
- package/bin/windows-task-channel/src/ChannelCliContext.cs +210 -0
- package/bin/windows-task-channel/src/ChannelFiles.cs +231 -0
- package/bin/windows-task-channel/src/ChannelNative.cs +477 -0
- package/bin/windows-task-channel/src/ChannelSession.cs +203 -0
- package/bin/windows-task-channel/src/TaskChannelGuard.cs +686 -0
- package/bin/windows-task-channel.mjs +335 -0
- package/docs/account-upgrades.md +218 -0
- package/docs/compatibility.md +39 -0
- package/docs/configuration.md +146 -0
- package/docs/dashboards.md +125 -0
- package/docs/execution-context.md +119 -0
- package/docs/http-api.md +47 -0
- package/docs/install.md +52 -0
- package/docs/operations.md +99 -0
- package/docs/upgrades.md +324 -0
- package/package.json +19 -5
- package/supervisor/supervise.mjs +226 -40
- package/tools/account-upgrade/qualify-two-account.ps1 +66 -0
- package/tools/windows-legacy/README.md +251 -0
- package/tools/windows-legacy/build.ps1 +122 -0
- package/tools/windows-legacy/inventory.mjs +92 -0
- package/tools/windows-process-lifetime/README.md +119 -0
- package/tools/windows-process-lifetime/build.ps1 +122 -0
- package/tools/windows-process-lifetime/inventory.mjs +86 -0
- package/tools/windows-task-channel/README.md +329 -0
- package/tools/windows-task-channel/build.ps1 +107 -0
- package/tools/windows-task-channel/inventory.mjs +117 -0
- package/ui/dist/THIRD_PARTY_NOTICES.txt +1 -28
- package/ui/dist/assets/{index-CgPruWYi.js → index-CcwMo2sI.js} +1 -1
- package/ui/dist/assets/index-PVcuDNrN.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/third-party-manifest.json +37 -25
- package/ui/dist/assets/index-CaLPg0c-.css +0 -1
package/THIRD_PARTY_NOTICES.txt
CHANGED
|
@@ -192,7 +192,7 @@ SOFTWARE.
|
|
|
192
192
|
*/
|
|
193
193
|
|
|
194
194
|
==============================================================================
|
|
195
|
-
tailwindcss@3.
|
|
195
|
+
tailwindcss@4.3.3
|
|
196
196
|
Declared license: MIT
|
|
197
197
|
|
|
198
198
|
--- LICENSE ---
|
|
@@ -218,33 +218,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
218
218
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
219
219
|
SOFTWARE.
|
|
220
220
|
|
|
221
|
-
--- lib/css/LICENSE ---
|
|
222
|
-
MIT License
|
|
223
|
-
|
|
224
|
-
Copyright (c) Nicolas Gallagher
|
|
225
|
-
Copyright (c) Jonathan Neal
|
|
226
|
-
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
|
|
227
|
-
Copyright (c) Adam Wathan
|
|
228
|
-
Copyright (c) Jonathan Reinink
|
|
229
|
-
|
|
230
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
231
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
232
|
-
in the Software without restriction, including without limitation the rights
|
|
233
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
234
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
235
|
-
furnished to do so, subject to the following conditions:
|
|
236
|
-
|
|
237
|
-
The above copyright notice and this permission notice shall be included in all
|
|
238
|
-
copies or substantial portions of the Software.
|
|
239
|
-
|
|
240
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
241
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
242
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
243
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
244
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
245
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
246
|
-
SOFTWARE.
|
|
247
|
-
|
|
248
221
|
==============================================================================
|
|
249
222
|
vite@7.3.6
|
|
250
223
|
Declared license: MIT
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Performs fixed controller task operations while preserving the registered account.
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Reads a bounded controller request on stdin. This adapter changes only the exact
|
|
6
|
+
selected registration, preserving principal, logon type, run level and full security.
|
|
7
|
+
It never stops processes, edits configuration, enables privileges or changes ACLs.
|
|
8
|
+
.PARAMETER Action
|
|
9
|
+
Exact task operation chosen by the trusted controller.
|
|
10
|
+
.OUTPUTS
|
|
11
|
+
Bounded JSON task/profile/session evidence. A successful launch call is not readiness.
|
|
12
|
+
.EXAMPLE
|
|
13
|
+
.\account-task.ps1 -Action inspect
|
|
14
|
+
#>
|
|
15
|
+
[CmdletBinding()]
|
|
16
|
+
param(
|
|
17
|
+
[Parameter(Mandatory)]
|
|
18
|
+
[ValidateSet('inspect', 'session', 'bootstrap', 'hold', 'repoint', 'release', 'restore', 'launch')]
|
|
19
|
+
[string]$Action
|
|
20
|
+
)
|
|
21
|
+
$ErrorActionPreference = 'Stop'
|
|
22
|
+
. (Join-Path $PSScriptRoot 'legacy-task.ps1') -Operation inspect -Port 1 -FunctionsOnly
|
|
23
|
+
$text = [Console]::In.ReadToEnd()
|
|
24
|
+
if ($text.Length -gt 65536) { throw 'Controller request exceeds its bound.' }
|
|
25
|
+
$request = $text | ConvertFrom-Json
|
|
26
|
+
$targetSid = $request.targetSid
|
|
27
|
+
if ($targetSid -cnotmatch '^S-1-[0-9]+(?:-[0-9]+)+$' -or
|
|
28
|
+
([Security.Principal.SecurityIdentifier]::new($targetSid)).Value -cne $targetSid) {
|
|
29
|
+
throw 'A canonical target SID is required.'
|
|
30
|
+
}
|
|
31
|
+
$impersonated = [Security.Principal.WindowsIdentity]::GetCurrent($true)
|
|
32
|
+
if ($null -ne $impersonated) {
|
|
33
|
+
$impersonated.Dispose()
|
|
34
|
+
throw 'Impersonating task-adapter contexts are unsupported.'
|
|
35
|
+
}
|
|
36
|
+
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
37
|
+
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
|
38
|
+
if ($targetSid -cne $currentSid -and
|
|
39
|
+
-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
40
|
+
throw 'The current task-adapter token is not an enabled administrator.'
|
|
41
|
+
}
|
|
42
|
+
$path = $request.taskPath
|
|
43
|
+
if ([string]::IsNullOrEmpty($path) -or
|
|
44
|
+
$path.Length -gt 1024 -or
|
|
45
|
+
-not $path.StartsWith('\') -or
|
|
46
|
+
$path.Contains('/') -or
|
|
47
|
+
$path.Split('\') -contains '..') { throw 'An exact task path is required.' }
|
|
48
|
+
$separator = $path.LastIndexOf('\')
|
|
49
|
+
$folderPath = if ($separator -eq 0) { '\' } else { $path.Substring(0, $separator) }
|
|
50
|
+
$name = $path.Substring($separator + 1)
|
|
51
|
+
$service = New-Object -ComObject Schedule.Service
|
|
52
|
+
$service.Connect()
|
|
53
|
+
$folder = $service.GetFolder($folderPath)
|
|
54
|
+
$task = $folder.GetTask($name)
|
|
55
|
+
$record = Get-TaskRecord -Task $task
|
|
56
|
+
$record.runAsName = $task.Definition.Principal.UserId
|
|
57
|
+
if ($record.path -cne $path -or
|
|
58
|
+
$record.userSid -cne $targetSid -or
|
|
59
|
+
$record.logonType -ne 3 -or
|
|
60
|
+
$record.runLevel -ne 0) { throw 'Selected task does not preserve the requested interactive account.' }
|
|
61
|
+
Assert-TaskSecurity -Actual $record.security -Expected $record.security -RuntimeSid $targetSid
|
|
62
|
+
if ($Action -eq 'inspect') {
|
|
63
|
+
$profiles = @(Get-CimInstance -ClassName Win32_UserProfile -Filter "SID='$targetSid'" -Property SID, LocalPath, Loaded -OperationTimeoutSec 10)
|
|
64
|
+
if ($profiles.Count -ne 1 -or
|
|
65
|
+
-not $profiles[0].Loaded) { throw 'The selected user profile is not loaded in an existing interactive session.' }
|
|
66
|
+
@{
|
|
67
|
+
record = $record
|
|
68
|
+
profilePath = [IO.Path]::GetFullPath($profiles[0].LocalPath)
|
|
69
|
+
allowDemandStart = $task.Definition.Settings.AllowDemandStart
|
|
70
|
+
} | ConvertTo-Json -Depth 12 -Compress
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
if ($record.xmlSha256 -cne $request.expected.xmlSha256 -or
|
|
74
|
+
$record.userSid -cne $request.expected.userSid) { throw 'Task definition changed after the approved plan.' }
|
|
75
|
+
Assert-TaskSecurity -Actual $record.security -Expected $request.expected.security -RuntimeSid $targetSid
|
|
76
|
+
if ($Action -eq 'session') {
|
|
77
|
+
$port = [int]$request.port
|
|
78
|
+
if ($port -lt 1 -or $port -gt 65535) { throw 'Invalid selected port.' }
|
|
79
|
+
$listeners = @(Get-CimInstance -Namespace 'root/StandardCimv2' -ClassName MSFT_NetTCPConnection `
|
|
80
|
+
-Filter "LocalAddress='127.0.0.1' AND LocalPort=$port AND State=2" -OperationTimeoutSec 10)
|
|
81
|
+
if ($listeners.Count -ne 1) { throw 'Selected listener is absent or ambiguous.' }
|
|
82
|
+
$processId = [int]$listeners[0].OwningProcess
|
|
83
|
+
$process = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$processId" -Property ProcessId, ParentProcessId, SessionId, CreationDate, CommandLine, ExecutablePath -OperationTimeoutSec 10
|
|
84
|
+
$owner = Invoke-CimMethod -InputObject $process -MethodName GetOwnerSid -ErrorAction Stop
|
|
85
|
+
$scriptPath = [IO.Path]::GetFullPath((Join-Path $request.root 'bin\mcp-bridge.mjs'))
|
|
86
|
+
$commands = @(
|
|
87
|
+
"`"$($process.ExecutablePath)`" `"$scriptPath`" --port $port",
|
|
88
|
+
"$($process.ExecutablePath) `"$scriptPath`" --port $port"
|
|
89
|
+
)
|
|
90
|
+
if ($null -ne $request.managed) {
|
|
91
|
+
$parts = @((Join-Path $request.root 'supervisor\bridge-child.mjs'), '--port', "$port", '--config', $request.managed.config, '--cwd', $request.managed.cwd)
|
|
92
|
+
$arguments = ($parts | ForEach-Object {
|
|
93
|
+
if ($_ -match '\s') { "`"$_`"" } else { $_ }
|
|
94
|
+
}) -join ' '
|
|
95
|
+
$commands = @("`"$($process.ExecutablePath)`" $arguments", "$($process.ExecutablePath) $arguments")
|
|
96
|
+
}
|
|
97
|
+
if ($owner.ReturnValue -ne 0 -or
|
|
98
|
+
$owner.Sid -cne $targetSid -or
|
|
99
|
+
$process.SessionId -le 0 -or
|
|
100
|
+
$process.CommandLine -notin $commands) {
|
|
101
|
+
throw 'Selected listener account/session/absolute legacy command could not be verified.'
|
|
102
|
+
}
|
|
103
|
+
$parentId = [int]$process.ParentProcessId
|
|
104
|
+
$parent = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$parentId" -Property ProcessId, ExecutablePath, SessionId, CreationDate -OperationTimeoutSec 10
|
|
105
|
+
$parentOwner = Invoke-CimMethod -InputObject $parent -MethodName GetOwnerSid -ErrorAction Stop
|
|
106
|
+
if ($parentOwner.ReturnValue -ne 0 -or
|
|
107
|
+
$parentOwner.Sid -cne $targetSid -or
|
|
108
|
+
$parent.SessionId -ne $process.SessionId -or
|
|
109
|
+
$parent.CreationDate -gt $process.CreationDate) { throw 'Selected supervisor account/session/generation is ambiguous.' }
|
|
110
|
+
@{ ownerSid = $owner.Sid; sessionId = $process.SessionId; pid = $processId
|
|
111
|
+
createdUtc = $process.CreationDate.ToUniversalTime().ToString('o')
|
|
112
|
+
supervisorImage = @{ path = $parent.ExecutablePath; sha256 = (Get-FileHash -LiteralPath $parent.ExecutablePath -Algorithm SHA256).Hash.ToLowerInvariant() }
|
|
113
|
+
} | ConvertTo-Json -Depth 4 -Compress
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
if ($Action -eq 'launch') {
|
|
117
|
+
if (-not $task.Enabled -or
|
|
118
|
+
-not $task.Definition.Settings.AllowDemandStart -or
|
|
119
|
+
$request.sessionId -le 0) { throw 'The selected task cannot demand-start in the requested session.' }
|
|
120
|
+
$running = $task.RunEx($null, 12, [int]$request.sessionId, $targetSid)
|
|
121
|
+
@{ requested = $true; instanceGuid = $running.InstanceGuid; readinessProven = $false } | ConvertTo-Json -Compress
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
$definition = $task.Definition
|
|
125
|
+
switch ($Action) {
|
|
126
|
+
'bootstrap' {
|
|
127
|
+
if (-not $task.Enabled -or
|
|
128
|
+
-not $definition.Settings.AllowDemandStart) { throw 'Bootstrap requires an enabled demand-start task.' }
|
|
129
|
+
if ($definition.Actions.Count -ne 1) { throw 'Unsupported task actions.' }
|
|
130
|
+
$definition.Actions.Item(1).Path = $request.bootstrap.nodePath
|
|
131
|
+
$definition.Actions.Item(1).Arguments = $request.bootstrap.arguments
|
|
132
|
+
$definition.Actions.Item(1).WorkingDirectory = $request.bootstrap.cwd
|
|
133
|
+
foreach ($trigger in $definition.Triggers) { $trigger.Enabled = $false }
|
|
134
|
+
$definition.Settings.RestartCount = 0
|
|
135
|
+
}
|
|
136
|
+
'repoint' {
|
|
137
|
+
if ($definition.Actions.Count -ne 1) { throw 'Unsupported task actions.' }
|
|
138
|
+
$definition.Actions.Item(1).Path = 'wscript.exe'
|
|
139
|
+
$definition.Actions.Item(1).Arguments = "`"$($request.launcher)`""
|
|
140
|
+
$definition.Actions.Item(1).WorkingDirectory = ''
|
|
141
|
+
}
|
|
142
|
+
'hold' {
|
|
143
|
+
foreach ($trigger in $definition.Triggers) { $trigger.Enabled = $false }
|
|
144
|
+
$definition.Settings.RestartCount = 0
|
|
145
|
+
}
|
|
146
|
+
'release' {
|
|
147
|
+
$original = $service.NewTask(0)
|
|
148
|
+
$original.XmlText = $request.original.xml
|
|
149
|
+
if ($original.Triggers.Count -ne $definition.Triggers.Count) { throw 'Task trigger identity changed.' }
|
|
150
|
+
for ($index = 1; $index -le $definition.Triggers.Count; $index++) {
|
|
151
|
+
$definition.Triggers.Item($index).Enabled = $original.Triggers.Item($index).Enabled
|
|
152
|
+
}
|
|
153
|
+
$definition.Settings.RestartCount = $original.Settings.RestartCount
|
|
154
|
+
}
|
|
155
|
+
'restore' { $definition.XmlText = $request.original.xml }
|
|
156
|
+
}
|
|
157
|
+
Assert-TaskSecurity -Actual $record.security -Expected $request.original.security -RuntimeSid $targetSid
|
|
158
|
+
if ((Get-UserSid -User $definition.Principal.UserId) -cne $targetSid -or
|
|
159
|
+
$definition.Principal.LogonType -ne $record.logonType -or
|
|
160
|
+
$definition.Principal.RunLevel -ne $record.runLevel) { throw 'Task account semantics changed.' }
|
|
161
|
+
$expectedXml = $definition.XmlText
|
|
162
|
+
$folder.RegisterTaskDefinition($name, $definition, 52, $definition.Principal.UserId, $null, 3, $record.security.sddl) | Out-Null
|
|
163
|
+
$updated = Get-TaskRecord -Task ($folder.GetTask($name))
|
|
164
|
+
$updated.runAsName = $definition.Principal.UserId
|
|
165
|
+
Assert-TaskSecurity -Actual $updated.security -Expected $record.security -RuntimeSid $targetSid
|
|
166
|
+
if ($updated.xmlSha256 -cne (Get-XmlHash -Xml $expectedXml)) { throw 'Task update outcome differs from the exact requested definition.' }
|
|
167
|
+
$updated | ConvertTo-Json -Depth 12 -Compress
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Reads the selected per-user Pacemaker task and its legacy task identity.
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Returns task actions and principal data without changing tasks or processes.
|
|
6
|
+
.PARAMETER Port
|
|
7
|
+
Selected service port.
|
|
8
|
+
.OUTPUTS
|
|
9
|
+
JSON array of existing task identities.
|
|
10
|
+
.EXAMPLE
|
|
11
|
+
.\inspect-task.ps1 -Port 8791
|
|
12
|
+
#>
|
|
13
|
+
[CmdletBinding()]
|
|
14
|
+
param([ValidateRange(1, 65535)][int]$Port)
|
|
15
|
+
|
|
16
|
+
$ErrorActionPreference = 'Stop'
|
|
17
|
+
$service = New-Object -ComObject Schedule.Service
|
|
18
|
+
$service.Connect()
|
|
19
|
+
$folder = $service.GetFolder('\')
|
|
20
|
+
$names = @("McpPacemaker-$Port", 'McpPacemaker')
|
|
21
|
+
$records = @()
|
|
22
|
+
foreach ($task in $folder.GetTasks(1)) {
|
|
23
|
+
if ($task.Name -notin $names) { continue }
|
|
24
|
+
$actions = @()
|
|
25
|
+
foreach ($action in $task.Definition.Actions) {
|
|
26
|
+
$actions += @{
|
|
27
|
+
type = $action.Type
|
|
28
|
+
path = $action.Path
|
|
29
|
+
arguments = $action.Arguments
|
|
30
|
+
workingDirectory = $action.WorkingDirectory
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
$records += @{
|
|
34
|
+
name = $task.Name
|
|
35
|
+
enabled = $task.Enabled
|
|
36
|
+
user = $task.Definition.Principal.UserId
|
|
37
|
+
logonType = $task.Definition.Principal.LogonType
|
|
38
|
+
runLevel = $task.Definition.Principal.RunLevel
|
|
39
|
+
actions = $actions
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
ConvertTo-Json -InputObject $records -Depth 5 -Compress
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Checks session facts for the original-user broker's bounded captured identities.
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Reads a same-user request on stdin. Queries only its listed process identities,
|
|
6
|
+
or the selected managed loopback listener, and performs no process or security
|
|
7
|
+
mutation. Native held-generation revalidation remains the stop authority.
|
|
8
|
+
.OUTPUTS
|
|
9
|
+
A verification result without command lines or account identifiers.
|
|
10
|
+
.EXAMPLE
|
|
11
|
+
.\inspect-worker-process-sessions.ps1
|
|
12
|
+
#>
|
|
13
|
+
[CmdletBinding()]
|
|
14
|
+
param()
|
|
15
|
+
$ErrorActionPreference = 'Stop'
|
|
16
|
+
$watch = [Diagnostics.Stopwatch]::StartNew()
|
|
17
|
+
$stream = [Console]::OpenStandardInput()
|
|
18
|
+
$bytes = [byte[]]::new(131073)
|
|
19
|
+
$length = 0
|
|
20
|
+
while ($length -lt $bytes.Length) {
|
|
21
|
+
$read = $stream.Read($bytes, $length, $bytes.Length - $length)
|
|
22
|
+
if ($read -eq 0) { break }
|
|
23
|
+
$length += $read
|
|
24
|
+
}
|
|
25
|
+
if ($length -gt 131072) { throw 'Worker identity request exceeds its bound.' }
|
|
26
|
+
$text = [Text.UTF8Encoding]::new($false, $true).GetString($bytes, 0, $length)
|
|
27
|
+
$request = $text | ConvertFrom-Json
|
|
28
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
29
|
+
$hasSessionType = $request.sessionId -is [long] -or $request.sessionId -is [int]
|
|
30
|
+
if ($request.ownerSid -isnot [string] -or
|
|
31
|
+
$request.ownerSid -cne $sid -or
|
|
32
|
+
$request.ownerSid.Length -gt 184 -or
|
|
33
|
+
-not $hasSessionType -or
|
|
34
|
+
$request.sessionId -ne [Diagnostics.Process]::GetCurrentProcess().SessionId) {
|
|
35
|
+
throw 'Worker process/session scope is not the actual current account.'
|
|
36
|
+
}
|
|
37
|
+
if ($null -ne $request.port) {
|
|
38
|
+
if ((($request.PSObject.Properties.Name | Sort-Object) -join ',') -cne 'ownerSid,port,sessionId') {
|
|
39
|
+
throw 'Invalid managed listener request shape.'
|
|
40
|
+
}
|
|
41
|
+
$port = [int]$request.port
|
|
42
|
+
if ($port -lt 1 -or
|
|
43
|
+
$port -gt 65535 -or
|
|
44
|
+
$null -ne $request.identities) { throw 'Invalid managed listener scope.' }
|
|
45
|
+
$listeners = @(Get-CimInstance -Namespace 'root/StandardCimv2' -ClassName MSFT_NetTCPConnection `
|
|
46
|
+
-Filter "LocalAddress='127.0.0.1' AND LocalPort=$port AND State=2" -OperationTimeoutSec 10)
|
|
47
|
+
# A verified stopped/failed-start generation has no listener; its native proof is checked by the caller.
|
|
48
|
+
if ($listeners.Count -eq 0) {
|
|
49
|
+
'{"verified":true}'
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if ($listeners.Count -ne 1) { throw 'Managed listener is ambiguous.' }
|
|
53
|
+
$processId = [int]$listeners[0].OwningProcess
|
|
54
|
+
$row = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$processId" -OperationTimeoutSec 10
|
|
55
|
+
$owner = Invoke-CimMethod -InputObject $row -MethodName GetOwnerSid -ErrorAction Stop
|
|
56
|
+
if ($null -eq $row -or
|
|
57
|
+
$row.SessionId -ne $request.sessionId -or
|
|
58
|
+
$owner.ReturnValue -ne 0 -or
|
|
59
|
+
$owner.Sid -cne $sid) { throw 'Managed listener account/session changed; replan before stop.' }
|
|
60
|
+
'{"verified":true}'
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if ((($request.PSObject.Properties.Name | Sort-Object) -join ',') -cne 'identities,ownerSid,sessionId' -or
|
|
64
|
+
$request.identities -isnot [Array]) { throw 'Invalid captured identity request shape.' }
|
|
65
|
+
$identities = @($request.identities)
|
|
66
|
+
if ($identities.Count -lt 2 -or
|
|
67
|
+
$identities.Count -gt 512) { throw 'Captured identity count is unsupported.' }
|
|
68
|
+
$seen = @{}
|
|
69
|
+
foreach ($identity in $identities) {
|
|
70
|
+
if ((($identity.PSObject.Properties.Name | Sort-Object) -join ',') -cne 'creationTime,ownerSid,pid' -or
|
|
71
|
+
($identity.pid -isnot [long] -and $identity.pid -isnot [int]) -or
|
|
72
|
+
$identity.pid -le 0 -or
|
|
73
|
+
$identity.pid -gt [int]::MaxValue -or
|
|
74
|
+
$identity.ownerSid -isnot [string] -or
|
|
75
|
+
$identity.creationTime -isnot [string]) { throw 'Invalid captured identity shape.' }
|
|
76
|
+
$number = [int]$identity.pid
|
|
77
|
+
if ($seen.ContainsKey($number) -or
|
|
78
|
+
$identity.ownerSid -cne $sid -or
|
|
79
|
+
$identity.creationTime -cnotmatch '^[1-9][0-9]{0,18}$') {
|
|
80
|
+
throw 'Invalid original-user captured identity.'
|
|
81
|
+
}
|
|
82
|
+
$null = [DateTime]::FromFileTimeUtc([long]$identity.creationTime)
|
|
83
|
+
$seen[$number] = $true
|
|
84
|
+
}
|
|
85
|
+
$query = {
|
|
86
|
+
param([string]$Filter)
|
|
87
|
+
$ErrorActionPreference = 'Stop'
|
|
88
|
+
Import-Module CimCmdlets -ErrorAction Stop
|
|
89
|
+
$session = New-CimSession -ErrorAction Stop
|
|
90
|
+
try {
|
|
91
|
+
foreach ($row in Get-CimInstance -CimSession $session -ClassName Win32_Process -Filter $Filter `
|
|
92
|
+
-Property ProcessId, SessionId, CreationDate -OperationTimeoutSec 2) {
|
|
93
|
+
[pscustomobject]@{
|
|
94
|
+
pid = [int]$row.ProcessId
|
|
95
|
+
sessionId = [int]$row.SessionId
|
|
96
|
+
creationTicks = $row.CreationDate.ToUniversalTime().Ticks
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} finally { Remove-CimSession -CimSession $session }
|
|
100
|
+
}
|
|
101
|
+
$observed = @{}
|
|
102
|
+
$pool = [RunspaceFactory]::CreateRunspacePool(1, 4)
|
|
103
|
+
$pool.Open()
|
|
104
|
+
try {
|
|
105
|
+
for ($offset = 0; $offset -lt $identities.Count; $offset += 128) {
|
|
106
|
+
$pending = @()
|
|
107
|
+
try {
|
|
108
|
+
for ($index = $offset; $index -lt [Math]::Min($offset + 128, $identities.Count); $index += 32) {
|
|
109
|
+
if ($watch.ElapsedMilliseconds -ge 13000) { throw 'Worker session query deadline.' }
|
|
110
|
+
$batch = @($identities[$index..([Math]::Min($index + 31, $identities.Count - 1))])
|
|
111
|
+
$expected = @{}
|
|
112
|
+
$clauses = foreach ($identity in $batch) {
|
|
113
|
+
$expected[[int]$identity.pid] = $true
|
|
114
|
+
'ProcessId=' + ([int]$identity.pid).ToString([Globalization.CultureInfo]::InvariantCulture)
|
|
115
|
+
}
|
|
116
|
+
$pipeline = [PowerShell]::Create()
|
|
117
|
+
$pipeline.RunspacePool = $pool
|
|
118
|
+
$null = $pipeline.AddScript($query.ToString()).AddArgument(($clauses -join ' OR '))
|
|
119
|
+
$pending += @{ Pipeline = $pipeline; Expected = $expected; Result = $pipeline.BeginInvoke() }
|
|
120
|
+
}
|
|
121
|
+
foreach ($item in $pending) {
|
|
122
|
+
while (-not $item.Result.IsCompleted) {
|
|
123
|
+
if ($watch.ElapsedMilliseconds -ge 13000) { throw 'Worker session query deadline.' }
|
|
124
|
+
Start-Sleep -Milliseconds 10
|
|
125
|
+
}
|
|
126
|
+
$rows = @($item.Pipeline.EndInvoke($item.Result))
|
|
127
|
+
if ($item.Pipeline.HadErrors -or
|
|
128
|
+
$rows.Count -ne $item.Expected.Count) { throw 'Worker session query did not return its exact captured set.' }
|
|
129
|
+
foreach ($row in $rows) {
|
|
130
|
+
$number = [int]$row.pid
|
|
131
|
+
if (-not $item.Expected.ContainsKey($number) -or
|
|
132
|
+
$observed.ContainsKey($number)) { throw 'Worker session query returned an unexpected or duplicate identity.' }
|
|
133
|
+
$observed[$number] = $row
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
foreach ($item in $pending) {
|
|
138
|
+
if (-not $item.Result.IsCompleted) { $item.Pipeline.Stop() }
|
|
139
|
+
$item.Pipeline.Dispose()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} finally { $pool.Close(); $pool.Dispose() }
|
|
144
|
+
foreach ($identity in $identities) {
|
|
145
|
+
$matching = $observed[[int]$identity.pid]
|
|
146
|
+
if ($null -eq $matching -or
|
|
147
|
+
$matching.sessionId -ne $request.sessionId) { throw 'Captured generation/session changed; replan before stop.' }
|
|
148
|
+
$actual = $matching.creationTicks
|
|
149
|
+
$expected = [DateTime]::FromFileTimeUtc([long]$identity.creationTime).Ticks
|
|
150
|
+
# CIM creation dates have microsecond precision; this read never authorizes termination.
|
|
151
|
+
if ([decimal]::Truncate([decimal]$actual / 10) -ne [decimal]::Truncate([decimal]$expected / 10)) {
|
|
152
|
+
throw 'Captured process birth changed; replan before stop.'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
'{"verified":true}'
|