@node9/proxy 2.23.2 → 2.24.0
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 +81 -11
- package/dist/cli.mjs +81 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -60496,11 +60496,15 @@ function injectableJobs(wf, raw, untrustedTrigger) {
|
|
|
60496
60496
|
}
|
|
60497
60497
|
return out;
|
|
60498
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
|
+
}
|
|
60499
60503
|
function usesPatIn(jobs) {
|
|
60500
60504
|
for (const j of jobs)
|
|
60501
60505
|
for (const step of j.steps ?? []) {
|
|
60502
|
-
const gt = str(step.with?.["github_token"]);
|
|
60503
|
-
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;
|
|
60504
60508
|
}
|
|
60505
60509
|
return false;
|
|
60506
60510
|
}
|
|
@@ -60533,6 +60537,36 @@ function hasMetaWritePerm(wf, agentJobs) {
|
|
|
60533
60537
|
function hasEnvDeny(steps) {
|
|
60534
60538
|
return steps.some((s) => /"?mode"?\s*:\s*"?deny/i.test(str(s.with?.["settings"])));
|
|
60535
60539
|
}
|
|
60540
|
+
var CLAUDE_CODE_ACTION_RE = /anthropics\/claude-code-action@/i;
|
|
60541
|
+
var FULL_OUTPUT_ACTION_RE = /anthropics\/claude-code(-base)?-action@/i;
|
|
60542
|
+
function pinnedBelow(uses, min) {
|
|
60543
|
+
const m = /@v?(\d+)\.(\d+)\.(\d+)$/.exec(uses.trim());
|
|
60544
|
+
if (!m) return false;
|
|
60545
|
+
const v = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
60546
|
+
for (let i = 0; i < 3; i++) if (v[i] !== min[i]) return v[i] < min[i];
|
|
60547
|
+
return false;
|
|
60548
|
+
}
|
|
60549
|
+
var FULL_OUTPUT_MIN = [1, 0, 16];
|
|
60550
|
+
var ENV_SCRUB_MIN = [1, 0, 77];
|
|
60551
|
+
function showsFullOutput(steps) {
|
|
60552
|
+
return steps.some(
|
|
60553
|
+
(s) => FULL_OUTPUT_ACTION_RE.test(s.uses ?? "") && !pinnedBelow(s.uses ?? "", FULL_OUTPUT_MIN) && str(s.with?.["show_full_output"]) === "true"
|
|
60554
|
+
);
|
|
60555
|
+
}
|
|
60556
|
+
function envScrubOptedOut(pairs, wf) {
|
|
60557
|
+
const wfEnv = wf.env;
|
|
60558
|
+
const KEY = "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB";
|
|
60559
|
+
return pairs.some(({ job, step }) => {
|
|
60560
|
+
const uses = step.uses ?? "";
|
|
60561
|
+
if (!CLAUDE_CODE_ACTION_RE.test(uses) || pinnedBelow(uses, ENV_SCRUB_MIN)) return false;
|
|
60562
|
+
if (!str(step.with?.["allowed_non_write_users"]).trim()) return false;
|
|
60563
|
+
const holder = [step.env, job.env, wfEnv].find((e) => e != null && KEY in e);
|
|
60564
|
+
if (!holder) return false;
|
|
60565
|
+
const v = str(holder[KEY]).trim();
|
|
60566
|
+
if (!v || /\$\{\{/.test(v)) return false;
|
|
60567
|
+
return !/^(1|true|yes|on)$/i.test(v);
|
|
60568
|
+
});
|
|
60569
|
+
}
|
|
60536
60570
|
function agentActionsPinned(steps) {
|
|
60537
60571
|
const agent = steps.filter(isAgentStep).filter((s) => s.uses);
|
|
60538
60572
|
if (agent.length === 0) return false;
|
|
@@ -60652,6 +60686,19 @@ function analyzeWorkflow(path80, content) {
|
|
|
60652
60686
|
signals.push('allowed_non_write_users: "*" with github_token \u2014 any user can trigger the agent');
|
|
60653
60687
|
if (elevated) signals.push("elevated permissions (contents/id-token: write)");
|
|
60654
60688
|
if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
|
|
60689
|
+
const scopedPairs = allSteps(wf).filter(
|
|
60690
|
+
(p) => isAgentStep(p.step) && (injJobs.length === 0 || injJobs.includes(p.job))
|
|
60691
|
+
);
|
|
60692
|
+
const fullOutput = showsFullOutput(scopedPairs.map((p) => p.step));
|
|
60693
|
+
const scrubOff = envScrubOptedOut(scopedPairs, wf);
|
|
60694
|
+
if (fullOutput)
|
|
60695
|
+
signals.push(
|
|
60696
|
+
"`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"
|
|
60697
|
+
);
|
|
60698
|
+
if (scrubOff)
|
|
60699
|
+
signals.push(
|
|
60700
|
+
"`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
|
+
);
|
|
60655
60702
|
if (!gate && reach > 0) signals.push("no effective actor gate");
|
|
60656
60703
|
const noExplicitPerms = wf.permissions == null && jobList(wf).every((j) => j.permissions == null);
|
|
60657
60704
|
if (noExplicitPerms && reach > 0 && (broadTools || githubWriteTool))
|
|
@@ -60672,7 +60719,7 @@ function analyzeWorkflow(path80, content) {
|
|
|
60672
60719
|
'allowed_non_write_users: "*" is set without github_token, so the action keeps its default write gate'
|
|
60673
60720
|
);
|
|
60674
60721
|
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";
|
|
60675
|
-
|
|
60722
|
+
const finding = {
|
|
60676
60723
|
check: "CI-2",
|
|
60677
60724
|
// One verdict per workflow file — the finding IS the file's reachability score.
|
|
60678
60725
|
rule: "CI-2.injectable-workflow",
|
|
@@ -60684,13 +60731,20 @@ function analyzeWorkflow(path80, content) {
|
|
|
60684
60731
|
mitigations: mitigations.length ? mitigations : void 0,
|
|
60685
60732
|
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."
|
|
60686
60733
|
};
|
|
60734
|
+
const extraFix = [
|
|
60735
|
+
fullOutput ? "Remove `show_full_output: true` (the action documents it for debugging only)." : "",
|
|
60736
|
+
scrubOff ? "Remove the `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` opt-out." : ""
|
|
60737
|
+
].filter(Boolean).join(" ");
|
|
60738
|
+
if (extraFix) finding.fix = `${finding.fix} ${extraFix}`;
|
|
60739
|
+
return finding;
|
|
60687
60740
|
}
|
|
60688
60741
|
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;
|
|
60689
60742
|
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;
|
|
60690
60743
|
function fuelSecretNames(agentSteps) {
|
|
60691
60744
|
const out = /* @__PURE__ */ new Set();
|
|
60692
60745
|
const add = (v) => {
|
|
60693
|
-
for (const m of str(v).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi))
|
|
60746
|
+
for (const m of dropDeadSecretRefs(str(v)).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi))
|
|
60747
|
+
out.add(m[1]);
|
|
60694
60748
|
};
|
|
60695
60749
|
for (const st of agentSteps) {
|
|
60696
60750
|
for (const [k, v] of Object.entries(st.with ?? {})) if (FUEL_INPUT_KEYS.test(k)) add(v);
|
|
@@ -60712,7 +60766,7 @@ function agentReachableSecrets(wf, agentSteps, agentJobs) {
|
|
|
60712
60766
|
blobs.push(str(wf.env));
|
|
60713
60767
|
const fuel = fuelSecretNames(agentSteps);
|
|
60714
60768
|
const found = /* @__PURE__ */ new Map();
|
|
60715
|
-
for (const b of blobs) {
|
|
60769
|
+
for (const b of blobs.map(dropDeadSecretRefs)) {
|
|
60716
60770
|
for (const m of b.matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) {
|
|
60717
60771
|
const name = m[1];
|
|
60718
60772
|
if (AGENT_FUEL_RE.test(name) || fuel.has(name)) continue;
|
|
@@ -60840,19 +60894,35 @@ function analyzeAgentConfig(path80, content) {
|
|
|
60840
60894
|
);
|
|
60841
60895
|
if (broad.length > 0) {
|
|
60842
60896
|
const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
|
|
60897
|
+
const bashBackstop = deny.some((d) => /^Bash\b/.test(d));
|
|
60898
|
+
const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(a));
|
|
60899
|
+
const high = bareShell && !bashBackstop;
|
|
60900
|
+
const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
|
|
60901
|
+
if (high)
|
|
60902
|
+
signals.push(
|
|
60903
|
+
"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"
|
|
60904
|
+
);
|
|
60905
|
+
else if (bareShell)
|
|
60906
|
+
signals.push(
|
|
60907
|
+
"a `Bash` deny list narrows the unrestricted `Bash` allow; it blocks only the commands it names"
|
|
60908
|
+
);
|
|
60909
|
+
if (broad.some((a) => /^Bash\(git:/.test(a)))
|
|
60910
|
+
signals.push(
|
|
60911
|
+
"`Bash(git:*)` pre-approves every git command; git can run other programs (`-c core.pager=\u2026`, `!` aliases), so this is broader than it looks"
|
|
60912
|
+
);
|
|
60913
|
+
if (broad.some((a) => /^Write|^Edit$/.test(a)))
|
|
60914
|
+
signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
|
|
60915
|
+
if (!high && !bareShell && !hasBackstop) signals.push("no `deny` entry narrows these grants");
|
|
60843
60916
|
findings.push({
|
|
60844
60917
|
check: "CI-1",
|
|
60845
60918
|
rule: "CI-1.broad-allow",
|
|
60846
60919
|
// File-level: one finding per config file. Adding a SECOND broad allow makes the
|
|
60847
60920
|
// same statement about the same file, so it must not read as a new finding.
|
|
60848
60921
|
dimension: "toolRules",
|
|
60849
|
-
severity:
|
|
60850
|
-
title:
|
|
60922
|
+
severity: high ? "high" : "medium",
|
|
60923
|
+
title: high ? "Committed agent config pre-authorizes broad tools with no deny backstop" : "Committed agent config pre-authorizes broad tools",
|
|
60851
60924
|
file: path80,
|
|
60852
|
-
signals
|
|
60853
|
-
`broad allow(s): ${broad.slice(0, 5).join(", ")}`,
|
|
60854
|
-
hasBackstop ? "a `deny` list backstops the broad allow" : "no `deny` entry covers Bash/Write/Edit \u2014 every contributor is pre-authorized for catastrophic tools"
|
|
60855
|
-
],
|
|
60925
|
+
signals,
|
|
60856
60926
|
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."
|
|
60857
60927
|
});
|
|
60858
60928
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -60485,11 +60485,15 @@ function injectableJobs(wf, raw, untrustedTrigger) {
|
|
|
60485
60485
|
}
|
|
60486
60486
|
return out;
|
|
60487
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
|
+
}
|
|
60488
60492
|
function usesPatIn(jobs) {
|
|
60489
60493
|
for (const j of jobs)
|
|
60490
60494
|
for (const step of j.steps ?? []) {
|
|
60491
|
-
const gt = str(step.with?.["github_token"]);
|
|
60492
|
-
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;
|
|
60493
60497
|
}
|
|
60494
60498
|
return false;
|
|
60495
60499
|
}
|
|
@@ -60522,6 +60526,36 @@ function hasMetaWritePerm(wf, agentJobs) {
|
|
|
60522
60526
|
function hasEnvDeny(steps) {
|
|
60523
60527
|
return steps.some((s) => /"?mode"?\s*:\s*"?deny/i.test(str(s.with?.["settings"])));
|
|
60524
60528
|
}
|
|
60529
|
+
var CLAUDE_CODE_ACTION_RE = /anthropics\/claude-code-action@/i;
|
|
60530
|
+
var FULL_OUTPUT_ACTION_RE = /anthropics\/claude-code(-base)?-action@/i;
|
|
60531
|
+
function pinnedBelow(uses, min) {
|
|
60532
|
+
const m = /@v?(\d+)\.(\d+)\.(\d+)$/.exec(uses.trim());
|
|
60533
|
+
if (!m) return false;
|
|
60534
|
+
const v = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
60535
|
+
for (let i = 0; i < 3; i++) if (v[i] !== min[i]) return v[i] < min[i];
|
|
60536
|
+
return false;
|
|
60537
|
+
}
|
|
60538
|
+
var FULL_OUTPUT_MIN = [1, 0, 16];
|
|
60539
|
+
var ENV_SCRUB_MIN = [1, 0, 77];
|
|
60540
|
+
function showsFullOutput(steps) {
|
|
60541
|
+
return steps.some(
|
|
60542
|
+
(s) => FULL_OUTPUT_ACTION_RE.test(s.uses ?? "") && !pinnedBelow(s.uses ?? "", FULL_OUTPUT_MIN) && str(s.with?.["show_full_output"]) === "true"
|
|
60543
|
+
);
|
|
60544
|
+
}
|
|
60545
|
+
function envScrubOptedOut(pairs, wf) {
|
|
60546
|
+
const wfEnv = wf.env;
|
|
60547
|
+
const KEY = "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB";
|
|
60548
|
+
return pairs.some(({ job, step }) => {
|
|
60549
|
+
const uses = step.uses ?? "";
|
|
60550
|
+
if (!CLAUDE_CODE_ACTION_RE.test(uses) || pinnedBelow(uses, ENV_SCRUB_MIN)) return false;
|
|
60551
|
+
if (!str(step.with?.["allowed_non_write_users"]).trim()) return false;
|
|
60552
|
+
const holder = [step.env, job.env, wfEnv].find((e) => e != null && KEY in e);
|
|
60553
|
+
if (!holder) return false;
|
|
60554
|
+
const v = str(holder[KEY]).trim();
|
|
60555
|
+
if (!v || /\$\{\{/.test(v)) return false;
|
|
60556
|
+
return !/^(1|true|yes|on)$/i.test(v);
|
|
60557
|
+
});
|
|
60558
|
+
}
|
|
60525
60559
|
function agentActionsPinned(steps) {
|
|
60526
60560
|
const agent = steps.filter(isAgentStep).filter((s) => s.uses);
|
|
60527
60561
|
if (agent.length === 0) return false;
|
|
@@ -60641,6 +60675,19 @@ function analyzeWorkflow(path80, content) {
|
|
|
60641
60675
|
signals.push('allowed_non_write_users: "*" with github_token \u2014 any user can trigger the agent');
|
|
60642
60676
|
if (elevated) signals.push("elevated permissions (contents/id-token: write)");
|
|
60643
60677
|
if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
|
|
60678
|
+
const scopedPairs = allSteps(wf).filter(
|
|
60679
|
+
(p) => isAgentStep(p.step) && (injJobs.length === 0 || injJobs.includes(p.job))
|
|
60680
|
+
);
|
|
60681
|
+
const fullOutput = showsFullOutput(scopedPairs.map((p) => p.step));
|
|
60682
|
+
const scrubOff = envScrubOptedOut(scopedPairs, wf);
|
|
60683
|
+
if (fullOutput)
|
|
60684
|
+
signals.push(
|
|
60685
|
+
"`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"
|
|
60686
|
+
);
|
|
60687
|
+
if (scrubOff)
|
|
60688
|
+
signals.push(
|
|
60689
|
+
"`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
|
+
);
|
|
60644
60691
|
if (!gate && reach > 0) signals.push("no effective actor gate");
|
|
60645
60692
|
const noExplicitPerms = wf.permissions == null && jobList(wf).every((j) => j.permissions == null);
|
|
60646
60693
|
if (noExplicitPerms && reach > 0 && (broadTools || githubWriteTool))
|
|
@@ -60661,7 +60708,7 @@ function analyzeWorkflow(path80, content) {
|
|
|
60661
60708
|
'allowed_non_write_users: "*" is set without github_token, so the action keeps its default write gate'
|
|
60662
60709
|
);
|
|
60663
60710
|
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
|
-
|
|
60711
|
+
const finding = {
|
|
60665
60712
|
check: "CI-2",
|
|
60666
60713
|
// One verdict per workflow file — the finding IS the file's reachability score.
|
|
60667
60714
|
rule: "CI-2.injectable-workflow",
|
|
@@ -60673,13 +60720,20 @@ function analyzeWorkflow(path80, content) {
|
|
|
60673
60720
|
mitigations: mitigations.length ? mitigations : void 0,
|
|
60674
60721
|
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."
|
|
60675
60722
|
};
|
|
60723
|
+
const extraFix = [
|
|
60724
|
+
fullOutput ? "Remove `show_full_output: true` (the action documents it for debugging only)." : "",
|
|
60725
|
+
scrubOff ? "Remove the `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` opt-out." : ""
|
|
60726
|
+
].filter(Boolean).join(" ");
|
|
60727
|
+
if (extraFix) finding.fix = `${finding.fix} ${extraFix}`;
|
|
60728
|
+
return finding;
|
|
60676
60729
|
}
|
|
60677
60730
|
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;
|
|
60678
60731
|
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;
|
|
60679
60732
|
function fuelSecretNames(agentSteps) {
|
|
60680
60733
|
const out = /* @__PURE__ */ new Set();
|
|
60681
60734
|
const add = (v) => {
|
|
60682
|
-
for (const m of str(v).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi))
|
|
60735
|
+
for (const m of dropDeadSecretRefs(str(v)).matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi))
|
|
60736
|
+
out.add(m[1]);
|
|
60683
60737
|
};
|
|
60684
60738
|
for (const st of agentSteps) {
|
|
60685
60739
|
for (const [k, v] of Object.entries(st.with ?? {})) if (FUEL_INPUT_KEYS.test(k)) add(v);
|
|
@@ -60701,7 +60755,7 @@ function agentReachableSecrets(wf, agentSteps, agentJobs) {
|
|
|
60701
60755
|
blobs.push(str(wf.env));
|
|
60702
60756
|
const fuel = fuelSecretNames(agentSteps);
|
|
60703
60757
|
const found = /* @__PURE__ */ new Map();
|
|
60704
|
-
for (const b of blobs) {
|
|
60758
|
+
for (const b of blobs.map(dropDeadSecretRefs)) {
|
|
60705
60759
|
for (const m of b.matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/gi)) {
|
|
60706
60760
|
const name = m[1];
|
|
60707
60761
|
if (AGENT_FUEL_RE.test(name) || fuel.has(name)) continue;
|
|
@@ -60829,19 +60883,35 @@ function analyzeAgentConfig(path80, content) {
|
|
|
60829
60883
|
);
|
|
60830
60884
|
if (broad.length > 0) {
|
|
60831
60885
|
const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
|
|
60886
|
+
const bashBackstop = deny.some((d) => /^Bash\b/.test(d));
|
|
60887
|
+
const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(a));
|
|
60888
|
+
const high = bareShell && !bashBackstop;
|
|
60889
|
+
const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
|
|
60890
|
+
if (high)
|
|
60891
|
+
signals.push(
|
|
60892
|
+
"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"
|
|
60893
|
+
);
|
|
60894
|
+
else if (bareShell)
|
|
60895
|
+
signals.push(
|
|
60896
|
+
"a `Bash` deny list narrows the unrestricted `Bash` allow; it blocks only the commands it names"
|
|
60897
|
+
);
|
|
60898
|
+
if (broad.some((a) => /^Bash\(git:/.test(a)))
|
|
60899
|
+
signals.push(
|
|
60900
|
+
"`Bash(git:*)` pre-approves every git command; git can run other programs (`-c core.pager=\u2026`, `!` aliases), so this is broader than it looks"
|
|
60901
|
+
);
|
|
60902
|
+
if (broad.some((a) => /^Write|^Edit$/.test(a)))
|
|
60903
|
+
signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
|
|
60904
|
+
if (!high && !bareShell && !hasBackstop) signals.push("no `deny` entry narrows these grants");
|
|
60832
60905
|
findings.push({
|
|
60833
60906
|
check: "CI-1",
|
|
60834
60907
|
rule: "CI-1.broad-allow",
|
|
60835
60908
|
// File-level: one finding per config file. Adding a SECOND broad allow makes the
|
|
60836
60909
|
// same statement about the same file, so it must not read as a new finding.
|
|
60837
60910
|
dimension: "toolRules",
|
|
60838
|
-
severity:
|
|
60839
|
-
title:
|
|
60911
|
+
severity: high ? "high" : "medium",
|
|
60912
|
+
title: high ? "Committed agent config pre-authorizes broad tools with no deny backstop" : "Committed agent config pre-authorizes broad tools",
|
|
60840
60913
|
file: path80,
|
|
60841
|
-
signals
|
|
60842
|
-
`broad allow(s): ${broad.slice(0, 5).join(", ")}`,
|
|
60843
|
-
hasBackstop ? "a `deny` list backstops the broad allow" : "no `deny` entry covers Bash/Write/Edit \u2014 every contributor is pre-authorized for catastrophic tools"
|
|
60844
|
-
],
|
|
60914
|
+
signals,
|
|
60845
60915
|
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."
|
|
60846
60916
|
});
|
|
60847
60917
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.24.0",
|
|
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",
|