@node9/proxy 1.11.0 → 1.11.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.
package/dist/index.js CHANGED
@@ -503,9 +503,8 @@ var DEFAULT_CONFIG = {
503
503
  {
504
504
  field: "command",
505
505
  op: "matches",
506
- // Require the recursive flag to be preceded by whitespace so that
507
- // filenames containing "-r" (e.g. "ai-review.yml") don't false-positive.
508
- value: "rm\\b.*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
506
+ // Anchor rm as a shell command (not inside a string arg like a git commit message).
507
+ value: "(^|&&|\\|\\||;)\\s*rm\\b[^;&|]*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
509
508
  },
510
509
  {
511
510
  field: "command",
@@ -534,6 +533,13 @@ var DEFAULT_CONFIG = {
534
533
  name: "review-drop-truncate-shell",
535
534
  tool: "bash",
536
535
  conditions: [
536
+ {
537
+ field: "command",
538
+ op: "matches",
539
+ // Require a DB CLI in the command so grep/cat/echo of SQL strings don't trigger.
540
+ value: "(^|&&|\\|\\||;|\\|)\\s*(psql|mysql|sqlite3|sqlplus|cockroach|clickhouse-client|mongo)\\b",
541
+ flags: "i"
542
+ },
537
543
  {
538
544
  field: "command",
539
545
  op: "matches",
@@ -554,7 +560,9 @@ var DEFAULT_CONFIG = {
554
560
  {
555
561
  field: "command",
556
562
  op: "matches",
557
- value: "\\bgit\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
563
+ // Anchor git as a shell command so node -e / python -c scripts containing
564
+ // "git push --force" as a string don't false-positive.
565
+ value: "(^|&&|\\|\\||;)\\s*git\\s+push[^;&|]*(--force|--force-with-lease|-f\\b)",
558
566
  flags: "i"
559
567
  }
560
568
  ],
@@ -564,29 +572,20 @@ var DEFAULT_CONFIG = {
564
572
  description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
565
573
  },
566
574
  {
567
- name: "review-git-push",
575
+ name: "review-git-destructive",
568
576
  tool: "bash",
569
577
  conditions: [
570
578
  {
571
579
  field: "command",
572
580
  op: "matches",
573
- value: "\\bgit\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
581
+ value: "\\bgit\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
574
582
  flags: "i"
575
- }
576
- ],
577
- conditionMode: "all",
578
- verdict: "review",
579
- reason: "git push sends changes to a shared remote",
580
- description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
581
- },
582
- {
583
- name: "review-git-destructive",
584
- tool: "bash",
585
- conditions: [
583
+ },
586
584
  {
587
585
  field: "command",
588
- op: "matches",
589
- value: "\\bgit\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
586
+ op: "notMatches",
587
+ // Exclude recovery ops — these resolve a conflict, not start a destructive action.
588
+ value: "\\bgit\\s+rebase\\s+--(abort|continue|skip)\\b",
590
589
  flags: "i"
591
590
  }
592
591
  ],
@@ -612,7 +611,9 @@ var DEFAULT_CONFIG = {
612
611
  {
613
612
  field: "command",
614
613
  op: "matches",
615
- value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
614
+ // Anchor curl/wget as a shell command so node -e scripts testing this
615
+ // regex pattern don't self-match as a false positive.
616
+ value: "(^|&&|\\|\\||;)\\s*(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
616
617
  flags: "i"
617
618
  }
618
619
  ],
@@ -1013,7 +1014,7 @@ var DLP_PATTERNS = [
1013
1014
  regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
1014
1015
  severity: "block"
1015
1016
  },
1016
- { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
1017
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i, severity: "review" }
1017
1018
  ];
1018
1019
  var SENSITIVE_PATH_PATTERNS = [
1019
1020
  /[/\\]\.ssh[/\\]/i,
@@ -1603,12 +1604,25 @@ function getNestedValue(obj, path15) {
1603
1604
  if (!obj || typeof obj !== "object") return null;
1604
1605
  return path15.split(".").reduce((prev, curr) => prev?.[curr], obj);
1605
1606
  }
1607
+ function stripStringArguments(cmd) {
1608
+ let result = cmd;
1609
+ result = result.replace(
1610
+ /\b(node|python3?|ruby|perl|php|deno)\s+(-[ecr]|eval)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/gi,
1611
+ '$1 $2 ""'
1612
+ );
1613
+ result = result.replace(
1614
+ /\s(-m|--message|--body|--title|--description)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
1615
+ ' $1 ""'
1616
+ );
1617
+ return result;
1618
+ }
1606
1619
  function evaluateSmartConditions(args, rule) {
1607
1620
  if (!rule.conditions || rule.conditions.length === 0) return true;
1608
1621
  const mode = rule.conditionMode ?? "all";
1609
1622
  const results = rule.conditions.map((cond) => {
1610
1623
  const rawVal = getNestedValue(args, cond.field);
1611
- const val = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1624
+ const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1625
+ const val = cond.field === "command" && normalized !== null ? stripStringArguments(normalized) : normalized;
1612
1626
  switch (cond.op) {
1613
1627
  case "exists":
1614
1628
  return val !== null && val !== "";
@@ -2413,7 +2427,8 @@ function escapePango(text) {
2413
2427
  function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
2414
2428
  const lines = [];
2415
2429
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2416
- lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2430
+ const safeAgent = (agent ?? "AI Agent").replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80);
2431
+ lines.push(`\u{1F916} ${safeAgent} | \u{1F527} ${toolName}`);
2417
2432
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2418
2433
  if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
2419
2434
  lines.push("");
@@ -2790,7 +2805,16 @@ async function authorizeHeadless(toolName, args, meta, options) {
2790
2805
  if (!options?.calledFromDaemon) {
2791
2806
  const actId = (0, import_crypto3.randomUUID)();
2792
2807
  const actTs = Date.now();
2793
- await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
2808
+ await notifyActivity({
2809
+ id: actId,
2810
+ ts: actTs,
2811
+ tool: toolName,
2812
+ args,
2813
+ status: "pending",
2814
+ // Strip ANSI escape sequences — agent name comes from caller-supplied metadata
2815
+ // and may be displayed in a terminal (node9 tail/watch), enabling injection.
2816
+ agent: meta?.agent ? meta.agent.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80) : void 0
2817
+ });
2794
2818
  const result = await _authorizeHeadlessCore(toolName, args, meta, {
2795
2819
  ...options,
2796
2820
  activityId: actId
package/dist/index.mjs CHANGED
@@ -473,9 +473,8 @@ var DEFAULT_CONFIG = {
473
473
  {
474
474
  field: "command",
475
475
  op: "matches",
476
- // Require the recursive flag to be preceded by whitespace so that
477
- // filenames containing "-r" (e.g. "ai-review.yml") don't false-positive.
478
- value: "rm\\b.*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
476
+ // Anchor rm as a shell command (not inside a string arg like a git commit message).
477
+ value: "(^|&&|\\|\\||;)\\s*rm\\b[^;&|]*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
479
478
  },
480
479
  {
481
480
  field: "command",
@@ -504,6 +503,13 @@ var DEFAULT_CONFIG = {
504
503
  name: "review-drop-truncate-shell",
505
504
  tool: "bash",
506
505
  conditions: [
506
+ {
507
+ field: "command",
508
+ op: "matches",
509
+ // Require a DB CLI in the command so grep/cat/echo of SQL strings don't trigger.
510
+ value: "(^|&&|\\|\\||;|\\|)\\s*(psql|mysql|sqlite3|sqlplus|cockroach|clickhouse-client|mongo)\\b",
511
+ flags: "i"
512
+ },
507
513
  {
508
514
  field: "command",
509
515
  op: "matches",
@@ -524,7 +530,9 @@ var DEFAULT_CONFIG = {
524
530
  {
525
531
  field: "command",
526
532
  op: "matches",
527
- value: "\\bgit\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
533
+ // Anchor git as a shell command so node -e / python -c scripts containing
534
+ // "git push --force" as a string don't false-positive.
535
+ value: "(^|&&|\\|\\||;)\\s*git\\s+push[^;&|]*(--force|--force-with-lease|-f\\b)",
528
536
  flags: "i"
529
537
  }
530
538
  ],
@@ -534,29 +542,20 @@ var DEFAULT_CONFIG = {
534
542
  description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
535
543
  },
536
544
  {
537
- name: "review-git-push",
545
+ name: "review-git-destructive",
538
546
  tool: "bash",
539
547
  conditions: [
540
548
  {
541
549
  field: "command",
542
550
  op: "matches",
543
- value: "\\bgit\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
551
+ value: "\\bgit\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
544
552
  flags: "i"
545
- }
546
- ],
547
- conditionMode: "all",
548
- verdict: "review",
549
- reason: "git push sends changes to a shared remote",
550
- description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
551
- },
552
- {
553
- name: "review-git-destructive",
554
- tool: "bash",
555
- conditions: [
553
+ },
556
554
  {
557
555
  field: "command",
558
- op: "matches",
559
- value: "\\bgit\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
556
+ op: "notMatches",
557
+ // Exclude recovery ops — these resolve a conflict, not start a destructive action.
558
+ value: "\\bgit\\s+rebase\\s+--(abort|continue|skip)\\b",
560
559
  flags: "i"
561
560
  }
562
561
  ],
@@ -582,7 +581,9 @@ var DEFAULT_CONFIG = {
582
581
  {
583
582
  field: "command",
584
583
  op: "matches",
585
- value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
584
+ // Anchor curl/wget as a shell command so node -e scripts testing this
585
+ // regex pattern don't self-match as a false positive.
586
+ value: "(^|&&|\\|\\||;)\\s*(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
586
587
  flags: "i"
587
588
  }
588
589
  ],
@@ -983,7 +984,7 @@ var DLP_PATTERNS = [
983
984
  regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
984
985
  severity: "block"
985
986
  },
986
- { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
987
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i, severity: "review" }
987
988
  ];
988
989
  var SENSITIVE_PATH_PATTERNS = [
989
990
  /[/\\]\.ssh[/\\]/i,
@@ -1573,12 +1574,25 @@ function getNestedValue(obj, path15) {
1573
1574
  if (!obj || typeof obj !== "object") return null;
1574
1575
  return path15.split(".").reduce((prev, curr) => prev?.[curr], obj);
1575
1576
  }
1577
+ function stripStringArguments(cmd) {
1578
+ let result = cmd;
1579
+ result = result.replace(
1580
+ /\b(node|python3?|ruby|perl|php|deno)\s+(-[ecr]|eval)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/gi,
1581
+ '$1 $2 ""'
1582
+ );
1583
+ result = result.replace(
1584
+ /\s(-m|--message|--body|--title|--description)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
1585
+ ' $1 ""'
1586
+ );
1587
+ return result;
1588
+ }
1576
1589
  function evaluateSmartConditions(args, rule) {
1577
1590
  if (!rule.conditions || rule.conditions.length === 0) return true;
1578
1591
  const mode = rule.conditionMode ?? "all";
1579
1592
  const results = rule.conditions.map((cond) => {
1580
1593
  const rawVal = getNestedValue(args, cond.field);
1581
- const val = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1594
+ const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1595
+ const val = cond.field === "command" && normalized !== null ? stripStringArguments(normalized) : normalized;
1582
1596
  switch (cond.op) {
1583
1597
  case "exists":
1584
1598
  return val !== null && val !== "";
@@ -2383,7 +2397,8 @@ function escapePango(text) {
2383
2397
  function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
2384
2398
  const lines = [];
2385
2399
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2386
- lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2400
+ const safeAgent = (agent ?? "AI Agent").replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80);
2401
+ lines.push(`\u{1F916} ${safeAgent} | \u{1F527} ${toolName}`);
2387
2402
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2388
2403
  if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
2389
2404
  lines.push("");
@@ -2760,7 +2775,16 @@ async function authorizeHeadless(toolName, args, meta, options) {
2760
2775
  if (!options?.calledFromDaemon) {
2761
2776
  const actId = randomUUID();
2762
2777
  const actTs = Date.now();
2763
- await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
2778
+ await notifyActivity({
2779
+ id: actId,
2780
+ ts: actTs,
2781
+ tool: toolName,
2782
+ args,
2783
+ status: "pending",
2784
+ // Strip ANSI escape sequences — agent name comes from caller-supplied metadata
2785
+ // and may be displayed in a terminal (node9 tail/watch), enabling injection.
2786
+ agent: meta?.agent ? meta.agent.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80) : void 0
2787
+ });
2764
2788
  const result = await _authorizeHeadlessCore(toolName, args, meta, {
2765
2789
  ...options,
2766
2790
  activityId: actId
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "field": "command",
12
12
  "op": "matches",
13
- "value": "(curl|wget)\\s+[^|]*\\|\\s*(?:(bash|sh|zsh|fish)|(python3?|ruby|perl|node)\\b(?!\\s+-[cem]\\b))",
13
+ "value": "(^|&&|\\|\\||;)\\s*(curl|wget)\\s+[^|]*\\|\\s*(?:(bash|sh|zsh|fish)|(python3?|ruby|perl|node)\\b(?!\\s+-[cem]\\b))",
14
14
  "flags": "i"
15
15
  }
16
16
  ],
@@ -24,7 +24,7 @@
24
24
  {
25
25
  "field": "command",
26
26
  "op": "matches",
27
- "value": "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
27
+ "value": "\\bbase64\\s+(-d|--decode)[^|;&]*\\|\\s*(bash|sh|zsh)",
28
28
  "flags": "i"
29
29
  }
30
30
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",