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.
@@ -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'], { windowsHide: true, timeout: ACL_TIMEOUT_MS })
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'], { windowsHide: true, timeout: ACL_TIMEOUT_MS })
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 owner = await windowsOwnerSid(run)
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
- await run('icacls.exe', [path, '/inheritance:r', '/grant:r', '*' + owner + ':' + flags,
94
- '*' + SYSTEM_SID + ':' + flags], { windowsHide: true, timeout: ACL_TIMEOUT_MS })
95
- const { entries } = windowsAclEntries(await readWindowsAcl(path, run))
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('icacls.exe', [path, entry.type === 'D' ? '/remove:d' : '/remove:g', '*' + sid],
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 owner = await windowsOwnerSid(run)
134
- assertRestrictedWindowsAcl(await readWindowsAcl(path, run), owner)
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'], { env: environment, windowsHide: true, timeout: LOOKUP_TIMEOUT_MS })
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
@@ -26,5 +26,5 @@
26
26
  "url": "https://github.com/88208555/MergeGuard-clitax.git"
27
27
  },
28
28
  "type": "module",
29
- "version": "7.0.40"
29
+ "version": "7.0.41"
30
30
  }
package/skill/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: '智能合并守卫:快照分支+预演+验证式合并+规则衰
5
5
 
6
6
  # MergeGuard
7
7
 
8
- Package version: v7.0.40
8
+ Package version: v7.0.41
9
9
 
10
10
  当前实现是“规则编译与守门协议”,不是可直接操作 git 的合并器。任何仓库写入、快照、合并或回滚都必须交给真实 local runner;远程纯运行时一律 fail-closed。
11
11
 
package/skill/skill.json CHANGED
@@ -5,6 +5,6 @@
5
5
  "method": "POST",
6
6
  "name": "mergeguard",
7
7
  "type": "Skill",
8
- "version": "v7.0.40",
8
+ "version": "v7.0.41",
9
9
  "schemaVersion": "mergeguard.skill.request/1.0"
10
10
  }