@node9/proxy 1.59.0 → 1.60.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 +382 -75
- package/dist/cli.mjs +382 -75
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -52978,6 +52978,28 @@ var SURFACE_FILES = [
|
|
|
52978
52978
|
".clinerules"
|
|
52979
52979
|
];
|
|
52980
52980
|
var WORKFLOW_DIR = ".github/workflows";
|
|
52981
|
+
var SURFACE_BASENAME = /(^|\/)(CLAUDE|AGENTS|GEMINI)\.md$|(^|\/)\.cursorrules$|(^|\/)\.(windsurf|cline)rules$|(^|\/)copilot-instructions\.md$|(^|\/)\.claude\/settings(\.local)?\.json$|(^|\/)\.mcp\.json$|(^|\/)\.cursor\/mcp\.json$|(^|\/)\.codex\/config\.toml$/;
|
|
52982
|
+
var IGNORE_HARD = /(^|\/)(node_modules|vendor|\.git|\.next|\.venv|site-packages)\//;
|
|
52983
|
+
var IGNORE_SOFT = /(^|\/)(dist|build|out|target)\//;
|
|
52984
|
+
var isIgnoredDir = (relSlash) => IGNORE_HARD.test(relSlash) || IGNORE_SOFT.test(relSlash);
|
|
52985
|
+
var MAX_SURFACE_FILES = 200;
|
|
52986
|
+
function pickSurfacePaths(paths, truncated, notes) {
|
|
52987
|
+
const surface = paths.filter((p) => SURFACE_BASENAME.test(p) && !IGNORE_HARD.test(p));
|
|
52988
|
+
const matched = surface.filter((p) => !IGNORE_SOFT.test(p));
|
|
52989
|
+
const softSkipped = surface.filter((p) => IGNORE_SOFT.test(p));
|
|
52990
|
+
const capped = matched.slice(0, MAX_SURFACE_FILES);
|
|
52991
|
+
if (truncated || matched.length > MAX_SURFACE_FILES) {
|
|
52992
|
+
notes.push(
|
|
52993
|
+
`repo tree is large/truncated \u2014 some agent-surface files may be INCOMPLETE (scanned ${capped.length} of ${matched.length}${truncated ? "+" : ""}).`
|
|
52994
|
+
);
|
|
52995
|
+
}
|
|
52996
|
+
if (softSkipped.length) {
|
|
52997
|
+
notes.push(
|
|
52998
|
+
`skipped ${softSkipped.length} agent-surface file(s) under a build-output dir (dist/build/out/target), e.g. ${softSkipped.slice(0, 3).join(", ")} \u2014 if any is a real committed config, move it out of the build dir to have it scanned.`
|
|
52999
|
+
);
|
|
53000
|
+
}
|
|
53001
|
+
return capped;
|
|
53002
|
+
}
|
|
52981
53003
|
async function pooled(items, limit, fn) {
|
|
52982
53004
|
const out = new Array(items.length);
|
|
52983
53005
|
let next = 0;
|
|
@@ -53069,13 +53091,37 @@ async function listWorkflowPaths(owner, repo, notes) {
|
|
|
53069
53091
|
if (status !== 200 || !Array.isArray(json)) return [];
|
|
53070
53092
|
return json.filter((e) => e.type === "file" && /\.ya?ml$/.test(e.name)).map((e) => e.path);
|
|
53071
53093
|
}
|
|
53094
|
+
var ROOT_WORKFLOW_RE = /^\.github\/workflows\/[^/]+\.ya?ml$/;
|
|
53095
|
+
async function listSurfaceTree(owner, repo, notes) {
|
|
53096
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/HEAD?recursive=1`;
|
|
53097
|
+
const { status, json } = await ghGet(url);
|
|
53098
|
+
if (status === 403 || status === 429) {
|
|
53099
|
+
if (!notes.includes(RATE_LIMIT_NOTE)) notes.push(RATE_LIMIT_NOTE);
|
|
53100
|
+
return null;
|
|
53101
|
+
}
|
|
53102
|
+
if (status === 0) {
|
|
53103
|
+
if (!notes.includes(NETWORK_NOTE)) notes.push(NETWORK_NOTE);
|
|
53104
|
+
return null;
|
|
53105
|
+
}
|
|
53106
|
+
if (status !== 200 || !json || typeof json !== "object") return null;
|
|
53107
|
+
const tree = json;
|
|
53108
|
+
if (!Array.isArray(tree.tree)) return null;
|
|
53109
|
+
const blobs = tree.tree.filter((e) => e.type === "blob" && typeof e.path === "string").map((e) => e.path);
|
|
53110
|
+
return {
|
|
53111
|
+
surface: pickSurfacePaths(blobs, !!tree.truncated, notes),
|
|
53112
|
+
workflows: blobs.filter((p) => ROOT_WORKFLOW_RE.test(p))
|
|
53113
|
+
};
|
|
53114
|
+
}
|
|
53072
53115
|
var FETCH_CONCURRENCY = 8;
|
|
53073
53116
|
async function fetchGitHubTree(owner, repo, onProgress) {
|
|
53074
53117
|
const notes = [];
|
|
53075
53118
|
try {
|
|
53076
|
-
onProgress?.({ phase: "
|
|
53077
|
-
const
|
|
53078
|
-
const
|
|
53119
|
+
onProgress?.({ phase: "discovering agent surface", done: 0, total: 1 });
|
|
53120
|
+
const discovered = await listSurfaceTree(owner, repo, notes);
|
|
53121
|
+
const workflowPaths = discovered ? discovered.workflows : await listWorkflowPaths(owner, repo, notes);
|
|
53122
|
+
const allPaths = [
|
|
53123
|
+
.../* @__PURE__ */ new Set([...SURFACE_FILES, ...discovered?.surface ?? [], ...workflowPaths])
|
|
53124
|
+
];
|
|
53079
53125
|
let done = 0;
|
|
53080
53126
|
const fetched = await pooled(allPaths, FETCH_CONCURRENCY, async (p) => {
|
|
53081
53127
|
const f = await fetchOne(owner, repo, p, notes);
|
|
@@ -53084,7 +53130,9 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
53084
53130
|
});
|
|
53085
53131
|
return { source: `${owner}/${repo}`, files: fetched.filter((f) => !!f), notes };
|
|
53086
53132
|
} catch (err2) {
|
|
53087
|
-
notes.push(
|
|
53133
|
+
notes.push(
|
|
53134
|
+
`fetch degraded: ${err2?.message ?? "network error"} \u2014 results may be INCOMPLETE (the repo could not be fetched).`
|
|
53135
|
+
);
|
|
53088
53136
|
return { source: `${owner}/${repo}`, files: [], notes };
|
|
53089
53137
|
}
|
|
53090
53138
|
}
|
|
@@ -53101,7 +53149,42 @@ function readLocalTree(dir) {
|
|
|
53101
53149
|
} catch {
|
|
53102
53150
|
}
|
|
53103
53151
|
};
|
|
53104
|
-
|
|
53152
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53153
|
+
const collect = (rel) => {
|
|
53154
|
+
if (seen.has(rel)) return;
|
|
53155
|
+
seen.add(rel);
|
|
53156
|
+
add(rel);
|
|
53157
|
+
};
|
|
53158
|
+
for (const p of SURFACE_FILES) collect(p);
|
|
53159
|
+
const matches = [];
|
|
53160
|
+
const MAX_DIRS = 5e3;
|
|
53161
|
+
let dirsVisited = 0;
|
|
53162
|
+
const walk = (relDir) => {
|
|
53163
|
+
if (matches.length >= MAX_SURFACE_FILES || dirsVisited >= MAX_DIRS) return;
|
|
53164
|
+
dirsVisited++;
|
|
53165
|
+
let entries;
|
|
53166
|
+
try {
|
|
53167
|
+
entries = fs60.readdirSync(path57.join(root, relDir), { withFileTypes: true });
|
|
53168
|
+
} catch {
|
|
53169
|
+
return;
|
|
53170
|
+
}
|
|
53171
|
+
for (const e of entries) {
|
|
53172
|
+
if (matches.length >= MAX_SURFACE_FILES || dirsVisited >= MAX_DIRS) return;
|
|
53173
|
+
const rel = relDir ? `${relDir}/${e.name}` : e.name;
|
|
53174
|
+
if (e.isDirectory()) {
|
|
53175
|
+
if (isIgnoredDir(`${rel}/`)) continue;
|
|
53176
|
+
walk(rel);
|
|
53177
|
+
} else if (e.isFile() && SURFACE_BASENAME.test(rel)) {
|
|
53178
|
+
matches.push(rel);
|
|
53179
|
+
}
|
|
53180
|
+
}
|
|
53181
|
+
};
|
|
53182
|
+
walk("");
|
|
53183
|
+
if (matches.length >= MAX_SURFACE_FILES || dirsVisited >= MAX_DIRS)
|
|
53184
|
+
notes.push(
|
|
53185
|
+
`repo is large \u2014 some agent-surface files may be INCOMPLETE (capped at ${MAX_SURFACE_FILES} files / ${MAX_DIRS} dirs).`
|
|
53186
|
+
);
|
|
53187
|
+
for (const rel of matches) collect(rel);
|
|
53105
53188
|
const wfDir = path57.join(root, WORKFLOW_DIR);
|
|
53106
53189
|
try {
|
|
53107
53190
|
if (fs60.existsSync(wfDir)) {
|
|
@@ -53142,6 +53225,18 @@ var AGENT_ACTION_RE = /(anthropics\/claude-code(-base)?-action|anthropics\/claud
|
|
|
53142
53225
|
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;
|
|
53143
53226
|
var EXFIL_RCE_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|sh[\s):]|eval|rm[\s):]|git push)/i;
|
|
53144
53227
|
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;
|
|
53228
|
+
var EGRESS_TOOL_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\(\s*(curl|wget|python|python3|node|deno|bun|ruby|perl|php|pip|pipx|npx|nc|ncat|ssh|scp|http|gh api)|WebFetch|WebSearch/i;
|
|
53229
|
+
var MEMBERSHIP_CHECK_RE = /orgs\/[^/'"\s]+\/memberships\/|teams\/[^/'"\s]+\/memberships\/|getCollaboratorPermissionLevel|collaborators\/[^/'"\s]+\/permission|checkMembershipForUser|getMembershipForUser/i;
|
|
53230
|
+
var STRANGER_ISSUE_TYPES = /* @__PURE__ */ new Set(["opened", "edited", "reopened", "closed"]);
|
|
53231
|
+
var STRANGER_PR_TYPES = /* @__PURE__ */ new Set([
|
|
53232
|
+
"opened",
|
|
53233
|
+
"edited",
|
|
53234
|
+
"reopened",
|
|
53235
|
+
"closed",
|
|
53236
|
+
"synchronize",
|
|
53237
|
+
"ready_for_review",
|
|
53238
|
+
"converted_to_draft"
|
|
53239
|
+
]);
|
|
53145
53240
|
function str(v) {
|
|
53146
53241
|
return typeof v === "string" ? v : v == null ? "" : JSON.stringify(v);
|
|
53147
53242
|
}
|
|
@@ -53152,9 +53247,57 @@ function triggerKeys(wf, raw) {
|
|
|
53152
53247
|
if (on && typeof on === "object") return Object.keys(on);
|
|
53153
53248
|
return [];
|
|
53154
53249
|
}
|
|
53250
|
+
function onObject(wf, raw) {
|
|
53251
|
+
return wf.on ?? raw["on"] ?? raw[true];
|
|
53252
|
+
}
|
|
53253
|
+
function activityTypes(node) {
|
|
53254
|
+
return node && Array.isArray(node.types) ? node.types.map(String) : [];
|
|
53255
|
+
}
|
|
53256
|
+
function keyStrangerFirable(on, key) {
|
|
53257
|
+
switch (key) {
|
|
53258
|
+
case "issue_comment":
|
|
53259
|
+
case "pull_request_review":
|
|
53260
|
+
case "pull_request_review_comment":
|
|
53261
|
+
case "workflow_run":
|
|
53262
|
+
case "discussion":
|
|
53263
|
+
case "discussion_comment":
|
|
53264
|
+
case "fork":
|
|
53265
|
+
case "watch":
|
|
53266
|
+
case "public":
|
|
53267
|
+
return true;
|
|
53268
|
+
case "pull_request":
|
|
53269
|
+
case "pull_request_target": {
|
|
53270
|
+
const ts = activityTypes(on[key]);
|
|
53271
|
+
return ts.length ? ts.some((t) => STRANGER_PR_TYPES.has(t)) : true;
|
|
53272
|
+
}
|
|
53273
|
+
case "issues": {
|
|
53274
|
+
const ts = activityTypes(on["issues"]);
|
|
53275
|
+
return ts.length ? ts.some((t) => STRANGER_ISSUE_TYPES.has(t)) : true;
|
|
53276
|
+
}
|
|
53277
|
+
// workflow_call / workflow_dispatch / schedule / push / create / delete / …
|
|
53278
|
+
// are NOT stranger-firable (require write access, or run in a trusted context).
|
|
53279
|
+
default:
|
|
53280
|
+
return false;
|
|
53281
|
+
}
|
|
53282
|
+
}
|
|
53283
|
+
function triggerReach(wf, raw) {
|
|
53284
|
+
const on = onObject(wf, raw) ?? {};
|
|
53285
|
+
const keys = triggerKeys(wf, raw);
|
|
53286
|
+
const firable = keys.filter((k) => keyStrangerFirable(on, k));
|
|
53287
|
+
const untrusted = firable.length > 0;
|
|
53288
|
+
const reusable = !untrusted && keys.some((k) => /^workflow_call$/i.test(k));
|
|
53289
|
+
const secretExposed = firable.some((t) => /pull_request_target|workflow_run/i.test(t));
|
|
53290
|
+
const privileged = untrusted && firable.some(
|
|
53291
|
+
(t) => /pull_request_target|pull_request_review|workflow_run|issue|discussion/i.test(t)
|
|
53292
|
+
);
|
|
53293
|
+
return { keys, untrusted, reusable, secretExposed, privileged };
|
|
53294
|
+
}
|
|
53295
|
+
function jobList(wf) {
|
|
53296
|
+
return Object.values(wf.jobs ?? {}).filter((j) => j != null);
|
|
53297
|
+
}
|
|
53155
53298
|
function allSteps(wf) {
|
|
53156
53299
|
const out = [];
|
|
53157
|
-
for (const job of
|
|
53300
|
+
for (const job of jobList(wf)) {
|
|
53158
53301
|
for (const step of job.steps ?? []) out.push({ job, step });
|
|
53159
53302
|
}
|
|
53160
53303
|
return out;
|
|
@@ -53209,63 +53352,100 @@ function promptTakesUntrusted(steps) {
|
|
|
53209
53352
|
}
|
|
53210
53353
|
return false;
|
|
53211
53354
|
}
|
|
53212
|
-
var
|
|
53355
|
+
var NONCONTAINS_GATE_RE = new RegExp(
|
|
53356
|
+
[
|
|
53357
|
+
String.raw`==\s*['"]?(OWNER|MEMBER|COLLABORATOR)\b`,
|
|
53358
|
+
String.raw`==\s*['"](write|admin|maintain)`,
|
|
53359
|
+
String.raw`github\.actor\s*==`,
|
|
53360
|
+
String.raw`user\.login\s*==`,
|
|
53361
|
+
String.raw`head\.repo\.full_name\s*==\s*github\.repository`
|
|
53362
|
+
].join("|"),
|
|
53363
|
+
"i"
|
|
53364
|
+
);
|
|
53365
|
+
var PERMISSION_OUTPUT_GATE_RE = /steps\.[\w-]+\.outputs\.[\w-]*(permission|allowed|authoriz|is[_-]?(admin|member|maintainer|collaborator))[\w-]*\s*==\s*['"]?(true|admin|write|maintain)/i;
|
|
53366
|
+
var CONTAINS_GATE_RE = /contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
|
|
53367
|
+
var NEGATED_CONTAINS_RE = /!\s*\(?\s*contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
|
|
53213
53368
|
function labelTypeConfigured(wf, raw) {
|
|
53214
53369
|
const on = wf.on ?? raw["on"] ?? raw[true];
|
|
53215
53370
|
const prTypes = (t) => t && Array.isArray(t.types) ? t.types.map(String) : [];
|
|
53216
53371
|
return prTypes(on?.["pull_request_target"]).includes("labeled") || prTypes(on?.["pull_request"]).includes("labeled");
|
|
53217
53372
|
}
|
|
53218
53373
|
function ifsAreGated(ifs, labelConfigured) {
|
|
53219
|
-
const
|
|
53374
|
+
const containsGate = CONTAINS_GATE_RE.test(ifs) && !NEGATED_CONTAINS_RE.test(ifs);
|
|
53375
|
+
const gated = NONCONTAINS_GATE_RE.test(ifs) || containsGate;
|
|
53220
53376
|
const labelGated = labelConfigured && /event\.label|label\.name/i.test(ifs);
|
|
53221
53377
|
return gated || labelGated;
|
|
53222
53378
|
}
|
|
53223
53379
|
function hasActorGate(wf, raw) {
|
|
53224
|
-
const ifs = [
|
|
53225
|
-
wf.jobs ? Object.values(wf.jobs).map((j) => j.if) : [],
|
|
53226
|
-
allSteps(wf).map((s) => s.step.if)
|
|
53227
|
-
].flat().map(str).join(" ");
|
|
53380
|
+
const ifs = [jobList(wf).map((j) => j.if), allSteps(wf).map((s) => s.step.if)].flat().map(str).join(" ");
|
|
53228
53381
|
return ifsAreGated(ifs, labelTypeConfigured(wf, raw));
|
|
53229
53382
|
}
|
|
53383
|
+
function hasStepMembershipGate(job) {
|
|
53384
|
+
const steps = job.steps ?? [];
|
|
53385
|
+
const gateIds = steps.filter((s) => s.id && MEMBERSHIP_CHECK_RE.test(str(s.run))).map((s) => s.id);
|
|
53386
|
+
if (!gateIds.length) return false;
|
|
53387
|
+
return steps.some(
|
|
53388
|
+
(s) => isAgentStep(s) && gateIds.some(
|
|
53389
|
+
(id) => new RegExp(`steps\\.${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.outputs\\.`).test(
|
|
53390
|
+
str(s.if)
|
|
53391
|
+
)
|
|
53392
|
+
)
|
|
53393
|
+
);
|
|
53394
|
+
}
|
|
53230
53395
|
function jobActorGate(job, wf, raw) {
|
|
53231
53396
|
const ifs = [job.if, ...(job.steps ?? []).map((s) => s.if)].map(str).join(" ");
|
|
53232
|
-
return ifsAreGated(ifs, labelTypeConfigured(wf, raw)) || /assignee\.login\s*==|event\.assignee\b/i.test(ifs)
|
|
53397
|
+
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
|
|
53398
|
+
hasStepMembershipGate(job);
|
|
53233
53399
|
}
|
|
53234
53400
|
function hasImplicitActorGate(agentSteps, bypassActive) {
|
|
53235
53401
|
if (bypassActive) return false;
|
|
53236
53402
|
return agentSteps.some((s) => /anthropics\/claude-code-action@/i.test(s.uses ?? ""));
|
|
53237
53403
|
}
|
|
53238
|
-
function
|
|
53404
|
+
function injectableJobs(wf, raw, untrustedTrigger) {
|
|
53239
53405
|
const out = [];
|
|
53240
|
-
for (const job of
|
|
53406
|
+
for (const job of jobList(wf)) {
|
|
53241
53407
|
const a = (job.steps ?? []).filter(isAgentStep);
|
|
53242
53408
|
if (!a.length) continue;
|
|
53243
53409
|
const jobStar = str(a.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean)) === "*";
|
|
53244
|
-
|
|
53245
|
-
|
|
53246
|
-
out.push(
|
|
53410
|
+
if (jobActorGate(job, wf, raw) || hasImplicitActorGate(a, jobStar && untrustedTrigger))
|
|
53411
|
+
continue;
|
|
53412
|
+
out.push(job);
|
|
53247
53413
|
}
|
|
53248
53414
|
return out;
|
|
53249
53415
|
}
|
|
53250
|
-
function
|
|
53251
|
-
const
|
|
53252
|
-
const
|
|
53253
|
-
|
|
53254
|
-
|
|
53255
|
-
|
|
53416
|
+
function usesPatIn(jobs) {
|
|
53417
|
+
for (const j of jobs)
|
|
53418
|
+
for (const step of j.steps ?? []) {
|
|
53419
|
+
const gt = str(step.with?.["github_token"]);
|
|
53420
|
+
if (/secrets\./i.test(gt) && !/secrets\.GITHUB_TOKEN/i.test(gt)) return true;
|
|
53421
|
+
}
|
|
53422
|
+
return false;
|
|
53256
53423
|
}
|
|
53257
|
-
function
|
|
53258
|
-
|
|
53259
|
-
|
|
53424
|
+
function jobPerms(wf, job) {
|
|
53425
|
+
return job.permissions != null ? str(job.permissions) : str(wf.permissions);
|
|
53426
|
+
}
|
|
53427
|
+
function jobsHaveWrite(wf, jobs, rx) {
|
|
53428
|
+
return jobs.some((j) => {
|
|
53429
|
+
const p = jobPerms(wf, j);
|
|
53430
|
+
return /\bwrite-all\b/i.test(p) || rx.test(p);
|
|
53431
|
+
});
|
|
53432
|
+
}
|
|
53433
|
+
function hasIdTokenWrite(wf, jobs) {
|
|
53434
|
+
const rx = /["']?id-token["']?\s*:\s*["']?write/i;
|
|
53435
|
+
return jobs.some((j) => rx.test(jobPerms(wf, j)));
|
|
53436
|
+
}
|
|
53437
|
+
function hasPrWritePerm(wf, jobs) {
|
|
53438
|
+
return jobsHaveWrite(wf, jobs, /["']?pull-requests["']?\s*:\s*["']?write/i);
|
|
53439
|
+
}
|
|
53440
|
+
function hasCodeWritePerm(wf, agentJobs) {
|
|
53441
|
+
return jobsHaveWrite(
|
|
53442
|
+
wf,
|
|
53443
|
+
agentJobs,
|
|
53444
|
+
/["']?(contents|packages|actions|deployments)["']?\s*:\s*["']?write/i
|
|
53260
53445
|
);
|
|
53261
|
-
return check(wf.permissions) || agentJobs.some((j) => check(j.permissions));
|
|
53262
53446
|
}
|
|
53263
|
-
function
|
|
53264
|
-
|
|
53265
|
-
const gt = str(step.with?.["github_token"]);
|
|
53266
|
-
if (/secrets\./i.test(gt) && !/secrets\.GITHUB_TOKEN/i.test(gt)) return true;
|
|
53267
|
-
}
|
|
53268
|
-
return false;
|
|
53447
|
+
function hasMetaWritePerm(wf, agentJobs) {
|
|
53448
|
+
return jobsHaveWrite(wf, agentJobs, /["']?(pull-requests|issues)["']?\s*:\s*["']?write/i);
|
|
53269
53449
|
}
|
|
53270
53450
|
function hasEnvDeny(steps) {
|
|
53271
53451
|
return steps.some((s) => /"?mode"?\s*:\s*"?deny/i.test(str(s.with?.["settings"])));
|
|
@@ -53298,33 +53478,38 @@ function analyzeWorkflow(path70, content) {
|
|
|
53298
53478
|
)
|
|
53299
53479
|
];
|
|
53300
53480
|
if (agentSteps.length === 0) return null;
|
|
53301
|
-
const
|
|
53302
|
-
|
|
53303
|
-
|
|
53304
|
-
|
|
53305
|
-
|
|
53306
|
-
|
|
53481
|
+
const {
|
|
53482
|
+
keys: triggers,
|
|
53483
|
+
untrusted: forkInput,
|
|
53484
|
+
secretExposed,
|
|
53485
|
+
reusable,
|
|
53486
|
+
privileged
|
|
53487
|
+
} = triggerReach(wf, raw);
|
|
53307
53488
|
const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
|
|
53308
53489
|
const nonWriteStar = nonWrite === "*";
|
|
53309
53490
|
const nonWriteList = !!nonWrite && nonWrite !== "*";
|
|
53310
|
-
const untrustedTrigger =
|
|
53491
|
+
const untrustedTrigger = forkInput || reusable;
|
|
53311
53492
|
const bypassActive = nonWriteStar && untrustedTrigger;
|
|
53312
53493
|
const head = untrustedHeadCheckout(steps);
|
|
53313
53494
|
const promptUntrusted = promptTakesUntrusted(agentSteps);
|
|
53314
|
-
const
|
|
53495
|
+
const injJobs = injectableJobs(wf, raw, untrustedTrigger);
|
|
53496
|
+
const powerJobs = injJobs.length ? injJobs : agentJobs;
|
|
53497
|
+
const powerSteps = injJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep));
|
|
53498
|
+
const scopedSteps = powerSteps.length ? powerSteps : agentSteps;
|
|
53499
|
+
const toolsBlob = collectTools(scopedSteps);
|
|
53500
|
+
const reach = untrustedTrigger && injJobs.length ? Math.max(
|
|
53315
53501
|
head === "root" ? 3 : head === "subdir" ? 1 : 0,
|
|
53316
53502
|
promptUntrusted ? 2 : 0,
|
|
53317
53503
|
bypassActive ? 2 : 0
|
|
53318
53504
|
) : 0;
|
|
53319
|
-
const powerSteps = injectableAgentSteps(wf, raw, untrustedTrigger);
|
|
53320
|
-
const scopedSteps = powerSteps.length ? powerSteps : agentSteps;
|
|
53321
|
-
const toolsBlob = collectTools(scopedSteps);
|
|
53322
53505
|
const broadTools = BROAD_TOOL_RE.test(toolsBlob);
|
|
53323
|
-
const
|
|
53324
|
-
const
|
|
53506
|
+
const egressTool = EGRESS_TOOL_RE.test(toolsBlob);
|
|
53507
|
+
const elevated = hasCodeWritePerm(wf, powerJobs) || hasIdTokenWrite(wf, powerJobs) && egressTool;
|
|
53508
|
+
const pat = usesPatIn(powerJobs);
|
|
53325
53509
|
const power = (broadTools ? 2 : 0) + (bypassActive ? 1 : 0) + (elevated ? 1 : 0) + (pat ? 1 : 0);
|
|
53326
53510
|
const explicitGate = hasActorGate(wf, raw);
|
|
53327
53511
|
const implicitGate = hasImplicitActorGate(agentSteps, bypassActive);
|
|
53512
|
+
const membershipGated = injJobs.length === 0 && jobList(wf).some((j) => (j.steps ?? []).some(isAgentStep) && hasStepMembershipGate(j));
|
|
53328
53513
|
const gate = explicitGate || implicitGate;
|
|
53329
53514
|
const envDeny = hasEnvDeny(agentSteps);
|
|
53330
53515
|
const pinned = agentActionsPinned(agentSteps);
|
|
@@ -53333,7 +53518,7 @@ function analyzeWorkflow(path70, content) {
|
|
|
53333
53518
|
if (envDeny) score -= 1;
|
|
53334
53519
|
if (pinned) score -= 1;
|
|
53335
53520
|
score = Math.max(0, score);
|
|
53336
|
-
if (score === 0 && !secretExposed) return null;
|
|
53521
|
+
if (score === 0 && !secretExposed && !reusable) return null;
|
|
53337
53522
|
let severity = severityFromScore(score);
|
|
53338
53523
|
if (gate && severity && severity !== "advisory") severity = "advisory";
|
|
53339
53524
|
if (reach === 0 && severity && severity !== "advisory") severity = "advisory";
|
|
@@ -53341,9 +53526,20 @@ function analyzeWorkflow(path70, content) {
|
|
|
53341
53526
|
severity = "medium";
|
|
53342
53527
|
const exfilOrRce = EXFIL_RCE_RE.test(toolsBlob);
|
|
53343
53528
|
const githubWriteTool = GH_WRITE_TOOL_RE.test(toolsBlob);
|
|
53344
|
-
const
|
|
53529
|
+
const rce = exfilOrRce;
|
|
53530
|
+
const codeWrite = pat || hasCodeWritePerm(wf, powerJobs) && githubWriteTool;
|
|
53531
|
+
const metaWrite = hasMetaWritePerm(wf, powerJobs) && githubWriteTool;
|
|
53532
|
+
const canDamage = rce || codeWrite || metaWrite;
|
|
53345
53533
|
if (!canDamage && severity && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
|
|
53346
|
-
if (
|
|
53534
|
+
if (canDamage && !rce && !codeWrite && severity === "critical") severity = "high";
|
|
53535
|
+
const issueOnlyWrite = metaWrite && !hasPrWritePerm(wf, powerJobs) && !hasCodeWritePerm(wf, powerJobs);
|
|
53536
|
+
if (issueOnlyWrite && !rce && severity && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium)
|
|
53537
|
+
severity = "medium";
|
|
53538
|
+
if (untrustedTrigger && !privileged && !reusable && severity && severity !== "advisory")
|
|
53539
|
+
severity = "advisory";
|
|
53540
|
+
const reusableLoadedGun = reusable && (head === "root" || promptUntrusted);
|
|
53541
|
+
if (reusable && !reusableLoadedGun && severity && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium)
|
|
53542
|
+
severity = "medium";
|
|
53347
53543
|
if (!severity) severity = "advisory";
|
|
53348
53544
|
const signals = [];
|
|
53349
53545
|
if (secretExposed)
|
|
@@ -53351,7 +53547,11 @@ function analyzeWorkflow(path70, content) {
|
|
|
53351
53547
|
`runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).join(", ")})`
|
|
53352
53548
|
);
|
|
53353
53549
|
else if (forkInput) signals.push(`triggered by untrusted input (${triggers.join(", ")})`);
|
|
53354
|
-
if (
|
|
53550
|
+
else if (reusable)
|
|
53551
|
+
signals.push(
|
|
53552
|
+
reusableLoadedGun ? `reusable workflow (${triggers.join(", ")}) that checks out an untrusted head / ingests untrusted input \u2014 exploitable the moment a caller wires a fork trigger (reachability depends on the caller, but this workflow is built to process attacker input)` : `reusable workflow (${triggers.join(", ")}) \u2014 no untrusted trigger of its own; reachability depends on the caller's trigger + actor gate`
|
|
53553
|
+
);
|
|
53554
|
+
if (forkInput && !privileged && !reusable)
|
|
53355
53555
|
signals.push(
|
|
53356
53556
|
"triggered by `pull_request` \u2014 fork PRs run with a read-only token (lower risk than pull_request_target)"
|
|
53357
53557
|
);
|
|
@@ -53363,8 +53563,14 @@ function analyzeWorkflow(path70, content) {
|
|
|
53363
53563
|
if (elevated) signals.push("elevated permissions (contents/id-token: write)");
|
|
53364
53564
|
if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
|
|
53365
53565
|
if (!gate && reach > 0) signals.push("no effective actor gate");
|
|
53566
|
+
const noExplicitPerms = wf.permissions == null && jobList(wf).every((j) => j.permissions == null);
|
|
53567
|
+
if (noExplicitPerms && reach > 0 && (broadTools || githubWriteTool))
|
|
53568
|
+
signals.push(
|
|
53569
|
+
"no explicit `permissions:` \u2014 the token defaults to the repo/org setting, which may grant write; set it explicitly to read-only"
|
|
53570
|
+
);
|
|
53366
53571
|
const mitigations = [];
|
|
53367
|
-
if (explicitGate
|
|
53572
|
+
if (explicitGate || membershipGated)
|
|
53573
|
+
mitigations.push("actor-gated (maintainer/label/write-user required)");
|
|
53368
53574
|
else if (implicitGate)
|
|
53369
53575
|
mitigations.push("claude-code-action gates the agent to write-access users by default");
|
|
53370
53576
|
if (head === "subdir") mitigations.push("untrusted head isolated in a subdir, not root");
|
|
@@ -53417,12 +53623,12 @@ function agentReachableSecrets(wf, agentSteps, agentJobs) {
|
|
|
53417
53623
|
found.set(name, classifySecret(name));
|
|
53418
53624
|
}
|
|
53419
53625
|
}
|
|
53420
|
-
const idToken =
|
|
53626
|
+
const idToken = hasIdTokenWrite(wf, agentJobs);
|
|
53421
53627
|
const out = [...found].map(([name, kind]) => ({ name, kind }));
|
|
53422
53628
|
if (idToken) out.push({ name: "id-token (cloud OIDC)", kind: "cloud-oidc" });
|
|
53423
53629
|
return out;
|
|
53424
53630
|
}
|
|
53425
|
-
function evalAgentJob(job, wf, raw, untrustedTrigger) {
|
|
53631
|
+
function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
|
|
53426
53632
|
const jobSteps = job.steps ?? [];
|
|
53427
53633
|
const jobAgentSteps = jobSteps.filter(isAgentStep);
|
|
53428
53634
|
if (jobAgentSteps.length === 0) return null;
|
|
@@ -53449,6 +53655,8 @@ function evalAgentJob(job, wf, raw, untrustedTrigger) {
|
|
|
53449
53655
|
} else {
|
|
53450
53656
|
return null;
|
|
53451
53657
|
}
|
|
53658
|
+
const loadedGun = head === "root" || promptTakesUntrusted(jobAgentSteps);
|
|
53659
|
+
if (reusable && !loadedGun && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
|
|
53452
53660
|
return { severity, secrets, injectable, canReadEnv };
|
|
53453
53661
|
}
|
|
53454
53662
|
function analyzeWorkflowSecrets(path70, content) {
|
|
@@ -53461,12 +53669,10 @@ function analyzeWorkflowSecrets(path70, content) {
|
|
|
53461
53669
|
const wf = raw;
|
|
53462
53670
|
const all = allSteps(wf);
|
|
53463
53671
|
if (!all.some((s) => isAgentStep(s.step))) return null;
|
|
53464
|
-
const
|
|
53465
|
-
const untrustedTrigger =
|
|
53466
|
-
(t) => /pull_request_target|workflow_run|issue|pull_request|workflow_call/i.test(t)
|
|
53467
|
-
);
|
|
53672
|
+
const { untrusted: forkInput, reusable } = triggerReach(wf, raw);
|
|
53673
|
+
const untrustedTrigger = forkInput || reusable;
|
|
53468
53674
|
const agentJobs = [...new Set(all.filter((s) => isAgentStep(s.step)).map((s) => s.job))];
|
|
53469
|
-
const worst = agentJobs.map((job) => evalAgentJob(job, wf, raw, untrustedTrigger)).filter((e) => e !== null).sort((a, b) => SEVERITY_RANK2[b.severity] - SEVERITY_RANK2[a.severity])[0];
|
|
53675
|
+
const worst = agentJobs.map((job) => evalAgentJob(job, wf, raw, untrustedTrigger, reusable)).filter((e) => e !== null).sort((a, b) => SEVERITY_RANK2[b.severity] - SEVERITY_RANK2[a.severity])[0];
|
|
53470
53676
|
if (!worst) return null;
|
|
53471
53677
|
return {
|
|
53472
53678
|
check: "CI-4",
|
|
@@ -53509,18 +53715,20 @@ function analyzeAgentConfig(path70, content) {
|
|
|
53509
53715
|
}
|
|
53510
53716
|
const findings = [];
|
|
53511
53717
|
for (const cmd of hookCommands(cfg.hooks)) {
|
|
53512
|
-
const
|
|
53513
|
-
|
|
53514
|
-
|
|
53718
|
+
const remoteExec = /\|\s*(sh|bash|zsh)\b/.test(cmd) || /\b(curl|wget|iwr|irm)\b/.test(cmd);
|
|
53719
|
+
const isNpx = /\bnpx\b/.test(cmd);
|
|
53720
|
+
if (!remoteExec && !isNpx) continue;
|
|
53721
|
+
const unpinned = /@latest\b/.test(cmd) || isNpx && !/@\d/.test(cmd);
|
|
53722
|
+
const high = remoteExec || unpinned;
|
|
53515
53723
|
findings.push({
|
|
53516
53724
|
check: "CI-1",
|
|
53517
53725
|
dimension: "toolRules",
|
|
53518
|
-
severity:
|
|
53519
|
-
title:
|
|
53726
|
+
severity: high ? "high" : "medium",
|
|
53727
|
+
title: high ? "Agent hook runs UNPINNED/remote third-party code on every action" : "Agent hook runs third-party code in the agent hot path",
|
|
53520
53728
|
file: path70,
|
|
53521
53729
|
signals: [
|
|
53522
53730
|
`hook command: \`${cmd.slice(0, 120)}\``,
|
|
53523
|
-
unpinned ? "unpinned \u2014 a compromised/yanked package = code execution on every contributor" : "pinned, but still a standing supply-chain dependency in the agent hot path"
|
|
53731
|
+
remoteExec ? "fetch-and-run (curl|wget / pipe-to-shell) \u2014 unpinnable remote code execution on every contributor" : unpinned ? "unpinned \u2014 a compromised/yanked package = code execution on every contributor" : "pinned, but still a standing supply-chain dependency in the agent hot path"
|
|
53524
53732
|
],
|
|
53525
53733
|
fix: "Vendor the command as a committed local script, or pin an exact version and treat updates as security-reviewed."
|
|
53526
53734
|
});
|
|
@@ -53531,17 +53739,18 @@ function analyzeAgentConfig(path70, content) {
|
|
|
53531
53739
|
(a) => /^Bash$|^Bash\(\s*\*|^Bash\(git:|^Write\(\s*\*|^Write$|^Edit$/.test(a)
|
|
53532
53740
|
);
|
|
53533
53741
|
if (broad.length > 0) {
|
|
53742
|
+
const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
|
|
53534
53743
|
findings.push({
|
|
53535
53744
|
check: "CI-1",
|
|
53536
53745
|
dimension: "toolRules",
|
|
53537
|
-
severity: "medium",
|
|
53538
|
-
title: "Committed agent config pre-authorizes broad tools",
|
|
53746
|
+
severity: hasBackstop ? "medium" : "high",
|
|
53747
|
+
title: hasBackstop ? "Committed agent config pre-authorizes broad tools" : "Committed agent config pre-authorizes broad tools with no deny backstop",
|
|
53539
53748
|
file: path70,
|
|
53540
53749
|
signals: [
|
|
53541
53750
|
`broad allow(s): ${broad.slice(0, 5).join(", ")}`,
|
|
53542
|
-
|
|
53751
|
+
hasBackstop ? "a `deny` list backstops the broad allow" : "no `deny` entry covers Bash/Write/Edit \u2014 every contributor is pre-authorized for catastrophic tools"
|
|
53543
53752
|
],
|
|
53544
|
-
fix: "Scope the allow-list to specific read-only subcommands (e.g. `Bash(gh pr view:*)`); avoid bare `Bash`/`git:`/`Write
|
|
53753
|
+
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."
|
|
53545
53754
|
});
|
|
53546
53755
|
}
|
|
53547
53756
|
return findings;
|
|
@@ -53556,8 +53765,11 @@ function analyzeMcp(path70, content) {
|
|
|
53556
53765
|
} catch {
|
|
53557
53766
|
return [];
|
|
53558
53767
|
}
|
|
53768
|
+
return analyzeMcpServers(cfg.mcpServers ?? {}, path70);
|
|
53769
|
+
}
|
|
53770
|
+
function analyzeMcpServers(servers, path70) {
|
|
53559
53771
|
const findings = [];
|
|
53560
|
-
for (const [name, srv] of Object.entries(
|
|
53772
|
+
for (const [name, srv] of Object.entries(servers ?? {})) {
|
|
53561
53773
|
if (!srv || srv.disabled) continue;
|
|
53562
53774
|
const argv = [srv.command, ...Array.isArray(srv.args) ? srv.args.map(String) : []].join(" ");
|
|
53563
53775
|
if (/\bnpx\b/.test(argv) && (/@latest\b/.test(argv) || !/@\d/.test(argv))) {
|
|
@@ -53592,8 +53804,63 @@ function analyzeMcp(path70, content) {
|
|
|
53592
53804
|
return findings;
|
|
53593
53805
|
}
|
|
53594
53806
|
|
|
53807
|
+
// src/ci-check/codex.ts
|
|
53808
|
+
import { parse as parseToml5 } from "smol-toml";
|
|
53809
|
+
function analyzeCodexConfig(path70, content) {
|
|
53810
|
+
let cfg;
|
|
53811
|
+
try {
|
|
53812
|
+
cfg = parseToml5(content);
|
|
53813
|
+
} catch {
|
|
53814
|
+
return [];
|
|
53815
|
+
}
|
|
53816
|
+
const findings = [];
|
|
53817
|
+
findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path70));
|
|
53818
|
+
const sandbox = typeof cfg.sandbox_mode === "string" ? cfg.sandbox_mode : "";
|
|
53819
|
+
const approval = typeof cfg.approval_policy === "string" ? cfg.approval_policy : "";
|
|
53820
|
+
const fullAccess = /danger-full-access/i.test(sandbox);
|
|
53821
|
+
const noApproval = /^never$/i.test(approval);
|
|
53822
|
+
if (fullAccess || noApproval) {
|
|
53823
|
+
const signals = [
|
|
53824
|
+
fullAccess ? 'sandbox_mode = "danger-full-access" \u2014 the agent runs arbitrary commands with full disk + network access' : null,
|
|
53825
|
+
noApproval ? 'approval_policy = "never" \u2014 no human approval for agent actions' : null
|
|
53826
|
+
].filter((s) => s !== null);
|
|
53827
|
+
findings.push({
|
|
53828
|
+
check: "CI-1",
|
|
53829
|
+
dimension: "toolRules",
|
|
53830
|
+
severity: fullAccess ? "high" : "medium",
|
|
53831
|
+
title: fullAccess ? "Codex config grants a full-access sandbox" : "Codex config never requires approval",
|
|
53832
|
+
file: path70,
|
|
53833
|
+
signals,
|
|
53834
|
+
fix: 'Commit a least-privilege Codex config: prefer `sandbox_mode = "read-only"` (or `"workspace-write"`) and `approval_policy = "on-request"`/`"on-failure"`. A repo-committed config applies to every contributor who runs Codex here.'
|
|
53835
|
+
});
|
|
53836
|
+
}
|
|
53837
|
+
return findings;
|
|
53838
|
+
}
|
|
53839
|
+
|
|
53595
53840
|
// src/ci-check/instructions.ts
|
|
53596
|
-
var
|
|
53841
|
+
var TAG_CHARS = /[\u{E0000}-\u{E007F}]/u;
|
|
53842
|
+
var BIDI_OVERRIDE = /[]/;
|
|
53843
|
+
var BIDI_EMBED_ISOLATE = /[--]/;
|
|
53844
|
+
function isZwLegitScript(cp) {
|
|
53845
|
+
if (cp === void 0) return false;
|
|
53846
|
+
return cp >= 3584 && cp <= 3711 || cp >= 3712 && cp <= 3839 || cp >= 4096 && cp <= 4255 || cp >= 6016 && cp <= 6143 || cp >= 12352 && cp <= 12543 || cp >= 13312 && cp <= 40959 || cp >= 44032 && cp <= 55215;
|
|
53847
|
+
}
|
|
53848
|
+
var isAsciiWordChar = (ch) => !!ch && /[A-Za-z0-9]/.test(ch);
|
|
53849
|
+
function suspiciousZeroWidth(text) {
|
|
53850
|
+
let n = 0;
|
|
53851
|
+
for (let i = 0; i < text.length; i++) {
|
|
53852
|
+
const c = text.charCodeAt(i);
|
|
53853
|
+
if (c !== 8203 && c !== 8288) continue;
|
|
53854
|
+
if (isZwLegitScript(text.codePointAt(i - 1)) || isZwLegitScript(text.codePointAt(i + 1)))
|
|
53855
|
+
continue;
|
|
53856
|
+
const prev = text[i - 1];
|
|
53857
|
+
const next = text[i + 1];
|
|
53858
|
+
if (!prev || !next || /\s/.test(prev) || /\s/.test(next)) continue;
|
|
53859
|
+
if (isAsciiWordChar(prev) && isAsciiWordChar(next)) n++;
|
|
53860
|
+
}
|
|
53861
|
+
return n;
|
|
53862
|
+
}
|
|
53863
|
+
var stripZeroWidth = (t) => t.replace(/[]/g, "");
|
|
53597
53864
|
var OVERRIDE_RE = /ignore\s+(all\s+)?(previous|prior|the\s+above)\s+(instructions|prompts?|rules)|disregard\s+(the\s+|your\s+)?(system\s+)?(prompt|instructions|rules)|forget\s+(everything|all\s+(previous|prior))|you\s+are\s+now\s+(a|an|the)\b|<\/?system>/i;
|
|
53598
53865
|
var FETCH_OBEY_RE = /\b(curl|wget|iwr|invoke-webrequest)\b[^\n|]*\|\s*(bash|sh|zsh|python3?|node|iex)\b|\b(curl|wget)\b[^\n]*&&[^\n]*\b(bash|sh)\b/i;
|
|
53599
53866
|
var SECRET_PATH_RE = /~\/\.aws\/credentials|~\/\.ssh\/id_[a-z]+|~\/\.config\/gh\/hosts|read\s+the\s+(token|secret|api[_ ]?key|password)\s+(in|from)\s+[.`'"]?\.?env/i;
|
|
@@ -53624,15 +53891,53 @@ function mk(severity, title, signals, fix, path70) {
|
|
|
53624
53891
|
function analyzeInstructionFile(path70, content) {
|
|
53625
53892
|
const findings = [];
|
|
53626
53893
|
const decoded = decodeSuspiciousBase64(content);
|
|
53627
|
-
if (
|
|
53894
|
+
if (TAG_CHARS.test(content))
|
|
53895
|
+
findings.push(
|
|
53896
|
+
mk(
|
|
53897
|
+
"critical",
|
|
53898
|
+
"Unicode tag characters in an agent instruction file",
|
|
53899
|
+
[
|
|
53900
|
+
"contains Unicode tag characters (U+E0000\u2013E007F) \u2014 an invisible instruction-smuggling channel with no legitimate use in text"
|
|
53901
|
+
],
|
|
53902
|
+
"Remove the tag characters. Instruction files must be plain, reviewable text.",
|
|
53903
|
+
path70
|
|
53904
|
+
)
|
|
53905
|
+
);
|
|
53906
|
+
if (BIDI_OVERRIDE.test(content))
|
|
53628
53907
|
findings.push(
|
|
53629
53908
|
mk(
|
|
53630
53909
|
"critical",
|
|
53631
|
-
"
|
|
53910
|
+
"Bidirectional override characters in an agent instruction file",
|
|
53911
|
+
[
|
|
53912
|
+
"contains a bidi override (U+202D/U+202E) \u2014 a Trojan-Source technique that visually reorders text so a human reads something different from what the agent parses"
|
|
53913
|
+
],
|
|
53914
|
+
"Remove the bidi override characters.",
|
|
53915
|
+
path70
|
|
53916
|
+
)
|
|
53917
|
+
);
|
|
53918
|
+
else if (BIDI_EMBED_ISOLATE.test(content))
|
|
53919
|
+
findings.push(
|
|
53920
|
+
mk(
|
|
53921
|
+
"advisory",
|
|
53922
|
+
"Bidirectional formatting characters in an agent instruction file",
|
|
53923
|
+
[
|
|
53924
|
+
"contains bidi embed/isolate characters (U+202A\u2013202C / U+2066\u20132069) \u2014 legitimate in right-to-left text, but confirm they are not being used to hide or reorder instructions"
|
|
53925
|
+
],
|
|
53926
|
+
"Confirm the bidi marks are legitimate RTL formatting; remove otherwise.",
|
|
53927
|
+
path70
|
|
53928
|
+
)
|
|
53929
|
+
);
|
|
53930
|
+
const zw = suspiciousZeroWidth(content);
|
|
53931
|
+
if (zw > 0) {
|
|
53932
|
+
const revealed = OVERRIDE_RE.test(stripZeroWidth(content)) && !OVERRIDE_RE.test(content);
|
|
53933
|
+
findings.push(
|
|
53934
|
+
mk(
|
|
53935
|
+
revealed ? "critical" : "medium",
|
|
53936
|
+
"Zero-width characters splitting text in an agent instruction file",
|
|
53632
53937
|
[
|
|
53633
|
-
"
|
|
53938
|
+
revealed ? "a zero-width character conceals a prompt-override directive that only appears once the hidden characters are stripped" : "a zero-width character splits a visible Latin word \u2014 a concealment technique (hides text from human review while the agent reads it as contiguous)"
|
|
53634
53939
|
],
|
|
53635
|
-
"Remove the
|
|
53940
|
+
"Remove the zero-width characters. Instruction files must be plain, reviewable text.",
|
|
53636
53941
|
path70
|
|
53637
53942
|
)
|
|
53638
53943
|
);
|
|
@@ -53716,6 +54021,8 @@ function scanTree(tree) {
|
|
|
53716
54021
|
findings.push(...analyzeAgentConfig(file.path, file.content));
|
|
53717
54022
|
} else if (/\.mcp\.json$|\.cursor\/mcp\.json$/.test(file.path)) {
|
|
53718
54023
|
findings.push(...analyzeMcp(file.path, file.content));
|
|
54024
|
+
} else if (/(^|\/)\.codex\/config\.toml$/.test(file.path)) {
|
|
54025
|
+
findings.push(...analyzeCodexConfig(file.path, file.content));
|
|
53719
54026
|
} else if (/(^|\/)(CLAUDE|AGENTS|GEMINI)\.md$|(^|\/)\.cursorrules$|copilot-instructions\.md$|(^|\/)\.(windsurf|cline)rules$/.test(
|
|
53720
54027
|
file.path
|
|
53721
54028
|
)) {
|