@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.
- package/dist/cli.js +46 -15
- package/dist/cli.mjs +46 -15
- 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 =
|
|
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
|
|
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 =
|
|
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)
|
|
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))
|
|
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
|
|
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:
|
|
60840
|
-
title:
|
|
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 =
|
|
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
|
|
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 =
|
|
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)
|
|
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))
|
|
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
|
|
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:
|
|
60829
|
-
title:
|
|
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.
|
|
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",
|