@triflux/remote 10.78.0 → 10.80.0
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/hub/team/native-peer.mjs
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
assertWindowsAcl,
|
|
9
|
+
readWindowsAcls,
|
|
10
|
+
readWindowsPeerState,
|
|
11
|
+
} from "../../scripts/lib/native-peer-windows.mjs";
|
|
7
12
|
import { JsonRpcWsUdsClient } from "../workers/lib/jsonrpc-ws-uds.mjs";
|
|
8
13
|
|
|
9
14
|
const WAKE =
|
|
@@ -103,9 +108,23 @@ export async function sendCodexPeer({ client, threadId, message }) {
|
|
|
103
108
|
export function assertPrivateFile(
|
|
104
109
|
file,
|
|
105
110
|
kind,
|
|
106
|
-
{
|
|
111
|
+
{
|
|
112
|
+
socket = false,
|
|
113
|
+
privateMode = true,
|
|
114
|
+
platform = process.platform,
|
|
115
|
+
fs: io = fs,
|
|
116
|
+
acls,
|
|
117
|
+
} = {},
|
|
107
118
|
) {
|
|
108
|
-
const meta =
|
|
119
|
+
const meta = io.lstatSync(file);
|
|
120
|
+
if (platform === "win32") {
|
|
121
|
+
if (meta.isSymbolicLink() || !meta.isFile())
|
|
122
|
+
throw new Error(`Unsafe ${kind}`);
|
|
123
|
+
assertWindowsAcl((acls || readWindowsAcls([file])).get(file), kind, {
|
|
124
|
+
privateMode,
|
|
125
|
+
});
|
|
126
|
+
return meta;
|
|
127
|
+
}
|
|
109
128
|
if (
|
|
110
129
|
meta.isSymbolicLink() ||
|
|
111
130
|
meta.uid !== process.getuid() ||
|
|
@@ -133,8 +152,11 @@ export async function deliverCodex({
|
|
|
133
152
|
message,
|
|
134
153
|
home = os.homedir(),
|
|
135
154
|
env = process.env,
|
|
155
|
+
platform = process.platform,
|
|
136
156
|
clientFactory = (socketPath) => new JsonRpcWsUdsClient({ socketPath }),
|
|
137
157
|
}) {
|
|
158
|
+
if (platform === "win32")
|
|
159
|
+
throw new Error("Windows 에서는 Codex 대상을 아직 지원하지 않는다");
|
|
138
160
|
const client = clientFactory(resolveCodexSocket(home, env));
|
|
139
161
|
try {
|
|
140
162
|
await client.connect();
|
|
@@ -150,6 +172,8 @@ export async function deliverCodex({
|
|
|
150
172
|
}
|
|
151
173
|
|
|
152
174
|
function actualProcStart(pid) {
|
|
175
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
176
|
+
throw new Error("Invalid process id");
|
|
153
177
|
return execFileSync("/bin/ps", ["-p", String(pid), "-o", "lstart="], {
|
|
154
178
|
encoding: "utf8",
|
|
155
179
|
timeout: 2000,
|
|
@@ -159,26 +183,79 @@ function actualProcStart(pid) {
|
|
|
159
183
|
.replace(/\s+/g, " ");
|
|
160
184
|
}
|
|
161
185
|
|
|
186
|
+
function readClaudeRegistry({ home, platform, fs: io, readWindowsState }) {
|
|
187
|
+
const paths = platform === "win32" ? path.win32 : path;
|
|
188
|
+
const dir = paths.join(home, ".claude", "sessions");
|
|
189
|
+
const names = io.readdirSync(dir);
|
|
190
|
+
let acls, procStarts;
|
|
191
|
+
if (platform === "win32") {
|
|
192
|
+
const parents = [home, paths.join(home, ".claude"), dir];
|
|
193
|
+
({ acls, procStarts } = readWindowsState(
|
|
194
|
+
[
|
|
195
|
+
...parents,
|
|
196
|
+
...names
|
|
197
|
+
.filter(
|
|
198
|
+
(name) => /^\d+\.json$/.test(name) || /^\d+\..*\.key$/.test(name),
|
|
199
|
+
)
|
|
200
|
+
.map((name) => paths.join(dir, name)),
|
|
201
|
+
],
|
|
202
|
+
{
|
|
203
|
+
pids: names
|
|
204
|
+
.filter((name) => /^\d+\.json$/.test(name))
|
|
205
|
+
.map((name) => Number(name.slice(0, -5)))
|
|
206
|
+
.filter((pid) => Number.isSafeInteger(pid) && pid > 0),
|
|
207
|
+
},
|
|
208
|
+
));
|
|
209
|
+
for (const parent of parents) {
|
|
210
|
+
const meta = io.lstatSync(parent);
|
|
211
|
+
if (!meta.isDirectory() || meta.isSymbolicLink())
|
|
212
|
+
throw new Error("Unsafe Claude sessions directory");
|
|
213
|
+
assertWindowsAcl(acls.get(parent), "Claude sessions directory", {
|
|
214
|
+
privateMode: false,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const records = [];
|
|
219
|
+
for (const name of names.filter((name) => /^\d+\.json$/.test(name))) {
|
|
220
|
+
const file = paths.join(dir, name);
|
|
221
|
+
try {
|
|
222
|
+
assertPrivateFile(file, "Claude session", {
|
|
223
|
+
privateMode: false,
|
|
224
|
+
platform,
|
|
225
|
+
fs: io,
|
|
226
|
+
acls,
|
|
227
|
+
});
|
|
228
|
+
const record = JSON.parse(io.readFileSync(file, "utf8"));
|
|
229
|
+
if (
|
|
230
|
+
`${record.pid}.json` === name &&
|
|
231
|
+
typeof record.sessionId === "string" &&
|
|
232
|
+
record.sessionId
|
|
233
|
+
)
|
|
234
|
+
records.push(record);
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
return { dir, names, records, acls, procStarts };
|
|
238
|
+
}
|
|
239
|
+
|
|
162
240
|
export function resolveClaudeReceiver({
|
|
163
241
|
sessionId,
|
|
164
242
|
home = os.homedir(),
|
|
165
|
-
|
|
243
|
+
platform = process.platform,
|
|
244
|
+
hostname = os.hostname(),
|
|
245
|
+
getProcStart,
|
|
246
|
+
fs: io = fs,
|
|
247
|
+
readWindowsState = readWindowsPeerState,
|
|
248
|
+
registry = readClaudeRegistry({
|
|
249
|
+
home,
|
|
250
|
+
platform,
|
|
251
|
+
fs: io,
|
|
252
|
+
readWindowsState,
|
|
253
|
+
}),
|
|
166
254
|
}) {
|
|
167
|
-
const dir
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const file = path.join(dir, name);
|
|
172
|
-
let record;
|
|
173
|
-
try {
|
|
174
|
-
assertPrivateFile(file, "Claude session", { privateMode: false });
|
|
175
|
-
record = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
176
|
-
} catch {
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (record.sessionId === sessionId && `${record.pid}.json` === name)
|
|
180
|
-
matches.push(record);
|
|
181
|
-
}
|
|
255
|
+
const { dir, names, records, acls, procStarts } = registry;
|
|
256
|
+
getProcStart ??= (pid) =>
|
|
257
|
+
platform === "win32" ? procStarts.get(pid) : actualProcStart(pid);
|
|
258
|
+
const matches = records.filter((record) => record.sessionId === sessionId);
|
|
182
259
|
if (matches.length !== 1)
|
|
183
260
|
throw new Error("Claude session identity missing or ambiguous");
|
|
184
261
|
const record = matches[0];
|
|
@@ -187,30 +264,53 @@ export function resolveClaudeReceiver({
|
|
|
187
264
|
"Claude session is a parked parent; use its live continuation name",
|
|
188
265
|
);
|
|
189
266
|
const pid = Number(record.pid);
|
|
267
|
+
const windows = platform === "win32";
|
|
268
|
+
const normalizeStart = (start) =>
|
|
269
|
+
typeof start === "string"
|
|
270
|
+
? windows
|
|
271
|
+
? start
|
|
272
|
+
: start.replace(/\s+/g, " ")
|
|
273
|
+
: null;
|
|
274
|
+
const domainMatches = (domain) =>
|
|
275
|
+
windows
|
|
276
|
+
? typeof domain === "string" &&
|
|
277
|
+
domain.toLowerCase() === `win32:${hostname}`.toLowerCase()
|
|
278
|
+
: domain === platform;
|
|
190
279
|
if (
|
|
191
280
|
!Number.isSafeInteger(pid) ||
|
|
192
281
|
pid <= 0 ||
|
|
193
|
-
record.pidDomain
|
|
194
|
-
record.procStart
|
|
282
|
+
!domainMatches(record.pidDomain) ||
|
|
283
|
+
(windows && !/^\d+$/.test(record.procStart)) ||
|
|
284
|
+
normalizeStart(record.procStart) !== getProcStart(pid)
|
|
195
285
|
)
|
|
196
286
|
throw new Error("Claude process identity stale");
|
|
197
|
-
const keyFiles =
|
|
198
|
-
|
|
199
|
-
|
|
287
|
+
const keyFiles = names.filter((name) =>
|
|
288
|
+
windows
|
|
289
|
+
? new RegExp(`^${pid}\\.[0-9a-f]{64}\\.key$`).test(name)
|
|
290
|
+
: name.startsWith(`${pid}.`) && name.endsWith(".key"),
|
|
291
|
+
);
|
|
200
292
|
if (keyFiles.length !== 1)
|
|
201
293
|
throw new Error("Claude peer key missing or ambiguous");
|
|
202
|
-
const keyFile = path.join(dir, keyFiles[0]);
|
|
203
|
-
assertPrivateFile(keyFile, "Claude peer key");
|
|
204
|
-
const key = JSON.parse(
|
|
294
|
+
const keyFile = (windows ? path.win32 : path).join(dir, keyFiles[0]);
|
|
295
|
+
assertPrivateFile(keyFile, "Claude peer key", { platform, fs: io, acls });
|
|
296
|
+
const key = JSON.parse(io.readFileSync(keyFile, "utf8"));
|
|
205
297
|
if (
|
|
206
|
-
key.
|
|
207
|
-
record.procStart
|
|
208
|
-
key.pidDomain
|
|
298
|
+
normalizeStart(windows ? key.procStartFt : key.procStart) !==
|
|
299
|
+
normalizeStart(record.procStart) ||
|
|
300
|
+
!domainMatches(key.pidDomain) ||
|
|
209
301
|
typeof key.peerToken !== "string" ||
|
|
210
302
|
!key.peerToken
|
|
211
303
|
)
|
|
212
304
|
throw new Error("Claude peer key identity mismatch");
|
|
213
305
|
const socketPath = record.messagingSocketPath;
|
|
306
|
+
if (windows) {
|
|
307
|
+
if (
|
|
308
|
+
typeof socketPath !== "string" ||
|
|
309
|
+
!/^\\\\\.\\pipe\\LOCAL\\cc-msg-[0-9a-f]{32}$/.test(socketPath)
|
|
310
|
+
)
|
|
311
|
+
throw new Error("Invalid Claude peer socket");
|
|
312
|
+
return { socketPath, peerToken: key.peerToken };
|
|
313
|
+
}
|
|
214
314
|
if (
|
|
215
315
|
typeof socketPath !== "string" ||
|
|
216
316
|
!path.isAbsolute(socketPath) ||
|
|
@@ -223,19 +323,22 @@ export function resolveClaudeReceiver({
|
|
|
223
323
|
|
|
224
324
|
export function resolveClaudeRegistryTarget(
|
|
225
325
|
query,
|
|
226
|
-
{
|
|
326
|
+
{
|
|
327
|
+
home = os.homedir(),
|
|
328
|
+
platform = process.platform,
|
|
329
|
+
hostname = os.hostname(),
|
|
330
|
+
getProcStart,
|
|
331
|
+
fs: io = fs,
|
|
332
|
+
readWindowsState = readWindowsPeerState,
|
|
333
|
+
} = {},
|
|
227
334
|
) {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (`${record.pid}.json` === name && record.sessionId)
|
|
236
|
-
records.push(record);
|
|
237
|
-
} catch {}
|
|
238
|
-
}
|
|
335
|
+
const registry = readClaudeRegistry({
|
|
336
|
+
home,
|
|
337
|
+
platform,
|
|
338
|
+
fs: io,
|
|
339
|
+
readWindowsState,
|
|
340
|
+
});
|
|
341
|
+
const { records } = registry;
|
|
239
342
|
const exactId = records.filter(
|
|
240
343
|
(x) => x.sessionId.toLowerCase() === query.toLowerCase(),
|
|
241
344
|
);
|
|
@@ -254,35 +357,51 @@ export function resolveClaudeRegistryTarget(
|
|
|
254
357
|
(x) =>
|
|
255
358
|
!x.parkedJobId && (!parked.length || x.jobId === parked[0].parkedJobId),
|
|
256
359
|
);
|
|
257
|
-
const live =
|
|
360
|
+
const live = [];
|
|
361
|
+
for (const x of eligible) {
|
|
258
362
|
try {
|
|
259
|
-
resolveClaudeReceiver({
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
363
|
+
const receiver = resolveClaudeReceiver({
|
|
364
|
+
sessionId: x.sessionId,
|
|
365
|
+
home,
|
|
366
|
+
platform,
|
|
367
|
+
hostname,
|
|
368
|
+
getProcStart,
|
|
369
|
+
fs: io,
|
|
370
|
+
registry,
|
|
371
|
+
});
|
|
372
|
+
live.push({ ...x, receiver });
|
|
373
|
+
} catch {}
|
|
374
|
+
}
|
|
265
375
|
if (live.length !== 1)
|
|
266
376
|
throw new Error(
|
|
267
377
|
live.length
|
|
268
378
|
? "Ambiguous live Claude targets"
|
|
269
379
|
: "No live Claude continuation; parked parent is not a delivery target",
|
|
270
380
|
);
|
|
271
|
-
return {
|
|
381
|
+
return {
|
|
382
|
+
sessionId: live[0].sessionId,
|
|
383
|
+
name: live[0].name,
|
|
384
|
+
receiver: live[0].receiver,
|
|
385
|
+
};
|
|
272
386
|
}
|
|
273
387
|
|
|
274
388
|
export async function deliverClaude({
|
|
275
389
|
sessionId,
|
|
276
390
|
message,
|
|
277
391
|
home = os.homedir(),
|
|
278
|
-
|
|
392
|
+
platform = process.platform,
|
|
393
|
+
receiver = resolveClaudeReceiver({ sessionId, home, platform }),
|
|
394
|
+
net: transport = net,
|
|
279
395
|
socketDir = path.dirname(receiver.socketPath),
|
|
280
396
|
// 150ms 유지 후 리드의 peer 수신·응답을 확인해 8초 고정 대기를 줄였다.
|
|
281
397
|
holdMs = 150,
|
|
282
398
|
}) {
|
|
283
|
-
const own =
|
|
399
|
+
const own =
|
|
400
|
+
platform === "win32"
|
|
401
|
+
? `\\\\.\\pipe\\LOCAL\\cc-msg-${randomBytes(16).toString("hex")}`
|
|
402
|
+
: path.join(socketDir, `${process.pid}.sock`);
|
|
284
403
|
// 이 일회성 송신자는 회신을 라우팅하지 않는다. 열린 reply 소켓을 즉시 닫는다.
|
|
285
|
-
const server =
|
|
404
|
+
const server = transport.createServer((connection) => connection.destroy());
|
|
286
405
|
await new Promise((resolve, reject) => {
|
|
287
406
|
server.once("error", reject);
|
|
288
407
|
server.listen(own, () => {
|
|
@@ -290,8 +409,8 @@ export async function deliverClaude({
|
|
|
290
409
|
resolve();
|
|
291
410
|
});
|
|
292
411
|
});
|
|
293
|
-
const inode = fs.lstatSync(own).ino;
|
|
294
|
-
fs.chmodSync(own, 0o600);
|
|
412
|
+
const inode = platform === "win32" ? null : fs.lstatSync(own).ino;
|
|
413
|
+
if (platform !== "win32") fs.chmodSync(own, 0o600);
|
|
295
414
|
const from = `uds:${own}`;
|
|
296
415
|
const safe = message.replaceAll("&", "&").replaceAll("<", "<");
|
|
297
416
|
const frame = {
|
|
@@ -307,7 +426,7 @@ export async function deliverClaude({
|
|
|
307
426
|
try {
|
|
308
427
|
try {
|
|
309
428
|
await new Promise((resolve, reject) => {
|
|
310
|
-
const conn =
|
|
429
|
+
const conn = transport.connect(receiver.socketPath);
|
|
311
430
|
conn.setTimeout(5000, () =>
|
|
312
431
|
conn.destroy(new Error("Claude peer connect timed out")),
|
|
313
432
|
);
|
|
@@ -338,7 +457,8 @@ export async function deliverClaude({
|
|
|
338
457
|
} finally {
|
|
339
458
|
server.close();
|
|
340
459
|
try {
|
|
341
|
-
if (fs.lstatSync(own).ino === inode)
|
|
460
|
+
if (platform !== "win32" && fs.lstatSync(own).ino === inode)
|
|
461
|
+
fs.unlinkSync(own);
|
|
342
462
|
} catch {}
|
|
343
463
|
}
|
|
344
464
|
}
|
package/package.json
CHANGED
|
@@ -125,6 +125,7 @@ function trustedPackageTree(packageRoot, nodePath, tmuxPath) {
|
|
|
125
125
|
const meta = existing(current);
|
|
126
126
|
if (
|
|
127
127
|
!meta ||
|
|
128
|
+
path.basename(current).toLowerCase() === ".git" ||
|
|
128
129
|
meta.isSymbolicLink() ||
|
|
129
130
|
![0, process.getuid()].includes(meta.uid) ||
|
|
130
131
|
(meta.mode & 0o002) !== 0
|
|
@@ -232,8 +233,8 @@ export function inspectPeerPermission({
|
|
|
232
233
|
tmuxPath,
|
|
233
234
|
platform = process.platform,
|
|
234
235
|
} = {}) {
|
|
236
|
+
if (platform === "win32") return { status: "unsupported" };
|
|
235
237
|
const plan = peerPermissionPlan({ home, packageRoot, nodePath, tmuxPath });
|
|
236
|
-
if (platform === "win32") return { status: "unsupported", plan };
|
|
237
238
|
const effectiveNodePath = nodePath || resolveStableNodeBin();
|
|
238
239
|
if (
|
|
239
240
|
!trustedPackageTree(
|
|
@@ -334,12 +335,9 @@ export function installPeerPermission({
|
|
|
334
335
|
platform,
|
|
335
336
|
});
|
|
336
337
|
const { plan, status } = inspected;
|
|
338
|
+
if (status === "unsupported") return { status, changed: false };
|
|
337
339
|
if (status === "approved") return { status: "approved", changed: false };
|
|
338
|
-
if (
|
|
339
|
-
status === "conflict" ||
|
|
340
|
-
status === "unavailable" ||
|
|
341
|
-
status === "unsupported"
|
|
342
|
-
)
|
|
340
|
+
if (status === "conflict" || status === "unavailable")
|
|
343
341
|
return {
|
|
344
342
|
status,
|
|
345
343
|
changed: false,
|
|
@@ -502,8 +500,10 @@ export async function offerPeerPermission({
|
|
|
502
500
|
tmuxPath,
|
|
503
501
|
platform,
|
|
504
502
|
});
|
|
505
|
-
if (state.status === "unsupported")
|
|
503
|
+
if (state.status === "unsupported") {
|
|
504
|
+
notice("Windows 는 자동 승인 없이 tfx-peer 를 직접 실행한다");
|
|
506
505
|
return { status: "unsupported", changed: false };
|
|
506
|
+
}
|
|
507
507
|
if (state.status === "approved")
|
|
508
508
|
return { status: "approved", changed: false };
|
|
509
509
|
if (fromUpdate) {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const WINDOWS_POWERSHELL = String.raw`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`;
|
|
4
|
+
|
|
5
|
+
function windowsPowerShellEnv() {
|
|
6
|
+
// PowerShell 7의 모듈 경로를 빼서 Windows PowerShell 5.1이 기본값을 복원하게 한다.
|
|
7
|
+
return Object.fromEntries(
|
|
8
|
+
Object.entries(process.env).filter(([key]) =>
|
|
9
|
+
["systemroot", "windir", "temp", "tmp", "userprofile"].includes(
|
|
10
|
+
key.toLowerCase(),
|
|
11
|
+
),
|
|
12
|
+
),
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readWindowsPeerState(
|
|
17
|
+
files,
|
|
18
|
+
{ pids = [], execFile = execFileSync } = {},
|
|
19
|
+
) {
|
|
20
|
+
if (!files.length && !pids.length)
|
|
21
|
+
return { acls: new Map(), procStarts: new Map() };
|
|
22
|
+
const script = `
|
|
23
|
+
$ErrorActionPreference = 'Stop'
|
|
24
|
+
[Console]::InputEncoding = [Text.UTF8Encoding]::new($false)
|
|
25
|
+
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
|
|
26
|
+
$request = ConvertFrom-Json ([Console]::In.ReadToEnd())
|
|
27
|
+
$userSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
28
|
+
$acls = @(foreach ($p in $request.paths) {
|
|
29
|
+
try {
|
|
30
|
+
$item = if ([System.IO.Directory]::Exists($p)) { [System.IO.DirectoryInfo]::new($p) } else { [System.IO.FileInfo]::new($p) }
|
|
31
|
+
$acl = $item.GetAccessControl()
|
|
32
|
+
[pscustomobject]@{
|
|
33
|
+
path = $p
|
|
34
|
+
userSid = $userSid
|
|
35
|
+
ownerSid = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
|
|
36
|
+
reparsePoint = [bool]($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
|
|
37
|
+
access = @(foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
|
|
38
|
+
[pscustomobject]@{
|
|
39
|
+
sid = $rule.IdentityReference.Value
|
|
40
|
+
rights = [long]$rule.FileSystemRights
|
|
41
|
+
type = $rule.AccessControlType.ToString()
|
|
42
|
+
inheritanceFlags = [int]$rule.InheritanceFlags
|
|
43
|
+
propagationFlags = [int]$rule.PropagationFlags
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
[pscustomobject]@{ path = $p; error = $_.Exception.Message }
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
$processes = @(foreach ($processId in $request.pids) {
|
|
52
|
+
try {
|
|
53
|
+
$start = [System.Diagnostics.Process]::GetProcessById($processId).StartTime.ToFileTimeUtc().ToString([Globalization.CultureInfo]::InvariantCulture)
|
|
54
|
+
[pscustomobject]@{ pid = $processId; procStart = $start }
|
|
55
|
+
} catch {
|
|
56
|
+
[pscustomobject]@{ pid = $processId; error = $_.Exception.Message }
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
ConvertTo-Json -InputObject @{ acls = $acls; processes = $processes } -Depth 5 -Compress
|
|
60
|
+
`;
|
|
61
|
+
const rows = JSON.parse(
|
|
62
|
+
execFile(
|
|
63
|
+
WINDOWS_POWERSHELL,
|
|
64
|
+
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
65
|
+
{
|
|
66
|
+
input: JSON.stringify({ paths: files, pids }),
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
timeout: 10000,
|
|
69
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
70
|
+
windowsHide: true,
|
|
71
|
+
env: windowsPowerShellEnv(),
|
|
72
|
+
},
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
if (!Array.isArray(rows.acls) || !Array.isArray(rows.processes))
|
|
76
|
+
throw new Error("Invalid Windows peer response");
|
|
77
|
+
return {
|
|
78
|
+
acls: new Map(
|
|
79
|
+
rows.acls
|
|
80
|
+
.filter((row) => !Object.hasOwn(row, "error"))
|
|
81
|
+
.map((row) => [row.path, row]),
|
|
82
|
+
),
|
|
83
|
+
procStarts: new Map(
|
|
84
|
+
rows.processes
|
|
85
|
+
.filter(
|
|
86
|
+
(row) =>
|
|
87
|
+
!Object.hasOwn(row, "error") &&
|
|
88
|
+
typeof row.procStart === "string" &&
|
|
89
|
+
/^\d+$/.test(row.procStart),
|
|
90
|
+
)
|
|
91
|
+
.map((row) => [row.pid, row.procStart]),
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function readWindowsAcls(files, options) {
|
|
97
|
+
return readWindowsPeerState(files, options).acls;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function assertWindowsAcl(acl, kind, { privateMode = true } = {}) {
|
|
101
|
+
const trusted = new Set([acl?.userSid, "S-1-5-18", "S-1-5-32-544"]);
|
|
102
|
+
// 개별 쓰기·삭제·ACL 변경과 generic write/all을 함께 거른다.
|
|
103
|
+
const writes =
|
|
104
|
+
0x2 |
|
|
105
|
+
0x4 |
|
|
106
|
+
0x10 |
|
|
107
|
+
0x40 |
|
|
108
|
+
0x100 |
|
|
109
|
+
0x10000 |
|
|
110
|
+
0x40000 |
|
|
111
|
+
0x80000 |
|
|
112
|
+
0x40000000 |
|
|
113
|
+
0x10000000;
|
|
114
|
+
if (
|
|
115
|
+
!/^S-1-\d+(?:-\d+)+$/.test(acl?.userSid || "") ||
|
|
116
|
+
!trusted.has(acl.ownerSid) ||
|
|
117
|
+
acl.reparsePoint !== false ||
|
|
118
|
+
!Array.isArray(acl.access) ||
|
|
119
|
+
!acl.access.length ||
|
|
120
|
+
acl.access.some(
|
|
121
|
+
(ace) =>
|
|
122
|
+
!/^S-1-\d+(?:-\d+)+$/.test(ace.sid || "") ||
|
|
123
|
+
!["Allow", "Deny"].includes(ace.type) ||
|
|
124
|
+
!Number.isInteger(ace.rights) ||
|
|
125
|
+
ace.rights < -2147483648 ||
|
|
126
|
+
ace.rights > 0xffffffff ||
|
|
127
|
+
(ace.type === "Allow" &&
|
|
128
|
+
!(
|
|
129
|
+
Number.isInteger(ace.propagationFlags) &&
|
|
130
|
+
(ace.propagationFlags & 2) !== 0
|
|
131
|
+
) &&
|
|
132
|
+
!trusted.has(ace.sid) &&
|
|
133
|
+
(privateMode || (ace.rights & writes) !== 0)),
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
throw new Error(`Unsafe ${kind}`);
|
|
137
|
+
}
|