@blogic-cz/agent-tools 1.6.0 → 1.6.1
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 +1 -1
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/credential-guard/index.ts +172 -96
package/README.md
CHANGED
|
@@ -650,7 +650,7 @@ The guard permits static `printenv` reads and simple `echo`/`printf` expansions
|
|
|
650
650
|
|
|
651
651
|
Python inline programs and quoted stdin heredocs can also display approved values through `os.getenv("NAME")`, `os.environ.get("NAME", "default")`, or `os.environ["NAME"]`. The complete program must be `import os` followed by `print(...)` statements using these reads and plain string labels or defaults. Dynamic names, aliases, escaped or interpolated strings, additional statements, and whole-environment output remain unsupported. The same redirection and pipeline restrictions apply.
|
|
652
652
|
|
|
653
|
-
Literal assignments such as `F=README.md; cat "$F"` work in supported static commands. Bounded proofs also support a literal `for` loop over a fixed list,
|
|
653
|
+
Literal assignments such as `F=README.md; cat "$F"` work in supported static commands. Bounded proofs also support a literal `for` loop over a fixed list, a single ASCII-letter path binding such as `cd PATH && A=docs/report.md && shasum -a 256 "$A"` or `F=README.md; cat "$F"`, including the existing `git add -- "$T/file"` form, and literal path braces such as `rg -n -i PATTERN src/{a,b} docs/{guide,api}.md`. Path braces work with supported passive text commands and `cat`, and support one unquoted comma list per path, at most 16 members per list and 32 generated paths per command, within 64 KiB. Nested braces, ranges, empty members, mixed quoting, command positions, and compound commands remain unsupported. Path bindings accept literal values and `;`, newline, or `&&` sequencing; a binding conditional on successful `cd` is proved only for uses in that same `&&` list. Their uses must be supported read-only file operands or explicit `git add --` paths. Rebinding, executable operands, startup-variable names, and uncertain scopes cannot establish this proof. Loop binders are a single lowercase letter or `T`, and their uses must occupy supported data operands. Every body command and expanded path is checked; unsupported control flow, mutations such as `printf -v`, and unknown command roles cannot establish the proof. Other variable assignments and brace expansions retain conservative refusal. Fully quoted literal braces remain usable in static searches.
|
|
654
654
|
|
|
655
655
|
Ordinary navigation operands such as `cd "$HOME"` and `git -C "$WORKTREE_ROOT" status --short` do not need display permission. This does not authorize printing those values, using them as code/options, or reading unknown file paths. In `printf`, approved values belong in data arguments after a fixed nonmutating format, for example `printf -- '%s' "$WORKSPACE_LABEL"`.
|
|
656
656
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/credential-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAK5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM/D,8CAA8C;AAC9C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,CAAC;AAQF,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACxE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3E,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,sBAAsB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjF,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACxD,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/credential-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAK5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM/D,8CAA8C;AAC9C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,CAAC;AAQF,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACxE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3E,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,sBAAsB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjF,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACxD,CAAC;AA6qEF,6CAA6C;AAC7C,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAErE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAMD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,eAAe,CA4TrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UAnhFD,SAAS,UAAU,UAAU,KAAK,IAmhFM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YArhFC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAqhFjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aAvhFE,MAAM,KAAK,OAuhFgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aAzhFE,MAAM,KAAK,OAyhFgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YA3hFC,MAAM,KAAK,OA2hF0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YA7hFC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IA6hFf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YA/hFC,MAAM,KAAK,OA+hFsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YAjiFC,MAAM,KAAK,MAAM,GAAG,IAiiFa,CAAC"}
|
package/package.json
CHANGED
|
@@ -190,11 +190,11 @@ function getLeadingLiteralAssignments(
|
|
|
190
190
|
const assignments = new Map<string, { value: string; end: number }>();
|
|
191
191
|
let offset = 0;
|
|
192
192
|
while (offset === 0 || command[offset - 1] === ";" || command[offset - 1] === "\n") {
|
|
193
|
-
while (
|
|
193
|
+
while (/[ \t\n]/.test(command[offset] ?? "")) offset++;
|
|
194
194
|
const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=([A-Za-z0-9_./-]+)/.exec(command.slice(offset));
|
|
195
195
|
if (!assignment) break;
|
|
196
196
|
const end = offset + assignment[0].length;
|
|
197
|
-
const next = command.slice(end).match(
|
|
197
|
+
const next = command.slice(end).match(/^[ \t]*/)?.[0].length ?? 0;
|
|
198
198
|
const separator = command[end + next];
|
|
199
199
|
if (separator !== ";" && separator !== "\n") break;
|
|
200
200
|
assignments.set(assignment[1] ?? "", { value: assignment[2] ?? "", end });
|
|
@@ -239,39 +239,45 @@ function hasSafeLocalAssignmentCommands(
|
|
|
239
239
|
assignments.get(assignment[1] ?? "")?.value === assignment[2]
|
|
240
240
|
);
|
|
241
241
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (name === "find") {
|
|
256
|
-
return !argv.slice(1).some((arg) => /^-(?:exec|ok|delete|fprint|fprintf)/.test(arg));
|
|
257
|
-
}
|
|
258
|
-
if (name === "bun") return argv[1] === "run" && ["db-tool", "gh-tool"].includes(argv[2] ?? "");
|
|
242
|
+
return isSafeLocalAssignmentCommand(words);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isSafeLocalAssignmentCommand(words: string[]): boolean {
|
|
247
|
+
const argv = unwrapStaticCommand(words);
|
|
248
|
+
if (isSafeHerdrTabCreate(argv)) return true;
|
|
249
|
+
const name = argv[0]?.split("/").at(-1) ?? "";
|
|
250
|
+
if (name === "wc" && hasExecutionOption(argv.slice(1), ["files0-from"])) return false;
|
|
251
|
+
if (name === "cd") return isNavigationOperand(argv, "LOCALVALUE");
|
|
252
|
+
if (name === "git") {
|
|
253
|
+
let index = 1;
|
|
254
|
+
while (argv[index] === "-C" && argv[index + 1]) index += 2;
|
|
259
255
|
return (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
(name === "sed" &&
|
|
264
|
-
argv
|
|
265
|
-
.slice(1)
|
|
266
|
-
.every(
|
|
267
|
-
(arg) =>
|
|
268
|
-
/^-[En]+$/.test(arg) ||
|
|
269
|
-
/^s([/|]).*\1[^/|]*\1[gp]*$/.test(arg) ||
|
|
270
|
-
/^[A-Za-z0-9_./-]+$/.test(arg),
|
|
271
|
-
) &&
|
|
272
|
-
!argv.includes("-f"))
|
|
256
|
+
["branch", "diff", "log", "rev-parse", "rev-list", "show", "status"].includes(
|
|
257
|
+
argv[index] ?? "",
|
|
258
|
+
) && !hasExecutionOption(argv.slice(index + 1), ["exec", "ext-diff", "textconv"])
|
|
273
259
|
);
|
|
274
|
-
}
|
|
260
|
+
}
|
|
261
|
+
if (name === "find") {
|
|
262
|
+
return !argv.slice(1).some((arg) => /^-(?:exec|ok|delete|fprint|fprintf)/.test(arg));
|
|
263
|
+
}
|
|
264
|
+
if (name === "bun") return argv[1] === "run" && ["db-tool", "gh-tool"].includes(argv[2] ?? "");
|
|
265
|
+
return (
|
|
266
|
+
isPassiveTextCommand(argv) ||
|
|
267
|
+
["ls", "cat", "tail", "wc", "uniq", "cut", "mv"].includes(name) ||
|
|
268
|
+
(name === "sort" &&
|
|
269
|
+
!hasExecutionOption(argv.slice(1), ["compress-program", "files0-from", "output"], "o")) ||
|
|
270
|
+
(name === "sed" &&
|
|
271
|
+
argv
|
|
272
|
+
.slice(1)
|
|
273
|
+
.every(
|
|
274
|
+
(arg) =>
|
|
275
|
+
/^-[En]+$/.test(arg) ||
|
|
276
|
+
/^s([/|]).*\1[^/|]*\1[gp]*$/.test(arg) ||
|
|
277
|
+
/^[A-Za-z0-9_./-]+$/.test(arg),
|
|
278
|
+
) &&
|
|
279
|
+
!argv.includes("-f"))
|
|
280
|
+
);
|
|
275
281
|
}
|
|
276
282
|
|
|
277
283
|
function hasSensitivePathRedirect(
|
|
@@ -429,6 +435,7 @@ function literalBindingText(
|
|
|
429
435
|
let quote: "'" | '"' | undefined;
|
|
430
436
|
for (let i = 0; i < command.length; i++) {
|
|
431
437
|
const char = command[i] ?? "";
|
|
438
|
+
if (!quote && /[^\S \t\n]/.test(char)) return undefined;
|
|
432
439
|
if (char === "\\" && quote !== "'") {
|
|
433
440
|
const next = command[++i];
|
|
434
441
|
if (next === undefined || /[\r\n]/.test(next)) return undefined;
|
|
@@ -524,7 +531,7 @@ function materializeLiteralShell(command: string): LiteralShellProof | undefined
|
|
|
524
531
|
if (new TextEncoder().encode(command).byteLength > 65_536) return undefined;
|
|
525
532
|
const marker = "GUARDLITERALBINDING";
|
|
526
533
|
if (command.includes(marker)) return undefined;
|
|
527
|
-
let script = command.
|
|
534
|
+
let script = command.replace(/^[ \t\n]+|[ \t\n]+$/g, "");
|
|
528
535
|
const wrapped = parseStaticShellCommands(script);
|
|
529
536
|
if (
|
|
530
537
|
wrapped &&
|
|
@@ -534,7 +541,10 @@ function materializeLiteralShell(command: string): LiteralShellProof | undefined
|
|
|
534
541
|
wrapped.redirects.length === 0
|
|
535
542
|
) {
|
|
536
543
|
const argv = unwrapProofCommand(wrapped.pipelines[0]?.[0] ?? [], marker);
|
|
537
|
-
if (argv?.[0] === "sh" && argv[1] === "-c" && argv.length === 3)
|
|
544
|
+
if (argv?.[0] === "sh" && argv[1] === "-c" && argv.length === 3) {
|
|
545
|
+
if (!literalBindingText(script, "", "")) return undefined;
|
|
546
|
+
script = argv[2] ?? "";
|
|
547
|
+
}
|
|
538
548
|
}
|
|
539
549
|
const loop =
|
|
540
550
|
/^for[ \t]+([A-Za-z_]\w*)[ \t]+in[ \t]+([^;\n]+);[ \t\r\n]*do[ \t\r\n]+([\s\S]*);[ \t\r\n]*done[ \t\r\n]*$/.exec(
|
|
@@ -587,64 +597,135 @@ function materializeLiteralShell(command: string): LiteralShellProof | undefined
|
|
|
587
597
|
paths: [],
|
|
588
598
|
};
|
|
589
599
|
}
|
|
600
|
+
// Single ASCII-letter names avoid shell startup/special variables such as PATH and IFS.
|
|
590
601
|
const assignment =
|
|
591
|
-
/^cd[ \t]+(~?\/?[A-Za-z0-9_./-]+)[ \t]
|
|
592
|
-
|
|
602
|
+
/^(?:cd[ \t]+(~?\/?[A-Za-z0-9_./-]+)[ \t]*(;|\n|&&)[ \t]*)?([A-Za-z])=([A-Za-z0-9_./][A-Za-z0-9_./-]*)[ \t]*(;|\n|&&)[ \t]*/.exec(
|
|
603
|
+
script,
|
|
593
604
|
);
|
|
594
|
-
if (assignment
|
|
605
|
+
if (assignment) {
|
|
595
606
|
const rest = literalBindingText(
|
|
596
|
-
|
|
597
|
-
assignment[
|
|
607
|
+
script.slice(assignment[0].length),
|
|
608
|
+
assignment[3] ?? "",
|
|
598
609
|
marker,
|
|
599
610
|
);
|
|
600
|
-
if (!rest) return undefined;
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
if (
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
parsed.pipelines[0]?.length === 1 &&
|
|
611
|
-
parsed.redirects.length === 0
|
|
612
|
-
? parsed.pipelines[0]?.[0]
|
|
613
|
-
: undefined;
|
|
611
|
+
if (!rest || !rest.text.includes(marker)) return undefined;
|
|
612
|
+
// No branches, background jobs or pipeline-local state. FD duplication stays inert.
|
|
613
|
+
const syntax = rest.syntax.replace(/[<>]&[0-9-]+/g, "");
|
|
614
|
+
if (/\|\||(^|[^&])&(?!&)|[^\S \t\n]/.test(syntax)) return undefined;
|
|
615
|
+
if (assignment[2] === "&&") {
|
|
616
|
+
if (assignment[5] !== "&&") return undefined;
|
|
617
|
+
const boundary = rest.syntax.search(/[;\r\n]/);
|
|
618
|
+
if (boundary >= 0 && rest.text.slice(boundary).includes(marker)) return undefined;
|
|
619
|
+
}
|
|
620
|
+
const parsed = parseStaticShellCommands(rest.text);
|
|
614
621
|
if (
|
|
615
|
-
!
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
622
|
+
!parsed ||
|
|
623
|
+
typeof parsed === "string" ||
|
|
624
|
+
parsed.redirects.some(({ target }) => target.includes(marker)) ||
|
|
625
|
+
!parsed.pipelines.flat().length ||
|
|
626
|
+
parsed.pipelines.flat().length > 32
|
|
619
627
|
)
|
|
620
628
|
return undefined;
|
|
621
|
-
const value = assignment[
|
|
629
|
+
const value = assignment[4] ?? "";
|
|
630
|
+
const paths: string[] = [];
|
|
631
|
+
for (const words of parsed.pipelines.flat()) {
|
|
632
|
+
const marked = unwrapProofCommand(words, marker);
|
|
633
|
+
if (!marked) return undefined;
|
|
634
|
+
const argv = marked.map((word) => word.replaceAll(marker, () => value));
|
|
635
|
+
const name = argv[0]?.split("/").at(-1) ?? "";
|
|
636
|
+
const uses = marked.flatMap((word, index) => (word.includes(marker) ? [index] : []));
|
|
637
|
+
const checksum =
|
|
638
|
+
["shasum", "sha1sum", "sha256sum", "sha512sum", "md5sum", "cksum"].includes(name) &&
|
|
639
|
+
!hasExecutionOption(argv.slice(1), ["check"], "c");
|
|
640
|
+
if (!uses.length) {
|
|
641
|
+
if (
|
|
642
|
+
!isSafeLocalAssignmentCommand(argv) &&
|
|
643
|
+
inlineProgramIndex(argv) === undefined &&
|
|
644
|
+
!(argv[0] === "git" && argv[1] === "commit") &&
|
|
645
|
+
!checksum
|
|
646
|
+
)
|
|
647
|
+
return undefined;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (
|
|
651
|
+
uses.some(
|
|
652
|
+
(index) => index === 0 || !/^[A-Za-z0-9_./][A-Za-z0-9_./-]*$/.test(argv[index] ?? ""),
|
|
653
|
+
)
|
|
654
|
+
)
|
|
655
|
+
return undefined;
|
|
656
|
+
const gitAdd =
|
|
657
|
+
argv.slice(0, 3).join(" ") === "git add --" && uses.every((index) => index >= 3);
|
|
658
|
+
const reader =
|
|
659
|
+
["cat", "ls", "head", "tail", "wc", "uniq", "cut", "sort", "grep", "rg"].includes(name) &&
|
|
660
|
+
isSafeLocalAssignmentCommand(argv);
|
|
661
|
+
if (!gitAdd && !reader && !checksum) return undefined;
|
|
662
|
+
paths.push(...uses.map((index) => argv[index] ?? ""));
|
|
663
|
+
}
|
|
664
|
+
const prefix = assignment[1] ? `cd ${assignment[1]}${assignment[2]} ` : "";
|
|
622
665
|
const size =
|
|
623
|
-
new TextEncoder().encode(rest.text).byteLength +
|
|
624
|
-
(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (size > 65_536) return undefined;
|
|
628
|
-
const paths = argv.slice(3).map((word) => word.replaceAll(marker, () => value));
|
|
629
|
-
return {
|
|
630
|
-
command: `cd ${assignment[1]} && ${use.replaceAll(marker, () => value)}${suffix}`,
|
|
631
|
-
paths,
|
|
632
|
-
};
|
|
666
|
+
new TextEncoder().encode(prefix + rest.text).byteLength +
|
|
667
|
+
(rest.text.split(marker).length - 1) * (value.length - marker.length);
|
|
668
|
+
if (paths.length > 32 || size > 65_536) return undefined;
|
|
669
|
+
return { command: prefix + rest.text.replaceAll(marker, () => value), paths };
|
|
633
670
|
}
|
|
634
|
-
//
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
671
|
+
// Expand only complete, unquoted path words. Keep all other source text intact.
|
|
672
|
+
const paths: string[] = [];
|
|
673
|
+
let expanded = "";
|
|
674
|
+
let marked = "";
|
|
675
|
+
let start = 0;
|
|
676
|
+
let quote: "'" | '"' | undefined;
|
|
677
|
+
let braces = false;
|
|
678
|
+
const finishWord = (end: number): boolean => {
|
|
679
|
+
const word = command.slice(start, end);
|
|
680
|
+
if (!braces) {
|
|
681
|
+
expanded += word;
|
|
682
|
+
marked += word;
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
// ponytail: one comma-list per path, no nesting; extend only with a bounded shell parser.
|
|
686
|
+
const match =
|
|
687
|
+
/^([A-Za-z0-9_./-]*\/[A-Za-z0-9_./-]*)\{([A-Za-z0-9_./-]+(?:,[A-Za-z0-9_./-]+)+)\}([A-Za-z0-9_./-]*)$/.exec(
|
|
688
|
+
word,
|
|
689
|
+
);
|
|
690
|
+
if (!match) return false;
|
|
691
|
+
const members = (match[2] ?? "").split(",");
|
|
692
|
+
if (members.length > 16 || paths.length + members.length > 32) return false;
|
|
693
|
+
const values = members.map((member) => (match[1] ?? "") + member + (match[3] ?? ""));
|
|
694
|
+
const text = values.join(" ");
|
|
695
|
+
if (expanded.length + text.length + command.length - end > 65_536) return false;
|
|
696
|
+
paths.push(...values);
|
|
697
|
+
expanded += text;
|
|
698
|
+
marked += marker;
|
|
699
|
+
return true;
|
|
700
|
+
};
|
|
701
|
+
for (let i = 0; i <= command.length; i++) {
|
|
702
|
+
const char = command[i];
|
|
703
|
+
if (quote === "'") {
|
|
704
|
+
if (char === quote) quote = undefined;
|
|
705
|
+
} else if (char === "\\") {
|
|
706
|
+
if (command[i + 1] === undefined || /[\r\n]/.test(command[i + 1] ?? "")) return undefined;
|
|
707
|
+
i++;
|
|
708
|
+
} else if (quote === '"') {
|
|
709
|
+
if (char === quote) quote = undefined;
|
|
710
|
+
} else if (char === "'" || char === '"') {
|
|
711
|
+
quote = char;
|
|
712
|
+
} else if (char === "{" || char === "}") {
|
|
713
|
+
braces = true;
|
|
714
|
+
} else if (char === undefined || /[ \t\n]/.test(char)) {
|
|
715
|
+
if (!finishWord(i)) return undefined;
|
|
716
|
+
expanded += char ?? "";
|
|
717
|
+
marked += char ?? "";
|
|
718
|
+
start = i + 1;
|
|
719
|
+
braces = false;
|
|
720
|
+
} else if (/[\s;&|<>#]/.test(char)) {
|
|
721
|
+
// Other JS whitespace is a literal shell word character, not a token boundary.
|
|
722
|
+
return undefined;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (quote || !paths.length || new TextEncoder().encode(expanded).byteLength > 65_536)
|
|
726
|
+
return undefined;
|
|
644
727
|
const parsed = parseStaticShellCommands(marked);
|
|
645
728
|
if (
|
|
646
|
-
!lexical ||
|
|
647
|
-
/[;&|<>\r\n]/.test(lexical.syntax) ||
|
|
648
729
|
!parsed ||
|
|
649
730
|
typeof parsed === "string" ||
|
|
650
731
|
parsed.redirects.length ||
|
|
@@ -653,24 +734,19 @@ function materializeLiteralShell(command: string): LiteralShellProof | undefined
|
|
|
653
734
|
)
|
|
654
735
|
return undefined;
|
|
655
736
|
const argv = unwrapProofCommand(parsed.pipelines[0]?.[0] ?? [], marker);
|
|
737
|
+
const expandedCommands = parseStaticShellCommands(expanded);
|
|
738
|
+
const expandedArgv =
|
|
739
|
+
expandedCommands && typeof expandedCommands !== "string"
|
|
740
|
+
? unwrapProofCommand(expandedCommands.pipelines[0]?.[0] ?? [], marker)
|
|
741
|
+
: undefined;
|
|
742
|
+
// Classify actual options too: a marked path can hide an executor such as git grep -O/path.
|
|
656
743
|
if (
|
|
657
744
|
!argv ||
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
argv[1] !== "-n" ||
|
|
661
|
-
argv[2]?.startsWith("-") ||
|
|
662
|
-
argv[3] !== marker
|
|
745
|
+
!expandedArgv ||
|
|
746
|
+
(!isPassiveTextCommand(expandedArgv) && expandedArgv[0]?.split("/").at(-1) !== "cat")
|
|
663
747
|
)
|
|
664
748
|
return undefined;
|
|
665
|
-
|
|
666
|
-
if (!members) return undefined;
|
|
667
|
-
const values = (members[1] ?? "").split(",");
|
|
668
|
-
const size =
|
|
669
|
-
new TextEncoder().encode(command.slice(0, start)).byteLength +
|
|
670
|
-
values.reduce((total, value) => total + path.length - members[0].length + value.length + 1, -1);
|
|
671
|
-
if (size > 65_536) return undefined;
|
|
672
|
-
const paths = values.map((member) => path.replace(members[0], () => member));
|
|
673
|
-
return { command: command.slice(0, start) + paths.join(" "), paths };
|
|
749
|
+
return { command: expanded, paths };
|
|
674
750
|
}
|
|
675
751
|
|
|
676
752
|
/** Complete package metadata loops: no prelude, loader shadowing, mutation or arbitrary suffix. */
|
|
@@ -1710,7 +1786,7 @@ function hasSensitiveFileRead(command: string, isPathBlocked: (path: string) =>
|
|
|
1710
1786
|
if (/\bcat\b[^;&|]*<<-?\s*\S+[\s\S]*\b(?:secret|credential)\b/i.test(command)) return true;
|
|
1711
1787
|
const parsed = parseStaticShellCommands(command);
|
|
1712
1788
|
const readers =
|
|
1713
|
-
/\b(?:cat|cp|grep|rg|sed|awk|jq|head|tail|less|more|source|ls|sort|uniq|cut|wc|mv|find)\b/i;
|
|
1789
|
+
/\b(?:cat|cp|grep|rg|sed|awk|jq|head|tail|less|more|source|ls|sort|uniq|cut|wc|mv|find|shasum|sha1sum|sha256sum|sha512sum|md5sum|cksum)\b/i;
|
|
1714
1790
|
if (parsed && parsed !== "brace-expansion") {
|
|
1715
1791
|
return parsed.pipelines.flat().some((words) => {
|
|
1716
1792
|
const argv = unwrapStaticCommand(words);
|