@blogic-cz/agent-tools 1.6.11 → 1.6.13
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.
|
@@ -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;AA66FF,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,CA+XrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UAt1GD,SAAS,UAAU,UAAU,KAAK,IAs1GM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAx1GC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAw1GjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aA11GE,MAAM,KAAK,OA01GgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aA51GE,MAAM,KAAK,OA41GgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YA91GC,MAAM,KAAK,OA81G0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YAh2GC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAg2Gf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YAl2GC,MAAM,KAAK,OAk2GsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YAp2GC,MAAM,KAAK,MAAM,GAAG,IAo2Ga,CAAC"}
|
package/package.json
CHANGED
|
@@ -226,11 +226,13 @@ function isSafeHerdrTabCreate(argv: string[]): boolean {
|
|
|
226
226
|
function hasSafeLocalAssignmentCommands(
|
|
227
227
|
command: string,
|
|
228
228
|
assignments: Map<string, { value: string; end: number }>,
|
|
229
|
+
statusCaptures: Set<string>,
|
|
229
230
|
): boolean {
|
|
230
231
|
if (!assignments.size) return true;
|
|
231
232
|
const parsed = parseStaticShellCommands(command);
|
|
232
233
|
if (!parsed || typeof parsed === "string") return false;
|
|
233
234
|
return parsed.pipelines.flat().every((words) => {
|
|
235
|
+
if (words.length === 1 && statusCaptures.has(words[0] ?? "")) return true;
|
|
234
236
|
const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=([A-Za-z0-9_./-]+)$/.exec(words[0] ?? "");
|
|
235
237
|
if (assignment) {
|
|
236
238
|
return (
|
|
@@ -420,6 +422,7 @@ function literalBindingText(
|
|
|
420
422
|
command: string,
|
|
421
423
|
name: string,
|
|
422
424
|
value: string,
|
|
425
|
+
allowBrackets = false,
|
|
423
426
|
): { text: string; syntax: string } | undefined {
|
|
424
427
|
let text = "";
|
|
425
428
|
let syntax = "";
|
|
@@ -451,7 +454,11 @@ function literalBindingText(
|
|
|
451
454
|
syntax += quote ? " ".repeat(value.length) : value;
|
|
452
455
|
i += match[0].length - 1;
|
|
453
456
|
} else {
|
|
454
|
-
if (
|
|
457
|
+
if (
|
|
458
|
+
char === "`" ||
|
|
459
|
+
(!quote && (/[(){}*?#]/.test(char) || (!allowBrackets && (char === "[" || char === "]"))))
|
|
460
|
+
)
|
|
461
|
+
return undefined;
|
|
455
462
|
text += char;
|
|
456
463
|
syntax += quote ? " " : char;
|
|
457
464
|
}
|
|
@@ -606,24 +613,45 @@ function literalShellBodies(command: string): string[] | undefined {
|
|
|
606
613
|
return bodies;
|
|
607
614
|
}
|
|
608
615
|
|
|
609
|
-
function isProvedLiteralShellBody(
|
|
616
|
+
function isProvedLiteralShellBody(
|
|
617
|
+
body: string,
|
|
618
|
+
allowedNames: Set<string>,
|
|
619
|
+
isPathBlocked: (path: string) => boolean,
|
|
620
|
+
onProvedPredicate: () => void,
|
|
621
|
+
): boolean {
|
|
610
622
|
if (!body.trim()) return true;
|
|
611
623
|
const materialized = materializeLiteralShell(body)?.command ?? body;
|
|
612
624
|
const parsed = parseStaticShellCommands(materialized);
|
|
613
|
-
if (!parsed || typeof parsed === "string"
|
|
614
|
-
|
|
625
|
+
if (!parsed || typeof parsed === "string") {
|
|
626
|
+
let proved = false;
|
|
627
|
+
if (
|
|
628
|
+
hasEnvironmentVariableExpansionRead(body, allowedNames, isPathBlocked, undefined, () => {
|
|
629
|
+
proved = true;
|
|
630
|
+
}) ||
|
|
631
|
+
!proved
|
|
632
|
+
)
|
|
633
|
+
return false;
|
|
634
|
+
onProvedPredicate();
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
if (!hasProvedNavigationSequence(materialized)) return false;
|
|
615
638
|
if (isStaticHerdrPrompt(materialized)) return true;
|
|
616
639
|
return parsed.pipelines.flat().every((words) => {
|
|
617
640
|
const argv = unwrapProofCommand(words, "\0");
|
|
618
641
|
if (!argv) return false;
|
|
619
642
|
if (argv[0]?.split("/").at(-1) === "env") return false;
|
|
620
643
|
const name = unwrapStaticCommand(argv)[0]?.split("/").at(-1) ?? "";
|
|
621
|
-
if (isShellWord(name))
|
|
644
|
+
if (isShellWord(name)) {
|
|
645
|
+
const nested = literalShellBodies(
|
|
646
|
+
words.map((word) => "'" + word.replaceAll("'", "'\"'\"'") + "'").join(" "),
|
|
647
|
+
);
|
|
622
648
|
return (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
649
|
+
nested !== undefined &&
|
|
650
|
+
nested.every((inner) =>
|
|
651
|
+
isProvedLiteralShellBody(inner, allowedNames, isPathBlocked, onProvedPredicate),
|
|
652
|
+
)
|
|
626
653
|
);
|
|
654
|
+
}
|
|
627
655
|
return (
|
|
628
656
|
isProvedCwdSubprocessRole(unwrapStaticCommand(argv)) ||
|
|
629
657
|
(name === "bun" && isSafeLocalAssignmentCommand(argv) && !isUnsupportedInlineRuntime(argv)) ||
|
|
@@ -1890,6 +1918,37 @@ function isStaticNavigationCommand(argv: string[]): boolean {
|
|
|
1890
1918
|
);
|
|
1891
1919
|
}
|
|
1892
1920
|
|
|
1921
|
+
/** Literal wrappers and exact directory-operation roles; unknown consumers keep refusal. */
|
|
1922
|
+
function isProvedDirectoryCommand(words: string[]): boolean {
|
|
1923
|
+
const argv = unwrapStaticCommand(words);
|
|
1924
|
+
if (
|
|
1925
|
+
!words
|
|
1926
|
+
.slice(0, words.length - argv.length)
|
|
1927
|
+
.every((word) => ["rtk", "proxy", "command", "--"].includes(word))
|
|
1928
|
+
)
|
|
1929
|
+
return false;
|
|
1930
|
+
if (argv[0] === "cd") return isStaticNavigationCommand(argv);
|
|
1931
|
+
if (argv[0] !== "git") return false;
|
|
1932
|
+
let index = 1;
|
|
1933
|
+
while (argv[index] === "-C") {
|
|
1934
|
+
if (!isStaticNavigationCommand(["cd", argv[index + 1] ?? ""])) return false;
|
|
1935
|
+
index += 2;
|
|
1936
|
+
}
|
|
1937
|
+
const args = argv.slice(index);
|
|
1938
|
+
if (args[0] === "status")
|
|
1939
|
+
return args.length === 1 || (args.length === 2 && args[1] === "--short");
|
|
1940
|
+
if (args[0] === "fetch")
|
|
1941
|
+
return (
|
|
1942
|
+
args.length >= 2 &&
|
|
1943
|
+
args.length <= 3 &&
|
|
1944
|
+
args.slice(1).every((arg) => /^[A-Za-z0-9_][A-Za-z0-9_./-]*$/.test(arg))
|
|
1945
|
+
);
|
|
1946
|
+
return (
|
|
1947
|
+
(args.length === 2 && args[0] === "rev-parse" && args[1] === "HEAD") ||
|
|
1948
|
+
(args.length === 3 && args[0] === "rev-list" && args[1] === "--count" && args[2] === "HEAD")
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1893
1952
|
/** Prove a single cd whose uses are gated on success, without simulating shell state. */
|
|
1894
1953
|
function hasProvedNavigationSequence(command: string): boolean {
|
|
1895
1954
|
const literal = literalBindingText(command, "", "");
|
|
@@ -2279,6 +2338,7 @@ function hasSensitiveFileRead(command: string, isPathBlocked: (path: string) =>
|
|
|
2279
2338
|
if (parsed && parsed !== "brace-expansion") {
|
|
2280
2339
|
let directory = "";
|
|
2281
2340
|
const trackDirectory = hasProvedNavigationSequence(command);
|
|
2341
|
+
const safeDirectorySequence = parsed.pipelines.flat().every(isProvedDirectoryCommand);
|
|
2282
2342
|
return parsed.pipelines.flat().some((words) => {
|
|
2283
2343
|
const argv = unwrapStaticCommand(words);
|
|
2284
2344
|
const inspectedArgv =
|
|
@@ -2291,19 +2351,36 @@ function hasSensitiveFileRead(command: string, isPathBlocked: (path: string) =>
|
|
|
2291
2351
|
path.startsWith("/") ? posix.normalize(path) : posix.join(commandDirectory, path);
|
|
2292
2352
|
const blockedOperand = (path: string) =>
|
|
2293
2353
|
[path, inDirectory(path)].some((value) => isPathBlocked(value) || isSensitivePath(value));
|
|
2294
|
-
|
|
2354
|
+
// Directory slots use path policies, not the broad credential-file name heuristic.
|
|
2355
|
+
const blockedDirectory = (path: string) =>
|
|
2356
|
+
[path, inDirectory(path)].some(
|
|
2357
|
+
(value) => isPathBlocked(value) || isPathBlocked(value + "/"),
|
|
2358
|
+
);
|
|
2295
2359
|
if (name === "cd") {
|
|
2296
2360
|
const path = inspected[inspected[1] === "--" ? 2 : 1];
|
|
2297
2361
|
if (path === undefined) return false;
|
|
2298
2362
|
if (blockedDirectory(path)) return true;
|
|
2363
|
+
// A successful cd alone proves neither its later readers nor pipeline consumers safe.
|
|
2364
|
+
if (
|
|
2365
|
+
(!trackDirectory || !safeDirectorySequence) &&
|
|
2366
|
+
(blockedOperand(path) || blockedOperand(path + "/"))
|
|
2367
|
+
)
|
|
2368
|
+
return true;
|
|
2299
2369
|
if (trackDirectory && isStaticNavigationCommand(inspected)) directory = inDirectory(path);
|
|
2300
2370
|
return false;
|
|
2301
2371
|
}
|
|
2302
2372
|
if (name === "git") {
|
|
2303
2373
|
let index = 1;
|
|
2304
2374
|
while (inspected[index] === "-C" && inspected[index + 1]) {
|
|
2305
|
-
|
|
2306
|
-
|
|
2375
|
+
const path = inspected[index + 1] ?? "";
|
|
2376
|
+
if (blockedDirectory(path)) return true;
|
|
2377
|
+
// Git -C affects this command only; following consumers keep the shell directory.
|
|
2378
|
+
if (
|
|
2379
|
+
!isProvedDirectoryCommand(words) &&
|
|
2380
|
+
(blockedOperand(path) || blockedOperand(path + "/"))
|
|
2381
|
+
)
|
|
2382
|
+
return true;
|
|
2383
|
+
commandDirectory = inDirectory(path);
|
|
2307
2384
|
index += 2;
|
|
2308
2385
|
}
|
|
2309
2386
|
// Git grep has the same pattern-versus-file roles as grep below.
|
|
@@ -2483,7 +2560,10 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2483
2560
|
command: string,
|
|
2484
2561
|
allowedNames: Set<string>,
|
|
2485
2562
|
isPathBlocked: (path: string) => boolean,
|
|
2563
|
+
onProvedStatus?: (command: string) => void,
|
|
2564
|
+
onProvedPredicate?: () => void,
|
|
2486
2565
|
): boolean {
|
|
2566
|
+
const originalCommand = command;
|
|
2487
2567
|
let heredoc = boundedHeredoc(command);
|
|
2488
2568
|
while (heredoc) {
|
|
2489
2569
|
// In an unquoted heredoc, # and quote characters are data, not shell syntax.
|
|
@@ -2492,10 +2572,18 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2492
2572
|
heredoc = boundedHeredoc(command);
|
|
2493
2573
|
}
|
|
2494
2574
|
if (!command.includes("$") && !command.includes("`")) return false;
|
|
2575
|
+
let predicateSyntaxIntact = command === originalCommand;
|
|
2495
2576
|
let invalidExpansion = false;
|
|
2496
2577
|
const assignments = getLeadingLiteralAssignments(command);
|
|
2497
|
-
const localValues = new Map<string, string>();
|
|
2498
2578
|
let localIndex = 0;
|
|
2579
|
+
const directCaptures = new Map<string, string>();
|
|
2580
|
+
const statusReferences = new Map<string, { name: string; fallback: string }>();
|
|
2581
|
+
const marker = (prefix: string): string => {
|
|
2582
|
+
let value: string;
|
|
2583
|
+
do value = `${prefix}${localIndex++}END`;
|
|
2584
|
+
while (command.includes(value));
|
|
2585
|
+
return value;
|
|
2586
|
+
};
|
|
2499
2587
|
let expansions = 0;
|
|
2500
2588
|
let normalized = "";
|
|
2501
2589
|
let quote: "'" | '"' | undefined;
|
|
@@ -2503,6 +2591,7 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2503
2591
|
for (let i = 0; i < command.length; i++) {
|
|
2504
2592
|
const char = command[i] ?? "";
|
|
2505
2593
|
if (!quote && char === "#" && !wordStarted) {
|
|
2594
|
+
predicateSyntaxIntact = false;
|
|
2506
2595
|
while (i + 1 < command.length && command[i + 1] !== "\n") i++;
|
|
2507
2596
|
continue;
|
|
2508
2597
|
}
|
|
@@ -2534,7 +2623,18 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2534
2623
|
}
|
|
2535
2624
|
if (command[i + 1] === "?") {
|
|
2536
2625
|
expansions++;
|
|
2537
|
-
|
|
2626
|
+
const value = marker("NUMERICSTATUSVALUE");
|
|
2627
|
+
// Only an exact standalone assignment token captures in the parent shell.
|
|
2628
|
+
// Quoting/escaping the name can instead produce an executable command word.
|
|
2629
|
+
const capture = /(?:^|[;\n])[ \t]*([A-Za-z_][A-Za-z0-9_]*)=("?)$/.exec(command.slice(0, i));
|
|
2630
|
+
const ending = capture?.[2] === '"' ? '"' : "";
|
|
2631
|
+
if (
|
|
2632
|
+
capture &&
|
|
2633
|
+
command.slice(i + 2).startsWith(ending) &&
|
|
2634
|
+
/^[ \t]*(?:[;\n]|$)/.test(command.slice(i + 2 + ending.length))
|
|
2635
|
+
)
|
|
2636
|
+
directCaptures.set(value, capture[1] ?? "");
|
|
2637
|
+
normalized += value;
|
|
2538
2638
|
i++;
|
|
2539
2639
|
continue;
|
|
2540
2640
|
}
|
|
@@ -2558,26 +2658,101 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2558
2658
|
}
|
|
2559
2659
|
const assignment = assignments.get(name);
|
|
2560
2660
|
if (assignment && assignment.end >= i) invalidExpansion = true;
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2661
|
+
const reference = marker("STATUSREFERENCE");
|
|
2662
|
+
const fallback =
|
|
2663
|
+
assignment && assignment.end < i
|
|
2664
|
+
? assignment.value
|
|
2665
|
+
: allowedNames.has(name)
|
|
2666
|
+
? quote
|
|
2667
|
+
? "ENVVALUE"
|
|
2668
|
+
: "ENVVALUEUNQUOTED"
|
|
2669
|
+
: quote
|
|
2670
|
+
? "UNKNOWNVALUE"
|
|
2671
|
+
: "UNKNOWNVALUEUNQUOTED";
|
|
2672
|
+
statusReferences.set(reference, { name, fallback });
|
|
2673
|
+
normalized += reference;
|
|
2570
2674
|
i += variable[0].length - 1;
|
|
2571
2675
|
}
|
|
2572
2676
|
if (!expansions) return invalidExpansion;
|
|
2573
2677
|
if (invalidExpansion) return true;
|
|
2574
2678
|
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
);
|
|
2679
|
+
// Resolve every reference at its command position; a later capture cannot prove
|
|
2680
|
+
// an earlier read, and only proved passive intermediates preserve the binding.
|
|
2681
|
+
const initial = parseStaticShellCommands(normalized);
|
|
2682
|
+
if (!initial || typeof initial === "string") return true;
|
|
2683
|
+
const captures = new Map<string, string>();
|
|
2684
|
+
for (const words of initial.pipelines.flat()) {
|
|
2685
|
+
const capture = /^([A-Za-z_][A-Za-z0-9_]*)=(.+)$/.exec(words[0] ?? "");
|
|
2686
|
+
if (capture && directCaptures.get(capture[2] ?? "") === capture[1])
|
|
2687
|
+
captures.set(words[0] ?? "", capture[1] ?? "");
|
|
2688
|
+
}
|
|
2689
|
+
const statusCaptures = new Set<string>();
|
|
2690
|
+
const localStatuses = new Set<string>();
|
|
2691
|
+
const resolutions = new Map<string, string>();
|
|
2692
|
+
const capturedNames = new Set(captures.values());
|
|
2693
|
+
const bound = new Set<string>();
|
|
2694
|
+
if (captures.size) {
|
|
2695
|
+
const syntax = literalBindingText(normalized, "", "")?.syntax;
|
|
2696
|
+
if (syntax === undefined || /&&|\|\||&/.test(syntax.replace(/[<>]&[0-9-]+/g, ""))) return true;
|
|
2697
|
+
}
|
|
2698
|
+
for (const [index, pipeline] of initial.pipelines.entries()) {
|
|
2699
|
+
for (const words of pipeline) {
|
|
2700
|
+
for (const arg of words) {
|
|
2701
|
+
for (const [reference, { name, fallback }] of statusReferences) {
|
|
2702
|
+
if (!arg.includes(reference)) continue;
|
|
2703
|
+
if (bound.has(name)) {
|
|
2704
|
+
const value = marker("NUMERICSTATUSVALUE");
|
|
2705
|
+
localStatuses.add(value);
|
|
2706
|
+
resolutions.set(reference, value);
|
|
2707
|
+
} else {
|
|
2708
|
+
if (capturedNames.has(name)) return true;
|
|
2709
|
+
resolutions.set(reference, fallback);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
const capture = captures.get(words[0] ?? "");
|
|
2714
|
+
if (capture) {
|
|
2715
|
+
if (
|
|
2716
|
+
pipeline.length !== 1 ||
|
|
2717
|
+
words.length !== 1 ||
|
|
2718
|
+
initial.redirects.some((redirect) => redirect.pipeline === index) ||
|
|
2719
|
+
// Inherited export attributes can make a numeric value select code or paths.
|
|
2720
|
+
/^(?:LD_|DYLD_|_RLD_|LDR_|GIT_CONFIG_(?:KEY|VALUE)_)/.test(capture) ||
|
|
2721
|
+
/^(?:PATH|IFS|CDPATH|BASH_ENV|ENV|FPATH|ZDOTDIR|SHELLOPTS|BASHOPTS|PROMPT_COMMAND|PS[0-4]|GLOBIGNORE|RANDOM|SECONDS|PIPESTATUS|UID|EUID|BASH_XTRACEFD|HOME|XDG_CONFIG_HOME|LIBPATH|SHLIB_PATH|NODE_OPTIONS|NODE_PATH|BUN_OPTIONS|BUN_PRELOAD|PYTHONPATH|PYTHONSTARTUP|PYTHONHOME|RUBYOPT|RUBYLIB|GEM_HOME|GEM_PATH|PERL5OPT|PERL5LIB|PERLLIB|JAVA_TOOL_OPTIONS|JDK_JAVA_OPTIONS|CLASSPATH|_JAVA_OPTIONS|JAVA_HOME|GIT_SSH_COMMAND|GIT_SSH|GIT_PROXY_COMMAND|GIT_EXEC_PATH|GIT_CONFIG|GIT_CONFIG_GLOBAL|GIT_CONFIG_SYSTEM|GIT_CONFIG_PARAMETERS|GIT_CONFIG_COUNT|GIT_ASKPASS|SSH_ASKPASS|GIT_EDITOR|GIT_SEQUENCE_EDITOR|GIT_PAGER|PAGER|EDITOR|VISUAL)$/.test(
|
|
2722
|
+
capture,
|
|
2723
|
+
)
|
|
2724
|
+
)
|
|
2725
|
+
return true;
|
|
2726
|
+
bound.add(capture);
|
|
2727
|
+
statusCaptures.add(words[0] ?? "");
|
|
2728
|
+
continue;
|
|
2729
|
+
}
|
|
2730
|
+
const argv = unwrapStaticCommand(words);
|
|
2731
|
+
const name = argv[0]?.split("/").at(-1) ?? "";
|
|
2732
|
+
if (
|
|
2733
|
+
captures.size &&
|
|
2734
|
+
/^(?:alias|unalias|readonly|declare|typeset|local|export|eval|source|\.|trap|set|read|unset|let)$/.test(
|
|
2735
|
+
name,
|
|
2736
|
+
)
|
|
2737
|
+
)
|
|
2738
|
+
return true;
|
|
2739
|
+
if (!isPassiveTextCommand(argv) && name !== "cat") bound.clear();
|
|
2740
|
+
}
|
|
2741
|
+
for (const redirect of initial.redirects.filter((value) => value.pipeline === index)) {
|
|
2742
|
+
if (
|
|
2743
|
+
[...statusReferences].some(
|
|
2744
|
+
([reference, { name }]) => capturedNames.has(name) && redirect.target.includes(reference),
|
|
2745
|
+
)
|
|
2746
|
+
)
|
|
2747
|
+
return true;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
for (const [reference, { fallback }] of statusReferences) {
|
|
2751
|
+
normalized = normalized.replaceAll(reference, () => resolutions.get(reference) ?? fallback);
|
|
2752
|
+
}
|
|
2753
|
+
const localMaterialized = normalized;
|
|
2579
2754
|
// Options must be checked after local literals become actual argv, never as placeholders.
|
|
2580
|
-
if (!hasSafeLocalAssignmentCommands(localMaterialized, assignments)) return true;
|
|
2755
|
+
if (!hasSafeLocalAssignmentCommands(localMaterialized, assignments, statusCaptures)) return true;
|
|
2581
2756
|
const parsed = parseStaticShellCommands(localMaterialized);
|
|
2582
2757
|
if (!parsed || typeof parsed === "string") return true;
|
|
2583
2758
|
for (const [index, pipeline] of parsed.pipelines.entries()) {
|
|
@@ -2586,6 +2761,7 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2586
2761
|
if (parsed.redirects.some((redirect) => redirect.pipeline === index)) return true;
|
|
2587
2762
|
if (
|
|
2588
2763
|
pipeline.some((words) => {
|
|
2764
|
+
if (words.length === 1 && statusCaptures.has(words[0] ?? "")) return false;
|
|
2589
2765
|
const argv = unwrapStaticCommand(words);
|
|
2590
2766
|
if (!words.some((arg) => arg.includes("NUMERICSTATUSVALUE")))
|
|
2591
2767
|
return !isPassiveTextCommand(argv);
|
|
@@ -2594,6 +2770,9 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2594
2770
|
return (
|
|
2595
2771
|
!["echo", "printf"].includes(name ?? "") ||
|
|
2596
2772
|
!isPassiveTextCommand(argv) ||
|
|
2773
|
+
argv.some(
|
|
2774
|
+
(arg) => arg.startsWith("-") && [...localStatuses].some((value) => arg.includes(value)),
|
|
2775
|
+
) ||
|
|
2597
2776
|
words
|
|
2598
2777
|
.slice(0, words.length - argv.length)
|
|
2599
2778
|
.some((arg) => arg.includes("NUMERICSTATUSVALUE")) ||
|
|
@@ -2617,11 +2796,12 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2617
2796
|
return true;
|
|
2618
2797
|
}
|
|
2619
2798
|
if (!localMaterialized.includes("ENVVALUE")) {
|
|
2620
|
-
|
|
2799
|
+
const blocked =
|
|
2621
2800
|
hasSensitiveFileRead(localMaterialized, isPathBlocked) ||
|
|
2622
2801
|
hasSensitivePathRedirect(localMaterialized, isPathBlocked) ||
|
|
2623
|
-
hasEnvironmentRead(localMaterialized, allowedNames)
|
|
2624
|
-
);
|
|
2802
|
+
hasEnvironmentRead(localMaterialized, allowedNames);
|
|
2803
|
+
if (!blocked && statusCaptures.size) onProvedStatus?.(localMaterialized);
|
|
2804
|
+
return blocked;
|
|
2625
2805
|
}
|
|
2626
2806
|
|
|
2627
2807
|
for (const [index, pipeline] of parsed.pipelines.entries()) {
|
|
@@ -2657,11 +2837,25 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
2657
2837
|
return true;
|
|
2658
2838
|
}
|
|
2659
2839
|
}
|
|
2660
|
-
const materialized =
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2840
|
+
const materialized = normalized.replaceAll("ENVVALUE", "SAFEENVVALUE");
|
|
2841
|
+
const blocked = hasSensitiveFileRead(materialized, isPathBlocked);
|
|
2842
|
+
if (!blocked && statusCaptures.size) onProvedStatus?.(materialized);
|
|
2843
|
+
// This is a complete command proof, not merely a recursively safe shell body.
|
|
2844
|
+
if (
|
|
2845
|
+
!blocked &&
|
|
2846
|
+
parsed.pipelines.length === 1 &&
|
|
2847
|
+
parsed.pipelines[0]?.length === 1 &&
|
|
2848
|
+
!parsed.redirects.length
|
|
2849
|
+
) {
|
|
2850
|
+
const argv = unwrapProofCommand(parsed.pipelines[0]?.[0] ?? [], "ENVVALUE");
|
|
2851
|
+
if (argv && isApprovedEnvironmentPredicate(argv) && predicateSyntaxIntact) {
|
|
2852
|
+
// Roles alone cannot prove original shell boundaries or discarded controls.
|
|
2853
|
+
// Brackets are enabled only for the completely proved bracket builtin.
|
|
2854
|
+
const binding = literalBindingText(normalized, "", "", argv[0] === "[");
|
|
2855
|
+
if (binding && !/[;&|<>\r\n]/.test(binding.syntax.trim())) onProvedPredicate?.();
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
return blocked;
|
|
2665
2859
|
}
|
|
2666
2860
|
|
|
2667
2861
|
/** Quoted approved metadata can be compared or checked for blankness, never evaluated. */
|
|
@@ -2971,11 +3165,37 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
2971
3165
|
const shellBodies = literalShellBodies(command);
|
|
2972
3166
|
if (!shellBodies || (shellBodies.length && shellDepth >= 8))
|
|
2973
3167
|
return "cannot prove a bounded literal shell invocation";
|
|
3168
|
+
const provedPredicateShellBodies: string[] = [];
|
|
2974
3169
|
for (const body of shellBodies) {
|
|
2975
3170
|
const reason = getDangerousBashReason(body, shellDepth + 1);
|
|
2976
3171
|
if (reason || getBlockedCliTool(body, true, shellDepth + 1))
|
|
2977
3172
|
return reason ?? "invokes a blocked CLI from a shell body";
|
|
2978
|
-
if (
|
|
3173
|
+
if (
|
|
3174
|
+
!isProvedLiteralShellBody(body, allowedEnvironmentVariables, isPathBlocked, () => {
|
|
3175
|
+
provedPredicateShellBodies.push(body);
|
|
3176
|
+
})
|
|
3177
|
+
)
|
|
3178
|
+
return "cannot prove a closed literal shell body";
|
|
3179
|
+
}
|
|
3180
|
+
if (provedPredicateShellBodies.length) {
|
|
3181
|
+
const context = parseStaticShellCommands(command);
|
|
3182
|
+
const binding = literalBindingText(command, "", "");
|
|
3183
|
+
const argv =
|
|
3184
|
+
context && typeof context !== "string"
|
|
3185
|
+
? unwrapProofCommand(context.pipelines[0]?.[0] ?? [], "\0")
|
|
3186
|
+
: undefined;
|
|
3187
|
+
if (
|
|
3188
|
+
!context ||
|
|
3189
|
+
typeof context === "string" ||
|
|
3190
|
+
context.pipelines.length !== 1 ||
|
|
3191
|
+
context.pipelines[0]?.length !== 1 ||
|
|
3192
|
+
context.redirects.length !== 0 ||
|
|
3193
|
+
!binding ||
|
|
3194
|
+
/[;&|<>\r\n]/.test(binding.syntax.trim()) ||
|
|
3195
|
+
!argv ||
|
|
3196
|
+
!SHELL_IDENTITIES.has(argv[0] ?? "")
|
|
3197
|
+
)
|
|
3198
|
+
return "cannot prove a single literal shell predicate invocation";
|
|
2979
3199
|
}
|
|
2980
3200
|
const proof = materializeLiteralShell(command);
|
|
2981
3201
|
if (!proof && /^\s*for\b/.test(command)) return "cannot prove a bounded literal shell loop";
|
|
@@ -2999,19 +3219,28 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
2999
3219
|
hasSensitivePathRedirect(command, isPathBlocked)
|
|
3000
3220
|
)
|
|
3001
3221
|
return "blocked by the file-access policy; a sensitive path matched or this command form could not be verified";
|
|
3222
|
+
let environmentCommand = command;
|
|
3002
3223
|
if (
|
|
3003
3224
|
!isLiteralTextWrite(command) &&
|
|
3004
|
-
hasEnvironmentVariableExpansionRead(
|
|
3225
|
+
hasEnvironmentVariableExpansionRead(
|
|
3226
|
+
command,
|
|
3227
|
+
allowedEnvironmentVariables,
|
|
3228
|
+
isPathBlocked,
|
|
3229
|
+
(proved) => {
|
|
3230
|
+
environmentCommand = proved;
|
|
3231
|
+
},
|
|
3232
|
+
)
|
|
3005
3233
|
) {
|
|
3006
3234
|
return "expands an unapproved or executable environment variable value";
|
|
3007
3235
|
}
|
|
3008
3236
|
if (
|
|
3009
3237
|
hasEnvironmentSourceAccess(command, rawEnvironmentSource, allowedEnvironmentVariables) ||
|
|
3010
3238
|
hasEnvironmentRead(
|
|
3011
|
-
|
|
3239
|
+
environmentCommand,
|
|
3012
3240
|
allowedEnvironmentVariables,
|
|
3013
3241
|
allowPackageInventory,
|
|
3014
3242
|
shellBodies.filter((body) => {
|
|
3243
|
+
if (provedPredicateShellBodies.includes(body)) return true;
|
|
3015
3244
|
const parsed = parseStaticShellCommands(body);
|
|
3016
3245
|
return (
|
|
3017
3246
|
parsed &&
|