@getmarrow/mcp 3.9.108 → 3.9.109
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 +16 -1
- package/dist/cli.js +26 -1
- package/dist/cli.js.map +1 -1
- package/dist/control-notice.d.ts +13 -0
- package/dist/control-notice.d.ts.map +1 -0
- package/dist/control-notice.js +76 -0
- package/dist/control-notice.js.map +1 -0
- package/dist/control-routes.d.ts +10 -0
- package/dist/control-routes.d.ts.map +1 -0
- package/dist/control-routes.js +81 -0
- package/dist/control-routes.js.map +1 -0
- package/dist/hook-context.d.ts.map +1 -1
- package/dist/hook-context.js +34 -11
- package/dist/hook-context.js.map +1 -1
- package/dist/hook-contract.d.ts +2 -2
- package/dist/hook-contract.js +1 -1
- package/dist/hook-pre-action.d.ts +2 -0
- package/dist/hook-pre-action.d.ts.map +1 -1
- package/dist/hook-pre-action.js +86 -20
- package/dist/hook-pre-action.js.map +1 -1
- package/dist/hook-session.d.ts.map +1 -1
- package/dist/hook-session.js +12 -7
- package/dist/hook-session.js.map +1 -1
- package/dist/hook-tool-policy.d.ts +1 -1
- package/dist/hook-tool-policy.d.ts.map +1 -1
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +19 -14
- package/dist/hook.js.map +1 -1
- package/dist/ipv4-first.d.ts +7 -0
- package/dist/ipv4-first.d.ts.map +1 -0
- package/dist/ipv4-first.js +60 -0
- package/dist/ipv4-first.js.map +1 -0
- package/dist/key-class.d.ts +16 -0
- package/dist/key-class.d.ts.map +1 -0
- package/dist/key-class.js +37 -0
- package/dist/key-class.js.map +1 -0
- package/dist/keyless-notice.d.ts +10 -0
- package/dist/keyless-notice.d.ts.map +1 -0
- package/dist/keyless-notice.js +47 -0
- package/dist/keyless-notice.js.map +1 -0
- package/dist/normalized-action.d.ts +4 -0
- package/dist/normalized-action.d.ts.map +1 -1
- package/dist/normalized-action.js +60 -27
- package/dist/normalized-action.js.map +1 -1
- package/dist/owner-bypass.d.ts +9 -0
- package/dist/owner-bypass.d.ts.map +1 -0
- package/dist/owner-bypass.js +79 -0
- package/dist/owner-bypass.js.map +1 -0
- package/dist/redact.d.ts +5 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +61 -9
- package/dist/redact.js.map +1 -1
- package/dist/seatbelt/hook.d.ts +3 -1
- package/dist/seatbelt/hook.d.ts.map +1 -1
- package/dist/seatbelt/hook.js +7 -3
- package/dist/seatbelt/hook.js.map +1 -1
- package/dist/seatbelt/mapper.d.ts.map +1 -1
- package/dist/seatbelt/mapper.js +849 -12
- package/dist/seatbelt/mapper.js.map +1 -1
- package/dist/seatbelt/patch-text.d.ts +49 -0
- package/dist/seatbelt/patch-text.d.ts.map +1 -0
- package/dist/seatbelt/patch-text.js +376 -0
- package/dist/seatbelt/patch-text.js.map +1 -0
- package/dist/seatbelt/tables.d.ts +66 -0
- package/dist/seatbelt/tables.d.ts.map +1 -1
- package/dist/seatbelt/tables.js +467 -3
- package/dist/seatbelt/tables.js.map +1 -1
- package/dist/seatbelt/win-protected.d.ts +52 -0
- package/dist/seatbelt/win-protected.d.ts.map +1 -0
- package/dist/seatbelt/win-protected.js +1279 -0
- package/dist/seatbelt/win-protected.js.map +1 -0
- package/dist/self-protect.d.ts +18 -0
- package/dist/self-protect.d.ts.map +1 -0
- package/dist/self-protect.js +185 -0
- package/dist/self-protect.js.map +1 -0
- package/dist/session-id.d.ts +2 -0
- package/dist/session-id.d.ts.map +1 -0
- package/dist/session-id.js +32 -0
- package/dist/session-id.js.map +1 -0
- package/dist/session-loop-guard.d.ts +7 -0
- package/dist/session-loop-guard.d.ts.map +1 -1
- package/dist/session-loop-guard.js +168 -1
- package/dist/session-loop-guard.js.map +1 -1
- package/dist/session-marker.d.ts +7 -0
- package/dist/session-marker.d.ts.map +1 -0
- package/dist/session-marker.js +42 -0
- package/dist/session-marker.js.map +1 -0
- package/dist/win-private-state.d.ts +117 -0
- package/dist/win-private-state.d.ts.map +1 -0
- package/dist/win-private-state.js +544 -0
- package/dist/win-private-state.js.map +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/seatbelt/mapper.js
CHANGED
|
@@ -50,6 +50,7 @@ const secret_paths_1 = require("../secret-paths");
|
|
|
50
50
|
const tables_1 = require("./tables");
|
|
51
51
|
const classifier_pin_1 = require("./classifier-pin");
|
|
52
52
|
const env_names_1 = require("./env-names");
|
|
53
|
+
const patch_text_1 = require("./patch-text");
|
|
53
54
|
const longopts_1 = require("./longopts");
|
|
54
55
|
const guards_1 = require("./guards");
|
|
55
56
|
const env_dump_1 = require("./env-dump");
|
|
@@ -119,7 +120,10 @@ function evaluateSeatbeltShell(command, env) {
|
|
|
119
120
|
try {
|
|
120
121
|
const run = newRun(env, command);
|
|
121
122
|
run.shell = true;
|
|
122
|
-
|
|
123
|
+
if (env.platform === 'win32')
|
|
124
|
+
psTop(String(command == null ? '' : command), run);
|
|
125
|
+
else
|
|
126
|
+
evalShell(String(command == null ? '' : command), newCtx(run), run);
|
|
123
127
|
return finish(run);
|
|
124
128
|
}
|
|
125
129
|
catch {
|
|
@@ -142,7 +146,16 @@ function failClosedBare() {
|
|
|
142
146
|
}
|
|
143
147
|
function evaluateSeatbelt(event, env) {
|
|
144
148
|
try {
|
|
145
|
-
|
|
149
|
+
const startedAt = Date.now();
|
|
150
|
+
const decision = evaluateEvent(event, env);
|
|
151
|
+
// RC-1 (3.9.106): on win32 only, the Windows S0/S4 stops are added (src/seatbelt/win-protected.ts). They only
|
|
152
|
+
// add hits; every other platform returns the decision unchanged. A throw there is the stop below.
|
|
153
|
+
if (env.platform !== 'win32')
|
|
154
|
+
return decision;
|
|
155
|
+
return require('./win-protected').withWindowsProtected(decision, event, env, {
|
|
156
|
+
shellInvocations, mcpCall, pathInputs, walkPathInputs, patchPaths, protectedExtra: SEATBELT_PROTECTED_EXTRA, operandOmits: OPERAND_RULE_OMITS,
|
|
157
|
+
tools: { read: READ_TOOLS, grep: GREP_TOOLS, write: WRITE_TOOLS, delete: DELETE_TOOLS, list: LIST_TOOLS, inert: INERT_TOOLS, nonShell: NON_SHELL_TOOLS, nativeShell: NATIVE_SHELL_EQUIVALENTS },
|
|
158
|
+
}, startedAt);
|
|
146
159
|
}
|
|
147
160
|
catch {
|
|
148
161
|
try {
|
|
@@ -222,11 +235,16 @@ function evaluateEvent(event, env) {
|
|
|
222
235
|
exhaust(run, 'budget_time');
|
|
223
236
|
run.lineSource = [...new Set(invocation.plain)].join('\n');
|
|
224
237
|
const lastGroup = invocation.groups[invocation.groups.length - 1];
|
|
238
|
+
// A3: PowerShell / cmd text is the PowerShell tool's, and on native Windows every shell tool's (the host shell may be either).
|
|
239
|
+
const psTopMode = env.platform === 'win32' || evName === 'powershell' || evName === 'cmd';
|
|
225
240
|
invocation.shell.forEach((text, k) => {
|
|
226
241
|
if (clock(run))
|
|
227
242
|
return;
|
|
228
243
|
run.topLast = invocation.groups[k] === lastGroup;
|
|
229
|
-
|
|
244
|
+
if (psTopMode)
|
|
245
|
+
psTop(text, run);
|
|
246
|
+
else
|
|
247
|
+
evalShell(text, newCtx(run), run);
|
|
230
248
|
});
|
|
231
249
|
run.topLast = true;
|
|
232
250
|
for (const code of invocation.code) {
|
|
@@ -1010,6 +1028,8 @@ function newRun(rawEnv, source, deadline) {
|
|
|
1010
1028
|
scriptExpansions: 0,
|
|
1011
1029
|
deadline: deadline !== undefined ? deadline : deadlineFor(env),
|
|
1012
1030
|
exhausted: false,
|
|
1031
|
+
psSteps: 0,
|
|
1032
|
+
psOver: false,
|
|
1013
1033
|
links: new Map(),
|
|
1014
1034
|
linksUnknown: false,
|
|
1015
1035
|
headMayChange: false,
|
|
@@ -1035,7 +1055,7 @@ function newCtx(run) {
|
|
|
1035
1055
|
const cwd = typeof run.env.cwd === 'string' && path.posix.isAbsolute(run.env.cwd) ? path.posix.normalize(run.env.cwd) : null;
|
|
1036
1056
|
return {
|
|
1037
1057
|
cwds: [cwd], vars: new Map([['HOME', [run.home]]]), exported: new Set(['HOME']), aliases: new Map(), gitAliases: new Map(),
|
|
1038
|
-
gitConfig: new Map(), written: new Set(), writtenFrom: new Map(), depth: 0, remote: false, kubeContext: undefined, tfWorkspace: undefined, pulumiStack: undefined,
|
|
1058
|
+
gitConfig: new Map(), written: new Set(), writtenFrom: new Map(), checkedOut: new Set(), depth: 0, remote: false, kubeContext: undefined, tfWorkspace: undefined, pulumiStack: undefined,
|
|
1039
1059
|
decodedVars: new Set(), candidates: new Map(), fds: new Map(), ifs: undefined, backstop: false, arrays: new Set(),
|
|
1040
1060
|
};
|
|
1041
1061
|
}
|
|
@@ -1145,13 +1165,17 @@ function finish(run) {
|
|
|
1145
1165
|
if (run.fsState.errored)
|
|
1146
1166
|
hit(run, 'Sx', 'fs_error');
|
|
1147
1167
|
const hits = [...run.hits].sort((a, b) => CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category));
|
|
1168
|
+
// A3: within the first category, the A3 reasons lead, and then the decision's rule is `<category>.<reason>`.
|
|
1169
|
+
const a3 = hits.length ? hits.findIndex((h) => h.category === hits[0].category && tables_1.A3_REASONS.has(h.reason)) : -1;
|
|
1170
|
+
if (a3 > 0)
|
|
1171
|
+
hits.unshift(...hits.splice(a3, 1));
|
|
1148
1172
|
const category = hits.length ? hits[0].category : 'none';
|
|
1149
1173
|
if (run.parse !== 'ok')
|
|
1150
1174
|
run.flags.add(`parse_${run.parse}`);
|
|
1151
1175
|
return {
|
|
1152
1176
|
decision: hits.length ? 'stop' : 'pass',
|
|
1153
1177
|
category,
|
|
1154
|
-
rule: RULE_IDS[category],
|
|
1178
|
+
rule: a3 >= 0 ? `${category}.${hits[0].reason}` : RULE_IDS[category],
|
|
1155
1179
|
reasons: hits.map((h) => h.reason),
|
|
1156
1180
|
hits,
|
|
1157
1181
|
flags: [...run.flags].sort(),
|
|
@@ -2198,9 +2222,22 @@ function evalShell(src, ctx, run) {
|
|
|
2198
2222
|
tripwire(src.slice(0, tables_1.LIMITS.maxCommandBytes), run);
|
|
2199
2223
|
return;
|
|
2200
2224
|
}
|
|
2225
|
+
// A3: shell text that looks like PowerShell with script blocks (`Get-ChildItem | % { ri $_ -r }`): its blocks are statements too.
|
|
2226
|
+
if (ctx.ps === undefined && tables_1.PS_SNIFF.test(src)) {
|
|
2227
|
+
const flat = (0, tables_1.psFlatten)((0, tables_1.psClean)(src));
|
|
2228
|
+
if (flat !== src)
|
|
2229
|
+
psProbe(flat, childCtx(ctx, { ps: 1 }), run);
|
|
2230
|
+
psLexical((0, tables_1.psNormalize)(src), (0, tables_1.psClean)(src), ctx, run, false);
|
|
2231
|
+
}
|
|
2232
|
+
// A3: a .NET / COM delete of a whole tree (the cheap word test first; the text is only scanned when it says "delete").
|
|
2233
|
+
if (/delete|directory\]\s*::/i.test(src) && (0, tables_1.dotNetTreeDelete)((0, tables_1.psClean)(src), ctx.ps !== undefined))
|
|
2234
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
2201
2235
|
const parsed = safeParse(src, { depth: ctx.depth });
|
|
2202
2236
|
if (!parsed.ok) {
|
|
2203
2237
|
run.headMayChange = true;
|
|
2238
|
+
// A3: PowerShell syntax bash cannot read: the delete verb, `iex` or `& $x` in it is still a stop.
|
|
2239
|
+
if (ctx.ps === undefined)
|
|
2240
|
+
psUnparsed(src, run);
|
|
2204
2241
|
const big = parsed.error === 'too_large' || parsed.error === 'too_many_commands' || parsed.error === 'parse_cost';
|
|
2205
2242
|
worseParse(run, big ? 'too_large' : 'unparsed');
|
|
2206
2243
|
tripwire(src.slice(0, tables_1.LIMITS.maxCommandBytes), run);
|
|
@@ -2907,6 +2944,13 @@ function dispatch(args, ctx, run, x) {
|
|
|
2907
2944
|
const prog = progName(args[0], run);
|
|
2908
2945
|
if (prog === null)
|
|
2909
2946
|
return dynamicProgram(args, ctx, run, x);
|
|
2947
|
+
// A3b: on native Windows the program is also read by its Windows name (`C:\Program Files\Git\cmd\git.exe`, `GIT.EXE`):
|
|
2948
|
+
// a second reading next to the unchanged one, so it can only add stops.
|
|
2949
|
+
if (run.env.platform === 'win32' && args[0].text !== null) {
|
|
2950
|
+
const winProg = (0, tables_1.windowsProgramName)(args[0].text);
|
|
2951
|
+
if (winProg !== prog && winProg !== '')
|
|
2952
|
+
dispatchProgram(winProg, args, args.slice(1), false, ctx, run, x);
|
|
2953
|
+
}
|
|
2910
2954
|
// A glob in the command word names whatever matches it at run time (`/bin/s[h]`, `np[m]`): read each
|
|
2911
2955
|
// match as the program, and stop when it matches nothing the mapper can list (audit N1).
|
|
2912
2956
|
if (args[0].glob && args[0].text !== null && args[0].text !== '[' && args[0].text !== '[[' && /[*?[]/.test(args[0].text)) {
|
|
@@ -2933,6 +2977,8 @@ function dispatch(args, ctx, run, x) {
|
|
|
2933
2977
|
return dispatchProgram(prog, args, rest, pathLike, ctx, run, x);
|
|
2934
2978
|
}
|
|
2935
2979
|
function dispatchProgram(prog, args, rest, pathLike, ctx, run, x) {
|
|
2980
|
+
// A3: delete verbs, pwsh / cmd / iex / Start-Process text. Adds hits only; the program is then handled as before.
|
|
2981
|
+
windowsShell(args, rest, ctx, run, x);
|
|
2936
2982
|
// Task runners and builders run project code the mapper does not read (audit T1/T3).
|
|
2937
2983
|
if (tables_1.HEAD_RUNNERS.has(prog) || tables_1.HEAD_CHANGERS.has(prog))
|
|
2938
2984
|
run.headMayChange = true;
|
|
@@ -2954,6 +3000,11 @@ function dispatchProgram(prog, args, rest, pathLike, ctx, run, x) {
|
|
|
2954
3000
|
zipCommandTexts(rest, ctx, run);
|
|
2955
3001
|
// I3: programs that change something outside this machine (held only while Marrow is unreachable).
|
|
2956
3002
|
outageRiskTags(prog, rest, run);
|
|
3003
|
+
// WK-5: the dashed spellings of `git apply` and `git am`. They were unknown programs on master, which read a file redirected into them as a script and marked the head as
|
|
3004
|
+
// changeable: that scan still runs after this. They are no `case` label and no KNOWN_COMMANDS member on purpose: the keyless normalizer keeps its own copy of that table
|
|
3005
|
+
// (src/normalized-action.ts, test/n5-full-scope.test.js rule P) and test/seatbelt-patch-r2.test.js wants every case label in the table.
|
|
3006
|
+
if (prog === 'git-apply' || prog === 'git-am')
|
|
3007
|
+
patchFamilyWrites(prog === 'git-am' ? 'am' : 'apply', rest, ctx, run, x, new Map());
|
|
2957
3008
|
switch (prog) {
|
|
2958
3009
|
case 'cd':
|
|
2959
3010
|
case 'pushd': return changeDir(rest, ctx, run, x.cmd.subshell);
|
|
@@ -3085,6 +3136,8 @@ function dispatchProgram(prog, args, rest, pathLike, ctx, run, x) {
|
|
|
3085
3136
|
}
|
|
3086
3137
|
return;
|
|
3087
3138
|
}
|
|
3139
|
+
// WK-5: the files these write are named inside the patch text.
|
|
3140
|
+
case 'patch': return patchFamilyWrites('patch', rest, ctx, run, x);
|
|
3088
3141
|
default: break;
|
|
3089
3142
|
}
|
|
3090
3143
|
// An awk by a name the table does not have (`original-awk`, `goawk`, `my-awk`): read as awk, and then as the unknown program it also is (as `nawk` is).
|
|
@@ -3302,6 +3355,9 @@ function dynamicProgram(args, ctx, run, x) {
|
|
|
3302
3355
|
function isPureSubstitution(a) {
|
|
3303
3356
|
return a.text === null && a.pattern === DYN && !!a.word && a.word.slots.length === 1 && a.word.slots[0].type === 'sub';
|
|
3304
3357
|
}
|
|
3358
|
+
// The dash spellings of `git apply` and `git am` have a handler (the switch in dispatch) but are not in KNOWN_COMMANDS: the keyless normalizer keeps its own
|
|
3359
|
+
// copy of that table (src/normalized-action.ts) and test/n5-full-scope.test.js rule P needs the two to agree, so the names stay out of it. A wrapper's tail still reads them.
|
|
3360
|
+
const PATCH_DASH_PROGRAMS = new Set(['git-apply', 'git-am']);
|
|
3305
3361
|
// A word that starts a command the mapper has a handler for (or a script path).
|
|
3306
3362
|
function isCommandWord(a, run) {
|
|
3307
3363
|
if (a.text === null)
|
|
@@ -3309,8 +3365,13 @@ function isCommandWord(a, run) {
|
|
|
3309
3365
|
const base = progName(a, run) || '';
|
|
3310
3366
|
if (NESTED_NOT_PROGRAMS.has(base))
|
|
3311
3367
|
return false;
|
|
3312
|
-
if (tables_1.KNOWN_COMMANDS.has(base) || /^python3\.\d+$/.test(base) || /^mkfs(?:\.|$)/.test(base))
|
|
3368
|
+
if (tables_1.KNOWN_COMMANDS.has(base) || PATCH_DASH_PROGRAMS.has(base) || /^python3\.\d+$/.test(base) || /^mkfs(?:\.|$)/.test(base))
|
|
3313
3369
|
return true;
|
|
3370
|
+
if (run.env.platform === 'win32') {
|
|
3371
|
+
const win = (0, tables_1.windowsProgramName)(a.text);
|
|
3372
|
+
if (win !== base && (tables_1.KNOWN_COMMANDS.has(win) || PATCH_DASH_PROGRAMS.has(win)) && !NESTED_NOT_PROGRAMS.has(win))
|
|
3373
|
+
return true;
|
|
3374
|
+
}
|
|
3314
3375
|
if (a.text.includes('/')) {
|
|
3315
3376
|
const name = base.toLowerCase();
|
|
3316
3377
|
if (tables_1.NON_SCRIPT_NAMES.has(name))
|
|
@@ -3583,7 +3644,7 @@ function unknownProgram(prog, args, ctx, run, x) {
|
|
|
3583
3644
|
if (tables_1.INERT_PROGRAMS.has(prog) || ctx.backstop)
|
|
3584
3645
|
return;
|
|
3585
3646
|
const rest = args.slice(1);
|
|
3586
|
-
scanArgvForCommands(rest, ctx, run, x, false, env_dump_1.EXEC_WRAPPERS.has(prog));
|
|
3647
|
+
scanArgvForCommands(rest, tables_1.PS_OUTPUT_CMDLETS.has(prog.toLowerCase()) || (ctx.ps !== undefined && tables_1.PS_BLOCK_WORDS.has(prog.toLowerCase())) ? { ...ctx, a3Quiet: true } : ctx, run, x, false, env_dump_1.EXEC_WRAPPERS.has(prog));
|
|
3587
3648
|
// stdin text: `echo 'npm publish' | at now`, `at now <<< 'npm publish'`, `batch <<EOF …`.
|
|
3588
3649
|
const src = stdinSource(x, ctx, run);
|
|
3589
3650
|
// Decoded text piped into a program that runs what it reads (at, batch, crontab, a function) or
|
|
@@ -3741,12 +3802,15 @@ function fetcherCommand(cmd) {
|
|
|
3741
3802
|
// A shell or interpreter that reads its program from stdin: a heredoc, a here-string (audit F7), or a
|
|
3742
3803
|
// pipeline. The Q4 pass (`curl … | sh`, flag remote_exec) covers exactly a fetcher that is the stage
|
|
3743
3804
|
// directly upstream of the shell (audit F8). Any other stage is classified by what it emits.
|
|
3744
|
-
function stdinExec(prog, rest, x, ctx, run, viaCode = false) {
|
|
3745
|
-
|
|
3805
|
+
function stdinExec(prog, rest, x, ctx, run, viaCode = false, ps = false) {
|
|
3806
|
+
// ps (A3): the program is pwsh / powershell / cmd: its stdin text and script file are PowerShell / cmd text.
|
|
3807
|
+
const asShell = ps || tables_1.SHELLS.has(prog) || prog === 'sh';
|
|
3746
3808
|
// `viaCode`: interpreter code that reads its own stdin and runs it (`python3 -c 'exec(sys.stdin.read())'`):
|
|
3747
3809
|
// the text may be shell text for os.system as well as code.
|
|
3748
3810
|
const code = (text) => {
|
|
3749
|
-
if (
|
|
3811
|
+
if (ps)
|
|
3812
|
+
psNested(text, ctx, run);
|
|
3813
|
+
else if (asShell)
|
|
3750
3814
|
evalShell(text, childCtx(ctx), run);
|
|
3751
3815
|
else {
|
|
3752
3816
|
scanInterpreterCode(text, run);
|
|
@@ -3757,6 +3821,10 @@ function stdinExec(prog, rest, x, ctx, run, viaCode = false) {
|
|
|
3757
3821
|
// A file the program reads as its program text (`bash -s < f`, `python3 < f`, or a file that a pipe
|
|
3758
3822
|
// into a group carries): a shell script is evaluated, interpreter code is scanned.
|
|
3759
3823
|
const fileCode = (f) => {
|
|
3824
|
+
if (ps) {
|
|
3825
|
+
powershellScript(f, ctx, run);
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3760
3828
|
if (asShell) {
|
|
3761
3829
|
scriptFile(f, rest, ctx, run, x);
|
|
3762
3830
|
return;
|
|
@@ -4737,11 +4805,51 @@ function checkProtectedWrite(a, ctx, run, reason, tree) {
|
|
|
4737
4805
|
}
|
|
4738
4806
|
return false;
|
|
4739
4807
|
}
|
|
4740
|
-
function markWritten(a, ctx) {
|
|
4808
|
+
function markWritten(a, ctx, into = ctx.written) {
|
|
4741
4809
|
if (a.text === null)
|
|
4742
4810
|
return;
|
|
4743
4811
|
for (const c of ctx.cwds)
|
|
4744
|
-
|
|
4812
|
+
into.add(path.posix.resolve(c || '/__unknown_cwd__', a.text));
|
|
4813
|
+
}
|
|
4814
|
+
// WK-5 patch 2: what `git checkout` and `git restore` rewrite on this line, for the patch readers only (the script,
|
|
4815
|
+
// package.json and Makefile readers use `ctx.written` and keep their answers). One plain file name adds its path, and
|
|
4816
|
+
// the reader tests a patch file by prefix, so a folder named here (`patches`, `.`) covers every file under it. A word
|
|
4817
|
+
// that is not one plain name makes the whole line a rewrite of anything (CHECKED_OUT_ANY, never an absolute path): a
|
|
4818
|
+
// variable or substitution, a glob (quoted too: git reads a pathspec as a glob), an empty word, pathspec magic (`:/`,
|
|
4819
|
+
// `:(glob)…`), a name that cannot be made absolute, or `--pathspec-from-file` (any abbreviation git reads), whose list
|
|
4820
|
+
// the mapper does not read.
|
|
4821
|
+
const CHECKED_OUT_ANY = '*';
|
|
4822
|
+
function markCheckedOut(args, ctx, run) {
|
|
4823
|
+
// After a bare `--` every word is a path, even one that starts with a dash (`git checkout main -- -x.diff`).
|
|
4824
|
+
let paths = false;
|
|
4825
|
+
for (const a of args) {
|
|
4826
|
+
const lead = a.text === null ? a.pattern : a.text;
|
|
4827
|
+
if (!paths && lead === '--') {
|
|
4828
|
+
paths = true;
|
|
4829
|
+
continue;
|
|
4830
|
+
}
|
|
4831
|
+
if (!paths && lead.startsWith('-')) {
|
|
4832
|
+
const long = lead.split('=')[0];
|
|
4833
|
+
if (long.length > 2 && '--pathspec-from-file'.startsWith(long))
|
|
4834
|
+
ctx.checkedOut.add(CHECKED_OUT_ANY);
|
|
4835
|
+
continue;
|
|
4836
|
+
}
|
|
4837
|
+
const abs = a.text === null || a.glob || a.text === '' || a.text.startsWith(':') || /[*?[\\{]/.test(a.text) ? null : absCandidates(a.text, ctx, run);
|
|
4838
|
+
if (abs === null) {
|
|
4839
|
+
ctx.checkedOut.add(CHECKED_OUT_ANY);
|
|
4840
|
+
continue;
|
|
4841
|
+
}
|
|
4842
|
+
markWritten(a, ctx, ctx.checkedOut);
|
|
4843
|
+
for (const p of abs)
|
|
4844
|
+
ctx.checkedOut.add(p);
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
// True when a patch file at `p` may have been rewritten by a `git checkout` or `git restore` earlier on this line.
|
|
4848
|
+
function checkedOutCovers(ctx, p) {
|
|
4849
|
+
if (ctx.checkedOut.has(CHECKED_OUT_ANY))
|
|
4850
|
+
return true;
|
|
4851
|
+
const base = path.posix.basename(p);
|
|
4852
|
+
return [...ctx.checkedOut].some((w) => path.posix.basename(w) === base || isAncestorOrSelf(w, p));
|
|
4745
4853
|
}
|
|
4746
4854
|
// ---------------------------------------------------------------------------------------------
|
|
4747
4855
|
// S5 workspace boundary
|
|
@@ -6185,6 +6293,197 @@ function targetDirFlag(rest) {
|
|
|
6185
6293
|
return null;
|
|
6186
6294
|
}
|
|
6187
6295
|
// ---------------------------------------------------------------------------------------------
|
|
6296
|
+
// WK-5: `patch`, `git apply` and `git am` write the files their patch text names.
|
|
6297
|
+
//
|
|
6298
|
+
// The names are inside the patch file, not in the command, so the mapper reads a literal patch file (the bounded,
|
|
6299
|
+
// no-symlink reader the script checks use) and checks every name like the destination of tee, cp and mv. What it
|
|
6300
|
+
// cannot know for certain is an unknown write: S0, reason `<label>_unknown_<why>`, never a pass. That covers patch
|
|
6301
|
+
// text on stdin (heredoc, here-string, pipe, process substitution, a group or shell that feeds it), a dynamic or glob
|
|
6302
|
+
// word, an unreadable, missing, non-regular, oversize (LIMITS.maxPatchBytes) or just-written file, a header the
|
|
6303
|
+
// reader doubts, an option the parser does not list, and a working folder or git work tree the mapper cannot name.
|
|
6304
|
+
// A dry run (`patch --dry-run`, `git apply --check/--stat/--numstat/--summary`) writes nothing and keeps the old
|
|
6305
|
+
// answer, but only when every option is known, none repeats, negates or turns the apply back on, and the call has
|
|
6306
|
+
// no extra output file.
|
|
6307
|
+
//
|
|
6308
|
+
// Residual (same as the script-file reads): a patch file rewritten earlier in the same command line by a program the
|
|
6309
|
+
// mapper does not model (an interpreter, an unknown tool) is read as it is on disk now.
|
|
6310
|
+
function patchFamilyWrites(kind, args, ctx, run, x, overrides) {
|
|
6311
|
+
// Files on another machine (`ssh host 'patch …'`) are not local files: the answer stays what it was (like protectedOperandCheck).
|
|
6312
|
+
if (ctx.remote)
|
|
6313
|
+
return;
|
|
6314
|
+
const label = kind === 'patch' ? 'patch_write' : kind === 'apply' ? 'git_apply_write' : 'git_am_write';
|
|
6315
|
+
const unknown = (why) => { hit(run, 'S0', `${label}_unknown_${why}`); };
|
|
6316
|
+
const ownStdin = x.cmd.heredoc !== null || x.cmd.redirects.some((r) => r.op.startsWith('<')) || x.info.upstream.length > 0 || Boolean(x.info.lost) || Boolean(ctx.stdinFeed);
|
|
6317
|
+
const readsFile = (text) => (absCandidates(text, ctx, run) || []).some((p) => readCached(run, p, tables_1.LIMITS.maxPatchBytes) !== null);
|
|
6318
|
+
// Reached through the tail-position scan of an unknown wrapper, a bare `patch` word is data (`poetry version patch`,
|
|
6319
|
+
// `semver -i patch 1.2.3`): with no option, no stdin source and no operand that is a file, it is not a patch call.
|
|
6320
|
+
if (kind === 'patch' && ctx.backstop && !ownStdin && args.every((a) => a.text !== null && !a.glob && !a.text.startsWith('-') && !readsFile(a.text)))
|
|
6321
|
+
return;
|
|
6322
|
+
if (args.some((a) => a.text === null || a.glob)) {
|
|
6323
|
+
unknown('dynamic_word');
|
|
6324
|
+
return;
|
|
6325
|
+
}
|
|
6326
|
+
const call = (0, patch_text_1.parsePatchCall)(kind, args.map((a) => a.text));
|
|
6327
|
+
if (!call.ok) {
|
|
6328
|
+
unknown(call.why);
|
|
6329
|
+
return;
|
|
6330
|
+
}
|
|
6331
|
+
if (call.info || call.readOnly)
|
|
6332
|
+
return;
|
|
6333
|
+
if (ctx.cwds.length === 0 || ctx.cwds.some((c) => c === null) || ctx.cwdStrict === true) {
|
|
6334
|
+
unknown('cwd');
|
|
6335
|
+
return;
|
|
6336
|
+
}
|
|
6337
|
+
if (kind !== 'patch') {
|
|
6338
|
+
// Another repository, work tree or configuration (`--work-tree`, GIT_WORK_TREE, core.worktree): the files land elsewhere.
|
|
6339
|
+
const moved = (overrides !== undefined && overrides.has(CONFIG_UNKNOWN))
|
|
6340
|
+
|| ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE'].some((name) => envValue(x, ctx, name) !== undefined);
|
|
6341
|
+
if (moved) {
|
|
6342
|
+
unknown('git_repository');
|
|
6343
|
+
return;
|
|
6344
|
+
}
|
|
6345
|
+
for (const c of ctx.cwds) {
|
|
6346
|
+
const configured = gitAll(run, ctx, c, 'core.worktree', overrides);
|
|
6347
|
+
if (configured === null || configured.length) {
|
|
6348
|
+
unknown('git_worktree');
|
|
6349
|
+
return;
|
|
6350
|
+
}
|
|
6351
|
+
}
|
|
6352
|
+
}
|
|
6353
|
+
const dir = call.dirs.length === 1 && call.dirs[0] !== '' ? call.dirs[0] : null;
|
|
6354
|
+
// `..` in the folder is a form the reader does not follow (git apply refuses it; patch writes outside the tree).
|
|
6355
|
+
if (dir !== null && dir.split('/').includes('..')) {
|
|
6356
|
+
unknown('dotdot');
|
|
6357
|
+
return;
|
|
6358
|
+
}
|
|
6359
|
+
// Where the patch comes from. patch: -i FILE, or its second operand, or stdin; its first operand is a file it writes.
|
|
6360
|
+
// git: every operand is a patch file (`-` is stdin), none at all is stdin.
|
|
6361
|
+
let patchFiles = [];
|
|
6362
|
+
let fromStdin = false;
|
|
6363
|
+
let originals = [];
|
|
6364
|
+
if (kind === 'patch') {
|
|
6365
|
+
if (call.inputs.length === 1) {
|
|
6366
|
+
originals = call.operands;
|
|
6367
|
+
if (call.inputs[0] === '-')
|
|
6368
|
+
fromStdin = true;
|
|
6369
|
+
else
|
|
6370
|
+
patchFiles = [call.inputs[0]];
|
|
6371
|
+
}
|
|
6372
|
+
else if (call.operands.length > 2) {
|
|
6373
|
+
unknown('operands');
|
|
6374
|
+
return;
|
|
6375
|
+
}
|
|
6376
|
+
else if (call.operands.length === 2) {
|
|
6377
|
+
originals = [call.operands[0]];
|
|
6378
|
+
if (call.operands[1] === '-')
|
|
6379
|
+
fromStdin = true;
|
|
6380
|
+
else
|
|
6381
|
+
patchFiles = [call.operands[1]];
|
|
6382
|
+
}
|
|
6383
|
+
else {
|
|
6384
|
+
originals = call.operands;
|
|
6385
|
+
fromStdin = true;
|
|
6386
|
+
}
|
|
6387
|
+
// GNU patch changes directory first, then opens -i FILE: which folder a relative file is read from is not certain.
|
|
6388
|
+
if (dir !== null && patchFiles.some((f) => !f.startsWith('/') && !f.startsWith('~'))) {
|
|
6389
|
+
unknown('dir_relative_input');
|
|
6390
|
+
return;
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6393
|
+
else {
|
|
6394
|
+
patchFiles = call.operands.filter((o) => o !== '-');
|
|
6395
|
+
fromStdin = call.operands.length === 0 || call.operands.includes('-');
|
|
6396
|
+
}
|
|
6397
|
+
const files = patchFiles.map((f) => litArg(f));
|
|
6398
|
+
if (fromStdin) {
|
|
6399
|
+
// Exactly one plain `< file` on this command, and nothing else feeding stdin.
|
|
6400
|
+
const ins = x.cmd.redirects.filter((r) => r.op.startsWith('<'));
|
|
6401
|
+
const one = ins.length === 1 && ins[0].op === '<' && (ins[0].fd === null || ins[0].fd === '0') && ins[0].target && !ins[0].target.substitution ? ins[0].target : null;
|
|
6402
|
+
const target = one ? resolveWord(one, ctx, run) : null;
|
|
6403
|
+
if (x.cmd.heredoc !== null || x.info.upstream.length > 0 || x.info.lost || ctx.stdinFeed || target === null || target.text === null || target.glob) {
|
|
6404
|
+
unknown('stdin');
|
|
6405
|
+
return;
|
|
6406
|
+
}
|
|
6407
|
+
files.push(litArg(target.text));
|
|
6408
|
+
}
|
|
6409
|
+
const texts = [];
|
|
6410
|
+
for (const f of files) {
|
|
6411
|
+
const abs = absCandidates(f.text, ctx, run);
|
|
6412
|
+
if (!abs || abs.length === 0 || abs.some((p) => p.startsWith('/__unknown_cwd__'))) {
|
|
6413
|
+
unknown('path');
|
|
6414
|
+
return;
|
|
6415
|
+
}
|
|
6416
|
+
for (const p of abs) {
|
|
6417
|
+
// A file this command line writes first (or a name `ln -s` made) is not the file the mapper can read now.
|
|
6418
|
+
const base = path.posix.basename(p);
|
|
6419
|
+
if (ctx.written.has(p) || [...ctx.written].some((w) => path.posix.basename(w) === base) || checkedOutCovers(ctx, p)) {
|
|
6420
|
+
unknown('written_here');
|
|
6421
|
+
return;
|
|
6422
|
+
}
|
|
6423
|
+
if (run.links.size > 0 && applyLinks(p, run) !== p) {
|
|
6424
|
+
unknown('link');
|
|
6425
|
+
return;
|
|
6426
|
+
}
|
|
6427
|
+
const text = readCached(run, p, tables_1.LIMITS.maxPatchBytes);
|
|
6428
|
+
if (text === null) {
|
|
6429
|
+
unknown('unreadable');
|
|
6430
|
+
return;
|
|
6431
|
+
}
|
|
6432
|
+
texts.push(text);
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6435
|
+
const writes = new Set([...call.extraWrites, ...originals]);
|
|
6436
|
+
for (const text of texts) {
|
|
6437
|
+
const names = (0, patch_text_1.readPatchNames)(text, kind === 'am');
|
|
6438
|
+
if (names.doubt !== null) {
|
|
6439
|
+
unknown(`header_${names.doubt}`);
|
|
6440
|
+
return;
|
|
6441
|
+
}
|
|
6442
|
+
for (const name of (0, patch_text_1.targetNames)(names.names, kind, call.strip))
|
|
6443
|
+
writes.add(name);
|
|
6444
|
+
}
|
|
6445
|
+
// -d / --directory: the name is also read under that folder.
|
|
6446
|
+
const all = new Set();
|
|
6447
|
+
for (const w of writes) {
|
|
6448
|
+
all.add(w);
|
|
6449
|
+
if (dir !== null)
|
|
6450
|
+
all.add(`${dir}/${w}`);
|
|
6451
|
+
}
|
|
6452
|
+
if (all.size > patch_text_1.MAX_NAMES * 4) {
|
|
6453
|
+
unknown('too_many_targets');
|
|
6454
|
+
return;
|
|
6455
|
+
}
|
|
6456
|
+
// `git am` always, and `git apply` for a git diff run in a sub-folder, name files from the top of the work tree, not from
|
|
6457
|
+
// the working folder (apply keeps the ones under that folder). A home-anchored protected path (`~/.local/bin/**`) is then
|
|
6458
|
+
// read under the top as well as under the folder, for every folder the call may run in. A top that cannot be found is
|
|
6459
|
+
// an unknown write.
|
|
6460
|
+
const tops = [];
|
|
6461
|
+
if (kind !== 'patch') {
|
|
6462
|
+
for (const c of ctx.cwds) {
|
|
6463
|
+
let top;
|
|
6464
|
+
try {
|
|
6465
|
+
top = run.env.git.toplevel(c);
|
|
6466
|
+
}
|
|
6467
|
+
catch {
|
|
6468
|
+
unknown('git_toplevel');
|
|
6469
|
+
return;
|
|
6470
|
+
}
|
|
6471
|
+
if (top !== null && path.posix.isAbsolute(top) && top !== c && !tops.includes(top))
|
|
6472
|
+
tops.push(top);
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
for (const w of all) {
|
|
6476
|
+
if (clock(run))
|
|
6477
|
+
return;
|
|
6478
|
+
const arg = litArg(w);
|
|
6479
|
+
checkProtectedWrite(arg, ctx, run, label, false);
|
|
6480
|
+
if (!w.startsWith('/') && !w.startsWith('~'))
|
|
6481
|
+
for (const top of tops)
|
|
6482
|
+
checkProtectedWrite(litArg(path.posix.join(top, w)), ctx, run, label, false);
|
|
6483
|
+
markWritten(arg, ctx);
|
|
6484
|
+
}
|
|
6485
|
+
}
|
|
6486
|
+
// ---------------------------------------------------------------------------------------------
|
|
6188
6487
|
// builtins
|
|
6189
6488
|
// `cd /proc/$$`, `cd /proc/$(pgrep node)/task/*`, `cd $PID` from `/proc`: a target that is a process or thread id (a dynamic part or a glob) under
|
|
6190
6489
|
// `/proc` is also read as `self`, so a later `cat environ` names the process file. The unknown folder stays possible, as before.
|
|
@@ -9436,7 +9735,13 @@ function handleGit(rest, ctx, run, x, cOverrides, aliasDepth) {
|
|
|
9436
9735
|
for (const a of args)
|
|
9437
9736
|
if (a.text === null || !a.text.startsWith('-'))
|
|
9438
9737
|
checkProtectedWrite(a, gctx, run, `git_${sub}`, sub === 'rm' || sub === 'mv');
|
|
9738
|
+
// WK-5: checkout and restore rewrite the files they name (a ref among the words only adds a name nothing reads).
|
|
9739
|
+
if (sub === 'checkout' || sub === 'restore')
|
|
9740
|
+
markCheckedOut(args, gctx, run);
|
|
9439
9741
|
}
|
|
9742
|
+
// WK-5: `git apply` and `git am` write the files their patch text names.
|
|
9743
|
+
if (sub === 'apply' || sub === 'am')
|
|
9744
|
+
patchFamilyWrites(sub, args, gctx, run, x, overrides);
|
|
9440
9745
|
// `git mv link y` moves the symlink like `mv` does (audit N3).
|
|
9441
9746
|
if (sub === 'mv')
|
|
9442
9747
|
carryLinks('mv', args, gctx, run);
|
|
@@ -11752,4 +12057,536 @@ function marrowExtract(text) {
|
|
|
11752
12057
|
out[k] = v;
|
|
11753
12058
|
return JSON.stringify(Object.keys(out).sort().map((k) => [k, out[k]]));
|
|
11754
12059
|
}
|
|
12060
|
+
// ---------------------------------------------------------------------------------------------
|
|
12061
|
+
// A3: PowerShell and cmd text (Windows hotfix). Every function here only ADDS hits: the unchanged readings of the
|
|
12062
|
+
// same text still run, so no existing stop can go away.
|
|
12063
|
+
const DYN_ALL = new RegExp(DYN, 'g');
|
|
12064
|
+
const DYN_PLACEHOLDER = '${__marrow_dynamic}';
|
|
12065
|
+
/** An argument as text: the literal, or the literal parts with a placeholder for each unknown piece. */
|
|
12066
|
+
function dynText(a) { return a.text !== null ? a.text : a.pattern.replace(DYN_ALL, DYN_PLACEHOLDER); }
|
|
12067
|
+
/** The event's own shell text on native Windows or from the PowerShell / cmd tool: the usual reading, as PowerShell / cmd text. */
|
|
12068
|
+
function psTop(text, run) {
|
|
12069
|
+
const ctx = newCtx(run);
|
|
12070
|
+
ctx.ps = 1;
|
|
12071
|
+
evalShell(text, ctx, run);
|
|
12072
|
+
psExtras(text, ctx, run);
|
|
12073
|
+
}
|
|
12074
|
+
/** Text a PowerShell or cmd command line runs: pwsh -c, cmd /c, iex, Start-Process, a decoded -EncodedCommand, a script file. */
|
|
12075
|
+
function psNested(text, ctx, run) {
|
|
12076
|
+
if (clock(run))
|
|
12077
|
+
return;
|
|
12078
|
+
const level = (ctx.ps || 0) + 1;
|
|
12079
|
+
if (level > tables_1.LIMITS.maxPsDepth) {
|
|
12080
|
+
worseParse(run, 'too_large');
|
|
12081
|
+
run.flags.add('depth_limit');
|
|
12082
|
+
hit(run, 'Sx', 'budget_depth');
|
|
12083
|
+
return;
|
|
12084
|
+
}
|
|
12085
|
+
const c = childCtx(ctx, { ps: level });
|
|
12086
|
+
evalShell(text, c, run);
|
|
12087
|
+
psExtras(text, c, run);
|
|
12088
|
+
}
|
|
12089
|
+
/** Code text of a pwsh -Command / cmd /c: a command line that is nothing but unknown pieces cannot be read. */
|
|
12090
|
+
function psCommandText(text, ctx, run) {
|
|
12091
|
+
if (text.trim() === '')
|
|
12092
|
+
return;
|
|
12093
|
+
if (text.split(DYN_PLACEHOLDER).join('').trim() === '') {
|
|
12094
|
+
worseParse(run, 'dynamic');
|
|
12095
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12096
|
+
return;
|
|
12097
|
+
}
|
|
12098
|
+
psNested(text, ctx, run);
|
|
12099
|
+
}
|
|
12100
|
+
// The other readings of PowerShell / cmd text, next to the one evalShell already did: the text without its escape
|
|
12101
|
+
// characters and (on native Windows) with its backslashes kept, both as complete readings; the text with its block braces
|
|
12102
|
+
// turned into line breaks as a probe (only the A3 hits count, so `{ $_.Name }` does not become a dynamic command); and the
|
|
12103
|
+
// stops for text bash cannot read, for `& $x`, and for tree deletes through .NET.
|
|
12104
|
+
function psExtras(text, c, run) {
|
|
12105
|
+
if (run.exhausted || Buffer.byteLength(text) > tables_1.LIMITS.maxCommandBytes)
|
|
12106
|
+
return;
|
|
12107
|
+
const cleaned = (0, tables_1.psClean)(text);
|
|
12108
|
+
const flat = (0, tables_1.psFlatten)(cleaned);
|
|
12109
|
+
// PowerShell keeps a backslash as a plain character, even before a quote: the readings with every backslash doubled show
|
|
12110
|
+
// the text as PowerShell reads it (`"C:\"` is a whole string, `C:\tools\git.exe` one program word).
|
|
12111
|
+
const keep = (t) => t.replace(/\\/g, '\\\\');
|
|
12112
|
+
const faithful = new Set([keep(text), keep(cleaned)]);
|
|
12113
|
+
const full = new Set(faithful);
|
|
12114
|
+
full.add(cleaned);
|
|
12115
|
+
full.delete(text);
|
|
12116
|
+
for (const reading of full) {
|
|
12117
|
+
if (clock(run))
|
|
12118
|
+
return;
|
|
12119
|
+
evalShell(reading, childCtx(c), run);
|
|
12120
|
+
}
|
|
12121
|
+
if (flat !== cleaned && flat !== text)
|
|
12122
|
+
psProbe(flat, c, run);
|
|
12123
|
+
psLexical((0, tables_1.psNormalize)(text), cleaned, c, run, true);
|
|
12124
|
+
if (tables_1.PS_DYNAMIC_CALL.test(cleaned) || (0, tables_1.psDynamicCode)(cleaned)) {
|
|
12125
|
+
worseParse(run, 'dynamic');
|
|
12126
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12127
|
+
}
|
|
12128
|
+
if ((0, tables_1.psDefaultParameters)(cleaned) || PS_ALIAS_DRIVE.test(cleaned))
|
|
12129
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12130
|
+
}
|
|
12131
|
+
// An alias made through the Alias: drive (`Set-Item Alias:z Remove-Item`, `New-Item -Path Alias:z`, `${alias:z} = ...`) hides a delete verb like Set-Alias does.
|
|
12132
|
+
const PS_ALIAS_DRIVE = /(?:\b(?:set-item|new-item|copy-item|rename-item|move-item|set-content|si|ni|cpi|rni|mi|sc)\b[^\n;|]{0,200}\balias:|\$\{alias:)/i;
|
|
12133
|
+
// The always-on lexical check of PowerShell / cmd text (rules i to iv and vi), independent of how bash parses it: a PowerShell
|
|
12134
|
+
// reading and a cmd reading of the statements, with quotes as data. A delete verb is judged where a command starts (the first
|
|
12135
|
+
// word of a statement, or any word of an if / for / foreach / call / start statement). Text the tokenizer does not read at all
|
|
12136
|
+
// (an unterminated quote, a here-string, a block comment) stops when it holds a delete verb or `iex` anywhere.
|
|
12137
|
+
function psLexical(raw, cleaned, c, run, inPs) {
|
|
12138
|
+
if (run.psOver)
|
|
12139
|
+
return;
|
|
12140
|
+
let readable = true;
|
|
12141
|
+
for (const cmdMode of [false, true]) {
|
|
12142
|
+
if (clock(run))
|
|
12143
|
+
return;
|
|
12144
|
+
// The tokenizer reads the escape characters (backtick, caret) itself, so an escaped quote keeps its meaning.
|
|
12145
|
+
const { statements, ok } = (0, tables_1.psStatements)(raw, cmdMode);
|
|
12146
|
+
if (!ok)
|
|
12147
|
+
readable = false;
|
|
12148
|
+
for (const st of statements) {
|
|
12149
|
+
const w = st.words;
|
|
12150
|
+
// Work is counted per statement and per word, and a statement of more words than one command may have stops like it does.
|
|
12151
|
+
if (psSpend(run, 1 + w.length, w.length > tables_1.LIMITS.maxWords))
|
|
12152
|
+
return;
|
|
12153
|
+
const open = tables_1.PS_KEYWORDS.has(w[0].toLowerCase());
|
|
12154
|
+
// An argument list written as an array (`@('/c', 'rd /s /q x')`) is a statement of its own that starts with the switch.
|
|
12155
|
+
if (w.length > 1 && /^[/-][a-z]/i.test(w[0])) {
|
|
12156
|
+
const launched = /^\//.test(w[0]) ? cmdSwitchText(w.map(litArg)) : ((0, tables_1.psOption)(w[0])?.kind === 'command' ? w.slice(1).join(' ') : null);
|
|
12157
|
+
if (launched !== null && launched.trim() !== '')
|
|
12158
|
+
psCommandText(launched, c, run);
|
|
12159
|
+
}
|
|
12160
|
+
// Both answers that look at the words after a program word are read from one pass over the statement, not one per word.
|
|
12161
|
+
let deleteStops = null;
|
|
12162
|
+
let switchAt = null;
|
|
12163
|
+
for (let p = 0; p < (open ? w.length : 1); p += 1) {
|
|
12164
|
+
if ((p & 63) === 63 && clock(run))
|
|
12165
|
+
return;
|
|
12166
|
+
const name = (0, tables_1.windowsProgramName)(w[p]);
|
|
12167
|
+
const prev = p > 0 ? w[p - 1].toLowerCase() : '';
|
|
12168
|
+
// cmd: a command word that is a variable (`%X%`, `!X!`, a loop variable `%a`) can be anything. After if / for ... the
|
|
12169
|
+
// command starts after the condition, so a variable that is followed by an option (`/s`, `-r`) counts too.
|
|
12170
|
+
if (cmdMode && CMD_DYNAMIC_WORD.test(w[p]) && (p === 0 || prev === 'call' || prev === 'do' || prev === 'then' || prev === 'else' || prev === 'start'
|
|
12171
|
+
|| (open && w[p + 1] !== undefined && /^[/-]/.test(w[p + 1])))) {
|
|
12172
|
+
worseParse(run, 'dynamic');
|
|
12173
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12174
|
+
}
|
|
12175
|
+
if (tables_1.WIN_DELETE_VERBS.has(name) || (inPs && tables_1.WIN_SHELL_DELETE_VERBS.has(name))) {
|
|
12176
|
+
if (deleteStops === null)
|
|
12177
|
+
deleteStops = (0, tables_1.windowsDeleteStopsFrom)(w, st.piped);
|
|
12178
|
+
if (deleteStops(p))
|
|
12179
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12180
|
+
}
|
|
12181
|
+
else if (name === 'iex' || name === 'invoke-expression') {
|
|
12182
|
+
const code = w[p + 1];
|
|
12183
|
+
if (code === undefined || st.piped || code.includes('$')) {
|
|
12184
|
+
worseParse(run, 'dynamic');
|
|
12185
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12186
|
+
}
|
|
12187
|
+
else
|
|
12188
|
+
psNested(code, c, run);
|
|
12189
|
+
}
|
|
12190
|
+
else if (name === 'cmd') {
|
|
12191
|
+
if (switchAt === null)
|
|
12192
|
+
switchAt = cmdSwitchIndexes(w);
|
|
12193
|
+
const at = switchAt[p + 1];
|
|
12194
|
+
// The command line is the rest of the statement from the switch on: its words are work too.
|
|
12195
|
+
if (at >= 0 && !psSpend(run, w.length - at, false))
|
|
12196
|
+
cmdLine(cmdSwitchLine(w, at), c, run);
|
|
12197
|
+
if (run.psOver)
|
|
12198
|
+
return;
|
|
12199
|
+
}
|
|
12200
|
+
}
|
|
12201
|
+
}
|
|
12202
|
+
}
|
|
12203
|
+
if (!readable && inPs && tables_1.WIN_DELETE_WORDS.test(cleaned))
|
|
12204
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12205
|
+
if (!readable && inPs && tables_1.PS_EVAL_WORDS.test(cleaned)) {
|
|
12206
|
+
worseParse(run, 'dynamic');
|
|
12207
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12208
|
+
}
|
|
12209
|
+
}
|
|
12210
|
+
// The work budget of psLexical, for the whole run: one step per statement and per word, 64 times the word budget of one
|
|
12211
|
+
// command. Over it, or over the word budget in one statement, psLexical stops reading and the run records the same stop
|
|
12212
|
+
// as any other word budget (Sx.budget_words). The rest of the evaluation goes on, so the other readings keep their own hits.
|
|
12213
|
+
const PS_LEX_STEPS = tables_1.LIMITS.maxWords * 64;
|
|
12214
|
+
function psSpend(run, steps, tooWide) {
|
|
12215
|
+
if (run.psOver)
|
|
12216
|
+
return true;
|
|
12217
|
+
run.psSteps += steps;
|
|
12218
|
+
if (!tooWide && run.psSteps <= PS_LEX_STEPS)
|
|
12219
|
+
return false;
|
|
12220
|
+
run.psOver = true;
|
|
12221
|
+
worseParse(run, 'too_large');
|
|
12222
|
+
hit(run, 'Sx', 'budget_words');
|
|
12223
|
+
return true;
|
|
12224
|
+
}
|
|
12225
|
+
// A reading whose only hits are A3 hits (and the budget stops): everything else it finds is dropped.
|
|
12226
|
+
function psProbe(text, c, run) {
|
|
12227
|
+
const saved = { hits: run.hits, parse: run.parse, flags: run.flags, risk: run.risk, head: run.headMayChange };
|
|
12228
|
+
run.hits = [];
|
|
12229
|
+
run.flags = new Set(saved.flags);
|
|
12230
|
+
run.risk = new Set(saved.risk);
|
|
12231
|
+
try {
|
|
12232
|
+
evalShell(text, childCtx(c), run);
|
|
12233
|
+
}
|
|
12234
|
+
finally {
|
|
12235
|
+
const kept = run.hits.filter((h) => tables_1.A3_REASONS.has(h.reason) || h.reason.startsWith('budget_'));
|
|
12236
|
+
run.hits = saved.hits;
|
|
12237
|
+
run.parse = saved.parse;
|
|
12238
|
+
run.flags = saved.flags;
|
|
12239
|
+
run.risk = saved.risk;
|
|
12240
|
+
run.headMayChange = saved.head;
|
|
12241
|
+
if (run.exhausted || run.psOver)
|
|
12242
|
+
worseParse(run, 'too_large');
|
|
12243
|
+
for (const h of kept)
|
|
12244
|
+
hit(run, h.category, h.reason);
|
|
12245
|
+
}
|
|
12246
|
+
}
|
|
12247
|
+
// Plain shell text bash cannot read, outside PowerShell / cmd text: the cmdlet name, a script block called with `&` / `.`
|
|
12248
|
+
// around a delete verb, `iex`, and `& $x` are still stops.
|
|
12249
|
+
function psUnparsed(src, run) {
|
|
12250
|
+
const t = (0, tables_1.psClean)(src);
|
|
12251
|
+
if (tables_1.WIN_CMDLET_WORD.test(t) || (tables_1.PS_BLOCK_CALL.test(t) && tables_1.WIN_DELETE_WORDS.test(t)))
|
|
12252
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12253
|
+
if (tables_1.PS_EVAL_WORDS.test(t) || tables_1.PS_DYNAMIC_AMP.test(t))
|
|
12254
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12255
|
+
}
|
|
12256
|
+
// The arguments of a delete verb as text; `-Confirm:$false` is a literal switch value, not a variable.
|
|
12257
|
+
function deleteArgs(rest) {
|
|
12258
|
+
return rest.map((a) => {
|
|
12259
|
+
if (a.text !== null)
|
|
12260
|
+
return a.text;
|
|
12261
|
+
const m = /^(-[A-Za-z]+:)\u0001$/.exec(a.pattern);
|
|
12262
|
+
return m && a.word && a.word.variables.length === 1 && /^(?:true|false)$/i.test(a.word.variables[0]) ? `${m[1]}$${a.word.variables[0]}` : null;
|
|
12263
|
+
});
|
|
12264
|
+
}
|
|
12265
|
+
// The program of this command by its Windows name: delete verbs, cmd, pwsh / powershell, iex, Start-Process and the rest.
|
|
12266
|
+
function windowsShell(args, rest, ctx, run, x) {
|
|
12267
|
+
const first = args[0].text;
|
|
12268
|
+
if (first === null || ctx.a3Quiet === true)
|
|
12269
|
+
return;
|
|
12270
|
+
const name = (0, tables_1.windowsProgramName)(first);
|
|
12271
|
+
const win = run.env.platform === 'win32';
|
|
12272
|
+
if (tables_1.WIN_DELETE_VERBS.has(name) || ((ctx.ps !== undefined || win) && tables_1.WIN_SHELL_DELETE_VERBS.has(name))) {
|
|
12273
|
+
if ((0, tables_1.windowsDeleteStops)(deleteArgs(rest), x.cmd.pipedInput || x.info.upstream.length > 0))
|
|
12274
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12275
|
+
return;
|
|
12276
|
+
}
|
|
12277
|
+
switch (name) {
|
|
12278
|
+
case 'cmd': return handleCmd(rest, ctx, run, x);
|
|
12279
|
+
case 'pwsh':
|
|
12280
|
+
case 'powershell': return handlePowerShell(rest, ctx, run, x);
|
|
12281
|
+
case 'iex':
|
|
12282
|
+
case 'invoke-expression': return handleInvokeExpression(rest, ctx, run, x);
|
|
12283
|
+
case 'start-process':
|
|
12284
|
+
case 'saps': return handleStartProcess(rest, ctx, run);
|
|
12285
|
+
case 'invoke-command':
|
|
12286
|
+
case 'icm':
|
|
12287
|
+
psCommandText(rest.map(dynText).join(' '), ctx, run);
|
|
12288
|
+
return;
|
|
12289
|
+
case 'set-alias':
|
|
12290
|
+
case 'new-alias':
|
|
12291
|
+
case 'sal':
|
|
12292
|
+
case 'nal':
|
|
12293
|
+
case 'import-alias': {
|
|
12294
|
+
// An alias can hide the verb from every later command (and `Set-Alias z Remove-Item` is the way to do it): any alias in
|
|
12295
|
+
// PowerShell text or on native Windows is a stop, and so is an alias for a delete verb anywhere.
|
|
12296
|
+
let hides = ctx.ps !== undefined || win;
|
|
12297
|
+
for (const a of rest) {
|
|
12298
|
+
const aliased = a.text === null ? '' : (0, tables_1.windowsProgramName)(a.text);
|
|
12299
|
+
if (tables_1.WIN_DELETE_VERBS.has(aliased) || tables_1.WIN_SHELL_DELETE_VERBS.has(aliased))
|
|
12300
|
+
hides = true;
|
|
12301
|
+
}
|
|
12302
|
+
if (hides)
|
|
12303
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12304
|
+
return;
|
|
12305
|
+
}
|
|
12306
|
+
// Windows-native destructive commands, each with a POSIX analog that stops (rsync --delete, mkfs, find -delete).
|
|
12307
|
+
case 'robocopy':
|
|
12308
|
+
// `robocopy <empty folder> <target> /mir` empties the target tree; /mov and /move delete what they copy.
|
|
12309
|
+
if (rest.some((a) => a.text !== null && /^\/(?:mir|purge|mov|move)$/i.test(a.text)))
|
|
12310
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12311
|
+
return;
|
|
12312
|
+
case 'format-volume':
|
|
12313
|
+
case 'clear-disk':
|
|
12314
|
+
case 'initialize-disk':
|
|
12315
|
+
case 'remove-partition':
|
|
12316
|
+
case 'diskpart':
|
|
12317
|
+
case 'clear-recyclebin':
|
|
12318
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12319
|
+
return;
|
|
12320
|
+
case 'format':
|
|
12321
|
+
if (rest.some((a) => a.text !== null && /^[a-z]:\\?$/i.test(a.text)))
|
|
12322
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12323
|
+
return;
|
|
12324
|
+
case 'vssadmin':
|
|
12325
|
+
if (rest.some((a) => a.text !== null && /^delete$/i.test(a.text)))
|
|
12326
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12327
|
+
return;
|
|
12328
|
+
case 'wsl':
|
|
12329
|
+
if (rest.some((a) => a.text !== null && /^--unregister/i.test(a.text)))
|
|
12330
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12331
|
+
return;
|
|
12332
|
+
case 'wmic':
|
|
12333
|
+
if (rest.some((a) => a.text !== null && /^shadowcopy$/i.test(a.text)) && rest.some((a) => a.text !== null && /^delete$/i.test(a.text)))
|
|
12334
|
+
hit(run, 'S5', 'windows_shell_delete_unparsed');
|
|
12335
|
+
execWrapper(rest, ctx, run);
|
|
12336
|
+
return;
|
|
12337
|
+
// Programs whose argument strings are command lines.
|
|
12338
|
+
case 'invoke-wmimethod':
|
|
12339
|
+
case 'invoke-cimmethod':
|
|
12340
|
+
case 'schtasks':
|
|
12341
|
+
case 'forfiles':
|
|
12342
|
+
case 'conhost':
|
|
12343
|
+
case 'runas':
|
|
12344
|
+
case 'mshta':
|
|
12345
|
+
case 'cscript':
|
|
12346
|
+
case 'wscript':
|
|
12347
|
+
case 'psexec':
|
|
12348
|
+
case 'psexec64':
|
|
12349
|
+
execWrapper(rest, ctx, run);
|
|
12350
|
+
return;
|
|
12351
|
+
case 'start':
|
|
12352
|
+
if (ctx.ps !== undefined || win)
|
|
12353
|
+
execWrapper(rest, ctx, run);
|
|
12354
|
+
return;
|
|
12355
|
+
default: return;
|
|
12356
|
+
}
|
|
12357
|
+
}
|
|
12358
|
+
// The command line after cmd's /c, /k or /r switch, or null when there is none. Switches may be run together (/d/s/c) and the
|
|
12359
|
+
// text may follow the switch without a blank (/c"dir"); the words after it are the command line, joined as cmd joins them.
|
|
12360
|
+
function cmdSwitchText(rest) {
|
|
12361
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
12362
|
+
const attached = cmdSwitchAttached(rest[i].text);
|
|
12363
|
+
if (attached === null)
|
|
12364
|
+
continue;
|
|
12365
|
+
return [attached, ...rest.slice(i + 1).map(dynText)].filter((part) => part !== '').join(' ');
|
|
12366
|
+
}
|
|
12367
|
+
return null;
|
|
12368
|
+
}
|
|
12369
|
+
// The text a cmd switch word carries after its switch letter (`/c"dir"` gives `"dir"`, `/d/s/c` gives ``), or null when the word is no /c, /k or /r switch.
|
|
12370
|
+
function cmdSwitchAttached(t) {
|
|
12371
|
+
if (t === null || t[0] !== '/')
|
|
12372
|
+
return null;
|
|
12373
|
+
const parts = t.slice(1).split('/');
|
|
12374
|
+
const j = parts.findIndex((part) => /^[ckr]/i.test(part));
|
|
12375
|
+
if (j < 0)
|
|
12376
|
+
return null;
|
|
12377
|
+
return parts[j].slice(1) + (j < parts.length - 1 ? `/${parts.slice(j + 1).join('/')}` : '');
|
|
12378
|
+
}
|
|
12379
|
+
// For every position, the index of the first cmd switch word at or after it (-1 = none; one extra slot past the end): the
|
|
12380
|
+
// answer of cmdSwitchText for every word of a statement, from one pass.
|
|
12381
|
+
function cmdSwitchIndexes(words) {
|
|
12382
|
+
const next = new Int32Array(words.length + 1);
|
|
12383
|
+
next[words.length] = -1;
|
|
12384
|
+
for (let i = words.length - 1; i >= 0; i -= 1)
|
|
12385
|
+
next[i] = cmdSwitchAttached(words[i]) !== null ? i : next[i + 1];
|
|
12386
|
+
return next;
|
|
12387
|
+
}
|
|
12388
|
+
// The command line of a statement whose word `at` is a cmd switch: what cmdSwitchText gives for the words from there.
|
|
12389
|
+
function cmdSwitchLine(words, at) {
|
|
12390
|
+
return [cmdSwitchAttached(words[at]), ...words.slice(at + 1)].filter((part) => part !== '').join(' ');
|
|
12391
|
+
}
|
|
12392
|
+
// A cmd command line, read as it is and with one pair of outer quotes taken off.
|
|
12393
|
+
function cmdLine(text, ctx, run) {
|
|
12394
|
+
psCommandText(text, ctx, run);
|
|
12395
|
+
if (text.length >= 2 && text.startsWith('"') && text.endsWith('"'))
|
|
12396
|
+
psCommandText(text.slice(1, -1), ctx, run);
|
|
12397
|
+
}
|
|
12398
|
+
function handleCmd(rest, ctx, run, x) {
|
|
12399
|
+
const line = cmdSwitchText(rest);
|
|
12400
|
+
if (line !== null) {
|
|
12401
|
+
cmdLine(line, ctx, run);
|
|
12402
|
+
return;
|
|
12403
|
+
}
|
|
12404
|
+
// A switch word that is not a literal might be /c; without /c cmd reads its commands from stdin.
|
|
12405
|
+
if (rest.some((a) => a.text === null)) {
|
|
12406
|
+
worseParse(run, 'dynamic');
|
|
12407
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12408
|
+
return;
|
|
12409
|
+
}
|
|
12410
|
+
stdinExec('sh', [], x, ctx, run, false, true);
|
|
12411
|
+
}
|
|
12412
|
+
// Programs whose argument strings are command lines (wmic process call create "cmd /c …", schtasks /tr "…", forfiles /c "…",
|
|
12413
|
+
// conhost cmd /c …, start /b "" cmd /c …): each argument with blanks is a command line, and so is the rest of the arguments
|
|
12414
|
+
// from any word that names a delete verb, a shell or PowerShell. Brace, `=` and `;` forms (@{CommandLine='…'}) are read too.
|
|
12415
|
+
function execWrapper(rest, ctx, run) {
|
|
12416
|
+
const texts = rest.map(dynText);
|
|
12417
|
+
let trials = 0;
|
|
12418
|
+
for (let k = 0; k < rest.length; k += 1) {
|
|
12419
|
+
if (clock(run))
|
|
12420
|
+
return;
|
|
12421
|
+
const w = texts[k];
|
|
12422
|
+
if (/\s/.test(w) && w.length > 2) {
|
|
12423
|
+
psCommandText(w, ctx, run);
|
|
12424
|
+
const flat = w.replace(/[{}=;@]/g, ' ');
|
|
12425
|
+
if (flat !== w)
|
|
12426
|
+
psCommandText(flat, ctx, run);
|
|
12427
|
+
}
|
|
12428
|
+
const t = rest[k].text;
|
|
12429
|
+
const name = t === null ? '' : (0, tables_1.windowsProgramName)(t);
|
|
12430
|
+
if (tables_1.WIN_DELETE_VERBS.has(name) || tables_1.WIN_SHELL_DELETE_VERBS.has(name) || ['cmd', 'pwsh', 'powershell', 'iex', 'invoke-expression', 'start-process', 'saps', 'robocopy'].includes(name)) {
|
|
12431
|
+
trials += 1;
|
|
12432
|
+
if (trials > tables_1.LIMITS.maxTailTrials) {
|
|
12433
|
+
exhaust(run, 'budget_argv_scan');
|
|
12434
|
+
return;
|
|
12435
|
+
}
|
|
12436
|
+
psCommandText(texts.slice(k).join(' '), ctx, run);
|
|
12437
|
+
}
|
|
12438
|
+
}
|
|
12439
|
+
}
|
|
12440
|
+
const CMD_DYNAMIC_WORD = /%[^%\s]*%|![^!\s]*!|^%{1,2}~?[a-z]$/i;
|
|
12441
|
+
const PS_FILE_SUFFIX = /\.(?:ps1|psm1|bat|cmd)$/i;
|
|
12442
|
+
// pwsh / powershell: -Command (and its prefixes) takes the rest of the line as code, -EncodedCommand a base64 UTF-16LE
|
|
12443
|
+
// text, -File a script, and a first word that is no option is read both as code and as a script path.
|
|
12444
|
+
function handlePowerShell(rest, ctx, run, x) {
|
|
12445
|
+
// With no code, file or encoded text, and with `-` (or `-Command -`), PowerShell reads its commands from stdin.
|
|
12446
|
+
const stdin = () => stdinExec('sh', [], x, ctx, run, false, true);
|
|
12447
|
+
let ran = false;
|
|
12448
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
12449
|
+
const a = rest[i];
|
|
12450
|
+
if (a.text === null) {
|
|
12451
|
+
worseParse(run, 'dynamic');
|
|
12452
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12453
|
+
return;
|
|
12454
|
+
}
|
|
12455
|
+
if (a.text === '-') {
|
|
12456
|
+
stdin();
|
|
12457
|
+
return;
|
|
12458
|
+
}
|
|
12459
|
+
const opt = (0, tables_1.psOption)(a.text);
|
|
12460
|
+
if (opt === null) {
|
|
12461
|
+
psCommandText(rest.slice(i).map(dynText).join(' '), ctx, run);
|
|
12462
|
+
if (!/\s/.test(a.text) && (PS_FILE_SUFFIX.test(a.text) || /[\\/]/.test(a.text)))
|
|
12463
|
+
powershellScript(a, ctx, run);
|
|
12464
|
+
return;
|
|
12465
|
+
}
|
|
12466
|
+
const value = opt.attached !== null ? litArg(opt.attached) : rest[i + 1];
|
|
12467
|
+
const eats = opt.attached === null ? 1 : 0;
|
|
12468
|
+
if (opt.kind === 'command') {
|
|
12469
|
+
const tail = (opt.attached !== null ? [litArg(opt.attached)] : []).concat(rest.slice(i + 1));
|
|
12470
|
+
if (tail.length === 0 || (tail.length === 1 && tail[0].text === '-')) {
|
|
12471
|
+
stdin();
|
|
12472
|
+
return;
|
|
12473
|
+
}
|
|
12474
|
+
psCommandText(tail.map(dynText).join(' '), ctx, run);
|
|
12475
|
+
return;
|
|
12476
|
+
}
|
|
12477
|
+
if (opt.kind === 'encoded' || (opt.name === 'ep' && value !== undefined && (value.text === null || !tables_1.PS_EXECUTION_POLICIES.test(value.text)))) {
|
|
12478
|
+
const decoded = value !== undefined && value.text !== null ? (0, tables_1.decodeEncodedCommand)(value.text) : null;
|
|
12479
|
+
if (decoded === null)
|
|
12480
|
+
hit(run, 'Sx', 'encoded_powershell_unreadable');
|
|
12481
|
+
else
|
|
12482
|
+
psNested(decoded, ctx, run);
|
|
12483
|
+
ran = true;
|
|
12484
|
+
i += eats;
|
|
12485
|
+
continue;
|
|
12486
|
+
}
|
|
12487
|
+
if (opt.kind === 'file') {
|
|
12488
|
+
powershellScript(value, ctx, run);
|
|
12489
|
+
return;
|
|
12490
|
+
}
|
|
12491
|
+
if (opt.kind === 'value' && value !== undefined && !(value.text !== null && (0, tables_1.psOption)(value.text) !== null))
|
|
12492
|
+
i += eats;
|
|
12493
|
+
}
|
|
12494
|
+
if (!ran)
|
|
12495
|
+
stdin();
|
|
12496
|
+
}
|
|
12497
|
+
// A script PowerShell runs (-File, a path as first word). Its text is read as PowerShell text; one that cannot be read
|
|
12498
|
+
// (missing, too big, not a literal path, or a Windows path the mapper cannot open) is a stop.
|
|
12499
|
+
function powershellScript(a, ctx, run) {
|
|
12500
|
+
if (!a || a.text === null || !expandScript(run)) {
|
|
12501
|
+
if (!run.exhausted)
|
|
12502
|
+
hit(run, 'Sx', 'powershell_script_unreadable');
|
|
12503
|
+
return;
|
|
12504
|
+
}
|
|
12505
|
+
const found = (absCandidates(a.text, ctx, run) || []).filter((p) => !p.startsWith('/__unknown_cwd__')).map((p) => readCached(run, p, tables_1.LIMITS.maxScriptBytes));
|
|
12506
|
+
if (found.length === 0 || found.some((c) => c === null)) {
|
|
12507
|
+
hit(run, 'Sx', 'powershell_script_unreadable');
|
|
12508
|
+
return;
|
|
12509
|
+
}
|
|
12510
|
+
for (const c of found)
|
|
12511
|
+
psNested(c, ctx, run);
|
|
12512
|
+
}
|
|
12513
|
+
// iex / Invoke-Expression: a literal argument is PowerShell text; anything else (a variable, a pipe, no argument) is code
|
|
12514
|
+
// nobody can read.
|
|
12515
|
+
function handleInvokeExpression(rest, ctx, run, x) {
|
|
12516
|
+
const args = rest.filter((a) => !(a.text !== null && (0, tables_1.psOption)(a.text)?.kind === 'command'));
|
|
12517
|
+
const code = args[0];
|
|
12518
|
+
if (code === undefined || x.cmd.pipedInput || x.info.upstream.length > 0) {
|
|
12519
|
+
worseParse(run, 'dynamic');
|
|
12520
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12521
|
+
return;
|
|
12522
|
+
}
|
|
12523
|
+
if (code.text !== null) {
|
|
12524
|
+
psNested(code.text, ctx, run);
|
|
12525
|
+
return;
|
|
12526
|
+
}
|
|
12527
|
+
if (code.alts) {
|
|
12528
|
+
for (const v of code.alts)
|
|
12529
|
+
psNested(v, ctx, run);
|
|
12530
|
+
return;
|
|
12531
|
+
}
|
|
12532
|
+
worseParse(run, 'dynamic');
|
|
12533
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12534
|
+
}
|
|
12535
|
+
const START_PROCESS_OPTIONS = ['filepath', 'argumentlist', 'workingdirectory', 'loaduserprofile', 'nonewwindow', 'passthru', 'redirectstandarderror',
|
|
12536
|
+
'redirectstandardinput', 'redirectstandardoutput', 'usenewenvironment', 'verb', 'wait', 'windowstyle', 'credential', 'inputobject'];
|
|
12537
|
+
const START_PROCESS_VALUE_OPTIONS = new Set(['workingdirectory', 'redirectstandarderror', 'redirectstandardinput', 'redirectstandardoutput', 'verb',
|
|
12538
|
+
'windowstyle', 'credential', 'inputobject']);
|
|
12539
|
+
function startOption(word) {
|
|
12540
|
+
const m = /^-([a-z]+)(?::([\s\S]*))?$/i.exec(word);
|
|
12541
|
+
if (!m)
|
|
12542
|
+
return null;
|
|
12543
|
+
const name = m[1].toLowerCase();
|
|
12544
|
+
const full = START_PROCESS_OPTIONS.find((k) => k.startsWith(name));
|
|
12545
|
+
return full ? { name: full, attached: m[2] !== undefined ? m[2] : null } : null;
|
|
12546
|
+
}
|
|
12547
|
+
// Start-Process / saps / start: `<program> <ArgumentList>` is read as one command line. A program that is not a literal
|
|
12548
|
+
// is code nobody can read. Commas of -ArgumentList 'a','b' (which bash joined into one word) read as spaces.
|
|
12549
|
+
function handleStartProcess(rest, ctx, run) {
|
|
12550
|
+
let program;
|
|
12551
|
+
const argv = [];
|
|
12552
|
+
let listed = false;
|
|
12553
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
12554
|
+
const a = rest[i];
|
|
12555
|
+
const opt = a.text !== null ? startOption(a.text) : null;
|
|
12556
|
+
if (opt) {
|
|
12557
|
+
if (opt.name === 'filepath')
|
|
12558
|
+
program = opt.attached !== null ? litArg(opt.attached) : rest[++i];
|
|
12559
|
+
else if (opt.name === 'argumentlist') {
|
|
12560
|
+
listed = true;
|
|
12561
|
+
if (opt.attached !== null)
|
|
12562
|
+
argv.push(litArg(opt.attached));
|
|
12563
|
+
else if (i + 1 < rest.length)
|
|
12564
|
+
argv.push(rest[++i]);
|
|
12565
|
+
while (i + 1 < rest.length && !(rest[i + 1].text !== null && startOption(rest[i + 1].text)))
|
|
12566
|
+
argv.push(rest[++i]);
|
|
12567
|
+
}
|
|
12568
|
+
else if (START_PROCESS_VALUE_OPTIONS.has(opt.name) && opt.attached === null)
|
|
12569
|
+
i += 1;
|
|
12570
|
+
continue;
|
|
12571
|
+
}
|
|
12572
|
+
if (program === undefined)
|
|
12573
|
+
program = a;
|
|
12574
|
+
else if (!listed)
|
|
12575
|
+
argv.push(a);
|
|
12576
|
+
}
|
|
12577
|
+
if (!program)
|
|
12578
|
+
return;
|
|
12579
|
+
if (program.text === null) {
|
|
12580
|
+
worseParse(run, 'dynamic');
|
|
12581
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12582
|
+
return;
|
|
12583
|
+
}
|
|
12584
|
+
const name = (0, tables_1.windowsProgramName)(program.text);
|
|
12585
|
+
if ((name === 'cmd' || name === 'pwsh' || name === 'powershell') && argv.some((a) => a.text === null)) {
|
|
12586
|
+
worseParse(run, 'dynamic');
|
|
12587
|
+
hit(run, 'Sx', 'dynamic_powershell_code');
|
|
12588
|
+
return;
|
|
12589
|
+
}
|
|
12590
|
+
psNested(`${shellQuote(program.text)} ${argv.map(dynText).join(' ').replace(/,/g, ' ')}`.trim(), ctx, run);
|
|
12591
|
+
}
|
|
11755
12592
|
//# sourceMappingURL=mapper.js.map
|