@node9/proxy 1.58.1 → 1.58.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.
Files changed (3) hide show
  1. package/dist/cli.js +84 -25
  2. package/dist/cli.mjs +84 -25
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -52950,7 +52950,25 @@ var import_chalk30 = __toESM(require("chalk"));
52950
52950
  // src/ci-check/fetch.ts
52951
52951
  var import_fs59 = __toESM(require("fs"));
52952
52952
  var import_path56 = __toESM(require("path"));
52953
+ var import_node_child_process = require("child_process");
52953
52954
  var import_undici = __toESM(require_undici());
52955
+ var cachedGhToken;
52956
+ function resolveGitHubToken() {
52957
+ const env = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
52958
+ if (env) return env;
52959
+ if (cachedGhToken === void 0) {
52960
+ try {
52961
+ cachedGhToken = (0, import_node_child_process.execFileSync)("gh", ["auth", "token"], {
52962
+ encoding: "utf8",
52963
+ stdio: ["ignore", "pipe", "ignore"],
52964
+ timeout: 3e3
52965
+ }).trim() || null;
52966
+ } catch {
52967
+ cachedGhToken = null;
52968
+ }
52969
+ }
52970
+ return cachedGhToken ?? void 0;
52971
+ }
52954
52972
  var SURFACE_FILES = [
52955
52973
  ".claude/settings.json",
52956
52974
  ".claude/settings.local.json",
@@ -52996,7 +53014,7 @@ function ghHeaders() {
52996
53014
  "User-Agent": "node9-scan-repo",
52997
53015
  "X-GitHub-Api-Version": "2022-11-28"
52998
53016
  };
52999
- const tok = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
53017
+ const tok = resolveGitHubToken();
53000
53018
  if (tok) h.Authorization = `Bearer ${tok}`;
53001
53019
  return h;
53002
53020
  }
@@ -53019,7 +53037,7 @@ async function ghGet(url) {
53019
53037
  }
53020
53038
  return { status: res.statusCode, json };
53021
53039
  }
53022
- var RATE_LIMIT_NOTE = "GitHub rate limit hit \u2014 results may be INCOMPLETE (a missing file could be unread, not absent). Set GITHUB_TOKEN.";
53040
+ var RATE_LIMIT_NOTE = "GitHub rate limit hit \u2014 results may be INCOMPLETE (a missing file could be unread, not absent). Set GITHUB_TOKEN or run `gh auth login`.";
53023
53041
  var NETWORK_NOTE = "A network error/timeout occurred \u2014 results may be INCOMPLETE (some files were not fetched).";
53024
53042
  async function fetchOne(owner, repo, filePath, notes) {
53025
53043
  const url = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`;
@@ -53109,8 +53127,20 @@ async function fetchTree(input, onProgress) {
53109
53127
 
53110
53128
  // src/ci-check/workflows.ts
53111
53129
  var import_yaml = require("yaml");
53130
+
53131
+ // src/ci-check/types.ts
53132
+ var SEVERITY_RANK2 = {
53133
+ critical: 4,
53134
+ high: 3,
53135
+ medium: 2,
53136
+ advisory: 1
53137
+ };
53138
+
53139
+ // src/ci-check/workflows.ts
53112
53140
  var AGENT_ACTION_RE = /(anthropics\/claude-code(-base)?-action|anthropics\/claude-code|openai\/codex|codex-action|run-?aider|aider-?action|google-github-actions\/run-gemini)/i;
53113
53141
  var BROAD_TOOL_RE = /(^|["\s,])(Bash|Write|Edit)(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|git push|git commit|git config|git:|rm|sh|bash|eval|npx|pip)/i;
53142
+ var EXFIL_RCE_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|sh[\s):]|eval|rm[\s):]|git push)/i;
53143
+ var GH_WRITE_TOOL_RE = /Bash\(\s*(gh api|gh:|gh (pr|issue) (comment|edit|merge|close|review|create|ready|lock|reopen)|git push|git:)/i;
53114
53144
  function str(v) {
53115
53145
  return typeof v === "string" ? v : v == null ? "" : JSON.stringify(v);
53116
53146
  }
@@ -53135,11 +53165,12 @@ function isAgentStep(step) {
53135
53165
  return false;
53136
53166
  }
53137
53167
  function collectTools(steps) {
53168
+ const stripDeny = (x) => x.replace(/--disallowed[-_]?tools\s+("[^"]*"|'[^']*'|\S+)/gi, " ").replace(/["']disallowed[_]?[tT]ools["']\s*:\s*\[[^\]]*\]/g, " ");
53138
53169
  let s = "";
53139
53170
  for (const st of steps) {
53140
53171
  const w = st.with ?? {};
53141
- s += " " + str(w["claude_args"]) + " " + str(w["allowed_tools"]) + " " + str(w["allowedTools"]);
53142
- s += " " + str(w["settings"]);
53172
+ s += " " + stripDeny(str(w["claude_args"])) + " " + str(w["allowed_tools"]) + " " + str(w["allowedTools"]);
53173
+ s += " " + stripDeny(str(w["settings"]));
53143
53174
  }
53144
53175
  return s;
53145
53176
  }
@@ -53177,12 +53208,22 @@ function hasActorGate(wf, raw) {
53177
53208
  const labelGated = !!labeled && /event\.label|label\.name/i.test(ifs);
53178
53209
  return gated || labelGated;
53179
53210
  }
53180
- function permsElevated(wf) {
53211
+ function hasImplicitActorGate(agentSteps, bypassActive) {
53212
+ if (bypassActive) return false;
53213
+ return agentSteps.some((s) => /anthropics\/claude-code-action@/i.test(s.uses ?? ""));
53214
+ }
53215
+ function permsElevated(wf, agentJobs) {
53181
53216
  const check = (p) => {
53182
53217
  const s = str(p);
53183
53218
  return /["']?(contents|id-token|packages)["']?\s*:\s*["']?write/i.test(s);
53184
53219
  };
53185
- return check(wf.permissions) || Object.values(wf.jobs ?? {}).some((j) => check(j.permissions));
53220
+ return check(wf.permissions) || agentJobs.some((j) => check(j.permissions));
53221
+ }
53222
+ function hasGithubWritePerm(wf, agentJobs) {
53223
+ const check = (p) => /["']?(contents|pull-requests|issues|packages|actions|deployments)["']?\s*:\s*["']?write/i.test(
53224
+ str(p)
53225
+ );
53226
+ return check(wf.permissions) || agentJobs.some((j) => check(j.permissions));
53186
53227
  }
53187
53228
  function usesPat(wf) {
53188
53229
  for (const { step } of allSteps(wf)) {
@@ -53216,6 +53257,11 @@ function analyzeWorkflow(path70, content) {
53216
53257
  const wf = raw;
53217
53258
  const steps = allSteps(wf).map((s) => s.step);
53218
53259
  const agentSteps = steps.filter(isAgentStep);
53260
+ const agentJobs = [
53261
+ ...new Set(
53262
+ allSteps(wf).filter((s) => isAgentStep(s.step)).map((s) => s.job)
53263
+ )
53264
+ ];
53219
53265
  if (agentSteps.length === 0) return null;
53220
53266
  const triggers = triggerKeys(wf, raw);
53221
53267
  const secretExposed = triggers.some((t) => /pull_request_target|workflow_run/i.test(t));
@@ -53223,19 +53269,23 @@ function analyzeWorkflow(path70, content) {
53223
53269
  const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
53224
53270
  const nonWriteStar = nonWrite === "*";
53225
53271
  const nonWriteList = !!nonWrite && nonWrite !== "*";
53272
+ const untrustedTrigger = secretExposed || forkInput;
53273
+ const bypassActive = nonWriteStar && untrustedTrigger;
53226
53274
  const head = untrustedHeadCheckout(wf);
53227
53275
  const promptUntrusted = promptTakesUntrusted(agentSteps);
53228
53276
  const reach = Math.max(
53229
53277
  head === "root" ? 3 : head === "subdir" ? 1 : 0,
53230
53278
  promptUntrusted ? 2 : 0,
53231
- nonWriteStar ? 2 : 0
53279
+ bypassActive ? 2 : 0
53232
53280
  );
53233
53281
  const toolsBlob = collectTools(agentSteps);
53234
53282
  const broadTools = BROAD_TOOL_RE.test(toolsBlob);
53235
- const elevated = permsElevated(wf);
53283
+ const elevated = permsElevated(wf, agentJobs);
53236
53284
  const pat = usesPat(wf);
53237
- const power = (broadTools ? 2 : 0) + (nonWriteStar ? 1 : 0) + (elevated ? 1 : 0) + (pat ? 1 : 0);
53238
- const gate = hasActorGate(wf, raw);
53285
+ const power = (broadTools ? 2 : 0) + (bypassActive ? 1 : 0) + (elevated ? 1 : 0) + (pat ? 1 : 0);
53286
+ const explicitGate = hasActorGate(wf, raw);
53287
+ const implicitGate = hasImplicitActorGate(agentSteps, bypassActive);
53288
+ const gate = explicitGate || implicitGate;
53239
53289
  const envDeny = hasEnvDeny(agentSteps);
53240
53290
  const pinned = agentActionsPinned(agentSteps);
53241
53291
  let score = reach + power;
@@ -53245,7 +53295,14 @@ function analyzeWorkflow(path70, content) {
53245
53295
  score = Math.max(0, score);
53246
53296
  if (score === 0 && !secretExposed) return null;
53247
53297
  let severity = severityFromScore(score);
53248
- if (gate && reach > 0 && severity && severity !== "advisory") severity = "advisory";
53298
+ if (gate && severity && severity !== "advisory") severity = "advisory";
53299
+ if (reach === 0 && severity && severity !== "advisory") severity = "advisory";
53300
+ if (!broadTools && severity && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium)
53301
+ severity = "medium";
53302
+ const exfilOrRce = EXFIL_RCE_RE.test(toolsBlob);
53303
+ const githubWriteTool = GH_WRITE_TOOL_RE.test(toolsBlob);
53304
+ const canDamage = exfilOrRce || (hasGithubWritePerm(wf, agentJobs) || pat) && githubWriteTool;
53305
+ if (!canDamage && severity && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
53249
53306
  if (!severity) severity = "advisory";
53250
53307
  const signals = [];
53251
53308
  if (secretExposed)
@@ -53257,12 +53314,14 @@ function analyzeWorkflow(path70, content) {
53257
53314
  if (head === "subdir") signals.push("checks out the untrusted PR head into an isolated subdir");
53258
53315
  if (promptUntrusted) signals.push("feeds untrusted PR/issue text to the agent");
53259
53316
  if (broadTools) signals.push("agent has broad/write-capable tools (Bash/Write/curl/git push)");
53260
- if (nonWriteStar) signals.push('allowed_non_write_users: "*" \u2014 any user can trigger the agent');
53317
+ if (bypassActive) signals.push('allowed_non_write_users: "*" \u2014 any user can trigger the agent');
53261
53318
  if (elevated) signals.push("elevated permissions (contents/id-token: write)");
53262
53319
  if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
53263
53320
  if (!gate && reach > 0) signals.push("no effective actor gate");
53264
53321
  const mitigations = [];
53265
- if (gate) mitigations.push("actor-gated (maintainer/label/write-user required)");
53322
+ if (explicitGate) mitigations.push("actor-gated (maintainer/label/write-user required)");
53323
+ else if (implicitGate)
53324
+ mitigations.push("claude-code-action gates the agent to write-access users by default");
53266
53325
  if (head === "subdir") mitigations.push("untrusted head isolated in a subdir, not root");
53267
53326
  if (envDeny) mitigations.push("secrets env-denied from the agent subprocess");
53268
53327
  if (pinned) mitigations.push("agent action pinned to a commit SHA");
@@ -53389,14 +53448,6 @@ function analyzeMcp(path70, content) {
53389
53448
  return findings;
53390
53449
  }
53391
53450
 
53392
- // src/ci-check/types.ts
53393
- var SEVERITY_RANK2 = {
53394
- critical: 4,
53395
- high: 3,
53396
- medium: 2,
53397
- advisory: 1
53398
- };
53399
-
53400
53451
  // src/ci-check/index.ts
53401
53452
  function worstOf(findings) {
53402
53453
  let worst = null;
@@ -53427,7 +53478,8 @@ function scanTree(tree) {
53427
53478
  findings.sort(
53428
53479
  (a, b) => SEVERITY_RANK2[b.severity] - SEVERITY_RANK2[a.severity] || a.file.localeCompare(b.file)
53429
53480
  );
53430
- return { source: tree.source, findings, inspected, notes, worst: worstOf(findings) };
53481
+ const incomplete = notes.some((nt) => /may be INCOMPLETE/i.test(nt));
53482
+ return { source: tree.source, findings, inspected, notes, worst: worstOf(findings), incomplete };
53431
53483
  }
53432
53484
  async function scanRepo(input, onProgress) {
53433
53485
  const tree = await fetchTree(input, onProgress);
@@ -53454,9 +53506,15 @@ function ownedHint(source) {
53454
53506
  function renderScan(res) {
53455
53507
  const L = [];
53456
53508
  const n = res.findings.length;
53457
- const head = res.worst === "critical" || res.worst === "high" ? import_chalk29.default.red.bold("\u26A0\uFE0F agent-security risk found") : res.worst ? import_chalk29.default.yellow("agent-security notes") : import_chalk29.default.green("\u2705 agent-security: clean");
53509
+ const head = res.worst === "critical" || res.worst === "high" ? import_chalk29.default.red.bold("\u26A0\uFE0F agent-security risk found") : res.worst ? import_chalk29.default.yellow("agent-security notes") : res.incomplete ? import_chalk29.default.yellow.bold("\u26A0\uFE0F INCOMPLETE \u2014 could not read all files") : import_chalk29.default.green("\u2705 agent-security: clean");
53458
53510
  L.push(`\u{1F6E1}\uFE0F ${import_chalk29.default.bold("node9 scan-repo")} \xB7 ${res.source} \xB7 ${head}`);
53459
53511
  L.push(import_chalk29.default.gray(` inspected ${res.inspected.length} config file(s), ${n} finding(s)`));
53512
+ if (res.incomplete)
53513
+ L.push(
53514
+ import_chalk29.default.yellow.bold(
53515
+ " \u26A0\uFE0F This scan was rate-limited and is NOT a clean bill of health \u2014 set GITHUB_TOKEN and re-run."
53516
+ )
53517
+ );
53460
53518
  L.push("");
53461
53519
  for (const f of res.findings) {
53462
53520
  L.push(
@@ -53468,7 +53526,7 @@ function renderScan(res) {
53468
53526
  L.push(import_chalk29.default.cyan(` \u2192 ${f.fix}`));
53469
53527
  L.push("");
53470
53528
  }
53471
- if (n === 0) {
53529
+ if (n === 0 && !res.incomplete) {
53472
53530
  L.push(
53473
53531
  import_chalk29.default.gray(" No committed agent hooks, injectable workflows, or unpinned MCP servers.")
53474
53532
  );
@@ -53488,7 +53546,7 @@ function renderScan(res) {
53488
53546
  }
53489
53547
  function renderScanMarkdown(res) {
53490
53548
  const L = [];
53491
- const status = res.worst === "critical" || res.worst === "high" ? "\u26A0\uFE0F" : res.worst ? "\u{1F7E1}" : "\u2705";
53549
+ const status = res.worst === "critical" || res.worst === "high" ? "\u26A0\uFE0F" : res.worst ? "\u{1F7E1}" : res.incomplete ? "\u26A0\uFE0F" : "\u2705";
53492
53550
  L.push(`### \u{1F6E1}\uFE0F node9 agent-security \xB7 \`${res.source}\` \xB7 ${status}`);
53493
53551
  L.push("");
53494
53552
  L.push(
@@ -53511,6 +53569,7 @@ function renderScanMarkdown(res) {
53511
53569
  function exitCodeFor(res) {
53512
53570
  if (res.worst === "critical" || res.worst === "high") return 2;
53513
53571
  if (res.worst === "medium") return 1;
53572
+ if (res.incomplete) return 3;
53514
53573
  return 0;
53515
53574
  }
53516
53575
 
package/dist/cli.mjs CHANGED
@@ -52944,6 +52944,24 @@ import chalk30 from "chalk";
52944
52944
  var import_undici = __toESM(require_undici());
52945
52945
  import fs60 from "fs";
52946
52946
  import path57 from "path";
52947
+ import { execFileSync as execFileSync2 } from "child_process";
52948
+ var cachedGhToken;
52949
+ function resolveGitHubToken() {
52950
+ const env = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
52951
+ if (env) return env;
52952
+ if (cachedGhToken === void 0) {
52953
+ try {
52954
+ cachedGhToken = execFileSync2("gh", ["auth", "token"], {
52955
+ encoding: "utf8",
52956
+ stdio: ["ignore", "pipe", "ignore"],
52957
+ timeout: 3e3
52958
+ }).trim() || null;
52959
+ } catch {
52960
+ cachedGhToken = null;
52961
+ }
52962
+ }
52963
+ return cachedGhToken ?? void 0;
52964
+ }
52947
52965
  var SURFACE_FILES = [
52948
52966
  ".claude/settings.json",
52949
52967
  ".claude/settings.local.json",
@@ -52989,7 +53007,7 @@ function ghHeaders() {
52989
53007
  "User-Agent": "node9-scan-repo",
52990
53008
  "X-GitHub-Api-Version": "2022-11-28"
52991
53009
  };
52992
- const tok = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
53010
+ const tok = resolveGitHubToken();
52993
53011
  if (tok) h.Authorization = `Bearer ${tok}`;
52994
53012
  return h;
52995
53013
  }
@@ -53012,7 +53030,7 @@ async function ghGet(url) {
53012
53030
  }
53013
53031
  return { status: res.statusCode, json };
53014
53032
  }
53015
- var RATE_LIMIT_NOTE = "GitHub rate limit hit \u2014 results may be INCOMPLETE (a missing file could be unread, not absent). Set GITHUB_TOKEN.";
53033
+ var RATE_LIMIT_NOTE = "GitHub rate limit hit \u2014 results may be INCOMPLETE (a missing file could be unread, not absent). Set GITHUB_TOKEN or run `gh auth login`.";
53016
53034
  var NETWORK_NOTE = "A network error/timeout occurred \u2014 results may be INCOMPLETE (some files were not fetched).";
53017
53035
  async function fetchOne(owner, repo, filePath, notes) {
53018
53036
  const url = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`;
@@ -53102,8 +53120,20 @@ async function fetchTree(input, onProgress) {
53102
53120
 
53103
53121
  // src/ci-check/workflows.ts
53104
53122
  import { parse as parseYaml } from "yaml";
53123
+
53124
+ // src/ci-check/types.ts
53125
+ var SEVERITY_RANK2 = {
53126
+ critical: 4,
53127
+ high: 3,
53128
+ medium: 2,
53129
+ advisory: 1
53130
+ };
53131
+
53132
+ // src/ci-check/workflows.ts
53105
53133
  var AGENT_ACTION_RE = /(anthropics\/claude-code(-base)?-action|anthropics\/claude-code|openai\/codex|codex-action|run-?aider|aider-?action|google-github-actions\/run-gemini)/i;
53106
53134
  var BROAD_TOOL_RE = /(^|["\s,])(Bash|Write|Edit)(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|git push|git commit|git config|git:|rm|sh|bash|eval|npx|pip)/i;
53135
+ var EXFIL_RCE_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|sh[\s):]|eval|rm[\s):]|git push)/i;
53136
+ var GH_WRITE_TOOL_RE = /Bash\(\s*(gh api|gh:|gh (pr|issue) (comment|edit|merge|close|review|create|ready|lock|reopen)|git push|git:)/i;
53107
53137
  function str(v) {
53108
53138
  return typeof v === "string" ? v : v == null ? "" : JSON.stringify(v);
53109
53139
  }
@@ -53128,11 +53158,12 @@ function isAgentStep(step) {
53128
53158
  return false;
53129
53159
  }
53130
53160
  function collectTools(steps) {
53161
+ const stripDeny = (x) => x.replace(/--disallowed[-_]?tools\s+("[^"]*"|'[^']*'|\S+)/gi, " ").replace(/["']disallowed[_]?[tT]ools["']\s*:\s*\[[^\]]*\]/g, " ");
53131
53162
  let s = "";
53132
53163
  for (const st of steps) {
53133
53164
  const w = st.with ?? {};
53134
- s += " " + str(w["claude_args"]) + " " + str(w["allowed_tools"]) + " " + str(w["allowedTools"]);
53135
- s += " " + str(w["settings"]);
53165
+ s += " " + stripDeny(str(w["claude_args"])) + " " + str(w["allowed_tools"]) + " " + str(w["allowedTools"]);
53166
+ s += " " + stripDeny(str(w["settings"]));
53136
53167
  }
53137
53168
  return s;
53138
53169
  }
@@ -53170,12 +53201,22 @@ function hasActorGate(wf, raw) {
53170
53201
  const labelGated = !!labeled && /event\.label|label\.name/i.test(ifs);
53171
53202
  return gated || labelGated;
53172
53203
  }
53173
- function permsElevated(wf) {
53204
+ function hasImplicitActorGate(agentSteps, bypassActive) {
53205
+ if (bypassActive) return false;
53206
+ return agentSteps.some((s) => /anthropics\/claude-code-action@/i.test(s.uses ?? ""));
53207
+ }
53208
+ function permsElevated(wf, agentJobs) {
53174
53209
  const check = (p) => {
53175
53210
  const s = str(p);
53176
53211
  return /["']?(contents|id-token|packages)["']?\s*:\s*["']?write/i.test(s);
53177
53212
  };
53178
- return check(wf.permissions) || Object.values(wf.jobs ?? {}).some((j) => check(j.permissions));
53213
+ return check(wf.permissions) || agentJobs.some((j) => check(j.permissions));
53214
+ }
53215
+ function hasGithubWritePerm(wf, agentJobs) {
53216
+ const check = (p) => /["']?(contents|pull-requests|issues|packages|actions|deployments)["']?\s*:\s*["']?write/i.test(
53217
+ str(p)
53218
+ );
53219
+ return check(wf.permissions) || agentJobs.some((j) => check(j.permissions));
53179
53220
  }
53180
53221
  function usesPat(wf) {
53181
53222
  for (const { step } of allSteps(wf)) {
@@ -53209,6 +53250,11 @@ function analyzeWorkflow(path70, content) {
53209
53250
  const wf = raw;
53210
53251
  const steps = allSteps(wf).map((s) => s.step);
53211
53252
  const agentSteps = steps.filter(isAgentStep);
53253
+ const agentJobs = [
53254
+ ...new Set(
53255
+ allSteps(wf).filter((s) => isAgentStep(s.step)).map((s) => s.job)
53256
+ )
53257
+ ];
53212
53258
  if (agentSteps.length === 0) return null;
53213
53259
  const triggers = triggerKeys(wf, raw);
53214
53260
  const secretExposed = triggers.some((t) => /pull_request_target|workflow_run/i.test(t));
@@ -53216,19 +53262,23 @@ function analyzeWorkflow(path70, content) {
53216
53262
  const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
53217
53263
  const nonWriteStar = nonWrite === "*";
53218
53264
  const nonWriteList = !!nonWrite && nonWrite !== "*";
53265
+ const untrustedTrigger = secretExposed || forkInput;
53266
+ const bypassActive = nonWriteStar && untrustedTrigger;
53219
53267
  const head = untrustedHeadCheckout(wf);
53220
53268
  const promptUntrusted = promptTakesUntrusted(agentSteps);
53221
53269
  const reach = Math.max(
53222
53270
  head === "root" ? 3 : head === "subdir" ? 1 : 0,
53223
53271
  promptUntrusted ? 2 : 0,
53224
- nonWriteStar ? 2 : 0
53272
+ bypassActive ? 2 : 0
53225
53273
  );
53226
53274
  const toolsBlob = collectTools(agentSteps);
53227
53275
  const broadTools = BROAD_TOOL_RE.test(toolsBlob);
53228
- const elevated = permsElevated(wf);
53276
+ const elevated = permsElevated(wf, agentJobs);
53229
53277
  const pat = usesPat(wf);
53230
- const power = (broadTools ? 2 : 0) + (nonWriteStar ? 1 : 0) + (elevated ? 1 : 0) + (pat ? 1 : 0);
53231
- const gate = hasActorGate(wf, raw);
53278
+ const power = (broadTools ? 2 : 0) + (bypassActive ? 1 : 0) + (elevated ? 1 : 0) + (pat ? 1 : 0);
53279
+ const explicitGate = hasActorGate(wf, raw);
53280
+ const implicitGate = hasImplicitActorGate(agentSteps, bypassActive);
53281
+ const gate = explicitGate || implicitGate;
53232
53282
  const envDeny = hasEnvDeny(agentSteps);
53233
53283
  const pinned = agentActionsPinned(agentSteps);
53234
53284
  let score = reach + power;
@@ -53238,7 +53288,14 @@ function analyzeWorkflow(path70, content) {
53238
53288
  score = Math.max(0, score);
53239
53289
  if (score === 0 && !secretExposed) return null;
53240
53290
  let severity = severityFromScore(score);
53241
- if (gate && reach > 0 && severity && severity !== "advisory") severity = "advisory";
53291
+ if (gate && severity && severity !== "advisory") severity = "advisory";
53292
+ if (reach === 0 && severity && severity !== "advisory") severity = "advisory";
53293
+ if (!broadTools && severity && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium)
53294
+ severity = "medium";
53295
+ const exfilOrRce = EXFIL_RCE_RE.test(toolsBlob);
53296
+ const githubWriteTool = GH_WRITE_TOOL_RE.test(toolsBlob);
53297
+ const canDamage = exfilOrRce || (hasGithubWritePerm(wf, agentJobs) || pat) && githubWriteTool;
53298
+ if (!canDamage && severity && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
53242
53299
  if (!severity) severity = "advisory";
53243
53300
  const signals = [];
53244
53301
  if (secretExposed)
@@ -53250,12 +53307,14 @@ function analyzeWorkflow(path70, content) {
53250
53307
  if (head === "subdir") signals.push("checks out the untrusted PR head into an isolated subdir");
53251
53308
  if (promptUntrusted) signals.push("feeds untrusted PR/issue text to the agent");
53252
53309
  if (broadTools) signals.push("agent has broad/write-capable tools (Bash/Write/curl/git push)");
53253
- if (nonWriteStar) signals.push('allowed_non_write_users: "*" \u2014 any user can trigger the agent');
53310
+ if (bypassActive) signals.push('allowed_non_write_users: "*" \u2014 any user can trigger the agent');
53254
53311
  if (elevated) signals.push("elevated permissions (contents/id-token: write)");
53255
53312
  if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
53256
53313
  if (!gate && reach > 0) signals.push("no effective actor gate");
53257
53314
  const mitigations = [];
53258
- if (gate) mitigations.push("actor-gated (maintainer/label/write-user required)");
53315
+ if (explicitGate) mitigations.push("actor-gated (maintainer/label/write-user required)");
53316
+ else if (implicitGate)
53317
+ mitigations.push("claude-code-action gates the agent to write-access users by default");
53259
53318
  if (head === "subdir") mitigations.push("untrusted head isolated in a subdir, not root");
53260
53319
  if (envDeny) mitigations.push("secrets env-denied from the agent subprocess");
53261
53320
  if (pinned) mitigations.push("agent action pinned to a commit SHA");
@@ -53382,14 +53441,6 @@ function analyzeMcp(path70, content) {
53382
53441
  return findings;
53383
53442
  }
53384
53443
 
53385
- // src/ci-check/types.ts
53386
- var SEVERITY_RANK2 = {
53387
- critical: 4,
53388
- high: 3,
53389
- medium: 2,
53390
- advisory: 1
53391
- };
53392
-
53393
53444
  // src/ci-check/index.ts
53394
53445
  function worstOf(findings) {
53395
53446
  let worst = null;
@@ -53420,7 +53471,8 @@ function scanTree(tree) {
53420
53471
  findings.sort(
53421
53472
  (a, b) => SEVERITY_RANK2[b.severity] - SEVERITY_RANK2[a.severity] || a.file.localeCompare(b.file)
53422
53473
  );
53423
- return { source: tree.source, findings, inspected, notes, worst: worstOf(findings) };
53474
+ const incomplete = notes.some((nt) => /may be INCOMPLETE/i.test(nt));
53475
+ return { source: tree.source, findings, inspected, notes, worst: worstOf(findings), incomplete };
53424
53476
  }
53425
53477
  async function scanRepo(input, onProgress) {
53426
53478
  const tree = await fetchTree(input, onProgress);
@@ -53447,9 +53499,15 @@ function ownedHint(source) {
53447
53499
  function renderScan(res) {
53448
53500
  const L = [];
53449
53501
  const n = res.findings.length;
53450
- const head = res.worst === "critical" || res.worst === "high" ? chalk29.red.bold("\u26A0\uFE0F agent-security risk found") : res.worst ? chalk29.yellow("agent-security notes") : chalk29.green("\u2705 agent-security: clean");
53502
+ const head = res.worst === "critical" || res.worst === "high" ? chalk29.red.bold("\u26A0\uFE0F agent-security risk found") : res.worst ? chalk29.yellow("agent-security notes") : res.incomplete ? chalk29.yellow.bold("\u26A0\uFE0F INCOMPLETE \u2014 could not read all files") : chalk29.green("\u2705 agent-security: clean");
53451
53503
  L.push(`\u{1F6E1}\uFE0F ${chalk29.bold("node9 scan-repo")} \xB7 ${res.source} \xB7 ${head}`);
53452
53504
  L.push(chalk29.gray(` inspected ${res.inspected.length} config file(s), ${n} finding(s)`));
53505
+ if (res.incomplete)
53506
+ L.push(
53507
+ chalk29.yellow.bold(
53508
+ " \u26A0\uFE0F This scan was rate-limited and is NOT a clean bill of health \u2014 set GITHUB_TOKEN and re-run."
53509
+ )
53510
+ );
53453
53511
  L.push("");
53454
53512
  for (const f of res.findings) {
53455
53513
  L.push(
@@ -53461,7 +53519,7 @@ function renderScan(res) {
53461
53519
  L.push(chalk29.cyan(` \u2192 ${f.fix}`));
53462
53520
  L.push("");
53463
53521
  }
53464
- if (n === 0) {
53522
+ if (n === 0 && !res.incomplete) {
53465
53523
  L.push(
53466
53524
  chalk29.gray(" No committed agent hooks, injectable workflows, or unpinned MCP servers.")
53467
53525
  );
@@ -53481,7 +53539,7 @@ function renderScan(res) {
53481
53539
  }
53482
53540
  function renderScanMarkdown(res) {
53483
53541
  const L = [];
53484
- const status = res.worst === "critical" || res.worst === "high" ? "\u26A0\uFE0F" : res.worst ? "\u{1F7E1}" : "\u2705";
53542
+ const status = res.worst === "critical" || res.worst === "high" ? "\u26A0\uFE0F" : res.worst ? "\u{1F7E1}" : res.incomplete ? "\u26A0\uFE0F" : "\u2705";
53485
53543
  L.push(`### \u{1F6E1}\uFE0F node9 agent-security \xB7 \`${res.source}\` \xB7 ${status}`);
53486
53544
  L.push("");
53487
53545
  L.push(
@@ -53504,6 +53562,7 @@ function renderScanMarkdown(res) {
53504
53562
  function exitCodeFor(res) {
53505
53563
  if (res.worst === "critical" || res.worst === "high") return 2;
53506
53564
  if (res.worst === "medium") return 1;
53565
+ if (res.incomplete) return 3;
53507
53566
  return 0;
53508
53567
  }
53509
53568
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.58.1",
3
+ "version": "1.58.3",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",