@blogic-cz/agent-tools 1.5.1 → 1.5.2
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;AAM5E,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;AAM5E,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;AA8lBF,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,CAuNrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UA/1BD,SAAS,UAAU,UAAU,KAAK,IA+1BM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAj2BC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAi2BjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aAn2BE,MAAM,KAAK,OAm2BgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aAr2BE,MAAM,KAAK,OAq2BgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YAv2BC,MAAM,KAAK,OAu2B0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YAz2BC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAy2Bf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YA32BC,MAAM,KAAK,OA22BsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YA72BC,MAAM,KAAK,MAAM,GAAG,IA62Ba,CAAC"}
|
package/package.json
CHANGED
|
@@ -139,19 +139,6 @@ const SECRET_PATTERNS = [
|
|
|
139
139
|
},
|
|
140
140
|
];
|
|
141
141
|
|
|
142
|
-
/**
|
|
143
|
-
* Dangerous bash patterns that might expose secrets.
|
|
144
|
-
*/
|
|
145
|
-
const DEFAULT_DANGEROUS_BASH_PATTERNS: RegExp[] = [
|
|
146
|
-
/\bcat\s+\S*\.env/i,
|
|
147
|
-
/\bcat\s+\S*\.pem/i,
|
|
148
|
-
/\bcat\s+\S*\.key/i,
|
|
149
|
-
/\bcat\s+\S*secret/i,
|
|
150
|
-
/\bcat\s+\S*credential/i,
|
|
151
|
-
/\bcat\s+\S*\/\.ssh\//i,
|
|
152
|
-
/\bcat\s+\S*\/\.aws\//i,
|
|
153
|
-
];
|
|
154
|
-
|
|
155
142
|
/**
|
|
156
143
|
* CLI tools that must use wrapper tools for security and audit.
|
|
157
144
|
*/
|
|
@@ -313,13 +300,28 @@ function parseStaticShellCommands(
|
|
|
313
300
|
|
|
314
301
|
function mentionsEnvironmentRead(text: string): boolean {
|
|
315
302
|
// `--env dev` is a flag of another command, not the env command.
|
|
316
|
-
return /printenv|(
|
|
303
|
+
return /(?<![\w.-])printenv\b(?!-)|(?<![\w.-])-\w*Oprintenv\b|(?<![\w.-])\benv\b(?!-)/i.test(
|
|
304
|
+
text.replace(/['"\\]/g, ""),
|
|
305
|
+
);
|
|
317
306
|
}
|
|
318
307
|
|
|
319
308
|
function hasBraceExpansion(text: string): boolean {
|
|
320
309
|
return /\{[^{}]*(?:,|\.\.)[^{}]*\}/.test(text);
|
|
321
310
|
}
|
|
322
311
|
|
|
312
|
+
function hasNonExecutingAwkProgram(text: string): boolean {
|
|
313
|
+
const match = /\bawk\s+(['"])([\s\S]*?)\1/.exec(text);
|
|
314
|
+
if (!match) return false;
|
|
315
|
+
const program = match[2] ?? "";
|
|
316
|
+
return !/\b(?:system|getline)\s*\(|\||\b(?:print|printf)\b[^\n]*>/.test(program);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function hasShellBraceExpansion(text: string): boolean {
|
|
320
|
+
if (!hasBraceExpansion(text)) return false;
|
|
321
|
+
if (!hasNonExecutingAwkProgram(text)) return true;
|
|
322
|
+
return hasBraceExpansion(text.replace(/(\bawk\s+(['"]))[\s\S]*?\2/, "awk "));
|
|
323
|
+
}
|
|
324
|
+
|
|
323
325
|
/**
|
|
324
326
|
* The command without quoted regex quantifiers such as `{0,80}`. Many commands re-parse quoted
|
|
325
327
|
* text as shell code, so every other quoted brace stays. When both sides are digits, the expansion
|
|
@@ -355,7 +357,14 @@ function withoutQuotedQuantifiers(command: string): string {
|
|
|
355
357
|
|
|
356
358
|
/** Braces in static words, except `{m,n}` with digits on both sides (see above). */
|
|
357
359
|
function hasArgumentBraceExpansion(text: string): boolean {
|
|
358
|
-
return
|
|
360
|
+
return hasShellBraceExpansion(text.replace(/\{\d+,\d+\}/g, ""));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function hasCommandArgumentBraceExpansion(argv: string[]): boolean {
|
|
364
|
+
return (argv[0]?.split("/").at(-1) === "awk" && !isAwkExecution(argv)) ||
|
|
365
|
+
isJqObjectConstruction(argv)
|
|
366
|
+
? false
|
|
367
|
+
: hasArgumentBraceExpansion(argv.join(" "));
|
|
359
368
|
}
|
|
360
369
|
|
|
361
370
|
/** echo and printf print their arguments; grep -o, rg and git grep print matched pattern text. */
|
|
@@ -363,6 +372,68 @@ function isLiteralTextProducer(argv: string[]): boolean {
|
|
|
363
372
|
return isPassiveTextCommand(argv) && argv[0]?.split("/").at(-1) !== "head";
|
|
364
373
|
}
|
|
365
374
|
|
|
375
|
+
function isAwkExecution(argv: string[]): boolean {
|
|
376
|
+
if (argv[0]?.split("/").at(-1) !== "awk") return false;
|
|
377
|
+
const program = argv.slice(1).join(" ");
|
|
378
|
+
return /\b(?:system|getline)\s*\(|\||\b(?:print|printf)\b[^\n]*>/.test(program);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isJqObjectConstruction(argv: string[]): boolean {
|
|
382
|
+
return (
|
|
383
|
+
argv[0]?.split("/").at(-1) === "jq" &&
|
|
384
|
+
argv
|
|
385
|
+
.slice(1)
|
|
386
|
+
.some((arg) => /^\{[A-Za-z_][A-Za-z0-9_-]*(?:,\s*[A-Za-z_][A-Za-z0-9_-]*)*\}$/.test(arg)) &&
|
|
387
|
+
!argv.slice(1).some((arg) => /\benv\b|\binput(?:s)?\b/.test(arg))
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Index in `args` where the command wrapped by `env` starts, or null when `env` only lists. */
|
|
392
|
+
function environmentCommandStart(args: string[]): number | null {
|
|
393
|
+
for (let i = 0; i < args.length; i++) {
|
|
394
|
+
const arg = args[i] ?? "";
|
|
395
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(arg)) continue;
|
|
396
|
+
if (arg === "--") return i + 1 === args.length ? null : i + 1;
|
|
397
|
+
if (arg === "-u" || arg === "--unset" || arg === "-C" || arg === "--chdir") {
|
|
398
|
+
i++;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (arg === "-i" || arg === "--ignore-environment" || arg.startsWith("--chdir=")) continue;
|
|
402
|
+
if (arg.startsWith("-")) continue;
|
|
403
|
+
return i;
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function isEnvironmentListing(argv: string[]): boolean {
|
|
409
|
+
return environmentCommandStart(argv.slice(1)) === null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function unwrapEnvironmentCommand(argv: string[]): string[] | null {
|
|
413
|
+
if (argv[0]?.split("/").at(-1) !== "env") return null;
|
|
414
|
+
const args = argv.slice(1);
|
|
415
|
+
const start = environmentCommandStart(args);
|
|
416
|
+
return start === null ? null : args.slice(start);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isSafeLiteralInspection(argv: string[]): boolean {
|
|
420
|
+
const name = argv[0]?.split("/").at(-1) ?? "";
|
|
421
|
+
return (
|
|
422
|
+
["head", "tail"].includes(name) &&
|
|
423
|
+
!argv.slice(1).some((arg) => /\.env(?:\.|$)|\.(?:pem|key)\b|secret|credential/i.test(arg))
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isSafeLiteralConsumer(argv: string[]): boolean {
|
|
428
|
+
return (
|
|
429
|
+
isSafeLiteralInspection(argv) ||
|
|
430
|
+
(argv[0]?.split("/").at(-1) === "bun" &&
|
|
431
|
+
argv[1] === "run" &&
|
|
432
|
+
argv[2] === "gh-tool" &&
|
|
433
|
+
argv.includes("--body-file"))
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
366
437
|
function unwrapStaticCommand(words: string[]): string[] {
|
|
367
438
|
const argv = [...words];
|
|
368
439
|
const executable = () => argv[0]?.split("/").at(-1);
|
|
@@ -401,6 +472,8 @@ function isPassiveTextCommand(argv: string[]): boolean {
|
|
|
401
472
|
return true;
|
|
402
473
|
}
|
|
403
474
|
if (name === "rg") return !hasExecutionOption(args, ["pre", "hostname-bin"]);
|
|
475
|
+
if (name === "awk") return !isAwkExecution(argv);
|
|
476
|
+
if (name === "jq") return isJqObjectConstruction(argv);
|
|
404
477
|
return (
|
|
405
478
|
name === "git" &&
|
|
406
479
|
args[0] === "grep" &&
|
|
@@ -411,7 +484,12 @@ function isPassiveTextCommand(argv: string[]): boolean {
|
|
|
411
484
|
function hasStaticEnvironmentRead(argv: string[], allowedNames: Set<string>): boolean {
|
|
412
485
|
const name = argv[0]?.split("/").at(-1);
|
|
413
486
|
if (name === "printenv") return !isAllowedEnvironmentRead(argv, allowedNames);
|
|
414
|
-
if (name === "env")
|
|
487
|
+
if (name === "env") {
|
|
488
|
+
const wrapped = unwrapEnvironmentCommand(argv);
|
|
489
|
+
return wrapped === null
|
|
490
|
+
? isEnvironmentListing(argv)
|
|
491
|
+
: hasStaticEnvironmentRead(unwrapStaticCommand(wrapped), allowedNames);
|
|
492
|
+
}
|
|
415
493
|
if (isPassiveTextCommand(argv)) return false;
|
|
416
494
|
// Any command may re-parse an argument as shell code (trap, find -exec, awk system()).
|
|
417
495
|
return (
|
|
@@ -419,32 +497,68 @@ function hasStaticEnvironmentRead(argv: string[], allowedNames: Set<string>): bo
|
|
|
419
497
|
);
|
|
420
498
|
}
|
|
421
499
|
|
|
500
|
+
function isStaticHerdrPrompt(command: string): boolean {
|
|
501
|
+
if (/^herdr\s+agent\s+prompt\s+\S+\s+(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')$/.test(command.trim())) {
|
|
502
|
+
if (!/[$`]/.test(command)) return true;
|
|
503
|
+
return !hasSensitiveFileRead(command) && !mentionsEnvironmentRead(command);
|
|
504
|
+
}
|
|
505
|
+
const parsed = parseStaticShellCommands(command);
|
|
506
|
+
if (
|
|
507
|
+
!parsed ||
|
|
508
|
+
typeof parsed === "string" ||
|
|
509
|
+
parsed.pipelines.length !== 1 ||
|
|
510
|
+
parsed.pipelines[0]?.length !== 1
|
|
511
|
+
) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
const argv = unwrapStaticCommand(parsed.pipelines[0]?.[0] ?? []);
|
|
515
|
+
return argv[0] === "herdr" && argv[1] === "agent" && argv[2] === "prompt";
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function isHerdrPrompt(command: string): boolean {
|
|
519
|
+
return /^herdr\s+agent\s+prompt\b/.test(command.trim());
|
|
520
|
+
}
|
|
521
|
+
|
|
422
522
|
function hasEnvironmentRead(command: string, allowedNames: Set<string>): boolean {
|
|
523
|
+
if (isStaticHerdrPrompt(command)) return false;
|
|
524
|
+
if (isHerdrPrompt(command)) return true;
|
|
423
525
|
if (isLiteralTextWrite(command)) return false;
|
|
424
526
|
// ponytail: complex shell syntax stays conservative; use a shell AST if more exceptions are needed.
|
|
425
527
|
const parsed = parseStaticShellCommands(command);
|
|
426
528
|
if (parsed === "brace-expansion") return true;
|
|
427
529
|
if (!parsed) {
|
|
428
|
-
|
|
530
|
+
const unquoted = command.replace(/'(?:\\.|[^'])*'/g, " ").replace(/"(?:\\.|[^"$`])*"/g, " ");
|
|
531
|
+
const hasExecutor =
|
|
532
|
+
/\|\s*(?:sh|bash|zsh|xargs)|\b(?:sh|bash|zsh)\b|\$\(|`|\b(?:eval|source)\b|\bfind\b.*\b-exec\b/i.test(
|
|
533
|
+
command,
|
|
534
|
+
);
|
|
535
|
+
return (
|
|
536
|
+
mentionsEnvironmentRead(unquoted) ||
|
|
537
|
+
(mentionsEnvironmentRead(command) && hasExecutor) ||
|
|
538
|
+
hasShellBraceExpansion(withoutQuotedQuantifiers(command))
|
|
539
|
+
);
|
|
429
540
|
}
|
|
430
541
|
const pipelines = parsed.pipelines.map((commands) => commands.map(unwrapStaticCommand));
|
|
431
542
|
const unwrapped = pipelines.flat();
|
|
432
|
-
if (unwrapped.some((argv) => hasStaticEnvironmentRead(argv, allowedNames)))
|
|
543
|
+
if (unwrapped.some((argv) => hasStaticEnvironmentRead(argv, allowedNames))) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
433
546
|
|
|
434
547
|
// A literal producer can feed executable text to a shell, xargs, or an unknown consumer.
|
|
435
|
-
|
|
548
|
+
const result = pipelines.some(
|
|
436
549
|
(pipeline) =>
|
|
437
550
|
pipeline.length > 1 &&
|
|
438
551
|
pipeline.some(
|
|
439
552
|
(argv) =>
|
|
440
553
|
isAllowedEnvironmentRead(argv, allowedNames) ||
|
|
441
554
|
(isLiteralTextProducer(argv) &&
|
|
442
|
-
(mentionsEnvironmentRead(argv.join(" ")) ||
|
|
555
|
+
(mentionsEnvironmentRead(argv.join(" ")) || hasCommandArgumentBraceExpansion(argv))),
|
|
443
556
|
) &&
|
|
444
557
|
pipeline.some(
|
|
445
558
|
(argv) => !isPassiveTextCommand(argv) && !isAllowedEnvironmentRead(argv, allowedNames),
|
|
446
559
|
),
|
|
447
560
|
);
|
|
561
|
+
return result;
|
|
448
562
|
}
|
|
449
563
|
|
|
450
564
|
/** A single literal writer cannot execute its body or feed another command. */
|
|
@@ -469,12 +583,73 @@ function isLiteralTextWrite(command: string): boolean {
|
|
|
469
583
|
}
|
|
470
584
|
|
|
471
585
|
// Output redirection does not execute text. Other unsupported syntax still fails parsing.
|
|
472
|
-
const parsed = parseStaticShellCommands(header.replace(
|
|
473
|
-
if (!parsed || parsed === "brace-expansion"
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
586
|
+
const parsed = parseStaticShellCommands(header.replace(/2>&1/g, " ").replace(/>>?/g, " "));
|
|
587
|
+
if (!parsed || parsed === "brace-expansion") {
|
|
588
|
+
const balancedQuotes =
|
|
589
|
+
(command.match(/'/g)?.length ?? 0) % 2 === 0 && (command.match(/"/g)?.length ?? 0) % 2 === 0;
|
|
590
|
+
return (
|
|
591
|
+
balancedQuotes &&
|
|
592
|
+
/^(?:cd\s+\S+\s+&&\s+)?(?:echo|printf)\b[\s\S]*>>?\s+\S+\s*(?:&&\s+tail\b.*)?$/i.test(command)
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
const commands = parsed.pipelines.flat().map(unwrapStaticCommand);
|
|
596
|
+
const writerIndexes = commands
|
|
597
|
+
.map((argv, index) => (writers.includes(argv[0]?.split("/").at(-1) ?? "") ? index : -1))
|
|
598
|
+
.filter((index) => index >= 0);
|
|
599
|
+
return (
|
|
600
|
+
writerIndexes.length === 1 &&
|
|
601
|
+
commands.every((argv, index) => {
|
|
602
|
+
if (index === writerIndexes[0]) return true;
|
|
603
|
+
const name = argv[0]?.split("/").at(-1) ?? "";
|
|
604
|
+
return name === "cd" || isSafeLiteralConsumer(argv);
|
|
605
|
+
})
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function hasSensitiveFileRead(command: string): boolean {
|
|
610
|
+
const sensitivePath =
|
|
611
|
+
/(?:^|[/\s'"])(?:[.]env(?\b)|\S*[.](?:pem|key)\b|\S*[.](?:ssh|aws)[/]|(?:\S*secret(?:-|[/\s.]|$))|(?:(?:secrets?|credentials?)(?:[/\s.]|$)|[A-Za-z0-9_.-]*(?:secrets?|credentials?)(?:[/\s.]|$)|[A-Za-z0-9_.-]*-(?:secrets?|credentials?)(?:[/\s.-]|$)))/i;
|
|
612
|
+
const sensitiveCamelCase =
|
|
613
|
+
/(?:^|[/\s'"])[A-Za-z0-9_.-]*(?:(?:secret|credential)[A-Z]|(?:Secret|Credential)[A-Z])[^/\s'"]*/;
|
|
614
|
+
const sensitiveName = /secret|credential/i;
|
|
615
|
+
const isSensitivePath = (text: string) =>
|
|
616
|
+
sensitivePath.test(text) || sensitiveCamelCase.test(text) || sensitiveName.test(text);
|
|
617
|
+
if (/(?:^|[\s;&|])--env-file(?:=|\s)[^;&|]*\.env\b/i.test(command)) return true;
|
|
618
|
+
if (/\bcat\b[^;&|]*<<-?\s*\S+[\s\S]*\b(?:secret|credential)\b/i.test(command)) return true;
|
|
619
|
+
const parsed = parseStaticShellCommands(command);
|
|
620
|
+
const readers = /\b(?:cat|cp|grep|rg|sed|awk|head|tail|less|more|source|ls)\b/i;
|
|
621
|
+
if (parsed && parsed !== "brace-expansion") {
|
|
622
|
+
return parsed.pipelines.flat().some((words) => {
|
|
623
|
+
const argv = unwrapStaticCommand(words);
|
|
624
|
+
const inspectedArgv =
|
|
625
|
+
argv[0]?.split("/").at(-1) === "env" ? unwrapEnvironmentCommand(argv) : argv;
|
|
626
|
+
if (inspectedArgv === null) return false;
|
|
627
|
+
const inspected = unwrapStaticCommand(inspectedArgv);
|
|
628
|
+
const name = inspected[0]?.split("/").at(-1) ?? "";
|
|
629
|
+
return readers.test(name) && inspected.slice(1).some(isSensitivePath);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
const unquoted = command.replace(/'(?:\\.|[^'])*'/g, " ").replace(/"(?:\\.|[^"$`])*"/g, " ");
|
|
633
|
+
if (/\b(?:bun|node|python3?|ruby)\b/i.test(command) && isSensitivePath(command)) {
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
return (
|
|
637
|
+
new RegExp(readers.source + "[^;&|]*" + sensitivePath.source, "i").test(unquoted) ||
|
|
638
|
+
new RegExp(readers.source + "[^;&|]*" + sensitiveCamelCase.source).test(unquoted) ||
|
|
639
|
+
new RegExp(readers.source + "[^;&|]*" + sensitiveName.source, "i").test(unquoted)
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function hasEnvironmentVariableExpansionRead(command: string): boolean {
|
|
644
|
+
if (!/\$\{?[A-Za-z_][A-Za-z0-9_]*\}?/.test(command)) return false;
|
|
645
|
+
return /\b(?:echo|printf)\b[^;&|]*\$|\b(?:sh|bash|zsh)\b[^;&|]*\s-c\b/i.test(command);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function hasEnvironmentSourceAccess(command: string): boolean {
|
|
649
|
+
return (
|
|
650
|
+
/\b(?:os\.environ|process\.env|Environment\.GetEnvironmentVariable)\b/.test(command) &&
|
|
651
|
+
/\b(?:python3?|node|bun)\b/.test(command)
|
|
652
|
+
);
|
|
478
653
|
}
|
|
479
654
|
|
|
480
655
|
/** Extract file path from hook arguments. */
|
|
@@ -527,8 +702,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
527
702
|
];
|
|
528
703
|
|
|
529
704
|
const dangerousBashPatterns = [
|
|
530
|
-
...
|
|
531
|
-
...(config?.additionalDangerousBashPatterns ?? []).map((p) => new RegExp(p)),
|
|
705
|
+
...(config?.additionalDangerousBashPatterns?.map((p) => new RegExp(p)) ?? []),
|
|
532
706
|
];
|
|
533
707
|
|
|
534
708
|
const blockedCliTools: BlockedCliTool[] = [
|
|
@@ -579,6 +753,9 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
579
753
|
function isDangerousBashCommand(command: string): boolean {
|
|
580
754
|
return (
|
|
581
755
|
dangerousBashPatterns.some((pattern) => pattern.test(command)) ||
|
|
756
|
+
hasSensitiveFileRead(command) ||
|
|
757
|
+
hasEnvironmentVariableExpansionRead(command) ||
|
|
758
|
+
hasEnvironmentSourceAccess(command) ||
|
|
582
759
|
hasEnvironmentRead(command, allowedEnvironmentVariables)
|
|
583
760
|
);
|
|
584
761
|
}
|
|
@@ -632,7 +809,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
632
809
|
|
|
633
810
|
function handleToolExecuteBefore(input: HookInput, output: HookOutput): void {
|
|
634
811
|
// Normalize tool name across platforms:
|
|
635
|
-
// -
|
|
812
|
+
// - AI coding agents pass capitalized: "Bash", "Read", "Write", "Edit"
|
|
636
813
|
// - OpenCode MCP tools pass prefixed: "mcp_bash", "mcp_read", "mcp_write", "mcp_edit"
|
|
637
814
|
const tool = input.tool.toLowerCase().replace(/^mcp_/, "");
|
|
638
815
|
const args = output.args;
|
|
@@ -645,7 +822,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
645
822
|
`\u{1F6AB} Access blocked: "${filePath}" is a sensitive file.\n\n` +
|
|
646
823
|
`This file may contain credentials or secrets.\n` +
|
|
647
824
|
`If you need this file's content, ask the user to provide relevant parts.\n\n` +
|
|
648
|
-
`Think this should be allowed?
|
|
825
|
+
`Think this should be allowed? Review the project repository, extend the guard, and submit a PR.`,
|
|
649
826
|
);
|
|
650
827
|
}
|
|
651
828
|
}
|
|
@@ -661,7 +838,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
661
838
|
`\u{1F6AB} Secret detected: Potential ${detected.name} found in content.\n\n` +
|
|
662
839
|
`Matched: ${detected.match}\n\n` +
|
|
663
840
|
`Never commit secrets to code. Use environment variables or secret managers.\n\n` +
|
|
664
|
-
`Think this is a false positive?
|
|
841
|
+
`Think this is a false positive? Review the project repository, fix the pattern, and submit a PR.`,
|
|
665
842
|
);
|
|
666
843
|
}
|
|
667
844
|
}
|
|
@@ -676,7 +853,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
676
853
|
`\u{1F6AB} Command blocked: This command might expose secrets.\n\n` +
|
|
677
854
|
`Command: ${command}\n\n` +
|
|
678
855
|
`If you need environment info, ask the user directly.\n\n` +
|
|
679
|
-
`Think this is wrong?
|
|
856
|
+
`Think this is wrong? Review the project repository, adjust the patterns, and submit a PR.`,
|
|
680
857
|
);
|
|
681
858
|
}
|
|
682
859
|
|
|
@@ -698,7 +875,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
698
875
|
`AI agents must use wrapper tools for security and audit.\n\n` +
|
|
699
876
|
`Use instead: bun ${skillName}\n\n` +
|
|
700
877
|
`Example: bun ${skillName} --help\n\n` +
|
|
701
|
-
`Think this tool should be allowed?
|
|
878
|
+
`Think this tool should be allowed? Review the project repository, extend the whitelist, and submit a PR.\n` +
|
|
702
879
|
`→ Skill "${skillName}"`,
|
|
703
880
|
);
|
|
704
881
|
}
|