@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.
- package/dist/cli.js +67 -11
- package/dist/cli.mjs +67 -11
- 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 (
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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.
|
|
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",
|