@foldkit/devtools-mcp 0.24.1 → 0.25.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/README.md +14 -12
- package/dist/relayClient.d.ts +37 -0
- package/dist/relayClient.d.ts.map +1 -0
- package/dist/relayClient.js +276 -0
- package/dist/relayLocation.d.ts +9 -1
- package/dist/relayLocation.d.ts.map +1 -1
- package/dist/relayLocation.js +57 -19
- package/dist/relayRegistry.d.ts +12 -2
- package/dist/relayRegistry.d.ts.map +1 -1
- package/dist/relayRegistry.js +74 -14
- package/dist/relayRegistryTrust.d.ts +19 -0
- package/dist/relayRegistryTrust.d.ts.map +1 -0
- package/dist/relayRegistryTrust.js +456 -0
- package/dist/server.js +6383 -1375
- package/dist/tools.d.ts +2 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +31 -50
- package/package.json +4 -4
- package/dist/webSocketClient.d.ts +0 -28
- package/dist/webSocketClient.d.ts.map +0 -1
- package/dist/webSocketClient.js +0 -134
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Effect, FileSystem, Option } from 'effect';
|
|
2
|
+
import { ChildProcessSpawner } from 'effect/process';
|
|
3
|
+
export type RelayRegistryInspection = Readonly<{
|
|
4
|
+
directory: string;
|
|
5
|
+
maybeRefusal: Option.Option<string>;
|
|
6
|
+
}>;
|
|
7
|
+
export type RelayRegistryTrust = Readonly<{
|
|
8
|
+
inspect: (directory: string) => Effect.Effect<RelayRegistryInspection, never, ChildProcessSpawner.ChildProcessSpawner>;
|
|
9
|
+
}>;
|
|
10
|
+
type DirectoryInfo = Pick<FileSystem.File.Info, 'mode' | 'uid'>;
|
|
11
|
+
export declare const relayRegistryDirectoryRefusal: (info: DirectoryInfo, maybeCurrentUid: Option.Option<number>) => Option.Option<string>;
|
|
12
|
+
export declare const windowsDirectoryRefusal: (sddl: string, currentUserSid: string) => Option.Option<string>;
|
|
13
|
+
export declare const windowsDirectoryChainRefusal: (descriptors: ReadonlyArray<string>, currentUserSid: string) => Option.Option<string>;
|
|
14
|
+
export declare const powershellPath: (systemRoot: string) => string;
|
|
15
|
+
export declare const windowsTempRootRefusal: (directory: string, tempRoot: string) => Option.Option<string>;
|
|
16
|
+
export declare const probeWindowsDirectory: (maybeExecutable: Option.Option<string>, directory: string) => Effect.Effect<Option.Option<string>, never, ChildProcessSpawner.ChildProcessSpawner>;
|
|
17
|
+
export declare const relayRegistryTrust: RelayRegistryTrust;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=relayRegistryTrust.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relayRegistryTrust.d.ts","sourceRoot":"","sources":["../src/relayRegistryTrust.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,MAAM,EACN,UAAU,EACV,MAAM,EAIP,MAAM,QAAQ,CAAA;AACf,OAAO,EAAgB,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAyElE,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CACpC,CAAC,CAAA;AAEF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,OAAO,EAAE,CACP,SAAS,EAAE,MAAM,KACd,MAAM,CAAC,MAAM,CAChB,uBAAuB,EACvB,KAAK,EACL,mBAAmB,CAAC,mBAAmB,CACxC,CAAA;CACF,CAAC,CAAA;AAEF,KAAK,aAAa,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,CAAA;AAE/D,eAAO,MAAM,6BAA6B,SAClC,aAAa,mBACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KACrC,MAAM,CAAC,MAAM,CAAC,MAAM,CAkBnB,CAAA;AA2OJ,eAAO,MAAM,uBAAuB,SAC5B,MAAM,kBACI,MAAM,KACrB,MAAM,CAAC,MAAM,CAAC,MAAM,CAwCtB,CAAA;AAuFD,eAAO,MAAM,4BAA4B,gBAC1B,aAAa,CAAC,MAAM,CAAC,kBAClB,MAAM,KACrB,MAAM,CAAC,MAAM,CAAC,MAAM,CAwEtB,CAAA;AAED,eAAO,MAAM,cAAc,eAAgB,MAAM,KAAG,MAOjD,CAAA;AAEH,eAAO,MAAM,sBAAsB,cACtB,MAAM,YACP,MAAM,KACf,MAAM,CAAC,MAAM,CAAC,MAAM,CAGgB,CAAA;AAuHvC,eAAO,MAAM,qBAAqB,oBACf,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,aAC3B,MAAM,KAChB,MAAM,CAAC,MAAM,CACd,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACrB,KAAK,EACL,mBAAmB,CAAC,mBAAmB,CAQrC,CAAA;AA+BJ,eAAO,MAAM,kBAAkB,EAAE,kBAgFhC,CAAA"}
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { Array, Data, Duration, Effect, Option, Stream, String, pipe, } from 'effect';
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
3
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, win32 } from 'node:path';
|
|
6
|
+
const PERMISSIONS_BEYOND_OWNER = 0o077;
|
|
7
|
+
const PARENT_WRITE_PERMISSIONS = 0o022;
|
|
8
|
+
const STICKY_DIRECTORY = 0o1000;
|
|
9
|
+
const ROOT_UID = 0;
|
|
10
|
+
const PROBE_TIMEOUT = Duration.seconds(10);
|
|
11
|
+
const PROBED_DIRECTORY_VARIABLE = 'FOLDKIT_DEVTOOLS_PROBED_DIRECTORY';
|
|
12
|
+
const DARWIN_ACL_ENTRY_PATTERN = /^\s*\d+:\s+.+\s(allow|deny)\s+\S+/;
|
|
13
|
+
const DARWIN_DIRECTORY_MODE_PATTERN = /^d[rwxstST-]{9}[+@ ]/;
|
|
14
|
+
const PROBE_SCRIPT = `$sections = [System.Security.AccessControl.AccessControlSections]::Owner -bor [System.Security.AccessControl.AccessControlSections]::Access; $directories = [System.Collections.Generic.List[string]]::new(); $directory = [System.IO.DirectoryInfo]::new($env:${PROBED_DIRECTORY_VARIABLE}); while ($null -ne $directory) { $directories.Insert(0, $directory.FullName); $directory = $directory.Parent }; foreach ($path in $directories) { $attributes = [System.IO.File]::GetAttributes($path); if (($attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { 'REPARSE_POINT' } else { 'DIRECTORY' }; ([System.IO.Directory]::GetAccessControl($path)).GetSecurityDescriptorSddlForm($sections) }; & "$env:SystemRoot\\System32\\whoami.exe" /user /fo csv /nh`;
|
|
15
|
+
const UNVERIFIED_OWNERSHIP = 'has ownership that cannot be verified';
|
|
16
|
+
const UNVERIFIED_PERMISSIONS = 'has permissions that cannot be verified';
|
|
17
|
+
const OWNED_BY_ANOTHER_USER = 'is owned by another user';
|
|
18
|
+
const SHARED_WITH_OTHER_USERS = 'is readable or writable by other users';
|
|
19
|
+
const NOT_REGULAR_DIRECTORY = 'is not a regular directory';
|
|
20
|
+
const REPLACEABLE_PARENT = 'has a parent directory that another user can replace';
|
|
21
|
+
const REPARSE_PARENT = 'has a reparse-point directory in its path';
|
|
22
|
+
const OUTSIDE_WINDOWS_TEMP = 'is outside the Windows user temporary directory';
|
|
23
|
+
const SECURITY_DESCRIPTOR_PATTERN = /^O:(S-1-[0-9-]+|[A-Z]{2})(?:G:(?:S-1-[0-9-]+|[A-Z]{2}))?(?:D:([A-Z_]*)((?:\([^()]*\))*))?(?:S:.*)?$/;
|
|
24
|
+
const OWNER_GROUP = 1;
|
|
25
|
+
const ACCESS_CONTROL_FLAGS_GROUP = 2;
|
|
26
|
+
const ACCESS_CONTROL_ENTRIES_GROUP = 3;
|
|
27
|
+
const ACCESS_CONTROL_ENTRY_PATTERN = /\(([^()]*)\)/g;
|
|
28
|
+
const ACCESS_CONTROL_ENTRY_GROUP = 1;
|
|
29
|
+
const ACCESS_CONTROL_ENTRY_FIELD_COUNT = 6;
|
|
30
|
+
const NO_ACCESS_CONTROL_FLAG = 'NO_ACCESS_CONTROL';
|
|
31
|
+
const ALLOW_ENTRY_TYPES = ['A', 'OA'];
|
|
32
|
+
const DENY_ENTRY_TYPES = ['D', 'OD'];
|
|
33
|
+
const PRIVILEGED_ACCOUNTS = [
|
|
34
|
+
'SY',
|
|
35
|
+
'S-1-5-18',
|
|
36
|
+
'BA',
|
|
37
|
+
'S-1-5-32-544',
|
|
38
|
+
'LA',
|
|
39
|
+
];
|
|
40
|
+
const TRUSTED_INSTALLER_SID = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464';
|
|
41
|
+
const CREATOR_ACCOUNTS = [
|
|
42
|
+
'CO',
|
|
43
|
+
'S-1-3-0',
|
|
44
|
+
'OW',
|
|
45
|
+
'S-1-3-4',
|
|
46
|
+
];
|
|
47
|
+
const QUOTED_CSV_FIELD_PATTERN = /"([^"]*)"/g;
|
|
48
|
+
const QUOTED_CSV_FIELD_GROUP = 1;
|
|
49
|
+
const SECURITY_IDENTIFIER_PATTERN = /^S-1-[0-9-]+$/;
|
|
50
|
+
class WindowsProbeFailed extends Data.TaggedError('WindowsProbeFailed') {
|
|
51
|
+
}
|
|
52
|
+
class DirectoryProbeFailed extends Data.TaggedError('DirectoryProbeFailed') {
|
|
53
|
+
}
|
|
54
|
+
export const relayRegistryDirectoryRefusal = (info, maybeCurrentUid) => Option.match(maybeCurrentUid, {
|
|
55
|
+
onNone: () => Option.some(UNVERIFIED_OWNERSHIP),
|
|
56
|
+
onSome: currentUid => {
|
|
57
|
+
if (Option.isNone(info.uid)) {
|
|
58
|
+
return Option.some(UNVERIFIED_OWNERSHIP);
|
|
59
|
+
}
|
|
60
|
+
if (info.uid.value !== currentUid) {
|
|
61
|
+
return Option.some(OWNED_BY_ANOTHER_USER);
|
|
62
|
+
}
|
|
63
|
+
if ((info.mode & PERMISSIONS_BEYOND_OWNER) !== 0) {
|
|
64
|
+
return Option.some(SHARED_WITH_OTHER_USERS);
|
|
65
|
+
}
|
|
66
|
+
return Option.none();
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const posixDirectoryRefusal = (modeValue, uidValue) => {
|
|
70
|
+
const mode = globalThis.Number(modeValue);
|
|
71
|
+
const uid = globalThis.Number(uidValue);
|
|
72
|
+
if (!globalThis.Number.isSafeInteger(mode) ||
|
|
73
|
+
!globalThis.Number.isSafeInteger(uid)) {
|
|
74
|
+
return Option.some(UNVERIFIED_OWNERSHIP);
|
|
75
|
+
}
|
|
76
|
+
return relayRegistryDirectoryRefusal({ mode, uid: Option.some(uid) }, Option.fromNullishOr(process.getuid?.()));
|
|
77
|
+
};
|
|
78
|
+
const directoryChain = (directory, parentOf = dirname) => {
|
|
79
|
+
const directories = [];
|
|
80
|
+
let current = directory;
|
|
81
|
+
while (true) {
|
|
82
|
+
directories.push(current);
|
|
83
|
+
const parent = parentOf(current);
|
|
84
|
+
if (parent === current) {
|
|
85
|
+
return Array.reverse(directories);
|
|
86
|
+
}
|
|
87
|
+
current = parent;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const posixParentChainRefusal = (directory) => Effect.tryPromise({
|
|
91
|
+
try: async () => {
|
|
92
|
+
const currentUid = process.getuid?.();
|
|
93
|
+
if (currentUid === undefined) {
|
|
94
|
+
return Option.some(UNVERIFIED_OWNERSHIP);
|
|
95
|
+
}
|
|
96
|
+
for (const component of directoryChain(directory)) {
|
|
97
|
+
const entry = await lstat(component, { bigint: true });
|
|
98
|
+
const uid = globalThis.Number(entry.uid);
|
|
99
|
+
const mode = globalThis.Number(entry.mode);
|
|
100
|
+
if (entry.isSymbolicLink() ||
|
|
101
|
+
!entry.isDirectory() ||
|
|
102
|
+
!globalThis.Number.isSafeInteger(uid) ||
|
|
103
|
+
!globalThis.Number.isSafeInteger(mode)) {
|
|
104
|
+
return Option.some(UNVERIFIED_OWNERSHIP);
|
|
105
|
+
}
|
|
106
|
+
if (uid !== currentUid && uid !== ROOT_UID) {
|
|
107
|
+
return Option.some(REPLACEABLE_PARENT);
|
|
108
|
+
}
|
|
109
|
+
const isParent = component !== directory;
|
|
110
|
+
const isWritableByOtherUsers = (mode & PARENT_WRITE_PERMISSIONS) !== 0;
|
|
111
|
+
const isSticky = (mode & STICKY_DIRECTORY) !== 0;
|
|
112
|
+
if (isParent && isWritableByOtherUsers && !isSticky) {
|
|
113
|
+
return Option.some(REPLACEABLE_PARENT);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return Option.none();
|
|
117
|
+
},
|
|
118
|
+
catch: () => new DirectoryProbeFailed(),
|
|
119
|
+
});
|
|
120
|
+
const darwinAclOutputRefusal = (output, isLeaf) => {
|
|
121
|
+
const outputLines = String.split(output, /\r?\n/);
|
|
122
|
+
const lines = output.endsWith('\n')
|
|
123
|
+
? Array.dropRight(outputLines, 1)
|
|
124
|
+
: outputLines;
|
|
125
|
+
const maybeSummary = Array.head(lines);
|
|
126
|
+
if (Option.isNone(maybeSummary) ||
|
|
127
|
+
!DARWIN_DIRECTORY_MODE_PATTERN.test(maybeSummary.value)) {
|
|
128
|
+
return Option.some(UNVERIFIED_PERMISSIONS);
|
|
129
|
+
}
|
|
130
|
+
for (const line of Array.drop(lines, 1)) {
|
|
131
|
+
const maybeAction = Option.flatMap(Option.fromNullishOr(DARWIN_ACL_ENTRY_PATTERN.exec(line)), entry => Array.get(entry, 1));
|
|
132
|
+
if (Option.isNone(maybeAction)) {
|
|
133
|
+
return Option.some(UNVERIFIED_PERMISSIONS);
|
|
134
|
+
}
|
|
135
|
+
if (maybeAction.value === 'allow') {
|
|
136
|
+
return Option.some(isLeaf ? SHARED_WITH_OTHER_USERS : REPLACEABLE_PARENT);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return Option.none();
|
|
140
|
+
};
|
|
141
|
+
const darwinAclChainRefusal = (directory) => Effect.gen(function* () {
|
|
142
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
143
|
+
for (const component of directoryChain(directory)) {
|
|
144
|
+
const probe = yield* spawner.spawn(ChildProcess.make('/bin/ls', ['-l', '-d', '-e', '-P', '-b', '--', component], {
|
|
145
|
+
env: { LC_ALL: 'C' },
|
|
146
|
+
extendEnv: true,
|
|
147
|
+
stdin: 'ignore',
|
|
148
|
+
stderr: 'ignore',
|
|
149
|
+
}));
|
|
150
|
+
const [output, exitCode] = yield* Effect.all([
|
|
151
|
+
pipe(probe.stdout, Stream.decodeText, Stream.mkString),
|
|
152
|
+
probe.exitCode,
|
|
153
|
+
], { concurrency: 'unbounded' });
|
|
154
|
+
if (exitCode !== 0) {
|
|
155
|
+
return yield* Effect.fail(new DirectoryProbeFailed());
|
|
156
|
+
}
|
|
157
|
+
const maybeRefusal = darwinAclOutputRefusal(output, component === directory);
|
|
158
|
+
if (Option.isSome(maybeRefusal)) {
|
|
159
|
+
return maybeRefusal;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return Option.none();
|
|
163
|
+
}).pipe(Effect.scoped, Effect.mapError(() => new DirectoryProbeFailed()), Effect.timeoutOrElse({
|
|
164
|
+
duration: PROBE_TIMEOUT,
|
|
165
|
+
orElse: () => Effect.fail(new DirectoryProbeFailed()),
|
|
166
|
+
}));
|
|
167
|
+
const capturedGroup = (match, group) => Option.flatMap(Array.get(match, group), Option.fromNullishOr);
|
|
168
|
+
const parseAccessControlEntry = (entry) => {
|
|
169
|
+
const fields = String.split(entry, ';');
|
|
170
|
+
if (fields.length !== ACCESS_CONTROL_ENTRY_FIELD_COUNT) {
|
|
171
|
+
return Option.none();
|
|
172
|
+
}
|
|
173
|
+
const type = Array.headNonEmpty(fields);
|
|
174
|
+
const isKnownType = Array.contains(ALLOW_ENTRY_TYPES, type) ||
|
|
175
|
+
Array.contains(DENY_ENTRY_TYPES, type);
|
|
176
|
+
if (!isKnownType) {
|
|
177
|
+
return Option.none();
|
|
178
|
+
}
|
|
179
|
+
return Option.all({
|
|
180
|
+
type: Option.some(type),
|
|
181
|
+
flags: Array.get(fields, 1),
|
|
182
|
+
rights: Array.get(fields, 2),
|
|
183
|
+
account: Option.some(Array.lastNonEmpty(fields)),
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
const parseAccessControlEntries = (entries) => pipe(entries, String.matchAll(ACCESS_CONTROL_ENTRY_PATTERN), Array.fromIterable, Array.map(entry => Option.flatMap(capturedGroup(entry, ACCESS_CONTROL_ENTRY_GROUP), parseAccessControlEntry)), Option.all);
|
|
187
|
+
const parseSecurityDescriptor = (sddl) => Option.flatMap(Option.fromNullishOr(SECURITY_DESCRIPTOR_PATTERN.exec(sddl)), descriptor => Option.all({
|
|
188
|
+
owner: capturedGroup(descriptor, OWNER_GROUP),
|
|
189
|
+
flags: capturedGroup(descriptor, ACCESS_CONTROL_FLAGS_GROUP),
|
|
190
|
+
entries: Option.flatMap(capturedGroup(descriptor, ACCESS_CONTROL_ENTRIES_GROUP), parseAccessControlEntries),
|
|
191
|
+
}));
|
|
192
|
+
export const windowsDirectoryRefusal = (sddl, currentUserSid) => {
|
|
193
|
+
const maybeRawDescriptor = Option.fromNullishOr(SECURITY_DESCRIPTOR_PATTERN.exec(sddl));
|
|
194
|
+
if (Option.isNone(maybeRawDescriptor)) {
|
|
195
|
+
return Option.some(UNVERIFIED_OWNERSHIP);
|
|
196
|
+
}
|
|
197
|
+
const maybeDescriptor = parseSecurityDescriptor(sddl);
|
|
198
|
+
if (Option.isNone(maybeDescriptor)) {
|
|
199
|
+
return Option.some(UNVERIFIED_PERMISSIONS);
|
|
200
|
+
}
|
|
201
|
+
const { owner, flags, entries } = maybeDescriptor.value;
|
|
202
|
+
if (String.includes(NO_ACCESS_CONTROL_FLAG)(flags)) {
|
|
203
|
+
return Option.some(SHARED_WITH_OTHER_USERS);
|
|
204
|
+
}
|
|
205
|
+
const trustedOwners = [currentUserSid, ...PRIVILEGED_ACCOUNTS];
|
|
206
|
+
if (!Array.contains(trustedOwners, owner)) {
|
|
207
|
+
return Option.some(OWNED_BY_ANOTHER_USER);
|
|
208
|
+
}
|
|
209
|
+
const trustedAccounts = [...trustedOwners, ...CREATOR_ACCOUNTS];
|
|
210
|
+
const isSharedWithOtherUsers = Array.some(entries, ({ type, account }) => Array.contains(ALLOW_ENTRY_TYPES, type) &&
|
|
211
|
+
!Array.contains(trustedAccounts, account));
|
|
212
|
+
if (isSharedWithOtherUsers) {
|
|
213
|
+
return Option.some(SHARED_WITH_OTHER_USERS);
|
|
214
|
+
}
|
|
215
|
+
return Option.none();
|
|
216
|
+
};
|
|
217
|
+
const SDDL_ACCESS_RIGHT_CODES = [
|
|
218
|
+
'GA',
|
|
219
|
+
'GR',
|
|
220
|
+
'GW',
|
|
221
|
+
'GX',
|
|
222
|
+
'RC',
|
|
223
|
+
'LC',
|
|
224
|
+
'SD',
|
|
225
|
+
'WD',
|
|
226
|
+
'WO',
|
|
227
|
+
'FA',
|
|
228
|
+
'FR',
|
|
229
|
+
'FW',
|
|
230
|
+
'FX',
|
|
231
|
+
];
|
|
232
|
+
const HEX_ACCESS_RIGHTS_PATTERN = /^0x[0-9a-f]+$/i;
|
|
233
|
+
const SYMBOLIC_ACCESS_RIGHTS_PATTERN = /[A-Z]{2}/g;
|
|
234
|
+
const WRITE_DATA_ACCESS = 0x0000_0002;
|
|
235
|
+
const WRITE_ATTRIBUTES_ACCESS = 0x0000_0100;
|
|
236
|
+
const DELETE_ACCESS = 0x0001_0000;
|
|
237
|
+
const DELETE_CHILD_ACCESS = 0x0000_0040;
|
|
238
|
+
const CHANGE_PERMISSIONS_ACCESS = 0x0004_0000;
|
|
239
|
+
const TAKE_OWNERSHIP_ACCESS = 0x0008_0000;
|
|
240
|
+
const GENERIC_ALL_ACCESS = 0x1000_0000;
|
|
241
|
+
const GENERIC_WRITE_ACCESS = 0x4000_0000;
|
|
242
|
+
const KNOWN_FILE_ACCESS = 0xf11f_01ff;
|
|
243
|
+
const CHILD_REPLACEMENT_ACCESS = WRITE_DATA_ACCESS |
|
|
244
|
+
WRITE_ATTRIBUTES_ACCESS |
|
|
245
|
+
DELETE_ACCESS |
|
|
246
|
+
CHANGE_PERMISSIONS_ACCESS |
|
|
247
|
+
TAKE_OWNERSHIP_ACCESS |
|
|
248
|
+
GENERIC_ALL_ACCESS |
|
|
249
|
+
GENERIC_WRITE_ACCESS;
|
|
250
|
+
const PARENT_REPLACEMENT_ACCESS = DELETE_CHILD_ACCESS |
|
|
251
|
+
CHANGE_PERMISSIONS_ACCESS |
|
|
252
|
+
TAKE_OWNERSHIP_ACCESS |
|
|
253
|
+
GENERIC_ALL_ACCESS;
|
|
254
|
+
const symbolicAccessRights = (rights) => Option.flatMap(pipe(rights, String.matchAll(SYMBOLIC_ACCESS_RIGHTS_PATTERN), Array.fromIterable, Array.map(Array.head), Option.all), codes => {
|
|
255
|
+
const isComplete = Array.length(codes) * 2 === rights.length;
|
|
256
|
+
const areKnown = Array.every(codes, code => Array.contains(SDDL_ACCESS_RIGHT_CODES, code));
|
|
257
|
+
return isComplete && areKnown && Array.isArrayNonEmpty(codes)
|
|
258
|
+
? Option.some(codes)
|
|
259
|
+
: Option.none();
|
|
260
|
+
});
|
|
261
|
+
const accessGrantsReplacement = (rights, mask, symbolicRights) => {
|
|
262
|
+
if (HEX_ACCESS_RIGHTS_PATTERN.test(rights)) {
|
|
263
|
+
const parsed = globalThis.Number.parseInt(rights.slice(2), 16);
|
|
264
|
+
const isKnownMask = globalThis.Number.isSafeInteger(parsed) &&
|
|
265
|
+
parsed <= 0xffff_ffff &&
|
|
266
|
+
((parsed >>> 0) & ~KNOWN_FILE_ACCESS) === 0;
|
|
267
|
+
return isKnownMask ? Option.some((parsed & mask) !== 0) : Option.none();
|
|
268
|
+
}
|
|
269
|
+
return Option.map(symbolicAccessRights(rights), codes => Array.some(codes, code => Array.contains([...symbolicRights, 'GA', 'FA'], code)));
|
|
270
|
+
};
|
|
271
|
+
export const windowsDirectoryChainRefusal = (descriptors, currentUserSid) => {
|
|
272
|
+
if (Option.isNone(Array.head(descriptors))) {
|
|
273
|
+
return Option.some(UNVERIFIED_OWNERSHIP);
|
|
274
|
+
}
|
|
275
|
+
const maybeParentDescriptors = Option.all(Array.map(Array.dropRight(descriptors, 1), parseSecurityDescriptor));
|
|
276
|
+
if (Option.isNone(maybeParentDescriptors)) {
|
|
277
|
+
return Option.some(UNVERIFIED_PERMISSIONS);
|
|
278
|
+
}
|
|
279
|
+
const parentDescriptors = maybeParentDescriptors.value;
|
|
280
|
+
const trustedParentOwners = [
|
|
281
|
+
currentUserSid,
|
|
282
|
+
...PRIVILEGED_ACCOUNTS,
|
|
283
|
+
TRUSTED_INSTALLER_SID,
|
|
284
|
+
];
|
|
285
|
+
const trustedAccounts = [...trustedParentOwners, ...CREATOR_ACCOUNTS];
|
|
286
|
+
for (const [index, descriptor] of parentDescriptors.entries()) {
|
|
287
|
+
if (!Array.contains(trustedParentOwners, descriptor.owner)) {
|
|
288
|
+
return Option.some(REPLACEABLE_PARENT);
|
|
289
|
+
}
|
|
290
|
+
if (String.includes(NO_ACCESS_CONTROL_FLAG)(descriptor.flags)) {
|
|
291
|
+
return Option.some(REPLACEABLE_PARENT);
|
|
292
|
+
}
|
|
293
|
+
const isRoot = index === 0;
|
|
294
|
+
for (const entry of descriptor.entries) {
|
|
295
|
+
const isUntrustedAllow = Array.contains(ALLOW_ENTRY_TYPES, entry.type) &&
|
|
296
|
+
!Array.contains(trustedAccounts, entry.account) &&
|
|
297
|
+
!String.includes('IO')(entry.flags);
|
|
298
|
+
if (!isUntrustedAllow) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const maybeCanReplaceSelf = isRoot
|
|
302
|
+
? Option.some(false)
|
|
303
|
+
: accessGrantsReplacement(entry.rights, CHILD_REPLACEMENT_ACCESS, [
|
|
304
|
+
'SD',
|
|
305
|
+
'WD',
|
|
306
|
+
'WO',
|
|
307
|
+
'FW',
|
|
308
|
+
'GW',
|
|
309
|
+
]);
|
|
310
|
+
const maybeCanReplaceChild = accessGrantsReplacement(entry.rights, PARENT_REPLACEMENT_ACCESS, ['DC', 'WD', 'WO']);
|
|
311
|
+
const maybeCanReplace = Option.map(Option.all([maybeCanReplaceSelf, maybeCanReplaceChild]), Array.some(value => value));
|
|
312
|
+
if (Option.isNone(maybeCanReplace)) {
|
|
313
|
+
return Option.some(UNVERIFIED_PERMISSIONS);
|
|
314
|
+
}
|
|
315
|
+
if (maybeCanReplace.value) {
|
|
316
|
+
return Option.some(REPLACEABLE_PARENT);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return Option.none();
|
|
321
|
+
};
|
|
322
|
+
export const powershellPath = (systemRoot) => win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
323
|
+
export const windowsTempRootRefusal = (directory, tempRoot) => win32.relative(tempRoot, win32.dirname(directory)) === ''
|
|
324
|
+
? Option.none()
|
|
325
|
+
: Option.some(OUTSIDE_WINDOWS_TEMP);
|
|
326
|
+
const currentUserSidFromCsv = (identity) => pipe(identity, String.matchAll(QUOTED_CSV_FIELD_PATTERN), Array.fromIterable, Array.map(field => capturedGroup(field, QUOTED_CSV_FIELD_GROUP)), Array.getSomes, Array.findLast(field => SECURITY_IDENTIFIER_PATTERN.test(field)));
|
|
327
|
+
const probeOutputRefusal = (output, directory) => {
|
|
328
|
+
const lines = pipe(output, String.split(/\r?\n/), Array.map(String.trim), Array.filter(String.isNonEmpty));
|
|
329
|
+
const maybeCurrentUserSid = Option.flatMap(Array.last(lines), currentUserSidFromCsv);
|
|
330
|
+
const directoryResults = Array.take(lines, Array.length(lines) - 1);
|
|
331
|
+
const expectedDirectoryCount = Array.length(directoryChain(directory, win32.dirname));
|
|
332
|
+
return Option.match(maybeCurrentUserSid, {
|
|
333
|
+
onNone: () => Effect.fail(new WindowsProbeFailed()),
|
|
334
|
+
onSome: currentUserSid => {
|
|
335
|
+
if (Array.length(directoryResults) !== expectedDirectoryCount * 2) {
|
|
336
|
+
return Effect.fail(new WindowsProbeFailed());
|
|
337
|
+
}
|
|
338
|
+
const results = Array.chunksOf(directoryResults, 2);
|
|
339
|
+
const kinds = Array.getSomes(Array.map(results, Array.head));
|
|
340
|
+
const descriptors = Array.getSomes(Array.map(results, Array.last));
|
|
341
|
+
if (Array.length(kinds) !== expectedDirectoryCount ||
|
|
342
|
+
Array.length(descriptors) !== expectedDirectoryCount) {
|
|
343
|
+
return Effect.fail(new WindowsProbeFailed());
|
|
344
|
+
}
|
|
345
|
+
if (Array.some(kinds, kind => kind !== 'DIRECTORY' && kind !== 'REPARSE_POINT')) {
|
|
346
|
+
return Effect.fail(new WindowsProbeFailed());
|
|
347
|
+
}
|
|
348
|
+
if (Array.some(kinds, kind => kind === 'REPARSE_POINT')) {
|
|
349
|
+
return Effect.succeed(Option.some(REPARSE_PARENT));
|
|
350
|
+
}
|
|
351
|
+
const maybeLeafDescriptor = Array.last(descriptors);
|
|
352
|
+
return Option.match(maybeLeafDescriptor, {
|
|
353
|
+
onNone: () => Effect.fail(new WindowsProbeFailed()),
|
|
354
|
+
onSome: leafDescriptor => Effect.succeed(Option.orElse(windowsDirectoryChainRefusal(descriptors, currentUserSid), () => windowsDirectoryRefusal(leafDescriptor, currentUserSid))),
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
const readWindowsDirectoryRefusal = (directory, executable) => Effect.gen(function* () {
|
|
360
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
361
|
+
const probe = yield* spawner.spawn(ChildProcess.make(executable, ['-NoProfile', '-NonInteractive', '-Command', PROBE_SCRIPT], {
|
|
362
|
+
env: { [PROBED_DIRECTORY_VARIABLE]: directory },
|
|
363
|
+
extendEnv: true,
|
|
364
|
+
stdin: 'ignore',
|
|
365
|
+
stderr: 'ignore',
|
|
366
|
+
}));
|
|
367
|
+
const [output, exitCode] = yield* Effect.all([pipe(probe.stdout, Stream.decodeText, Stream.mkString), probe.exitCode], { concurrency: 'unbounded' });
|
|
368
|
+
if (exitCode === 0) {
|
|
369
|
+
return yield* probeOutputRefusal(output, directory);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
return yield* Effect.fail(new WindowsProbeFailed());
|
|
373
|
+
}
|
|
374
|
+
}).pipe(Effect.scoped, Effect.mapError(() => new WindowsProbeFailed()), Effect.timeoutOrElse({
|
|
375
|
+
duration: PROBE_TIMEOUT,
|
|
376
|
+
orElse: () => Effect.fail(new WindowsProbeFailed()),
|
|
377
|
+
}));
|
|
378
|
+
export const probeWindowsDirectory = (maybeExecutable, directory) => Option.match(maybeExecutable, {
|
|
379
|
+
onNone: () => Effect.succeed(Option.some(UNVERIFIED_OWNERSHIP)),
|
|
380
|
+
onSome: executable => readWindowsDirectoryRefusal(directory, executable).pipe(Effect.orElseSucceed(() => Option.some(UNVERIFIED_OWNERSHIP))),
|
|
381
|
+
});
|
|
382
|
+
const probeDirectoryEntry = (directory) => Effect.tryPromise({
|
|
383
|
+
try: async () => {
|
|
384
|
+
const entry = await lstat(directory, { bigint: true });
|
|
385
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
386
|
+
return {
|
|
387
|
+
directory,
|
|
388
|
+
entry,
|
|
389
|
+
maybeRefusal: Option.some(NOT_REGULAR_DIRECTORY),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const canonicalDirectory = await realpath(directory);
|
|
393
|
+
const canonicalEntry = await lstat(canonicalDirectory, { bigint: true });
|
|
394
|
+
const isSameEntry = entry.dev === canonicalEntry.dev && entry.ino === canonicalEntry.ino;
|
|
395
|
+
return {
|
|
396
|
+
directory: canonicalDirectory,
|
|
397
|
+
entry: canonicalEntry,
|
|
398
|
+
maybeRefusal: isSameEntry
|
|
399
|
+
? Option.none()
|
|
400
|
+
: Option.some(UNVERIFIED_OWNERSHIP),
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
catch: () => new DirectoryProbeFailed(),
|
|
404
|
+
});
|
|
405
|
+
export const relayRegistryTrust = {
|
|
406
|
+
inspect: directory => Effect.gen(function* () {
|
|
407
|
+
const probe = yield* probeDirectoryEntry(directory);
|
|
408
|
+
if (Option.isSome(probe.maybeRefusal)) {
|
|
409
|
+
return {
|
|
410
|
+
directory: probe.directory,
|
|
411
|
+
maybeRefusal: probe.maybeRefusal,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const maybeRefusal = process.platform === 'win32'
|
|
415
|
+
? yield* Effect.gen(function* () {
|
|
416
|
+
const tempRoot = yield* Effect.tryPromise({
|
|
417
|
+
try: () => realpath(tmpdir()),
|
|
418
|
+
catch: () => new DirectoryProbeFailed(),
|
|
419
|
+
});
|
|
420
|
+
const maybeLocationRefusal = windowsTempRootRefusal(probe.directory, tempRoot);
|
|
421
|
+
if (Option.isSome(maybeLocationRefusal)) {
|
|
422
|
+
return maybeLocationRefusal;
|
|
423
|
+
}
|
|
424
|
+
return yield* probeWindowsDirectory(Option.map(Option.fromNullishOr(process.env['SystemRoot']), powershellPath), probe.directory);
|
|
425
|
+
})
|
|
426
|
+
: yield* posixParentChainRefusal(probe.directory).pipe(Effect.flatMap(maybeParentRefusal => {
|
|
427
|
+
if (Option.isSome(maybeParentRefusal)) {
|
|
428
|
+
return Effect.succeed(maybeParentRefusal);
|
|
429
|
+
}
|
|
430
|
+
const maybeLeafRefusal = posixDirectoryRefusal(probe.entry.mode, probe.entry.uid);
|
|
431
|
+
if (Option.isSome(maybeLeafRefusal)) {
|
|
432
|
+
return Effect.succeed(maybeLeafRefusal);
|
|
433
|
+
}
|
|
434
|
+
return process.platform === 'darwin'
|
|
435
|
+
? darwinAclChainRefusal(probe.directory)
|
|
436
|
+
: Effect.succeed(Option.none());
|
|
437
|
+
}));
|
|
438
|
+
if (Option.isSome(maybeRefusal)) {
|
|
439
|
+
return { directory: probe.directory, maybeRefusal };
|
|
440
|
+
}
|
|
441
|
+
const confirmation = yield* probeDirectoryEntry(directory);
|
|
442
|
+
const isSameDirectory = Option.isNone(confirmation.maybeRefusal) &&
|
|
443
|
+
confirmation.directory === probe.directory &&
|
|
444
|
+
confirmation.entry.dev === probe.entry.dev &&
|
|
445
|
+
confirmation.entry.ino === probe.entry.ino;
|
|
446
|
+
return {
|
|
447
|
+
directory: probe.directory,
|
|
448
|
+
maybeRefusal: isSameDirectory
|
|
449
|
+
? Option.none()
|
|
450
|
+
: Option.some(UNVERIFIED_OWNERSHIP),
|
|
451
|
+
};
|
|
452
|
+
}).pipe(Effect.orElseSucceed(() => ({
|
|
453
|
+
directory,
|
|
454
|
+
maybeRefusal: Option.some(UNVERIFIED_OWNERSHIP),
|
|
455
|
+
}))),
|
|
456
|
+
};
|