@getmarrow/mcp 3.9.103 → 3.9.106
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 +26 -5
- package/dist/approval-reply.d.ts +2 -0
- package/dist/approval-reply.d.ts.map +1 -0
- package/dist/approval-reply.js +18 -0
- package/dist/approval-reply.js.map +1 -0
- package/dist/cli.js +1677 -1636
- package/dist/cli.js.map +1 -1
- package/dist/codex-plugin-pins.d.ts.map +1 -1
- package/dist/codex-plugin-pins.js +18 -0
- package/dist/codex-plugin-pins.js.map +1 -1
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +83 -4
- package/dist/env.js.map +1 -1
- package/dist/hook-context.d.ts.map +1 -1
- package/dist/hook-context.js +25 -0
- package/dist/hook-context.js.map +1 -1
- package/dist/hook-contract.d.ts +4 -3
- package/dist/hook-contract.d.ts.map +1 -1
- package/dist/hook-contract.js +36 -3
- package/dist/hook-contract.js.map +1 -1
- package/dist/hook-pre-action.d.ts +38 -7
- package/dist/hook-pre-action.d.ts.map +1 -1
- package/dist/hook-pre-action.js +254 -21
- package/dist/hook-pre-action.js.map +1 -1
- package/dist/hook-tool-policy.d.ts +73 -4
- package/dist/hook-tool-policy.d.ts.map +1 -1
- package/dist/hook-tool-policy.js +287 -18
- package/dist/hook-tool-policy.js.map +1 -1
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +5 -3
- package/dist/hook.js.map +1 -1
- package/dist/host-approval-state.d.ts +7 -0
- package/dist/host-approval-state.d.ts.map +1 -1
- package/dist/host-approval-state.js +15 -0
- package/dist/host-approval-state.js.map +1 -1
- package/dist/host-approval.d.ts +22 -1
- package/dist/host-approval.d.ts.map +1 -1
- package/dist/host-approval.js +65 -4
- package/dist/host-approval.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/keyless-server.d.ts +53 -0
- package/dist/keyless-server.d.ts.map +1 -0
- package/dist/keyless-server.js +230 -0
- package/dist/keyless-server.js.map +1 -0
- package/dist/normalized-action.d.ts +2 -0
- package/dist/normalized-action.d.ts.map +1 -1
- package/dist/normalized-action.js +65 -10
- package/dist/normalized-action.js.map +1 -1
- package/dist/own-server-identity.d.ts +22 -3
- package/dist/own-server-identity.d.ts.map +1 -1
- package/dist/own-server-identity.js +98 -27
- package/dist/own-server-identity.js.map +1 -1
- package/dist/own-tool.d.ts +14 -6
- package/dist/own-tool.d.ts.map +1 -1
- package/dist/own-tool.js +18 -7
- package/dist/own-tool.js.map +1 -1
- package/dist/seatbelt/env-dump.d.ts +45 -0
- package/dist/seatbelt/env-dump.d.ts.map +1 -0
- package/dist/seatbelt/env-dump.js +308 -0
- package/dist/seatbelt/env-dump.js.map +1 -0
- package/dist/seatbelt/fallback-copy.d.ts +16 -0
- package/dist/seatbelt/fallback-copy.d.ts.map +1 -0
- package/dist/seatbelt/fallback-copy.js +68 -0
- package/dist/seatbelt/fallback-copy.js.map +1 -0
- package/dist/seatbelt/hook.d.ts +21 -2
- package/dist/seatbelt/hook.d.ts.map +1 -1
- package/dist/seatbelt/hook.js +45 -11
- package/dist/seatbelt/hook.js.map +1 -1
- package/dist/seatbelt/mapper.d.ts.map +1 -1
- package/dist/seatbelt/mapper.js +360 -27
- package/dist/seatbelt/mapper.js.map +1 -1
- package/dist/seatbelt/tables.d.ts +1 -1
- package/dist/seatbelt/tables.d.ts.map +1 -1
- package/dist/seatbelt/tables.js +9 -16
- package/dist/seatbelt/tables.js.map +1 -1
- package/dist/secret-paths.d.ts +17 -0
- package/dist/secret-paths.d.ts.map +1 -0
- package/dist/secret-paths.js +32 -0
- package/dist/secret-paths.js.map +1 -0
- package/dist/session-loop-guard.d.ts.map +1 -1
- package/dist/session-loop-guard.js +15 -5
- package/dist/session-loop-guard.js.map +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/seatbelt/mapper.js
CHANGED
|
@@ -50,6 +50,7 @@ const tables_1 = require("./tables");
|
|
|
50
50
|
const classifier_pin_1 = require("./classifier-pin");
|
|
51
51
|
const longopts_1 = require("./longopts");
|
|
52
52
|
const guards_1 = require("./guards");
|
|
53
|
+
const env_dump_1 = require("./env-dump");
|
|
53
54
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
54
55
|
const shellParse = require('../../vendor/classifier/shell-parse.js');
|
|
55
56
|
const baseline = require('../../vendor/classifier/policy-baseline.js');
|
|
@@ -182,6 +183,11 @@ function evaluateEvent(event, env) {
|
|
|
182
183
|
if (Array.isArray(rawInput))
|
|
183
184
|
rawInput = { commands: rawInput.filter((v) => typeof v === 'string'), items: rawInput.filter((v) => v && typeof v === 'object') };
|
|
184
185
|
const input = (rawInput && typeof rawInput === 'object' ? rawInput : {});
|
|
186
|
+
// A Claude Code built-in that runs a fixed local git command (exact label) is read as that command,
|
|
187
|
+
// as the keyed hook reads it (hook-tool-policy NATIVE_SHELL_EQUIVALENTS).
|
|
188
|
+
const nativeShell = NATIVE_SHELL_EQUIVALENTS.get(tool);
|
|
189
|
+
if (nativeShell !== undefined)
|
|
190
|
+
rawText = rawText === null ? nativeShell : `${rawText}\n${nativeShell}`;
|
|
185
191
|
// One wall-clock ceiling for the whole event, counted from here (audit N6): the input walks below
|
|
186
192
|
// run before any classification and share it.
|
|
187
193
|
const deadline = deadlineFor(env);
|
|
@@ -197,9 +203,14 @@ function evaluateEvent(event, env) {
|
|
|
197
203
|
}
|
|
198
204
|
const evName = mcp ? mcp.name : name;
|
|
199
205
|
const evInput = mcp ? mcp.args : input;
|
|
206
|
+
// Claude Code's Workflow (exact host label) runs its `script` as JavaScript that only starts
|
|
207
|
+
// sub-agents, whose own tools reach the hook: that one field is not shell text (as the keyed hook
|
|
208
|
+
// reads it, hook-tool-policy NON_SHELL_COMMAND_TOOLS). Every other field is read as for any tool.
|
|
209
|
+
const shellInput = !mcp && tool === 'Workflow' && typeof evInput.script === 'string'
|
|
210
|
+
? Object.fromEntries(Object.entries(evInput).filter(([key]) => key !== 'script')) : evInput;
|
|
200
211
|
// A tool that runs a shell command (audit F11): a known shell tool, or any tool whose input names a
|
|
201
212
|
// command, script or argv field, at any depth.
|
|
202
|
-
const invocation = shellInvocations(evName,
|
|
213
|
+
const invocation = shellInvocations(evName, shellInput, deadline, rawText);
|
|
203
214
|
if (invocation) {
|
|
204
215
|
const run = newRun(env, invocation.shell.join('\n'), deadline);
|
|
205
216
|
run.shell = true;
|
|
@@ -377,6 +388,11 @@ function mcpPathChecks(label, input, ctx, run, allStrings = true) {
|
|
|
377
388
|
checkProtectedWrite(litArg(p), ctx, run, 'mcp_write', false);
|
|
378
389
|
}
|
|
379
390
|
}
|
|
391
|
+
// Claude Code built-ins that run a fixed local git command (exact host label).
|
|
392
|
+
const NATIVE_SHELL_EQUIVALENTS = new Map([
|
|
393
|
+
['EnterWorktree', 'git worktree add'],
|
|
394
|
+
['ExitWorktree', 'git worktree remove'],
|
|
395
|
+
]);
|
|
380
396
|
// Tools that never run what their `command`/`script` field says (it names a skill, a slash command,
|
|
381
397
|
// a sub-agent task), plus Marrow's own tools.
|
|
382
398
|
const NON_SHELL_TOOLS = new Set(['slashcommand', 'skill', 'task', 'agent', 'todowrite', 'todoread', 'exitplanmode', 'enterplanmode',
|
|
@@ -978,6 +994,7 @@ function newRun(rawEnv, source, deadline) {
|
|
|
978
994
|
protectedAnchors: anchors.map((a) => (fold ? a.toLowerCase() : a)),
|
|
979
995
|
operandAnchors: operandAnchors.map((a) => (fold ? a.toLowerCase() : a)),
|
|
980
996
|
secretRegexes: tables_1.SECRET_PATTERNS.map((p) => classifierModule.globToRegex(p, home)),
|
|
997
|
+
presentSecretRegexes: tables_1.PRESENT_SECRET_PATTERNS.map((p) => classifierModule.globToRegex(p, home)),
|
|
981
998
|
branchRegexes: baseline.BASELINE_POLICY.protected_branches.map((p) => classifierModule.globToRegex(p, '/nonexistent-home')),
|
|
982
999
|
topSource: source,
|
|
983
1000
|
work: 0,
|
|
@@ -2916,6 +2933,8 @@ function dispatchProgram(prog, args, rest, pathLike, ctx, run, x) {
|
|
|
2916
2933
|
run.headMayChange = true;
|
|
2917
2934
|
// Generic operand checks apply to every program (S4 reads, S0 writes by write-programs).
|
|
2918
2935
|
genericOperandChecks(prog, args, ctx, run, x.info.subs);
|
|
2936
|
+
// Every command that prints the environment (a word-level table shared with the keyed hook).
|
|
2937
|
+
envDumpChecks(prog, rest, ctx, run, x);
|
|
2919
2938
|
// NEW-2: a protected path as an operand of a program that is not a known reader.
|
|
2920
2939
|
protectedOperandCheck(prog, rest, ctx, run);
|
|
2921
2940
|
// Patch round 4: the command zip runs to test its archive (-TT, --unzip-command) is command text.
|
|
@@ -3014,6 +3033,10 @@ function dispatchProgram(prog, args, rest, pathLike, ctx, run, x) {
|
|
|
3014
3033
|
case 'awk':
|
|
3015
3034
|
case 'gawk':
|
|
3016
3035
|
case 'mawk': return awkProgram(rest, ctx, run, x);
|
|
3036
|
+
// nawk was read as an unknown program before: it keeps that argv scan (the break falls through to unknownProgram) besides the awk reading.
|
|
3037
|
+
case 'nawk':
|
|
3038
|
+
awkProgram(rest, ctx, run, x);
|
|
3039
|
+
break;
|
|
3017
3040
|
case 'sed':
|
|
3018
3041
|
case 'gsed': return sedProgram(rest, ctx, run);
|
|
3019
3042
|
case 'ps': return psProgram(rest, run);
|
|
@@ -3292,7 +3315,7 @@ const NESTED_NOT_PROGRAMS = new Set(['set', 'export', 'declare', 'typeset', 'rea
|
|
|
3292
3315
|
// Every command hidden in the argv of a program the mapper has no handler for (`strace -f npm
|
|
3293
3316
|
// publish`, `cross-env NODE_ENV=production vercel --prod`, `tmux new -d 'npm publish'`). Used for
|
|
3294
3317
|
// unknown programs and for known wrappers with open grammars (docker run IMAGE CMD…).
|
|
3295
|
-
function scanArgvForCommands(rest, ctx, run, x, remote = false) {
|
|
3318
|
+
function scanArgvForCommands(rest, ctx, run, x, remote = false, execWrapper = false) {
|
|
3296
3319
|
const fed = withFeed(ctx, x, run).stdinFeed;
|
|
3297
3320
|
const base = remote ? childCtx(ctx, { cwds: [null], remote: true, vars: new Map(), aliases: new Map(), backstop: true, stdinFeed: fed }) : { ...ctx, backstop: true };
|
|
3298
3321
|
const env = new Map(x.env);
|
|
@@ -3323,7 +3346,7 @@ function scanArgvForCommands(rest, ctx, run, x, remote = false) {
|
|
|
3323
3346
|
continue;
|
|
3324
3347
|
const tail = rest.slice(k);
|
|
3325
3348
|
const b = progName(w, run);
|
|
3326
|
-
if ((b === 'env' || b === 'printenv') && tail.length === 1)
|
|
3349
|
+
if ((b === 'env' || b === 'printenv') && tail.length === 1 && !execWrapper)
|
|
3327
3350
|
continue;
|
|
3328
3351
|
trials += 1;
|
|
3329
3352
|
if (trials > tables_1.LIMITS.maxTailTrials) {
|
|
@@ -3545,7 +3568,7 @@ function unknownProgram(prog, args, ctx, run, x) {
|
|
|
3545
3568
|
if (tables_1.INERT_PROGRAMS.has(prog) || ctx.backstop)
|
|
3546
3569
|
return;
|
|
3547
3570
|
const rest = args.slice(1);
|
|
3548
|
-
scanArgvForCommands(rest, ctx, run, x);
|
|
3571
|
+
scanArgvForCommands(rest, ctx, run, x, false, env_dump_1.EXEC_WRAPPERS.has(prog));
|
|
3549
3572
|
// stdin text: `echo 'npm publish' | at now`, `at now <<< 'npm publish'`, `batch <<EOF …`.
|
|
3550
3573
|
const src = stdinSource(x, ctx, run);
|
|
3551
3574
|
// Decoded text piped into a program that runs what it reads (at, batch, crontab, a function) or
|
|
@@ -3930,7 +3953,11 @@ function interpreterEnvDump(code) {
|
|
|
3930
3953
|
if (rest.includes('\u0004SECRETENV\u0004'))
|
|
3931
3954
|
return true;
|
|
3932
3955
|
// What is left: any other mention of the environment object is a dump or a computed lookup.
|
|
3933
|
-
|
|
3956
|
+
if (/\bos\.environb?\b|\bprocess\.env\b|%ENV\b|\bENV\b(?!\s*(?:=|==)\s*['"])|\bENVIRON\b|\benvironb?\b\s*(?:[.)\],;:|]|$)|\$_ENV|\$_SERVER|\bSystem\.getenv\s*\(\s*\)|\bSystem\.get_env\s*\(\s*\)|\bgetenv\s*\(\s*(?:null)?\s*\)|\bphpinfo\s*\(|\benvironment\.get_environment_variables|\bGet-ChildItem\s+env:/i.test(rest))
|
|
3957
|
+
return true;
|
|
3958
|
+
// JavaScript reaches the environment object without spelling it out: `const { env } = process`, `process['en' + 'v']`, `with (process) env`, `Reflect.get(process, 'env')`.
|
|
3959
|
+
return (/\bprocess\b/.test(rest) && (/\benv\b/.test(rest) || /\bprocess\s*(?:\?\.)?\s*\[(?!\s*(['"])[A-Za-z_]\w*\1\s*\])/.test(rest)))
|
|
3960
|
+
|| /\b(?:globalThis|global)\s*(?:\?\.)?\s*\[(?!\s*(['"])[A-Za-z_]\w*\1\s*\])/.test(rest);
|
|
3934
3961
|
}
|
|
3935
3962
|
function scanInterpreterCode(code, run) {
|
|
3936
3963
|
if ((0, tables_1.interpreterTreeDelete)(code))
|
|
@@ -4243,7 +4270,9 @@ function longestRealpath(abs, run) {
|
|
|
4243
4270
|
}
|
|
4244
4271
|
return null;
|
|
4245
4272
|
}
|
|
4246
|
-
|
|
4273
|
+
// `mustExist`: a `<name>.env` path (PRESENT_SECRET_PATTERNS) counts only when it is there to read; samples, git
|
|
4274
|
+
// pathspecs and git objects (history) leave it false.
|
|
4275
|
+
function isSecretPath(abs, run, mustExist = false) {
|
|
4247
4276
|
const p = run.fold ? abs.toLowerCase() : abs;
|
|
4248
4277
|
const base = path.posix.basename(p);
|
|
4249
4278
|
if (/^\.env(?:\.|$)/.test(base) && tables_1.ENV_TEMPLATE_SUFFIX.test(base))
|
|
@@ -4254,8 +4283,11 @@ function isSecretPath(abs, run) {
|
|
|
4254
4283
|
return true;
|
|
4255
4284
|
}
|
|
4256
4285
|
const hits = run.secretRegexes.some((re) => re.test(p) || (run.fold && re.test(abs)));
|
|
4257
|
-
if (!hits)
|
|
4258
|
-
|
|
4286
|
+
if (!hits) {
|
|
4287
|
+
if (!run.presentSecretRegexes.some((re) => re.test(p) || (run.fold && re.test(abs))))
|
|
4288
|
+
return false;
|
|
4289
|
+
return !mustExist || safeRealpath(run.env, abs) !== null;
|
|
4290
|
+
}
|
|
4259
4291
|
if (/\/\.ssh\//.test(p) && tables_1.SSH_PUBLIC.test(base))
|
|
4260
4292
|
return false;
|
|
4261
4293
|
return true;
|
|
@@ -4391,25 +4423,35 @@ function sampleMatch(a, ctx, run, samples) {
|
|
|
4391
4423
|
}
|
|
4392
4424
|
function cwdsList(ctx) { return ctx.cwds.map((c) => c || '/__unknown_cwd__'); }
|
|
4393
4425
|
// S4: a secret path as an operand. Returns true on a stop.
|
|
4394
|
-
|
|
4426
|
+
// `history`: git reads the path from its object store (a pathspec after `--`, a `REV:path` object), so it
|
|
4427
|
+
// need not exist in the work tree.
|
|
4428
|
+
// `pattern`: the pattern of a search (the word in `grep -rn <word> src`), which names no file to read.
|
|
4429
|
+
function checkSecretOperand(a, ctx, run, reason, history = false, pattern = false) {
|
|
4395
4430
|
if (a.text === null && a.alts && a.alts.length) {
|
|
4396
4431
|
for (const v of a.alts)
|
|
4397
|
-
if (checkSecretOperand({ ...litArg(v), glob: a.glob, word: a.word }, ctx, run, reason))
|
|
4432
|
+
if (checkSecretOperand({ ...litArg(v), glob: a.glob, word: a.word }, ctx, run, reason, history, pattern))
|
|
4398
4433
|
return true;
|
|
4399
4434
|
return false;
|
|
4400
4435
|
}
|
|
4401
4436
|
for (const text of operandPaths(a)) {
|
|
4402
4437
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(text))
|
|
4403
4438
|
continue;
|
|
4439
|
+
// A name under an unknown working folder (the event gave no absolute cwd): a relative operand, `..` included,
|
|
4440
|
+
// or one under /proc/<pid>/cwd. It may be there to read, so a `<name>.env` operand counts as present and a
|
|
4441
|
+
// glob that can expand to a `*.env` name stops (fail closed). The pattern of a search names no file.
|
|
4442
|
+
const underUnknown = !pattern && ctx.cwds.some((c) => c === null) && (() => {
|
|
4443
|
+
const t = expandTilde(text, run);
|
|
4444
|
+
return t !== null && (!path.posix.isAbsolute(t) || /^\/proc\/[^/]+\/cwd(?:\/|$)/.test(t));
|
|
4445
|
+
})();
|
|
4404
4446
|
// A glob is also listed (through links and names made in this command) and each match judged.
|
|
4405
4447
|
if (a.text !== null && a.glob && /[*?[]/.test(text) && !a.nonAscii && run.env.fs.readdir) {
|
|
4406
4448
|
const matches = expandGlob(text, ctx, run);
|
|
4407
4449
|
if (matches !== null) {
|
|
4408
4450
|
for (const mm of matches.slice(0, tables_1.LIMITS.maxGlobMatches))
|
|
4409
|
-
if (checkSecretOperand({ ...litArg(mm), glob: false }, ctx, run, reason))
|
|
4451
|
+
if (checkSecretOperand({ ...litArg(mm), glob: false }, ctx, run, reason, history, pattern))
|
|
4410
4452
|
return true;
|
|
4411
4453
|
for (const k of linkNamesMatching(text, ctx, run))
|
|
4412
|
-
if (checkSecretOperand({ ...litArg(k), glob: false }, ctx, run, reason))
|
|
4454
|
+
if (checkSecretOperand({ ...litArg(k), glob: false }, ctx, run, reason, history, pattern))
|
|
4413
4455
|
return true;
|
|
4414
4456
|
}
|
|
4415
4457
|
}
|
|
@@ -4418,6 +4460,8 @@ function checkSecretOperand(a, ctx, run, reason) {
|
|
|
4418
4460
|
if (/[A-Za-z0-9._~/-]/.test(probe.pattern.replace(new RegExp(DYN, 'g'), '')) && sampleMatch(probe, ctx, run, (0, tables_1.secretSamples)(run.home, cwdsList(ctx)))) {
|
|
4419
4461
|
return hit(run, 'S4', `${reason}_secret_pattern`);
|
|
4420
4462
|
}
|
|
4463
|
+
if (underUnknown && a.text !== null && a.glob && /[*?[]/.test(text) && globMayEndWith(text, '.env', run.fold))
|
|
4464
|
+
return hit(run, 'S4', `${reason}_secret_pattern`);
|
|
4421
4465
|
if (a.text === null)
|
|
4422
4466
|
continue;
|
|
4423
4467
|
}
|
|
@@ -4428,13 +4472,135 @@ function checkSecretOperand(a, ctx, run, reason) {
|
|
|
4428
4472
|
// A path through a link whose target the mapper cannot follow (audit N3).
|
|
4429
4473
|
if (run.links.size > 0 && applyLinks(p, run) === '/__unknown_link__')
|
|
4430
4474
|
return hit(run, 'S4', `${reason}_unknown_link`);
|
|
4475
|
+
const mustExist = !history && !(underUnknown || (!pattern && p.startsWith('/__unknown_cwd__/')));
|
|
4431
4476
|
for (const r of realCandidates(p, run, true))
|
|
4432
|
-
if (isSecretPath(r, run))
|
|
4477
|
+
if (isSecretPath(r, run, mustExist))
|
|
4433
4478
|
return hit(run, 'S4', `${reason}_secret_path`);
|
|
4434
4479
|
}
|
|
4435
4480
|
}
|
|
4436
4481
|
return false;
|
|
4437
4482
|
}
|
|
4483
|
+
// Whether some name the glob matches (shell rules: `*` and `?` stay inside one path segment) ends with `suffix`:
|
|
4484
|
+
// a walk over (glob position, how much of the suffix the name ends with so far), trying the suffix characters
|
|
4485
|
+
// and a few others for every wildcard.
|
|
4486
|
+
function globMayEndWith(glob, suffix, ci) {
|
|
4487
|
+
const fold = (c) => (ci ? c.toLowerCase() : c);
|
|
4488
|
+
const toks = [];
|
|
4489
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
4490
|
+
const ch = glob[i];
|
|
4491
|
+
if (ch === '*') {
|
|
4492
|
+
if (toks[toks.length - 1]?.k !== 'star')
|
|
4493
|
+
toks.push({ k: 'star' });
|
|
4494
|
+
continue;
|
|
4495
|
+
}
|
|
4496
|
+
if (ch === '?') {
|
|
4497
|
+
toks.push({ k: 'any1' });
|
|
4498
|
+
continue;
|
|
4499
|
+
}
|
|
4500
|
+
if (ch === '[') {
|
|
4501
|
+
const end = glob.indexOf(']', i + 2);
|
|
4502
|
+
if (end > 0) {
|
|
4503
|
+
let body = glob.slice(i + 1, end);
|
|
4504
|
+
const neg = body.startsWith('!') || body.startsWith('^');
|
|
4505
|
+
if (neg)
|
|
4506
|
+
body = body.slice(1);
|
|
4507
|
+
const tok = { k: 'cls', neg, chars: [], ranges: [] };
|
|
4508
|
+
for (let q = 0; q < body.length; q += 1) {
|
|
4509
|
+
if (body[q + 1] === '-' && q + 2 < body.length) {
|
|
4510
|
+
tok.ranges.push([fold(body[q]), fold(body[q + 2])]);
|
|
4511
|
+
q += 2;
|
|
4512
|
+
}
|
|
4513
|
+
else
|
|
4514
|
+
tok.chars.push(fold(body[q]));
|
|
4515
|
+
}
|
|
4516
|
+
toks.push(tok);
|
|
4517
|
+
i = end;
|
|
4518
|
+
continue;
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
toks.push({ k: 'lit', c: fold(ch) });
|
|
4522
|
+
}
|
|
4523
|
+
const want = fold(suffix);
|
|
4524
|
+
const tries = [...new Set([...want, 'x', 'A', '0', '_', '-', ...toks.flatMap((t) => (t.k === 'cls' ? [...t.chars, ...t.ranges.flat()] : []))])].filter((c) => c !== '/');
|
|
4525
|
+
const step = (k, c) => {
|
|
4526
|
+
const s = want.slice(0, k) + c;
|
|
4527
|
+
for (let n = Math.min(s.length, want.length); n > 0; n -= 1)
|
|
4528
|
+
if (s.endsWith(want.slice(0, n)))
|
|
4529
|
+
return n;
|
|
4530
|
+
return 0;
|
|
4531
|
+
};
|
|
4532
|
+
const accepts = (t, c) => {
|
|
4533
|
+
if (t.k === 'lit')
|
|
4534
|
+
return t.c === c;
|
|
4535
|
+
if (t.k === 'any1' || t.k === 'star')
|
|
4536
|
+
return c !== '/';
|
|
4537
|
+
const inSet = t.chars.includes(c) || t.ranges.some(([lo, hi]) => c >= lo && c <= hi);
|
|
4538
|
+
return t.neg ? !inSet && c !== '/' : inSet;
|
|
4539
|
+
};
|
|
4540
|
+
const seen = new Set();
|
|
4541
|
+
const queue = [[0, 0]];
|
|
4542
|
+
while (queue.length) {
|
|
4543
|
+
const [i, k] = queue.pop();
|
|
4544
|
+
const key = `${i}:${k}`;
|
|
4545
|
+
if (seen.has(key))
|
|
4546
|
+
continue;
|
|
4547
|
+
seen.add(key);
|
|
4548
|
+
if (i === toks.length) {
|
|
4549
|
+
if (k === want.length)
|
|
4550
|
+
return true;
|
|
4551
|
+
continue;
|
|
4552
|
+
}
|
|
4553
|
+
const t = toks[i];
|
|
4554
|
+
const chars = t.k === 'lit' ? [t.c] : tries;
|
|
4555
|
+
if (t.k === 'star')
|
|
4556
|
+
queue.push([i + 1, k]);
|
|
4557
|
+
for (const c of chars)
|
|
4558
|
+
if (accepts(t, c))
|
|
4559
|
+
queue.push([t.k === 'star' ? i : i + 1, step(k, c)]);
|
|
4560
|
+
}
|
|
4561
|
+
return false;
|
|
4562
|
+
}
|
|
4563
|
+
// The words a search reads as its pattern, not as a file: the value of -e/--regexp (also attached), and the
|
|
4564
|
+
// first operand when no -e/-f gave one. A form not listed here is read as a file (the strict reading).
|
|
4565
|
+
function searchPatternArgs(rest, end = rest.length) {
|
|
4566
|
+
const out = new Set();
|
|
4567
|
+
let given = false;
|
|
4568
|
+
let first = null;
|
|
4569
|
+
for (let i = 0; i < end; i += 1) {
|
|
4570
|
+
const t = rest[i].text;
|
|
4571
|
+
if (t === null) {
|
|
4572
|
+
if (first === null)
|
|
4573
|
+
first = rest[i];
|
|
4574
|
+
continue;
|
|
4575
|
+
}
|
|
4576
|
+
if (t === '--')
|
|
4577
|
+
break;
|
|
4578
|
+
if (t === '-e' || t === '--regexp') {
|
|
4579
|
+
given = true;
|
|
4580
|
+
if (rest[i + 1])
|
|
4581
|
+
out.add(rest[i + 1]);
|
|
4582
|
+
i += 1;
|
|
4583
|
+
continue;
|
|
4584
|
+
}
|
|
4585
|
+
if (/^(?:--regexp=|-e.)/.test(t)) {
|
|
4586
|
+
given = true;
|
|
4587
|
+
out.add(rest[i]);
|
|
4588
|
+
continue;
|
|
4589
|
+
}
|
|
4590
|
+
if (t === '-f' || t === '--file' || /^(?:--file=|-f.)/.test(t)) {
|
|
4591
|
+
given = true;
|
|
4592
|
+
if (t === '-f' || t === '--file')
|
|
4593
|
+
i += 1;
|
|
4594
|
+
continue;
|
|
4595
|
+
}
|
|
4596
|
+
if (first === null && !t.startsWith('-'))
|
|
4597
|
+
first = rest[i];
|
|
4598
|
+
}
|
|
4599
|
+
if (!given && first !== null)
|
|
4600
|
+
out.add(first);
|
|
4601
|
+
return out;
|
|
4602
|
+
}
|
|
4603
|
+
const SEARCH_PROGRAMS = new Set(['grep', 'egrep', 'fgrep', 'zgrep', 'rg', 'ag', 'ack']);
|
|
4438
4604
|
// Path-ish parts of an operand: the value after `--opt=`, after `@` (curl -d @file) and after `:`
|
|
4439
4605
|
// for git object paths (HEAD:.env).
|
|
4440
4606
|
function operandPaths(a) {
|
|
@@ -4732,6 +4898,7 @@ function genericOperandChecks(prog, args, ctx, run, subs = []) {
|
|
|
4732
4898
|
recursiveSecretDirRead(prog, rest, ctx, run);
|
|
4733
4899
|
if (!tables_1.S4_EXEMPT_PROGRAMS.has(prog) && prog !== 'git') {
|
|
4734
4900
|
let skipNext = false;
|
|
4901
|
+
const patterns = SEARCH_PROGRAMS.has(prog) ? searchPatternArgs(rest) : null;
|
|
4735
4902
|
for (let i = 0; i < rest.length; i += 1) {
|
|
4736
4903
|
const a = rest[i];
|
|
4737
4904
|
if (skipNext) {
|
|
@@ -4745,7 +4912,7 @@ function genericOperandChecks(prog, args, ctx, run, subs = []) {
|
|
|
4745
4912
|
}
|
|
4746
4913
|
if (clock(run))
|
|
4747
4914
|
return;
|
|
4748
|
-
checkSecretOperand(a, ctx, run, 'operand');
|
|
4915
|
+
checkSecretOperand(a, ctx, run, 'operand', false, patterns !== null && patterns.has(a));
|
|
4749
4916
|
}
|
|
4750
4917
|
}
|
|
4751
4918
|
// Copying or archiving a directory that holds credential files (cp -r ~/.ssh, tar ~) is a read too.
|
|
@@ -6670,6 +6837,7 @@ function xargsWrapper(prog, rest, ctx, run, x) {
|
|
|
6670
6837
|
const sep = inner.findIndex((a) => a.text !== null && /^::::?\+?$/.test(a.text));
|
|
6671
6838
|
const inputs = sep >= 0 ? inner.slice(sep + 1) : [];
|
|
6672
6839
|
inner = sep >= 0 ? inner.slice(0, sep) : inner;
|
|
6840
|
+
xargsRunsBareEnv(inner, run);
|
|
6673
6841
|
if (inner.length === 0) {
|
|
6674
6842
|
// `parallel ::: 'cmd1' 'cmd2'` and `… | parallel`: every input line is a command line.
|
|
6675
6843
|
if (sep >= 0) {
|
|
@@ -6692,6 +6860,7 @@ function xargsWrapper(prog, rest, ctx, run, x) {
|
|
|
6692
6860
|
evalShell(/\{(?:[.\/#%]|\/\.|\d+)?\}/.test(inner.map((a) => a.text || '').join(' ')) ? text : `${text} ${marker}`, childCtx(ctx), run);
|
|
6693
6861
|
return;
|
|
6694
6862
|
}
|
|
6863
|
+
xargsRunsBareEnv(inner, run);
|
|
6695
6864
|
if (inner.length === 0)
|
|
6696
6865
|
inner = [litArg('echo')];
|
|
6697
6866
|
// Input lines become extra operands: unknown targets (spec A6). The -I replacement string stands
|
|
@@ -6894,12 +7063,13 @@ function handleShell(prog, rest, ctx, run, x) {
|
|
|
6894
7063
|
}
|
|
6895
7064
|
function handleInterpreter(prog, rest, ctx, run, x) {
|
|
6896
7065
|
const codeFlags = {
|
|
6897
|
-
node: /^(?:-e|--eval|-p|--print|-pe)$/, nodejs: /^(?:-e|--eval|-p|--print)$/, bun: /^(?:-e|--eval|-p|--print)$/,
|
|
7066
|
+
node: /^(?:-e|--eval|-p|--print|-pe|-ep)$/, 'ts-node': /^(?:-e|--eval|-p|--print)$/, tsx: /^(?:-e|--eval|-p|--print)$/, nodejs: /^(?:-e|--eval|-p|--print)$/, bun: /^(?:-e|--eval|-p|--print)$/,
|
|
6898
7067
|
deno: /^(?:eval)$/, php: /^-r$/, ruby: /^-[a-zA-Z]*e$/, perl: /^-[a-zA-Z]*[eE]$/, lua: /^-e$/, irb: /^-e$/,
|
|
6899
7068
|
pwsh: /^-(?:c|command|e|ec|enc|encodedcommand)$/i, powershell: /^-(?:c|command|e|ec|enc|encodedcommand)$/i,
|
|
6900
7069
|
};
|
|
6901
|
-
const isPython = /^(?:python[23]?(?:\.\d+)?|ipython)$/.test(prog);
|
|
6902
|
-
|
|
7070
|
+
const isPython = /^(?:python[23]?(?:\.\d+)?|ipython|pypy3?(?:\.\d+)?)$/.test(prog);
|
|
7071
|
+
// pypy keeps the `-e` it was read with before it was read as Python (a stop, never a pass), besides Python's `-c`.
|
|
7072
|
+
const codeRe = isPython ? (prog.startsWith('pypy') ? /^(?:-[a-zA-Z]*c|-e)$/ : /^-[a-zA-Z]*c$/) : (codeFlags[prog] || /^-e$/);
|
|
6903
7073
|
for (let i = 0; i < rest.length; i += 1) {
|
|
6904
7074
|
const a = rest[i];
|
|
6905
7075
|
const t = a.text;
|
|
@@ -6914,6 +7084,12 @@ function handleInterpreter(prog, rest, ctx, run, x) {
|
|
|
6914
7084
|
hit(run, 'Sx', 'dynamic_interpreter_code');
|
|
6915
7085
|
return;
|
|
6916
7086
|
}
|
|
7087
|
+
// The code attached to its flag: `node --eval=CODE`, `perl -e'CODE'`, `ruby -e'CODE'`, `php -r'CODE'`, `python3 -c'CODE'`.
|
|
7088
|
+
const attachedCode = /^(?:node|nodejs|bun)$/.test(prog) ? /^--(?:eval|print)=([\s\S]+)$/.exec(t)
|
|
7089
|
+
: isPython ? /^-[a-zA-Z]*c([\s\S]+)$/.exec(t) : prog === 'perl' ? /^-[a-zA-Z]*[eE]([\s\S]+)$/.exec(t) : prog === 'ruby' ? /^-[a-zA-Z]*e([\s\S]+)$/.exec(t)
|
|
7090
|
+
: prog === 'php' ? /^-r([\s\S]+)$/.exec(t) : null;
|
|
7091
|
+
if (attachedCode)
|
|
7092
|
+
scanInterpreterCode(attachedCode[1], run);
|
|
6917
7093
|
if (codeRe.test(t) || (prog === 'deno' && t === 'eval')) {
|
|
6918
7094
|
const code = rest[i + 1];
|
|
6919
7095
|
if (!code)
|
|
@@ -7027,15 +7203,61 @@ function stdinInterpreter(prog, x, ctx, run) {
|
|
|
7027
7203
|
}
|
|
7028
7204
|
// BSD `ps e`, `ps auxe`, `ps eww` print every process's environment.
|
|
7029
7205
|
function psProgram(rest, run) {
|
|
7206
|
+
const words = rest.map((a) => a.text);
|
|
7207
|
+
if ((0, env_dump_1.psPrintsEnvironment)(words))
|
|
7208
|
+
hit(run, 'S4', 'ps_environment');
|
|
7209
|
+
// What 3.9.103 stopped and no later version loosens: the keyed hook leaves this reading out.
|
|
7210
|
+
else if ((0, env_dump_1.psLooselyPrintsEnvironment)(words))
|
|
7211
|
+
hit(run, 'S4', 'ps_environment_loose');
|
|
7212
|
+
}
|
|
7213
|
+
// Commands that print many or all environment variables, found by their words alone (never by file contents):
|
|
7214
|
+
// `compgen -e`, `tmux show-environment`, `systemctl show-environment`, `php -i`, a jq filter that reads `env`,
|
|
7215
|
+
// `npm run env`, and any program given /proc/<pid>/environ.
|
|
7216
|
+
function envDumpChecks(prog, rest, ctx, run, x) {
|
|
7217
|
+
const words = rest.map((a) => a.text);
|
|
7218
|
+
if (prog === 'compgen' && (0, env_dump_1.compgenListsVariables)(words))
|
|
7219
|
+
hit(run, 'S4', 'env_dump_compgen');
|
|
7220
|
+
else if (prog === 'tmux' && ((0, env_dump_1.tmuxShowsEnvironment)(words) || (0, env_dump_1.tmuxRunsEnvironmentPrinter)(words)))
|
|
7221
|
+
hit(run, 'S4', 'env_dump_tmux');
|
|
7222
|
+
else if (prog === 'strace' && (0, env_dump_1.straceShowsEnvironment)(words))
|
|
7223
|
+
hit(run, 'S4', 'env_dump_strace');
|
|
7224
|
+
else if (prog === 'man' && (0, env_dump_1.manPagerPrintsEnvironment)(words))
|
|
7225
|
+
hit(run, 'S4', 'env_dump_env');
|
|
7226
|
+
else if (prog === 'systemctl' && (0, env_dump_1.systemctlShowsEnvironment)(words))
|
|
7227
|
+
hit(run, 'S4', 'env_dump_systemctl');
|
|
7228
|
+
else if (prog === 'php' && words.some((w) => w === '-i' || w === '--info'))
|
|
7229
|
+
hit(run, 'S4', 'env_dump_php_info');
|
|
7230
|
+
else if (prog === 'jq' && ((0, env_dump_1.jqFilters)(words).some(env_dump_1.jqFilterReadsEnvironment) || (x.cmd.heredoc !== null && words.some((w) => w !== null && STDIN_PATHS.has(w)) && (0, env_dump_1.jqFilterReadsEnvironment)(x.cmd.heredoc))))
|
|
7231
|
+
hit(run, 'S4', 'env_dump_jq');
|
|
7232
|
+
else if ((prog === 'npm' || prog === 'pnpm' || prog === 'yarn' || prog === 'bun') && (0, env_dump_1.packageRunsEnvScript)(words))
|
|
7233
|
+
hit(run, 'S4', 'env_dump_npm_run_env');
|
|
7234
|
+
for (const [name, values] of x.env) {
|
|
7235
|
+
if (env_dump_1.HELPER_ENV_VARS.has(name) && values !== null && values.some((v) => env_dump_1.ENV_PRINTER_COMMAND.test(v))) {
|
|
7236
|
+
hit(run, 'S4', 'env_dump_env');
|
|
7237
|
+
break;
|
|
7238
|
+
}
|
|
7239
|
+
}
|
|
7030
7240
|
for (const a of rest) {
|
|
7031
|
-
|
|
7241
|
+
// A dynamic part of the path (`/proc/$(pgrep node)/environ`) stands for any pid.
|
|
7242
|
+
const text = a.text !== null ? a.text : a.pattern.split(DYN).join('x');
|
|
7243
|
+
if (!text.startsWith('/') && !text.includes('proc'))
|
|
7032
7244
|
continue;
|
|
7033
|
-
|
|
7034
|
-
|
|
7245
|
+
const candidates = (a.text !== null ? absCandidates(a.text, ctx, run) || [] : [text]).map((p) => (p.length > 1 ? p.replace(/\/+$/, '') : p));
|
|
7246
|
+
if (candidates.some((p) => env_dump_1.PROC_ENVIRON.test(p) || (0, env_dump_1.procGlobMatchesEnviron)(p) || (env_dump_1.RECURSIVE_READERS.has(prog) && env_dump_1.PROC_PROCESS_DIRECTORY.test(p)))) {
|
|
7247
|
+
hit(run, 'S4', 'proc_environ');
|
|
7035
7248
|
return;
|
|
7036
7249
|
}
|
|
7037
7250
|
}
|
|
7038
7251
|
}
|
|
7252
|
+
// `xargs env`, `parallel env ::: x`: the command is `env` or `printenv` with only options and assignments, so it prints the environment.
|
|
7253
|
+
function xargsRunsBareEnv(words, run) {
|
|
7254
|
+
const b = words[0] && words[0].text !== null ? progName(words[0], run) : null;
|
|
7255
|
+
if (b !== 'env' && b !== 'printenv')
|
|
7256
|
+
return;
|
|
7257
|
+
const bare = words.slice(1).every((a) => a.text !== null && (a.text.startsWith('-') || (b === 'env' && (/^[A-Za-z_]\w*=/.test(a.text) || /^\{[^}]*\}$/.test(a.text)))));
|
|
7258
|
+
if (bare)
|
|
7259
|
+
hit(run, 'S4', b === 'env' ? 'env_dump_env' : 'env_dump_printenv');
|
|
7260
|
+
}
|
|
7039
7261
|
// trap 'cmd' SIGNAL…: the command runs when the signal arrives.
|
|
7040
7262
|
// util-linux `script [-q] [-c COMMAND] [file]`: COMMAND runs in a pty with this command's stdin.
|
|
7041
7263
|
function scriptSession(rest, ctx, run, x) {
|
|
@@ -7069,6 +7291,12 @@ function trapBuiltin(rest, ctx, run, x) {
|
|
|
7069
7291
|
}
|
|
7070
7292
|
// awk programs run commands through system() and pipes.
|
|
7071
7293
|
const AWK_EXEC = /\bsystem\s*\(|\|\s*&?\s*["'$a-zA-Z(]|\bgetline\b|\bENVIRON\b|\bPROCINFO\b/;
|
|
7294
|
+
// A program that can start a command and names its own arguments (`system(ARGV[1])`, `for (i = 1; i < ARGC; i++) s = s ARGV[i]`) may run them: they are command text.
|
|
7295
|
+
const AWK_RUNS_ARGV = /\bsystem\s*\(|\|\s*&?\s*["'$a-zA-Z(]|\bgetline\b/;
|
|
7296
|
+
function awkArgvCommands(text, rest, ctx, run, x) {
|
|
7297
|
+
if (AWK_RUNS_ARGV.test(text) && /\b(?:ARGV|ARGC)\b/.test(text) && !ctx.backstop)
|
|
7298
|
+
scanArgvForCommands(rest, ctx, run, x);
|
|
7299
|
+
}
|
|
7072
7300
|
function awkProgram(rest, ctx, run, x) {
|
|
7073
7301
|
const vf = new Set(['-F', '-v', '-W', '--field-separator', '--assign', '-i', '-l', '--include', '--load']);
|
|
7074
7302
|
let program = null;
|
|
@@ -7080,8 +7308,10 @@ function awkProgram(rest, ctx, run, x) {
|
|
|
7080
7308
|
if (f && f.text !== null && (f.text === '-' || STDIN_PATHS.has(f.text))) {
|
|
7081
7309
|
const src = stdinSource(x, ctx, run);
|
|
7082
7310
|
for (const text of src.texts)
|
|
7083
|
-
if (AWK_EXEC.test(text))
|
|
7311
|
+
if (AWK_EXEC.test(text)) {
|
|
7084
7312
|
scanInterpreterCode(text, run);
|
|
7313
|
+
awkArgvCommands(text, rest, ctx, run, x);
|
|
7314
|
+
}
|
|
7085
7315
|
if (src.unknown || (src.texts.length === 0 && src.files.length === 0)) {
|
|
7086
7316
|
worseParse(run, 'dynamic');
|
|
7087
7317
|
hit(run, 'Sx', 'dynamic_stdin_code');
|
|
@@ -7092,8 +7322,10 @@ function awkProgram(rest, ctx, run, x) {
|
|
|
7092
7322
|
if (f && f.text !== null) {
|
|
7093
7323
|
for (const abs of absCandidates(f.text, ctx, run) || []) {
|
|
7094
7324
|
const content = safeRead(run.env, abs, tables_1.LIMITS.maxScriptBytes);
|
|
7095
|
-
if (content !== null && AWK_EXEC.test(content))
|
|
7325
|
+
if (content !== null && AWK_EXEC.test(content)) {
|
|
7096
7326
|
scanInterpreterCode(content, run);
|
|
7327
|
+
awkArgvCommands(content, rest, ctx, run, x);
|
|
7328
|
+
}
|
|
7097
7329
|
}
|
|
7098
7330
|
}
|
|
7099
7331
|
i += 1;
|
|
@@ -7113,6 +7345,7 @@ function awkProgram(rest, ctx, run, x) {
|
|
|
7113
7345
|
const code = program.text !== null ? program.text : argText(program);
|
|
7114
7346
|
if (AWK_EXEC.test(code)) {
|
|
7115
7347
|
scanInterpreterCode(code, run);
|
|
7348
|
+
awkArgvCommands(code, rest, ctx, run, x);
|
|
7116
7349
|
// `awk '{system($0)}'` runs the lines it reads: the input is a command text (audit N1).
|
|
7117
7350
|
if (/\bsystem\s*\(|\|\s*&?\s*["'$a-zA-Z(]|\bgetline\b/.test(code))
|
|
7118
7351
|
stdinExec('sh', [], x, ctx, run);
|
|
@@ -8290,19 +8523,119 @@ function handleGit(rest, ctx, run, x, cOverrides, aliasDepth) {
|
|
|
8290
8523
|
hit(run, 'S4', 'git_credential_get');
|
|
8291
8524
|
return;
|
|
8292
8525
|
}
|
|
8293
|
-
|
|
8294
|
-
|
|
8526
|
+
// `git log` prints names and messages, but with patch output it prints the content of the files it
|
|
8527
|
+
// names, as `git show` does: then its operands are read like those of `git show`.
|
|
8528
|
+
if (!tables_1.GIT_S4_EXEMPT_SUBS.has(sub) || (sub === 'log' && gitLogPrintsPatch(args))) {
|
|
8529
|
+
// Commit messages are prose, never paths. A pathspec after `--` or a `REV:path` object is read from history.
|
|
8530
|
+
// log, whatchanged and format-patch take no message: their -m and -F are flags, and the next word is read.
|
|
8531
|
+
const prose = !GIT_PATCH_LOG_SUBS.has(sub);
|
|
8532
|
+
const dashDash = args.findIndex((a) => a.text === '--');
|
|
8533
|
+
const patterns = sub === 'grep' ? searchPatternArgs(args, dashDash >= 0 ? dashDash : args.length) : null;
|
|
8295
8534
|
for (let k = 0; k < args.length; k += 1) {
|
|
8296
8535
|
const t = args[k].text;
|
|
8297
|
-
if (t === '-m' || t === '--message' || t === '-F' || t === '--file' || (t !== null && /^-[a-zA-Z]*m$/.test(t))) {
|
|
8536
|
+
if (prose && (t === '-m' || t === '--message' || t === '-F' || t === '--file' || (t !== null && /^-[a-zA-Z]*m$/.test(t)))) {
|
|
8298
8537
|
k += 1;
|
|
8299
8538
|
continue;
|
|
8300
8539
|
}
|
|
8301
|
-
if (t !== null && /^(?:--message=|-m.)/.test(t))
|
|
8540
|
+
if (prose && t !== null && /^(?:--message=|-m.)/.test(t))
|
|
8302
8541
|
continue;
|
|
8303
|
-
|
|
8542
|
+
const history = (dashDash >= 0 && k > dashDash) || t === null || /^[^/:]+:/.test(t);
|
|
8543
|
+
checkSecretOperand(args[k], gctx, run, 'git_operand', history, patterns !== null && patterns.has(args[k]));
|
|
8544
|
+
// `-L<range>:<file>` (or `-L <range>:<file>`) prints the history of <file>: every part after a colon is read.
|
|
8545
|
+
if (!prose && t !== null && /^-L/.test(t) && (dashDash < 0 || k < dashDash)) {
|
|
8546
|
+
const value = t === '-L' ? (args[k + 1] ? args[k + 1].text : null) : t.slice(2);
|
|
8547
|
+
if (value !== null) {
|
|
8548
|
+
for (let c = value.indexOf(':'); c >= 0; c = value.indexOf(':', c + 1)) {
|
|
8549
|
+
if (c + 1 < value.length)
|
|
8550
|
+
checkSecretOperand(litArg(value.slice(c + 1)), gctx, run, 'git_operand', true);
|
|
8551
|
+
}
|
|
8552
|
+
}
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
// Subcommands whose output can be the patches of the files they name; none takes a commit message.
|
|
8558
|
+
const GIT_PATCH_LOG_SUBS = new Set(['log', 'whatchanged', 'format-patch']);
|
|
8559
|
+
// Long options of `git log` that print patches (each implies --patch, or prints changed lines: --check).
|
|
8560
|
+
const GIT_LOG_PATCH_LONG = new Set(['--patch', '--patch-with-stat', '--patch-with-raw', '--unified', '--binary', '--check', '--cc', '--dd',
|
|
8561
|
+
'--remerge-diff', '--combined']);
|
|
8562
|
+
// Words that never take the next word as their value (so a `-s` after one of them is an option).
|
|
8563
|
+
const GIT_LOG_FLAG_WORDS = new Set([...GIT_LOG_PATCH_LONG, '-p', '-u', '-s', '-c', '-m', '--no-patch', '--oneline', '--stat', '--all', '--follow',
|
|
8564
|
+
'--graph', '--reverse', '--first-parent', '--no-merges', '--merges', '--name-only', '--name-status', '--raw', '--numstat', '--shortstat',
|
|
8565
|
+
'--summary', '--decorate', '--full-diff', '--color', '--no-color', '--abbrev-commit', '--no-abbrev-commit', '--topo-order', '--date-order',
|
|
8566
|
+
'--no-renames', '--no-ext-diff', '--no-textconv', '--no-diff-merges']);
|
|
8567
|
+
// Whether `git log <args>` prints patches. On: -p, -u, --patch, -U<n>, --unified, -L, --binary, --check, -c,
|
|
8568
|
+
// --cc, --dd, --remerge-diff, --diff-merges=<format> other than off/none, --patch-with-stat/raw, a short cluster
|
|
8569
|
+
// with p, u, U or L (`-sp`), a word the mapper cannot read, and any long option not on the read list (git takes
|
|
8570
|
+
// no abbreviation here, so it cannot be a known one). Off: -s or --no-patch after the last of them, only where the
|
|
8571
|
+
// word cannot be the value of the word before it (`--author -s`) and only before --end-of-options. A word after
|
|
8572
|
+
// `--` is a pathspec (`git log -- -p`).
|
|
8573
|
+
function gitLogPrintsPatch(args) {
|
|
8574
|
+
let on = false;
|
|
8575
|
+
let offs = true;
|
|
8576
|
+
// A word that is not an option itself or holds its value (`--author=x`, `-n5`, `-Sx`) takes no next word.
|
|
8577
|
+
const mayTakeNext = (k) => {
|
|
8578
|
+
if (k < 0)
|
|
8579
|
+
return false;
|
|
8580
|
+
const p = args[k].text;
|
|
8581
|
+
if (p === null)
|
|
8582
|
+
return true;
|
|
8583
|
+
if (!p.startsWith('-') || p === '-' || p.includes('=') || GIT_LOG_FLAG_WORDS.has(p))
|
|
8584
|
+
return false;
|
|
8585
|
+
if (p.startsWith('--'))
|
|
8586
|
+
return true;
|
|
8587
|
+
const m = p.slice(1).match(/[SGOIlLnU]/);
|
|
8588
|
+
return m !== null && m.index === p.length - 2;
|
|
8589
|
+
};
|
|
8590
|
+
for (let k = 0; k < args.length; k += 1) {
|
|
8591
|
+
const t = args[k].text;
|
|
8592
|
+
if (t === '--')
|
|
8593
|
+
break;
|
|
8594
|
+
if (t === null) {
|
|
8595
|
+
on = true;
|
|
8596
|
+
continue;
|
|
8597
|
+
}
|
|
8598
|
+
if (t === '--end-of-options') {
|
|
8599
|
+
offs = false;
|
|
8600
|
+
continue;
|
|
8601
|
+
}
|
|
8602
|
+
const off = offs && !mayTakeNext(k - 1);
|
|
8603
|
+
if (t === '-c' || t === '--no-patch') {
|
|
8604
|
+
if (t === '-c')
|
|
8605
|
+
on = true;
|
|
8606
|
+
else if (off)
|
|
8607
|
+
on = false;
|
|
8608
|
+
continue;
|
|
8609
|
+
}
|
|
8610
|
+
if (t.startsWith('--')) {
|
|
8611
|
+
const eq = t.indexOf('=');
|
|
8612
|
+
const name = eq === -1 ? t : t.slice(0, eq);
|
|
8613
|
+
if (GIT_LOG_PATCH_LONG.has(name))
|
|
8614
|
+
on = true;
|
|
8615
|
+
else if (name === '--diff-merges') {
|
|
8616
|
+
if (!/^--diff-merges=(?:off|none)$/.test(t))
|
|
8617
|
+
on = true;
|
|
8618
|
+
}
|
|
8619
|
+
else if (t.length > 2 && (0, longopts_1.unknownLong)([args[k]], longopts_1.GIT_READ_LONG, 2, false) !== -1)
|
|
8620
|
+
on = true;
|
|
8621
|
+
continue;
|
|
8622
|
+
}
|
|
8623
|
+
// A short word or cluster, left to right: p, u, U<n> and L<range> print patches, s stops them; a letter
|
|
8624
|
+
// that takes a value ends the word (`-Sfoo`, `-n5`).
|
|
8625
|
+
if (/^-[A-Za-z]/.test(t)) {
|
|
8626
|
+
for (const ch of t.slice(1)) {
|
|
8627
|
+
if (ch === 'p' || ch === 'u' || ch === 'U' || ch === 'L')
|
|
8628
|
+
on = true;
|
|
8629
|
+
else if (ch === 's') {
|
|
8630
|
+
if (off)
|
|
8631
|
+
on = false;
|
|
8632
|
+
}
|
|
8633
|
+
if (/[^A-Za-z]|[SGOIlLnUMCBX]/.test(ch))
|
|
8634
|
+
break;
|
|
8635
|
+
}
|
|
8304
8636
|
}
|
|
8305
8637
|
}
|
|
8638
|
+
return on;
|
|
8306
8639
|
}
|
|
8307
8640
|
// Options that hand git a command to run: rebase --exec, filter-branch filters, bisect run, submodule
|
|
8308
8641
|
// foreach, and the pack programs of fetch and push.
|