@node9/proxy 2.23.1 → 2.23.3

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.
Files changed (3) hide show
  1. package/dist/cli.js +46 -15
  2. package/dist/cli.mjs +46 -15
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -60475,6 +60475,11 @@ function jobActorGate(job, wf, raw) {
60475
60475
  return ifsAreGated(ifs, labelTypeConfigured(wf, raw)) || /assignee\.login\s*==|event\.assignee\b/i.test(ifs) || PERMISSION_OUTPUT_GATE_RE.test(ifs) || // [2] job-scoped permission-check-output gate
60476
60476
  hasStepMembershipGate(job);
60477
60477
  }
60478
+ function bypassArmed(steps) {
60479
+ return steps.some(
60480
+ (s) => str(s.with?.["allowed_non_write_users"]) === "*" && (!!str(s.with?.["github_token"]) || !!str(s.env?.["OVERRIDE_GITHUB_TOKEN"]))
60481
+ );
60482
+ }
60478
60483
  function hasImplicitActorGate(agentSteps, bypassActive) {
60479
60484
  if (bypassActive) return false;
60480
60485
  return agentSteps.some((s) => /anthropics\/claude-code-action@/i.test(s.uses ?? ""));
@@ -60484,18 +60489,22 @@ function injectableJobs(wf, raw, untrustedTrigger) {
60484
60489
  for (const job of jobList(wf)) {
60485
60490
  const a = (job.steps ?? []).filter(isAgentStep);
60486
60491
  if (!a.length) continue;
60487
- const jobStar = str(a.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean)) === "*";
60492
+ const jobStar = bypassArmed(a);
60488
60493
  if (jobActorGate(job, wf, raw) || hasImplicitActorGate(a, jobStar && untrustedTrigger))
60489
60494
  continue;
60490
60495
  out.push(job);
60491
60496
  }
60492
60497
  return out;
60493
60498
  }
60499
+ var DEAD_SECRET_TAIL_RE = /(?<=(?:\$\{\{|\(|,)\s*)(secrets\.GITHUB_TOKEN|github\.token)(?:\s*\|\|\s*(?:'[^']*'|[A-Za-z_][A-Za-z0-9_.]*))+(?=\s*(?:\}\}|\)|,))/gi;
60500
+ function dropDeadSecretRefs(text) {
60501
+ return text.replace(DEAD_SECRET_TAIL_RE, "$1");
60502
+ }
60494
60503
  function usesPatIn(jobs) {
60495
60504
  for (const j of jobs)
60496
60505
  for (const step of j.steps ?? []) {
60497
- const gt = str(step.with?.["github_token"]);
60498
- if (/secrets\./i.test(gt) && !/secrets\.GITHUB_TOKEN/i.test(gt)) return true;
60506
+ const gt = dropDeadSecretRefs(str(step.with?.["github_token"]));
60507
+ if (/secrets\.(?!GITHUB_TOKEN\b)[A-Za-z_]/i.test(gt)) return true;
60499
60508
  }
60500
60509
  return false;
60501
60510
  }
@@ -60564,7 +60573,8 @@ function analyzeWorkflow(path80, content) {
60564
60573
  privileged
60565
60574
  } = triggerReach(wf, raw);
60566
60575
  const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
60567
- const nonWriteStar = nonWrite === "*";
60576
+ const nonWriteStar = bypassArmed(agentSteps);
60577
+ const starWithoutToken = nonWrite === "*" && !nonWriteStar;
60568
60578
  const nonWriteList = !!nonWrite && nonWrite !== "*";
60569
60579
  const untrustedTrigger = forkInput || reusable;
60570
60580
  const bypassActive = nonWriteStar && untrustedTrigger;
@@ -60642,7 +60652,8 @@ function analyzeWorkflow(path80, content) {
60642
60652
  names.length ? `agent has broad/write-capable tools: ${names.map((n) => `\`${n}\``).join(", ")}` : "agent has broad/write-capable tool grants"
60643
60653
  );
60644
60654
  }
60645
- if (bypassActive) signals.push('allowed_non_write_users: "*" \u2014 any user can trigger the agent');
60655
+ if (bypassActive)
60656
+ signals.push('allowed_non_write_users: "*" with github_token \u2014 any user can trigger the agent');
60646
60657
  if (elevated) signals.push("elevated permissions (contents/id-token: write)");
60647
60658
  if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
60648
60659
  if (!gate && reach > 0) signals.push("no effective actor gate");
@@ -60660,6 +60671,10 @@ function analyzeWorkflow(path80, content) {
60660
60671
  if (envDeny) mitigations.push("secrets env-denied from the agent subprocess");
60661
60672
  if (pinned) mitigations.push("agent action pinned to a commit SHA");
60662
60673
  if (nonWriteList) mitigations.push('non-write users scoped to a list, not "*"');
60674
+ if (starWithoutToken)
60675
+ mitigations.push(
60676
+ 'allowed_non_write_users: "*" is set without github_token, so the action keeps its default write gate'
60677
+ );
60663
60678
  const title = severity === "critical" || severity === "high" ? "Injectable agent workflow \u2014 untrusted input reaches a tool-using agent with secrets" : severity === "medium" ? "Agent workflow with a risky pattern (partially mitigated)" : "Agent workflow on a privileged trigger \u2014 review the actor gate";
60664
60679
  return {
60665
60680
  check: "CI-2",
@@ -60679,7 +60694,8 @@ var FUEL_INPUT_KEYS = /^(anthropic_api_key|anthropic_auth_token|anthropic_base_u
60679
60694
  function fuelSecretNames(agentSteps) {
60680
60695
  const out = /* @__PURE__ */ new Set();
60681
60696
  const add = (v) => {
60682
- for (const m of str(v).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) out.add(m[1]);
60697
+ for (const m of dropDeadSecretRefs(str(v)).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi))
60698
+ out.add(m[1]);
60683
60699
  };
60684
60700
  for (const st of agentSteps) {
60685
60701
  for (const [k, v] of Object.entries(st.with ?? {})) if (FUEL_INPUT_KEYS.test(k)) add(v);
@@ -60701,7 +60717,7 @@ function agentReachableSecrets(wf, agentSteps, agentJobs) {
60701
60717
  blobs.push(str(wf.env));
60702
60718
  const fuel = fuelSecretNames(agentSteps);
60703
60719
  const found = /* @__PURE__ */ new Map();
60704
- for (const b of blobs) {
60720
+ for (const b of blobs.map(dropDeadSecretRefs)) {
60705
60721
  for (const m of b.matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) {
60706
60722
  const name = m[1];
60707
60723
  if (AGENT_FUEL_RE.test(name) || fuel.has(name)) continue;
@@ -60719,8 +60735,7 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
60719
60735
  if (jobAgentSteps.length === 0) return null;
60720
60736
  const secrets = agentReachableSecrets(wf, jobAgentSteps, [job]);
60721
60737
  if (secrets.length === 0) return null;
60722
- const nonWriteStar = str(jobAgentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean)) === "*";
60723
- const bypassActive = nonWriteStar && untrustedTrigger;
60738
+ const bypassActive = bypassArmed(jobAgentSteps) && untrustedTrigger;
60724
60739
  const head = untrustedHeadCheckout(jobSteps);
60725
60740
  const reach = Math.max(
60726
60741
  head === "root" ? 3 : head === "subdir" ? 1 : 0,
@@ -60830,19 +60845,35 @@ function analyzeAgentConfig(path80, content) {
60830
60845
  );
60831
60846
  if (broad.length > 0) {
60832
60847
  const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
60848
+ const bashBackstop = deny.some((d) => /^Bash\b/.test(d));
60849
+ const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(a));
60850
+ const high = bareShell && !bashBackstop;
60851
+ const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
60852
+ if (high)
60853
+ signals.push(
60854
+ "unrestricted `Bash` with no `deny` backstop \u2014 any command an injected instruction names runs without a prompt, for everyone who opens this repo with the agent"
60855
+ );
60856
+ else if (bareShell)
60857
+ signals.push(
60858
+ "a `Bash` deny list narrows the unrestricted `Bash` allow; it blocks only the commands it names"
60859
+ );
60860
+ if (broad.some((a) => /^Bash\(git:/.test(a)))
60861
+ signals.push(
60862
+ "`Bash(git:*)` pre-approves every git command; git can run other programs (`-c core.pager=\u2026`, `!` aliases), so this is broader than it looks"
60863
+ );
60864
+ if (broad.some((a) => /^Write|^Edit$/.test(a)))
60865
+ signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
60866
+ if (!high && !bareShell && !hasBackstop) signals.push("no `deny` entry narrows these grants");
60833
60867
  findings.push({
60834
60868
  check: "CI-1",
60835
60869
  rule: "CI-1.broad-allow",
60836
60870
  // File-level: one finding per config file. Adding a SECOND broad allow makes the
60837
60871
  // same statement about the same file, so it must not read as a new finding.
60838
60872
  dimension: "toolRules",
60839
- severity: hasBackstop ? "medium" : "high",
60840
- title: hasBackstop ? "Committed agent config pre-authorizes broad tools" : "Committed agent config pre-authorizes broad tools with no deny backstop",
60873
+ severity: high ? "high" : "medium",
60874
+ title: high ? "Committed agent config pre-authorizes broad tools with no deny backstop" : "Committed agent config pre-authorizes broad tools",
60841
60875
  file: path80,
60842
- signals: [
60843
- `broad allow(s): ${broad.slice(0, 5).join(", ")}`,
60844
- hasBackstop ? "a `deny` list backstops the broad allow" : "no `deny` entry covers Bash/Write/Edit \u2014 every contributor is pre-authorized for catastrophic tools"
60845
- ],
60876
+ signals,
60846
60877
  fix: "Scope the allow-list to specific read-only subcommands (e.g. `Bash(gh pr view:*)`); avoid bare `Bash`/`git:`/`Write`, or add a `deny` backstop."
60847
60878
  });
60848
60879
  }
package/dist/cli.mjs CHANGED
@@ -60464,6 +60464,11 @@ function jobActorGate(job, wf, raw) {
60464
60464
  return ifsAreGated(ifs, labelTypeConfigured(wf, raw)) || /assignee\.login\s*==|event\.assignee\b/i.test(ifs) || PERMISSION_OUTPUT_GATE_RE.test(ifs) || // [2] job-scoped permission-check-output gate
60465
60465
  hasStepMembershipGate(job);
60466
60466
  }
60467
+ function bypassArmed(steps) {
60468
+ return steps.some(
60469
+ (s) => str(s.with?.["allowed_non_write_users"]) === "*" && (!!str(s.with?.["github_token"]) || !!str(s.env?.["OVERRIDE_GITHUB_TOKEN"]))
60470
+ );
60471
+ }
60467
60472
  function hasImplicitActorGate(agentSteps, bypassActive) {
60468
60473
  if (bypassActive) return false;
60469
60474
  return agentSteps.some((s) => /anthropics\/claude-code-action@/i.test(s.uses ?? ""));
@@ -60473,18 +60478,22 @@ function injectableJobs(wf, raw, untrustedTrigger) {
60473
60478
  for (const job of jobList(wf)) {
60474
60479
  const a = (job.steps ?? []).filter(isAgentStep);
60475
60480
  if (!a.length) continue;
60476
- const jobStar = str(a.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean)) === "*";
60481
+ const jobStar = bypassArmed(a);
60477
60482
  if (jobActorGate(job, wf, raw) || hasImplicitActorGate(a, jobStar && untrustedTrigger))
60478
60483
  continue;
60479
60484
  out.push(job);
60480
60485
  }
60481
60486
  return out;
60482
60487
  }
60488
+ var DEAD_SECRET_TAIL_RE = /(?<=(?:\$\{\{|\(|,)\s*)(secrets\.GITHUB_TOKEN|github\.token)(?:\s*\|\|\s*(?:'[^']*'|[A-Za-z_][A-Za-z0-9_.]*))+(?=\s*(?:\}\}|\)|,))/gi;
60489
+ function dropDeadSecretRefs(text) {
60490
+ return text.replace(DEAD_SECRET_TAIL_RE, "$1");
60491
+ }
60483
60492
  function usesPatIn(jobs) {
60484
60493
  for (const j of jobs)
60485
60494
  for (const step of j.steps ?? []) {
60486
- const gt = str(step.with?.["github_token"]);
60487
- if (/secrets\./i.test(gt) && !/secrets\.GITHUB_TOKEN/i.test(gt)) return true;
60495
+ const gt = dropDeadSecretRefs(str(step.with?.["github_token"]));
60496
+ if (/secrets\.(?!GITHUB_TOKEN\b)[A-Za-z_]/i.test(gt)) return true;
60488
60497
  }
60489
60498
  return false;
60490
60499
  }
@@ -60553,7 +60562,8 @@ function analyzeWorkflow(path80, content) {
60553
60562
  privileged
60554
60563
  } = triggerReach(wf, raw);
60555
60564
  const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
60556
- const nonWriteStar = nonWrite === "*";
60565
+ const nonWriteStar = bypassArmed(agentSteps);
60566
+ const starWithoutToken = nonWrite === "*" && !nonWriteStar;
60557
60567
  const nonWriteList = !!nonWrite && nonWrite !== "*";
60558
60568
  const untrustedTrigger = forkInput || reusable;
60559
60569
  const bypassActive = nonWriteStar && untrustedTrigger;
@@ -60631,7 +60641,8 @@ function analyzeWorkflow(path80, content) {
60631
60641
  names.length ? `agent has broad/write-capable tools: ${names.map((n) => `\`${n}\``).join(", ")}` : "agent has broad/write-capable tool grants"
60632
60642
  );
60633
60643
  }
60634
- if (bypassActive) signals.push('allowed_non_write_users: "*" \u2014 any user can trigger the agent');
60644
+ if (bypassActive)
60645
+ signals.push('allowed_non_write_users: "*" with github_token \u2014 any user can trigger the agent');
60635
60646
  if (elevated) signals.push("elevated permissions (contents/id-token: write)");
60636
60647
  if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
60637
60648
  if (!gate && reach > 0) signals.push("no effective actor gate");
@@ -60649,6 +60660,10 @@ function analyzeWorkflow(path80, content) {
60649
60660
  if (envDeny) mitigations.push("secrets env-denied from the agent subprocess");
60650
60661
  if (pinned) mitigations.push("agent action pinned to a commit SHA");
60651
60662
  if (nonWriteList) mitigations.push('non-write users scoped to a list, not "*"');
60663
+ if (starWithoutToken)
60664
+ mitigations.push(
60665
+ 'allowed_non_write_users: "*" is set without github_token, so the action keeps its default write gate'
60666
+ );
60652
60667
  const title = severity === "critical" || severity === "high" ? "Injectable agent workflow \u2014 untrusted input reaches a tool-using agent with secrets" : severity === "medium" ? "Agent workflow with a risky pattern (partially mitigated)" : "Agent workflow on a privileged trigger \u2014 review the actor gate";
60653
60668
  return {
60654
60669
  check: "CI-2",
@@ -60668,7 +60683,8 @@ var FUEL_INPUT_KEYS = /^(anthropic_api_key|anthropic_auth_token|anthropic_base_u
60668
60683
  function fuelSecretNames(agentSteps) {
60669
60684
  const out = /* @__PURE__ */ new Set();
60670
60685
  const add = (v) => {
60671
- for (const m of str(v).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) out.add(m[1]);
60686
+ for (const m of dropDeadSecretRefs(str(v)).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi))
60687
+ out.add(m[1]);
60672
60688
  };
60673
60689
  for (const st of agentSteps) {
60674
60690
  for (const [k, v] of Object.entries(st.with ?? {})) if (FUEL_INPUT_KEYS.test(k)) add(v);
@@ -60690,7 +60706,7 @@ function agentReachableSecrets(wf, agentSteps, agentJobs) {
60690
60706
  blobs.push(str(wf.env));
60691
60707
  const fuel = fuelSecretNames(agentSteps);
60692
60708
  const found = /* @__PURE__ */ new Map();
60693
- for (const b of blobs) {
60709
+ for (const b of blobs.map(dropDeadSecretRefs)) {
60694
60710
  for (const m of b.matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) {
60695
60711
  const name = m[1];
60696
60712
  if (AGENT_FUEL_RE.test(name) || fuel.has(name)) continue;
@@ -60708,8 +60724,7 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
60708
60724
  if (jobAgentSteps.length === 0) return null;
60709
60725
  const secrets = agentReachableSecrets(wf, jobAgentSteps, [job]);
60710
60726
  if (secrets.length === 0) return null;
60711
- const nonWriteStar = str(jobAgentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean)) === "*";
60712
- const bypassActive = nonWriteStar && untrustedTrigger;
60727
+ const bypassActive = bypassArmed(jobAgentSteps) && untrustedTrigger;
60713
60728
  const head = untrustedHeadCheckout(jobSteps);
60714
60729
  const reach = Math.max(
60715
60730
  head === "root" ? 3 : head === "subdir" ? 1 : 0,
@@ -60819,19 +60834,35 @@ function analyzeAgentConfig(path80, content) {
60819
60834
  );
60820
60835
  if (broad.length > 0) {
60821
60836
  const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
60837
+ const bashBackstop = deny.some((d) => /^Bash\b/.test(d));
60838
+ const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(a));
60839
+ const high = bareShell && !bashBackstop;
60840
+ const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
60841
+ if (high)
60842
+ signals.push(
60843
+ "unrestricted `Bash` with no `deny` backstop \u2014 any command an injected instruction names runs without a prompt, for everyone who opens this repo with the agent"
60844
+ );
60845
+ else if (bareShell)
60846
+ signals.push(
60847
+ "a `Bash` deny list narrows the unrestricted `Bash` allow; it blocks only the commands it names"
60848
+ );
60849
+ if (broad.some((a) => /^Bash\(git:/.test(a)))
60850
+ signals.push(
60851
+ "`Bash(git:*)` pre-approves every git command; git can run other programs (`-c core.pager=\u2026`, `!` aliases), so this is broader than it looks"
60852
+ );
60853
+ if (broad.some((a) => /^Write|^Edit$/.test(a)))
60854
+ signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
60855
+ if (!high && !bareShell && !hasBackstop) signals.push("no `deny` entry narrows these grants");
60822
60856
  findings.push({
60823
60857
  check: "CI-1",
60824
60858
  rule: "CI-1.broad-allow",
60825
60859
  // File-level: one finding per config file. Adding a SECOND broad allow makes the
60826
60860
  // same statement about the same file, so it must not read as a new finding.
60827
60861
  dimension: "toolRules",
60828
- severity: hasBackstop ? "medium" : "high",
60829
- title: hasBackstop ? "Committed agent config pre-authorizes broad tools" : "Committed agent config pre-authorizes broad tools with no deny backstop",
60862
+ severity: high ? "high" : "medium",
60863
+ title: high ? "Committed agent config pre-authorizes broad tools with no deny backstop" : "Committed agent config pre-authorizes broad tools",
60830
60864
  file: path80,
60831
- signals: [
60832
- `broad allow(s): ${broad.slice(0, 5).join(", ")}`,
60833
- hasBackstop ? "a `deny` list backstops the broad allow" : "no `deny` entry covers Bash/Write/Edit \u2014 every contributor is pre-authorized for catastrophic tools"
60834
- ],
60865
+ signals,
60835
60866
  fix: "Scope the allow-list to specific read-only subcommands (e.g. `Bash(gh pr view:*)`); avoid bare `Bash`/`git:`/`Write`, or add a `deny` backstop."
60836
60867
  });
60837
60868
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.23.1",
3
+ "version": "2.23.3",
4
4
  "description": "IAM for your AI agents. Set what Claude Code, Codex, Gemini, Cursor and any MCP server are allowed to do, review risky actions before they run, and keep every action on the record.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",