@node9/proxy 2.23.3 → 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 +117 -12
  2. package/dist/cli.mjs +117 -12
  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;
@@ -60537,6 +60580,36 @@ function hasMetaWritePerm(wf, agentJobs) {
60537
60580
  function hasEnvDeny(steps) {
60538
60581
  return steps.some((s) => /"?mode"?\s*:\s*"?deny/i.test(str(s.with?.["settings"])));
60539
60582
  }
60583
+ var CLAUDE_CODE_ACTION_RE = /anthropics\/claude-code-action@/i;
60584
+ var FULL_OUTPUT_ACTION_RE = /anthropics\/claude-code(-base)?-action@/i;
60585
+ function pinnedBelow(uses, min) {
60586
+ const m = /@v?(\d+)\.(\d+)\.(\d+)$/.exec(uses.trim());
60587
+ if (!m) return false;
60588
+ const v = [Number(m[1]), Number(m[2]), Number(m[3])];
60589
+ for (let i = 0; i < 3; i++) if (v[i] !== min[i]) return v[i] < min[i];
60590
+ return false;
60591
+ }
60592
+ var FULL_OUTPUT_MIN = [1, 0, 16];
60593
+ var ENV_SCRUB_MIN = [1, 0, 77];
60594
+ function showsFullOutput(steps) {
60595
+ return steps.some(
60596
+ (s) => FULL_OUTPUT_ACTION_RE.test(s.uses ?? "") && !pinnedBelow(s.uses ?? "", FULL_OUTPUT_MIN) && str(s.with?.["show_full_output"]) === "true"
60597
+ );
60598
+ }
60599
+ function envScrubOptedOut(pairs, wf) {
60600
+ const wfEnv = wf.env;
60601
+ const KEY = "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB";
60602
+ return pairs.some(({ job, step }) => {
60603
+ const uses = step.uses ?? "";
60604
+ if (!CLAUDE_CODE_ACTION_RE.test(uses) || pinnedBelow(uses, ENV_SCRUB_MIN)) return false;
60605
+ if (!str(step.with?.["allowed_non_write_users"]).trim()) return false;
60606
+ const holder = [step.env, job.env, wfEnv].find((e) => e != null && KEY in e);
60607
+ if (!holder) return false;
60608
+ const v = str(holder[KEY]).trim();
60609
+ if (!v || /\$\{\{/.test(v)) return false;
60610
+ return !/^(1|true|yes|on)$/i.test(v);
60611
+ });
60612
+ }
60540
60613
  function agentActionsPinned(steps) {
60541
60614
  const agent = steps.filter(isAgentStep).filter((s) => s.uses);
60542
60615
  if (agent.length === 0) return false;
@@ -60595,10 +60668,10 @@ function analyzeWorkflow(path80, content) {
60595
60668
  const elevated = hasCodeWritePerm(wf, powerJobs) || hasIdTokenWrite(wf, powerJobs) && egressTool;
60596
60669
  const pat = usesPatIn(powerJobs);
60597
60670
  const power = (broadTools ? 2 : 0) + (bypassActive ? 1 : 0) + (elevated ? 1 : 0) + (pat ? 1 : 0);
60598
- const explicitGate = hasActorGate(wf, raw);
60599
60671
  const implicitGate = hasImplicitActorGate(agentSteps, bypassActive);
60600
60672
  const membershipGated = injJobs.length === 0 && jobList(wf).some((j) => (j.steps ?? []).some(isAgentStep) && hasStepMembershipGate(j));
60601
- const gate = explicitGate || implicitGate;
60673
+ const gate = injJobs.length === 0;
60674
+ const gatedSiblings = agentJobs.filter((j) => !injJobs.includes(j)).length;
60602
60675
  const envDeny = hasEnvDeny(agentSteps);
60603
60676
  const pinned = agentActionsPinned(agentSteps);
60604
60677
  let score = reach + power;
@@ -60656,16 +60729,42 @@ function analyzeWorkflow(path80, content) {
60656
60729
  signals.push('allowed_non_write_users: "*" with github_token \u2014 any user can trigger the agent');
60657
60730
  if (elevated) signals.push("elevated permissions (contents/id-token: write)");
60658
60731
  if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
60732
+ const scopedPairs = allSteps(wf).filter(
60733
+ (p) => isAgentStep(p.step) && (injJobs.length === 0 || injJobs.includes(p.job))
60734
+ );
60735
+ const fullOutput = showsFullOutput(scopedPairs.map((p) => p.step));
60736
+ const scrubOff = envScrubOptedOut(scopedPairs, wf);
60737
+ if (fullOutput)
60738
+ signals.push(
60739
+ "`show_full_output: true` writes every agent command and its output to the Actions log, public on a public repo; GitHub masks known secret values there, but an agent told to encode one first can publish it without any network tool"
60740
+ );
60741
+ if (scrubOff)
60742
+ signals.push(
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"
60744
+ );
60659
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
+ }
60660
60759
  const noExplicitPerms = wf.permissions == null && jobList(wf).every((j) => j.permissions == null);
60661
60760
  if (noExplicitPerms && reach > 0 && (broadTools || githubWriteTool))
60662
60761
  signals.push(
60663
60762
  "no explicit `permissions:` \u2014 the token defaults to the repo/org setting, which may grant write; set it explicitly to read-only"
60664
60763
  );
60665
60764
  const mitigations = [];
60666
- if (explicitGate || membershipGated)
60765
+ if (gate && (membershipGated || agentJobs.some((j) => jobGated(j, wf, raw))))
60667
60766
  mitigations.push("actor-gated (maintainer/label/write-user required)");
60668
- else if (implicitGate)
60767
+ else if (gate && implicitGate)
60669
60768
  mitigations.push("claude-code-action gates the agent to write-access users by default");
60670
60769
  if (head === "subdir") mitigations.push("untrusted head isolated in a subdir, not root");
60671
60770
  if (envDeny) mitigations.push("secrets env-denied from the agent subprocess");
@@ -60676,7 +60775,7 @@ function analyzeWorkflow(path80, content) {
60676
60775
  'allowed_non_write_users: "*" is set without github_token, so the action keeps its default write gate'
60677
60776
  );
60678
60777
  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";
60679
- return {
60778
+ const finding = {
60680
60779
  check: "CI-2",
60681
60780
  // One verdict per workflow file — the finding IS the file's reachability score.
60682
60781
  rule: "CI-2.injectable-workflow",
@@ -60688,6 +60787,12 @@ function analyzeWorkflow(path80, content) {
60688
60787
  mitigations: mitigations.length ? mitigations : void 0,
60689
60788
  fix: head === "root" && privileged ? "Do not check out the untrusted PR head into the workspace root under a privileged trigger (pull_request_target/workflow_run) \u2014 check out the base ref, or isolate the head in a subdir (--add-dir). Add an actor gate and scope the agent tools." : head === "root" ? "This runs under `pull_request` (fork PRs get a read-only token), so the head checkout is low-risk today \u2014 keep it on `pull_request` (not `pull_request_target`) and keep the actor gate + scoped tools." : "Add/verify an actor gate, scope the agent tools to read-only, and env-deny secrets. See Anthropic\u2019s claude-code-action security doc."
60690
60789
  };
60790
+ const extraFix = [
60791
+ fullOutput ? "Remove `show_full_output: true` (the action documents it for debugging only)." : "",
60792
+ scrubOff ? "Remove the `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` opt-out." : ""
60793
+ ].filter(Boolean).join(" ");
60794
+ if (extraFix) finding.fix = `${finding.fix} ${extraFix}`;
60795
+ return finding;
60691
60796
  }
60692
60797
  var AGENT_FUEL_RE = /^(ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL|CLAUDE_CODE_OAUTH_TOKEN|OPENAI_API_KEY|OPENAI_BASE_URL|GEMINI_API_KEY|GOOGLE_API_KEY|GITHUB_TOKEN)$/i;
60693
60798
  var FUEL_INPUT_KEYS = /^(anthropic_api_key|anthropic_auth_token|anthropic_base_url|claude_code_oauth_token|openai_api_key|openai_base_url|gemini_api_key|google_api_key)$/i;
@@ -60742,7 +60847,7 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
60742
60847
  promptTakesUntrusted(jobAgentSteps) ? 2 : 0,
60743
60848
  bypassActive ? 2 : 0
60744
60849
  );
60745
- const gate = jobActorGate(job, wf, raw) || hasImplicitActorGate(jobAgentSteps, bypassActive);
60850
+ const gate = jobGated(job, wf, raw) || hasImplicitActorGate(jobAgentSteps, bypassActive);
60746
60851
  const injectable = untrustedTrigger && !gate && reach > 0;
60747
60852
  const canReadEnv = EXFIL_RCE_RE.test(collectTools(jobAgentSteps));
60748
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;
@@ -60526,6 +60569,36 @@ function hasMetaWritePerm(wf, agentJobs) {
60526
60569
  function hasEnvDeny(steps) {
60527
60570
  return steps.some((s) => /"?mode"?\s*:\s*"?deny/i.test(str(s.with?.["settings"])));
60528
60571
  }
60572
+ var CLAUDE_CODE_ACTION_RE = /anthropics\/claude-code-action@/i;
60573
+ var FULL_OUTPUT_ACTION_RE = /anthropics\/claude-code(-base)?-action@/i;
60574
+ function pinnedBelow(uses, min) {
60575
+ const m = /@v?(\d+)\.(\d+)\.(\d+)$/.exec(uses.trim());
60576
+ if (!m) return false;
60577
+ const v = [Number(m[1]), Number(m[2]), Number(m[3])];
60578
+ for (let i = 0; i < 3; i++) if (v[i] !== min[i]) return v[i] < min[i];
60579
+ return false;
60580
+ }
60581
+ var FULL_OUTPUT_MIN = [1, 0, 16];
60582
+ var ENV_SCRUB_MIN = [1, 0, 77];
60583
+ function showsFullOutput(steps) {
60584
+ return steps.some(
60585
+ (s) => FULL_OUTPUT_ACTION_RE.test(s.uses ?? "") && !pinnedBelow(s.uses ?? "", FULL_OUTPUT_MIN) && str(s.with?.["show_full_output"]) === "true"
60586
+ );
60587
+ }
60588
+ function envScrubOptedOut(pairs, wf) {
60589
+ const wfEnv = wf.env;
60590
+ const KEY = "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB";
60591
+ return pairs.some(({ job, step }) => {
60592
+ const uses = step.uses ?? "";
60593
+ if (!CLAUDE_CODE_ACTION_RE.test(uses) || pinnedBelow(uses, ENV_SCRUB_MIN)) return false;
60594
+ if (!str(step.with?.["allowed_non_write_users"]).trim()) return false;
60595
+ const holder = [step.env, job.env, wfEnv].find((e) => e != null && KEY in e);
60596
+ if (!holder) return false;
60597
+ const v = str(holder[KEY]).trim();
60598
+ if (!v || /\$\{\{/.test(v)) return false;
60599
+ return !/^(1|true|yes|on)$/i.test(v);
60600
+ });
60601
+ }
60529
60602
  function agentActionsPinned(steps) {
60530
60603
  const agent = steps.filter(isAgentStep).filter((s) => s.uses);
60531
60604
  if (agent.length === 0) return false;
@@ -60584,10 +60657,10 @@ function analyzeWorkflow(path80, content) {
60584
60657
  const elevated = hasCodeWritePerm(wf, powerJobs) || hasIdTokenWrite(wf, powerJobs) && egressTool;
60585
60658
  const pat = usesPatIn(powerJobs);
60586
60659
  const power = (broadTools ? 2 : 0) + (bypassActive ? 1 : 0) + (elevated ? 1 : 0) + (pat ? 1 : 0);
60587
- const explicitGate = hasActorGate(wf, raw);
60588
60660
  const implicitGate = hasImplicitActorGate(agentSteps, bypassActive);
60589
60661
  const membershipGated = injJobs.length === 0 && jobList(wf).some((j) => (j.steps ?? []).some(isAgentStep) && hasStepMembershipGate(j));
60590
- const gate = explicitGate || implicitGate;
60662
+ const gate = injJobs.length === 0;
60663
+ const gatedSiblings = agentJobs.filter((j) => !injJobs.includes(j)).length;
60591
60664
  const envDeny = hasEnvDeny(agentSteps);
60592
60665
  const pinned = agentActionsPinned(agentSteps);
60593
60666
  let score = reach + power;
@@ -60645,16 +60718,42 @@ function analyzeWorkflow(path80, content) {
60645
60718
  signals.push('allowed_non_write_users: "*" with github_token \u2014 any user can trigger the agent');
60646
60719
  if (elevated) signals.push("elevated permissions (contents/id-token: write)");
60647
60720
  if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
60721
+ const scopedPairs = allSteps(wf).filter(
60722
+ (p) => isAgentStep(p.step) && (injJobs.length === 0 || injJobs.includes(p.job))
60723
+ );
60724
+ const fullOutput = showsFullOutput(scopedPairs.map((p) => p.step));
60725
+ const scrubOff = envScrubOptedOut(scopedPairs, wf);
60726
+ if (fullOutput)
60727
+ signals.push(
60728
+ "`show_full_output: true` writes every agent command and its output to the Actions log, public on a public repo; GitHub masks known secret values there, but an agent told to encode one first can publish it without any network tool"
60729
+ );
60730
+ if (scrubOff)
60731
+ signals.push(
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"
60733
+ );
60648
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
+ }
60649
60748
  const noExplicitPerms = wf.permissions == null && jobList(wf).every((j) => j.permissions == null);
60650
60749
  if (noExplicitPerms && reach > 0 && (broadTools || githubWriteTool))
60651
60750
  signals.push(
60652
60751
  "no explicit `permissions:` \u2014 the token defaults to the repo/org setting, which may grant write; set it explicitly to read-only"
60653
60752
  );
60654
60753
  const mitigations = [];
60655
- if (explicitGate || membershipGated)
60754
+ if (gate && (membershipGated || agentJobs.some((j) => jobGated(j, wf, raw))))
60656
60755
  mitigations.push("actor-gated (maintainer/label/write-user required)");
60657
- else if (implicitGate)
60756
+ else if (gate && implicitGate)
60658
60757
  mitigations.push("claude-code-action gates the agent to write-access users by default");
60659
60758
  if (head === "subdir") mitigations.push("untrusted head isolated in a subdir, not root");
60660
60759
  if (envDeny) mitigations.push("secrets env-denied from the agent subprocess");
@@ -60665,7 +60764,7 @@ function analyzeWorkflow(path80, content) {
60665
60764
  'allowed_non_write_users: "*" is set without github_token, so the action keeps its default write gate'
60666
60765
  );
60667
60766
  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";
60668
- return {
60767
+ const finding = {
60669
60768
  check: "CI-2",
60670
60769
  // One verdict per workflow file — the finding IS the file's reachability score.
60671
60770
  rule: "CI-2.injectable-workflow",
@@ -60677,6 +60776,12 @@ function analyzeWorkflow(path80, content) {
60677
60776
  mitigations: mitigations.length ? mitigations : void 0,
60678
60777
  fix: head === "root" && privileged ? "Do not check out the untrusted PR head into the workspace root under a privileged trigger (pull_request_target/workflow_run) \u2014 check out the base ref, or isolate the head in a subdir (--add-dir). Add an actor gate and scope the agent tools." : head === "root" ? "This runs under `pull_request` (fork PRs get a read-only token), so the head checkout is low-risk today \u2014 keep it on `pull_request` (not `pull_request_target`) and keep the actor gate + scoped tools." : "Add/verify an actor gate, scope the agent tools to read-only, and env-deny secrets. See Anthropic\u2019s claude-code-action security doc."
60679
60778
  };
60779
+ const extraFix = [
60780
+ fullOutput ? "Remove `show_full_output: true` (the action documents it for debugging only)." : "",
60781
+ scrubOff ? "Remove the `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` opt-out." : ""
60782
+ ].filter(Boolean).join(" ");
60783
+ if (extraFix) finding.fix = `${finding.fix} ${extraFix}`;
60784
+ return finding;
60680
60785
  }
60681
60786
  var AGENT_FUEL_RE = /^(ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL|CLAUDE_CODE_OAUTH_TOKEN|OPENAI_API_KEY|OPENAI_BASE_URL|GEMINI_API_KEY|GOOGLE_API_KEY|GITHUB_TOKEN)$/i;
60682
60787
  var FUEL_INPUT_KEYS = /^(anthropic_api_key|anthropic_auth_token|anthropic_base_url|claude_code_oauth_token|openai_api_key|openai_base_url|gemini_api_key|google_api_key)$/i;
@@ -60731,7 +60836,7 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
60731
60836
  promptTakesUntrusted(jobAgentSteps) ? 2 : 0,
60732
60837
  bypassActive ? 2 : 0
60733
60838
  );
60734
- const gate = jobActorGate(job, wf, raw) || hasImplicitActorGate(jobAgentSteps, bypassActive);
60839
+ const gate = jobGated(job, wf, raw) || hasImplicitActorGate(jobAgentSteps, bypassActive);
60735
60840
  const injectable = untrustedTrigger && !gate && reach > 0;
60736
60841
  const canReadEnv = EXFIL_RCE_RE.test(collectTools(jobAgentSteps));
60737
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.23.3",
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",