@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.
- package/dist/cli.js +84 -25
- package/dist/cli.mjs +84 -25
- 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 =
|
|
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
|
|
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) ||
|
|
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
|
-
|
|
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) + (
|
|
53238
|
-
const
|
|
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 &&
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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) ||
|
|
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
|
-
|
|
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) + (
|
|
53231
|
-
const
|
|
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 &&
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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",
|