@node9/proxy 2.24.0 → 2.24.1

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 +67 -11
  2. package/dist/cli.mjs +67 -11
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -60454,10 +60454,6 @@ function ifsAreGated(ifs, labelConfigured) {
60454
60454
  const labelGated = labelConfigured && /event\.label|label\.name/i.test(ifs);
60455
60455
  return gated || labelGated;
60456
60456
  }
60457
- function hasActorGate(wf, raw) {
60458
- const ifs = [jobList(wf).map((j) => j.if), allSteps(wf).map((s) => s.step.if)].flat().map(str).join(" ");
60459
- return ifsAreGated(ifs, labelTypeConfigured(wf, raw));
60460
- }
60461
60457
  function hasStepMembershipGate(job) {
60462
60458
  const steps = job.steps ?? [];
60463
60459
  const gateIds = steps.filter((s) => s.id && MEMBERSHIP_CHECK_RE.test(str(s.run))).map((s) => s.id);
@@ -60475,6 +60471,54 @@ function jobActorGate(job, wf, raw) {
60475
60471
  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
60472
  hasStepMembershipGate(job);
60477
60473
  }
60474
+ var STATUS_FN_RE = /\b(success|failure|always|cancelled)\s*\(/i;
60475
+ var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]/;
60476
+ function needsOf(job) {
60477
+ return (Array.isArray(job.needs) ? job.needs : job.needs ? [job.needs] : []).map(String);
60478
+ }
60479
+ function escapeRe(x) {
60480
+ return x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
60481
+ }
60482
+ function upstreamJobGates(name, dep, wf, raw, readerIf) {
60483
+ const depIf = str(dep.if);
60484
+ const label2 = labelTypeConfigured(wf, raw);
60485
+ if (!/\|\|/.test(depIf) && (ifsAreGated(depIf, label2) || /assignee\.login\s*==|event\.assignee\b/i.test(depIf)))
60486
+ return true;
60487
+ const steps = dep.steps ?? [];
60488
+ const stepGated = ifsAreGated(steps.map((st) => str(st.if)).join(" "), label2) || hasStepMembershipGate(dep);
60489
+ if (!stepGated) return false;
60490
+ if (steps.some(
60491
+ (st) => FAILING_STEP_RE.test(str(st.run)) || FAILING_STEP_RE.test(str(st.with?.["script"]))
60492
+ ))
60493
+ return true;
60494
+ return readerIf !== null && new RegExp(`needs\\.${escapeRe(name)}\\.outputs\\.`).test(readerIf);
60495
+ }
60496
+ function gatingAncestors(job, wf, raw) {
60497
+ const out = /* @__PURE__ */ new Set();
60498
+ const seen = /* @__PURE__ */ new Set();
60499
+ const visit = (j, readerIf) => {
60500
+ for (const name of needsOf(j)) {
60501
+ const dep = wf.jobs?.[name];
60502
+ if (!dep) continue;
60503
+ if (upstreamJobGates(name, dep, wf, raw, readerIf)) out.add(name);
60504
+ if (seen.has(name)) continue;
60505
+ seen.add(name);
60506
+ visit(dep, str(dep.if));
60507
+ }
60508
+ };
60509
+ visit(job, str(job.if));
60510
+ return out;
60511
+ }
60512
+ function jobGated(job, wf, raw) {
60513
+ if (jobActorGate(job, wf, raw)) return true;
60514
+ const gating = gatingAncestors(job, wf, raw);
60515
+ if (gating.size === 0) return false;
60516
+ const ownIf = str(job.if);
60517
+ if (!STATUS_FN_RE.test(ownIf)) return true;
60518
+ return needsOf(job).some(
60519
+ (n) => gating.has(n) && new RegExp(`needs\\.${escapeRe(n)}\\.result\\s*==\\s*['"]success['"]`).test(ownIf)
60520
+ );
60521
+ }
60478
60522
  function bypassArmed(steps) {
60479
60523
  return steps.some(
60480
60524
  (s) => str(s.with?.["allowed_non_write_users"]) === "*" && (!!str(s.with?.["github_token"]) || !!str(s.env?.["OVERRIDE_GITHUB_TOKEN"]))
@@ -60490,8 +60534,7 @@ function injectableJobs(wf, raw, untrustedTrigger) {
60490
60534
  const a = (job.steps ?? []).filter(isAgentStep);
60491
60535
  if (!a.length) continue;
60492
60536
  const jobStar = bypassArmed(a);
60493
- if (jobActorGate(job, wf, raw) || hasImplicitActorGate(a, jobStar && untrustedTrigger))
60494
- continue;
60537
+ if (jobGated(job, wf, raw) || hasImplicitActorGate(a, jobStar && untrustedTrigger)) continue;
60495
60538
  out.push(job);
60496
60539
  }
60497
60540
  return out;
@@ -60625,10 +60668,10 @@ function analyzeWorkflow(path80, content) {
60625
60668
  const elevated = hasCodeWritePerm(wf, powerJobs) || hasIdTokenWrite(wf, powerJobs) && egressTool;
60626
60669
  const pat = usesPatIn(powerJobs);
60627
60670
  const power = (broadTools ? 2 : 0) + (bypassActive ? 1 : 0) + (elevated ? 1 : 0) + (pat ? 1 : 0);
60628
- const explicitGate = hasActorGate(wf, raw);
60629
60671
  const implicitGate = hasImplicitActorGate(agentSteps, bypassActive);
60630
60672
  const membershipGated = injJobs.length === 0 && jobList(wf).some((j) => (j.steps ?? []).some(isAgentStep) && hasStepMembershipGate(j));
60631
- const gate = explicitGate || implicitGate;
60673
+ const gate = injJobs.length === 0;
60674
+ const gatedSiblings = agentJobs.filter((j) => !injJobs.includes(j)).length;
60632
60675
  const envDeny = hasEnvDeny(agentSteps);
60633
60676
  const pinned = agentActionsPinned(agentSteps);
60634
60677
  let score = reach + power;
@@ -60700,15 +60743,28 @@ function analyzeWorkflow(path80, content) {
60700
60743
  "`CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` is switched off, so the action no longer scrubs the Anthropic key, cloud credentials and Actions runtime tokens from the agent's shell"
60701
60744
  );
60702
60745
  if (!gate && reach > 0) signals.push("no effective actor gate");
60746
+ if (!gate && reach > 0 && gatedSiblings > 0) {
60747
+ const siblings = agentJobs.filter((j) => !injJobs.includes(j));
60748
+ const explicit = siblings.filter((j) => jobGated(j, wf, raw)).length;
60749
+ const byDefault = siblings.length - explicit;
60750
+ const n = (k) => `${k} other agent job${k === 1 ? "" : "s"}`;
60751
+ const parts = [
60752
+ explicit ? `${n(explicit)} ${explicit === 1 ? "is" : "are"} actor-gated` : "",
60753
+ byDefault ? `${n(byDefault)} ${byDefault === 1 ? "is" : "are"} held by claude-code-action's default write-access gate` : ""
60754
+ ].filter(Boolean);
60755
+ signals.push(
60756
+ `${parts.join("; ")} in this workflow; this finding is about the ungated one${injJobs.length === 1 ? "" : "s"}`
60757
+ );
60758
+ }
60703
60759
  const noExplicitPerms = wf.permissions == null && jobList(wf).every((j) => j.permissions == null);
60704
60760
  if (noExplicitPerms && reach > 0 && (broadTools || githubWriteTool))
60705
60761
  signals.push(
60706
60762
  "no explicit `permissions:` \u2014 the token defaults to the repo/org setting, which may grant write; set it explicitly to read-only"
60707
60763
  );
60708
60764
  const mitigations = [];
60709
- if (explicitGate || membershipGated)
60765
+ if (gate && (membershipGated || agentJobs.some((j) => jobGated(j, wf, raw))))
60710
60766
  mitigations.push("actor-gated (maintainer/label/write-user required)");
60711
- else if (implicitGate)
60767
+ else if (gate && implicitGate)
60712
60768
  mitigations.push("claude-code-action gates the agent to write-access users by default");
60713
60769
  if (head === "subdir") mitigations.push("untrusted head isolated in a subdir, not root");
60714
60770
  if (envDeny) mitigations.push("secrets env-denied from the agent subprocess");
@@ -60791,7 +60847,7 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
60791
60847
  promptTakesUntrusted(jobAgentSteps) ? 2 : 0,
60792
60848
  bypassActive ? 2 : 0
60793
60849
  );
60794
- const gate = jobActorGate(job, wf, raw) || hasImplicitActorGate(jobAgentSteps, bypassActive);
60850
+ const gate = jobGated(job, wf, raw) || hasImplicitActorGate(jobAgentSteps, bypassActive);
60795
60851
  const injectable = untrustedTrigger && !gate && reach > 0;
60796
60852
  const canReadEnv = EXFIL_RCE_RE.test(collectTools(jobAgentSteps));
60797
60853
  const realSecrets = secrets.filter((s) => s.kind !== "cloud-oidc");
package/dist/cli.mjs CHANGED
@@ -60443,10 +60443,6 @@ function ifsAreGated(ifs, labelConfigured) {
60443
60443
  const labelGated = labelConfigured && /event\.label|label\.name/i.test(ifs);
60444
60444
  return gated || labelGated;
60445
60445
  }
60446
- function hasActorGate(wf, raw) {
60447
- const ifs = [jobList(wf).map((j) => j.if), allSteps(wf).map((s) => s.step.if)].flat().map(str).join(" ");
60448
- return ifsAreGated(ifs, labelTypeConfigured(wf, raw));
60449
- }
60450
60446
  function hasStepMembershipGate(job) {
60451
60447
  const steps = job.steps ?? [];
60452
60448
  const gateIds = steps.filter((s) => s.id && MEMBERSHIP_CHECK_RE.test(str(s.run))).map((s) => s.id);
@@ -60464,6 +60460,54 @@ function jobActorGate(job, wf, raw) {
60464
60460
  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
60461
  hasStepMembershipGate(job);
60466
60462
  }
60463
+ var STATUS_FN_RE = /\b(success|failure|always|cancelled)\s*\(/i;
60464
+ var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]/;
60465
+ function needsOf(job) {
60466
+ return (Array.isArray(job.needs) ? job.needs : job.needs ? [job.needs] : []).map(String);
60467
+ }
60468
+ function escapeRe(x) {
60469
+ return x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
60470
+ }
60471
+ function upstreamJobGates(name, dep, wf, raw, readerIf) {
60472
+ const depIf = str(dep.if);
60473
+ const label2 = labelTypeConfigured(wf, raw);
60474
+ if (!/\|\|/.test(depIf) && (ifsAreGated(depIf, label2) || /assignee\.login\s*==|event\.assignee\b/i.test(depIf)))
60475
+ return true;
60476
+ const steps = dep.steps ?? [];
60477
+ const stepGated = ifsAreGated(steps.map((st) => str(st.if)).join(" "), label2) || hasStepMembershipGate(dep);
60478
+ if (!stepGated) return false;
60479
+ if (steps.some(
60480
+ (st) => FAILING_STEP_RE.test(str(st.run)) || FAILING_STEP_RE.test(str(st.with?.["script"]))
60481
+ ))
60482
+ return true;
60483
+ return readerIf !== null && new RegExp(`needs\\.${escapeRe(name)}\\.outputs\\.`).test(readerIf);
60484
+ }
60485
+ function gatingAncestors(job, wf, raw) {
60486
+ const out = /* @__PURE__ */ new Set();
60487
+ const seen = /* @__PURE__ */ new Set();
60488
+ const visit = (j, readerIf) => {
60489
+ for (const name of needsOf(j)) {
60490
+ const dep = wf.jobs?.[name];
60491
+ if (!dep) continue;
60492
+ if (upstreamJobGates(name, dep, wf, raw, readerIf)) out.add(name);
60493
+ if (seen.has(name)) continue;
60494
+ seen.add(name);
60495
+ visit(dep, str(dep.if));
60496
+ }
60497
+ };
60498
+ visit(job, str(job.if));
60499
+ return out;
60500
+ }
60501
+ function jobGated(job, wf, raw) {
60502
+ if (jobActorGate(job, wf, raw)) return true;
60503
+ const gating = gatingAncestors(job, wf, raw);
60504
+ if (gating.size === 0) return false;
60505
+ const ownIf = str(job.if);
60506
+ if (!STATUS_FN_RE.test(ownIf)) return true;
60507
+ return needsOf(job).some(
60508
+ (n) => gating.has(n) && new RegExp(`needs\\.${escapeRe(n)}\\.result\\s*==\\s*['"]success['"]`).test(ownIf)
60509
+ );
60510
+ }
60467
60511
  function bypassArmed(steps) {
60468
60512
  return steps.some(
60469
60513
  (s) => str(s.with?.["allowed_non_write_users"]) === "*" && (!!str(s.with?.["github_token"]) || !!str(s.env?.["OVERRIDE_GITHUB_TOKEN"]))
@@ -60479,8 +60523,7 @@ function injectableJobs(wf, raw, untrustedTrigger) {
60479
60523
  const a = (job.steps ?? []).filter(isAgentStep);
60480
60524
  if (!a.length) continue;
60481
60525
  const jobStar = bypassArmed(a);
60482
- if (jobActorGate(job, wf, raw) || hasImplicitActorGate(a, jobStar && untrustedTrigger))
60483
- continue;
60526
+ if (jobGated(job, wf, raw) || hasImplicitActorGate(a, jobStar && untrustedTrigger)) continue;
60484
60527
  out.push(job);
60485
60528
  }
60486
60529
  return out;
@@ -60614,10 +60657,10 @@ function analyzeWorkflow(path80, content) {
60614
60657
  const elevated = hasCodeWritePerm(wf, powerJobs) || hasIdTokenWrite(wf, powerJobs) && egressTool;
60615
60658
  const pat = usesPatIn(powerJobs);
60616
60659
  const power = (broadTools ? 2 : 0) + (bypassActive ? 1 : 0) + (elevated ? 1 : 0) + (pat ? 1 : 0);
60617
- const explicitGate = hasActorGate(wf, raw);
60618
60660
  const implicitGate = hasImplicitActorGate(agentSteps, bypassActive);
60619
60661
  const membershipGated = injJobs.length === 0 && jobList(wf).some((j) => (j.steps ?? []).some(isAgentStep) && hasStepMembershipGate(j));
60620
- const gate = explicitGate || implicitGate;
60662
+ const gate = injJobs.length === 0;
60663
+ const gatedSiblings = agentJobs.filter((j) => !injJobs.includes(j)).length;
60621
60664
  const envDeny = hasEnvDeny(agentSteps);
60622
60665
  const pinned = agentActionsPinned(agentSteps);
60623
60666
  let score = reach + power;
@@ -60689,15 +60732,28 @@ function analyzeWorkflow(path80, content) {
60689
60732
  "`CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` is switched off, so the action no longer scrubs the Anthropic key, cloud credentials and Actions runtime tokens from the agent's shell"
60690
60733
  );
60691
60734
  if (!gate && reach > 0) signals.push("no effective actor gate");
60735
+ if (!gate && reach > 0 && gatedSiblings > 0) {
60736
+ const siblings = agentJobs.filter((j) => !injJobs.includes(j));
60737
+ const explicit = siblings.filter((j) => jobGated(j, wf, raw)).length;
60738
+ const byDefault = siblings.length - explicit;
60739
+ const n = (k) => `${k} other agent job${k === 1 ? "" : "s"}`;
60740
+ const parts = [
60741
+ explicit ? `${n(explicit)} ${explicit === 1 ? "is" : "are"} actor-gated` : "",
60742
+ byDefault ? `${n(byDefault)} ${byDefault === 1 ? "is" : "are"} held by claude-code-action's default write-access gate` : ""
60743
+ ].filter(Boolean);
60744
+ signals.push(
60745
+ `${parts.join("; ")} in this workflow; this finding is about the ungated one${injJobs.length === 1 ? "" : "s"}`
60746
+ );
60747
+ }
60692
60748
  const noExplicitPerms = wf.permissions == null && jobList(wf).every((j) => j.permissions == null);
60693
60749
  if (noExplicitPerms && reach > 0 && (broadTools || githubWriteTool))
60694
60750
  signals.push(
60695
60751
  "no explicit `permissions:` \u2014 the token defaults to the repo/org setting, which may grant write; set it explicitly to read-only"
60696
60752
  );
60697
60753
  const mitigations = [];
60698
- if (explicitGate || membershipGated)
60754
+ if (gate && (membershipGated || agentJobs.some((j) => jobGated(j, wf, raw))))
60699
60755
  mitigations.push("actor-gated (maintainer/label/write-user required)");
60700
- else if (implicitGate)
60756
+ else if (gate && implicitGate)
60701
60757
  mitigations.push("claude-code-action gates the agent to write-access users by default");
60702
60758
  if (head === "subdir") mitigations.push("untrusted head isolated in a subdir, not root");
60703
60759
  if (envDeny) mitigations.push("secrets env-denied from the agent subprocess");
@@ -60780,7 +60836,7 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
60780
60836
  promptTakesUntrusted(jobAgentSteps) ? 2 : 0,
60781
60837
  bypassActive ? 2 : 0
60782
60838
  );
60783
- const gate = jobActorGate(job, wf, raw) || hasImplicitActorGate(jobAgentSteps, bypassActive);
60839
+ const gate = jobGated(job, wf, raw) || hasImplicitActorGate(jobAgentSteps, bypassActive);
60784
60840
  const injectable = untrustedTrigger && !gate && reach > 0;
60785
60841
  const canReadEnv = EXFIL_RCE_RE.test(collectTools(jobAgentSteps));
60786
60842
  const realSecrets = secrets.filter((s) => s.kind !== "cloud-oidc");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.24.0",
3
+ "version": "2.24.1",
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",