@blogic-cz/agent-tools 1.6.3 → 1.6.4
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 +4 -0
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/credential-guard/index.ts +264 -32
package/README.md
CHANGED
|
@@ -668,6 +668,10 @@ Single literal `echo`/`printf` writes with output redirection and single `cat`/`
|
|
|
668
668
|
|
|
669
669
|
Python JSON parsing, dictionary iteration, and printing are not themselves evidence of credential access. They follow the same access and execution rules as other supported inline programs. This intentionally permits ordinary JSON output that older versions refused. Unknown secrets in ordinary files can therefore reach arbitrary shell output; wrapper content checks do not protect that output. A refusal caused by unsupported syntax means the guard could not establish its policy conditions, not that it observed a secret being read.
|
|
670
670
|
|
|
671
|
+
The bounded Python subprocess proof supports one immutable literal argv binding followed by one `subprocess.run` call. A literal `cwd` is replayed as a quoted, success-gated directory change before the nested argv, so relative reader operands and named directories receive the ordinary path policies. Supported directories are literal relative or absolute paths, including quoted spaces. Empty, dynamic, Unicode, escaped, tilde and other unsupported directory forms refuse rather than dropping the context. Cwd proof recursively unwraps only supported command, RTK proxy and environment wrappers. Environment directory/split-string options and unproved wrapper forms refuse. Only supported passive text, reader, metadata, literal Bun script and blocked CLI roles receive nested replay; shell executors, inline interpreter programs, unproved sed programs, find execution actions and unknown command roles refuse. Nested blocked CLI, environment, sensitive-path and custom-command policies remain active. Outer shell redirects retain their original shell directory. Arbitrary multi-call Python and `check_output` programs remain unsupported; direct Git metadata commands provide index/tree inspection.
|
|
672
|
+
|
|
673
|
+
Supported runtime script/tool operands form an argument boundary: a later tool `-c` configuration flag is data. Genuine eval and loader flags, Bun exec/repl and unproved startup-option arity still refuse. Literal Herdr prompts can compose with the existing literal agent-get and pane-read forms and passive consumers. One literal `--sql` operand on the supported `bun run db-tool sql/query` shape can contain SQL braces; substitutions, duplicate SQL operands and unknown wrapper options receive no exception. SQL output pipelines require closed passive consumers, so shell, xargs and unknown executors refuse. Numeric `$?` can appear only in passive echo/printf data operands, with passive pipeline consumers. Explicitly approved environment names can appear in quoted equality or blankness tests, including the quoted empty default `${NAME:-}`. This does not add default-approved names or permit shell evaluators. One sed print program with numeric or escaped-regex addresses/ranges and known `-n`/`-E` flags is passive; execution/read/write callbacks, extra programs and script files receive no print proof. Local file bindings use that same closed print proof and never a generic sed-word fallback. Ordinary path and configured command policies still apply to all these forms.
|
|
674
|
+
|
|
671
675
|
A separate bounded Node proof supports literal `package.json` inventory loops that print `bin` metadata or use the supported scripts filter. The complete invocation must preserve loader identity through known passive commands, closed literal writers, or proved inventories. Startup/environment mutations, unknown commands, loader changes, and other dynamic loading remain unproved and are refused.
|
|
672
676
|
|
|
673
677
|
### Setup for OpenCode
|
|
@@ -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;AAwhFF,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,UA93FD,SAAS,UAAU,UAAU,KAAK,IA83FM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAh4FC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAg4FjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aAl4FE,MAAM,KAAK,OAk4FgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aAp4FE,MAAM,KAAK,OAo4FgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YAt4FC,MAAM,KAAK,OAs4F0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YAx4FC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAw4Ff,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YA14FC,MAAM,KAAK,OA04FsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YA54FC,MAAM,KAAK,MAAM,GAAG,IA44Fa,CAAC"}
|
package/package.json
CHANGED
|
@@ -262,21 +262,12 @@ function isSafeLocalAssignmentCommand(words: string[]): boolean {
|
|
|
262
262
|
return !argv.slice(1).some((arg) => /^-(?:exec|ok|delete|fprint|fprintf)/.test(arg));
|
|
263
263
|
}
|
|
264
264
|
if (name === "bun") return argv[1] === "run" && ["db-tool", "gh-tool"].includes(argv[2] ?? "");
|
|
265
|
+
if (name === "sed") return isReadonlySedPrint(argv);
|
|
265
266
|
return (
|
|
266
267
|
isPassiveTextCommand(argv) ||
|
|
267
268
|
["ls", "cat", "tail", "wc", "uniq", "cut", "mv"].includes(name) ||
|
|
268
269
|
(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"))
|
|
270
|
+
!hasExecutionOption(argv.slice(1), ["compress-program", "files0-from", "output"], "o"))
|
|
280
271
|
);
|
|
281
272
|
}
|
|
282
273
|
|
|
@@ -502,13 +493,7 @@ function isLiteralLoopCommand(words: string[], marker: string, value: string): b
|
|
|
502
493
|
const format = passivePrintfFormatIndex(argv);
|
|
503
494
|
return format !== undefined && uses.every((index) => index > format);
|
|
504
495
|
}
|
|
505
|
-
if (name === "herdr")
|
|
506
|
-
return (
|
|
507
|
-
((argv.length === 4 && argv[1] === "agent" && argv[2] === "get") ||
|
|
508
|
-
isLiteralHerdrPaneRead(argv)) &&
|
|
509
|
-
/^[A-Za-z0-9_.:-]+$/.test(argv[3] ?? "") &&
|
|
510
|
-
uses.every((index) => index === 3)
|
|
511
|
-
);
|
|
496
|
+
if (name === "herdr") return isLiteralHerdrRead(argv) && uses.every((index) => index === 3);
|
|
512
497
|
if (name === "bun")
|
|
513
498
|
return (
|
|
514
499
|
argv.length === 9 &&
|
|
@@ -537,6 +522,14 @@ function isLiteralHerdrPaneRead(argv: string[]): boolean {
|
|
|
537
522
|
);
|
|
538
523
|
}
|
|
539
524
|
|
|
525
|
+
function isLiteralHerdrRead(argv: string[]): boolean {
|
|
526
|
+
return (
|
|
527
|
+
((argv.length === 4 && argv.slice(0, 3).join(" ") === "herdr agent get") ||
|
|
528
|
+
isLiteralHerdrPaneRead(argv)) &&
|
|
529
|
+
/^[A-Za-z0-9_.:-]+$/.test(argv[3] ?? "")
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
540
533
|
/** A complete stdin-only JSON projection, with no suffix, imports or executable input. */
|
|
541
534
|
function isLiteralStdinJsonDisplay(argv: string[]): boolean {
|
|
542
535
|
if (argv.length !== 3 || !["python", "python3"].includes(argv[0] ?? "") || argv[1] !== "-c")
|
|
@@ -852,15 +845,54 @@ function inlineProgramIndex(argv: string[]): number | undefined {
|
|
|
852
845
|
}
|
|
853
846
|
|
|
854
847
|
function isUnsupportedInlineRuntime(argv: string[]): boolean {
|
|
855
|
-
|
|
848
|
+
const runtime = inlineRuntimeKind(argv[0]);
|
|
849
|
+
if (!runtime) return false;
|
|
856
850
|
if (inlineProgramIndex(argv) !== undefined) return false;
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
851
|
+
let index = 1;
|
|
852
|
+
for (; index < argv.length; index++) {
|
|
853
|
+
const arg = argv[index] ?? "";
|
|
854
|
+
if (arg === "--" || !arg.startsWith("-")) break;
|
|
855
|
+
if (
|
|
860
856
|
/^(?:-[A-Za-z]*[cep]|-[rmp]|--(?:eval|print|require|import|loader|preload|experimental-loader))(?:$|=|[^-])/.test(
|
|
861
857
|
arg,
|
|
862
|
-
)
|
|
863
|
-
)
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
return true;
|
|
861
|
+
if (
|
|
862
|
+
(runtime === "python" && arg === "--check-hash-based-pycs") ||
|
|
863
|
+
(runtime === "node" &&
|
|
864
|
+
["--conditions", "-C", "--icu-data-dir", "--dns-result-order"].includes(arg))
|
|
865
|
+
) {
|
|
866
|
+
if (argv[index + 1] === undefined) return true;
|
|
867
|
+
index++;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
// Unknown startup-option arity cannot establish where the script/tool begins.
|
|
871
|
+
if (
|
|
872
|
+
!(
|
|
873
|
+
(runtime === "python" && /^-[uBISsEOqvV]+$/.test(arg)) ||
|
|
874
|
+
(runtime === "node" &&
|
|
875
|
+
/^(?:--(?:trace-warnings|no-warnings|version|help)|--(?:conditions|icu-data-dir|dns-result-order)=.+)$/.test(
|
|
876
|
+
arg,
|
|
877
|
+
)) ||
|
|
878
|
+
(runtime === "bun" && ["--no-env-file", "--version", "--help"].includes(arg)) ||
|
|
879
|
+
["--version", "--help", "-h"].includes(arg)
|
|
880
|
+
)
|
|
881
|
+
)
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
if (runtime === "bun" && ["exec", "repl"].includes(argv[index] ?? "")) return true;
|
|
885
|
+
// Bun retains eval/loader controls around its tool operand; only a later -c is tool data.
|
|
886
|
+
return (
|
|
887
|
+
runtime === "bun" &&
|
|
888
|
+
argv
|
|
889
|
+
.slice(index)
|
|
890
|
+
.some((arg) =>
|
|
891
|
+
/^(?:-[A-Za-z]*[ep]|-[rmp]|--(?:eval|print|require|import|loader|preload|experimental-loader))(?:$|=|[^-])/.test(
|
|
892
|
+
arg,
|
|
893
|
+
),
|
|
894
|
+
)
|
|
895
|
+
);
|
|
864
896
|
}
|
|
865
897
|
|
|
866
898
|
/** Move a complete, literal Python stdin program into the existing inline argv boundary. */
|
|
@@ -1045,7 +1077,64 @@ function staticSubprocessCommand(program: string): string | undefined {
|
|
|
1045
1077
|
(match) => match[1] ?? match[2] ?? "",
|
|
1046
1078
|
);
|
|
1047
1079
|
if (!values.length) return undefined;
|
|
1048
|
-
|
|
1080
|
+
const quote = (value: string) => "'" + value.replaceAll("'", "'\"'\"'") + "'";
|
|
1081
|
+
const command = values.map(quote).join(" ");
|
|
1082
|
+
const directories = [...call[0].matchAll(/\bcwd\s*=\s*(?:"([^"\\]*)"|'([^'\\]*)')/g)];
|
|
1083
|
+
if (!directories.length) return command;
|
|
1084
|
+
const cwd = directories[0]?.[1] ?? directories[0]?.[2] ?? "";
|
|
1085
|
+
// Never replay a cwd-bearing call as if it ran in the original shell directory.
|
|
1086
|
+
if (directories.length !== 1 || !isStaticNavigationCommand(["cd", "--", cwd])) return undefined;
|
|
1087
|
+
const argv = unwrapCwdSubprocessCommand(values);
|
|
1088
|
+
if (!argv || !isProvedCwdSubprocessRole(argv)) return undefined;
|
|
1089
|
+
// Keep wrapper assignments/options in policy replay after proving the nested command role.
|
|
1090
|
+
const replay = `cd -- ${quote(cwd)} && ${command}`;
|
|
1091
|
+
return hasProvedNavigationSequence(replay) ? replay : undefined;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function unwrapCwdSubprocessCommand(words: string[]): string[] | undefined {
|
|
1095
|
+
let argv = words;
|
|
1096
|
+
// Each successful wrapper removes words; the original word count bounds recursion.
|
|
1097
|
+
for (let depth = 0; depth < words.length; depth++) {
|
|
1098
|
+
const unwrapped = unwrapProofCommand(argv, "\0");
|
|
1099
|
+
if (!unwrapped) return undefined;
|
|
1100
|
+
argv = unwrapped;
|
|
1101
|
+
if (argv[0]?.split("/").at(-1) !== "env") return argv;
|
|
1102
|
+
const nested = unwrapEnvironmentCommand(argv);
|
|
1103
|
+
if (!nested) return undefined;
|
|
1104
|
+
// Inspect only env's prefix, not options belonging to its child command.
|
|
1105
|
+
// env directory, split-string and unknown options do not preserve the proved context.
|
|
1106
|
+
if (
|
|
1107
|
+
argv
|
|
1108
|
+
.slice(1, argv.length - nested.length)
|
|
1109
|
+
.some(
|
|
1110
|
+
(arg) =>
|
|
1111
|
+
arg.startsWith("-") && arg !== "--" && arg !== "-i" && arg !== "--ignore-environment",
|
|
1112
|
+
)
|
|
1113
|
+
)
|
|
1114
|
+
return undefined;
|
|
1115
|
+
argv = nested;
|
|
1116
|
+
}
|
|
1117
|
+
return undefined;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function isProvedCwdSubprocessRole(argv: string[]): boolean {
|
|
1121
|
+
const name = argv[0]?.split("/").at(-1) ?? "";
|
|
1122
|
+
if (inlineProgramIndex(argv) !== undefined || isUnsupportedInlineRuntime(argv)) return false;
|
|
1123
|
+
if (isPassiveTextCommand(argv)) return true;
|
|
1124
|
+
if (name === "find") return isSafeLocalAssignmentCommand(argv);
|
|
1125
|
+
if (["ls", "cat", "uniq", "cut"].includes(name)) return true;
|
|
1126
|
+
if (name === "wc") return !hasExecutionOption(argv.slice(1), ["files0-from"]);
|
|
1127
|
+
if (name === "sort")
|
|
1128
|
+
return !hasExecutionOption(argv.slice(1), ["compress-program", "files0-from"]);
|
|
1129
|
+
if (name === "git") {
|
|
1130
|
+
let index = 1;
|
|
1131
|
+
while (argv[index] === "-C" && argv[index + 1]) index += 2;
|
|
1132
|
+
return ["ls-files", "ls-tree", "status", "rev-parse", "rev-list"].includes(argv[index] ?? "");
|
|
1133
|
+
}
|
|
1134
|
+
// A literal Bun script keeps the existing supported test-runner route.
|
|
1135
|
+
if (name === "bun") return /^[A-Za-z0-9_./-]+\.(?:[cm]?[jt]s)$/.test(argv[1] ?? "");
|
|
1136
|
+
// Retain extraction so the nested CLI policy returns its ordinary descriptor.
|
|
1137
|
+
return DEFAULT_BLOCKED_CLI_TOOLS.some((tool) => tool.name === name);
|
|
1049
1138
|
}
|
|
1050
1139
|
|
|
1051
1140
|
function inlineSubprocessCommands(command: string): string[] {
|
|
@@ -1207,6 +1296,9 @@ function hasCommandArgumentBraceExpansion(argv: string[]): boolean {
|
|
|
1207
1296
|
argv[4] === "query"
|
|
1208
1297
|
)
|
|
1209
1298
|
return false;
|
|
1299
|
+
const sql = literalSqlArgumentIndex(argv);
|
|
1300
|
+
if (sql !== undefined)
|
|
1301
|
+
return argv.some((arg, index) => index !== sql && hasArgumentBraceExpansion(arg));
|
|
1210
1302
|
return (argv[0]?.split("/").at(-1) === "awk" && !isAwkExecution(argv)) ||
|
|
1211
1303
|
inlineProgramIndex(argv) !== undefined ||
|
|
1212
1304
|
isJqObjectConstruction(argv)
|
|
@@ -1214,6 +1306,32 @@ function hasCommandArgumentBraceExpansion(argv: string[]): boolean {
|
|
|
1214
1306
|
: hasArgumentBraceExpansion(argv.join(" "));
|
|
1215
1307
|
}
|
|
1216
1308
|
|
|
1309
|
+
function literalSqlArgumentIndex(argv: string[]): number | undefined {
|
|
1310
|
+
if (
|
|
1311
|
+
argv[0]?.split("/").at(-1) !== "bun" ||
|
|
1312
|
+
argv[1] !== "run" ||
|
|
1313
|
+
argv[2] !== "db-tool" ||
|
|
1314
|
+
!["sql", "query"].includes(argv[3] ?? "")
|
|
1315
|
+
)
|
|
1316
|
+
return undefined;
|
|
1317
|
+
let sql: number | undefined;
|
|
1318
|
+
const seen = new Set<string>();
|
|
1319
|
+
for (let index = 4; index < argv.length; index++) {
|
|
1320
|
+
const [option = "", ...attached] = (argv[index] ?? "").split("=");
|
|
1321
|
+
if (
|
|
1322
|
+
!["--sql", "--env", "--profile", "--limit", "--format"].includes(option) ||
|
|
1323
|
+
seen.has(option)
|
|
1324
|
+
)
|
|
1325
|
+
return undefined;
|
|
1326
|
+
seen.add(option);
|
|
1327
|
+
const value = attached.length ? attached.join("=") : argv[++index];
|
|
1328
|
+
if (!value || value.startsWith("--")) return undefined;
|
|
1329
|
+
if (option === "--sql") sql = index;
|
|
1330
|
+
else if (!/^[A-Za-z0-9_./-]+$/.test(value)) return undefined;
|
|
1331
|
+
}
|
|
1332
|
+
return sql;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1217
1335
|
/** echo and printf print their arguments; grep -o, rg and git grep print matched pattern text. */
|
|
1218
1336
|
function isLiteralTextProducer(argv: string[]): boolean {
|
|
1219
1337
|
return isPassiveTextCommand(argv) && argv[0]?.split("/").at(-1) !== "head";
|
|
@@ -1584,7 +1702,8 @@ function hasProvedNavigationSequence(command: string): boolean {
|
|
|
1584
1702
|
return false;
|
|
1585
1703
|
if (!changesDirectory.length) return true;
|
|
1586
1704
|
// Only the final AND list can use this directory. A failing cd never reaches its readers.
|
|
1587
|
-
|
|
1705
|
+
// Trim real trailing whitespace, not the spaces masking final quoted argv words.
|
|
1706
|
+
const syntax = literal.syntax.slice(0, command.replace(/[ \t\n]+$/g, "").length);
|
|
1588
1707
|
const boundary = Math.max(syntax.lastIndexOf(";"), syntax.lastIndexOf("\n"));
|
|
1589
1708
|
const tail = parseStaticShellCommands(command.slice(boundary + 1, syntax.length));
|
|
1590
1709
|
if (!tail || typeof tail === "string" || tail.pipelines.length < 2) return false;
|
|
@@ -1608,6 +1727,7 @@ function isPassiveTextCommand(argv: string[]): boolean {
|
|
|
1608
1727
|
}
|
|
1609
1728
|
if (name === "rg") return !hasExecutionOption(args, ["pre", "hostname-bin"]);
|
|
1610
1729
|
if (name === "awk") return !isAwkExecution(argv);
|
|
1730
|
+
if (name === "sed") return isReadonlySedPrint(argv);
|
|
1611
1731
|
if (name === "jq") return isJqObjectConstruction(argv);
|
|
1612
1732
|
return (
|
|
1613
1733
|
(name === "git" && args[0] === "status") ||
|
|
@@ -1617,6 +1737,19 @@ function isPassiveTextCommand(argv: string[]): boolean {
|
|
|
1617
1737
|
);
|
|
1618
1738
|
}
|
|
1619
1739
|
|
|
1740
|
+
/** One print program with optional numeric or escaped-regex addresses; no callbacks. */
|
|
1741
|
+
function isReadonlySedPrint(argv: string[]): boolean {
|
|
1742
|
+
if (argv[0]?.split("/").at(-1) !== "sed") return false;
|
|
1743
|
+
let index = 1;
|
|
1744
|
+
while (/^-[nE]+$/.test(argv[index] ?? "")) index++;
|
|
1745
|
+
if (argv[index] === "--") index++;
|
|
1746
|
+
const address = String.raw`(?:[0-9]+|\$|/(?:\\[^\r\n]|[^/\\\r\n])*/)`;
|
|
1747
|
+
return (
|
|
1748
|
+
new RegExp(`^(?:${address}(?:,${address})?)?p$`).test(argv[index] ?? "") &&
|
|
1749
|
+
argv.slice(index + 1).every((arg) => !arg.startsWith("-"))
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1620
1753
|
function hasStaticEnvironmentRead(
|
|
1621
1754
|
argv: string[],
|
|
1622
1755
|
allowedNames: Set<string>,
|
|
@@ -1702,6 +1835,7 @@ function isStaticHerdrPrompt(command: string): boolean {
|
|
|
1702
1835
|
argv.slice(1).every((arg) => arg === "--" || !arg.startsWith("-"))
|
|
1703
1836
|
);
|
|
1704
1837
|
if (isPassiveTextCommand(argv)) return true;
|
|
1838
|
+
if (isLiteralHerdrRead(argv)) return index === 0;
|
|
1705
1839
|
if (isStaticNavigationCommand(argv))
|
|
1706
1840
|
return index === 0 && (argv[0] !== "cd" || pipeline.length === 1);
|
|
1707
1841
|
return false;
|
|
@@ -1732,6 +1866,26 @@ function hasEnvironmentRead(
|
|
|
1732
1866
|
const parsed = parseStaticShellCommands(command);
|
|
1733
1867
|
if (parsed === "brace-expansion") return true;
|
|
1734
1868
|
if (!parsed) {
|
|
1869
|
+
const operands = parseStaticShellCommands(command, true);
|
|
1870
|
+
if (
|
|
1871
|
+
operands &&
|
|
1872
|
+
typeof operands !== "string" &&
|
|
1873
|
+
operands.pipelines.flat().some((words) => {
|
|
1874
|
+
const argv = unwrapStaticCommand(words);
|
|
1875
|
+
return (
|
|
1876
|
+
argv[0]?.split("/").at(-1) === "bun" &&
|
|
1877
|
+
argv[1] === "run" &&
|
|
1878
|
+
argv[2] === "db-tool" &&
|
|
1879
|
+
["sql", "query"].includes(argv[3] ?? "") &&
|
|
1880
|
+
argv.some(
|
|
1881
|
+
(arg, index) =>
|
|
1882
|
+
(arg === "--sql" && /\$\(|`/.test(argv[index + 1] ?? "")) ||
|
|
1883
|
+
(arg.startsWith("--sql=") && /\$\(|`/.test(arg)),
|
|
1884
|
+
)
|
|
1885
|
+
);
|
|
1886
|
+
})
|
|
1887
|
+
)
|
|
1888
|
+
return true;
|
|
1735
1889
|
const unquoted = command.replace(/'(?:\\.|[^'])*'/g, " ").replace(/"(?:\\.|[^"$`])*"/g, " ");
|
|
1736
1890
|
const hasExecutor =
|
|
1737
1891
|
/\|\s*(?:sh|bash|zsh|xargs)|\b(?:sh|bash|zsh)\b|\$\(|`|\b(?:eval|source)\b|\bfind\b.*\b-exec\b/i.test(
|
|
@@ -1745,6 +1899,16 @@ function hasEnvironmentRead(
|
|
|
1745
1899
|
}
|
|
1746
1900
|
const pipelines = parsed.pipelines.map((commands) => commands.map(unwrapStaticCommand));
|
|
1747
1901
|
const unwrapped = pipelines.flat();
|
|
1902
|
+
if (
|
|
1903
|
+
pipelines.some((pipeline) =>
|
|
1904
|
+
pipeline.some(
|
|
1905
|
+
(argv, index) =>
|
|
1906
|
+
literalSqlArgumentIndex(argv) !== undefined &&
|
|
1907
|
+
pipeline.slice(index + 1).some((consumer) => !isPassiveTextCommand(consumer)),
|
|
1908
|
+
),
|
|
1909
|
+
)
|
|
1910
|
+
)
|
|
1911
|
+
return true;
|
|
1748
1912
|
if (
|
|
1749
1913
|
parsed.redirects.length > 0 &&
|
|
1750
1914
|
unwrapped.some((argv) => isAllowedEnvironmentRead(argv, allowedNames))
|
|
@@ -2146,7 +2310,13 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2146
2310
|
normalized += char;
|
|
2147
2311
|
continue;
|
|
2148
2312
|
}
|
|
2149
|
-
|
|
2313
|
+
if (command[i + 1] === "?") {
|
|
2314
|
+
expansions++;
|
|
2315
|
+
normalized += "NUMERICSTATUSVALUE";
|
|
2316
|
+
i++;
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
const variable = /^\$\{([A-Za-z_][A-Za-z0-9_]*)(:-)?\}|^\$([A-Za-z_][A-Za-z0-9_]*)/.exec(
|
|
2150
2320
|
command.slice(i),
|
|
2151
2321
|
);
|
|
2152
2322
|
if (!variable) {
|
|
@@ -2157,7 +2327,13 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2157
2327
|
continue;
|
|
2158
2328
|
}
|
|
2159
2329
|
expansions++;
|
|
2160
|
-
const name = variable[1] ?? variable[
|
|
2330
|
+
const name = variable[1] ?? variable[3] ?? "";
|
|
2331
|
+
if (variable[2]) {
|
|
2332
|
+
if (quote !== '"' || !allowedNames.has(name)) invalidExpansion = true;
|
|
2333
|
+
normalized += "ENVTESTVALUE";
|
|
2334
|
+
i += variable[0].length - 1;
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2161
2337
|
const assignment = assignments.get(name);
|
|
2162
2338
|
if (assignment && assignment.end >= i) invalidExpansion = true;
|
|
2163
2339
|
if (assignment && assignment.end < i) {
|
|
@@ -2182,6 +2358,39 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2182
2358
|
if (!hasSafeLocalAssignmentCommands(localMaterialized, assignments)) return true;
|
|
2183
2359
|
const parsed = parseStaticShellCommands(localMaterialized);
|
|
2184
2360
|
if (!parsed || typeof parsed === "string") return true;
|
|
2361
|
+
for (const [index, pipeline] of parsed.pipelines.entries()) {
|
|
2362
|
+
if (!pipeline.some((words) => words.some((arg) => arg.includes("NUMERICSTATUSVALUE"))))
|
|
2363
|
+
continue;
|
|
2364
|
+
if (parsed.redirects.some((redirect) => redirect.pipeline === index)) return true;
|
|
2365
|
+
if (
|
|
2366
|
+
pipeline.some((words) => {
|
|
2367
|
+
const argv = unwrapStaticCommand(words);
|
|
2368
|
+
if (!words.some((arg) => arg.includes("NUMERICSTATUSVALUE")))
|
|
2369
|
+
return !isPassiveTextCommand(argv);
|
|
2370
|
+
const name = argv[0]?.split("/").at(-1);
|
|
2371
|
+
const format = name === "printf" ? passivePrintfFormatIndex(argv) : undefined;
|
|
2372
|
+
return (
|
|
2373
|
+
!["echo", "printf"].includes(name ?? "") ||
|
|
2374
|
+
!isPassiveTextCommand(argv) ||
|
|
2375
|
+
words
|
|
2376
|
+
.slice(0, words.length - argv.length)
|
|
2377
|
+
.some((arg) => arg.includes("NUMERICSTATUSVALUE")) ||
|
|
2378
|
+
argv[0]?.includes("NUMERICSTATUSVALUE") === true ||
|
|
2379
|
+
(name === "printf" &&
|
|
2380
|
+
(format === undefined ||
|
|
2381
|
+
argv.slice(0, format + 1).some((arg) => arg.includes("NUMERICSTATUSVALUE"))))
|
|
2382
|
+
);
|
|
2383
|
+
})
|
|
2384
|
+
)
|
|
2385
|
+
return true;
|
|
2386
|
+
}
|
|
2387
|
+
for (const words of parsed.pipelines.flat()) {
|
|
2388
|
+
if (
|
|
2389
|
+
words.some((arg) => arg.includes("ENVTESTVALUE")) &&
|
|
2390
|
+
!isApprovedEnvironmentPredicate(unwrapStaticCommand(words))
|
|
2391
|
+
)
|
|
2392
|
+
return true;
|
|
2393
|
+
}
|
|
2185
2394
|
for (const words of parsed.pipelines.flat()) {
|
|
2186
2395
|
if (!words.some((arg) => arg.includes("UNKNOWNVALUE"))) continue;
|
|
2187
2396
|
if (words.some((arg) => arg.includes("UNKNOWNVALUEUNQUOTED"))) return true;
|
|
@@ -2192,7 +2401,7 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2192
2401
|
)
|
|
2193
2402
|
return true;
|
|
2194
2403
|
}
|
|
2195
|
-
if (!localMaterialized.includes("ENVVALUE")) {
|
|
2404
|
+
if (!localMaterialized.includes("ENVVALUE") && !localMaterialized.includes("ENVTESTVALUE")) {
|
|
2196
2405
|
return (
|
|
2197
2406
|
hasSensitiveFileRead(localMaterialized, isPathBlocked) ||
|
|
2198
2407
|
hasSensitivePathRedirect(localMaterialized, isPathBlocked) ||
|
|
@@ -2202,7 +2411,7 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2202
2411
|
|
|
2203
2412
|
for (const [index, pipeline] of parsed.pipelines.entries()) {
|
|
2204
2413
|
const hasEnvironmentValue = pipeline.some((argv) =>
|
|
2205
|
-
argv.some((arg) => arg.includes("ENVVALUE")),
|
|
2414
|
+
argv.some((arg) => arg.includes("ENVVALUE") || arg.includes("ENVTESTVALUE")),
|
|
2206
2415
|
);
|
|
2207
2416
|
if (!hasEnvironmentValue) continue;
|
|
2208
2417
|
if (parsed.redirects.some((redirect) => redirect.pipeline === index)) return true;
|
|
@@ -2210,9 +2419,14 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2210
2419
|
pipeline.some((argv) => {
|
|
2211
2420
|
const unwrapped = unwrapStaticCommand(argv);
|
|
2212
2421
|
const name = unwrapped[0]?.split("/").at(-1) ?? "";
|
|
2213
|
-
if (argv.some((arg) => arg.includes("ENVVALUE"))) {
|
|
2214
|
-
if (
|
|
2422
|
+
if (argv.some((arg) => arg.includes("ENVVALUE") || arg.includes("ENVTESTVALUE"))) {
|
|
2423
|
+
if (
|
|
2424
|
+
argv
|
|
2425
|
+
.slice(0, argv.length - unwrapped.length)
|
|
2426
|
+
.some((arg) => arg.includes("ENVVALUE") || arg.includes("ENVTESTVALUE"))
|
|
2427
|
+
)
|
|
2215
2428
|
return true;
|
|
2429
|
+
if (isApprovedEnvironmentPredicate(unwrapped)) return false;
|
|
2216
2430
|
if (isNavigationOperand(unwrapped, "ENVVALUE")) {
|
|
2217
2431
|
return unwrapped.some((arg) => arg.includes("ENVVALUEUNQUOTED"));
|
|
2218
2432
|
}
|
|
@@ -2239,6 +2453,24 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2239
2453
|
return hasSensitiveFileRead(materialized, isPathBlocked);
|
|
2240
2454
|
}
|
|
2241
2455
|
|
|
2456
|
+
/** Quoted approved metadata can be compared or checked for blankness, never evaluated. */
|
|
2457
|
+
function isApprovedEnvironmentPredicate(argv: string[]): boolean {
|
|
2458
|
+
let args = argv.slice(1);
|
|
2459
|
+
if (argv[0] === "[") {
|
|
2460
|
+
if (args.at(-1) !== "]") return false;
|
|
2461
|
+
args = args.slice(0, -1);
|
|
2462
|
+
} else if (argv[0] !== "test") return false;
|
|
2463
|
+
const value = (arg: string | undefined) => arg === "ENVVALUE" || arg === "ENVTESTVALUE";
|
|
2464
|
+
const literal = (arg: string | undefined) =>
|
|
2465
|
+
/^[A-Za-z0-9_.:-]*$/.test(arg ?? "") && arg !== undefined;
|
|
2466
|
+
return (
|
|
2467
|
+
(args.length === 2 && ["-n", "-z"].includes(args[0] ?? "") && value(args[1])) ||
|
|
2468
|
+
(args.length === 3 &&
|
|
2469
|
+
args[1] === "=" &&
|
|
2470
|
+
((value(args[0]) && literal(args[2])) || (literal(args[0]) && value(args[2]))))
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2242
2474
|
function mentionsEnvironmentSource(command: string): boolean {
|
|
2243
2475
|
return /\b(?:os\s*\.\s*(?:environ|getenv)|process\s*\.\s*env|Environment\s*\.\s*GetEnvironmentVariable)\b/.test(
|
|
2244
2476
|
command,
|