cli-mergeguard 7.0.40 → 7.0.41
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/broker-account-storage.mjs +30 -13
- package/official-skill-update.mjs +3 -2
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/skill/skill.json +1 -1
|
@@ -13,6 +13,18 @@ const ACL_SID_ALIASES = Object.freeze({ SY: SYSTEM_SID, WD: 'S-1-1-0', BA: 'S-1-
|
|
|
13
13
|
BU: 'S-1-5-32-545', AU: 'S-1-5-11', CO: 'S-1-3-0', CG: 'S-1-3-1', AN: 'S-1-5-7' })
|
|
14
14
|
const SID_PATTERN = /^S-1-(?:[0-9]+-)*[0-9]+$/
|
|
15
15
|
const ACL_TIMEOUT_MS = 15_000
|
|
16
|
+
const WINDOWS_SYSTEM_EXECUTABLES = new Set(['whoami.exe', 'icacls.exe', 'where.exe'])
|
|
17
|
+
|
|
18
|
+
export function windowsSystemExecutable(name, environment = process.env) {
|
|
19
|
+
if (!WINDOWS_SYSTEM_EXECUTABLES.has(name)) throw new Error('Unsupported Windows system executable')
|
|
20
|
+
const root = environment.SystemRoot
|
|
21
|
+
if (typeof root !== 'string' || !/^[A-Za-z]:[\\/]/.test(root)
|
|
22
|
+
|| /[\u0000-\u001f"<>|?*]/.test(root) || root.slice(2).includes(':')
|
|
23
|
+
|| root.split(/[\\/]/).includes('..')) {
|
|
24
|
+
throw new Error('SystemRoot must identify an absolute Windows installation directory')
|
|
25
|
+
}
|
|
26
|
+
return win32.join(root, 'System32', name)
|
|
27
|
+
}
|
|
16
28
|
|
|
17
29
|
export function currentAccountHome() {
|
|
18
30
|
const home = userInfo().homedir
|
|
@@ -69,17 +81,19 @@ export function assertRestrictedWindowsAcl(text, ownerSid) {
|
|
|
69
81
|
if (expected.size) throw new Error('Windows account ACL is missing the user or SYSTEM')
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
async function windowsOwnerSid(run) {
|
|
73
|
-
const result = await run('whoami.exe', ['/user', '/fo', 'csv', '/nh'],
|
|
84
|
+
async function windowsOwnerSid(run, environment) {
|
|
85
|
+
const result = await run(windowsSystemExecutable('whoami.exe', environment), ['/user', '/fo', 'csv', '/nh'],
|
|
86
|
+
{ shell: false, windowsHide: true, timeout: ACL_TIMEOUT_MS })
|
|
74
87
|
const candidates = result.stdout.match(/S-1-(?:[0-9]+-)*[0-9]+/g)
|
|
75
88
|
if (candidates === null || candidates.length !== 1 || !SID_PATTERN.test(candidates[0])) throw new Error('Current Windows account SID could not be verified')
|
|
76
89
|
return candidates[0]
|
|
77
90
|
}
|
|
78
91
|
|
|
79
|
-
async function readWindowsAcl(path, run) {
|
|
92
|
+
async function readWindowsAcl(path, run, environment) {
|
|
80
93
|
const temporary = join(dirname(path), '.acl-' + randomUUID() + '.txt')
|
|
81
94
|
try {
|
|
82
|
-
await run('icacls.exe', [path, '/save', temporary, '/q'],
|
|
95
|
+
await run(windowsSystemExecutable('icacls.exe', environment), [path, '/save', temporary, '/q'],
|
|
96
|
+
{ shell: false, windowsHide: true, timeout: ACL_TIMEOUT_MS })
|
|
83
97
|
return aclText(await readFile(temporary))
|
|
84
98
|
} finally {
|
|
85
99
|
try { await rm(temporary) } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
@@ -88,19 +102,21 @@ async function readWindowsAcl(path, run) {
|
|
|
88
102
|
|
|
89
103
|
async function protectWindowsPath(path, directory, dependencies) {
|
|
90
104
|
const run = dependencies.execFile === undefined ? runFile : dependencies.execFile
|
|
91
|
-
const
|
|
105
|
+
const environment = dependencies.environment === undefined ? process.env : dependencies.environment
|
|
106
|
+
const owner = await windowsOwnerSid(run, environment)
|
|
92
107
|
const flags = directory ? '(OI)(CI)F' : 'F'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
const icacls = windowsSystemExecutable('icacls.exe', environment)
|
|
109
|
+
await run(icacls, [path, '/inheritance:r', '/grant:r', '*' + owner + ':' + flags,
|
|
110
|
+
'*' + SYSTEM_SID + ':' + flags], { shell: false, windowsHide: true, timeout: ACL_TIMEOUT_MS })
|
|
111
|
+
const { entries } = windowsAclEntries(await readWindowsAcl(path, run, environment))
|
|
96
112
|
for (const entry of entries) {
|
|
97
113
|
const sid = Object.hasOwn(ACL_SID_ALIASES, entry.sid) ? ACL_SID_ALIASES[entry.sid] : entry.sid
|
|
98
114
|
if (entry.type === 'A' && [owner, SYSTEM_SID].includes(sid)) continue
|
|
99
115
|
if (!SID_PATTERN.test(sid)) throw new Error('Unexpected Windows ACL trustee')
|
|
100
|
-
await run(
|
|
101
|
-
{ windowsHide: true, timeout: ACL_TIMEOUT_MS })
|
|
116
|
+
await run(icacls, [path, entry.type === 'D' ? '/remove:d' : '/remove:g', '*' + sid],
|
|
117
|
+
{ shell: false, windowsHide: true, timeout: ACL_TIMEOUT_MS })
|
|
102
118
|
}
|
|
103
|
-
assertRestrictedWindowsAcl(await readWindowsAcl(path, run), owner)
|
|
119
|
+
assertRestrictedWindowsAcl(await readWindowsAcl(path, run, environment), owner)
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
export async function protectAccountPath(path, directory, dependencies = {}) {
|
|
@@ -130,6 +146,7 @@ export async function verifyAccountPath(path, dependencies = {}) {
|
|
|
130
146
|
return
|
|
131
147
|
}
|
|
132
148
|
const run = dependencies.execFile === undefined ? runFile : dependencies.execFile
|
|
133
|
-
const
|
|
134
|
-
|
|
149
|
+
const environment = dependencies.environment === undefined ? process.env : dependencies.environment
|
|
150
|
+
const owner = await windowsOwnerSid(run, environment)
|
|
151
|
+
assertRestrictedWindowsAcl(await readWindowsAcl(path, run, environment), owner)
|
|
135
152
|
}
|
|
@@ -3,7 +3,7 @@ import { execFile, spawn } from 'node:child_process'
|
|
|
3
3
|
import { promisify } from 'node:util'
|
|
4
4
|
import { lstat, mkdtemp, readFile, rename, rm } from 'node:fs/promises'
|
|
5
5
|
import { join, win32 } from 'node:path'
|
|
6
|
-
import { assertAccountAncestors, currentAccountHome, ensureAccountDirectory } from './broker-account-storage.mjs'
|
|
6
|
+
import { assertAccountAncestors, currentAccountHome, ensureAccountDirectory, windowsSystemExecutable } from './broker-account-storage.mjs'
|
|
7
7
|
import { pathToFileURL } from 'node:url'
|
|
8
8
|
import { accountBrokerDirectory } from './broker-credentials.mjs'
|
|
9
9
|
import { createBrokerTransport } from './broker-transport.mjs'
|
|
@@ -70,7 +70,8 @@ export function windowsNpmEntry(output) {
|
|
|
70
70
|
|
|
71
71
|
async function npmInvocation(environment) {
|
|
72
72
|
if (process.platform !== 'win32') return { executable: 'npm', args: [] }
|
|
73
|
-
const found = await runFile('where.exe', ['npm'],
|
|
73
|
+
const found = await runFile(windowsSystemExecutable('where.exe', environment), ['npm'],
|
|
74
|
+
{ env: environment, shell: false, windowsHide: true, timeout: LOOKUP_TIMEOUT_MS })
|
|
74
75
|
const entry = windowsNpmEntry(found.stdout)
|
|
75
76
|
for (const path of [entry.command, entry.script]) {
|
|
76
77
|
await assertAccountAncestors(win32.dirname(path), 'win32')
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED