@node9/proxy 2.28.1 → 2.30.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 +1436 -155
- package/dist/cli.mjs +1436 -155
- package/dist/dashboard.mjs +11 -2
- package/dist/index.js +10 -1
- package/dist/index.mjs +10 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -11109,6 +11109,14 @@ async function checkStatePredicates(predicates) {
|
|
|
11109
11109
|
return null;
|
|
11110
11110
|
}
|
|
11111
11111
|
}
|
|
11112
|
+
function resolveDaemonPort(env = process.env) {
|
|
11113
|
+
if (env.NODE9_TESTING !== "1") return DEFAULT_DAEMON_PORT;
|
|
11114
|
+
const raw = env.NODE9_DAEMON_PORT;
|
|
11115
|
+
if (raw === void 0 || raw === "") return DEFAULT_DAEMON_PORT;
|
|
11116
|
+
const port = Number(raw);
|
|
11117
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) return DEFAULT_DAEMON_PORT;
|
|
11118
|
+
return port;
|
|
11119
|
+
}
|
|
11112
11120
|
function getInternalToken() {
|
|
11113
11121
|
try {
|
|
11114
11122
|
const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
|
|
@@ -11382,12 +11390,13 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
11382
11390
|
signal: AbortSignal.timeout(3e3)
|
|
11383
11391
|
});
|
|
11384
11392
|
}
|
|
11385
|
-
var ACTIVITY_SOCKET_PATH, DAEMON_PORT, DAEMON_HOST;
|
|
11393
|
+
var ACTIVITY_SOCKET_PATH, DEFAULT_DAEMON_PORT, DAEMON_PORT, DAEMON_HOST;
|
|
11386
11394
|
var init_daemon = __esm({
|
|
11387
11395
|
"src/auth/daemon.ts"() {
|
|
11388
11396
|
"use strict";
|
|
11389
11397
|
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os8.tmpdir(), "node9-activity.sock");
|
|
11390
|
-
|
|
11398
|
+
DEFAULT_DAEMON_PORT = 7391;
|
|
11399
|
+
DAEMON_PORT = resolveDaemonPort();
|
|
11391
11400
|
DAEMON_HOST = "127.0.0.1";
|
|
11392
11401
|
}
|
|
11393
11402
|
});
|
|
@@ -19429,19 +19438,24 @@ async function postCostBatches(apiUrl, apiKey, machineId, entries) {
|
|
|
19429
19438
|
} catch {
|
|
19430
19439
|
}
|
|
19431
19440
|
if (typeof stored === "number" && stored < batch.length) {
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
`[cost-sync] dropped ${batch.length - stored} of ${batch.length} rows
|
|
19435
|
-
`
|
|
19436
|
-
);
|
|
19441
|
+
debugLog(`[cost-sync] dropped ${batch.length - stored} of ${batch.length} rows
|
|
19442
|
+
`);
|
|
19437
19443
|
}
|
|
19438
19444
|
}
|
|
19439
19445
|
} catch (err2) {
|
|
19440
|
-
|
|
19446
|
+
debugLog(`[cost-sync] ${safeMessage(err2)}
|
|
19441
19447
|
`);
|
|
19442
19448
|
}
|
|
19443
19449
|
}
|
|
19444
19450
|
}
|
|
19451
|
+
function debugLog(line) {
|
|
19452
|
+
try {
|
|
19453
|
+
fs35.mkdirSync(path36.dirname(HOOK_DEBUG_LOG), { recursive: true });
|
|
19454
|
+
fs35.appendFileSync(HOOK_DEBUG_LOG, line);
|
|
19455
|
+
} catch (err2) {
|
|
19456
|
+
console.error(`[cost-sync] could not write ${HOOK_DEBUG_LOG}: ${safeMessage(err2)}`);
|
|
19457
|
+
}
|
|
19458
|
+
}
|
|
19445
19459
|
async function syncCost() {
|
|
19446
19460
|
const creds = getCredentials();
|
|
19447
19461
|
if (!creds?.apiKey || !creds?.apiUrl) return;
|
|
@@ -32323,7 +32337,7 @@ var require_diagnostics = __commonJS({
|
|
|
32323
32337
|
proxyConnected: diagnosticsChannel.channel("undici:proxy:connected")
|
|
32324
32338
|
};
|
|
32325
32339
|
var isTrackingClientEvents = false;
|
|
32326
|
-
function trackClientEvents(
|
|
32340
|
+
function trackClientEvents(debugLog2 = undiciDebugLog) {
|
|
32327
32341
|
if (isTrackingClientEvents) {
|
|
32328
32342
|
return;
|
|
32329
32343
|
}
|
|
@@ -32338,7 +32352,7 @@ var require_diagnostics = __commonJS({
|
|
|
32338
32352
|
const {
|
|
32339
32353
|
connectParams: { version: version2, protocol, port, host }
|
|
32340
32354
|
} = evt;
|
|
32341
|
-
|
|
32355
|
+
debugLog2(
|
|
32342
32356
|
"connecting to %s%s using %s%s",
|
|
32343
32357
|
host,
|
|
32344
32358
|
port ? `:${port}` : "",
|
|
@@ -32353,7 +32367,7 @@ var require_diagnostics = __commonJS({
|
|
|
32353
32367
|
const {
|
|
32354
32368
|
connectParams: { version: version2, protocol, port, host }
|
|
32355
32369
|
} = evt;
|
|
32356
|
-
|
|
32370
|
+
debugLog2(
|
|
32357
32371
|
"connected to %s%s using %s%s",
|
|
32358
32372
|
host,
|
|
32359
32373
|
port ? `:${port}` : "",
|
|
@@ -32369,7 +32383,7 @@ var require_diagnostics = __commonJS({
|
|
|
32369
32383
|
connectParams: { version: version2, protocol, port, host },
|
|
32370
32384
|
error
|
|
32371
32385
|
} = evt;
|
|
32372
|
-
|
|
32386
|
+
debugLog2(
|
|
32373
32387
|
"connection to %s%s using %s%s errored - %s",
|
|
32374
32388
|
host,
|
|
32375
32389
|
port ? `:${port}` : "",
|
|
@@ -32385,12 +32399,12 @@ var require_diagnostics = __commonJS({
|
|
|
32385
32399
|
const {
|
|
32386
32400
|
request: { method, path: path91, origin }
|
|
32387
32401
|
} = evt;
|
|
32388
|
-
|
|
32402
|
+
debugLog2("sending request to %s %s%s", method, origin, path91);
|
|
32389
32403
|
}
|
|
32390
32404
|
);
|
|
32391
32405
|
}
|
|
32392
32406
|
var isTrackingRequestEvents = false;
|
|
32393
|
-
function trackRequestEvents(
|
|
32407
|
+
function trackRequestEvents(debugLog2 = undiciDebugLog) {
|
|
32394
32408
|
if (isTrackingRequestEvents) {
|
|
32395
32409
|
return;
|
|
32396
32410
|
}
|
|
@@ -32406,7 +32420,7 @@ var require_diagnostics = __commonJS({
|
|
|
32406
32420
|
request: { method, path: path91, origin },
|
|
32407
32421
|
response: { statusCode }
|
|
32408
32422
|
} = evt;
|
|
32409
|
-
|
|
32423
|
+
debugLog2(
|
|
32410
32424
|
"received response to %s %s%s - HTTP %d",
|
|
32411
32425
|
method,
|
|
32412
32426
|
origin,
|
|
@@ -32421,7 +32435,7 @@ var require_diagnostics = __commonJS({
|
|
|
32421
32435
|
const {
|
|
32422
32436
|
request: { method, path: path91, origin }
|
|
32423
32437
|
} = evt;
|
|
32424
|
-
|
|
32438
|
+
debugLog2("trailers received from %s %s%s", method, origin, path91);
|
|
32425
32439
|
}
|
|
32426
32440
|
);
|
|
32427
32441
|
diagnosticsChannel.subscribe(
|
|
@@ -32431,7 +32445,7 @@ var require_diagnostics = __commonJS({
|
|
|
32431
32445
|
request: { method, path: path91, origin },
|
|
32432
32446
|
error
|
|
32433
32447
|
} = evt;
|
|
32434
|
-
|
|
32448
|
+
debugLog2(
|
|
32435
32449
|
"request to %s %s%s errored - %s",
|
|
32436
32450
|
method,
|
|
32437
32451
|
origin,
|
|
@@ -32442,7 +32456,7 @@ var require_diagnostics = __commonJS({
|
|
|
32442
32456
|
);
|
|
32443
32457
|
}
|
|
32444
32458
|
var isTrackingWebSocketEvents = false;
|
|
32445
|
-
function trackWebSocketEvents(
|
|
32459
|
+
function trackWebSocketEvents(debugLog2 = websocketDebuglog) {
|
|
32446
32460
|
if (isTrackingWebSocketEvents) {
|
|
32447
32461
|
return;
|
|
32448
32462
|
}
|
|
@@ -32456,9 +32470,9 @@ var require_diagnostics = __commonJS({
|
|
|
32456
32470
|
(evt) => {
|
|
32457
32471
|
if (evt.address != null) {
|
|
32458
32472
|
const { address, port } = evt.address;
|
|
32459
|
-
|
|
32473
|
+
debugLog2("connection opened %s%s", address, port ? `:${port}` : "");
|
|
32460
32474
|
} else {
|
|
32461
|
-
|
|
32475
|
+
debugLog2("connection opened");
|
|
32462
32476
|
}
|
|
32463
32477
|
}
|
|
32464
32478
|
);
|
|
@@ -32466,7 +32480,7 @@ var require_diagnostics = __commonJS({
|
|
|
32466
32480
|
"undici:websocket:close",
|
|
32467
32481
|
(evt) => {
|
|
32468
32482
|
const { websocket, code: code2, reason } = evt;
|
|
32469
|
-
|
|
32483
|
+
debugLog2(
|
|
32470
32484
|
"closed connection to %s - %s %s",
|
|
32471
32485
|
websocket.url,
|
|
32472
32486
|
code2,
|
|
@@ -32477,19 +32491,19 @@ var require_diagnostics = __commonJS({
|
|
|
32477
32491
|
diagnosticsChannel.subscribe(
|
|
32478
32492
|
"undici:websocket:socket_error",
|
|
32479
32493
|
(err2) => {
|
|
32480
|
-
|
|
32494
|
+
debugLog2("connection errored - %s", err2.message);
|
|
32481
32495
|
}
|
|
32482
32496
|
);
|
|
32483
32497
|
diagnosticsChannel.subscribe(
|
|
32484
32498
|
"undici:websocket:ping",
|
|
32485
32499
|
(evt) => {
|
|
32486
|
-
|
|
32500
|
+
debugLog2("ping received");
|
|
32487
32501
|
}
|
|
32488
32502
|
);
|
|
32489
32503
|
diagnosticsChannel.subscribe(
|
|
32490
32504
|
"undici:websocket:pong",
|
|
32491
32505
|
(evt) => {
|
|
32492
|
-
|
|
32506
|
+
debugLog2("pong received");
|
|
32493
32507
|
}
|
|
32494
32508
|
);
|
|
32495
32509
|
}
|
|
@@ -58976,6 +58990,42 @@ function shellOutputText(toolResponse) {
|
|
|
58976
58990
|
}
|
|
58977
58991
|
return collectToolOutputText(toolResponse).text;
|
|
58978
58992
|
}
|
|
58993
|
+
function mapToolOutputStrings(toolResponse, fn) {
|
|
58994
|
+
const walk = (v, depth) => {
|
|
58995
|
+
if (typeof v === "string") return fn(v);
|
|
58996
|
+
if (depth >= MAX_DEPTH3 || v === null || typeof v !== "object") return v;
|
|
58997
|
+
if (Array.isArray(v)) return v.map((x) => walk(x, depth + 1));
|
|
58998
|
+
const out = {};
|
|
58999
|
+
for (const [k, x] of Object.entries(v)) out[k] = walk(x, depth + 1);
|
|
59000
|
+
return out;
|
|
59001
|
+
};
|
|
59002
|
+
return walk(toolResponse, 0);
|
|
59003
|
+
}
|
|
59004
|
+
function frameToolOutputLeaves(toolResponse, isSuspect, frame, neutralize) {
|
|
59005
|
+
const suspect = [];
|
|
59006
|
+
let largestAt = -1;
|
|
59007
|
+
let largestLen = 0;
|
|
59008
|
+
mapToolOutputStrings(toolResponse, (s) => {
|
|
59009
|
+
const i2 = suspect.length;
|
|
59010
|
+
suspect.push(s.length > 0 && isSuspect(s));
|
|
59011
|
+
if (s.length > largestLen) {
|
|
59012
|
+
largestLen = s.length;
|
|
59013
|
+
largestAt = i2;
|
|
59014
|
+
}
|
|
59015
|
+
return s;
|
|
59016
|
+
});
|
|
59017
|
+
const anySuspect = suspect.some(Boolean);
|
|
59018
|
+
let i = 0;
|
|
59019
|
+
return mapToolOutputStrings(toolResponse, (s) => {
|
|
59020
|
+
const at = i++;
|
|
59021
|
+
const pick2 = anySuspect ? suspect[at] : at === largestAt;
|
|
59022
|
+
if (!pick2) return neutralize(s);
|
|
59023
|
+
const f = frame();
|
|
59024
|
+
return `${f.header}
|
|
59025
|
+
${neutralize(s)}
|
|
59026
|
+
${f.footer}`;
|
|
59027
|
+
});
|
|
59028
|
+
}
|
|
58979
59029
|
|
|
58980
59030
|
// src/cli/commands/log.ts
|
|
58981
59031
|
init_hasher();
|
|
@@ -59206,19 +59256,36 @@ function registerLogCommand(program2) {
|
|
|
59206
59256
|
process.stdout.write(JSON.stringify({ redacted: out, found, injection }) + "\n");
|
|
59207
59257
|
} else {
|
|
59208
59258
|
const warnings = [];
|
|
59259
|
+
const inPlace = agent === "Claude Code" && config.policy.dlp.enabled;
|
|
59260
|
+
const redactedNames = /* @__PURE__ */ new Set();
|
|
59261
|
+
let updatedToolOutput = void 0;
|
|
59262
|
+
if (inPlace) {
|
|
59263
|
+
const mapped = mapToolOutputStrings(payload.tool_response, (leaf) => {
|
|
59264
|
+
const r = redactText(leaf);
|
|
59265
|
+
for (const name of r.found) redactedNames.add(name);
|
|
59266
|
+
return r.result;
|
|
59267
|
+
});
|
|
59268
|
+
if (redactedNames.size > 0) updatedToolOutput = mapped;
|
|
59269
|
+
}
|
|
59209
59270
|
const hit = scanText(toolOutput);
|
|
59210
|
-
|
|
59211
|
-
|
|
59212
|
-
|
|
59213
|
-
`output-secret:${hit.patternName}`
|
|
59214
|
-
);
|
|
59271
|
+
const secretName = hit?.patternName ?? [...redactedNames][0];
|
|
59272
|
+
if (secretName) {
|
|
59273
|
+
await notifySessionTaint(payloadSessionId ?? "", `output-secret:${secretName}`);
|
|
59215
59274
|
warnings.push(
|
|
59216
|
-
`\u26A0\uFE0F node9: this tool output contained a credential (${
|
|
59275
|
+
updatedToolOutput !== void 0 ? `\u26A0\uFE0F node9: this tool output contained a credential (${secretName}); node9 replaced it with [node9-redacted:\u2026] before you read it. Treat the credential as compromised. node9 has flagged this session: the next network or write action will require approval.` : `\u26A0\uFE0F node9: this tool output contained a credential (${secretName}). Do not echo, store, or transmit it \u2014 treat it as compromised and rotate it. node9 has flagged this session: the next network or write action will require approval.`
|
|
59217
59276
|
);
|
|
59218
59277
|
}
|
|
59219
59278
|
if (injectionOn) {
|
|
59220
59279
|
const m = scanInjection(toolOutput, { tool: rawToolName });
|
|
59221
59280
|
if (m && atLeastConfidence(m.confidence, inj.minConfidence)) {
|
|
59281
|
+
if (agent === "Claude Code") {
|
|
59282
|
+
updatedToolOutput = frameToolOutputLeaves(
|
|
59283
|
+
updatedToolOutput ?? payload.tool_response,
|
|
59284
|
+
(leaf) => scanInjection(leaf, { tool: rawToolName }) !== null,
|
|
59285
|
+
newUntrustedFrame,
|
|
59286
|
+
neutralizeMarkers
|
|
59287
|
+
);
|
|
59288
|
+
}
|
|
59222
59289
|
await notifySessionTaint(
|
|
59223
59290
|
payloadSessionId ?? "",
|
|
59224
59291
|
`output-injection:${m.signals.join("+")}`
|
|
@@ -59228,12 +59295,22 @@ function registerLogCommand(program2) {
|
|
|
59228
59295
|
);
|
|
59229
59296
|
}
|
|
59230
59297
|
}
|
|
59231
|
-
if (
|
|
59298
|
+
if (redactedNames.size > 0) {
|
|
59299
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
59300
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
59301
|
+
tool,
|
|
59302
|
+
source: "output-redacted",
|
|
59303
|
+
outputRedacted: [...redactedNames],
|
|
59304
|
+
...payloadSessionId ? { sessionId: payloadSessionId } : {}
|
|
59305
|
+
});
|
|
59306
|
+
}
|
|
59307
|
+
if ((warnings.length > 0 || updatedToolOutput !== void 0) && (agent === "Claude Code" || agent === "Codex")) {
|
|
59232
59308
|
process.stdout.write(
|
|
59233
59309
|
JSON.stringify({
|
|
59234
59310
|
hookSpecificOutput: {
|
|
59235
59311
|
hookEventName: "PostToolUse",
|
|
59236
|
-
additionalContext: warnings.join("\n\n")
|
|
59312
|
+
...warnings.length > 0 ? { additionalContext: warnings.join("\n\n") } : {},
|
|
59313
|
+
...updatedToolOutput !== void 0 ? { updatedToolOutput } : {}
|
|
59237
59314
|
}
|
|
59238
59315
|
}) + "\n"
|
|
59239
59316
|
);
|
|
@@ -64475,6 +64552,97 @@ function isZwLegitScript(cp) {
|
|
|
64475
64552
|
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;
|
|
64476
64553
|
}
|
|
64477
64554
|
var isAsciiWordChar = (ch) => !!ch && /[A-Za-z0-9]/.test(ch);
|
|
64555
|
+
var LOOKALIKE = {
|
|
64556
|
+
\u0430: "a",
|
|
64557
|
+
\u0435: "e",
|
|
64558
|
+
\u043E: "o",
|
|
64559
|
+
\u0440: "p",
|
|
64560
|
+
\u0441: "c",
|
|
64561
|
+
\u0443: "y",
|
|
64562
|
+
\u0445: "x",
|
|
64563
|
+
\u0456: "i",
|
|
64564
|
+
\u0458: "j",
|
|
64565
|
+
\u0455: "s",
|
|
64566
|
+
"\u0501": "d",
|
|
64567
|
+
"\u051B": "q",
|
|
64568
|
+
"\u051D": "w",
|
|
64569
|
+
\u0410: "A",
|
|
64570
|
+
\u0412: "B",
|
|
64571
|
+
\u0415: "E",
|
|
64572
|
+
\u041A: "K",
|
|
64573
|
+
\u041C: "M",
|
|
64574
|
+
\u041D: "H",
|
|
64575
|
+
\u041E: "O",
|
|
64576
|
+
\u0420: "P",
|
|
64577
|
+
\u0421: "C",
|
|
64578
|
+
\u0422: "T",
|
|
64579
|
+
\u0425: "X",
|
|
64580
|
+
\u0406: "I",
|
|
64581
|
+
\u0408: "J",
|
|
64582
|
+
\u0405: "S",
|
|
64583
|
+
"\u050C": "G",
|
|
64584
|
+
"\u051A": "Q",
|
|
64585
|
+
"\u051C": "W",
|
|
64586
|
+
\u03BF: "o",
|
|
64587
|
+
\u03B1: "a",
|
|
64588
|
+
\u03C1: "p",
|
|
64589
|
+
\u03C4: "t",
|
|
64590
|
+
\u03BD: "v",
|
|
64591
|
+
\u03C5: "u",
|
|
64592
|
+
\u03BA: "k",
|
|
64593
|
+
\u03C7: "x",
|
|
64594
|
+
\u03B9: "i",
|
|
64595
|
+
\u0391: "A",
|
|
64596
|
+
\u0392: "B",
|
|
64597
|
+
\u0395: "E",
|
|
64598
|
+
\u0397: "H",
|
|
64599
|
+
\u0399: "I",
|
|
64600
|
+
\u039A: "K",
|
|
64601
|
+
\u039C: "M",
|
|
64602
|
+
\u039D: "N",
|
|
64603
|
+
\u039F: "O",
|
|
64604
|
+
\u03A1: "P",
|
|
64605
|
+
\u03A4: "T",
|
|
64606
|
+
\u03A7: "X",
|
|
64607
|
+
\u03A5: "Y",
|
|
64608
|
+
\u0396: "Z",
|
|
64609
|
+
// Armenian (review, round 2)
|
|
64610
|
+
"\u0585": "o",
|
|
64611
|
+
"\u057D": "u",
|
|
64612
|
+
"\u0570": "h",
|
|
64613
|
+
"\u0578": "n",
|
|
64614
|
+
"\u0581": "g",
|
|
64615
|
+
"\u0566": "q"
|
|
64616
|
+
};
|
|
64617
|
+
var MIXED_WORD_RE = /[A-Za-z\u0370-\u03ff\u0400-\u04ff\u0500-\u052f\u0530-\u058f]{4,60}/g;
|
|
64618
|
+
var FOREIGN_LETTER_RE = /[\u0370-\u03ff\u0400-\u04ff\u0500-\u052f\u0530-\u058f]/;
|
|
64619
|
+
var GREEK_RE = /[\u0370-\u03ff]/;
|
|
64620
|
+
var EXPLAINED_LINE_RE = /cyrillic|greek|armenian|homoglyph|confusable|look-?alike/i;
|
|
64621
|
+
var HOMOGLYPH_MAX_LINE = 2e3;
|
|
64622
|
+
function homoglyphWord(content) {
|
|
64623
|
+
const lines = content.split("\n");
|
|
64624
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64625
|
+
const text = lines[i];
|
|
64626
|
+
if (text.length > HOMOGLYPH_MAX_LINE) continue;
|
|
64627
|
+
const visible = text.replace(/<!--[\s\S]*?(?:-->|$)/g, "");
|
|
64628
|
+
if (EXPLAINED_LINE_RE.test(visible)) continue;
|
|
64629
|
+
for (const m of text.matchAll(MIXED_WORD_RE)) {
|
|
64630
|
+
const chars = [...m[0]];
|
|
64631
|
+
const latin = chars.filter((c) => /[A-Za-z]/.test(c)).length;
|
|
64632
|
+
const foreign = chars.filter((c) => FOREIGN_LETTER_RE.test(c));
|
|
64633
|
+
if (latin < 2 || !foreign.some((c) => c in LOOKALIKE) || foreign.length > latin) continue;
|
|
64634
|
+
const lead = chars.findIndex((c) => !FOREIGN_LETTER_RE.test(c));
|
|
64635
|
+
if (lead > 0 && chars.slice(0, lead).every((c) => GREEK_RE.test(c)) && lead === foreign.length)
|
|
64636
|
+
continue;
|
|
64637
|
+
return {
|
|
64638
|
+
folded: chars.map((c) => LOOKALIKE[c] ?? c).join(""),
|
|
64639
|
+
count: foreign.length,
|
|
64640
|
+
line: i + 1
|
|
64641
|
+
};
|
|
64642
|
+
}
|
|
64643
|
+
}
|
|
64644
|
+
return null;
|
|
64645
|
+
}
|
|
64478
64646
|
function suspiciousZeroWidth(text) {
|
|
64479
64647
|
let n = 0;
|
|
64480
64648
|
let first = -1;
|
|
@@ -64718,6 +64886,21 @@ var lineAt = (text, i) => i >= 0 ? lineAtIndex(text, i) : void 0;
|
|
|
64718
64886
|
function analyzeInstructionFile(path91, content) {
|
|
64719
64887
|
const findings = [];
|
|
64720
64888
|
const decoded = decodeSuspiciousBase64(content);
|
|
64889
|
+
const glyph = homoglyphWord(content);
|
|
64890
|
+
if (glyph)
|
|
64891
|
+
findings.push(
|
|
64892
|
+
mk2(
|
|
64893
|
+
"CI-6.homoglyph",
|
|
64894
|
+
"advisory",
|
|
64895
|
+
"A word in an agent instruction file is spelled with look-alike letters",
|
|
64896
|
+
[
|
|
64897
|
+
`\`${glyph.folded}\` is written with ${glyph.count} non-Latin look-alike letter(s): it reads the same to a reviewer, but a search, a replace or a policy check on the real word misses it`
|
|
64898
|
+
],
|
|
64899
|
+
"Spell the word with plain Latin letters, or explain on the same line why the look-alikes are there.",
|
|
64900
|
+
path91,
|
|
64901
|
+
glyph.line
|
|
64902
|
+
)
|
|
64903
|
+
);
|
|
64721
64904
|
if (TAG_CHARS.test(content))
|
|
64722
64905
|
findings.push(
|
|
64723
64906
|
mk2(
|
|
@@ -64856,13 +65039,17 @@ function analyzeInstructionFile(path91, content) {
|
|
|
64856
65039
|
}
|
|
64857
65040
|
|
|
64858
65041
|
// src/ci-check/route.ts
|
|
65042
|
+
var MCP_CONFIG_RE = /\.mcp\.json$|\.cursor\/mcp\.json$|(^|\/)\.vscode\/mcp\.json$|(^|\/)\.kiro\/settings\/mcp\.json$|(^|\/)\.gemini\/settings\.json$|(^|\/)opencode\.json$|(^|\/)\.opencode\/config\.json$/;
|
|
65043
|
+
var ISSUE_FORM_RE = /^\.github\/ISSUE_TEMPLATE\/[^/]+\.(ya?ml|md)$/i;
|
|
64859
65044
|
function routeOf(path91, skillDirs, appDirs = /* @__PURE__ */ new Set()) {
|
|
64860
65045
|
if (path91 === SUPPRESSIONS_FILE) return "suppressions";
|
|
64861
65046
|
if (/\.github\/workflows\/.+\.ya?ml$/.test(path91)) return "workflow";
|
|
65047
|
+
if (ISSUE_FORM_RE.test(path91)) return "issue-form";
|
|
65048
|
+
if (path91 === "package.json" || path91 === "pyproject.toml") return "manifest";
|
|
64862
65049
|
if (/\.claude\/settings(\.local)?\.json$/.test(path91)) return "agent-config";
|
|
64863
65050
|
if (isHookScript(path91)) return "hook-script";
|
|
64864
65051
|
if (isSkillScript(path91, skillDirs)) return "skill-script";
|
|
64865
|
-
if (
|
|
65052
|
+
if (MCP_CONFIG_RE.test(path91)) return "mcp";
|
|
64866
65053
|
if (/(^|\/)\.codex\/config\.toml$/.test(path91)) return "codex";
|
|
64867
65054
|
if (isInstructionFile(path91, skillDirs, appDirs)) return "instruction";
|
|
64868
65055
|
if (isAppDoc(path91, skillDirs, appDirs)) return "app-doc";
|
|
@@ -64894,6 +65081,11 @@ var SURFACE_FILES = [
|
|
|
64894
65081
|
".claude/settings.local.json",
|
|
64895
65082
|
".mcp.json",
|
|
64896
65083
|
".cursor/mcp.json",
|
|
65084
|
+
".vscode/mcp.json",
|
|
65085
|
+
".kiro/settings/mcp.json",
|
|
65086
|
+
".gemini/settings.json",
|
|
65087
|
+
"opencode.json",
|
|
65088
|
+
".opencode/config.json",
|
|
64897
65089
|
".codex/config.toml",
|
|
64898
65090
|
// CI-6: agent instruction files (auto-loaded into the agent's system prompt).
|
|
64899
65091
|
"CLAUDE.md",
|
|
@@ -64905,7 +65097,7 @@ var SURFACE_FILES = [
|
|
|
64905
65097
|
".clinerules"
|
|
64906
65098
|
];
|
|
64907
65099
|
var WORKFLOW_DIR = ".github/workflows";
|
|
64908
|
-
var CONFIG_FILE_RE = /(^|\/)\.claude\/settings(\.local)?\.json$|(^|\/)\.mcp\.json$|(^|\/)\.cursor\/mcp\.json$|(^|\/)\.codex\/config\.toml$/;
|
|
65100
|
+
var CONFIG_FILE_RE = /(^|\/)\.claude\/settings(\.local)?\.json$|(^|\/)\.mcp\.json$|(^|\/)\.cursor\/mcp\.json$|(^|\/)\.codex\/config\.toml$|(^|\/)\.vscode\/mcp\.json$|(^|\/)\.kiro\/settings\/mcp\.json$|(^|\/)\.gemini\/settings\.json$|(^|\/)opencode\.json$|(^|\/)\.opencode\/config\.json$/;
|
|
64909
65101
|
function selectSurface(paths) {
|
|
64910
65102
|
const skillDirs = skillDirsOf(paths);
|
|
64911
65103
|
const appDirs = appSkillDirsOf(paths, skillDirs);
|
|
@@ -65054,6 +65246,7 @@ async function listWorkflowPaths(owner, repo, notes) {
|
|
|
65054
65246
|
return json.filter((e) => e.type === "file" && /\.ya?ml$/.test(e.name)).map((e) => e.path);
|
|
65055
65247
|
}
|
|
65056
65248
|
var ROOT_WORKFLOW_RE = /^\.github\/workflows\/[^/]+\.ya?ml$/;
|
|
65249
|
+
var MAX_ISSUE_FORMS = 40;
|
|
65057
65250
|
function kindOfMode(mode) {
|
|
65058
65251
|
if (mode === "120000") return "link";
|
|
65059
65252
|
if (/^100[0-7]{3}$/.test(mode)) return "file";
|
|
@@ -65263,8 +65456,15 @@ function planSurface(paths, notes, truncated) {
|
|
|
65263
65456
|
const present = new Set(paths);
|
|
65264
65457
|
const root = SURFACE_FILES.filter((p) => present.has(p));
|
|
65265
65458
|
const workflows = paths.filter((p) => ROOT_WORKFLOW_RE.test(p));
|
|
65459
|
+
const allForms = paths.filter((p) => ISSUE_FORM_RE.test(p));
|
|
65460
|
+
const forms = allForms.slice(0, MAX_ISSUE_FORMS);
|
|
65461
|
+
if (allForms.length > MAX_ISSUE_FORMS)
|
|
65462
|
+
notes.push(
|
|
65463
|
+
`${allForms.length} issue forms; read the first ${MAX_ISSUE_FORMS} (a label an unread form applies is not known): results ${INCOMPLETE}.`
|
|
65464
|
+
);
|
|
65465
|
+
forms.push(...["package.json", "pyproject.toml"].filter((p) => present.has(p)));
|
|
65266
65466
|
const nested = pickSurfacePaths(paths, truncated, notes, { files: Number.POSITIVE_INFINITY });
|
|
65267
|
-
return [.../* @__PURE__ */ new Set([...root, ...workflows, ...nested])];
|
|
65467
|
+
return [.../* @__PURE__ */ new Set([...root, ...workflows, ...forms, ...nested])];
|
|
65268
65468
|
}
|
|
65269
65469
|
function planReads(l, R, opts = {}) {
|
|
65270
65470
|
const notes = [];
|
|
@@ -65330,7 +65530,7 @@ function planReads(l, R, opts = {}) {
|
|
|
65330
65530
|
function capNested(reads, cap, notes) {
|
|
65331
65531
|
if (cap === void 0) return reads;
|
|
65332
65532
|
const rootSet = new Set(SURFACE_FILES);
|
|
65333
|
-
const isRoot = (r) => rootSet.has(r.rel) || ROOT_WORKFLOW_RE.test(r.rel);
|
|
65533
|
+
const isRoot = (r) => rootSet.has(r.rel) || ROOT_WORKFLOW_RE.test(r.rel) || ISSUE_FORM_RE.test(r.rel) || r.rel === "package.json" || r.rel === "pyproject.toml";
|
|
65334
65534
|
const nested = reads.filter((r) => !isRoot(r));
|
|
65335
65535
|
if (nested.length < cap) return reads;
|
|
65336
65536
|
const keep = new Set(nested.slice(0, cap));
|
|
@@ -65426,6 +65626,45 @@ function readLocalFile(abs, rel) {
|
|
|
65426
65626
|
if (fd !== void 0) fs78.closeSync(fd);
|
|
65427
65627
|
}
|
|
65428
65628
|
}
|
|
65629
|
+
function readSmallFile(p, max) {
|
|
65630
|
+
let fd;
|
|
65631
|
+
try {
|
|
65632
|
+
fd = fs78.openSync(p, fs78.constants.O_RDONLY | (fs78.constants.O_NOFOLLOW ?? 0));
|
|
65633
|
+
const st = fs78.fstatSync(fd);
|
|
65634
|
+
if (!st.isFile() || st.size > max) return null;
|
|
65635
|
+
const buf = Buffer.alloc(st.size);
|
|
65636
|
+
const n = fs78.readSync(fd, buf, 0, st.size, 0);
|
|
65637
|
+
return buf.subarray(0, n).toString("utf8");
|
|
65638
|
+
} catch {
|
|
65639
|
+
return null;
|
|
65640
|
+
} finally {
|
|
65641
|
+
if (fd !== void 0) fs78.closeSync(fd);
|
|
65642
|
+
}
|
|
65643
|
+
}
|
|
65644
|
+
function localGitHubOwner(root) {
|
|
65645
|
+
let cfg = readSmallFile(path75.join(root, ".git", "config"), 1e6);
|
|
65646
|
+
if (cfg === null) {
|
|
65647
|
+
const pointer = readSmallFile(path75.join(root, ".git"), 4096);
|
|
65648
|
+
const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
|
|
65649
|
+
if (!m) return void 0;
|
|
65650
|
+
let gitDir = path75.resolve(root, m[1]);
|
|
65651
|
+
const common = readSmallFile(path75.join(gitDir, "commondir"), 4096);
|
|
65652
|
+
if (common !== null) gitDir = path75.resolve(gitDir, common.trim());
|
|
65653
|
+
cfg = readSmallFile(path75.join(gitDir, "config"), 1e6);
|
|
65654
|
+
if (cfg === null) return void 0;
|
|
65655
|
+
}
|
|
65656
|
+
let inOrigin = false;
|
|
65657
|
+
for (const line of cfg.split(/\r?\n/)) {
|
|
65658
|
+
const sec = /^\s*\[\s*([^\]\s]+)(?:\s+"([^"]*)")?\s*\]/.exec(line);
|
|
65659
|
+
if (sec) {
|
|
65660
|
+
inOrigin = sec[1].toLowerCase() === "remote" && sec[2] === "origin";
|
|
65661
|
+
continue;
|
|
65662
|
+
}
|
|
65663
|
+
const url = inOrigin && /^\s*url\s*=\s*(.+?)\s*$/i.exec(line);
|
|
65664
|
+
if (url) return /github\.com[:/]([\w.-]{1,100})\//i.exec(url[1])?.[1];
|
|
65665
|
+
}
|
|
65666
|
+
return void 0;
|
|
65667
|
+
}
|
|
65429
65668
|
function readLocalTree(dir) {
|
|
65430
65669
|
const root = dir.replace(/^~/, process.env.HOME ?? "~");
|
|
65431
65670
|
const notes = [];
|
|
@@ -65455,8 +65694,10 @@ function readLocalTree(dir) {
|
|
|
65455
65694
|
}
|
|
65456
65695
|
});
|
|
65457
65696
|
}
|
|
65697
|
+
const owner = localGitHubOwner(root);
|
|
65458
65698
|
return {
|
|
65459
65699
|
source: root,
|
|
65700
|
+
...owner ? { owner } : {},
|
|
65460
65701
|
files,
|
|
65461
65702
|
notes,
|
|
65462
65703
|
paths: plan.paths,
|
|
@@ -65569,8 +65810,10 @@ function readGitRefTree(dir, ref) {
|
|
|
65569
65810
|
}
|
|
65570
65811
|
files.push({ path: rel, content, route });
|
|
65571
65812
|
}
|
|
65813
|
+
const owner = localGitHubOwner(root);
|
|
65572
65814
|
return {
|
|
65573
65815
|
source: `${root}@${ref}`,
|
|
65816
|
+
...owner ? { owner } : {},
|
|
65574
65817
|
files,
|
|
65575
65818
|
notes,
|
|
65576
65819
|
paths: plan.paths,
|
|
@@ -65670,7 +65913,13 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65670
65913
|
const listed2 = await listTree(owner, repo, notes);
|
|
65671
65914
|
if (!listed2) {
|
|
65672
65915
|
const workflowPaths = await listWorkflowPaths(owner, repo, notes);
|
|
65673
|
-
const allPaths = [
|
|
65916
|
+
const allPaths = [
|
|
65917
|
+
.../* @__PURE__ */ new Set([...SURFACE_FILES, ...workflowPaths, "package.json", "pyproject.toml"])
|
|
65918
|
+
];
|
|
65919
|
+
if (!notes.includes(RATE_LIMIT_NOTE) && !notes.includes(NETWORK_NOTE))
|
|
65920
|
+
notes.push(
|
|
65921
|
+
`the repository tree could not be listed; only the root files and the workflows were read: nested agent files ${INCOMPLETE}.`
|
|
65922
|
+
);
|
|
65674
65923
|
const fetched2 = await pooled(
|
|
65675
65924
|
allPaths,
|
|
65676
65925
|
FETCH_CONCURRENCY,
|
|
@@ -65678,6 +65927,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65678
65927
|
);
|
|
65679
65928
|
return {
|
|
65680
65929
|
source: `${owner}/${repo}`,
|
|
65930
|
+
owner,
|
|
65681
65931
|
files: fetched2.filter((f) => !!f),
|
|
65682
65932
|
notes
|
|
65683
65933
|
};
|
|
@@ -65733,6 +65983,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65733
65983
|
notes.push(...session.closingNotes());
|
|
65734
65984
|
return {
|
|
65735
65985
|
source: `${owner}/${repo}`,
|
|
65986
|
+
owner,
|
|
65736
65987
|
files: fetched.filter((f) => !!f),
|
|
65737
65988
|
notes,
|
|
65738
65989
|
paths: plan.paths,
|
|
@@ -65743,7 +65994,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65743
65994
|
notes.push(
|
|
65744
65995
|
`fetch degraded (${err2?.message ?? "network error"}): results ${INCOMPLETE} (the repo could not be fetched).`
|
|
65745
65996
|
);
|
|
65746
|
-
return { source: `${owner}/${repo}`, files: [], notes };
|
|
65997
|
+
return { source: `${owner}/${repo}`, owner, files: [], notes };
|
|
65747
65998
|
}
|
|
65748
65999
|
}
|
|
65749
66000
|
async function fetchTree(input, onProgress) {
|
|
@@ -65906,7 +66157,15 @@ function explainGrant(f) {
|
|
|
65906
66157
|
const git = f.broad.some((a) => /^Bash\(git:/.test(a));
|
|
65907
66158
|
const files = f.broad.some((a) => /^Write|^Edit$/.test(a));
|
|
65908
66159
|
const template = TEMPLATE_PATH_RE.test(f.file);
|
|
65909
|
-
const
|
|
66160
|
+
const runner = f.broad.some((a) => !/^Bash\(git:|^Write|^Edit$/.test(a));
|
|
66161
|
+
const what = f.bareShell ? "run commands and change files" : sentence(
|
|
66162
|
+
[
|
|
66163
|
+
runner ? "run programs" : "",
|
|
66164
|
+
git ? "run git commands" : "",
|
|
66165
|
+
files ? "change files" : ""
|
|
66166
|
+
].filter(Boolean),
|
|
66167
|
+
true
|
|
66168
|
+
);
|
|
65910
66169
|
const saw = [];
|
|
65911
66170
|
if (template) saw.push("This is a template: every project made from it gets these permissions.");
|
|
65912
66171
|
saw.push(
|
|
@@ -65946,14 +66205,17 @@ function npxPackage(argv) {
|
|
|
65946
66205
|
return null;
|
|
65947
66206
|
}
|
|
65948
66207
|
function explainMcp(f) {
|
|
65949
|
-
const pkg = npxPackage(f.argv);
|
|
65950
|
-
const
|
|
66208
|
+
const pkg = f.pkg ?? npxPackage(f.argv);
|
|
66209
|
+
const registry = f.registry ?? "npm";
|
|
66210
|
+
const image = registry === "the image registry";
|
|
66211
|
+
const what = pkg ? `the newest ${code(pkg)}` : image ? "the newest image" : "the newest version of the package";
|
|
66212
|
+
const example = !pkg ? null : image ? `${pkg}@sha256:\u2026` : registry === "PyPI" ? `${pkg}==x.y.z` : registry === "the Go module proxy" ? `${pkg}@vX.Y.Z` : `${pkg}@x.y.z`;
|
|
65951
66213
|
return {
|
|
65952
66214
|
headline: `MCP server ${code(f.name, 60)} installs whatever version is newest, every time`,
|
|
65953
|
-
happens: `Each run downloads
|
|
66215
|
+
happens: `Each run downloads ${what} from ${registry}. If it is ever taken over, the next version runs on every developer's computer.`,
|
|
65954
66216
|
saw: [`It starts with ${code(f.argv, 120)}.`],
|
|
65955
66217
|
fix: [
|
|
65956
|
-
|
|
66218
|
+
example ? `Pin an exact version, for example ${code(example)}.` : "Pin an exact version."
|
|
65957
66219
|
]
|
|
65958
66220
|
};
|
|
65959
66221
|
}
|
|
@@ -65977,6 +66239,65 @@ function parseYamlStrict(text) {
|
|
|
65977
66239
|
return doc.toJS();
|
|
65978
66240
|
}
|
|
65979
66241
|
|
|
66242
|
+
// src/ci-check/frontmatter.ts
|
|
66243
|
+
function parseFrontmatter(content) {
|
|
66244
|
+
const text = content.replace(/^/, "").replace(/^\s*\n/, "");
|
|
66245
|
+
if (!/^---[ \t]*\r?\n/.test(text)) return null;
|
|
66246
|
+
const close = /\r?\n---[ \t]*(\r?\n|$)/.exec(text.slice(3));
|
|
66247
|
+
if (!close) return null;
|
|
66248
|
+
const block = text.slice(3, 3 + close.index);
|
|
66249
|
+
try {
|
|
66250
|
+
const parsed = parseYamlStrict(block);
|
|
66251
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
66252
|
+
} catch {
|
|
66253
|
+
return salvageTopLevel(block);
|
|
66254
|
+
}
|
|
66255
|
+
}
|
|
66256
|
+
function salvageTopLevel(block) {
|
|
66257
|
+
const out = {};
|
|
66258
|
+
const lines = block.split(/\r?\n/);
|
|
66259
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66260
|
+
const m = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i]);
|
|
66261
|
+
if (!m) continue;
|
|
66262
|
+
const [, key, rest] = m;
|
|
66263
|
+
if (rest.trim()) {
|
|
66264
|
+
out[key] = rest.trim();
|
|
66265
|
+
continue;
|
|
66266
|
+
}
|
|
66267
|
+
const items = [];
|
|
66268
|
+
while (i + 1 < lines.length && /^[ \t]+-[ \t]+/.test(lines[i + 1])) {
|
|
66269
|
+
items.push(lines[++i].replace(/^[ \t]+-[ \t]+/, "").trim());
|
|
66270
|
+
}
|
|
66271
|
+
if (items.length) out[key] = items;
|
|
66272
|
+
}
|
|
66273
|
+
return Object.keys(out).length ? out : null;
|
|
66274
|
+
}
|
|
66275
|
+
function splitGrants(value) {
|
|
66276
|
+
const out = [];
|
|
66277
|
+
let depth = 0;
|
|
66278
|
+
let cur = "";
|
|
66279
|
+
for (const ch of value) {
|
|
66280
|
+
if (ch === "(") depth++;
|
|
66281
|
+
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
66282
|
+
if (depth === 0 && (ch === "," || /\s/.test(ch))) {
|
|
66283
|
+
if (cur) out.push(cur);
|
|
66284
|
+
cur = "";
|
|
66285
|
+
continue;
|
|
66286
|
+
}
|
|
66287
|
+
cur += ch;
|
|
66288
|
+
}
|
|
66289
|
+
if (cur) out.push(cur);
|
|
66290
|
+
return out;
|
|
66291
|
+
}
|
|
66292
|
+
function allowedToolsOf(fm) {
|
|
66293
|
+
const raw = fm?.["allowed-tools"];
|
|
66294
|
+
if (typeof raw === "string") return splitGrants(raw);
|
|
66295
|
+
if (Array.isArray(raw)) {
|
|
66296
|
+
return raw.filter((x) => typeof x === "string").map((x) => x.trim()).filter(Boolean);
|
|
66297
|
+
}
|
|
66298
|
+
return [];
|
|
66299
|
+
}
|
|
66300
|
+
|
|
65980
66301
|
// src/ci-check/types.ts
|
|
65981
66302
|
var SEVERITY_RANK2 = {
|
|
65982
66303
|
critical: 4,
|
|
@@ -65985,11 +66306,28 @@ var SEVERITY_RANK2 = {
|
|
|
65985
66306
|
advisory: 1
|
|
65986
66307
|
};
|
|
65987
66308
|
|
|
66309
|
+
// src/ci-check/listing.ts
|
|
66310
|
+
var SAFE_LIST_FIELD_RE = /^(?:number|url|html_url|id|node_id|sha|state|createdat|updatedat|closedat|mergedat|headrefname|baserefname|isdraft|headrefoid|login|user|author_association)$/i;
|
|
66311
|
+
function sanitizedListing(t) {
|
|
66312
|
+
if (t.length > 5e4) return false;
|
|
66313
|
+
if (/\|\s*grep\s+-[a-zA-Z]*o/.test(t)) return true;
|
|
66314
|
+
const fieldsSafe = (expr, fieldRe) => {
|
|
66315
|
+
const fields = [...expr.matchAll(fieldRe)].map((m) => m[1]);
|
|
66316
|
+
return fields.length > 0 && fields.every((f) => SAFE_LIST_FIELD_RE.test(f));
|
|
66317
|
+
};
|
|
66318
|
+
const jq = [
|
|
66319
|
+
...t.matchAll(/(?:--jq|\s-q)[\s=]+(?:'([^']{0,500})'|"([^"]{0,500})"|([^\s'"]{1,200}))/g)
|
|
66320
|
+
].map((m) => m[1] ?? m[2] ?? m[3]);
|
|
66321
|
+
if (jq.length) return jq.every((e) => fieldsSafe(e, /\.([A-Za-z_]\w{0,60})/g));
|
|
66322
|
+
const json = [...t.matchAll(/--json[\s=]+['"]?([\w,]{1,500})/g)].map((m) => m[1]);
|
|
66323
|
+
return json.length > 0 && json.every((e) => fieldsSafe(e, /([A-Za-z_]\w{0,60})/g));
|
|
66324
|
+
}
|
|
66325
|
+
|
|
65988
66326
|
// src/ci-check/workflows.ts
|
|
65989
66327
|
var AGENT_ACTION_RE = /(anthropics\/claude-code(-base)?-action|anthropics\/claude-code|^\s*[\w.-]{1,100}\/claude-code-action@|openai\/codex|codex-action|run-?aider|aider-?action|google-github-actions\/run-gemini)/i;
|
|
65990
66328
|
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;
|
|
65991
66329
|
var EXFIL_RCE_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|sh[\s):]|eval|rm[\s):]|git push)/i;
|
|
65992
|
-
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;
|
|
66330
|
+
var GH_WRITE_TOOL_RE = /Bash\(\s*(gh api|gh:|gh (pr|issue) (comment|edit|merge|close|review|create|ready|lock|reopen)|gh (pr|issue)\s*[:*)]|git push|git:)/i;
|
|
65993
66331
|
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;
|
|
65994
66332
|
var MEMBERSHIP_CHECK_RE = /orgs\/[^/'"\s]+\/memberships\/|teams\/[^/'"\s]+\/memberships\/|getCollaboratorPermissionLevel|collaborators\/[^/'"\s]+\/permission|checkMembershipForUser|getMembershipForUser/i;
|
|
65995
66333
|
var STRANGER_ISSUE_TYPES = /* @__PURE__ */ new Set(["opened", "edited", "reopened", "closed"]);
|
|
@@ -66045,12 +66383,69 @@ function keyStrangerFirable(on, key) {
|
|
|
66045
66383
|
return false;
|
|
66046
66384
|
}
|
|
66047
66385
|
}
|
|
66386
|
+
var AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66387
|
+
function issueFormLabels(content) {
|
|
66388
|
+
const text = content.length > 2e5 ? content.slice(0, 2e5) : content;
|
|
66389
|
+
let value;
|
|
66390
|
+
let parsed = false;
|
|
66391
|
+
if (/^\uFEFF?\s*---[ \t]*\r?\n/.test(text)) {
|
|
66392
|
+
const fm = parseFrontmatter(text);
|
|
66393
|
+
parsed = fm !== null;
|
|
66394
|
+
value = fm?.["labels"];
|
|
66395
|
+
} else {
|
|
66396
|
+
try {
|
|
66397
|
+
const doc = parseYamlStrict(text);
|
|
66398
|
+
parsed = true;
|
|
66399
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc))
|
|
66400
|
+
value = doc["labels"];
|
|
66401
|
+
} catch {
|
|
66402
|
+
}
|
|
66403
|
+
}
|
|
66404
|
+
const raw = parsed ? Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [] : labelLines(text);
|
|
66405
|
+
return raw.filter((l) => typeof l === "string").map(
|
|
66406
|
+
(l) => l.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase()
|
|
66407
|
+
).filter((l) => l && l.length <= 100);
|
|
66408
|
+
}
|
|
66409
|
+
function labelLines(text) {
|
|
66410
|
+
const out = [];
|
|
66411
|
+
const lines = text.split(/\r?\n/);
|
|
66412
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66413
|
+
const m = /^labels:[ \t]*([^#]*)/.exec(lines[i]);
|
|
66414
|
+
if (!m) continue;
|
|
66415
|
+
const inline = m[1].trim();
|
|
66416
|
+
if (inline) {
|
|
66417
|
+
out.push(...inline.replace(/^\[|\]$/g, "").split(","));
|
|
66418
|
+
} else {
|
|
66419
|
+
for (let j = i + 1; j < lines.length && /^\s*-\s/.test(lines[j]); j++)
|
|
66420
|
+
out.push(lines[j].replace(/^\s*-\s/, "").replace(/\s#.*$/, ""));
|
|
66421
|
+
}
|
|
66422
|
+
}
|
|
66423
|
+
return out;
|
|
66424
|
+
}
|
|
66425
|
+
var CALLERS = /* @__PURE__ */ new WeakMap();
|
|
66048
66426
|
function triggerReach(wf, raw) {
|
|
66049
66427
|
const on = onObject(wf, raw) ?? {};
|
|
66050
66428
|
const keys = triggerKeys(wf, raw);
|
|
66051
|
-
const firable = keys.filter(
|
|
66429
|
+
const firable = keys.filter(
|
|
66430
|
+
(k) => keyStrangerFirable(on, k) || /^issues$/i.test(k) && testsAutoLabel(allIfText(wf))
|
|
66431
|
+
);
|
|
66052
66432
|
const untrusted = firable.length > 0;
|
|
66053
66433
|
const reusable = !untrusted && keys.some((k) => /^workflow_call$/i.test(k));
|
|
66434
|
+
const callers = CALLERS.get(wf);
|
|
66435
|
+
const open = (callers ?? []).filter((c) => c.untrusted && !c.gated && !c.dispatched);
|
|
66436
|
+
const unresolved = (callers ?? []).some(
|
|
66437
|
+
(c) => c.untrusted && !c.gated && c.dispatched || c.reusable && !c.gated
|
|
66438
|
+
);
|
|
66439
|
+
if (reusable && callers?.length && (open.length || !unresolved)) {
|
|
66440
|
+
return {
|
|
66441
|
+
keys: [...new Set(open.flatMap((c) => c.keys))],
|
|
66442
|
+
untrusted: open.length > 0,
|
|
66443
|
+
reusable: false,
|
|
66444
|
+
secretExposed: open.some((c) => c.secretExposed),
|
|
66445
|
+
privileged: open.some((c) => c.privileged),
|
|
66446
|
+
workflowRun: open.some((c) => c.workflowRun)
|
|
66447
|
+
};
|
|
66448
|
+
}
|
|
66054
66449
|
const secretExposed = firable.some((t) => /pull_request_target|workflow_run/i.test(t));
|
|
66055
66450
|
const privileged = untrusted && firable.some(
|
|
66056
66451
|
(t) => /pull_request_target|pull_request_review|workflow_run|issue|discussion/i.test(t)
|
|
@@ -66061,6 +66456,160 @@ function triggerReach(wf, raw) {
|
|
|
66061
66456
|
function jobList(wf) {
|
|
66062
66457
|
return Object.values(wf.jobs ?? {}).filter((j) => j != null);
|
|
66063
66458
|
}
|
|
66459
|
+
var ENV_REF_RE = /\$\{\{\s*env(?:\.([A-Za-z_][A-Za-z0-9_]{0,100})|\[\s*['"]([A-Za-z_][A-Za-z0-9_]{0,100})['"]\s*\])\s*\}\}/g;
|
|
66460
|
+
var ENV_REF_HOPS = 3;
|
|
66461
|
+
var RESOLVED_MAX_LEN = 1e5;
|
|
66462
|
+
function resolveEnvRefs(v, envs) {
|
|
66463
|
+
const holderOf = (name) => envs.find((e) => e != null && typeof e === "object" && name in e);
|
|
66464
|
+
for (let hop = 0; hop < ENV_REF_HOPS && v.includes("env"); hop++) {
|
|
66465
|
+
let projected = v.length;
|
|
66466
|
+
let any = false;
|
|
66467
|
+
for (const m of v.matchAll(ENV_REF_RE)) {
|
|
66468
|
+
const name = m[1] ?? m[2];
|
|
66469
|
+
const holder = holderOf(name);
|
|
66470
|
+
if (holder) {
|
|
66471
|
+
any = true;
|
|
66472
|
+
projected += str(holder[name]).length - m[0].length;
|
|
66473
|
+
}
|
|
66474
|
+
if (projected > RESOLVED_MAX_LEN) return v;
|
|
66475
|
+
}
|
|
66476
|
+
if (!any) break;
|
|
66477
|
+
v = v.replace(ENV_REF_RE, (whole, a, b) => {
|
|
66478
|
+
const name = a ?? b;
|
|
66479
|
+
const holder = holderOf(name);
|
|
66480
|
+
return holder ? str(holder[name]) : whole;
|
|
66481
|
+
});
|
|
66482
|
+
}
|
|
66483
|
+
return v;
|
|
66484
|
+
}
|
|
66485
|
+
function ifNeverTrue(v) {
|
|
66486
|
+
if (v === false) return true;
|
|
66487
|
+
if (typeof v !== "string") return false;
|
|
66488
|
+
const never = (x2) => {
|
|
66489
|
+
switch (x2.k) {
|
|
66490
|
+
case "atom":
|
|
66491
|
+
return /^false$/i.test(x2.t.trim());
|
|
66492
|
+
case "not":
|
|
66493
|
+
return false;
|
|
66494
|
+
case "and":
|
|
66495
|
+
return x2.c.some(never);
|
|
66496
|
+
case "or":
|
|
66497
|
+
return x2.c.every(never);
|
|
66498
|
+
}
|
|
66499
|
+
};
|
|
66500
|
+
const x = parseIf(v);
|
|
66501
|
+
return x !== null && never(x);
|
|
66502
|
+
}
|
|
66503
|
+
function normalizeWorkflow(raw) {
|
|
66504
|
+
const wf = raw;
|
|
66505
|
+
const wfEnv = raw.env;
|
|
66506
|
+
const jobs = wf.jobs;
|
|
66507
|
+
if (!jobs || typeof jobs !== "object") return wf;
|
|
66508
|
+
const wfEnvEvent = EVENT_CONTEXT_RE.test(str(wfEnv));
|
|
66509
|
+
for (const [name, job] of Object.entries(jobs)) {
|
|
66510
|
+
if (job == null || typeof job !== "object") continue;
|
|
66511
|
+
if (ifNeverTrue(job.if)) {
|
|
66512
|
+
delete jobs[name];
|
|
66513
|
+
continue;
|
|
66514
|
+
}
|
|
66515
|
+
if (!Array.isArray(job.steps)) continue;
|
|
66516
|
+
job.steps = job.steps.map(
|
|
66517
|
+
(st) => st != null && typeof st === "object" && !Array.isArray(st) ? { ...st } : st
|
|
66518
|
+
);
|
|
66519
|
+
const scopeEnv = {
|
|
66520
|
+
...wfEnv && typeof wfEnv === "object" ? wfEnv : {},
|
|
66521
|
+
...job.env && typeof job.env === "object" ? job.env : {}
|
|
66522
|
+
};
|
|
66523
|
+
const scopeEnvEvent = wfEnvEvent || EVENT_CONTEXT_RE.test(str(job.env));
|
|
66524
|
+
for (const step of job.steps) {
|
|
66525
|
+
if (step == null || typeof step !== "object") continue;
|
|
66526
|
+
step.scopeEnv = scopeEnv;
|
|
66527
|
+
step.scopeEnvEvent = scopeEnvEvent;
|
|
66528
|
+
if (step.with == null || typeof step.with !== "object") continue;
|
|
66529
|
+
const out = {};
|
|
66530
|
+
for (const [k, v] of Object.entries(step.with)) {
|
|
66531
|
+
const key = k.toLowerCase();
|
|
66532
|
+
if (key in out) continue;
|
|
66533
|
+
out[key] = typeof v === "string" ? resolveEnvRefs(v, [step.env, job.env, wfEnv]) : v;
|
|
66534
|
+
}
|
|
66535
|
+
step.with = out;
|
|
66536
|
+
}
|
|
66537
|
+
}
|
|
66538
|
+
return wf;
|
|
66539
|
+
}
|
|
66540
|
+
var LOADED = null;
|
|
66541
|
+
function loadWorkflow(content) {
|
|
66542
|
+
if (LOADED !== null && LOADED.content === content) return LOADED;
|
|
66543
|
+
const raw = parseYamlStrict(content) ?? {};
|
|
66544
|
+
const wf = raw && typeof raw === "object" ? normalizeWorkflow(raw) : raw;
|
|
66545
|
+
LOADED = { content, raw, wf };
|
|
66546
|
+
return LOADED;
|
|
66547
|
+
}
|
|
66548
|
+
var LOCAL_WORKFLOW_USES_RE = /^\s*\.\/\.github\/workflows\/([\w.-]{1,200}\.ya?ml)\s*$/i;
|
|
66549
|
+
function localCallsOf(path91, content, autoLabels) {
|
|
66550
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
66551
|
+
try {
|
|
66552
|
+
return localCallsOfInner(path91, content);
|
|
66553
|
+
} finally {
|
|
66554
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66555
|
+
}
|
|
66556
|
+
}
|
|
66557
|
+
function localCallsOfInner(path91, content) {
|
|
66558
|
+
let raw;
|
|
66559
|
+
try {
|
|
66560
|
+
raw = loadWorkflow(content).raw;
|
|
66561
|
+
} catch {
|
|
66562
|
+
return [];
|
|
66563
|
+
}
|
|
66564
|
+
if (!raw || typeof raw !== "object") return [];
|
|
66565
|
+
const wf = raw;
|
|
66566
|
+
CALLERS.delete(wf);
|
|
66567
|
+
const reach = triggerReach(wf, raw);
|
|
66568
|
+
const out = [];
|
|
66569
|
+
for (const job of jobList(wf)) {
|
|
66570
|
+
const m = LOCAL_WORKFLOW_USES_RE.exec(str(job.uses));
|
|
66571
|
+
if (!m) continue;
|
|
66572
|
+
out.push({
|
|
66573
|
+
target: `.github/workflows/${m[1]}`,
|
|
66574
|
+
caller: {
|
|
66575
|
+
file: path91,
|
|
66576
|
+
keys: reach.keys,
|
|
66577
|
+
untrusted: reach.untrusted,
|
|
66578
|
+
secretExposed: reach.secretExposed,
|
|
66579
|
+
privileged: reach.privileged,
|
|
66580
|
+
workflowRun: reach.workflowRun,
|
|
66581
|
+
gated: jobGated(job, wf, raw),
|
|
66582
|
+
dispatched: dispatchedByGatedJob(job, wf, raw),
|
|
66583
|
+
reusable: reach.reusable
|
|
66584
|
+
}
|
|
66585
|
+
});
|
|
66586
|
+
}
|
|
66587
|
+
return out;
|
|
66588
|
+
}
|
|
66589
|
+
function dispatchedByGatedJob(job, wf, raw) {
|
|
66590
|
+
const names = [...str(job.if).matchAll(/needs\.([\w-]{1,100})\.outputs\./g)].map((m) => m[1]);
|
|
66591
|
+
if (!names.length) return false;
|
|
66592
|
+
const jobs = wf.jobs ?? {};
|
|
66593
|
+
const label3 = labelTypeConfigured(wf, raw);
|
|
66594
|
+
const someBranchGated = (x) => {
|
|
66595
|
+
switch (x.k) {
|
|
66596
|
+
case "atom":
|
|
66597
|
+
return exprGated(x, label3);
|
|
66598
|
+
case "not":
|
|
66599
|
+
return false;
|
|
66600
|
+
case "and":
|
|
66601
|
+
case "or":
|
|
66602
|
+
return x.c.some(someBranchGated);
|
|
66603
|
+
}
|
|
66604
|
+
};
|
|
66605
|
+
return names.every((n) => {
|
|
66606
|
+
const up = jobs[n];
|
|
66607
|
+
if (up == null) return false;
|
|
66608
|
+
if (jobGated(up, wf, raw)) return true;
|
|
66609
|
+
const x = parseIf(str(up.if));
|
|
66610
|
+
return x !== null && someBranchGated(x);
|
|
66611
|
+
});
|
|
66612
|
+
}
|
|
66064
66613
|
function allSteps(wf) {
|
|
66065
66614
|
const out = [];
|
|
66066
66615
|
for (const job of jobList(wf)) {
|
|
@@ -66068,11 +66617,47 @@ function allSteps(wf) {
|
|
|
66068
66617
|
}
|
|
66069
66618
|
return out;
|
|
66070
66619
|
}
|
|
66620
|
+
var AGENT_CLI_RE = new RegExp(
|
|
66621
|
+
[
|
|
66622
|
+
// The binary at a command start, then its "do work" flag anywhere on the same command
|
|
66623
|
+
// (`claude --model x -p`, `claude "$Q" --print`, `npx @anthropic-ai/claude-code -p`): the
|
|
66624
|
+
// flag had to come first (review, round 2). Bounded: at most 300 characters per command.
|
|
66625
|
+
String.raw`(?:^|[\s|;&(])(?:claude|@anthropic-ai/claude-code(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\s(?:-p|--print|--continue|--resume)(?=[\s"'=]|$)`,
|
|
66626
|
+
String.raw`(?:^|[\s|;&(])(?:codex|@openai/codex(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\sexec\b`,
|
|
66627
|
+
String.raw`(?:^|[\s|;&(])(?:gemini|@google/gemini-cli(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\s(?:-p|--prompt)(?=[\s"'=]|$)`,
|
|
66628
|
+
String.raw`(?:^|[\s|;&(])copilot(?=\s)[^\n;&|]{0,300}?\s(?:-p|--prompt|--allow-all-tools)(?=[\s"'=]|$)`,
|
|
66629
|
+
String.raw`\b(?:aider\s|opencode\s+run\b|goose\s+run\b|kilo\s+run\b|pi\s+--print\b|llmproc\s|acpx\s|harbor\s+(?:check|run)\b)`,
|
|
66630
|
+
String.raw`api\.devin\.ai/|software-agent-sdk|\bagent_script\.py\b`
|
|
66631
|
+
].join("|"),
|
|
66632
|
+
"i"
|
|
66633
|
+
);
|
|
66634
|
+
var RUN_FUEL_ENV_RE = /^(ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|CLAUDE_CODE_OAUTH_TOKEN|OPENAI_API_KEY|GEMINI_API_KEY|GOOGLE_API_KEY|KILO_API_KEY|OPENCODE_API_KEY|DEVIN_API_KEY|LLM_API_KEY|AWS_BEARER_TOKEN_BEDROCK)$/i;
|
|
66635
|
+
var EVENT_CONTEXT_RE = /github\.event|context\.payload/;
|
|
66636
|
+
function isRunAgent(step) {
|
|
66637
|
+
return typeof step.run === "string" && !step.uses && AGENT_CLI_RE.test(step.run);
|
|
66638
|
+
}
|
|
66639
|
+
var FUEL_WITH_KEY_RE = /^(anthropic[_-]api[_-]key|anthropic[_-]auth[_-]token|claude[_-]code[_-]oauth[_-]token|claude[_-]oauth[_-]token|openai[_-]api[_-]key|gemini[_-]api[_-]key|google[_-]api[_-]key)$/i;
|
|
66640
|
+
var AGENT_PATH_RE = /(?:^|[/_.-])(claude|codex|gemini|opencode|goose|aider|kilo(?:code)?)(?:$|[/_.-])/i;
|
|
66641
|
+
var PROMPT_INPUT_RE = /^(prompt|direct_prompt|prompt_file|prompt-file|agent)$/i;
|
|
66642
|
+
function isWrapperAgent(step) {
|
|
66643
|
+
const uses = str(step.uses);
|
|
66644
|
+
const w = step.with ?? {};
|
|
66645
|
+
if (Object.keys(w).some((k) => FUEL_WITH_KEY_RE.test(k))) return true;
|
|
66646
|
+
const env = { ...step.scopeEnv ?? {}, ...step.env ?? {} };
|
|
66647
|
+
const fuelEnv = Object.keys(env).some((k) => RUN_FUEL_ENV_RE.test(k));
|
|
66648
|
+
if (/^\s*\.\//.test(uses)) return fuelEnv;
|
|
66649
|
+
const pathPart = uses.replace(/^\s*[^/@\s]{1,100}\//, "").replace(/@.*$/, "");
|
|
66650
|
+
return AGENT_PATH_RE.test(pathPart) && (Object.keys(w).some((k) => PROMPT_INPUT_RE.test(k)) || fuelEnv);
|
|
66651
|
+
}
|
|
66071
66652
|
function isAgentStep(step) {
|
|
66072
66653
|
if (step.uses && AGENT_ACTION_RE.test(step.uses)) return true;
|
|
66073
|
-
if (step.
|
|
66654
|
+
if (step.uses && isWrapperAgent(step)) return true;
|
|
66655
|
+
if (typeof step.run !== "string" || !step.run) return false;
|
|
66656
|
+
if (/\b(claude|aider|codex|gemini)\b/i.test(step.run) && /github\.event/.test(step.run))
|
|
66074
66657
|
return true;
|
|
66075
|
-
return false;
|
|
66658
|
+
if (!AGENT_CLI_RE.test(step.run)) return false;
|
|
66659
|
+
const env = { ...step.scopeEnv ?? {}, ...step.env ?? {} };
|
|
66660
|
+
return !!step.scopeEnvEvent || EVENT_CONTEXT_RE.test(str(step.env)) || Object.keys(env).some((k) => RUN_FUEL_ENV_RE.test(k));
|
|
66076
66661
|
}
|
|
66077
66662
|
function allowedToolsFromArgs(claudeArgs) {
|
|
66078
66663
|
let out = "";
|
|
@@ -66092,12 +66677,47 @@ function collectTools(steps) {
|
|
|
66092
66677
|
let s = "";
|
|
66093
66678
|
for (const st of steps) {
|
|
66094
66679
|
const w = st.with ?? {};
|
|
66095
|
-
|
|
66096
|
-
s += " " +
|
|
66680
|
+
const args = str(w["claude_args"]) + " " + str(w["codex-args"]) + " " + str(w["codex_args"]);
|
|
66681
|
+
s += " " + allowedToolsFromArgs(args);
|
|
66682
|
+
s += " " + str(w["allowed_tools"]) + " " + str(w["allowedtools"]);
|
|
66097
66683
|
s += " " + allowedToolsFromSettings(str(w["settings"]));
|
|
66684
|
+
if (PERMISSION_BYPASS_RE.test(args) || /^danger-full-access$/i.test(str(w["sandbox"])))
|
|
66685
|
+
s += " Bash";
|
|
66686
|
+
if (st.run && isRunAgent(st)) {
|
|
66687
|
+
s += " " + allowedToolsFromArgs(st.run);
|
|
66688
|
+
if (PERMISSION_BYPASS_RE.test(st.run)) s += " Bash";
|
|
66689
|
+
s += " " + permissionEnvGrants({ ...st.scopeEnv ?? {}, ...st.env ?? {} });
|
|
66690
|
+
}
|
|
66098
66691
|
}
|
|
66099
66692
|
return s;
|
|
66100
66693
|
}
|
|
66694
|
+
var PERMISSION_BYPASS_RE = /--dangerously-skip-permissions|--permission-mode[=\s]+["']?bypassPermissions|--dangerously-bypass-approvals-and-sandbox|--allow-all-tools|--yolo\b/i;
|
|
66695
|
+
function permissionEnvGrants(env) {
|
|
66696
|
+
let out = "";
|
|
66697
|
+
for (const key of ["OPENCODE_PERMISSION", "KILO_PERMISSION"]) {
|
|
66698
|
+
const raw = str(env[key]);
|
|
66699
|
+
if (!raw || raw.length > 2e4) continue;
|
|
66700
|
+
let cfg;
|
|
66701
|
+
try {
|
|
66702
|
+
cfg = JSON.parse(raw);
|
|
66703
|
+
} catch {
|
|
66704
|
+
continue;
|
|
66705
|
+
}
|
|
66706
|
+
if (!cfg || typeof cfg !== "object") continue;
|
|
66707
|
+
const c = cfg;
|
|
66708
|
+
const bash = c["bash"];
|
|
66709
|
+
if (bash === "allow") out += " Bash";
|
|
66710
|
+
else if (bash && typeof bash === "object") {
|
|
66711
|
+
for (const [pat, v] of Object.entries(bash)) {
|
|
66712
|
+
if (v !== "allow") continue;
|
|
66713
|
+
const cmd = pat.replace(/\*+\s*$/, "").trim();
|
|
66714
|
+
out += cmd ? ` Bash(${cmd}:*)` : " Bash";
|
|
66715
|
+
}
|
|
66716
|
+
}
|
|
66717
|
+
if (c["webfetch"] === "allow") out += " WebFetch";
|
|
66718
|
+
}
|
|
66719
|
+
return out;
|
|
66720
|
+
}
|
|
66101
66721
|
function toolTokens(blob) {
|
|
66102
66722
|
const out = [];
|
|
66103
66723
|
let buf = "";
|
|
@@ -66130,11 +66750,21 @@ function matchedBroadTools(blob) {
|
|
|
66130
66750
|
}
|
|
66131
66751
|
return out;
|
|
66132
66752
|
}
|
|
66753
|
+
var GH_PR_CHECKOUT_RE = /\bgh\s+pr\s+checkout\b/;
|
|
66754
|
+
var FETCH_PULL_RE = /\bgit\s+fetch\b[^\n]{0,200}\bpull\/[^\s/]{1,80}\/(?:head|merge)\b/;
|
|
66755
|
+
var GIT_CHECKOUT_RE = /\bgit\s+(?:checkout|switch)\b/;
|
|
66756
|
+
function runChecksOutHead(run) {
|
|
66757
|
+
if (GH_PR_CHECKOUT_RE.test(run)) return true;
|
|
66758
|
+
const m = FETCH_PULL_RE.exec(run);
|
|
66759
|
+
return m !== null && GIT_CHECKOUT_RE.test(run.slice(m.index));
|
|
66760
|
+
}
|
|
66133
66761
|
function untrustedHeadCheckout(steps) {
|
|
66134
66762
|
for (const step of steps) {
|
|
66763
|
+
if (!step.uses && typeof step.run === "string" && runChecksOutHead(step.run)) return "root";
|
|
66135
66764
|
if (!step.uses || !/actions\/checkout/.test(step.uses)) continue;
|
|
66136
66765
|
const ref = str(step.with?.["ref"]);
|
|
66137
|
-
if (/
|
|
66766
|
+
if (/refs\/pull\//.test(ref)) return step.with?.["path"] ? "subdir" : "root";
|
|
66767
|
+
if (/pull_request\.head|head[._-]sha|head_ref|expected_head|workflow_run\.head|inputs(?:\.|\[\s*['"])[\w-]*head/i.test(
|
|
66138
66768
|
ref
|
|
66139
66769
|
)) {
|
|
66140
66770
|
return step.with?.["path"] ? "subdir" : "root";
|
|
@@ -66144,22 +66774,61 @@ function untrustedHeadCheckout(steps) {
|
|
|
66144
66774
|
}
|
|
66145
66775
|
function promptTakesUntrusted(steps) {
|
|
66146
66776
|
for (const st of steps) {
|
|
66147
|
-
const blob = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + collectTools([st])
|
|
66777
|
+
const blob = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + collectTools([st]) + // T.2a: a run-step agent's prompt is its command line and the env it reads.
|
|
66778
|
+
(isRunAgent(st) ? " " + st.run + " " + str(st.env) + " " + str(st.scopeEnv) : "");
|
|
66148
66779
|
if (/github\.event\.(issue|comment|pull_request|review)\.(body|title)/i.test(blob)) return true;
|
|
66149
|
-
if (/pull_request\.number
|
|
66780
|
+
if (/pull_request\.number|(?<![\w./-])\/(?:code-)?review(?![\w-])/i.test(blob)) return true;
|
|
66150
66781
|
}
|
|
66151
66782
|
return false;
|
|
66152
66783
|
}
|
|
66153
66784
|
var ITEM_READ_TOOL_RE = /mcp__github__(get_issue|get_issue_comments|get_pull_request|get_pull_request_diff|get_pull_request_files|get_pull_request_comments|get_pull_request_reviews|search_issues|list_issues)\b|Bash\(\s*gh\s+(issue|pr)\s+(view|diff|list)\b|Bash\(\s*gh\s+(api|search)\b|Bash\(\s*gh\s*:/i;
|
|
66154
66785
|
var ITEM_HANDLE_RE = /github\.event\.(issue|pull_request|discussion)\.number|github\.event\.number\b|github\.event\.comment\.id/i;
|
|
66155
66786
|
var PAYLOAD_TEXT_RE = /github\.event\.(issue|comment|pull_request|review|discussion)\.(body|title)|context\.payload\.(issue|comment|pull_request|review|discussion)\b|GITHUB_EVENT_PATH|github\.event_path/i;
|
|
66787
|
+
var RUN_ARTIFACT_RE = /gh\s+run\s+download\b|\/runs\/\$\{\{\s*github\.event\.workflow_run\.id\s*\}\}\/(?:artifacts|logs)/i;
|
|
66788
|
+
var TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
66789
|
+
var UPSTREAM_TRUSTED = false;
|
|
66790
|
+
function upstreamRunTrusted(wf, raw) {
|
|
66791
|
+
const on = onObject(wf, raw);
|
|
66792
|
+
const run = on?.["workflow_run"];
|
|
66793
|
+
const names = Array.isArray(run?.workflows) ? run.workflows.map(String) : [];
|
|
66794
|
+
return names.length > 0 && names.every((n) => TRUSTED_RUN_SOURCES.has(n));
|
|
66795
|
+
}
|
|
66796
|
+
function runSourceOf(path91, content, autoLabels) {
|
|
66797
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
66798
|
+
try {
|
|
66799
|
+
let raw;
|
|
66800
|
+
try {
|
|
66801
|
+
raw = loadWorkflow(content).raw;
|
|
66802
|
+
} catch {
|
|
66803
|
+
return null;
|
|
66804
|
+
}
|
|
66805
|
+
if (!raw || typeof raw !== "object") return null;
|
|
66806
|
+
const wf = raw;
|
|
66807
|
+
CALLERS.delete(wf);
|
|
66808
|
+
const reach = triggerReach(wf, raw);
|
|
66809
|
+
const name = typeof raw["name"] === "string" && raw["name"] ? raw["name"] : path91;
|
|
66810
|
+
return { name, trusted: !reach.untrusted && !reach.reusable };
|
|
66811
|
+
} finally {
|
|
66812
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66813
|
+
}
|
|
66814
|
+
}
|
|
66815
|
+
function downloadsRunArtifacts(st) {
|
|
66816
|
+
if (UPSTREAM_TRUSTED) return false;
|
|
66817
|
+
if (/actions\/download-artifact/i.test(str(st.uses)))
|
|
66818
|
+
return /workflow_run\.id/.test(str(st.with?.["run-id"]) + " " + str(st.with?.["run_id"]));
|
|
66819
|
+
return RUN_ARTIFACT_RE.test(str(st.run) + " " + str(st.with?.["script"]));
|
|
66820
|
+
}
|
|
66821
|
+
var STEP_OUTPUT_REF_RE = /steps\.([\w-]{1,100})\.outputs\./g;
|
|
66156
66822
|
function jobReadsUntrustedItem(job) {
|
|
66157
66823
|
const steps = job.steps ?? [];
|
|
66158
66824
|
const agent = steps.filter(isAgentStep);
|
|
66159
66825
|
if (!agent.length) return null;
|
|
66160
66826
|
if (ITEM_READ_TOOL_RE.test(collectTools(agent))) return "tool";
|
|
66161
66827
|
for (const st of agent)
|
|
66162
|
-
if (ITEM_HANDLE_RE.test(
|
|
66828
|
+
if (ITEM_HANDLE_RE.test(
|
|
66829
|
+
str(st.with) + " " + str(st.env) + " " + str(job.env) + " " + (isRunAgent(st) ? str(st.run) : "")
|
|
66830
|
+
))
|
|
66831
|
+
return "handle";
|
|
66163
66832
|
for (let i = 0; i < steps.length; i++) {
|
|
66164
66833
|
const st = steps[i];
|
|
66165
66834
|
if (!isAgentStep(st) || !str(st.with?.["prompt_file"] ?? st.with?.["prompt-file"])) continue;
|
|
@@ -66167,6 +66836,20 @@ function jobReadsUntrustedItem(job) {
|
|
|
66167
66836
|
if (earlier.some((p) => PAYLOAD_TEXT_RE.test(str(p.run) + " " + str(p.with) + " " + str(p.env))))
|
|
66168
66837
|
return "prompt-file";
|
|
66169
66838
|
}
|
|
66839
|
+
const firstDownload = steps.findIndex(downloadsRunArtifacts);
|
|
66840
|
+
if (firstDownload >= 0 && steps.slice(firstDownload + 1).some(isAgentStep)) return "artifact";
|
|
66841
|
+
for (let i = 0; i < steps.length; i++) {
|
|
66842
|
+
const st = steps[i];
|
|
66843
|
+
if (!isAgentStep(st)) continue;
|
|
66844
|
+
const input = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + str(st.with?.["prompt_file"] ?? st.with?.["prompt-file"]);
|
|
66845
|
+
const ids = /* @__PURE__ */ new Set();
|
|
66846
|
+
for (const m of input.matchAll(STEP_OUTPUT_REF_RE)) ids.add(m[1]);
|
|
66847
|
+
for (const id of ids) {
|
|
66848
|
+
const src = steps.slice(0, i).find((p) => p.id === id);
|
|
66849
|
+
if (src && PAYLOAD_TEXT_RE.test(str(src.run) + " " + str(src.with) + " " + str(src.env)))
|
|
66850
|
+
return "step-output";
|
|
66851
|
+
}
|
|
66852
|
+
}
|
|
66170
66853
|
return null;
|
|
66171
66854
|
}
|
|
66172
66855
|
var NONCONTAINS_GATE_RE = new RegExp(
|
|
@@ -66179,19 +66862,46 @@ var NONCONTAINS_GATE_RE = new RegExp(
|
|
|
66179
66862
|
].join("|"),
|
|
66180
66863
|
"i"
|
|
66181
66864
|
);
|
|
66182
|
-
var
|
|
66865
|
+
var PERMISSION_OUTPUT_RE = /steps\.[\w-]{1,100}\.outputs\.([\w-]{1,200})\s*==\s*['"]?(?:true|admin|write|maintain)/gi;
|
|
66866
|
+
var PERMISSION_OUTPUT_NAME_RE = /permission|allowed|authoriz|is[_-]?(?:admin|member|maintainer|collaborator)/i;
|
|
66867
|
+
var PERMISSION_OUTPUT_GATE_RE = {
|
|
66868
|
+
test(ifs) {
|
|
66869
|
+
for (const m of ifs.matchAll(PERMISSION_OUTPUT_RE))
|
|
66870
|
+
if (PERMISSION_OUTPUT_NAME_RE.test(m[1])) return true;
|
|
66871
|
+
return false;
|
|
66872
|
+
}
|
|
66873
|
+
};
|
|
66183
66874
|
var CONTAINS_GATE_RE = /contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
|
|
66184
66875
|
var NEGATED_CONTAINS_RE = /!\s*\(?\s*contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
|
|
66185
66876
|
function labelTypeConfigured(wf, raw) {
|
|
66186
66877
|
const on = wf.on ?? raw["on"] ?? raw[true];
|
|
66187
66878
|
const prTypes = (t) => t && Array.isArray(t.types) ? t.types.map(String) : [];
|
|
66188
|
-
|
|
66879
|
+
const pr = prTypes(on?.["pull_request_target"]).includes("labeled") || prTypes(on?.["pull_request"]).includes("labeled");
|
|
66880
|
+
const onObj = onObject(wf, raw) ?? {};
|
|
66881
|
+
const issues = on != null && typeof on === "object" && "issues" in on && triggerKeys(wf, raw).every((k) => /^issues$/i.test(k) || !keyStrangerFirable(onObj, k));
|
|
66882
|
+
return { pr, issues };
|
|
66883
|
+
}
|
|
66884
|
+
var EVENT_LABEL_GATE_RE = /\blabel\.name\s*==\s*'|\b(?:contains|startsWith)\(\s*github\.event\.label\.name\b/i;
|
|
66885
|
+
var ISSUE_LABEL_GATE_RE = /(?<!!\s{0,5}\(?\s{0,5})contains\(\s*github\.event\.issue\.labels\.\*\.name\s*,\s*'[^']{1,100}'\s*\)(?!\s*(?:==\s*false|!=\s*true))/i;
|
|
66886
|
+
var PR_LABEL_GATE_RE = /(?<!!\s{0,5}\(?\s{0,5})contains\(\s*github\.event\.(?:pull_request|issue)\.labels\.\*\.name\s*,\s*'[^']{1,100}'\s*\)(?!\s*(?:==\s*false|!=\s*true))/i;
|
|
66887
|
+
var NONWRITE_SET_RE = /contains\(\s*(?:fromjson\(\s*['"]\[[^\]]{0,400}|'[^']{0,400})\b(CONTRIBUTOR|FIRST_TIME_CONTRIBUTOR|FIRST_TIMER|NONE|MANNEQUIN)\b/i;
|
|
66888
|
+
function allIfText(wf) {
|
|
66889
|
+
const s = jobList(wf).map((j) => str(j.if) + " " + (j.steps ?? []).map((st) => str(st.if)).join(" ")).join(" ");
|
|
66890
|
+
return s.length > IF_MAX_LEN ? "" : s;
|
|
66891
|
+
}
|
|
66892
|
+
function testsAutoLabel(ifs) {
|
|
66893
|
+
if (AUTO_LABELS.size === 0) return false;
|
|
66894
|
+
for (const m of ifs.matchAll(
|
|
66895
|
+
/labels\.\*\.name\s*,\s*'([^']{1,100})'|label\.name\s*==\s*'([^']{1,100})'/gi
|
|
66896
|
+
))
|
|
66897
|
+
if (AUTO_LABELS.has((m[1] ?? m[2]).toLowerCase())) return true;
|
|
66898
|
+
return false;
|
|
66189
66899
|
}
|
|
66190
|
-
|
|
66191
|
-
|
|
66192
|
-
const containsGate = CONTAINS_GATE_RE.test(
|
|
66193
|
-
const gated = NONCONTAINS_GATE_RE.test(
|
|
66194
|
-
const labelGated =
|
|
66900
|
+
function ifsAreGated(ifs, label3) {
|
|
66901
|
+
const m = maskLiterals(ifs);
|
|
66902
|
+
const containsGate = CONTAINS_GATE_RE.test(m) && !NEGATED_CONTAINS_RE.test(m) && !NONWRITE_SET_RE.test(ifs);
|
|
66903
|
+
const gated = NONCONTAINS_GATE_RE.test(m) || containsGate;
|
|
66904
|
+
const labelGated = ((label3.pr || label3.issues) && EVENT_LABEL_GATE_RE.test(m) || label3.pr && PR_LABEL_GATE_RE.test(m) || label3.issues && ISSUE_LABEL_GATE_RE.test(m)) && !testsAutoLabel(ifs);
|
|
66195
66905
|
return gated || labelGated;
|
|
66196
66906
|
}
|
|
66197
66907
|
function hasStepMembershipGate(job) {
|
|
@@ -66212,6 +66922,7 @@ function parseIf(src) {
|
|
|
66212
66922
|
let e = src.trim();
|
|
66213
66923
|
const wrapped = /^\$\{\{([\s\S]*)\}\}$/.exec(e);
|
|
66214
66924
|
if (wrapped) e = wrapped[1].trim();
|
|
66925
|
+
if (e.includes("${{")) return null;
|
|
66215
66926
|
if (!e || e.length > IF_MAX_LEN) return null;
|
|
66216
66927
|
let i = 0;
|
|
66217
66928
|
let depth = 0;
|
|
@@ -66297,12 +67008,21 @@ function stepIfText(steps) {
|
|
|
66297
67008
|
const s = steps.map((st) => str(st.if)).join(" ");
|
|
66298
67009
|
return s.length > IF_MAX_LEN ? "" : s;
|
|
66299
67010
|
}
|
|
67011
|
+
function maskLiterals(ifs) {
|
|
67012
|
+
return ifs.replace(
|
|
67013
|
+
/(==|!=)?(\s*)'(?:[^']|'')*'/g,
|
|
67014
|
+
(m, op, sp) => op ? m : `${sp}'_'`
|
|
67015
|
+
);
|
|
67016
|
+
}
|
|
67017
|
+
var CHAINED_COMPARE_RE = /(?:==|!=)[^=!]*(?:==|!=)/;
|
|
66300
67018
|
function gatedText(ifs, labelConfigured) {
|
|
66301
|
-
|
|
67019
|
+
const masked = maskLiterals(ifs);
|
|
67020
|
+
return ifsAreGated(ifs, labelConfigured) || ASSIGNEE_GATE_RE.test(masked) || PERMISSION_OUTPUT_GATE_RE.test(masked);
|
|
66302
67021
|
}
|
|
66303
67022
|
function exprGated(x, labelConfigured) {
|
|
66304
67023
|
switch (x.k) {
|
|
66305
67024
|
case "atom":
|
|
67025
|
+
if (CHAINED_COMPARE_RE.test(maskLiterals(x.t))) return false;
|
|
66306
67026
|
return gatedText(x.t, labelConfigured) || TRUSTED_EVENT_ATOM_RE.test(x.t);
|
|
66307
67027
|
case "not":
|
|
66308
67028
|
return false;
|
|
@@ -66338,11 +67058,43 @@ function jobOnWorkflowRun(job, workflowRun) {
|
|
|
66338
67058
|
}
|
|
66339
67059
|
function jobActorGate(job, wf, raw) {
|
|
66340
67060
|
const labelConfigured = labelTypeConfigured(wf, raw);
|
|
66341
|
-
const
|
|
66342
|
-
|
|
67061
|
+
const agent = (job.steps ?? []).filter(isAgentStep);
|
|
67062
|
+
const ifTotal = agent.reduce((n, s) => n + str(s.if).length, 0);
|
|
67063
|
+
const agentStepsGated = agent.length > 0 && ifTotal <= IF_MAX_LEN && agent.every((s) => {
|
|
67064
|
+
const x = parseIf(str(s.if));
|
|
67065
|
+
return x !== null && exprGated(x, labelConfigured);
|
|
67066
|
+
});
|
|
67067
|
+
return jobIfGated(str(job.if), labelConfigured) || agentStepsGated || hasStepMembershipGate(job);
|
|
67068
|
+
}
|
|
67069
|
+
var ACTOR_ALLOWLIST_RE = /\.includes\(\s*(?:context\.actor|github\.actor|context\.payload\.sender\.login|[\w.]{0,40}user\.login|login)\b|\b(?:context\.actor|github\.actor)\s*===?\s*['"]/;
|
|
67070
|
+
var ACTOR_ALLOWLIST_G = new RegExp(ACTOR_ALLOWLIST_RE.source, "g");
|
|
67071
|
+
var BLOCK_LIST_NAME_RE = /block|deny|ban|spam|abuse|exclu|ignore/i;
|
|
67072
|
+
function scriptAllowsByActor(code2) {
|
|
67073
|
+
if (code2.length > 2e5) return false;
|
|
67074
|
+
for (const m of code2.matchAll(ACTOR_ALLOWLIST_G)) {
|
|
67075
|
+
const at = m.index ?? 0;
|
|
67076
|
+
const before = code2.slice(Math.max(0, at - 80), at);
|
|
67077
|
+
if (m[0].startsWith(".includes")) {
|
|
67078
|
+
const list = /([\w$.]{1,60})$/.exec(before)?.[1] ?? "";
|
|
67079
|
+
if (BLOCK_LIST_NAME_RE.test(list)) continue;
|
|
67080
|
+
}
|
|
67081
|
+
const negated = /!\s{0,3}\(?\s{0,3}[\w$.]{0,60}$/.test(before);
|
|
67082
|
+
const ternary = /^[^?;\n]{0,150}\?\s{0,5}['"`]?(true|false)\b/.exec(code2.slice(at, at + 200));
|
|
67083
|
+
if (!ternary || ternary[1] === "true" !== negated) return true;
|
|
67084
|
+
}
|
|
67085
|
+
return false;
|
|
67086
|
+
}
|
|
67087
|
+
function readsOutputPositively(readerIf, name) {
|
|
67088
|
+
const ref = `needs\\.${escapeRe(name)}\\.outputs\\.[\\w-]{1,100}`;
|
|
67089
|
+
if (!new RegExp(ref).test(readerIf)) return false;
|
|
67090
|
+
const no = `'(?:false|no|0|)'(?![\\w'])`;
|
|
67091
|
+
return !new RegExp(
|
|
67092
|
+
`!\\s{0,5}\\(?\\s{0,5}(?:fromjson\\(\\s{0,5})?${ref}|${ref}\\s{0,5}\\)?\\s{0,5}(?:!=\\s{0,5}(?!${no})|==\\s{0,5}${no})`,
|
|
67093
|
+
"i"
|
|
67094
|
+
).test(readerIf);
|
|
66343
67095
|
}
|
|
66344
67096
|
var STATUS_FN_RE = /\b(success|failure|always|cancelled)\s*\(/i;
|
|
66345
|
-
var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]/;
|
|
67097
|
+
var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]|actions\.cancelWorkflowRun\b/;
|
|
66346
67098
|
function needsOf(job) {
|
|
66347
67099
|
return (Array.isArray(job.needs) ? job.needs : job.needs ? [job.needs] : []).map(String);
|
|
66348
67100
|
}
|
|
@@ -66354,13 +67106,18 @@ function upstreamJobGates(name, dep, wf, raw, readerIf) {
|
|
|
66354
67106
|
const label3 = labelTypeConfigured(wf, raw);
|
|
66355
67107
|
if (depIf && jobIfGated(depIf, label3)) return true;
|
|
66356
67108
|
const steps = dep.steps ?? [];
|
|
66357
|
-
|
|
67109
|
+
if (readerIf !== null && readsOutputPositively(readerIf, name) && steps.some((st) => {
|
|
67110
|
+
const code2 = str(st.run) + " " + str(st.with?.["script"]);
|
|
67111
|
+
return MEMBERSHIP_CHECK_RE.test(code2) || scriptAllowsByActor(code2);
|
|
67112
|
+
}))
|
|
67113
|
+
return true;
|
|
67114
|
+
const stepGated = ifsAreGated(stepIfText(steps), label3) || hasStepMembershipGate(dep) || steps.some((st) => MEMBERSHIP_CHECK_RE.test(str(st.with?.["script"])));
|
|
66358
67115
|
if (!stepGated) return false;
|
|
66359
67116
|
if (steps.some(
|
|
66360
67117
|
(st) => FAILING_STEP_RE.test(str(st.run)) || FAILING_STEP_RE.test(str(st.with?.["script"]))
|
|
66361
67118
|
))
|
|
66362
67119
|
return true;
|
|
66363
|
-
return readerIf !== null &&
|
|
67120
|
+
return readerIf !== null && readsOutputPositively(readerIf, name);
|
|
66364
67121
|
}
|
|
66365
67122
|
function gatingAncestors(job, wf, raw) {
|
|
66366
67123
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -66390,9 +67147,15 @@ function jobGated(job, wf, raw) {
|
|
|
66390
67147
|
}
|
|
66391
67148
|
function bypassArmed(steps) {
|
|
66392
67149
|
return steps.some(
|
|
66393
|
-
(s) => str(s.with?.["allowed_non_write_users"])
|
|
67150
|
+
(s) => nonWriteOpen(str(s.with?.["allowed_non_write_users"])) && (!!str(s.with?.["github_token"]) || !!str(s.env?.["OVERRIDE_GITHUB_TOKEN"]))
|
|
66394
67151
|
);
|
|
66395
67152
|
}
|
|
67153
|
+
var ACTOR_BOUND_RE = /\$\{\{[^}]{0,200}\b(github\.event\.[\w.]{0,80}\b(?:user|sender)\.login|github\.event\.sender\.login|github\.actor|github\.triggering_actor)\b/i;
|
|
67154
|
+
function nonWriteOpen(v) {
|
|
67155
|
+
const t = v.trim();
|
|
67156
|
+
if (t === "*") return true;
|
|
67157
|
+
return t.length <= 1e3 && ACTOR_BOUND_RE.test(t);
|
|
67158
|
+
}
|
|
66396
67159
|
function hasImplicitActorGate(agentSteps, bypassActive, workflowRun = false) {
|
|
66397
67160
|
if (bypassActive) return false;
|
|
66398
67161
|
return agentSteps.some((s) => {
|
|
@@ -66511,14 +67274,28 @@ function severityFromScore(score) {
|
|
|
66511
67274
|
if (score >= 1) return "advisory";
|
|
66512
67275
|
return null;
|
|
66513
67276
|
}
|
|
66514
|
-
function analyzeWorkflow(path91, content) {
|
|
67277
|
+
function analyzeWorkflow(path91, content, callers, autoLabels, trustedRunSources) {
|
|
67278
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67279
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67280
|
+
try {
|
|
67281
|
+
return analyzeWorkflowInner(path91, content, callers);
|
|
67282
|
+
} finally {
|
|
67283
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67284
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
67285
|
+
UPSTREAM_TRUSTED = false;
|
|
67286
|
+
}
|
|
67287
|
+
}
|
|
67288
|
+
function analyzeWorkflowInner(path91, content, callers) {
|
|
66515
67289
|
let raw;
|
|
66516
67290
|
try {
|
|
66517
|
-
raw =
|
|
67291
|
+
raw = loadWorkflow(content).raw;
|
|
66518
67292
|
} catch {
|
|
66519
67293
|
return null;
|
|
66520
67294
|
}
|
|
66521
67295
|
const wf = raw;
|
|
67296
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67297
|
+
else CALLERS.delete(wf);
|
|
67298
|
+
UPSTREAM_TRUSTED = upstreamRunTrusted(wf, raw);
|
|
66522
67299
|
const steps = allSteps(wf).map((s) => s.step);
|
|
66523
67300
|
const agentSteps = steps.filter(isAgentStep);
|
|
66524
67301
|
const agentJobs = [
|
|
@@ -66537,17 +67314,19 @@ function analyzeWorkflow(path91, content) {
|
|
|
66537
67314
|
} = triggerReach(wf, raw);
|
|
66538
67315
|
const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
|
|
66539
67316
|
const nonWriteStar = bypassArmed(agentSteps);
|
|
66540
|
-
const starWithoutToken = nonWrite
|
|
66541
|
-
const nonWriteList = !!nonWrite && nonWrite
|
|
67317
|
+
const starWithoutToken = nonWriteOpen(nonWrite) && !nonWriteStar;
|
|
67318
|
+
const nonWriteList = !!nonWrite && !nonWriteOpen(nonWrite);
|
|
67319
|
+
const nonWriteActor = nonWrite.trim() !== "*" && nonWriteOpen(nonWrite);
|
|
66542
67320
|
const untrustedTrigger = forkInput || reusable;
|
|
66543
67321
|
const bypassActive = nonWriteStar && untrustedTrigger;
|
|
66544
|
-
const head = untrustedHeadCheckout(steps);
|
|
67322
|
+
const head = agentSteps.length && agentSteps.every(readOnlySandbox) ? null : untrustedHeadCheckout(steps);
|
|
66545
67323
|
const promptUntrusted = promptTakesUntrusted(agentSteps);
|
|
66546
67324
|
const injJobs = injectableJobs(wf, raw, untrustedTrigger);
|
|
66547
67325
|
const powerJobs = injJobs.length ? injJobs : agentJobs;
|
|
66548
67326
|
const powerSteps = injJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep));
|
|
66549
67327
|
const scopedSteps = powerSteps.length ? powerSteps : agentSteps;
|
|
66550
|
-
const
|
|
67328
|
+
const headActionRuns = head === "root" && powerSteps.some((st) => /^\s*\.\//.test(str(st.uses)));
|
|
67329
|
+
const toolsBlob = collectTools(scopedSteps) + (headActionRuns ? " Bash" : "");
|
|
66551
67330
|
const itemRead = forkInput ? injJobs.map(jobReadsUntrustedItem).find(Boolean) ?? null : null;
|
|
66552
67331
|
const reach = untrustedTrigger && injJobs.length ? Math.max(
|
|
66553
67332
|
head === "root" ? 3 : head === "subdir" ? 1 : 0,
|
|
@@ -66595,20 +67374,32 @@ function analyzeWorkflow(path91, content) {
|
|
|
66595
67374
|
severity = "medium";
|
|
66596
67375
|
if (!severity) severity = "advisory";
|
|
66597
67376
|
const signals = [];
|
|
67377
|
+
const openCallers = (callers ?? []).filter((c) => c.untrusted && !c.gated && !c.dispatched);
|
|
67378
|
+
if (openCallers.length)
|
|
67379
|
+
signals.push(
|
|
67380
|
+
`called by ${openCallers.map((c) => `${safeText(c.file, 120)} (${c.keys.map((k) => safeText(k, 40)).join(", ")})`).join("; ")}`
|
|
67381
|
+
);
|
|
66598
67382
|
if (secretExposed)
|
|
66599
67383
|
signals.push(
|
|
66600
|
-
`runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).join(", ")})`
|
|
67384
|
+
`runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).map((t) => safeText(t, 40)).join(", ")})`
|
|
67385
|
+
);
|
|
67386
|
+
else if (forkInput)
|
|
67387
|
+
signals.push(
|
|
67388
|
+
`triggered by untrusted input (${triggers.map((t) => safeText(t, 40)).join(", ")})`
|
|
66601
67389
|
);
|
|
66602
|
-
else if (forkInput) signals.push(`triggered by untrusted input (${triggers.join(", ")})`);
|
|
66603
67390
|
else if (reusable)
|
|
66604
67391
|
signals.push(
|
|
66605
|
-
reusableLoadedGun ? `reusable workflow (${triggers.join(", ")}) that checks out an untrusted head / ingests untrusted input: 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(", ")}): no untrusted trigger of its own; reachability depends on the caller's trigger + actor gate`
|
|
67392
|
+
reusableLoadedGun ? `reusable workflow (${triggers.map((t) => safeText(t, 40)).join(", ")}) that checks out an untrusted head / ingests untrusted input: 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.map((t) => safeText(t, 40)).join(", ")}): no untrusted trigger of its own; reachability depends on the caller's trigger + actor gate`
|
|
66606
67393
|
);
|
|
66607
67394
|
if (forkInput && !privileged && !reusable)
|
|
66608
67395
|
signals.push(
|
|
66609
67396
|
"triggered by `pull_request`: fork PRs run with a read-only token (lower risk than pull_request_target)"
|
|
66610
67397
|
);
|
|
66611
67398
|
if (head === "root") signals.push("checks out the untrusted PR head into the workspace root");
|
|
67399
|
+
if (headActionRuns)
|
|
67400
|
+
signals.push(
|
|
67401
|
+
"runs the checked-out head's own action (`uses: ./\u2026`): the PR author's code runs with the job's secrets"
|
|
67402
|
+
);
|
|
66612
67403
|
if (head === "subdir") signals.push("checks out the untrusted PR head into an isolated subdir");
|
|
66613
67404
|
if (workflowRunJobs.length) {
|
|
66614
67405
|
const builds = workflowRunJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep)).map((st) => claudeActionBuild(st.uses)).filter((b) => b !== null);
|
|
@@ -66630,7 +67421,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66630
67421
|
if (promptUntrusted) signals.push("feeds untrusted PR/issue text to the agent");
|
|
66631
67422
|
else if (itemRead)
|
|
66632
67423
|
signals.push(
|
|
66633
|
-
itemRead === "prompt-file" ? "an earlier step writes untrusted PR/issue text into the prompt file the agent reads" : itemRead === "tool" ? "the agent fetches PR/issue text itself with a read tool (the text never appears in the prompt)" : "the agent is given the PR/issue number and reads the text itself (the text never appears in the prompt)"
|
|
67424
|
+
itemRead === "prompt-file" ? "an earlier step writes untrusted PR/issue text into the prompt file the agent reads" : itemRead === "artifact" ? "the job downloads the artifacts of the triggering run (produced by the PR) before the agent runs" : itemRead === "step-output" ? "the agent's prompt is an earlier step's output, built from untrusted PR/issue text" : itemRead === "tool" ? "the agent fetches PR/issue text itself with a read tool (the text never appears in the prompt)" : "the agent is given the PR/issue number and reads the text itself (the text never appears in the prompt)"
|
|
66634
67425
|
);
|
|
66635
67426
|
if (broadTools) {
|
|
66636
67427
|
const names = matchedBroadTools(toolsBlob);
|
|
@@ -66640,7 +67431,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66640
67431
|
}
|
|
66641
67432
|
if (bypassActive)
|
|
66642
67433
|
signals.push(
|
|
66643
|
-
'allowed_non_write_users: "*" with github_token, so any user can trigger the agent'
|
|
67434
|
+
nonWriteActor ? "allowed_non_write_users is the triggering user's own login, with github_token, so any user can trigger the agent" : 'allowed_non_write_users: "*" with github_token, so any user can trigger the agent'
|
|
66644
67435
|
);
|
|
66645
67436
|
if (elevated) signals.push("elevated permissions (contents/id-token: write)");
|
|
66646
67437
|
if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
|
|
@@ -66734,7 +67525,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66734
67525
|
return finding;
|
|
66735
67526
|
}
|
|
66736
67527
|
var AGENT_FUEL_RE = /^(ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL|CLAUDE_CODE_OAUTH_TOKEN|OPENAI_API_KEY|OPENAI_BASE_URL|GEMINI_API_KEY|GOOGLE_API_KEY|GITHUB_TOKEN)$/i;
|
|
66737
|
-
var FUEL_INPUT_KEYS = /^(
|
|
67528
|
+
var FUEL_INPUT_KEYS = /^(anthropic[_-]api[_-]key|anthropic[_-]auth[_-]token|anthropic[_-]base[_-]url|claude[_-]code[_-]oauth[_-]token|claude[_-]oauth[_-]token|openai[_-]api[_-]key|openai[_-]base[_-]url|gemini[_-]api[_-]key|google[_-]api[_-]key)$/i;
|
|
66738
67529
|
function fuelSecretNames(agentSteps) {
|
|
66739
67530
|
const out = /* @__PURE__ */ new Set();
|
|
66740
67531
|
const add = (v) => {
|
|
@@ -66807,14 +67598,28 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
|
|
|
66807
67598
|
if (reusable && !loadedGun && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
|
|
66808
67599
|
return { severity, secrets, injectable, canReadEnv };
|
|
66809
67600
|
}
|
|
66810
|
-
function analyzeWorkflowSecrets(path91, content) {
|
|
67601
|
+
function analyzeWorkflowSecrets(path91, content, callers, autoLabels, trustedRunSources) {
|
|
67602
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67603
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67604
|
+
try {
|
|
67605
|
+
return analyzeWorkflowSecretsInner(path91, content, callers);
|
|
67606
|
+
} finally {
|
|
67607
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67608
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
67609
|
+
UPSTREAM_TRUSTED = false;
|
|
67610
|
+
}
|
|
67611
|
+
}
|
|
67612
|
+
function analyzeWorkflowSecretsInner(path91, content, callers) {
|
|
66811
67613
|
let raw;
|
|
66812
67614
|
try {
|
|
66813
|
-
raw =
|
|
67615
|
+
raw = loadWorkflow(content).raw;
|
|
66814
67616
|
} catch {
|
|
66815
67617
|
return null;
|
|
66816
67618
|
}
|
|
66817
67619
|
const wf = raw;
|
|
67620
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67621
|
+
else CALLERS.delete(wf);
|
|
67622
|
+
UPSTREAM_TRUSTED = upstreamRunTrusted(wf, raw);
|
|
66818
67623
|
const all = allSteps(wf);
|
|
66819
67624
|
if (!all.some((s) => isAgentStep(s.step))) return null;
|
|
66820
67625
|
const { untrusted: forkInput, reusable } = triggerReach(wf, raw);
|
|
@@ -66846,70 +67651,281 @@ function analyzeWorkflowSecrets(path91, content) {
|
|
|
66846
67651
|
})
|
|
66847
67652
|
};
|
|
66848
67653
|
}
|
|
66849
|
-
|
|
66850
|
-
|
|
66851
|
-
|
|
66852
|
-
|
|
66853
|
-
|
|
66854
|
-
|
|
66855
|
-
|
|
66856
|
-
|
|
66857
|
-
|
|
66858
|
-
if (!close) return null;
|
|
66859
|
-
const block = text.slice(3, 3 + close.index);
|
|
67654
|
+
var COMMIT_PIN_RE = /[#@][0-9a-f]{40}\b/i;
|
|
67655
|
+
function marketplaceOwner(market) {
|
|
67656
|
+
if (/(^|[/\\])\.\.([/\\]|$)/.test(market)) return void 0;
|
|
67657
|
+
return /^(?:https?:\/\/(?:www\.)?|git@|ssh:\/\/git@)?github\.com[:/]([\w.-]{1,100})\/[\w.-]{1,100}(?:\.git)?\/?(?:[#@].*)?$/i.exec(
|
|
67658
|
+
market
|
|
67659
|
+
)?.[1] ?? /^([\w.-]{1,100})\/[\w.-]{1,100}(?:[#@].*)?$/.exec(market)?.[1];
|
|
67660
|
+
}
|
|
67661
|
+
function analyzeWorkflowPlugins(path91, content, owner) {
|
|
67662
|
+
let raw;
|
|
66860
67663
|
try {
|
|
66861
|
-
|
|
66862
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
67664
|
+
raw = loadWorkflow(content).raw;
|
|
66863
67665
|
} catch {
|
|
66864
|
-
return
|
|
67666
|
+
return [];
|
|
66865
67667
|
}
|
|
67668
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67669
|
+
const wf = raw;
|
|
67670
|
+
const out = [];
|
|
67671
|
+
const seen = /* @__PURE__ */ new Set();
|
|
67672
|
+
for (const { step } of allSteps(wf)) {
|
|
67673
|
+
if (!claudeActionBuild(step.uses)) continue;
|
|
67674
|
+
if (!str(step.with?.["plugins"]).trim()) continue;
|
|
67675
|
+
for (const m of str(step.with?.["plugin_marketplaces"]).split(/[\s,]+/)) {
|
|
67676
|
+
const market = m.trim();
|
|
67677
|
+
if (!market || COMMIT_PIN_RE.test(market)) continue;
|
|
67678
|
+
if (/^\.{1,2}(\/|$)/.test(market)) continue;
|
|
67679
|
+
const mo = marketplaceOwner(market);
|
|
67680
|
+
if (mo && mo.toLowerCase() === "anthropics") continue;
|
|
67681
|
+
if (owner && mo && mo.toLowerCase() === owner.toLowerCase()) continue;
|
|
67682
|
+
if (seen.has(market)) continue;
|
|
67683
|
+
seen.add(market);
|
|
67684
|
+
out.push({
|
|
67685
|
+
check: "CI-3",
|
|
67686
|
+
rule: "CI-3.plugin-unpinned",
|
|
67687
|
+
locator: safeText(market, 200),
|
|
67688
|
+
dimension: "mcp",
|
|
67689
|
+
severity: "medium",
|
|
67690
|
+
title: "Agent installs plugins from a third-party marketplace on every run",
|
|
67691
|
+
file: path91,
|
|
67692
|
+
signals: [
|
|
67693
|
+
`\`plugin_marketplaces: ${safeText(market, 120)}\` is not pinned to a commit: whoever controls it changes the code and prompts the agent runs`
|
|
67694
|
+
],
|
|
67695
|
+
fix: "Pin the marketplace to a commit (`<url>#<sha>`), or vendor the plugin into this repository."
|
|
67696
|
+
});
|
|
67697
|
+
}
|
|
67698
|
+
}
|
|
67699
|
+
return out;
|
|
66866
67700
|
}
|
|
66867
|
-
|
|
66868
|
-
|
|
66869
|
-
|
|
66870
|
-
|
|
66871
|
-
|
|
66872
|
-
|
|
66873
|
-
|
|
66874
|
-
|
|
66875
|
-
|
|
66876
|
-
|
|
67701
|
+
var ITEM_LIST_RE = /gh\s+(?:issue|pr)\s+(?:list|view)\b|gh\s+search\b|gh\s+api\s+[^\n]{0,80}\/(?:issues|pulls)\b|\blist_issues\b|\bsearch_issues\b|issues\.listForRepo|pulls\.list\b|(?<!\b(?:and|to|then|or|will|should|must|may|can|please|you|also)\s)\bopen (?:issues|pull requests)\b/i;
|
|
67702
|
+
var WEB_READ_RE = /(^|["\s,])(WebFetch|WebSearch)(\(|\s|,|"|$)/;
|
|
67703
|
+
function readsItemList(text) {
|
|
67704
|
+
if (text.length > 2e5) return ITEM_LIST_RE.test(text);
|
|
67705
|
+
return text.replace(/\\\r?\n/g, " ").split("\n").some((l) => ITEM_LIST_RE.test(l) && !sanitizedListing(l));
|
|
67706
|
+
}
|
|
67707
|
+
function analyzeTrustedReaders(path91, content, autoLabels, callers) {
|
|
67708
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67709
|
+
try {
|
|
67710
|
+
return analyzeTrustedReadersInner(path91, content, callers);
|
|
67711
|
+
} finally {
|
|
67712
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67713
|
+
}
|
|
67714
|
+
}
|
|
67715
|
+
function keysWithCallers(keys, callers) {
|
|
67716
|
+
return [.../* @__PURE__ */ new Set([...keys, ...(callers ?? []).flatMap((c) => c.keys)])];
|
|
67717
|
+
}
|
|
67718
|
+
function analyzeTrustedReadersInner(path91, content, callers) {
|
|
67719
|
+
let raw;
|
|
67720
|
+
try {
|
|
67721
|
+
raw = loadWorkflow(content).raw;
|
|
67722
|
+
} catch {
|
|
67723
|
+
return [];
|
|
67724
|
+
}
|
|
67725
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67726
|
+
const wf = raw;
|
|
67727
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67728
|
+
else CALLERS.delete(wf);
|
|
67729
|
+
const reach = triggerReach(wf, raw);
|
|
67730
|
+
if (reach.untrusted || reach.reusable) return [];
|
|
67731
|
+
const keys = keysWithCallers(reach.keys, callers);
|
|
67732
|
+
if (!keys.some((k) => /^schedule$/i.test(k))) return [];
|
|
67733
|
+
for (const job of jobList(wf)) {
|
|
67734
|
+
const jobIf = parseIf(str(job.if));
|
|
67735
|
+
if (jobIf !== null && !exprAllowsEvent(jobIf, "schedule")) continue;
|
|
67736
|
+
const steps = job.steps ?? [];
|
|
67737
|
+
const agentIdx = steps.findIndex(isAgentStep);
|
|
67738
|
+
if (agentIdx < 0) continue;
|
|
67739
|
+
const agent = steps.filter(isAgentStep);
|
|
67740
|
+
const tools = collectTools(agent);
|
|
67741
|
+
const promptText = agent.map(
|
|
67742
|
+
(st) => str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + (isRunAgent(st) ? str(st.run) : "")
|
|
67743
|
+
).join(" ");
|
|
67744
|
+
const earlier = steps.slice(0, agentIdx).filter((st) => !isAgentStep(st)).map((st) => str(st.run) + " " + str(st.with?.["script"]));
|
|
67745
|
+
const reads = [];
|
|
67746
|
+
if (ITEM_READ_TOOL_RE.test(tools) || readsItemList(promptText))
|
|
67747
|
+
reads.push("reads open issues or pull requests");
|
|
67748
|
+
else if (earlier.some(readsItemList))
|
|
67749
|
+
reads.push("an earlier step lists open issues or pull requests for it");
|
|
67750
|
+
if (WEB_READ_RE.test(tools)) reads.push("fetches third-party web content");
|
|
67751
|
+
if (!reads.length) continue;
|
|
67752
|
+
const withheld = agent.every(
|
|
67753
|
+
(st) => str(st.with?.["github_token"]).trim() === "''" || "github_token" in (st.with ?? {}) && str(st.with?.["github_token"]).trim() === "" || "GITHUB_TOKEN" in (st.env ?? {}) && str(st.env?.["GITHUB_TOKEN"]).trim() === ""
|
|
67754
|
+
);
|
|
67755
|
+
const checkouts = steps.slice(0, agentIdx).filter((st) => /actions\/checkout/i.test(str(st.uses)));
|
|
67756
|
+
const persisted = checkouts.some((st) => str(st.with?.["persist-credentials"]) !== "false");
|
|
67757
|
+
const checkoutPat = checkouts.some(
|
|
67758
|
+
(st) => /secrets\.(?!GITHUB_TOKEN\b)[A-Za-z_]/i.test(str(st.with?.["token"]))
|
|
67759
|
+
);
|
|
67760
|
+
const pat = usesPatIn([job]) || checkoutPat;
|
|
67761
|
+
const permWrite = jobsHaveWrite(
|
|
67762
|
+
wf,
|
|
67763
|
+
[job],
|
|
67764
|
+
/["']?(contents|issues|pull-requests)["']?\s*:\s*["']?write/i
|
|
67765
|
+
);
|
|
67766
|
+
const handsToken = agent.some(
|
|
67767
|
+
(st) => !!claudeActionBuild(st.uses) || !!str(st.with?.["github_token"]).trim()
|
|
67768
|
+
);
|
|
67769
|
+
const write2 = pat || permWrite && !withheld && (handsToken || persisted);
|
|
67770
|
+
if (!write2) continue;
|
|
67771
|
+
const writes = pat ? "a static PAT" : "a write token";
|
|
67772
|
+
return [
|
|
67773
|
+
{
|
|
67774
|
+
check: "CI-2",
|
|
67775
|
+
rule: "CI-2.trusted-trigger-reader",
|
|
67776
|
+
dimension: "workflows",
|
|
67777
|
+
severity: "advisory",
|
|
67778
|
+
title: "Agent on a trusted trigger reads content strangers write, with a write token",
|
|
67779
|
+
file: path91,
|
|
67780
|
+
signals: [
|
|
67781
|
+
`runs on ${keys.map((k) => safeText(k, 40)).join(", ")}: no stranger starts it`,
|
|
67782
|
+
...reads.map((r) => `the agent ${r}`),
|
|
67783
|
+
`it holds ${writes}: injected text in that content can act with it`
|
|
67784
|
+
],
|
|
67785
|
+
fix: "Give the agent a read-only token and apply its output in a separate step, or filter the items to trusted authors first."
|
|
67786
|
+
}
|
|
67787
|
+
];
|
|
67788
|
+
}
|
|
67789
|
+
return [];
|
|
67790
|
+
}
|
|
67791
|
+
var FORK_PR_TRIGGER_RE = /^(pull_request_target|issue_comment|workflow_run)$/i;
|
|
67792
|
+
var SAME_REPO_GATE_RE = new RegExp(
|
|
67793
|
+
[
|
|
67794
|
+
String.raw`head(?:\.repo|_repository)\.full_name\s*==\s*github\.(?:repository\b|event\.(?:pull_request\.base\.repo|repository)\.full_name)`,
|
|
67795
|
+
String.raw`github\.repository\s*==\s*github\.event\.(?:pull_request\.head\.repo|workflow_run\.head_repository)\.full_name`,
|
|
67796
|
+
String.raw`head(?:\.repo|_repository)\.fork\s*==\s*false`,
|
|
67797
|
+
String.raw`head(?:\.repo|_repository)\.owner\.login\s*==\s*github\.repository_owner`,
|
|
67798
|
+
String.raw`workflow_run\.pull_requests\[0\]`
|
|
67799
|
+
].join("|"),
|
|
67800
|
+
"i"
|
|
67801
|
+
);
|
|
67802
|
+
var NOT_FORK_ATOM_RE = /^github\.event\.(?:pull_request\.head\.repo|workflow_run\.head_repository)\.fork$/i;
|
|
67803
|
+
var TRUSTED_AUTHOR_GATE_RE = /\b(?:pull_request|issue)\.user\.login\s*==|github\.event\.(?:pull_request|issue)\.author_association\s*==\s*['"](?:OWNER|MEMBER|COLLABORATOR)|contains\(\s*fromjson\(\s*['"][^'"]{0,200}\b(?:OWNER|MEMBER|COLLABORATOR)\b[^'"]{0,200}['"]\s*\)\s*,\s*github\.event\.(?:pull_request|issue)\.author_association/i;
|
|
67804
|
+
function requiresAtom(src, re, events, notRe = NOT_FORK_ATOM_RE) {
|
|
67805
|
+
const x = parseIf(src);
|
|
67806
|
+
if (!x) return false;
|
|
67807
|
+
const req = (e) => {
|
|
67808
|
+
if (events?.length && events.every((ev) => !exprAllowsEvent(e, ev))) return true;
|
|
67809
|
+
switch (e.k) {
|
|
67810
|
+
case "atom": {
|
|
67811
|
+
const m = maskLiterals(e.t);
|
|
67812
|
+
return re.test(m) && !CHAINED_COMPARE_RE.test(m);
|
|
67813
|
+
}
|
|
67814
|
+
case "not":
|
|
67815
|
+
return e.c.k === "atom" && notRe.test(e.c.t.trim());
|
|
67816
|
+
case "and":
|
|
67817
|
+
return e.c.some(req);
|
|
67818
|
+
case "or":
|
|
67819
|
+
return e.c.every(req);
|
|
66877
67820
|
}
|
|
66878
|
-
|
|
66879
|
-
|
|
66880
|
-
|
|
67821
|
+
};
|
|
67822
|
+
return req(x);
|
|
67823
|
+
}
|
|
67824
|
+
var PR_EVENTS = [
|
|
67825
|
+
"pull_request_target",
|
|
67826
|
+
"pull_request",
|
|
67827
|
+
"pull_request_review",
|
|
67828
|
+
"pull_request_review_comment"
|
|
67829
|
+
];
|
|
67830
|
+
function headRefEvents(heads) {
|
|
67831
|
+
const evs = /* @__PURE__ */ new Set();
|
|
67832
|
+
for (const st of heads) {
|
|
67833
|
+
if (st.uses == null) return null;
|
|
67834
|
+
const m = /^\s*\$\{\{([\s\S]{1,500})\}\}\s*$/.exec(str(st.with?.["ref"]));
|
|
67835
|
+
if (!m) return null;
|
|
67836
|
+
for (const part of m[1].split("||").map((x) => x.trim())) {
|
|
67837
|
+
if (/^github\.event\.workflow_run\.[\w.]+$/.test(part)) evs.add("workflow_run");
|
|
67838
|
+
else if (/^github\.event\.pull_request\.[\w.]+$/.test(part))
|
|
67839
|
+
PR_EVENTS.forEach((e) => evs.add(e));
|
|
67840
|
+
else if (!/^(?:github\.(?:sha|ref|ref_name|event\.repository\.default_branch)|'[^']*')$/.test(part))
|
|
67841
|
+
return null;
|
|
66881
67842
|
}
|
|
66882
|
-
if (items.length) out[key] = items;
|
|
66883
67843
|
}
|
|
66884
|
-
return
|
|
67844
|
+
return evs.size ? [...evs] : null;
|
|
66885
67845
|
}
|
|
66886
|
-
function
|
|
66887
|
-
const
|
|
66888
|
-
|
|
66889
|
-
|
|
66890
|
-
|
|
66891
|
-
|
|
66892
|
-
|
|
66893
|
-
|
|
66894
|
-
|
|
66895
|
-
|
|
66896
|
-
|
|
67846
|
+
function readOnlySandbox(st) {
|
|
67847
|
+
const args = str(st.with?.["codex-args"]) + " " + str(st.with?.["codex_args"]);
|
|
67848
|
+
return /^read-only$/i.test(str(st.with?.["sandbox"])) && !PERMISSION_BYPASS_RE.test(args);
|
|
67849
|
+
}
|
|
67850
|
+
function requiresTrustedAuthor(src) {
|
|
67851
|
+
const x = parseIf(src);
|
|
67852
|
+
if (!x) return false;
|
|
67853
|
+
const req = (e) => {
|
|
67854
|
+
switch (e.k) {
|
|
67855
|
+
case "atom":
|
|
67856
|
+
return TRUSTED_AUTHOR_GATE_RE.test(e.t) && !NONWRITE_SET_RE.test(e.t) && !CHAINED_COMPARE_RE.test(maskLiterals(e.t));
|
|
67857
|
+
case "not":
|
|
67858
|
+
return false;
|
|
67859
|
+
case "and":
|
|
67860
|
+
return e.c.some(req);
|
|
67861
|
+
case "or":
|
|
67862
|
+
return e.c.every(req);
|
|
66897
67863
|
}
|
|
66898
|
-
|
|
67864
|
+
};
|
|
67865
|
+
return req(x);
|
|
67866
|
+
}
|
|
67867
|
+
function analyzeForkCodeOnTrust(path91, content, callers, autoLabels) {
|
|
67868
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67869
|
+
try {
|
|
67870
|
+
return analyzeForkCodeOnTrustInner(path91, content, callers);
|
|
67871
|
+
} finally {
|
|
67872
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66899
67873
|
}
|
|
66900
|
-
if (cur) out.push(cur);
|
|
66901
|
-
return out;
|
|
66902
67874
|
}
|
|
66903
|
-
function
|
|
66904
|
-
|
|
66905
|
-
|
|
66906
|
-
|
|
66907
|
-
|
|
67875
|
+
function analyzeForkCodeOnTrustInner(path91, content, callers) {
|
|
67876
|
+
let raw;
|
|
67877
|
+
try {
|
|
67878
|
+
raw = loadWorkflow(content).raw;
|
|
67879
|
+
} catch {
|
|
67880
|
+
return [];
|
|
67881
|
+
}
|
|
67882
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67883
|
+
const wf = raw;
|
|
67884
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67885
|
+
else CALLERS.delete(wf);
|
|
67886
|
+
const reach = triggerReach(wf, raw);
|
|
67887
|
+
const forkKeys = keysWithCallers(reach.keys, callers).filter((k) => FORK_PR_TRIGGER_RE.test(k));
|
|
67888
|
+
if (!forkKeys.length) return [];
|
|
67889
|
+
const inj = new Set(injectableJobs(wf, raw, reach.untrusted || reach.reusable));
|
|
67890
|
+
for (const job of jobList(wf)) {
|
|
67891
|
+
if (inj.has(job)) continue;
|
|
67892
|
+
const steps = job.steps ?? [];
|
|
67893
|
+
const agent = steps.filter(isAgentStep);
|
|
67894
|
+
if (!agent.length) continue;
|
|
67895
|
+
if (untrustedHeadCheckout(steps) !== "root") continue;
|
|
67896
|
+
if (agent.every(readOnlySandbox)) continue;
|
|
67897
|
+
if (requiresAtom(str(job.if), SAME_REPO_GATE_RE)) continue;
|
|
67898
|
+
if (agent.every((st) => requiresAtom(str(st.if), SAME_REPO_GATE_RE))) continue;
|
|
67899
|
+
const heads = steps.filter((st) => untrustedHeadCheckout([st]) === "root");
|
|
67900
|
+
if (heads.length && heads.every((st) => requiresAtom(str(st.if), SAME_REPO_GATE_RE))) continue;
|
|
67901
|
+
const evs = headRefEvents(heads);
|
|
67902
|
+
if (evs && !forkKeys.some((k) => evs.includes(k.toLowerCase()))) continue;
|
|
67903
|
+
if (evs && requiresAtom(str(job.if), SAME_REPO_GATE_RE, evs)) continue;
|
|
67904
|
+
if (requiresTrustedAuthor(str(job.if))) continue;
|
|
67905
|
+
const holdsSecret = /secrets\./i.test(str(job.steps) + " " + str(job.env));
|
|
67906
|
+
if (!holdsSecret && !jobsHaveWrite(wf, [job], /:\s*["']?write/i)) continue;
|
|
67907
|
+
return [
|
|
67908
|
+
{
|
|
67909
|
+
check: "CI-2",
|
|
67910
|
+
rule: "CI-2.fork-code-on-trust",
|
|
67911
|
+
dimension: "workflows",
|
|
67912
|
+
severity: "advisory",
|
|
67913
|
+
title: "A maintainer's action runs the agent inside the fork's code",
|
|
67914
|
+
file: path91,
|
|
67915
|
+
signals: [
|
|
67916
|
+
`triggered by ${forkKeys.map((k) => safeText(k, 40)).join(", ")}, behind a gate a maintainer passes (a comment, a label, a push to the PR branch, a reopen)`,
|
|
67917
|
+
"checks out the untrusted PR head into the workspace root",
|
|
67918
|
+
"the agent loads the fork's CLAUDE.md, .claude/settings.json hooks, .mcp.json and build scripts next to the job's secrets"
|
|
67919
|
+
],
|
|
67920
|
+
fix: "Check out the base branch and give the agent the diff, or check the PR head out into a subdirectory the agent does not start in; do not let it run the fork's build or hooks."
|
|
67921
|
+
}
|
|
67922
|
+
];
|
|
66908
67923
|
}
|
|
66909
67924
|
return [];
|
|
66910
67925
|
}
|
|
66911
67926
|
|
|
66912
67927
|
// src/ci-check/agent-config.ts
|
|
67928
|
+
import path76 from "path";
|
|
66913
67929
|
function asStrings(v) {
|
|
66914
67930
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
66915
67931
|
}
|
|
@@ -66927,11 +67943,22 @@ function hookCommands(hooks) {
|
|
|
66927
67943
|
return out;
|
|
66928
67944
|
}
|
|
66929
67945
|
var OPEN_RUNNER_RE = new RegExp(
|
|
66930
|
-
String.raw`^Bash\((?:[A-Z_][A-Z0-9_]{0,40}=\S{0,200} )*(?:/[\w./-]{0,80}/)?` + String.raw`(python(?:3(?:\.\d{1,2})?)?|node|deno(?: run)?|bun(?: x)?|ruby|perl|php|uv run|uvx|npx|npm exec|pnpm dlx|yarn dlx|pipx|docker run|` + String.raw`curl|wget|nc|ncat|ssh|scp|rsync|sh|bash|zsh|eval|source|xargs|env|sudo|gh
|
|
67946
|
+
String.raw`^Bash\((?:[A-Z_][A-Z0-9_]{0,40}=\S{0,200} )*(?:/[\w./-]{0,80}/)?` + String.raw`(python(?:3(?:\.\d{1,2})?)?|node|deno(?: run)?|bun(?: x)?|ruby|perl|php|uv run|uvx|npx|npm exec|pnpm dlx|yarn dlx|pipx|docker run|` + String.raw`curl|wget|nc|ncat|ssh|scp|rsync|sh|bash|zsh|eval|source|xargs|env|sudo|gh|` + // PowerShell, and the cmdlets that run a string or a program (a PowerShell grant is read
|
|
67947
|
+
// as Bash, review round 2).
|
|
67948
|
+
String.raw`pwsh|powershell|invoke-expression|iex|invoke-command|icm|start-process|invoke-webrequest|iwr|invoke-restmethod|irm)` + String.raw`(?: --?[A-Za-z][\w-]{0,30}){0,4} ?['"]?(?::\*| \*|\*)\)$`,
|
|
66931
67949
|
"i"
|
|
66932
67950
|
);
|
|
66933
67951
|
var OPEN_RUNNER_MAX = 300;
|
|
66934
67952
|
var INFO_FLAG_RE = /\s--(version|help)\b/;
|
|
67953
|
+
var EXTRA_RUNNER_RE = new RegExp(
|
|
67954
|
+
String.raw`^Bash\((?:[A-Z_][A-Z0-9_]{0,40}=\S{0,200} )*(?:/[\w./-]{0,80}/)?` + String.raw`(awk|gawk|mawk|sed|tee|tar|make(?! -n\b)|vim|vi|nvim|less|man|timeout|dd|install|` + String.raw`dotnet (?:run|script)|cargo run|go run|mvn exec|gradle run|npm start|tmux|` + String.raw`rm|git (?:push|config|submodule|filter-branch)|gh (?:repo|secret|auth|release))` + String.raw`(?: --?[A-Za-z][\w-]{0,30}){0,4} ?['"]?(?::\*| \*|\*)\)$`,
|
|
67955
|
+
"i"
|
|
67956
|
+
);
|
|
67957
|
+
var MCP_EXEC_RE = /^mcp__ide__executeCode$|^mcp__[\w-]{1,80}__(?:execute|exec|run)_(?:code|command|shell|sql|query)$|^mcp__[\w-]{1,80}__(?:ctx_)?(?:batch_)?execute$|^mcp__[\w-]{1,80}__(?:bash|shell|terminal)$/;
|
|
67958
|
+
function extraRunner(grant) {
|
|
67959
|
+
if (grant.length > OPEN_RUNNER_MAX || INFO_FLAG_RE.test(grant)) return false;
|
|
67960
|
+
return EXTRA_RUNNER_RE.test(grant.replace(/\s+/g, " ").replace(/\(\s+/, "(")) || MCP_EXEC_RE.test(grant);
|
|
67961
|
+
}
|
|
66935
67962
|
function openRunner(grant) {
|
|
66936
67963
|
if (grant.length > OPEN_RUNNER_MAX || INFO_FLAG_RE.test(grant)) return false;
|
|
66937
67964
|
return OPEN_RUNNER_RE.test(grant.replace(/\s+/g, " ").replace(/\(\s+/, "("));
|
|
@@ -66939,13 +67966,15 @@ function openRunner(grant) {
|
|
|
66939
67966
|
function gradeBroadGrant(allow, deny, opts = {}) {
|
|
66940
67967
|
const denySupported = opts.denySupported ?? true;
|
|
66941
67968
|
const scope = opts.scope ?? "for everyone who opens this repo with the agent";
|
|
66942
|
-
const
|
|
66943
|
-
|
|
66944
|
-
|
|
67969
|
+
const asBash = (a) => a.replace(/^PowerShell(?=$|\()/, "Bash");
|
|
67970
|
+
const broad = allow.filter((raw) => {
|
|
67971
|
+
const a = asBash(raw);
|
|
67972
|
+
return /^Bash$|^Bash\(\s*\*|^Bash\(git:|^Write\(\s*\*|^Write$|^Edit$/.test(a) || openRunner(a) || opts.settings === true && extraRunner(a);
|
|
67973
|
+
});
|
|
66945
67974
|
if (broad.length === 0) return null;
|
|
66946
|
-
const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
|
|
66947
|
-
const bashBackstop = deny.some((d) => /^Bash\b/.test(d));
|
|
66948
|
-
const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(a));
|
|
67975
|
+
const hasBackstop = deny.some((d) => /Bash|PowerShell|Write|Edit/.test(d));
|
|
67976
|
+
const bashBackstop = deny.some((d) => /^(?:Bash|PowerShell)\b/.test(d));
|
|
67977
|
+
const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(asBash(a)));
|
|
66949
67978
|
const high = bareShell && !bashBackstop;
|
|
66950
67979
|
const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
|
|
66951
67980
|
if (high)
|
|
@@ -66962,7 +67991,7 @@ function gradeBroadGrant(allow, deny, opts = {}) {
|
|
|
66962
67991
|
);
|
|
66963
67992
|
if (broad.some((a) => /^Write|^Edit$/.test(a)))
|
|
66964
67993
|
signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
|
|
66965
|
-
if (broad.some(openRunner))
|
|
67994
|
+
if (broad.some((a) => openRunner(asBash(a))))
|
|
66966
67995
|
signals.push(
|
|
66967
67996
|
"an interpreter, network tool, shell runner or `gh` grant ending in a wildcard runs any code or request an injected instruction writes, without a prompt"
|
|
66968
67997
|
);
|
|
@@ -67075,9 +68104,12 @@ function analyzeAgentConfig(path91, content, tree) {
|
|
|
67075
68104
|
}
|
|
67076
68105
|
}
|
|
67077
68106
|
}
|
|
67078
|
-
const allow =
|
|
68107
|
+
const allow = [
|
|
68108
|
+
...asStrings(cfg.permissions?.allow),
|
|
68109
|
+
...asStrings(cfg.permissions?.allowedTools)
|
|
68110
|
+
];
|
|
67079
68111
|
const deny = asStrings(cfg.permissions?.deny);
|
|
67080
|
-
const grade = gradeBroadGrant(allow, deny);
|
|
68112
|
+
const grade = gradeBroadGrant(allow, deny, { settings: true });
|
|
67081
68113
|
if (grade) {
|
|
67082
68114
|
const { high, signals } = grade;
|
|
67083
68115
|
const at = jsonValueIndex(content, grade.broad[0]);
|
|
@@ -67105,6 +68137,26 @@ function analyzeAgentConfig(path91, content, tree) {
|
|
|
67105
68137
|
return findings;
|
|
67106
68138
|
}
|
|
67107
68139
|
var GRANT_CARRIER_RE = /(^|\/)[Ss][Kk][Ii][Ll][Ll]\.md$|(^|\/)\.claude\/commands\/.+\.md$/;
|
|
68140
|
+
var ROOT_SURFACE_RE = /^\.(claude|agents|github|codex|cursor|gemini)\//;
|
|
68141
|
+
var ITEM_TEXT_RE = /gh\s+(?:pr|issue)\s+(?:view|diff|list)\b|gh\s+pr\s+checkout\b|git\s+fetch\s+[^\n]{0,80}\bpull\/|gh\s+api\s+graphql\b|gh\s+api\s+[^\n]{0,120}\/(?:pulls|issues)\b|mcp__github__(?:get_issue|get_pull_request|list_issues|search_issues)|--json\s+[^\n]{0,120}\b(?:body|title)\b/;
|
|
68142
|
+
var PR_TEXT_RE = /gh\s+pr\s+(?:view|diff)\b|gh\s+api\s+[^\n]{0,120}\/pulls\b/;
|
|
68143
|
+
var WEB_TEXT_RE = /\bsearch the web\b|\bfetch (?:the )?(?:url|page)\b/i;
|
|
68144
|
+
function skillReadsUntrusted(path91, content, allow, broad) {
|
|
68145
|
+
if (!ROOT_SURFACE_RE.test(path91)) return null;
|
|
68146
|
+
const body = content.replace(/^---[\s\S]*?\n---/, "");
|
|
68147
|
+
const runsCode = broad.map((a) => a.replace(/^PowerShell(?=$|\()/, "Bash")).some((a) => /^Bash$|^Bash\(\s*\*/.test(a) || openRunner(a) && !/^Bash\(\s*gh\b/.test(a));
|
|
68148
|
+
const webGrant = allow.some((a) => /^(WebFetch|WebSearch)\b/.test(a));
|
|
68149
|
+
const reads = (re) => body.replace(/\\\r?\n/g, " ").split("\n").some((l) => re.test(l) && !sanitizedListing(l));
|
|
68150
|
+
if (runsCode && reads(ITEM_TEXT_RE))
|
|
68151
|
+
return "it reads pull request or issue text that strangers write, with a grant that runs code";
|
|
68152
|
+
if (runsCode && (webGrant || WEB_TEXT_RE.test(body)))
|
|
68153
|
+
return "it reads web content anyone can publish, with a grant that runs code";
|
|
68154
|
+
const writes = allow.some((a) => /^(Write|Edit|MultiEdit)$/.test(a));
|
|
68155
|
+
const ghOrGit = allow.some((a) => /^Bash\(\s*(gh|git)\b/.test(a));
|
|
68156
|
+
if (writes && ghOrGit && reads(PR_TEXT_RE))
|
|
68157
|
+
return "it reads pull request text that strangers write, and may edit files and run git/gh on it";
|
|
68158
|
+
return null;
|
|
68159
|
+
}
|
|
67108
68160
|
function analyzeSkillGrants(path91, content) {
|
|
67109
68161
|
if (!GRANT_CARRIER_RE.test(path91)) return [];
|
|
67110
68162
|
const allow = allowedToolsOf(parseFrontmatter(content));
|
|
@@ -67116,6 +68168,21 @@ function analyzeSkillGrants(path91, content) {
|
|
|
67116
68168
|
if (!grade) return [];
|
|
67117
68169
|
const kind = /commands\//.test(path91) ? "slash command" : "skill";
|
|
67118
68170
|
const at = content.search(/^allowed-tools\s*:/m);
|
|
68171
|
+
const untrusted = skillReadsUntrusted(path91, content, allow, grade.broad);
|
|
68172
|
+
if (untrusted)
|
|
68173
|
+
return [
|
|
68174
|
+
{
|
|
68175
|
+
check: "CI-1",
|
|
68176
|
+
rule: "CI-1.skill-untrusted-input",
|
|
68177
|
+
...at >= 0 ? { line: lineAtIndex(content, at) } : {},
|
|
68178
|
+
dimension: "toolRules",
|
|
68179
|
+
severity: "advisory",
|
|
68180
|
+
title: `Committed ${kind} runs code or writes with text strangers control`,
|
|
68181
|
+
file: path91,
|
|
68182
|
+
signals: [...grade.signals, untrusted],
|
|
68183
|
+
fix: "Drop the shell or interpreter grant from a skill that reads PRs, issues or the web, or have it treat that text as data in a read-only step first."
|
|
68184
|
+
}
|
|
68185
|
+
];
|
|
67119
68186
|
return [
|
|
67120
68187
|
{
|
|
67121
68188
|
check: "CI-1",
|
|
@@ -67137,27 +68204,178 @@ function analyzeSkillGrants(path91, content) {
|
|
|
67137
68204
|
|
|
67138
68205
|
// src/ci-check/mcp.ts
|
|
67139
68206
|
init_dist();
|
|
67140
|
-
var LOCAL_RUNNER_RE = /^npx\s+(?:-y\s+|--yes\s+)?(?:tsx|ts-node|node)\s+(?:\.{1,2}\/|\$\{CLAUDE_PLUGIN_ROOT\}\/)?[\w@./-]+\.(?:m?[jt]s|cjs)(?:\s+[^\s;&|`$<>()]+)*$/;
|
|
67141
|
-
|
|
68207
|
+
var LOCAL_RUNNER_RE = /^(?:npx|pnpx|bunx)\s+(?:-y\s+|--yes\s+)?(?:tsx|ts-node|node)\s+(?:\.{1,2}\/|\$\{CLAUDE_PLUGIN_ROOT\}\/)?[\w@./-]+\.(?:m?[jt]s|cjs)(?:\s+[^\s;&|`$<>()]+)*$/;
|
|
68208
|
+
var LAUNCHERS = [
|
|
68209
|
+
{ re: /\bnpx\b/, name: "npx", registry: "npm" },
|
|
68210
|
+
{ re: /\bpnpx\b|\bpnpm\s+dlx\b/, name: "pnpm dlx", registry: "npm" },
|
|
68211
|
+
{ re: /\byarn\s+dlx\b/, name: "yarn dlx", registry: "npm" },
|
|
68212
|
+
{ re: /\bnpm\s+(?:exec|x)\b/, name: "npm exec", registry: "npm" },
|
|
68213
|
+
{ re: /\bbunx\b|\bbun\s+x\b/, name: "bunx", registry: "npm" },
|
|
68214
|
+
{ re: /\buvx\b|\buv\s+tool\s+run\b/, name: "uvx", registry: "PyPI" },
|
|
68215
|
+
{ re: /\bpipx\s+run\b/, name: "pipx run", registry: "PyPI" }
|
|
68216
|
+
];
|
|
68217
|
+
var PINNED_RE = /@v?\d|@[0-9a-f]{40}\b|==\s*\d/i;
|
|
68218
|
+
var LOCAL_FROM_RE = /(?:^|\s)--from(?:\s+|=)(?:\.{1,2}(?:\/\S*)?|\/\S*|~\/\S*)(?:\s|$)/;
|
|
68219
|
+
var RUN_VALUE_FLAGS = new Set(
|
|
68220
|
+
[
|
|
68221
|
+
"-a --attach --add-host --annotation --blkio-weight --blkio-weight-device --cap-add",
|
|
68222
|
+
"--cap-drop --cgroup-parent --cgroupns --cgroups --cgroup-conf --cidfile --cpu-period",
|
|
68223
|
+
"--cpu-quota --cpu-rt-period --cpu-rt-runtime -c --cpu-shares --cpus --cpuset-cpus",
|
|
68224
|
+
"--cpuset-mems --device --device-cgroup-rule --device-read-bps --device-read-iops",
|
|
68225
|
+
"--device-write-bps --device-write-iops --dns --dns-option --dns-search --domainname",
|
|
68226
|
+
"--entrypoint -e --env --env-file --expose --gpus --group-add --health-cmd",
|
|
68227
|
+
"--health-interval --health-retries --health-start-interval --health-start-period",
|
|
68228
|
+
"--health-timeout -h --hostname --ip --ip6 --ipc --isolation --kernel-memory -l --label",
|
|
68229
|
+
"--label-file --link --link-local-ip --log-driver --log-opt --mac-address -m --memory",
|
|
68230
|
+
"--memory-reservation --memory-swap --memory-swappiness --mount --name --network --net",
|
|
68231
|
+
"--network-alias --oom-score-adj --pid --pids-limit --platform -p --publish --pull",
|
|
68232
|
+
"--restart --runtime --security-opt --shm-size --stop-signal --stop-timeout",
|
|
68233
|
+
"--storage-opt --sysctl --tmpfs --ulimit -u --user --userns --uts -v --volume",
|
|
68234
|
+
"--volume-driver --volumes-from -w --workdir --secret --pod --arch --os --variant",
|
|
68235
|
+
"--timezone --umask --requires --uidmap --gidmap --subuidname --subgidname --hooks-dir",
|
|
68236
|
+
"--init-path --seccomp-policy --sdnotify --personality --preserve-fds --chrootdirs"
|
|
68237
|
+
].join(" ").split(" ")
|
|
68238
|
+
);
|
|
68239
|
+
function runImage(tokens) {
|
|
68240
|
+
const i = tokens.findIndex(
|
|
68241
|
+
(t, k) => /(^|\/)(docker|podman|nerdctl)$/.test(t) && (tokens[k + 1] === "run" || tokens[k + 1] === "container" && tokens[k + 2] === "run")
|
|
68242
|
+
);
|
|
68243
|
+
if (i < 0) return null;
|
|
68244
|
+
const from = tokens[i + 1] === "run" ? i + 2 : i + 3;
|
|
68245
|
+
for (let k = from; k < tokens.length; k++) {
|
|
68246
|
+
const t = tokens[k];
|
|
68247
|
+
if (t.startsWith("-")) {
|
|
68248
|
+
if (!t.includes("=") && RUN_VALUE_FLAGS.has(t)) k++;
|
|
68249
|
+
continue;
|
|
68250
|
+
}
|
|
68251
|
+
return t;
|
|
68252
|
+
}
|
|
68253
|
+
return null;
|
|
68254
|
+
}
|
|
68255
|
+
function launchedPackages(argv) {
|
|
68256
|
+
const t = argv.split(/\s+/).filter(Boolean);
|
|
68257
|
+
const i = t.findIndex((x) => /(^|\/)(npx|pnpx|bunx|uvx|dlx|exec|x|run)$/.test(x));
|
|
68258
|
+
if (i < 0) return [];
|
|
68259
|
+
const bare = (v) => v.startsWith("@") ? v.replace(/^(@[^/]+\/[^@]+).*$/, "$1") : v.replace(/(==|@).*$/, "");
|
|
68260
|
+
const flagged = [];
|
|
68261
|
+
let positional = null;
|
|
68262
|
+
for (let k = i + 1; k < t.length; k++) {
|
|
68263
|
+
const tok = t[k];
|
|
68264
|
+
const eq = /^(--from|-p|--package)=(.*)$/.exec(tok);
|
|
68265
|
+
if (eq) {
|
|
68266
|
+
flagged.push(eq[2]);
|
|
68267
|
+
continue;
|
|
68268
|
+
}
|
|
68269
|
+
if (/^(--from|-p|--package)$/.test(tok)) {
|
|
68270
|
+
if (t[k + 1]) flagged.push(t[++k]);
|
|
68271
|
+
continue;
|
|
68272
|
+
}
|
|
68273
|
+
if (tok.startsWith("-")) continue;
|
|
68274
|
+
positional ??= tok;
|
|
68275
|
+
}
|
|
68276
|
+
const names = flagged.length ? flagged : positional ? [positional] : [];
|
|
68277
|
+
return names.map((n) => bare(n).toLowerCase()).filter(Boolean);
|
|
68278
|
+
}
|
|
68279
|
+
var SHELL_RE = /(?:^|\/)(?:sh|bash|zsh|dash|cmd|pwsh|powershell)(?:\.exe)?$/i;
|
|
68280
|
+
var SHELL_OP_RE = /[;&|`$<>()]/;
|
|
68281
|
+
function launchesOwnPackage(argv, own) {
|
|
68282
|
+
if (own.size === 0) return false;
|
|
68283
|
+
if (SHELL_OP_RE.test(argv) || SHELL_RE.test(argv.split(/\s+/)[0] ?? "")) return false;
|
|
68284
|
+
const l = LAUNCHERS.find((x) => x.re.test(argv));
|
|
68285
|
+
if (!l) return false;
|
|
68286
|
+
const reg = l.registry === "PyPI" ? "pypi" : "npm";
|
|
68287
|
+
const pkgs = launchedPackages(argv);
|
|
68288
|
+
return pkgs.length > 0 && pkgs.every((p) => own.has(`${reg}:${p}`) || own.has(p));
|
|
68289
|
+
}
|
|
68290
|
+
function unpinnedLauncher(argv, own) {
|
|
68291
|
+
if (LOCAL_RUNNER_RE.test(argv)) return null;
|
|
68292
|
+
if (!SHELL_OP_RE.test(argv) && LOCAL_FROM_RE.test(argv)) return null;
|
|
68293
|
+
if (launchesOwnPackage(argv, own)) return null;
|
|
68294
|
+
const image = runImage(argv.split(/\s+/));
|
|
68295
|
+
if (image) {
|
|
68296
|
+
if (image.includes("@sha256:")) return null;
|
|
68297
|
+
const tail = image.slice(image.lastIndexOf("/") + 1);
|
|
68298
|
+
const tag = tail.includes(":") ? tail.slice(tail.indexOf(":") + 1) : "";
|
|
68299
|
+
return !tag || tag === "latest" ? { launcher: "docker run", registry: "the image registry" } : null;
|
|
68300
|
+
}
|
|
68301
|
+
const goMod = /\bgo\s+run\s+(?:-\S+\s+)*([^\s@]+)@(\S+)/.exec(argv);
|
|
68302
|
+
if (goMod)
|
|
68303
|
+
return /^v\d/.test(goMod[2]) || /^[0-9a-f]{40}$/.test(goMod[2]) ? null : { launcher: "go run", registry: "the Go module proxy" };
|
|
68304
|
+
const l = LAUNCHERS.find((x) => x.re.test(argv));
|
|
68305
|
+
if (!l) return null;
|
|
68306
|
+
return /@latest\b/.test(argv) || !PINNED_RE.test(argv) ? { launcher: l.name, registry: l.registry } : null;
|
|
68307
|
+
}
|
|
68308
|
+
function ownPackageNames(manifests) {
|
|
68309
|
+
const out = /* @__PURE__ */ new Set();
|
|
68310
|
+
for (const m of manifests) {
|
|
68311
|
+
if (m.path === "package.json") {
|
|
68312
|
+
try {
|
|
68313
|
+
const pkg = JSON.parse(m.content);
|
|
68314
|
+
if (typeof pkg.name === "string" && pkg.name && pkg.private !== true)
|
|
68315
|
+
out.add(`npm:${pkg.name.toLowerCase()}`);
|
|
68316
|
+
} catch {
|
|
68317
|
+
}
|
|
68318
|
+
} else if (m.path === "pyproject.toml") {
|
|
68319
|
+
const sec = /^\[(project|tool\.poetry)\]\s*$([\s\S]*?)(?=^\[|(?![\s\S]))/m.exec(m.content);
|
|
68320
|
+
const name = sec && /^name\s*=\s*["']([^"']{1,200})["']/m.exec(sec[2]);
|
|
68321
|
+
if (name) {
|
|
68322
|
+
out.add(`pypi:${name[1].toLowerCase()}`);
|
|
68323
|
+
out.add(`pypi:${name[1].toLowerCase().replace(/_/g, "-")}`);
|
|
68324
|
+
}
|
|
68325
|
+
}
|
|
68326
|
+
}
|
|
68327
|
+
return out;
|
|
68328
|
+
}
|
|
68329
|
+
function analyzeMcp(path91, content, own = /* @__PURE__ */ new Set()) {
|
|
67142
68330
|
let cfg;
|
|
67143
68331
|
try {
|
|
67144
68332
|
cfg = JSON.parse(content);
|
|
67145
68333
|
} catch {
|
|
67146
68334
|
return [];
|
|
67147
68335
|
}
|
|
67148
|
-
|
|
68336
|
+
if (!cfg || typeof cfg !== "object") return [];
|
|
68337
|
+
const sections = [];
|
|
68338
|
+
const obj = (x) => x && typeof x === "object" && !Array.isArray(x) ? x : {};
|
|
68339
|
+
sections.push(["mcpServers", obj(cfg.mcpServers)]);
|
|
68340
|
+
sections.push(["servers", obj(cfg.servers)]);
|
|
68341
|
+
const opencode = {};
|
|
68342
|
+
for (const [name, s] of Object.entries(obj(cfg.mcp))) {
|
|
68343
|
+
if (!s || typeof s !== "object") continue;
|
|
68344
|
+
const o = s;
|
|
68345
|
+
const cmd = Array.isArray(o.command) ? o.command.map(String) : [];
|
|
68346
|
+
opencode[name] = {
|
|
68347
|
+
command: cmd[0],
|
|
68348
|
+
args: cmd.slice(1),
|
|
68349
|
+
url: o.url,
|
|
68350
|
+
env: obj(o.environment),
|
|
68351
|
+
disabled: o.enabled === false
|
|
68352
|
+
};
|
|
68353
|
+
}
|
|
68354
|
+
sections.push(["mcp", opencode]);
|
|
68355
|
+
const count = /* @__PURE__ */ new Map();
|
|
68356
|
+
for (const [, sec] of sections)
|
|
68357
|
+
for (const name of Object.keys(sec)) count.set(name, (count.get(name) ?? 0) + 1);
|
|
68358
|
+
const servers = {};
|
|
68359
|
+
for (const [section, sec] of sections)
|
|
68360
|
+
for (const [name, spec] of Object.entries(sec)) {
|
|
68361
|
+
if (!spec || typeof spec !== "object") continue;
|
|
68362
|
+
const key = (count.get(name) ?? 0) > 1 ? `${section}.${name}` : name;
|
|
68363
|
+
servers[key] = { ...spec, anchor: name };
|
|
68364
|
+
}
|
|
68365
|
+
return analyzeMcpServers(servers, path91, content, own);
|
|
67149
68366
|
}
|
|
67150
|
-
function analyzeMcpServers(servers, path91, content = "") {
|
|
68367
|
+
function analyzeMcpServers(servers, path91, content = "", own = /* @__PURE__ */ new Set()) {
|
|
67151
68368
|
const findings = [];
|
|
67152
68369
|
for (const [name, srv] of Object.entries(servers ?? {})) {
|
|
67153
68370
|
if (!srv || srv.disabled) continue;
|
|
67154
68371
|
const argv = [srv.command, ...Array.isArray(srv.args) ? srv.args.map(String) : []].join(" ");
|
|
67155
|
-
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
68372
|
+
const esc = (srv.anchor ?? name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
67156
68373
|
const at = content.search(
|
|
67157
68374
|
new RegExp(`"${esc}"\\s*:|^[ \\t]*\\[mcp_servers\\.(?:"${esc}"|${esc})\\]`, "m")
|
|
67158
68375
|
);
|
|
67159
68376
|
const line = at >= 0 ? lineAtIndex(content, at) : void 0;
|
|
67160
|
-
|
|
68377
|
+
const why = unpinnedLauncher(argv, own);
|
|
68378
|
+
if (why) {
|
|
67161
68379
|
findings.push({
|
|
67162
68380
|
check: "CI-3",
|
|
67163
68381
|
rule: "CI-3.mcp-unpinned",
|
|
@@ -67167,9 +68385,16 @@ function analyzeMcpServers(servers, path91, content = "") {
|
|
|
67167
68385
|
title: `MCP server "${name}" runs an unpinned executable`,
|
|
67168
68386
|
file: path91,
|
|
67169
68387
|
...line ? { line } : {},
|
|
67170
|
-
signals: [
|
|
68388
|
+
signals: [
|
|
68389
|
+
why.launcher === "npx" ? `\`${safeText(argv, 120)}\`: unversioned/@latest npx` : `\`${safeText(argv, 120)}\`: unversioned/latest ${why.launcher}`
|
|
68390
|
+
],
|
|
67171
68391
|
fix: "Pin the MCP server package to an exact version so a PR (or a registry compromise) can\u2019t swap the toolchain.",
|
|
67172
|
-
explain: explainMcp({
|
|
68392
|
+
explain: explainMcp({
|
|
68393
|
+
name,
|
|
68394
|
+
argv,
|
|
68395
|
+
registry: why.registry,
|
|
68396
|
+
pkg: why.launcher === "docker run" ? runImage(argv.split(/\s+/))?.replace(/:[^/:]*$/, "") ?? null : why.launcher === "go run" ? /\bgo\s+run\s+(?:-\S+\s+)*([^\s@]+)@/.exec(argv)?.[1] ?? null : launchedPackages(argv)[0] ?? null
|
|
68397
|
+
})
|
|
67173
68398
|
});
|
|
67174
68399
|
}
|
|
67175
68400
|
for (const [k, v] of Object.entries(srv.env ?? {})) {
|
|
@@ -67198,7 +68423,7 @@ function analyzeMcpServers(servers, path91, content = "") {
|
|
|
67198
68423
|
|
|
67199
68424
|
// src/ci-check/codex.ts
|
|
67200
68425
|
import { parse as parseToml7 } from "smol-toml";
|
|
67201
|
-
function analyzeCodexConfig(path91, content) {
|
|
68426
|
+
function analyzeCodexConfig(path91, content, own = /* @__PURE__ */ new Set()) {
|
|
67202
68427
|
let cfg;
|
|
67203
68428
|
try {
|
|
67204
68429
|
cfg = parseToml7(content);
|
|
@@ -67206,7 +68431,7 @@ function analyzeCodexConfig(path91, content) {
|
|
|
67206
68431
|
return [];
|
|
67207
68432
|
}
|
|
67208
68433
|
const findings = [];
|
|
67209
|
-
findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path91, content));
|
|
68434
|
+
findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path91, content, own));
|
|
67210
68435
|
const sandbox = typeof cfg.sandbox_mode === "string" ? cfg.sandbox_mode : "";
|
|
67211
68436
|
const approval = typeof cfg.approval_policy === "string" ? cfg.approval_policy : "";
|
|
67212
68437
|
const fullAccess = /danger-full-access/i.test(sandbox);
|
|
@@ -67243,10 +68468,12 @@ var UNREAD_RULES = /* @__PURE__ */ new Set([
|
|
|
67243
68468
|
"CI-1.hook-script.unscanned-size",
|
|
67244
68469
|
"CI-6.skill-script.unscanned-size"
|
|
67245
68470
|
]);
|
|
67246
|
-
var CONCEALMENT = /* @__PURE__ */ new Set(["CI-6.unicode-tag-chars", "CI-6.bidi-override"]);
|
|
68471
|
+
var CONCEALMENT = /* @__PURE__ */ new Set(["CI-6.unicode-tag-chars", "CI-6.bidi-override", "CI-6.homoglyph"]);
|
|
67247
68472
|
function tierOf(f) {
|
|
67248
68473
|
const { rule, severity } = f;
|
|
67249
68474
|
if (rule === "CI-1.skill-allowed-tools") return "review";
|
|
68475
|
+
if (rule === "CI-2.trusted-trigger-reader" || rule === "CI-2.fork-code-on-trust" || rule === "CI-1.skill-untrusted-input")
|
|
68476
|
+
return "alert";
|
|
67250
68477
|
if (UNREAD_RULES.has(rule)) return "review";
|
|
67251
68478
|
if (rule.startsWith("CI-2.") || rule.startsWith("CI-4."))
|
|
67252
68479
|
return severity === "advisory" ? "note" : "alert";
|
|
@@ -67611,12 +68838,54 @@ function scanTree(tree) {
|
|
|
67611
68838
|
}
|
|
67612
68839
|
return named.get(dir).has(docPath);
|
|
67613
68840
|
};
|
|
68841
|
+
const callersOf = /* @__PURE__ */ new Map();
|
|
68842
|
+
const autoLabels = /* @__PURE__ */ new Set();
|
|
68843
|
+
const routeOfFile = (file) => file.route !== void 0 ? file.route : routeOf(file.path, skillDirs, appDirs);
|
|
68844
|
+
const manifests = [];
|
|
68845
|
+
for (const file of tree.files) {
|
|
68846
|
+
if (routeOfFile(file) !== "manifest") continue;
|
|
68847
|
+
try {
|
|
68848
|
+
manifests.push({ path: file.path, content: file.content });
|
|
68849
|
+
} catch {
|
|
68850
|
+
}
|
|
68851
|
+
}
|
|
68852
|
+
const ownPackages = ownPackageNames(manifests);
|
|
68853
|
+
for (const file of tree.files) {
|
|
68854
|
+
if (routeOfFile(file) !== "issue-form") continue;
|
|
68855
|
+
try {
|
|
68856
|
+
for (const l of issueFormLabels(file.content)) autoLabels.add(l);
|
|
68857
|
+
} catch {
|
|
68858
|
+
}
|
|
68859
|
+
}
|
|
68860
|
+
const runSources = /* @__PURE__ */ new Map();
|
|
68861
|
+
for (const file of tree.files) {
|
|
68862
|
+
if (routeOfFile(file) !== "workflow") continue;
|
|
68863
|
+
try {
|
|
68864
|
+
const src = runSourceOf(file.path, file.content, autoLabels);
|
|
68865
|
+
if (src) runSources.set(src.name, (runSources.get(src.name) ?? true) && src.trusted);
|
|
68866
|
+
} catch {
|
|
68867
|
+
}
|
|
68868
|
+
}
|
|
68869
|
+
const trustedRunSources = new Set([...runSources].filter(([, t]) => t).map(([n]) => n));
|
|
68870
|
+
for (const file of tree.files) {
|
|
68871
|
+
if (routeOfFile(file) !== "workflow") continue;
|
|
68872
|
+
try {
|
|
68873
|
+
const content = file.content;
|
|
68874
|
+
for (const { target, caller } of localCallsOf(file.path, content, autoLabels)) {
|
|
68875
|
+
const list = callersOf.get(target) ?? [];
|
|
68876
|
+
list.push(caller);
|
|
68877
|
+
callersOf.set(target, list);
|
|
68878
|
+
}
|
|
68879
|
+
} catch {
|
|
68880
|
+
}
|
|
68881
|
+
}
|
|
67614
68882
|
for (const file of tree.files) {
|
|
67615
68883
|
let route = file.route !== void 0 ? file.route : routeOf(file.path, skillDirs, appDirs);
|
|
67616
68884
|
if (route === "app-doc") {
|
|
67617
68885
|
if (!namedBy(file.path)) continue;
|
|
67618
68886
|
route = "instruction";
|
|
67619
68887
|
}
|
|
68888
|
+
if (route === "issue-form" || route === "manifest") continue;
|
|
67620
68889
|
inspected.push(file.path);
|
|
67621
68890
|
if (file.path === SUPPRESSIONS_FILE) continue;
|
|
67622
68891
|
const testKind = route && CONTENT_ROUTES.has(route) ? testPathKind(file.path, route) : "none";
|
|
@@ -67626,10 +68895,20 @@ function scanTree(tree) {
|
|
|
67626
68895
|
const content = file.content;
|
|
67627
68896
|
switch (route) {
|
|
67628
68897
|
case "workflow": {
|
|
67629
|
-
const
|
|
68898
|
+
const callers = callersOf.get(file.path);
|
|
68899
|
+
const f = analyzeWorkflow(file.path, content, callers, autoLabels, trustedRunSources);
|
|
67630
68900
|
if (f) findings.push(f);
|
|
67631
|
-
const s = analyzeWorkflowSecrets(
|
|
68901
|
+
const s = analyzeWorkflowSecrets(
|
|
68902
|
+
file.path,
|
|
68903
|
+
content,
|
|
68904
|
+
callers,
|
|
68905
|
+
autoLabels,
|
|
68906
|
+
trustedRunSources
|
|
68907
|
+
);
|
|
67632
68908
|
if (s) findings.push(s);
|
|
68909
|
+
findings.push(...analyzeWorkflowPlugins(file.path, content, tree.owner));
|
|
68910
|
+
findings.push(...analyzeTrustedReaders(file.path, content, autoLabels, callers));
|
|
68911
|
+
findings.push(...analyzeForkCodeOnTrust(file.path, content, callers, autoLabels));
|
|
67633
68912
|
break;
|
|
67634
68913
|
}
|
|
67635
68914
|
case "agent-config":
|
|
@@ -67642,10 +68921,10 @@ function scanTree(tree) {
|
|
|
67642
68921
|
findings.push(...analyzeScript(file.path, content, "CI-6.skill-script"));
|
|
67643
68922
|
break;
|
|
67644
68923
|
case "mcp":
|
|
67645
|
-
findings.push(...analyzeMcp(file.path, content));
|
|
68924
|
+
findings.push(...analyzeMcp(file.path, content, ownPackages));
|
|
67646
68925
|
break;
|
|
67647
68926
|
case "codex":
|
|
67648
|
-
findings.push(...analyzeCodexConfig(file.path, content));
|
|
68927
|
+
findings.push(...analyzeCodexConfig(file.path, content, ownPackages));
|
|
67649
68928
|
break;
|
|
67650
68929
|
case "instruction":
|
|
67651
68930
|
findings.push(...analyzeInstructionFile(file.path, content));
|
|
@@ -67653,7 +68932,9 @@ function scanTree(tree) {
|
|
|
67653
68932
|
break;
|
|
67654
68933
|
}
|
|
67655
68934
|
} catch (err2) {
|
|
67656
|
-
notes.push(
|
|
68935
|
+
notes.push(
|
|
68936
|
+
`checker degraded on ${file.path}: ${err2?.message ?? "error"}; results may be INCOMPLETE`
|
|
68937
|
+
);
|
|
67657
68938
|
}
|
|
67658
68939
|
if (testKind === "alerts-only") {
|
|
67659
68940
|
const kept = findings.splice(before).filter((f) => tierOf(f) === "alert");
|