@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.js
CHANGED
|
@@ -11103,6 +11103,14 @@ async function checkStatePredicates(predicates) {
|
|
|
11103
11103
|
return null;
|
|
11104
11104
|
}
|
|
11105
11105
|
}
|
|
11106
|
+
function resolveDaemonPort(env = process.env) {
|
|
11107
|
+
if (env.NODE9_TESTING !== "1") return DEFAULT_DAEMON_PORT;
|
|
11108
|
+
const raw = env.NODE9_DAEMON_PORT;
|
|
11109
|
+
if (raw === void 0 || raw === "") return DEFAULT_DAEMON_PORT;
|
|
11110
|
+
const port = Number(raw);
|
|
11111
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) return DEFAULT_DAEMON_PORT;
|
|
11112
|
+
return port;
|
|
11113
|
+
}
|
|
11106
11114
|
function getInternalToken() {
|
|
11107
11115
|
try {
|
|
11108
11116
|
const pidFile = import_path10.default.join(import_os8.default.homedir(), ".node9", "daemon.pid");
|
|
@@ -11376,7 +11384,7 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
11376
11384
|
signal: AbortSignal.timeout(3e3)
|
|
11377
11385
|
});
|
|
11378
11386
|
}
|
|
11379
|
-
var import_fs10, import_net, import_path10, import_os8, ACTIVITY_SOCKET_PATH, DAEMON_PORT, DAEMON_HOST;
|
|
11387
|
+
var import_fs10, import_net, import_path10, import_os8, ACTIVITY_SOCKET_PATH, DEFAULT_DAEMON_PORT, DAEMON_PORT, DAEMON_HOST;
|
|
11380
11388
|
var init_daemon = __esm({
|
|
11381
11389
|
"src/auth/daemon.ts"() {
|
|
11382
11390
|
"use strict";
|
|
@@ -11385,7 +11393,8 @@ var init_daemon = __esm({
|
|
|
11385
11393
|
import_path10 = __toESM(require("path"));
|
|
11386
11394
|
import_os8 = __toESM(require("os"));
|
|
11387
11395
|
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path10.default.join(import_os8.default.tmpdir(), "node9-activity.sock");
|
|
11388
|
-
|
|
11396
|
+
DEFAULT_DAEMON_PORT = 7391;
|
|
11397
|
+
DAEMON_PORT = resolveDaemonPort();
|
|
11389
11398
|
DAEMON_HOST = "127.0.0.1";
|
|
11390
11399
|
}
|
|
11391
11400
|
});
|
|
@@ -19433,19 +19442,24 @@ async function postCostBatches(apiUrl, apiKey, machineId, entries) {
|
|
|
19433
19442
|
} catch {
|
|
19434
19443
|
}
|
|
19435
19444
|
if (typeof stored === "number" && stored < batch.length) {
|
|
19436
|
-
|
|
19437
|
-
|
|
19438
|
-
`[cost-sync] dropped ${batch.length - stored} of ${batch.length} rows
|
|
19439
|
-
`
|
|
19440
|
-
);
|
|
19445
|
+
debugLog(`[cost-sync] dropped ${batch.length - stored} of ${batch.length} rows
|
|
19446
|
+
`);
|
|
19441
19447
|
}
|
|
19442
19448
|
}
|
|
19443
19449
|
} catch (err2) {
|
|
19444
|
-
|
|
19450
|
+
debugLog(`[cost-sync] ${safeMessage(err2)}
|
|
19445
19451
|
`);
|
|
19446
19452
|
}
|
|
19447
19453
|
}
|
|
19448
19454
|
}
|
|
19455
|
+
function debugLog(line) {
|
|
19456
|
+
try {
|
|
19457
|
+
import_fs33.default.mkdirSync(import_path34.default.dirname(HOOK_DEBUG_LOG), { recursive: true });
|
|
19458
|
+
import_fs33.default.appendFileSync(HOOK_DEBUG_LOG, line);
|
|
19459
|
+
} catch (err2) {
|
|
19460
|
+
console.error(`[cost-sync] could not write ${HOOK_DEBUG_LOG}: ${safeMessage(err2)}`);
|
|
19461
|
+
}
|
|
19462
|
+
}
|
|
19449
19463
|
async function syncCost() {
|
|
19450
19464
|
const creds = getCredentials();
|
|
19451
19465
|
if (!creds?.apiKey || !creds?.apiUrl) return;
|
|
@@ -32340,7 +32354,7 @@ var require_diagnostics = __commonJS({
|
|
|
32340
32354
|
proxyConnected: diagnosticsChannel.channel("undici:proxy:connected")
|
|
32341
32355
|
};
|
|
32342
32356
|
var isTrackingClientEvents = false;
|
|
32343
|
-
function trackClientEvents(
|
|
32357
|
+
function trackClientEvents(debugLog2 = undiciDebugLog) {
|
|
32344
32358
|
if (isTrackingClientEvents) {
|
|
32345
32359
|
return;
|
|
32346
32360
|
}
|
|
@@ -32355,7 +32369,7 @@ var require_diagnostics = __commonJS({
|
|
|
32355
32369
|
const {
|
|
32356
32370
|
connectParams: { version: version2, protocol, port, host }
|
|
32357
32371
|
} = evt;
|
|
32358
|
-
|
|
32372
|
+
debugLog2(
|
|
32359
32373
|
"connecting to %s%s using %s%s",
|
|
32360
32374
|
host,
|
|
32361
32375
|
port ? `:${port}` : "",
|
|
@@ -32370,7 +32384,7 @@ var require_diagnostics = __commonJS({
|
|
|
32370
32384
|
const {
|
|
32371
32385
|
connectParams: { version: version2, protocol, port, host }
|
|
32372
32386
|
} = evt;
|
|
32373
|
-
|
|
32387
|
+
debugLog2(
|
|
32374
32388
|
"connected to %s%s using %s%s",
|
|
32375
32389
|
host,
|
|
32376
32390
|
port ? `:${port}` : "",
|
|
@@ -32386,7 +32400,7 @@ var require_diagnostics = __commonJS({
|
|
|
32386
32400
|
connectParams: { version: version2, protocol, port, host },
|
|
32387
32401
|
error
|
|
32388
32402
|
} = evt;
|
|
32389
|
-
|
|
32403
|
+
debugLog2(
|
|
32390
32404
|
"connection to %s%s using %s%s errored - %s",
|
|
32391
32405
|
host,
|
|
32392
32406
|
port ? `:${port}` : "",
|
|
@@ -32402,12 +32416,12 @@ var require_diagnostics = __commonJS({
|
|
|
32402
32416
|
const {
|
|
32403
32417
|
request: { method, path: path91, origin }
|
|
32404
32418
|
} = evt;
|
|
32405
|
-
|
|
32419
|
+
debugLog2("sending request to %s %s%s", method, origin, path91);
|
|
32406
32420
|
}
|
|
32407
32421
|
);
|
|
32408
32422
|
}
|
|
32409
32423
|
var isTrackingRequestEvents = false;
|
|
32410
|
-
function trackRequestEvents(
|
|
32424
|
+
function trackRequestEvents(debugLog2 = undiciDebugLog) {
|
|
32411
32425
|
if (isTrackingRequestEvents) {
|
|
32412
32426
|
return;
|
|
32413
32427
|
}
|
|
@@ -32423,7 +32437,7 @@ var require_diagnostics = __commonJS({
|
|
|
32423
32437
|
request: { method, path: path91, origin },
|
|
32424
32438
|
response: { statusCode }
|
|
32425
32439
|
} = evt;
|
|
32426
|
-
|
|
32440
|
+
debugLog2(
|
|
32427
32441
|
"received response to %s %s%s - HTTP %d",
|
|
32428
32442
|
method,
|
|
32429
32443
|
origin,
|
|
@@ -32438,7 +32452,7 @@ var require_diagnostics = __commonJS({
|
|
|
32438
32452
|
const {
|
|
32439
32453
|
request: { method, path: path91, origin }
|
|
32440
32454
|
} = evt;
|
|
32441
|
-
|
|
32455
|
+
debugLog2("trailers received from %s %s%s", method, origin, path91);
|
|
32442
32456
|
}
|
|
32443
32457
|
);
|
|
32444
32458
|
diagnosticsChannel.subscribe(
|
|
@@ -32448,7 +32462,7 @@ var require_diagnostics = __commonJS({
|
|
|
32448
32462
|
request: { method, path: path91, origin },
|
|
32449
32463
|
error
|
|
32450
32464
|
} = evt;
|
|
32451
|
-
|
|
32465
|
+
debugLog2(
|
|
32452
32466
|
"request to %s %s%s errored - %s",
|
|
32453
32467
|
method,
|
|
32454
32468
|
origin,
|
|
@@ -32459,7 +32473,7 @@ var require_diagnostics = __commonJS({
|
|
|
32459
32473
|
);
|
|
32460
32474
|
}
|
|
32461
32475
|
var isTrackingWebSocketEvents = false;
|
|
32462
|
-
function trackWebSocketEvents(
|
|
32476
|
+
function trackWebSocketEvents(debugLog2 = websocketDebuglog) {
|
|
32463
32477
|
if (isTrackingWebSocketEvents) {
|
|
32464
32478
|
return;
|
|
32465
32479
|
}
|
|
@@ -32473,9 +32487,9 @@ var require_diagnostics = __commonJS({
|
|
|
32473
32487
|
(evt) => {
|
|
32474
32488
|
if (evt.address != null) {
|
|
32475
32489
|
const { address, port } = evt.address;
|
|
32476
|
-
|
|
32490
|
+
debugLog2("connection opened %s%s", address, port ? `:${port}` : "");
|
|
32477
32491
|
} else {
|
|
32478
|
-
|
|
32492
|
+
debugLog2("connection opened");
|
|
32479
32493
|
}
|
|
32480
32494
|
}
|
|
32481
32495
|
);
|
|
@@ -32483,7 +32497,7 @@ var require_diagnostics = __commonJS({
|
|
|
32483
32497
|
"undici:websocket:close",
|
|
32484
32498
|
(evt) => {
|
|
32485
32499
|
const { websocket, code: code2, reason } = evt;
|
|
32486
|
-
|
|
32500
|
+
debugLog2(
|
|
32487
32501
|
"closed connection to %s - %s %s",
|
|
32488
32502
|
websocket.url,
|
|
32489
32503
|
code2,
|
|
@@ -32494,19 +32508,19 @@ var require_diagnostics = __commonJS({
|
|
|
32494
32508
|
diagnosticsChannel.subscribe(
|
|
32495
32509
|
"undici:websocket:socket_error",
|
|
32496
32510
|
(err2) => {
|
|
32497
|
-
|
|
32511
|
+
debugLog2("connection errored - %s", err2.message);
|
|
32498
32512
|
}
|
|
32499
32513
|
);
|
|
32500
32514
|
diagnosticsChannel.subscribe(
|
|
32501
32515
|
"undici:websocket:ping",
|
|
32502
32516
|
(evt) => {
|
|
32503
|
-
|
|
32517
|
+
debugLog2("ping received");
|
|
32504
32518
|
}
|
|
32505
32519
|
);
|
|
32506
32520
|
diagnosticsChannel.subscribe(
|
|
32507
32521
|
"undici:websocket:pong",
|
|
32508
32522
|
(evt) => {
|
|
32509
|
-
|
|
32523
|
+
debugLog2("pong received");
|
|
32510
32524
|
}
|
|
32511
32525
|
);
|
|
32512
32526
|
}
|
|
@@ -58994,6 +59008,42 @@ function shellOutputText(toolResponse) {
|
|
|
58994
59008
|
}
|
|
58995
59009
|
return collectToolOutputText(toolResponse).text;
|
|
58996
59010
|
}
|
|
59011
|
+
function mapToolOutputStrings(toolResponse, fn) {
|
|
59012
|
+
const walk = (v, depth) => {
|
|
59013
|
+
if (typeof v === "string") return fn(v);
|
|
59014
|
+
if (depth >= MAX_DEPTH3 || v === null || typeof v !== "object") return v;
|
|
59015
|
+
if (Array.isArray(v)) return v.map((x) => walk(x, depth + 1));
|
|
59016
|
+
const out = {};
|
|
59017
|
+
for (const [k, x] of Object.entries(v)) out[k] = walk(x, depth + 1);
|
|
59018
|
+
return out;
|
|
59019
|
+
};
|
|
59020
|
+
return walk(toolResponse, 0);
|
|
59021
|
+
}
|
|
59022
|
+
function frameToolOutputLeaves(toolResponse, isSuspect, frame, neutralize) {
|
|
59023
|
+
const suspect = [];
|
|
59024
|
+
let largestAt = -1;
|
|
59025
|
+
let largestLen = 0;
|
|
59026
|
+
mapToolOutputStrings(toolResponse, (s) => {
|
|
59027
|
+
const i2 = suspect.length;
|
|
59028
|
+
suspect.push(s.length > 0 && isSuspect(s));
|
|
59029
|
+
if (s.length > largestLen) {
|
|
59030
|
+
largestLen = s.length;
|
|
59031
|
+
largestAt = i2;
|
|
59032
|
+
}
|
|
59033
|
+
return s;
|
|
59034
|
+
});
|
|
59035
|
+
const anySuspect = suspect.some(Boolean);
|
|
59036
|
+
let i = 0;
|
|
59037
|
+
return mapToolOutputStrings(toolResponse, (s) => {
|
|
59038
|
+
const at = i++;
|
|
59039
|
+
const pick2 = anySuspect ? suspect[at] : at === largestAt;
|
|
59040
|
+
if (!pick2) return neutralize(s);
|
|
59041
|
+
const f = frame();
|
|
59042
|
+
return `${f.header}
|
|
59043
|
+
${neutralize(s)}
|
|
59044
|
+
${f.footer}`;
|
|
59045
|
+
});
|
|
59046
|
+
}
|
|
58997
59047
|
|
|
58998
59048
|
// src/cli/commands/log.ts
|
|
58999
59049
|
init_hasher();
|
|
@@ -59224,19 +59274,36 @@ function registerLogCommand(program2) {
|
|
|
59224
59274
|
process.stdout.write(JSON.stringify({ redacted: out, found, injection }) + "\n");
|
|
59225
59275
|
} else {
|
|
59226
59276
|
const warnings = [];
|
|
59277
|
+
const inPlace = agent === "Claude Code" && config.policy.dlp.enabled;
|
|
59278
|
+
const redactedNames = /* @__PURE__ */ new Set();
|
|
59279
|
+
let updatedToolOutput = void 0;
|
|
59280
|
+
if (inPlace) {
|
|
59281
|
+
const mapped = mapToolOutputStrings(payload.tool_response, (leaf) => {
|
|
59282
|
+
const r = redactText(leaf);
|
|
59283
|
+
for (const name of r.found) redactedNames.add(name);
|
|
59284
|
+
return r.result;
|
|
59285
|
+
});
|
|
59286
|
+
if (redactedNames.size > 0) updatedToolOutput = mapped;
|
|
59287
|
+
}
|
|
59227
59288
|
const hit = scanText(toolOutput);
|
|
59228
|
-
|
|
59229
|
-
|
|
59230
|
-
|
|
59231
|
-
`output-secret:${hit.patternName}`
|
|
59232
|
-
);
|
|
59289
|
+
const secretName = hit?.patternName ?? [...redactedNames][0];
|
|
59290
|
+
if (secretName) {
|
|
59291
|
+
await notifySessionTaint(payloadSessionId ?? "", `output-secret:${secretName}`);
|
|
59233
59292
|
warnings.push(
|
|
59234
|
-
`\u26A0\uFE0F node9: this tool output contained a credential (${
|
|
59293
|
+
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.`
|
|
59235
59294
|
);
|
|
59236
59295
|
}
|
|
59237
59296
|
if (injectionOn) {
|
|
59238
59297
|
const m = scanInjection(toolOutput, { tool: rawToolName });
|
|
59239
59298
|
if (m && atLeastConfidence(m.confidence, inj.minConfidence)) {
|
|
59299
|
+
if (agent === "Claude Code") {
|
|
59300
|
+
updatedToolOutput = frameToolOutputLeaves(
|
|
59301
|
+
updatedToolOutput ?? payload.tool_response,
|
|
59302
|
+
(leaf) => scanInjection(leaf, { tool: rawToolName }) !== null,
|
|
59303
|
+
newUntrustedFrame,
|
|
59304
|
+
neutralizeMarkers
|
|
59305
|
+
);
|
|
59306
|
+
}
|
|
59240
59307
|
await notifySessionTaint(
|
|
59241
59308
|
payloadSessionId ?? "",
|
|
59242
59309
|
`output-injection:${m.signals.join("+")}`
|
|
@@ -59246,12 +59313,22 @@ function registerLogCommand(program2) {
|
|
|
59246
59313
|
);
|
|
59247
59314
|
}
|
|
59248
59315
|
}
|
|
59249
|
-
if (
|
|
59316
|
+
if (redactedNames.size > 0) {
|
|
59317
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
59318
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
59319
|
+
tool,
|
|
59320
|
+
source: "output-redacted",
|
|
59321
|
+
outputRedacted: [...redactedNames],
|
|
59322
|
+
...payloadSessionId ? { sessionId: payloadSessionId } : {}
|
|
59323
|
+
});
|
|
59324
|
+
}
|
|
59325
|
+
if ((warnings.length > 0 || updatedToolOutput !== void 0) && (agent === "Claude Code" || agent === "Codex")) {
|
|
59250
59326
|
process.stdout.write(
|
|
59251
59327
|
JSON.stringify({
|
|
59252
59328
|
hookSpecificOutput: {
|
|
59253
59329
|
hookEventName: "PostToolUse",
|
|
59254
|
-
additionalContext: warnings.join("\n\n")
|
|
59330
|
+
...warnings.length > 0 ? { additionalContext: warnings.join("\n\n") } : {},
|
|
59331
|
+
...updatedToolOutput !== void 0 ? { updatedToolOutput } : {}
|
|
59255
59332
|
}
|
|
59256
59333
|
}) + "\n"
|
|
59257
59334
|
);
|
|
@@ -64493,6 +64570,97 @@ function isZwLegitScript(cp) {
|
|
|
64493
64570
|
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;
|
|
64494
64571
|
}
|
|
64495
64572
|
var isAsciiWordChar = (ch) => !!ch && /[A-Za-z0-9]/.test(ch);
|
|
64573
|
+
var LOOKALIKE = {
|
|
64574
|
+
\u0430: "a",
|
|
64575
|
+
\u0435: "e",
|
|
64576
|
+
\u043E: "o",
|
|
64577
|
+
\u0440: "p",
|
|
64578
|
+
\u0441: "c",
|
|
64579
|
+
\u0443: "y",
|
|
64580
|
+
\u0445: "x",
|
|
64581
|
+
\u0456: "i",
|
|
64582
|
+
\u0458: "j",
|
|
64583
|
+
\u0455: "s",
|
|
64584
|
+
"\u0501": "d",
|
|
64585
|
+
"\u051B": "q",
|
|
64586
|
+
"\u051D": "w",
|
|
64587
|
+
\u0410: "A",
|
|
64588
|
+
\u0412: "B",
|
|
64589
|
+
\u0415: "E",
|
|
64590
|
+
\u041A: "K",
|
|
64591
|
+
\u041C: "M",
|
|
64592
|
+
\u041D: "H",
|
|
64593
|
+
\u041E: "O",
|
|
64594
|
+
\u0420: "P",
|
|
64595
|
+
\u0421: "C",
|
|
64596
|
+
\u0422: "T",
|
|
64597
|
+
\u0425: "X",
|
|
64598
|
+
\u0406: "I",
|
|
64599
|
+
\u0408: "J",
|
|
64600
|
+
\u0405: "S",
|
|
64601
|
+
"\u050C": "G",
|
|
64602
|
+
"\u051A": "Q",
|
|
64603
|
+
"\u051C": "W",
|
|
64604
|
+
\u03BF: "o",
|
|
64605
|
+
\u03B1: "a",
|
|
64606
|
+
\u03C1: "p",
|
|
64607
|
+
\u03C4: "t",
|
|
64608
|
+
\u03BD: "v",
|
|
64609
|
+
\u03C5: "u",
|
|
64610
|
+
\u03BA: "k",
|
|
64611
|
+
\u03C7: "x",
|
|
64612
|
+
\u03B9: "i",
|
|
64613
|
+
\u0391: "A",
|
|
64614
|
+
\u0392: "B",
|
|
64615
|
+
\u0395: "E",
|
|
64616
|
+
\u0397: "H",
|
|
64617
|
+
\u0399: "I",
|
|
64618
|
+
\u039A: "K",
|
|
64619
|
+
\u039C: "M",
|
|
64620
|
+
\u039D: "N",
|
|
64621
|
+
\u039F: "O",
|
|
64622
|
+
\u03A1: "P",
|
|
64623
|
+
\u03A4: "T",
|
|
64624
|
+
\u03A7: "X",
|
|
64625
|
+
\u03A5: "Y",
|
|
64626
|
+
\u0396: "Z",
|
|
64627
|
+
// Armenian (review, round 2)
|
|
64628
|
+
"\u0585": "o",
|
|
64629
|
+
"\u057D": "u",
|
|
64630
|
+
"\u0570": "h",
|
|
64631
|
+
"\u0578": "n",
|
|
64632
|
+
"\u0581": "g",
|
|
64633
|
+
"\u0566": "q"
|
|
64634
|
+
};
|
|
64635
|
+
var MIXED_WORD_RE = /[A-Za-z\u0370-\u03ff\u0400-\u04ff\u0500-\u052f\u0530-\u058f]{4,60}/g;
|
|
64636
|
+
var FOREIGN_LETTER_RE = /[\u0370-\u03ff\u0400-\u04ff\u0500-\u052f\u0530-\u058f]/;
|
|
64637
|
+
var GREEK_RE = /[\u0370-\u03ff]/;
|
|
64638
|
+
var EXPLAINED_LINE_RE = /cyrillic|greek|armenian|homoglyph|confusable|look-?alike/i;
|
|
64639
|
+
var HOMOGLYPH_MAX_LINE = 2e3;
|
|
64640
|
+
function homoglyphWord(content) {
|
|
64641
|
+
const lines = content.split("\n");
|
|
64642
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64643
|
+
const text = lines[i];
|
|
64644
|
+
if (text.length > HOMOGLYPH_MAX_LINE) continue;
|
|
64645
|
+
const visible = text.replace(/<!--[\s\S]*?(?:-->|$)/g, "");
|
|
64646
|
+
if (EXPLAINED_LINE_RE.test(visible)) continue;
|
|
64647
|
+
for (const m of text.matchAll(MIXED_WORD_RE)) {
|
|
64648
|
+
const chars = [...m[0]];
|
|
64649
|
+
const latin = chars.filter((c) => /[A-Za-z]/.test(c)).length;
|
|
64650
|
+
const foreign = chars.filter((c) => FOREIGN_LETTER_RE.test(c));
|
|
64651
|
+
if (latin < 2 || !foreign.some((c) => c in LOOKALIKE) || foreign.length > latin) continue;
|
|
64652
|
+
const lead = chars.findIndex((c) => !FOREIGN_LETTER_RE.test(c));
|
|
64653
|
+
if (lead > 0 && chars.slice(0, lead).every((c) => GREEK_RE.test(c)) && lead === foreign.length)
|
|
64654
|
+
continue;
|
|
64655
|
+
return {
|
|
64656
|
+
folded: chars.map((c) => LOOKALIKE[c] ?? c).join(""),
|
|
64657
|
+
count: foreign.length,
|
|
64658
|
+
line: i + 1
|
|
64659
|
+
};
|
|
64660
|
+
}
|
|
64661
|
+
}
|
|
64662
|
+
return null;
|
|
64663
|
+
}
|
|
64496
64664
|
function suspiciousZeroWidth(text) {
|
|
64497
64665
|
let n = 0;
|
|
64498
64666
|
let first = -1;
|
|
@@ -64736,6 +64904,21 @@ var lineAt = (text, i) => i >= 0 ? lineAtIndex(text, i) : void 0;
|
|
|
64736
64904
|
function analyzeInstructionFile(path91, content) {
|
|
64737
64905
|
const findings = [];
|
|
64738
64906
|
const decoded = decodeSuspiciousBase64(content);
|
|
64907
|
+
const glyph = homoglyphWord(content);
|
|
64908
|
+
if (glyph)
|
|
64909
|
+
findings.push(
|
|
64910
|
+
mk2(
|
|
64911
|
+
"CI-6.homoglyph",
|
|
64912
|
+
"advisory",
|
|
64913
|
+
"A word in an agent instruction file is spelled with look-alike letters",
|
|
64914
|
+
[
|
|
64915
|
+
`\`${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`
|
|
64916
|
+
],
|
|
64917
|
+
"Spell the word with plain Latin letters, or explain on the same line why the look-alikes are there.",
|
|
64918
|
+
path91,
|
|
64919
|
+
glyph.line
|
|
64920
|
+
)
|
|
64921
|
+
);
|
|
64739
64922
|
if (TAG_CHARS.test(content))
|
|
64740
64923
|
findings.push(
|
|
64741
64924
|
mk2(
|
|
@@ -64874,13 +65057,17 @@ function analyzeInstructionFile(path91, content) {
|
|
|
64874
65057
|
}
|
|
64875
65058
|
|
|
64876
65059
|
// src/ci-check/route.ts
|
|
65060
|
+
var MCP_CONFIG_RE = /\.mcp\.json$|\.cursor\/mcp\.json$|(^|\/)\.vscode\/mcp\.json$|(^|\/)\.kiro\/settings\/mcp\.json$|(^|\/)\.gemini\/settings\.json$|(^|\/)opencode\.json$|(^|\/)\.opencode\/config\.json$/;
|
|
65061
|
+
var ISSUE_FORM_RE = /^\.github\/ISSUE_TEMPLATE\/[^/]+\.(ya?ml|md)$/i;
|
|
64877
65062
|
function routeOf(path91, skillDirs, appDirs = /* @__PURE__ */ new Set()) {
|
|
64878
65063
|
if (path91 === SUPPRESSIONS_FILE) return "suppressions";
|
|
64879
65064
|
if (/\.github\/workflows\/.+\.ya?ml$/.test(path91)) return "workflow";
|
|
65065
|
+
if (ISSUE_FORM_RE.test(path91)) return "issue-form";
|
|
65066
|
+
if (path91 === "package.json" || path91 === "pyproject.toml") return "manifest";
|
|
64880
65067
|
if (/\.claude\/settings(\.local)?\.json$/.test(path91)) return "agent-config";
|
|
64881
65068
|
if (isHookScript(path91)) return "hook-script";
|
|
64882
65069
|
if (isSkillScript(path91, skillDirs)) return "skill-script";
|
|
64883
|
-
if (
|
|
65070
|
+
if (MCP_CONFIG_RE.test(path91)) return "mcp";
|
|
64884
65071
|
if (/(^|\/)\.codex\/config\.toml$/.test(path91)) return "codex";
|
|
64885
65072
|
if (isInstructionFile(path91, skillDirs, appDirs)) return "instruction";
|
|
64886
65073
|
if (isAppDoc(path91, skillDirs, appDirs)) return "app-doc";
|
|
@@ -64912,6 +65099,11 @@ var SURFACE_FILES = [
|
|
|
64912
65099
|
".claude/settings.local.json",
|
|
64913
65100
|
".mcp.json",
|
|
64914
65101
|
".cursor/mcp.json",
|
|
65102
|
+
".vscode/mcp.json",
|
|
65103
|
+
".kiro/settings/mcp.json",
|
|
65104
|
+
".gemini/settings.json",
|
|
65105
|
+
"opencode.json",
|
|
65106
|
+
".opencode/config.json",
|
|
64915
65107
|
".codex/config.toml",
|
|
64916
65108
|
// CI-6: agent instruction files (auto-loaded into the agent's system prompt).
|
|
64917
65109
|
"CLAUDE.md",
|
|
@@ -64923,7 +65115,7 @@ var SURFACE_FILES = [
|
|
|
64923
65115
|
".clinerules"
|
|
64924
65116
|
];
|
|
64925
65117
|
var WORKFLOW_DIR = ".github/workflows";
|
|
64926
|
-
var CONFIG_FILE_RE = /(^|\/)\.claude\/settings(\.local)?\.json$|(^|\/)\.mcp\.json$|(^|\/)\.cursor\/mcp\.json$|(^|\/)\.codex\/config\.toml$/;
|
|
65118
|
+
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$/;
|
|
64927
65119
|
function selectSurface(paths) {
|
|
64928
65120
|
const skillDirs = skillDirsOf(paths);
|
|
64929
65121
|
const appDirs = appSkillDirsOf(paths, skillDirs);
|
|
@@ -65072,6 +65264,7 @@ async function listWorkflowPaths(owner, repo, notes) {
|
|
|
65072
65264
|
return json.filter((e) => e.type === "file" && /\.ya?ml$/.test(e.name)).map((e) => e.path);
|
|
65073
65265
|
}
|
|
65074
65266
|
var ROOT_WORKFLOW_RE = /^\.github\/workflows\/[^/]+\.ya?ml$/;
|
|
65267
|
+
var MAX_ISSUE_FORMS = 40;
|
|
65075
65268
|
function kindOfMode(mode) {
|
|
65076
65269
|
if (mode === "120000") return "link";
|
|
65077
65270
|
if (/^100[0-7]{3}$/.test(mode)) return "file";
|
|
@@ -65281,8 +65474,15 @@ function planSurface(paths, notes, truncated) {
|
|
|
65281
65474
|
const present = new Set(paths);
|
|
65282
65475
|
const root = SURFACE_FILES.filter((p) => present.has(p));
|
|
65283
65476
|
const workflows = paths.filter((p) => ROOT_WORKFLOW_RE.test(p));
|
|
65477
|
+
const allForms = paths.filter((p) => ISSUE_FORM_RE.test(p));
|
|
65478
|
+
const forms = allForms.slice(0, MAX_ISSUE_FORMS);
|
|
65479
|
+
if (allForms.length > MAX_ISSUE_FORMS)
|
|
65480
|
+
notes.push(
|
|
65481
|
+
`${allForms.length} issue forms; read the first ${MAX_ISSUE_FORMS} (a label an unread form applies is not known): results ${INCOMPLETE}.`
|
|
65482
|
+
);
|
|
65483
|
+
forms.push(...["package.json", "pyproject.toml"].filter((p) => present.has(p)));
|
|
65284
65484
|
const nested = pickSurfacePaths(paths, truncated, notes, { files: Number.POSITIVE_INFINITY });
|
|
65285
|
-
return [.../* @__PURE__ */ new Set([...root, ...workflows, ...nested])];
|
|
65485
|
+
return [.../* @__PURE__ */ new Set([...root, ...workflows, ...forms, ...nested])];
|
|
65286
65486
|
}
|
|
65287
65487
|
function planReads(l, R, opts = {}) {
|
|
65288
65488
|
const notes = [];
|
|
@@ -65348,7 +65548,7 @@ function planReads(l, R, opts = {}) {
|
|
|
65348
65548
|
function capNested(reads, cap, notes) {
|
|
65349
65549
|
if (cap === void 0) return reads;
|
|
65350
65550
|
const rootSet = new Set(SURFACE_FILES);
|
|
65351
|
-
const isRoot = (r) => rootSet.has(r.rel) || ROOT_WORKFLOW_RE.test(r.rel);
|
|
65551
|
+
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";
|
|
65352
65552
|
const nested = reads.filter((r) => !isRoot(r));
|
|
65353
65553
|
if (nested.length < cap) return reads;
|
|
65354
65554
|
const keep = new Set(nested.slice(0, cap));
|
|
@@ -65444,6 +65644,45 @@ function readLocalFile(abs, rel) {
|
|
|
65444
65644
|
if (fd !== void 0) import_fs74.default.closeSync(fd);
|
|
65445
65645
|
}
|
|
65446
65646
|
}
|
|
65647
|
+
function readSmallFile(p, max) {
|
|
65648
|
+
let fd;
|
|
65649
|
+
try {
|
|
65650
|
+
fd = import_fs74.default.openSync(p, import_fs74.default.constants.O_RDONLY | (import_fs74.default.constants.O_NOFOLLOW ?? 0));
|
|
65651
|
+
const st = import_fs74.default.fstatSync(fd);
|
|
65652
|
+
if (!st.isFile() || st.size > max) return null;
|
|
65653
|
+
const buf = Buffer.alloc(st.size);
|
|
65654
|
+
const n = import_fs74.default.readSync(fd, buf, 0, st.size, 0);
|
|
65655
|
+
return buf.subarray(0, n).toString("utf8");
|
|
65656
|
+
} catch {
|
|
65657
|
+
return null;
|
|
65658
|
+
} finally {
|
|
65659
|
+
if (fd !== void 0) import_fs74.default.closeSync(fd);
|
|
65660
|
+
}
|
|
65661
|
+
}
|
|
65662
|
+
function localGitHubOwner(root) {
|
|
65663
|
+
let cfg = readSmallFile(import_path71.default.join(root, ".git", "config"), 1e6);
|
|
65664
|
+
if (cfg === null) {
|
|
65665
|
+
const pointer = readSmallFile(import_path71.default.join(root, ".git"), 4096);
|
|
65666
|
+
const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
|
|
65667
|
+
if (!m) return void 0;
|
|
65668
|
+
let gitDir = import_path71.default.resolve(root, m[1]);
|
|
65669
|
+
const common = readSmallFile(import_path71.default.join(gitDir, "commondir"), 4096);
|
|
65670
|
+
if (common !== null) gitDir = import_path71.default.resolve(gitDir, common.trim());
|
|
65671
|
+
cfg = readSmallFile(import_path71.default.join(gitDir, "config"), 1e6);
|
|
65672
|
+
if (cfg === null) return void 0;
|
|
65673
|
+
}
|
|
65674
|
+
let inOrigin = false;
|
|
65675
|
+
for (const line of cfg.split(/\r?\n/)) {
|
|
65676
|
+
const sec = /^\s*\[\s*([^\]\s]+)(?:\s+"([^"]*)")?\s*\]/.exec(line);
|
|
65677
|
+
if (sec) {
|
|
65678
|
+
inOrigin = sec[1].toLowerCase() === "remote" && sec[2] === "origin";
|
|
65679
|
+
continue;
|
|
65680
|
+
}
|
|
65681
|
+
const url = inOrigin && /^\s*url\s*=\s*(.+?)\s*$/i.exec(line);
|
|
65682
|
+
if (url) return /github\.com[:/]([\w.-]{1,100})\//i.exec(url[1])?.[1];
|
|
65683
|
+
}
|
|
65684
|
+
return void 0;
|
|
65685
|
+
}
|
|
65447
65686
|
function readLocalTree(dir) {
|
|
65448
65687
|
const root = dir.replace(/^~/, process.env.HOME ?? "~");
|
|
65449
65688
|
const notes = [];
|
|
@@ -65473,8 +65712,10 @@ function readLocalTree(dir) {
|
|
|
65473
65712
|
}
|
|
65474
65713
|
});
|
|
65475
65714
|
}
|
|
65715
|
+
const owner = localGitHubOwner(root);
|
|
65476
65716
|
return {
|
|
65477
65717
|
source: root,
|
|
65718
|
+
...owner ? { owner } : {},
|
|
65478
65719
|
files,
|
|
65479
65720
|
notes,
|
|
65480
65721
|
paths: plan.paths,
|
|
@@ -65587,8 +65828,10 @@ function readGitRefTree(dir, ref) {
|
|
|
65587
65828
|
}
|
|
65588
65829
|
files.push({ path: rel, content, route });
|
|
65589
65830
|
}
|
|
65831
|
+
const owner = localGitHubOwner(root);
|
|
65590
65832
|
return {
|
|
65591
65833
|
source: `${root}@${ref}`,
|
|
65834
|
+
...owner ? { owner } : {},
|
|
65592
65835
|
files,
|
|
65593
65836
|
notes,
|
|
65594
65837
|
paths: plan.paths,
|
|
@@ -65688,7 +65931,13 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65688
65931
|
const listed2 = await listTree(owner, repo, notes);
|
|
65689
65932
|
if (!listed2) {
|
|
65690
65933
|
const workflowPaths = await listWorkflowPaths(owner, repo, notes);
|
|
65691
|
-
const allPaths = [
|
|
65934
|
+
const allPaths = [
|
|
65935
|
+
.../* @__PURE__ */ new Set([...SURFACE_FILES, ...workflowPaths, "package.json", "pyproject.toml"])
|
|
65936
|
+
];
|
|
65937
|
+
if (!notes.includes(RATE_LIMIT_NOTE) && !notes.includes(NETWORK_NOTE))
|
|
65938
|
+
notes.push(
|
|
65939
|
+
`the repository tree could not be listed; only the root files and the workflows were read: nested agent files ${INCOMPLETE}.`
|
|
65940
|
+
);
|
|
65692
65941
|
const fetched2 = await pooled(
|
|
65693
65942
|
allPaths,
|
|
65694
65943
|
FETCH_CONCURRENCY,
|
|
@@ -65696,6 +65945,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65696
65945
|
);
|
|
65697
65946
|
return {
|
|
65698
65947
|
source: `${owner}/${repo}`,
|
|
65948
|
+
owner,
|
|
65699
65949
|
files: fetched2.filter((f) => !!f),
|
|
65700
65950
|
notes
|
|
65701
65951
|
};
|
|
@@ -65751,6 +66001,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65751
66001
|
notes.push(...session.closingNotes());
|
|
65752
66002
|
return {
|
|
65753
66003
|
source: `${owner}/${repo}`,
|
|
66004
|
+
owner,
|
|
65754
66005
|
files: fetched.filter((f) => !!f),
|
|
65755
66006
|
notes,
|
|
65756
66007
|
paths: plan.paths,
|
|
@@ -65761,7 +66012,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65761
66012
|
notes.push(
|
|
65762
66013
|
`fetch degraded (${err2?.message ?? "network error"}): results ${INCOMPLETE} (the repo could not be fetched).`
|
|
65763
66014
|
);
|
|
65764
|
-
return { source: `${owner}/${repo}`, files: [], notes };
|
|
66015
|
+
return { source: `${owner}/${repo}`, owner, files: [], notes };
|
|
65765
66016
|
}
|
|
65766
66017
|
}
|
|
65767
66018
|
async function fetchTree(input, onProgress) {
|
|
@@ -65924,7 +66175,15 @@ function explainGrant(f) {
|
|
|
65924
66175
|
const git = f.broad.some((a) => /^Bash\(git:/.test(a));
|
|
65925
66176
|
const files = f.broad.some((a) => /^Write|^Edit$/.test(a));
|
|
65926
66177
|
const template = TEMPLATE_PATH_RE.test(f.file);
|
|
65927
|
-
const
|
|
66178
|
+
const runner = f.broad.some((a) => !/^Bash\(git:|^Write|^Edit$/.test(a));
|
|
66179
|
+
const what = f.bareShell ? "run commands and change files" : sentence(
|
|
66180
|
+
[
|
|
66181
|
+
runner ? "run programs" : "",
|
|
66182
|
+
git ? "run git commands" : "",
|
|
66183
|
+
files ? "change files" : ""
|
|
66184
|
+
].filter(Boolean),
|
|
66185
|
+
true
|
|
66186
|
+
);
|
|
65928
66187
|
const saw = [];
|
|
65929
66188
|
if (template) saw.push("This is a template: every project made from it gets these permissions.");
|
|
65930
66189
|
saw.push(
|
|
@@ -65964,14 +66223,17 @@ function npxPackage(argv) {
|
|
|
65964
66223
|
return null;
|
|
65965
66224
|
}
|
|
65966
66225
|
function explainMcp(f) {
|
|
65967
|
-
const pkg = npxPackage(f.argv);
|
|
65968
|
-
const
|
|
66226
|
+
const pkg = f.pkg ?? npxPackage(f.argv);
|
|
66227
|
+
const registry = f.registry ?? "npm";
|
|
66228
|
+
const image = registry === "the image registry";
|
|
66229
|
+
const what = pkg ? `the newest ${code(pkg)}` : image ? "the newest image" : "the newest version of the package";
|
|
66230
|
+
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`;
|
|
65969
66231
|
return {
|
|
65970
66232
|
headline: `MCP server ${code(f.name, 60)} installs whatever version is newest, every time`,
|
|
65971
|
-
happens: `Each run downloads
|
|
66233
|
+
happens: `Each run downloads ${what} from ${registry}. If it is ever taken over, the next version runs on every developer's computer.`,
|
|
65972
66234
|
saw: [`It starts with ${code(f.argv, 120)}.`],
|
|
65973
66235
|
fix: [
|
|
65974
|
-
|
|
66236
|
+
example ? `Pin an exact version, for example ${code(example)}.` : "Pin an exact version."
|
|
65975
66237
|
]
|
|
65976
66238
|
};
|
|
65977
66239
|
}
|
|
@@ -65995,6 +66257,65 @@ function parseYamlStrict(text) {
|
|
|
65995
66257
|
return doc.toJS();
|
|
65996
66258
|
}
|
|
65997
66259
|
|
|
66260
|
+
// src/ci-check/frontmatter.ts
|
|
66261
|
+
function parseFrontmatter(content) {
|
|
66262
|
+
const text = content.replace(/^/, "").replace(/^\s*\n/, "");
|
|
66263
|
+
if (!/^---[ \t]*\r?\n/.test(text)) return null;
|
|
66264
|
+
const close = /\r?\n---[ \t]*(\r?\n|$)/.exec(text.slice(3));
|
|
66265
|
+
if (!close) return null;
|
|
66266
|
+
const block = text.slice(3, 3 + close.index);
|
|
66267
|
+
try {
|
|
66268
|
+
const parsed = parseYamlStrict(block);
|
|
66269
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
66270
|
+
} catch {
|
|
66271
|
+
return salvageTopLevel(block);
|
|
66272
|
+
}
|
|
66273
|
+
}
|
|
66274
|
+
function salvageTopLevel(block) {
|
|
66275
|
+
const out = {};
|
|
66276
|
+
const lines = block.split(/\r?\n/);
|
|
66277
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66278
|
+
const m = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i]);
|
|
66279
|
+
if (!m) continue;
|
|
66280
|
+
const [, key, rest] = m;
|
|
66281
|
+
if (rest.trim()) {
|
|
66282
|
+
out[key] = rest.trim();
|
|
66283
|
+
continue;
|
|
66284
|
+
}
|
|
66285
|
+
const items = [];
|
|
66286
|
+
while (i + 1 < lines.length && /^[ \t]+-[ \t]+/.test(lines[i + 1])) {
|
|
66287
|
+
items.push(lines[++i].replace(/^[ \t]+-[ \t]+/, "").trim());
|
|
66288
|
+
}
|
|
66289
|
+
if (items.length) out[key] = items;
|
|
66290
|
+
}
|
|
66291
|
+
return Object.keys(out).length ? out : null;
|
|
66292
|
+
}
|
|
66293
|
+
function splitGrants(value) {
|
|
66294
|
+
const out = [];
|
|
66295
|
+
let depth = 0;
|
|
66296
|
+
let cur = "";
|
|
66297
|
+
for (const ch of value) {
|
|
66298
|
+
if (ch === "(") depth++;
|
|
66299
|
+
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
66300
|
+
if (depth === 0 && (ch === "," || /\s/.test(ch))) {
|
|
66301
|
+
if (cur) out.push(cur);
|
|
66302
|
+
cur = "";
|
|
66303
|
+
continue;
|
|
66304
|
+
}
|
|
66305
|
+
cur += ch;
|
|
66306
|
+
}
|
|
66307
|
+
if (cur) out.push(cur);
|
|
66308
|
+
return out;
|
|
66309
|
+
}
|
|
66310
|
+
function allowedToolsOf(fm) {
|
|
66311
|
+
const raw = fm?.["allowed-tools"];
|
|
66312
|
+
if (typeof raw === "string") return splitGrants(raw);
|
|
66313
|
+
if (Array.isArray(raw)) {
|
|
66314
|
+
return raw.filter((x) => typeof x === "string").map((x) => x.trim()).filter(Boolean);
|
|
66315
|
+
}
|
|
66316
|
+
return [];
|
|
66317
|
+
}
|
|
66318
|
+
|
|
65998
66319
|
// src/ci-check/types.ts
|
|
65999
66320
|
var SEVERITY_RANK2 = {
|
|
66000
66321
|
critical: 4,
|
|
@@ -66003,11 +66324,28 @@ var SEVERITY_RANK2 = {
|
|
|
66003
66324
|
advisory: 1
|
|
66004
66325
|
};
|
|
66005
66326
|
|
|
66327
|
+
// src/ci-check/listing.ts
|
|
66328
|
+
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;
|
|
66329
|
+
function sanitizedListing(t) {
|
|
66330
|
+
if (t.length > 5e4) return false;
|
|
66331
|
+
if (/\|\s*grep\s+-[a-zA-Z]*o/.test(t)) return true;
|
|
66332
|
+
const fieldsSafe = (expr, fieldRe) => {
|
|
66333
|
+
const fields = [...expr.matchAll(fieldRe)].map((m) => m[1]);
|
|
66334
|
+
return fields.length > 0 && fields.every((f) => SAFE_LIST_FIELD_RE.test(f));
|
|
66335
|
+
};
|
|
66336
|
+
const jq = [
|
|
66337
|
+
...t.matchAll(/(?:--jq|\s-q)[\s=]+(?:'([^']{0,500})'|"([^"]{0,500})"|([^\s'"]{1,200}))/g)
|
|
66338
|
+
].map((m) => m[1] ?? m[2] ?? m[3]);
|
|
66339
|
+
if (jq.length) return jq.every((e) => fieldsSafe(e, /\.([A-Za-z_]\w{0,60})/g));
|
|
66340
|
+
const json = [...t.matchAll(/--json[\s=]+['"]?([\w,]{1,500})/g)].map((m) => m[1]);
|
|
66341
|
+
return json.length > 0 && json.every((e) => fieldsSafe(e, /([A-Za-z_]\w{0,60})/g));
|
|
66342
|
+
}
|
|
66343
|
+
|
|
66006
66344
|
// src/ci-check/workflows.ts
|
|
66007
66345
|
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;
|
|
66008
66346
|
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;
|
|
66009
66347
|
var EXFIL_RCE_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|sh[\s):]|eval|rm[\s):]|git push)/i;
|
|
66010
|
-
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;
|
|
66348
|
+
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;
|
|
66011
66349
|
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;
|
|
66012
66350
|
var MEMBERSHIP_CHECK_RE = /orgs\/[^/'"\s]+\/memberships\/|teams\/[^/'"\s]+\/memberships\/|getCollaboratorPermissionLevel|collaborators\/[^/'"\s]+\/permission|checkMembershipForUser|getMembershipForUser/i;
|
|
66013
66351
|
var STRANGER_ISSUE_TYPES = /* @__PURE__ */ new Set(["opened", "edited", "reopened", "closed"]);
|
|
@@ -66063,12 +66401,69 @@ function keyStrangerFirable(on, key) {
|
|
|
66063
66401
|
return false;
|
|
66064
66402
|
}
|
|
66065
66403
|
}
|
|
66404
|
+
var AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66405
|
+
function issueFormLabels(content) {
|
|
66406
|
+
const text = content.length > 2e5 ? content.slice(0, 2e5) : content;
|
|
66407
|
+
let value;
|
|
66408
|
+
let parsed = false;
|
|
66409
|
+
if (/^\uFEFF?\s*---[ \t]*\r?\n/.test(text)) {
|
|
66410
|
+
const fm = parseFrontmatter(text);
|
|
66411
|
+
parsed = fm !== null;
|
|
66412
|
+
value = fm?.["labels"];
|
|
66413
|
+
} else {
|
|
66414
|
+
try {
|
|
66415
|
+
const doc = parseYamlStrict(text);
|
|
66416
|
+
parsed = true;
|
|
66417
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc))
|
|
66418
|
+
value = doc["labels"];
|
|
66419
|
+
} catch {
|
|
66420
|
+
}
|
|
66421
|
+
}
|
|
66422
|
+
const raw = parsed ? Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [] : labelLines(text);
|
|
66423
|
+
return raw.filter((l) => typeof l === "string").map(
|
|
66424
|
+
(l) => l.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase()
|
|
66425
|
+
).filter((l) => l && l.length <= 100);
|
|
66426
|
+
}
|
|
66427
|
+
function labelLines(text) {
|
|
66428
|
+
const out = [];
|
|
66429
|
+
const lines = text.split(/\r?\n/);
|
|
66430
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66431
|
+
const m = /^labels:[ \t]*([^#]*)/.exec(lines[i]);
|
|
66432
|
+
if (!m) continue;
|
|
66433
|
+
const inline = m[1].trim();
|
|
66434
|
+
if (inline) {
|
|
66435
|
+
out.push(...inline.replace(/^\[|\]$/g, "").split(","));
|
|
66436
|
+
} else {
|
|
66437
|
+
for (let j = i + 1; j < lines.length && /^\s*-\s/.test(lines[j]); j++)
|
|
66438
|
+
out.push(lines[j].replace(/^\s*-\s/, "").replace(/\s#.*$/, ""));
|
|
66439
|
+
}
|
|
66440
|
+
}
|
|
66441
|
+
return out;
|
|
66442
|
+
}
|
|
66443
|
+
var CALLERS = /* @__PURE__ */ new WeakMap();
|
|
66066
66444
|
function triggerReach(wf, raw) {
|
|
66067
66445
|
const on = onObject(wf, raw) ?? {};
|
|
66068
66446
|
const keys = triggerKeys(wf, raw);
|
|
66069
|
-
const firable = keys.filter(
|
|
66447
|
+
const firable = keys.filter(
|
|
66448
|
+
(k) => keyStrangerFirable(on, k) || /^issues$/i.test(k) && testsAutoLabel(allIfText(wf))
|
|
66449
|
+
);
|
|
66070
66450
|
const untrusted = firable.length > 0;
|
|
66071
66451
|
const reusable = !untrusted && keys.some((k) => /^workflow_call$/i.test(k));
|
|
66452
|
+
const callers = CALLERS.get(wf);
|
|
66453
|
+
const open = (callers ?? []).filter((c) => c.untrusted && !c.gated && !c.dispatched);
|
|
66454
|
+
const unresolved = (callers ?? []).some(
|
|
66455
|
+
(c) => c.untrusted && !c.gated && c.dispatched || c.reusable && !c.gated
|
|
66456
|
+
);
|
|
66457
|
+
if (reusable && callers?.length && (open.length || !unresolved)) {
|
|
66458
|
+
return {
|
|
66459
|
+
keys: [...new Set(open.flatMap((c) => c.keys))],
|
|
66460
|
+
untrusted: open.length > 0,
|
|
66461
|
+
reusable: false,
|
|
66462
|
+
secretExposed: open.some((c) => c.secretExposed),
|
|
66463
|
+
privileged: open.some((c) => c.privileged),
|
|
66464
|
+
workflowRun: open.some((c) => c.workflowRun)
|
|
66465
|
+
};
|
|
66466
|
+
}
|
|
66072
66467
|
const secretExposed = firable.some((t) => /pull_request_target|workflow_run/i.test(t));
|
|
66073
66468
|
const privileged = untrusted && firable.some(
|
|
66074
66469
|
(t) => /pull_request_target|pull_request_review|workflow_run|issue|discussion/i.test(t)
|
|
@@ -66079,6 +66474,160 @@ function triggerReach(wf, raw) {
|
|
|
66079
66474
|
function jobList(wf) {
|
|
66080
66475
|
return Object.values(wf.jobs ?? {}).filter((j) => j != null);
|
|
66081
66476
|
}
|
|
66477
|
+
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;
|
|
66478
|
+
var ENV_REF_HOPS = 3;
|
|
66479
|
+
var RESOLVED_MAX_LEN = 1e5;
|
|
66480
|
+
function resolveEnvRefs(v, envs) {
|
|
66481
|
+
const holderOf = (name) => envs.find((e) => e != null && typeof e === "object" && name in e);
|
|
66482
|
+
for (let hop = 0; hop < ENV_REF_HOPS && v.includes("env"); hop++) {
|
|
66483
|
+
let projected = v.length;
|
|
66484
|
+
let any = false;
|
|
66485
|
+
for (const m of v.matchAll(ENV_REF_RE)) {
|
|
66486
|
+
const name = m[1] ?? m[2];
|
|
66487
|
+
const holder = holderOf(name);
|
|
66488
|
+
if (holder) {
|
|
66489
|
+
any = true;
|
|
66490
|
+
projected += str(holder[name]).length - m[0].length;
|
|
66491
|
+
}
|
|
66492
|
+
if (projected > RESOLVED_MAX_LEN) return v;
|
|
66493
|
+
}
|
|
66494
|
+
if (!any) break;
|
|
66495
|
+
v = v.replace(ENV_REF_RE, (whole, a, b) => {
|
|
66496
|
+
const name = a ?? b;
|
|
66497
|
+
const holder = holderOf(name);
|
|
66498
|
+
return holder ? str(holder[name]) : whole;
|
|
66499
|
+
});
|
|
66500
|
+
}
|
|
66501
|
+
return v;
|
|
66502
|
+
}
|
|
66503
|
+
function ifNeverTrue(v) {
|
|
66504
|
+
if (v === false) return true;
|
|
66505
|
+
if (typeof v !== "string") return false;
|
|
66506
|
+
const never = (x2) => {
|
|
66507
|
+
switch (x2.k) {
|
|
66508
|
+
case "atom":
|
|
66509
|
+
return /^false$/i.test(x2.t.trim());
|
|
66510
|
+
case "not":
|
|
66511
|
+
return false;
|
|
66512
|
+
case "and":
|
|
66513
|
+
return x2.c.some(never);
|
|
66514
|
+
case "or":
|
|
66515
|
+
return x2.c.every(never);
|
|
66516
|
+
}
|
|
66517
|
+
};
|
|
66518
|
+
const x = parseIf(v);
|
|
66519
|
+
return x !== null && never(x);
|
|
66520
|
+
}
|
|
66521
|
+
function normalizeWorkflow(raw) {
|
|
66522
|
+
const wf = raw;
|
|
66523
|
+
const wfEnv = raw.env;
|
|
66524
|
+
const jobs = wf.jobs;
|
|
66525
|
+
if (!jobs || typeof jobs !== "object") return wf;
|
|
66526
|
+
const wfEnvEvent = EVENT_CONTEXT_RE.test(str(wfEnv));
|
|
66527
|
+
for (const [name, job] of Object.entries(jobs)) {
|
|
66528
|
+
if (job == null || typeof job !== "object") continue;
|
|
66529
|
+
if (ifNeverTrue(job.if)) {
|
|
66530
|
+
delete jobs[name];
|
|
66531
|
+
continue;
|
|
66532
|
+
}
|
|
66533
|
+
if (!Array.isArray(job.steps)) continue;
|
|
66534
|
+
job.steps = job.steps.map(
|
|
66535
|
+
(st) => st != null && typeof st === "object" && !Array.isArray(st) ? { ...st } : st
|
|
66536
|
+
);
|
|
66537
|
+
const scopeEnv = {
|
|
66538
|
+
...wfEnv && typeof wfEnv === "object" ? wfEnv : {},
|
|
66539
|
+
...job.env && typeof job.env === "object" ? job.env : {}
|
|
66540
|
+
};
|
|
66541
|
+
const scopeEnvEvent = wfEnvEvent || EVENT_CONTEXT_RE.test(str(job.env));
|
|
66542
|
+
for (const step of job.steps) {
|
|
66543
|
+
if (step == null || typeof step !== "object") continue;
|
|
66544
|
+
step.scopeEnv = scopeEnv;
|
|
66545
|
+
step.scopeEnvEvent = scopeEnvEvent;
|
|
66546
|
+
if (step.with == null || typeof step.with !== "object") continue;
|
|
66547
|
+
const out = {};
|
|
66548
|
+
for (const [k, v] of Object.entries(step.with)) {
|
|
66549
|
+
const key = k.toLowerCase();
|
|
66550
|
+
if (key in out) continue;
|
|
66551
|
+
out[key] = typeof v === "string" ? resolveEnvRefs(v, [step.env, job.env, wfEnv]) : v;
|
|
66552
|
+
}
|
|
66553
|
+
step.with = out;
|
|
66554
|
+
}
|
|
66555
|
+
}
|
|
66556
|
+
return wf;
|
|
66557
|
+
}
|
|
66558
|
+
var LOADED = null;
|
|
66559
|
+
function loadWorkflow(content) {
|
|
66560
|
+
if (LOADED !== null && LOADED.content === content) return LOADED;
|
|
66561
|
+
const raw = parseYamlStrict(content) ?? {};
|
|
66562
|
+
const wf = raw && typeof raw === "object" ? normalizeWorkflow(raw) : raw;
|
|
66563
|
+
LOADED = { content, raw, wf };
|
|
66564
|
+
return LOADED;
|
|
66565
|
+
}
|
|
66566
|
+
var LOCAL_WORKFLOW_USES_RE = /^\s*\.\/\.github\/workflows\/([\w.-]{1,200}\.ya?ml)\s*$/i;
|
|
66567
|
+
function localCallsOf(path91, content, autoLabels) {
|
|
66568
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
66569
|
+
try {
|
|
66570
|
+
return localCallsOfInner(path91, content);
|
|
66571
|
+
} finally {
|
|
66572
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66573
|
+
}
|
|
66574
|
+
}
|
|
66575
|
+
function localCallsOfInner(path91, content) {
|
|
66576
|
+
let raw;
|
|
66577
|
+
try {
|
|
66578
|
+
raw = loadWorkflow(content).raw;
|
|
66579
|
+
} catch {
|
|
66580
|
+
return [];
|
|
66581
|
+
}
|
|
66582
|
+
if (!raw || typeof raw !== "object") return [];
|
|
66583
|
+
const wf = raw;
|
|
66584
|
+
CALLERS.delete(wf);
|
|
66585
|
+
const reach = triggerReach(wf, raw);
|
|
66586
|
+
const out = [];
|
|
66587
|
+
for (const job of jobList(wf)) {
|
|
66588
|
+
const m = LOCAL_WORKFLOW_USES_RE.exec(str(job.uses));
|
|
66589
|
+
if (!m) continue;
|
|
66590
|
+
out.push({
|
|
66591
|
+
target: `.github/workflows/${m[1]}`,
|
|
66592
|
+
caller: {
|
|
66593
|
+
file: path91,
|
|
66594
|
+
keys: reach.keys,
|
|
66595
|
+
untrusted: reach.untrusted,
|
|
66596
|
+
secretExposed: reach.secretExposed,
|
|
66597
|
+
privileged: reach.privileged,
|
|
66598
|
+
workflowRun: reach.workflowRun,
|
|
66599
|
+
gated: jobGated(job, wf, raw),
|
|
66600
|
+
dispatched: dispatchedByGatedJob(job, wf, raw),
|
|
66601
|
+
reusable: reach.reusable
|
|
66602
|
+
}
|
|
66603
|
+
});
|
|
66604
|
+
}
|
|
66605
|
+
return out;
|
|
66606
|
+
}
|
|
66607
|
+
function dispatchedByGatedJob(job, wf, raw) {
|
|
66608
|
+
const names = [...str(job.if).matchAll(/needs\.([\w-]{1,100})\.outputs\./g)].map((m) => m[1]);
|
|
66609
|
+
if (!names.length) return false;
|
|
66610
|
+
const jobs = wf.jobs ?? {};
|
|
66611
|
+
const label3 = labelTypeConfigured(wf, raw);
|
|
66612
|
+
const someBranchGated = (x) => {
|
|
66613
|
+
switch (x.k) {
|
|
66614
|
+
case "atom":
|
|
66615
|
+
return exprGated(x, label3);
|
|
66616
|
+
case "not":
|
|
66617
|
+
return false;
|
|
66618
|
+
case "and":
|
|
66619
|
+
case "or":
|
|
66620
|
+
return x.c.some(someBranchGated);
|
|
66621
|
+
}
|
|
66622
|
+
};
|
|
66623
|
+
return names.every((n) => {
|
|
66624
|
+
const up = jobs[n];
|
|
66625
|
+
if (up == null) return false;
|
|
66626
|
+
if (jobGated(up, wf, raw)) return true;
|
|
66627
|
+
const x = parseIf(str(up.if));
|
|
66628
|
+
return x !== null && someBranchGated(x);
|
|
66629
|
+
});
|
|
66630
|
+
}
|
|
66082
66631
|
function allSteps(wf) {
|
|
66083
66632
|
const out = [];
|
|
66084
66633
|
for (const job of jobList(wf)) {
|
|
@@ -66086,11 +66635,47 @@ function allSteps(wf) {
|
|
|
66086
66635
|
}
|
|
66087
66636
|
return out;
|
|
66088
66637
|
}
|
|
66638
|
+
var AGENT_CLI_RE = new RegExp(
|
|
66639
|
+
[
|
|
66640
|
+
// The binary at a command start, then its "do work" flag anywhere on the same command
|
|
66641
|
+
// (`claude --model x -p`, `claude "$Q" --print`, `npx @anthropic-ai/claude-code -p`): the
|
|
66642
|
+
// flag had to come first (review, round 2). Bounded: at most 300 characters per command.
|
|
66643
|
+
String.raw`(?:^|[\s|;&(])(?:claude|@anthropic-ai/claude-code(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\s(?:-p|--print|--continue|--resume)(?=[\s"'=]|$)`,
|
|
66644
|
+
String.raw`(?:^|[\s|;&(])(?:codex|@openai/codex(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\sexec\b`,
|
|
66645
|
+
String.raw`(?:^|[\s|;&(])(?:gemini|@google/gemini-cli(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\s(?:-p|--prompt)(?=[\s"'=]|$)`,
|
|
66646
|
+
String.raw`(?:^|[\s|;&(])copilot(?=\s)[^\n;&|]{0,300}?\s(?:-p|--prompt|--allow-all-tools)(?=[\s"'=]|$)`,
|
|
66647
|
+
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)`,
|
|
66648
|
+
String.raw`api\.devin\.ai/|software-agent-sdk|\bagent_script\.py\b`
|
|
66649
|
+
].join("|"),
|
|
66650
|
+
"i"
|
|
66651
|
+
);
|
|
66652
|
+
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;
|
|
66653
|
+
var EVENT_CONTEXT_RE = /github\.event|context\.payload/;
|
|
66654
|
+
function isRunAgent(step) {
|
|
66655
|
+
return typeof step.run === "string" && !step.uses && AGENT_CLI_RE.test(step.run);
|
|
66656
|
+
}
|
|
66657
|
+
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;
|
|
66658
|
+
var AGENT_PATH_RE = /(?:^|[/_.-])(claude|codex|gemini|opencode|goose|aider|kilo(?:code)?)(?:$|[/_.-])/i;
|
|
66659
|
+
var PROMPT_INPUT_RE = /^(prompt|direct_prompt|prompt_file|prompt-file|agent)$/i;
|
|
66660
|
+
function isWrapperAgent(step) {
|
|
66661
|
+
const uses = str(step.uses);
|
|
66662
|
+
const w = step.with ?? {};
|
|
66663
|
+
if (Object.keys(w).some((k) => FUEL_WITH_KEY_RE.test(k))) return true;
|
|
66664
|
+
const env = { ...step.scopeEnv ?? {}, ...step.env ?? {} };
|
|
66665
|
+
const fuelEnv = Object.keys(env).some((k) => RUN_FUEL_ENV_RE.test(k));
|
|
66666
|
+
if (/^\s*\.\//.test(uses)) return fuelEnv;
|
|
66667
|
+
const pathPart = uses.replace(/^\s*[^/@\s]{1,100}\//, "").replace(/@.*$/, "");
|
|
66668
|
+
return AGENT_PATH_RE.test(pathPart) && (Object.keys(w).some((k) => PROMPT_INPUT_RE.test(k)) || fuelEnv);
|
|
66669
|
+
}
|
|
66089
66670
|
function isAgentStep(step) {
|
|
66090
66671
|
if (step.uses && AGENT_ACTION_RE.test(step.uses)) return true;
|
|
66091
|
-
if (step.
|
|
66672
|
+
if (step.uses && isWrapperAgent(step)) return true;
|
|
66673
|
+
if (typeof step.run !== "string" || !step.run) return false;
|
|
66674
|
+
if (/\b(claude|aider|codex|gemini)\b/i.test(step.run) && /github\.event/.test(step.run))
|
|
66092
66675
|
return true;
|
|
66093
|
-
return false;
|
|
66676
|
+
if (!AGENT_CLI_RE.test(step.run)) return false;
|
|
66677
|
+
const env = { ...step.scopeEnv ?? {}, ...step.env ?? {} };
|
|
66678
|
+
return !!step.scopeEnvEvent || EVENT_CONTEXT_RE.test(str(step.env)) || Object.keys(env).some((k) => RUN_FUEL_ENV_RE.test(k));
|
|
66094
66679
|
}
|
|
66095
66680
|
function allowedToolsFromArgs(claudeArgs) {
|
|
66096
66681
|
let out = "";
|
|
@@ -66110,12 +66695,47 @@ function collectTools(steps) {
|
|
|
66110
66695
|
let s = "";
|
|
66111
66696
|
for (const st of steps) {
|
|
66112
66697
|
const w = st.with ?? {};
|
|
66113
|
-
|
|
66114
|
-
s += " " +
|
|
66698
|
+
const args = str(w["claude_args"]) + " " + str(w["codex-args"]) + " " + str(w["codex_args"]);
|
|
66699
|
+
s += " " + allowedToolsFromArgs(args);
|
|
66700
|
+
s += " " + str(w["allowed_tools"]) + " " + str(w["allowedtools"]);
|
|
66115
66701
|
s += " " + allowedToolsFromSettings(str(w["settings"]));
|
|
66702
|
+
if (PERMISSION_BYPASS_RE.test(args) || /^danger-full-access$/i.test(str(w["sandbox"])))
|
|
66703
|
+
s += " Bash";
|
|
66704
|
+
if (st.run && isRunAgent(st)) {
|
|
66705
|
+
s += " " + allowedToolsFromArgs(st.run);
|
|
66706
|
+
if (PERMISSION_BYPASS_RE.test(st.run)) s += " Bash";
|
|
66707
|
+
s += " " + permissionEnvGrants({ ...st.scopeEnv ?? {}, ...st.env ?? {} });
|
|
66708
|
+
}
|
|
66116
66709
|
}
|
|
66117
66710
|
return s;
|
|
66118
66711
|
}
|
|
66712
|
+
var PERMISSION_BYPASS_RE = /--dangerously-skip-permissions|--permission-mode[=\s]+["']?bypassPermissions|--dangerously-bypass-approvals-and-sandbox|--allow-all-tools|--yolo\b/i;
|
|
66713
|
+
function permissionEnvGrants(env) {
|
|
66714
|
+
let out = "";
|
|
66715
|
+
for (const key of ["OPENCODE_PERMISSION", "KILO_PERMISSION"]) {
|
|
66716
|
+
const raw = str(env[key]);
|
|
66717
|
+
if (!raw || raw.length > 2e4) continue;
|
|
66718
|
+
let cfg;
|
|
66719
|
+
try {
|
|
66720
|
+
cfg = JSON.parse(raw);
|
|
66721
|
+
} catch {
|
|
66722
|
+
continue;
|
|
66723
|
+
}
|
|
66724
|
+
if (!cfg || typeof cfg !== "object") continue;
|
|
66725
|
+
const c = cfg;
|
|
66726
|
+
const bash = c["bash"];
|
|
66727
|
+
if (bash === "allow") out += " Bash";
|
|
66728
|
+
else if (bash && typeof bash === "object") {
|
|
66729
|
+
for (const [pat, v] of Object.entries(bash)) {
|
|
66730
|
+
if (v !== "allow") continue;
|
|
66731
|
+
const cmd = pat.replace(/\*+\s*$/, "").trim();
|
|
66732
|
+
out += cmd ? ` Bash(${cmd}:*)` : " Bash";
|
|
66733
|
+
}
|
|
66734
|
+
}
|
|
66735
|
+
if (c["webfetch"] === "allow") out += " WebFetch";
|
|
66736
|
+
}
|
|
66737
|
+
return out;
|
|
66738
|
+
}
|
|
66119
66739
|
function toolTokens(blob) {
|
|
66120
66740
|
const out = [];
|
|
66121
66741
|
let buf = "";
|
|
@@ -66148,11 +66768,21 @@ function matchedBroadTools(blob) {
|
|
|
66148
66768
|
}
|
|
66149
66769
|
return out;
|
|
66150
66770
|
}
|
|
66771
|
+
var GH_PR_CHECKOUT_RE = /\bgh\s+pr\s+checkout\b/;
|
|
66772
|
+
var FETCH_PULL_RE = /\bgit\s+fetch\b[^\n]{0,200}\bpull\/[^\s/]{1,80}\/(?:head|merge)\b/;
|
|
66773
|
+
var GIT_CHECKOUT_RE = /\bgit\s+(?:checkout|switch)\b/;
|
|
66774
|
+
function runChecksOutHead(run) {
|
|
66775
|
+
if (GH_PR_CHECKOUT_RE.test(run)) return true;
|
|
66776
|
+
const m = FETCH_PULL_RE.exec(run);
|
|
66777
|
+
return m !== null && GIT_CHECKOUT_RE.test(run.slice(m.index));
|
|
66778
|
+
}
|
|
66151
66779
|
function untrustedHeadCheckout(steps) {
|
|
66152
66780
|
for (const step of steps) {
|
|
66781
|
+
if (!step.uses && typeof step.run === "string" && runChecksOutHead(step.run)) return "root";
|
|
66153
66782
|
if (!step.uses || !/actions\/checkout/.test(step.uses)) continue;
|
|
66154
66783
|
const ref = str(step.with?.["ref"]);
|
|
66155
|
-
if (/
|
|
66784
|
+
if (/refs\/pull\//.test(ref)) return step.with?.["path"] ? "subdir" : "root";
|
|
66785
|
+
if (/pull_request\.head|head[._-]sha|head_ref|expected_head|workflow_run\.head|inputs(?:\.|\[\s*['"])[\w-]*head/i.test(
|
|
66156
66786
|
ref
|
|
66157
66787
|
)) {
|
|
66158
66788
|
return step.with?.["path"] ? "subdir" : "root";
|
|
@@ -66162,22 +66792,61 @@ function untrustedHeadCheckout(steps) {
|
|
|
66162
66792
|
}
|
|
66163
66793
|
function promptTakesUntrusted(steps) {
|
|
66164
66794
|
for (const st of steps) {
|
|
66165
|
-
const blob = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + collectTools([st])
|
|
66795
|
+
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.
|
|
66796
|
+
(isRunAgent(st) ? " " + st.run + " " + str(st.env) + " " + str(st.scopeEnv) : "");
|
|
66166
66797
|
if (/github\.event\.(issue|comment|pull_request|review)\.(body|title)/i.test(blob)) return true;
|
|
66167
|
-
if (/pull_request\.number
|
|
66798
|
+
if (/pull_request\.number|(?<![\w./-])\/(?:code-)?review(?![\w-])/i.test(blob)) return true;
|
|
66168
66799
|
}
|
|
66169
66800
|
return false;
|
|
66170
66801
|
}
|
|
66171
66802
|
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;
|
|
66172
66803
|
var ITEM_HANDLE_RE = /github\.event\.(issue|pull_request|discussion)\.number|github\.event\.number\b|github\.event\.comment\.id/i;
|
|
66173
66804
|
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;
|
|
66805
|
+
var RUN_ARTIFACT_RE = /gh\s+run\s+download\b|\/runs\/\$\{\{\s*github\.event\.workflow_run\.id\s*\}\}\/(?:artifacts|logs)/i;
|
|
66806
|
+
var TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
66807
|
+
var UPSTREAM_TRUSTED = false;
|
|
66808
|
+
function upstreamRunTrusted(wf, raw) {
|
|
66809
|
+
const on = onObject(wf, raw);
|
|
66810
|
+
const run = on?.["workflow_run"];
|
|
66811
|
+
const names = Array.isArray(run?.workflows) ? run.workflows.map(String) : [];
|
|
66812
|
+
return names.length > 0 && names.every((n) => TRUSTED_RUN_SOURCES.has(n));
|
|
66813
|
+
}
|
|
66814
|
+
function runSourceOf(path91, content, autoLabels) {
|
|
66815
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
66816
|
+
try {
|
|
66817
|
+
let raw;
|
|
66818
|
+
try {
|
|
66819
|
+
raw = loadWorkflow(content).raw;
|
|
66820
|
+
} catch {
|
|
66821
|
+
return null;
|
|
66822
|
+
}
|
|
66823
|
+
if (!raw || typeof raw !== "object") return null;
|
|
66824
|
+
const wf = raw;
|
|
66825
|
+
CALLERS.delete(wf);
|
|
66826
|
+
const reach = triggerReach(wf, raw);
|
|
66827
|
+
const name = typeof raw["name"] === "string" && raw["name"] ? raw["name"] : path91;
|
|
66828
|
+
return { name, trusted: !reach.untrusted && !reach.reusable };
|
|
66829
|
+
} finally {
|
|
66830
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66831
|
+
}
|
|
66832
|
+
}
|
|
66833
|
+
function downloadsRunArtifacts(st) {
|
|
66834
|
+
if (UPSTREAM_TRUSTED) return false;
|
|
66835
|
+
if (/actions\/download-artifact/i.test(str(st.uses)))
|
|
66836
|
+
return /workflow_run\.id/.test(str(st.with?.["run-id"]) + " " + str(st.with?.["run_id"]));
|
|
66837
|
+
return RUN_ARTIFACT_RE.test(str(st.run) + " " + str(st.with?.["script"]));
|
|
66838
|
+
}
|
|
66839
|
+
var STEP_OUTPUT_REF_RE = /steps\.([\w-]{1,100})\.outputs\./g;
|
|
66174
66840
|
function jobReadsUntrustedItem(job) {
|
|
66175
66841
|
const steps = job.steps ?? [];
|
|
66176
66842
|
const agent = steps.filter(isAgentStep);
|
|
66177
66843
|
if (!agent.length) return null;
|
|
66178
66844
|
if (ITEM_READ_TOOL_RE.test(collectTools(agent))) return "tool";
|
|
66179
66845
|
for (const st of agent)
|
|
66180
|
-
if (ITEM_HANDLE_RE.test(
|
|
66846
|
+
if (ITEM_HANDLE_RE.test(
|
|
66847
|
+
str(st.with) + " " + str(st.env) + " " + str(job.env) + " " + (isRunAgent(st) ? str(st.run) : "")
|
|
66848
|
+
))
|
|
66849
|
+
return "handle";
|
|
66181
66850
|
for (let i = 0; i < steps.length; i++) {
|
|
66182
66851
|
const st = steps[i];
|
|
66183
66852
|
if (!isAgentStep(st) || !str(st.with?.["prompt_file"] ?? st.with?.["prompt-file"])) continue;
|
|
@@ -66185,6 +66854,20 @@ function jobReadsUntrustedItem(job) {
|
|
|
66185
66854
|
if (earlier.some((p) => PAYLOAD_TEXT_RE.test(str(p.run) + " " + str(p.with) + " " + str(p.env))))
|
|
66186
66855
|
return "prompt-file";
|
|
66187
66856
|
}
|
|
66857
|
+
const firstDownload = steps.findIndex(downloadsRunArtifacts);
|
|
66858
|
+
if (firstDownload >= 0 && steps.slice(firstDownload + 1).some(isAgentStep)) return "artifact";
|
|
66859
|
+
for (let i = 0; i < steps.length; i++) {
|
|
66860
|
+
const st = steps[i];
|
|
66861
|
+
if (!isAgentStep(st)) continue;
|
|
66862
|
+
const input = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + str(st.with?.["prompt_file"] ?? st.with?.["prompt-file"]);
|
|
66863
|
+
const ids = /* @__PURE__ */ new Set();
|
|
66864
|
+
for (const m of input.matchAll(STEP_OUTPUT_REF_RE)) ids.add(m[1]);
|
|
66865
|
+
for (const id of ids) {
|
|
66866
|
+
const src = steps.slice(0, i).find((p) => p.id === id);
|
|
66867
|
+
if (src && PAYLOAD_TEXT_RE.test(str(src.run) + " " + str(src.with) + " " + str(src.env)))
|
|
66868
|
+
return "step-output";
|
|
66869
|
+
}
|
|
66870
|
+
}
|
|
66188
66871
|
return null;
|
|
66189
66872
|
}
|
|
66190
66873
|
var NONCONTAINS_GATE_RE = new RegExp(
|
|
@@ -66197,19 +66880,46 @@ var NONCONTAINS_GATE_RE = new RegExp(
|
|
|
66197
66880
|
].join("|"),
|
|
66198
66881
|
"i"
|
|
66199
66882
|
);
|
|
66200
|
-
var
|
|
66883
|
+
var PERMISSION_OUTPUT_RE = /steps\.[\w-]{1,100}\.outputs\.([\w-]{1,200})\s*==\s*['"]?(?:true|admin|write|maintain)/gi;
|
|
66884
|
+
var PERMISSION_OUTPUT_NAME_RE = /permission|allowed|authoriz|is[_-]?(?:admin|member|maintainer|collaborator)/i;
|
|
66885
|
+
var PERMISSION_OUTPUT_GATE_RE = {
|
|
66886
|
+
test(ifs) {
|
|
66887
|
+
for (const m of ifs.matchAll(PERMISSION_OUTPUT_RE))
|
|
66888
|
+
if (PERMISSION_OUTPUT_NAME_RE.test(m[1])) return true;
|
|
66889
|
+
return false;
|
|
66890
|
+
}
|
|
66891
|
+
};
|
|
66201
66892
|
var CONTAINS_GATE_RE = /contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
|
|
66202
66893
|
var NEGATED_CONTAINS_RE = /!\s*\(?\s*contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
|
|
66203
66894
|
function labelTypeConfigured(wf, raw) {
|
|
66204
66895
|
const on = wf.on ?? raw["on"] ?? raw[true];
|
|
66205
66896
|
const prTypes = (t) => t && Array.isArray(t.types) ? t.types.map(String) : [];
|
|
66206
|
-
|
|
66897
|
+
const pr = prTypes(on?.["pull_request_target"]).includes("labeled") || prTypes(on?.["pull_request"]).includes("labeled");
|
|
66898
|
+
const onObj = onObject(wf, raw) ?? {};
|
|
66899
|
+
const issues = on != null && typeof on === "object" && "issues" in on && triggerKeys(wf, raw).every((k) => /^issues$/i.test(k) || !keyStrangerFirable(onObj, k));
|
|
66900
|
+
return { pr, issues };
|
|
66901
|
+
}
|
|
66902
|
+
var EVENT_LABEL_GATE_RE = /\blabel\.name\s*==\s*'|\b(?:contains|startsWith)\(\s*github\.event\.label\.name\b/i;
|
|
66903
|
+
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;
|
|
66904
|
+
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;
|
|
66905
|
+
var NONWRITE_SET_RE = /contains\(\s*(?:fromjson\(\s*['"]\[[^\]]{0,400}|'[^']{0,400})\b(CONTRIBUTOR|FIRST_TIME_CONTRIBUTOR|FIRST_TIMER|NONE|MANNEQUIN)\b/i;
|
|
66906
|
+
function allIfText(wf) {
|
|
66907
|
+
const s = jobList(wf).map((j) => str(j.if) + " " + (j.steps ?? []).map((st) => str(st.if)).join(" ")).join(" ");
|
|
66908
|
+
return s.length > IF_MAX_LEN ? "" : s;
|
|
66909
|
+
}
|
|
66910
|
+
function testsAutoLabel(ifs) {
|
|
66911
|
+
if (AUTO_LABELS.size === 0) return false;
|
|
66912
|
+
for (const m of ifs.matchAll(
|
|
66913
|
+
/labels\.\*\.name\s*,\s*'([^']{1,100})'|label\.name\s*==\s*'([^']{1,100})'/gi
|
|
66914
|
+
))
|
|
66915
|
+
if (AUTO_LABELS.has((m[1] ?? m[2]).toLowerCase())) return true;
|
|
66916
|
+
return false;
|
|
66207
66917
|
}
|
|
66208
|
-
|
|
66209
|
-
|
|
66210
|
-
const containsGate = CONTAINS_GATE_RE.test(
|
|
66211
|
-
const gated = NONCONTAINS_GATE_RE.test(
|
|
66212
|
-
const labelGated =
|
|
66918
|
+
function ifsAreGated(ifs, label3) {
|
|
66919
|
+
const m = maskLiterals(ifs);
|
|
66920
|
+
const containsGate = CONTAINS_GATE_RE.test(m) && !NEGATED_CONTAINS_RE.test(m) && !NONWRITE_SET_RE.test(ifs);
|
|
66921
|
+
const gated = NONCONTAINS_GATE_RE.test(m) || containsGate;
|
|
66922
|
+
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);
|
|
66213
66923
|
return gated || labelGated;
|
|
66214
66924
|
}
|
|
66215
66925
|
function hasStepMembershipGate(job) {
|
|
@@ -66230,6 +66940,7 @@ function parseIf(src) {
|
|
|
66230
66940
|
let e = src.trim();
|
|
66231
66941
|
const wrapped = /^\$\{\{([\s\S]*)\}\}$/.exec(e);
|
|
66232
66942
|
if (wrapped) e = wrapped[1].trim();
|
|
66943
|
+
if (e.includes("${{")) return null;
|
|
66233
66944
|
if (!e || e.length > IF_MAX_LEN) return null;
|
|
66234
66945
|
let i = 0;
|
|
66235
66946
|
let depth = 0;
|
|
@@ -66315,12 +67026,21 @@ function stepIfText(steps) {
|
|
|
66315
67026
|
const s = steps.map((st) => str(st.if)).join(" ");
|
|
66316
67027
|
return s.length > IF_MAX_LEN ? "" : s;
|
|
66317
67028
|
}
|
|
67029
|
+
function maskLiterals(ifs) {
|
|
67030
|
+
return ifs.replace(
|
|
67031
|
+
/(==|!=)?(\s*)'(?:[^']|'')*'/g,
|
|
67032
|
+
(m, op, sp) => op ? m : `${sp}'_'`
|
|
67033
|
+
);
|
|
67034
|
+
}
|
|
67035
|
+
var CHAINED_COMPARE_RE = /(?:==|!=)[^=!]*(?:==|!=)/;
|
|
66318
67036
|
function gatedText(ifs, labelConfigured) {
|
|
66319
|
-
|
|
67037
|
+
const masked = maskLiterals(ifs);
|
|
67038
|
+
return ifsAreGated(ifs, labelConfigured) || ASSIGNEE_GATE_RE.test(masked) || PERMISSION_OUTPUT_GATE_RE.test(masked);
|
|
66320
67039
|
}
|
|
66321
67040
|
function exprGated(x, labelConfigured) {
|
|
66322
67041
|
switch (x.k) {
|
|
66323
67042
|
case "atom":
|
|
67043
|
+
if (CHAINED_COMPARE_RE.test(maskLiterals(x.t))) return false;
|
|
66324
67044
|
return gatedText(x.t, labelConfigured) || TRUSTED_EVENT_ATOM_RE.test(x.t);
|
|
66325
67045
|
case "not":
|
|
66326
67046
|
return false;
|
|
@@ -66356,11 +67076,43 @@ function jobOnWorkflowRun(job, workflowRun) {
|
|
|
66356
67076
|
}
|
|
66357
67077
|
function jobActorGate(job, wf, raw) {
|
|
66358
67078
|
const labelConfigured = labelTypeConfigured(wf, raw);
|
|
66359
|
-
const
|
|
66360
|
-
|
|
67079
|
+
const agent = (job.steps ?? []).filter(isAgentStep);
|
|
67080
|
+
const ifTotal = agent.reduce((n, s) => n + str(s.if).length, 0);
|
|
67081
|
+
const agentStepsGated = agent.length > 0 && ifTotal <= IF_MAX_LEN && agent.every((s) => {
|
|
67082
|
+
const x = parseIf(str(s.if));
|
|
67083
|
+
return x !== null && exprGated(x, labelConfigured);
|
|
67084
|
+
});
|
|
67085
|
+
return jobIfGated(str(job.if), labelConfigured) || agentStepsGated || hasStepMembershipGate(job);
|
|
67086
|
+
}
|
|
67087
|
+
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*['"]/;
|
|
67088
|
+
var ACTOR_ALLOWLIST_G = new RegExp(ACTOR_ALLOWLIST_RE.source, "g");
|
|
67089
|
+
var BLOCK_LIST_NAME_RE = /block|deny|ban|spam|abuse|exclu|ignore/i;
|
|
67090
|
+
function scriptAllowsByActor(code2) {
|
|
67091
|
+
if (code2.length > 2e5) return false;
|
|
67092
|
+
for (const m of code2.matchAll(ACTOR_ALLOWLIST_G)) {
|
|
67093
|
+
const at = m.index ?? 0;
|
|
67094
|
+
const before = code2.slice(Math.max(0, at - 80), at);
|
|
67095
|
+
if (m[0].startsWith(".includes")) {
|
|
67096
|
+
const list = /([\w$.]{1,60})$/.exec(before)?.[1] ?? "";
|
|
67097
|
+
if (BLOCK_LIST_NAME_RE.test(list)) continue;
|
|
67098
|
+
}
|
|
67099
|
+
const negated = /!\s{0,3}\(?\s{0,3}[\w$.]{0,60}$/.test(before);
|
|
67100
|
+
const ternary = /^[^?;\n]{0,150}\?\s{0,5}['"`]?(true|false)\b/.exec(code2.slice(at, at + 200));
|
|
67101
|
+
if (!ternary || ternary[1] === "true" !== negated) return true;
|
|
67102
|
+
}
|
|
67103
|
+
return false;
|
|
67104
|
+
}
|
|
67105
|
+
function readsOutputPositively(readerIf, name) {
|
|
67106
|
+
const ref = `needs\\.${escapeRe(name)}\\.outputs\\.[\\w-]{1,100}`;
|
|
67107
|
+
if (!new RegExp(ref).test(readerIf)) return false;
|
|
67108
|
+
const no = `'(?:false|no|0|)'(?![\\w'])`;
|
|
67109
|
+
return !new RegExp(
|
|
67110
|
+
`!\\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})`,
|
|
67111
|
+
"i"
|
|
67112
|
+
).test(readerIf);
|
|
66361
67113
|
}
|
|
66362
67114
|
var STATUS_FN_RE = /\b(success|failure|always|cancelled)\s*\(/i;
|
|
66363
|
-
var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]/;
|
|
67115
|
+
var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]|actions\.cancelWorkflowRun\b/;
|
|
66364
67116
|
function needsOf(job) {
|
|
66365
67117
|
return (Array.isArray(job.needs) ? job.needs : job.needs ? [job.needs] : []).map(String);
|
|
66366
67118
|
}
|
|
@@ -66372,13 +67124,18 @@ function upstreamJobGates(name, dep, wf, raw, readerIf) {
|
|
|
66372
67124
|
const label3 = labelTypeConfigured(wf, raw);
|
|
66373
67125
|
if (depIf && jobIfGated(depIf, label3)) return true;
|
|
66374
67126
|
const steps = dep.steps ?? [];
|
|
66375
|
-
|
|
67127
|
+
if (readerIf !== null && readsOutputPositively(readerIf, name) && steps.some((st) => {
|
|
67128
|
+
const code2 = str(st.run) + " " + str(st.with?.["script"]);
|
|
67129
|
+
return MEMBERSHIP_CHECK_RE.test(code2) || scriptAllowsByActor(code2);
|
|
67130
|
+
}))
|
|
67131
|
+
return true;
|
|
67132
|
+
const stepGated = ifsAreGated(stepIfText(steps), label3) || hasStepMembershipGate(dep) || steps.some((st) => MEMBERSHIP_CHECK_RE.test(str(st.with?.["script"])));
|
|
66376
67133
|
if (!stepGated) return false;
|
|
66377
67134
|
if (steps.some(
|
|
66378
67135
|
(st) => FAILING_STEP_RE.test(str(st.run)) || FAILING_STEP_RE.test(str(st.with?.["script"]))
|
|
66379
67136
|
))
|
|
66380
67137
|
return true;
|
|
66381
|
-
return readerIf !== null &&
|
|
67138
|
+
return readerIf !== null && readsOutputPositively(readerIf, name);
|
|
66382
67139
|
}
|
|
66383
67140
|
function gatingAncestors(job, wf, raw) {
|
|
66384
67141
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -66408,9 +67165,15 @@ function jobGated(job, wf, raw) {
|
|
|
66408
67165
|
}
|
|
66409
67166
|
function bypassArmed(steps) {
|
|
66410
67167
|
return steps.some(
|
|
66411
|
-
(s) => str(s.with?.["allowed_non_write_users"])
|
|
67168
|
+
(s) => nonWriteOpen(str(s.with?.["allowed_non_write_users"])) && (!!str(s.with?.["github_token"]) || !!str(s.env?.["OVERRIDE_GITHUB_TOKEN"]))
|
|
66412
67169
|
);
|
|
66413
67170
|
}
|
|
67171
|
+
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;
|
|
67172
|
+
function nonWriteOpen(v) {
|
|
67173
|
+
const t = v.trim();
|
|
67174
|
+
if (t === "*") return true;
|
|
67175
|
+
return t.length <= 1e3 && ACTOR_BOUND_RE.test(t);
|
|
67176
|
+
}
|
|
66414
67177
|
function hasImplicitActorGate(agentSteps, bypassActive, workflowRun = false) {
|
|
66415
67178
|
if (bypassActive) return false;
|
|
66416
67179
|
return agentSteps.some((s) => {
|
|
@@ -66529,14 +67292,28 @@ function severityFromScore(score) {
|
|
|
66529
67292
|
if (score >= 1) return "advisory";
|
|
66530
67293
|
return null;
|
|
66531
67294
|
}
|
|
66532
|
-
function analyzeWorkflow(path91, content) {
|
|
67295
|
+
function analyzeWorkflow(path91, content, callers, autoLabels, trustedRunSources) {
|
|
67296
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67297
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67298
|
+
try {
|
|
67299
|
+
return analyzeWorkflowInner(path91, content, callers);
|
|
67300
|
+
} finally {
|
|
67301
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67302
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
67303
|
+
UPSTREAM_TRUSTED = false;
|
|
67304
|
+
}
|
|
67305
|
+
}
|
|
67306
|
+
function analyzeWorkflowInner(path91, content, callers) {
|
|
66533
67307
|
let raw;
|
|
66534
67308
|
try {
|
|
66535
|
-
raw =
|
|
67309
|
+
raw = loadWorkflow(content).raw;
|
|
66536
67310
|
} catch {
|
|
66537
67311
|
return null;
|
|
66538
67312
|
}
|
|
66539
67313
|
const wf = raw;
|
|
67314
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67315
|
+
else CALLERS.delete(wf);
|
|
67316
|
+
UPSTREAM_TRUSTED = upstreamRunTrusted(wf, raw);
|
|
66540
67317
|
const steps = allSteps(wf).map((s) => s.step);
|
|
66541
67318
|
const agentSteps = steps.filter(isAgentStep);
|
|
66542
67319
|
const agentJobs = [
|
|
@@ -66555,17 +67332,19 @@ function analyzeWorkflow(path91, content) {
|
|
|
66555
67332
|
} = triggerReach(wf, raw);
|
|
66556
67333
|
const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
|
|
66557
67334
|
const nonWriteStar = bypassArmed(agentSteps);
|
|
66558
|
-
const starWithoutToken = nonWrite
|
|
66559
|
-
const nonWriteList = !!nonWrite && nonWrite
|
|
67335
|
+
const starWithoutToken = nonWriteOpen(nonWrite) && !nonWriteStar;
|
|
67336
|
+
const nonWriteList = !!nonWrite && !nonWriteOpen(nonWrite);
|
|
67337
|
+
const nonWriteActor = nonWrite.trim() !== "*" && nonWriteOpen(nonWrite);
|
|
66560
67338
|
const untrustedTrigger = forkInput || reusable;
|
|
66561
67339
|
const bypassActive = nonWriteStar && untrustedTrigger;
|
|
66562
|
-
const head = untrustedHeadCheckout(steps);
|
|
67340
|
+
const head = agentSteps.length && agentSteps.every(readOnlySandbox) ? null : untrustedHeadCheckout(steps);
|
|
66563
67341
|
const promptUntrusted = promptTakesUntrusted(agentSteps);
|
|
66564
67342
|
const injJobs = injectableJobs(wf, raw, untrustedTrigger);
|
|
66565
67343
|
const powerJobs = injJobs.length ? injJobs : agentJobs;
|
|
66566
67344
|
const powerSteps = injJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep));
|
|
66567
67345
|
const scopedSteps = powerSteps.length ? powerSteps : agentSteps;
|
|
66568
|
-
const
|
|
67346
|
+
const headActionRuns = head === "root" && powerSteps.some((st) => /^\s*\.\//.test(str(st.uses)));
|
|
67347
|
+
const toolsBlob = collectTools(scopedSteps) + (headActionRuns ? " Bash" : "");
|
|
66569
67348
|
const itemRead = forkInput ? injJobs.map(jobReadsUntrustedItem).find(Boolean) ?? null : null;
|
|
66570
67349
|
const reach = untrustedTrigger && injJobs.length ? Math.max(
|
|
66571
67350
|
head === "root" ? 3 : head === "subdir" ? 1 : 0,
|
|
@@ -66613,20 +67392,32 @@ function analyzeWorkflow(path91, content) {
|
|
|
66613
67392
|
severity = "medium";
|
|
66614
67393
|
if (!severity) severity = "advisory";
|
|
66615
67394
|
const signals = [];
|
|
67395
|
+
const openCallers = (callers ?? []).filter((c) => c.untrusted && !c.gated && !c.dispatched);
|
|
67396
|
+
if (openCallers.length)
|
|
67397
|
+
signals.push(
|
|
67398
|
+
`called by ${openCallers.map((c) => `${safeText(c.file, 120)} (${c.keys.map((k) => safeText(k, 40)).join(", ")})`).join("; ")}`
|
|
67399
|
+
);
|
|
66616
67400
|
if (secretExposed)
|
|
66617
67401
|
signals.push(
|
|
66618
|
-
`runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).join(", ")})`
|
|
67402
|
+
`runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).map((t) => safeText(t, 40)).join(", ")})`
|
|
67403
|
+
);
|
|
67404
|
+
else if (forkInput)
|
|
67405
|
+
signals.push(
|
|
67406
|
+
`triggered by untrusted input (${triggers.map((t) => safeText(t, 40)).join(", ")})`
|
|
66619
67407
|
);
|
|
66620
|
-
else if (forkInput) signals.push(`triggered by untrusted input (${triggers.join(", ")})`);
|
|
66621
67408
|
else if (reusable)
|
|
66622
67409
|
signals.push(
|
|
66623
|
-
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`
|
|
67410
|
+
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`
|
|
66624
67411
|
);
|
|
66625
67412
|
if (forkInput && !privileged && !reusable)
|
|
66626
67413
|
signals.push(
|
|
66627
67414
|
"triggered by `pull_request`: fork PRs run with a read-only token (lower risk than pull_request_target)"
|
|
66628
67415
|
);
|
|
66629
67416
|
if (head === "root") signals.push("checks out the untrusted PR head into the workspace root");
|
|
67417
|
+
if (headActionRuns)
|
|
67418
|
+
signals.push(
|
|
67419
|
+
"runs the checked-out head's own action (`uses: ./\u2026`): the PR author's code runs with the job's secrets"
|
|
67420
|
+
);
|
|
66630
67421
|
if (head === "subdir") signals.push("checks out the untrusted PR head into an isolated subdir");
|
|
66631
67422
|
if (workflowRunJobs.length) {
|
|
66632
67423
|
const builds = workflowRunJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep)).map((st) => claudeActionBuild(st.uses)).filter((b) => b !== null);
|
|
@@ -66648,7 +67439,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66648
67439
|
if (promptUntrusted) signals.push("feeds untrusted PR/issue text to the agent");
|
|
66649
67440
|
else if (itemRead)
|
|
66650
67441
|
signals.push(
|
|
66651
|
-
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)"
|
|
67442
|
+
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)"
|
|
66652
67443
|
);
|
|
66653
67444
|
if (broadTools) {
|
|
66654
67445
|
const names = matchedBroadTools(toolsBlob);
|
|
@@ -66658,7 +67449,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66658
67449
|
}
|
|
66659
67450
|
if (bypassActive)
|
|
66660
67451
|
signals.push(
|
|
66661
|
-
'allowed_non_write_users: "*" with github_token, so any user can trigger the agent'
|
|
67452
|
+
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'
|
|
66662
67453
|
);
|
|
66663
67454
|
if (elevated) signals.push("elevated permissions (contents/id-token: write)");
|
|
66664
67455
|
if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
|
|
@@ -66752,7 +67543,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66752
67543
|
return finding;
|
|
66753
67544
|
}
|
|
66754
67545
|
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;
|
|
66755
|
-
var FUEL_INPUT_KEYS = /^(
|
|
67546
|
+
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;
|
|
66756
67547
|
function fuelSecretNames(agentSteps) {
|
|
66757
67548
|
const out = /* @__PURE__ */ new Set();
|
|
66758
67549
|
const add = (v) => {
|
|
@@ -66825,14 +67616,28 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
|
|
|
66825
67616
|
if (reusable && !loadedGun && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
|
|
66826
67617
|
return { severity, secrets, injectable, canReadEnv };
|
|
66827
67618
|
}
|
|
66828
|
-
function analyzeWorkflowSecrets(path91, content) {
|
|
67619
|
+
function analyzeWorkflowSecrets(path91, content, callers, autoLabels, trustedRunSources) {
|
|
67620
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67621
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67622
|
+
try {
|
|
67623
|
+
return analyzeWorkflowSecretsInner(path91, content, callers);
|
|
67624
|
+
} finally {
|
|
67625
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67626
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
67627
|
+
UPSTREAM_TRUSTED = false;
|
|
67628
|
+
}
|
|
67629
|
+
}
|
|
67630
|
+
function analyzeWorkflowSecretsInner(path91, content, callers) {
|
|
66829
67631
|
let raw;
|
|
66830
67632
|
try {
|
|
66831
|
-
raw =
|
|
67633
|
+
raw = loadWorkflow(content).raw;
|
|
66832
67634
|
} catch {
|
|
66833
67635
|
return null;
|
|
66834
67636
|
}
|
|
66835
67637
|
const wf = raw;
|
|
67638
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67639
|
+
else CALLERS.delete(wf);
|
|
67640
|
+
UPSTREAM_TRUSTED = upstreamRunTrusted(wf, raw);
|
|
66836
67641
|
const all = allSteps(wf);
|
|
66837
67642
|
if (!all.some((s) => isAgentStep(s.step))) return null;
|
|
66838
67643
|
const { untrusted: forkInput, reusable } = triggerReach(wf, raw);
|
|
@@ -66864,70 +67669,281 @@ function analyzeWorkflowSecrets(path91, content) {
|
|
|
66864
67669
|
})
|
|
66865
67670
|
};
|
|
66866
67671
|
}
|
|
66867
|
-
|
|
66868
|
-
|
|
66869
|
-
|
|
66870
|
-
|
|
66871
|
-
|
|
66872
|
-
|
|
66873
|
-
|
|
66874
|
-
|
|
66875
|
-
|
|
66876
|
-
if (!close) return null;
|
|
66877
|
-
const block = text.slice(3, 3 + close.index);
|
|
67672
|
+
var COMMIT_PIN_RE = /[#@][0-9a-f]{40}\b/i;
|
|
67673
|
+
function marketplaceOwner(market) {
|
|
67674
|
+
if (/(^|[/\\])\.\.([/\\]|$)/.test(market)) return void 0;
|
|
67675
|
+
return /^(?:https?:\/\/(?:www\.)?|git@|ssh:\/\/git@)?github\.com[:/]([\w.-]{1,100})\/[\w.-]{1,100}(?:\.git)?\/?(?:[#@].*)?$/i.exec(
|
|
67676
|
+
market
|
|
67677
|
+
)?.[1] ?? /^([\w.-]{1,100})\/[\w.-]{1,100}(?:[#@].*)?$/.exec(market)?.[1];
|
|
67678
|
+
}
|
|
67679
|
+
function analyzeWorkflowPlugins(path91, content, owner) {
|
|
67680
|
+
let raw;
|
|
66878
67681
|
try {
|
|
66879
|
-
|
|
66880
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
67682
|
+
raw = loadWorkflow(content).raw;
|
|
66881
67683
|
} catch {
|
|
66882
|
-
return
|
|
67684
|
+
return [];
|
|
66883
67685
|
}
|
|
67686
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67687
|
+
const wf = raw;
|
|
67688
|
+
const out = [];
|
|
67689
|
+
const seen = /* @__PURE__ */ new Set();
|
|
67690
|
+
for (const { step } of allSteps(wf)) {
|
|
67691
|
+
if (!claudeActionBuild(step.uses)) continue;
|
|
67692
|
+
if (!str(step.with?.["plugins"]).trim()) continue;
|
|
67693
|
+
for (const m of str(step.with?.["plugin_marketplaces"]).split(/[\s,]+/)) {
|
|
67694
|
+
const market = m.trim();
|
|
67695
|
+
if (!market || COMMIT_PIN_RE.test(market)) continue;
|
|
67696
|
+
if (/^\.{1,2}(\/|$)/.test(market)) continue;
|
|
67697
|
+
const mo = marketplaceOwner(market);
|
|
67698
|
+
if (mo && mo.toLowerCase() === "anthropics") continue;
|
|
67699
|
+
if (owner && mo && mo.toLowerCase() === owner.toLowerCase()) continue;
|
|
67700
|
+
if (seen.has(market)) continue;
|
|
67701
|
+
seen.add(market);
|
|
67702
|
+
out.push({
|
|
67703
|
+
check: "CI-3",
|
|
67704
|
+
rule: "CI-3.plugin-unpinned",
|
|
67705
|
+
locator: safeText(market, 200),
|
|
67706
|
+
dimension: "mcp",
|
|
67707
|
+
severity: "medium",
|
|
67708
|
+
title: "Agent installs plugins from a third-party marketplace on every run",
|
|
67709
|
+
file: path91,
|
|
67710
|
+
signals: [
|
|
67711
|
+
`\`plugin_marketplaces: ${safeText(market, 120)}\` is not pinned to a commit: whoever controls it changes the code and prompts the agent runs`
|
|
67712
|
+
],
|
|
67713
|
+
fix: "Pin the marketplace to a commit (`<url>#<sha>`), or vendor the plugin into this repository."
|
|
67714
|
+
});
|
|
67715
|
+
}
|
|
67716
|
+
}
|
|
67717
|
+
return out;
|
|
66884
67718
|
}
|
|
66885
|
-
|
|
66886
|
-
|
|
66887
|
-
|
|
66888
|
-
|
|
66889
|
-
|
|
66890
|
-
|
|
66891
|
-
|
|
66892
|
-
|
|
66893
|
-
|
|
66894
|
-
|
|
67719
|
+
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;
|
|
67720
|
+
var WEB_READ_RE = /(^|["\s,])(WebFetch|WebSearch)(\(|\s|,|"|$)/;
|
|
67721
|
+
function readsItemList(text) {
|
|
67722
|
+
if (text.length > 2e5) return ITEM_LIST_RE.test(text);
|
|
67723
|
+
return text.replace(/\\\r?\n/g, " ").split("\n").some((l) => ITEM_LIST_RE.test(l) && !sanitizedListing(l));
|
|
67724
|
+
}
|
|
67725
|
+
function analyzeTrustedReaders(path91, content, autoLabels, callers) {
|
|
67726
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67727
|
+
try {
|
|
67728
|
+
return analyzeTrustedReadersInner(path91, content, callers);
|
|
67729
|
+
} finally {
|
|
67730
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67731
|
+
}
|
|
67732
|
+
}
|
|
67733
|
+
function keysWithCallers(keys, callers) {
|
|
67734
|
+
return [.../* @__PURE__ */ new Set([...keys, ...(callers ?? []).flatMap((c) => c.keys)])];
|
|
67735
|
+
}
|
|
67736
|
+
function analyzeTrustedReadersInner(path91, content, callers) {
|
|
67737
|
+
let raw;
|
|
67738
|
+
try {
|
|
67739
|
+
raw = loadWorkflow(content).raw;
|
|
67740
|
+
} catch {
|
|
67741
|
+
return [];
|
|
67742
|
+
}
|
|
67743
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67744
|
+
const wf = raw;
|
|
67745
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67746
|
+
else CALLERS.delete(wf);
|
|
67747
|
+
const reach = triggerReach(wf, raw);
|
|
67748
|
+
if (reach.untrusted || reach.reusable) return [];
|
|
67749
|
+
const keys = keysWithCallers(reach.keys, callers);
|
|
67750
|
+
if (!keys.some((k) => /^schedule$/i.test(k))) return [];
|
|
67751
|
+
for (const job of jobList(wf)) {
|
|
67752
|
+
const jobIf = parseIf(str(job.if));
|
|
67753
|
+
if (jobIf !== null && !exprAllowsEvent(jobIf, "schedule")) continue;
|
|
67754
|
+
const steps = job.steps ?? [];
|
|
67755
|
+
const agentIdx = steps.findIndex(isAgentStep);
|
|
67756
|
+
if (agentIdx < 0) continue;
|
|
67757
|
+
const agent = steps.filter(isAgentStep);
|
|
67758
|
+
const tools = collectTools(agent);
|
|
67759
|
+
const promptText = agent.map(
|
|
67760
|
+
(st) => str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + (isRunAgent(st) ? str(st.run) : "")
|
|
67761
|
+
).join(" ");
|
|
67762
|
+
const earlier = steps.slice(0, agentIdx).filter((st) => !isAgentStep(st)).map((st) => str(st.run) + " " + str(st.with?.["script"]));
|
|
67763
|
+
const reads = [];
|
|
67764
|
+
if (ITEM_READ_TOOL_RE.test(tools) || readsItemList(promptText))
|
|
67765
|
+
reads.push("reads open issues or pull requests");
|
|
67766
|
+
else if (earlier.some(readsItemList))
|
|
67767
|
+
reads.push("an earlier step lists open issues or pull requests for it");
|
|
67768
|
+
if (WEB_READ_RE.test(tools)) reads.push("fetches third-party web content");
|
|
67769
|
+
if (!reads.length) continue;
|
|
67770
|
+
const withheld = agent.every(
|
|
67771
|
+
(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() === ""
|
|
67772
|
+
);
|
|
67773
|
+
const checkouts = steps.slice(0, agentIdx).filter((st) => /actions\/checkout/i.test(str(st.uses)));
|
|
67774
|
+
const persisted = checkouts.some((st) => str(st.with?.["persist-credentials"]) !== "false");
|
|
67775
|
+
const checkoutPat = checkouts.some(
|
|
67776
|
+
(st) => /secrets\.(?!GITHUB_TOKEN\b)[A-Za-z_]/i.test(str(st.with?.["token"]))
|
|
67777
|
+
);
|
|
67778
|
+
const pat = usesPatIn([job]) || checkoutPat;
|
|
67779
|
+
const permWrite = jobsHaveWrite(
|
|
67780
|
+
wf,
|
|
67781
|
+
[job],
|
|
67782
|
+
/["']?(contents|issues|pull-requests)["']?\s*:\s*["']?write/i
|
|
67783
|
+
);
|
|
67784
|
+
const handsToken = agent.some(
|
|
67785
|
+
(st) => !!claudeActionBuild(st.uses) || !!str(st.with?.["github_token"]).trim()
|
|
67786
|
+
);
|
|
67787
|
+
const write2 = pat || permWrite && !withheld && (handsToken || persisted);
|
|
67788
|
+
if (!write2) continue;
|
|
67789
|
+
const writes = pat ? "a static PAT" : "a write token";
|
|
67790
|
+
return [
|
|
67791
|
+
{
|
|
67792
|
+
check: "CI-2",
|
|
67793
|
+
rule: "CI-2.trusted-trigger-reader",
|
|
67794
|
+
dimension: "workflows",
|
|
67795
|
+
severity: "advisory",
|
|
67796
|
+
title: "Agent on a trusted trigger reads content strangers write, with a write token",
|
|
67797
|
+
file: path91,
|
|
67798
|
+
signals: [
|
|
67799
|
+
`runs on ${keys.map((k) => safeText(k, 40)).join(", ")}: no stranger starts it`,
|
|
67800
|
+
...reads.map((r) => `the agent ${r}`),
|
|
67801
|
+
`it holds ${writes}: injected text in that content can act with it`
|
|
67802
|
+
],
|
|
67803
|
+
fix: "Give the agent a read-only token and apply its output in a separate step, or filter the items to trusted authors first."
|
|
67804
|
+
}
|
|
67805
|
+
];
|
|
67806
|
+
}
|
|
67807
|
+
return [];
|
|
67808
|
+
}
|
|
67809
|
+
var FORK_PR_TRIGGER_RE = /^(pull_request_target|issue_comment|workflow_run)$/i;
|
|
67810
|
+
var SAME_REPO_GATE_RE = new RegExp(
|
|
67811
|
+
[
|
|
67812
|
+
String.raw`head(?:\.repo|_repository)\.full_name\s*==\s*github\.(?:repository\b|event\.(?:pull_request\.base\.repo|repository)\.full_name)`,
|
|
67813
|
+
String.raw`github\.repository\s*==\s*github\.event\.(?:pull_request\.head\.repo|workflow_run\.head_repository)\.full_name`,
|
|
67814
|
+
String.raw`head(?:\.repo|_repository)\.fork\s*==\s*false`,
|
|
67815
|
+
String.raw`head(?:\.repo|_repository)\.owner\.login\s*==\s*github\.repository_owner`,
|
|
67816
|
+
String.raw`workflow_run\.pull_requests\[0\]`
|
|
67817
|
+
].join("|"),
|
|
67818
|
+
"i"
|
|
67819
|
+
);
|
|
67820
|
+
var NOT_FORK_ATOM_RE = /^github\.event\.(?:pull_request\.head\.repo|workflow_run\.head_repository)\.fork$/i;
|
|
67821
|
+
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;
|
|
67822
|
+
function requiresAtom(src, re, events, notRe = NOT_FORK_ATOM_RE) {
|
|
67823
|
+
const x = parseIf(src);
|
|
67824
|
+
if (!x) return false;
|
|
67825
|
+
const req = (e) => {
|
|
67826
|
+
if (events?.length && events.every((ev) => !exprAllowsEvent(e, ev))) return true;
|
|
67827
|
+
switch (e.k) {
|
|
67828
|
+
case "atom": {
|
|
67829
|
+
const m = maskLiterals(e.t);
|
|
67830
|
+
return re.test(m) && !CHAINED_COMPARE_RE.test(m);
|
|
67831
|
+
}
|
|
67832
|
+
case "not":
|
|
67833
|
+
return e.c.k === "atom" && notRe.test(e.c.t.trim());
|
|
67834
|
+
case "and":
|
|
67835
|
+
return e.c.some(req);
|
|
67836
|
+
case "or":
|
|
67837
|
+
return e.c.every(req);
|
|
66895
67838
|
}
|
|
66896
|
-
|
|
66897
|
-
|
|
66898
|
-
|
|
67839
|
+
};
|
|
67840
|
+
return req(x);
|
|
67841
|
+
}
|
|
67842
|
+
var PR_EVENTS = [
|
|
67843
|
+
"pull_request_target",
|
|
67844
|
+
"pull_request",
|
|
67845
|
+
"pull_request_review",
|
|
67846
|
+
"pull_request_review_comment"
|
|
67847
|
+
];
|
|
67848
|
+
function headRefEvents(heads) {
|
|
67849
|
+
const evs = /* @__PURE__ */ new Set();
|
|
67850
|
+
for (const st of heads) {
|
|
67851
|
+
if (st.uses == null) return null;
|
|
67852
|
+
const m = /^\s*\$\{\{([\s\S]{1,500})\}\}\s*$/.exec(str(st.with?.["ref"]));
|
|
67853
|
+
if (!m) return null;
|
|
67854
|
+
for (const part of m[1].split("||").map((x) => x.trim())) {
|
|
67855
|
+
if (/^github\.event\.workflow_run\.[\w.]+$/.test(part)) evs.add("workflow_run");
|
|
67856
|
+
else if (/^github\.event\.pull_request\.[\w.]+$/.test(part))
|
|
67857
|
+
PR_EVENTS.forEach((e) => evs.add(e));
|
|
67858
|
+
else if (!/^(?:github\.(?:sha|ref|ref_name|event\.repository\.default_branch)|'[^']*')$/.test(part))
|
|
67859
|
+
return null;
|
|
66899
67860
|
}
|
|
66900
|
-
if (items.length) out[key] = items;
|
|
66901
67861
|
}
|
|
66902
|
-
return
|
|
67862
|
+
return evs.size ? [...evs] : null;
|
|
66903
67863
|
}
|
|
66904
|
-
function
|
|
66905
|
-
const
|
|
66906
|
-
|
|
66907
|
-
|
|
66908
|
-
|
|
66909
|
-
|
|
66910
|
-
|
|
66911
|
-
|
|
66912
|
-
|
|
66913
|
-
|
|
66914
|
-
|
|
67864
|
+
function readOnlySandbox(st) {
|
|
67865
|
+
const args = str(st.with?.["codex-args"]) + " " + str(st.with?.["codex_args"]);
|
|
67866
|
+
return /^read-only$/i.test(str(st.with?.["sandbox"])) && !PERMISSION_BYPASS_RE.test(args);
|
|
67867
|
+
}
|
|
67868
|
+
function requiresTrustedAuthor(src) {
|
|
67869
|
+
const x = parseIf(src);
|
|
67870
|
+
if (!x) return false;
|
|
67871
|
+
const req = (e) => {
|
|
67872
|
+
switch (e.k) {
|
|
67873
|
+
case "atom":
|
|
67874
|
+
return TRUSTED_AUTHOR_GATE_RE.test(e.t) && !NONWRITE_SET_RE.test(e.t) && !CHAINED_COMPARE_RE.test(maskLiterals(e.t));
|
|
67875
|
+
case "not":
|
|
67876
|
+
return false;
|
|
67877
|
+
case "and":
|
|
67878
|
+
return e.c.some(req);
|
|
67879
|
+
case "or":
|
|
67880
|
+
return e.c.every(req);
|
|
66915
67881
|
}
|
|
66916
|
-
|
|
67882
|
+
};
|
|
67883
|
+
return req(x);
|
|
67884
|
+
}
|
|
67885
|
+
function analyzeForkCodeOnTrust(path91, content, callers, autoLabels) {
|
|
67886
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67887
|
+
try {
|
|
67888
|
+
return analyzeForkCodeOnTrustInner(path91, content, callers);
|
|
67889
|
+
} finally {
|
|
67890
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66917
67891
|
}
|
|
66918
|
-
if (cur) out.push(cur);
|
|
66919
|
-
return out;
|
|
66920
67892
|
}
|
|
66921
|
-
function
|
|
66922
|
-
|
|
66923
|
-
|
|
66924
|
-
|
|
66925
|
-
|
|
67893
|
+
function analyzeForkCodeOnTrustInner(path91, content, callers) {
|
|
67894
|
+
let raw;
|
|
67895
|
+
try {
|
|
67896
|
+
raw = loadWorkflow(content).raw;
|
|
67897
|
+
} catch {
|
|
67898
|
+
return [];
|
|
67899
|
+
}
|
|
67900
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67901
|
+
const wf = raw;
|
|
67902
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67903
|
+
else CALLERS.delete(wf);
|
|
67904
|
+
const reach = triggerReach(wf, raw);
|
|
67905
|
+
const forkKeys = keysWithCallers(reach.keys, callers).filter((k) => FORK_PR_TRIGGER_RE.test(k));
|
|
67906
|
+
if (!forkKeys.length) return [];
|
|
67907
|
+
const inj = new Set(injectableJobs(wf, raw, reach.untrusted || reach.reusable));
|
|
67908
|
+
for (const job of jobList(wf)) {
|
|
67909
|
+
if (inj.has(job)) continue;
|
|
67910
|
+
const steps = job.steps ?? [];
|
|
67911
|
+
const agent = steps.filter(isAgentStep);
|
|
67912
|
+
if (!agent.length) continue;
|
|
67913
|
+
if (untrustedHeadCheckout(steps) !== "root") continue;
|
|
67914
|
+
if (agent.every(readOnlySandbox)) continue;
|
|
67915
|
+
if (requiresAtom(str(job.if), SAME_REPO_GATE_RE)) continue;
|
|
67916
|
+
if (agent.every((st) => requiresAtom(str(st.if), SAME_REPO_GATE_RE))) continue;
|
|
67917
|
+
const heads = steps.filter((st) => untrustedHeadCheckout([st]) === "root");
|
|
67918
|
+
if (heads.length && heads.every((st) => requiresAtom(str(st.if), SAME_REPO_GATE_RE))) continue;
|
|
67919
|
+
const evs = headRefEvents(heads);
|
|
67920
|
+
if (evs && !forkKeys.some((k) => evs.includes(k.toLowerCase()))) continue;
|
|
67921
|
+
if (evs && requiresAtom(str(job.if), SAME_REPO_GATE_RE, evs)) continue;
|
|
67922
|
+
if (requiresTrustedAuthor(str(job.if))) continue;
|
|
67923
|
+
const holdsSecret = /secrets\./i.test(str(job.steps) + " " + str(job.env));
|
|
67924
|
+
if (!holdsSecret && !jobsHaveWrite(wf, [job], /:\s*["']?write/i)) continue;
|
|
67925
|
+
return [
|
|
67926
|
+
{
|
|
67927
|
+
check: "CI-2",
|
|
67928
|
+
rule: "CI-2.fork-code-on-trust",
|
|
67929
|
+
dimension: "workflows",
|
|
67930
|
+
severity: "advisory",
|
|
67931
|
+
title: "A maintainer's action runs the agent inside the fork's code",
|
|
67932
|
+
file: path91,
|
|
67933
|
+
signals: [
|
|
67934
|
+
`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)`,
|
|
67935
|
+
"checks out the untrusted PR head into the workspace root",
|
|
67936
|
+
"the agent loads the fork's CLAUDE.md, .claude/settings.json hooks, .mcp.json and build scripts next to the job's secrets"
|
|
67937
|
+
],
|
|
67938
|
+
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."
|
|
67939
|
+
}
|
|
67940
|
+
];
|
|
66926
67941
|
}
|
|
66927
67942
|
return [];
|
|
66928
67943
|
}
|
|
66929
67944
|
|
|
66930
67945
|
// src/ci-check/agent-config.ts
|
|
67946
|
+
var import_path72 = __toESM(require("path"));
|
|
66931
67947
|
function asStrings(v) {
|
|
66932
67948
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
66933
67949
|
}
|
|
@@ -66945,11 +67961,22 @@ function hookCommands(hooks) {
|
|
|
66945
67961
|
return out;
|
|
66946
67962
|
}
|
|
66947
67963
|
var OPEN_RUNNER_RE = new RegExp(
|
|
66948
|
-
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
|
|
67964
|
+
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
|
|
67965
|
+
// as Bash, review round 2).
|
|
67966
|
+
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} ?['"]?(?::\*| \*|\*)\)$`,
|
|
66949
67967
|
"i"
|
|
66950
67968
|
);
|
|
66951
67969
|
var OPEN_RUNNER_MAX = 300;
|
|
66952
67970
|
var INFO_FLAG_RE = /\s--(version|help)\b/;
|
|
67971
|
+
var EXTRA_RUNNER_RE = new RegExp(
|
|
67972
|
+
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} ?['"]?(?::\*| \*|\*)\)$`,
|
|
67973
|
+
"i"
|
|
67974
|
+
);
|
|
67975
|
+
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)$/;
|
|
67976
|
+
function extraRunner(grant) {
|
|
67977
|
+
if (grant.length > OPEN_RUNNER_MAX || INFO_FLAG_RE.test(grant)) return false;
|
|
67978
|
+
return EXTRA_RUNNER_RE.test(grant.replace(/\s+/g, " ").replace(/\(\s+/, "(")) || MCP_EXEC_RE.test(grant);
|
|
67979
|
+
}
|
|
66953
67980
|
function openRunner(grant) {
|
|
66954
67981
|
if (grant.length > OPEN_RUNNER_MAX || INFO_FLAG_RE.test(grant)) return false;
|
|
66955
67982
|
return OPEN_RUNNER_RE.test(grant.replace(/\s+/g, " ").replace(/\(\s+/, "("));
|
|
@@ -66957,13 +67984,15 @@ function openRunner(grant) {
|
|
|
66957
67984
|
function gradeBroadGrant(allow, deny, opts = {}) {
|
|
66958
67985
|
const denySupported = opts.denySupported ?? true;
|
|
66959
67986
|
const scope = opts.scope ?? "for everyone who opens this repo with the agent";
|
|
66960
|
-
const
|
|
66961
|
-
|
|
66962
|
-
|
|
67987
|
+
const asBash = (a) => a.replace(/^PowerShell(?=$|\()/, "Bash");
|
|
67988
|
+
const broad = allow.filter((raw) => {
|
|
67989
|
+
const a = asBash(raw);
|
|
67990
|
+
return /^Bash$|^Bash\(\s*\*|^Bash\(git:|^Write\(\s*\*|^Write$|^Edit$/.test(a) || openRunner(a) || opts.settings === true && extraRunner(a);
|
|
67991
|
+
});
|
|
66963
67992
|
if (broad.length === 0) return null;
|
|
66964
|
-
const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
|
|
66965
|
-
const bashBackstop = deny.some((d) => /^Bash\b/.test(d));
|
|
66966
|
-
const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(a));
|
|
67993
|
+
const hasBackstop = deny.some((d) => /Bash|PowerShell|Write|Edit/.test(d));
|
|
67994
|
+
const bashBackstop = deny.some((d) => /^(?:Bash|PowerShell)\b/.test(d));
|
|
67995
|
+
const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(asBash(a)));
|
|
66967
67996
|
const high = bareShell && !bashBackstop;
|
|
66968
67997
|
const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
|
|
66969
67998
|
if (high)
|
|
@@ -66980,7 +68009,7 @@ function gradeBroadGrant(allow, deny, opts = {}) {
|
|
|
66980
68009
|
);
|
|
66981
68010
|
if (broad.some((a) => /^Write|^Edit$/.test(a)))
|
|
66982
68011
|
signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
|
|
66983
|
-
if (broad.some(openRunner))
|
|
68012
|
+
if (broad.some((a) => openRunner(asBash(a))))
|
|
66984
68013
|
signals.push(
|
|
66985
68014
|
"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"
|
|
66986
68015
|
);
|
|
@@ -67093,9 +68122,12 @@ function analyzeAgentConfig(path91, content, tree) {
|
|
|
67093
68122
|
}
|
|
67094
68123
|
}
|
|
67095
68124
|
}
|
|
67096
|
-
const allow =
|
|
68125
|
+
const allow = [
|
|
68126
|
+
...asStrings(cfg.permissions?.allow),
|
|
68127
|
+
...asStrings(cfg.permissions?.allowedTools)
|
|
68128
|
+
];
|
|
67097
68129
|
const deny = asStrings(cfg.permissions?.deny);
|
|
67098
|
-
const grade = gradeBroadGrant(allow, deny);
|
|
68130
|
+
const grade = gradeBroadGrant(allow, deny, { settings: true });
|
|
67099
68131
|
if (grade) {
|
|
67100
68132
|
const { high, signals } = grade;
|
|
67101
68133
|
const at = jsonValueIndex(content, grade.broad[0]);
|
|
@@ -67123,6 +68155,26 @@ function analyzeAgentConfig(path91, content, tree) {
|
|
|
67123
68155
|
return findings;
|
|
67124
68156
|
}
|
|
67125
68157
|
var GRANT_CARRIER_RE = /(^|\/)[Ss][Kk][Ii][Ll][Ll]\.md$|(^|\/)\.claude\/commands\/.+\.md$/;
|
|
68158
|
+
var ROOT_SURFACE_RE = /^\.(claude|agents|github|codex|cursor|gemini)\//;
|
|
68159
|
+
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/;
|
|
68160
|
+
var PR_TEXT_RE = /gh\s+pr\s+(?:view|diff)\b|gh\s+api\s+[^\n]{0,120}\/pulls\b/;
|
|
68161
|
+
var WEB_TEXT_RE = /\bsearch the web\b|\bfetch (?:the )?(?:url|page)\b/i;
|
|
68162
|
+
function skillReadsUntrusted(path91, content, allow, broad) {
|
|
68163
|
+
if (!ROOT_SURFACE_RE.test(path91)) return null;
|
|
68164
|
+
const body = content.replace(/^---[\s\S]*?\n---/, "");
|
|
68165
|
+
const runsCode = broad.map((a) => a.replace(/^PowerShell(?=$|\()/, "Bash")).some((a) => /^Bash$|^Bash\(\s*\*/.test(a) || openRunner(a) && !/^Bash\(\s*gh\b/.test(a));
|
|
68166
|
+
const webGrant = allow.some((a) => /^(WebFetch|WebSearch)\b/.test(a));
|
|
68167
|
+
const reads = (re) => body.replace(/\\\r?\n/g, " ").split("\n").some((l) => re.test(l) && !sanitizedListing(l));
|
|
68168
|
+
if (runsCode && reads(ITEM_TEXT_RE))
|
|
68169
|
+
return "it reads pull request or issue text that strangers write, with a grant that runs code";
|
|
68170
|
+
if (runsCode && (webGrant || WEB_TEXT_RE.test(body)))
|
|
68171
|
+
return "it reads web content anyone can publish, with a grant that runs code";
|
|
68172
|
+
const writes = allow.some((a) => /^(Write|Edit|MultiEdit)$/.test(a));
|
|
68173
|
+
const ghOrGit = allow.some((a) => /^Bash\(\s*(gh|git)\b/.test(a));
|
|
68174
|
+
if (writes && ghOrGit && reads(PR_TEXT_RE))
|
|
68175
|
+
return "it reads pull request text that strangers write, and may edit files and run git/gh on it";
|
|
68176
|
+
return null;
|
|
68177
|
+
}
|
|
67126
68178
|
function analyzeSkillGrants(path91, content) {
|
|
67127
68179
|
if (!GRANT_CARRIER_RE.test(path91)) return [];
|
|
67128
68180
|
const allow = allowedToolsOf(parseFrontmatter(content));
|
|
@@ -67134,6 +68186,21 @@ function analyzeSkillGrants(path91, content) {
|
|
|
67134
68186
|
if (!grade) return [];
|
|
67135
68187
|
const kind = /commands\//.test(path91) ? "slash command" : "skill";
|
|
67136
68188
|
const at = content.search(/^allowed-tools\s*:/m);
|
|
68189
|
+
const untrusted = skillReadsUntrusted(path91, content, allow, grade.broad);
|
|
68190
|
+
if (untrusted)
|
|
68191
|
+
return [
|
|
68192
|
+
{
|
|
68193
|
+
check: "CI-1",
|
|
68194
|
+
rule: "CI-1.skill-untrusted-input",
|
|
68195
|
+
...at >= 0 ? { line: lineAtIndex(content, at) } : {},
|
|
68196
|
+
dimension: "toolRules",
|
|
68197
|
+
severity: "advisory",
|
|
68198
|
+
title: `Committed ${kind} runs code or writes with text strangers control`,
|
|
68199
|
+
file: path91,
|
|
68200
|
+
signals: [...grade.signals, untrusted],
|
|
68201
|
+
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."
|
|
68202
|
+
}
|
|
68203
|
+
];
|
|
67137
68204
|
return [
|
|
67138
68205
|
{
|
|
67139
68206
|
check: "CI-1",
|
|
@@ -67155,27 +68222,178 @@ function analyzeSkillGrants(path91, content) {
|
|
|
67155
68222
|
|
|
67156
68223
|
// src/ci-check/mcp.ts
|
|
67157
68224
|
init_dist();
|
|
67158
|
-
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;&|`$<>()]+)*$/;
|
|
67159
|
-
|
|
68225
|
+
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;&|`$<>()]+)*$/;
|
|
68226
|
+
var LAUNCHERS = [
|
|
68227
|
+
{ re: /\bnpx\b/, name: "npx", registry: "npm" },
|
|
68228
|
+
{ re: /\bpnpx\b|\bpnpm\s+dlx\b/, name: "pnpm dlx", registry: "npm" },
|
|
68229
|
+
{ re: /\byarn\s+dlx\b/, name: "yarn dlx", registry: "npm" },
|
|
68230
|
+
{ re: /\bnpm\s+(?:exec|x)\b/, name: "npm exec", registry: "npm" },
|
|
68231
|
+
{ re: /\bbunx\b|\bbun\s+x\b/, name: "bunx", registry: "npm" },
|
|
68232
|
+
{ re: /\buvx\b|\buv\s+tool\s+run\b/, name: "uvx", registry: "PyPI" },
|
|
68233
|
+
{ re: /\bpipx\s+run\b/, name: "pipx run", registry: "PyPI" }
|
|
68234
|
+
];
|
|
68235
|
+
var PINNED_RE = /@v?\d|@[0-9a-f]{40}\b|==\s*\d/i;
|
|
68236
|
+
var LOCAL_FROM_RE = /(?:^|\s)--from(?:\s+|=)(?:\.{1,2}(?:\/\S*)?|\/\S*|~\/\S*)(?:\s|$)/;
|
|
68237
|
+
var RUN_VALUE_FLAGS = new Set(
|
|
68238
|
+
[
|
|
68239
|
+
"-a --attach --add-host --annotation --blkio-weight --blkio-weight-device --cap-add",
|
|
68240
|
+
"--cap-drop --cgroup-parent --cgroupns --cgroups --cgroup-conf --cidfile --cpu-period",
|
|
68241
|
+
"--cpu-quota --cpu-rt-period --cpu-rt-runtime -c --cpu-shares --cpus --cpuset-cpus",
|
|
68242
|
+
"--cpuset-mems --device --device-cgroup-rule --device-read-bps --device-read-iops",
|
|
68243
|
+
"--device-write-bps --device-write-iops --dns --dns-option --dns-search --domainname",
|
|
68244
|
+
"--entrypoint -e --env --env-file --expose --gpus --group-add --health-cmd",
|
|
68245
|
+
"--health-interval --health-retries --health-start-interval --health-start-period",
|
|
68246
|
+
"--health-timeout -h --hostname --ip --ip6 --ipc --isolation --kernel-memory -l --label",
|
|
68247
|
+
"--label-file --link --link-local-ip --log-driver --log-opt --mac-address -m --memory",
|
|
68248
|
+
"--memory-reservation --memory-swap --memory-swappiness --mount --name --network --net",
|
|
68249
|
+
"--network-alias --oom-score-adj --pid --pids-limit --platform -p --publish --pull",
|
|
68250
|
+
"--restart --runtime --security-opt --shm-size --stop-signal --stop-timeout",
|
|
68251
|
+
"--storage-opt --sysctl --tmpfs --ulimit -u --user --userns --uts -v --volume",
|
|
68252
|
+
"--volume-driver --volumes-from -w --workdir --secret --pod --arch --os --variant",
|
|
68253
|
+
"--timezone --umask --requires --uidmap --gidmap --subuidname --subgidname --hooks-dir",
|
|
68254
|
+
"--init-path --seccomp-policy --sdnotify --personality --preserve-fds --chrootdirs"
|
|
68255
|
+
].join(" ").split(" ")
|
|
68256
|
+
);
|
|
68257
|
+
function runImage(tokens) {
|
|
68258
|
+
const i = tokens.findIndex(
|
|
68259
|
+
(t, k) => /(^|\/)(docker|podman|nerdctl)$/.test(t) && (tokens[k + 1] === "run" || tokens[k + 1] === "container" && tokens[k + 2] === "run")
|
|
68260
|
+
);
|
|
68261
|
+
if (i < 0) return null;
|
|
68262
|
+
const from = tokens[i + 1] === "run" ? i + 2 : i + 3;
|
|
68263
|
+
for (let k = from; k < tokens.length; k++) {
|
|
68264
|
+
const t = tokens[k];
|
|
68265
|
+
if (t.startsWith("-")) {
|
|
68266
|
+
if (!t.includes("=") && RUN_VALUE_FLAGS.has(t)) k++;
|
|
68267
|
+
continue;
|
|
68268
|
+
}
|
|
68269
|
+
return t;
|
|
68270
|
+
}
|
|
68271
|
+
return null;
|
|
68272
|
+
}
|
|
68273
|
+
function launchedPackages(argv) {
|
|
68274
|
+
const t = argv.split(/\s+/).filter(Boolean);
|
|
68275
|
+
const i = t.findIndex((x) => /(^|\/)(npx|pnpx|bunx|uvx|dlx|exec|x|run)$/.test(x));
|
|
68276
|
+
if (i < 0) return [];
|
|
68277
|
+
const bare = (v) => v.startsWith("@") ? v.replace(/^(@[^/]+\/[^@]+).*$/, "$1") : v.replace(/(==|@).*$/, "");
|
|
68278
|
+
const flagged = [];
|
|
68279
|
+
let positional = null;
|
|
68280
|
+
for (let k = i + 1; k < t.length; k++) {
|
|
68281
|
+
const tok = t[k];
|
|
68282
|
+
const eq = /^(--from|-p|--package)=(.*)$/.exec(tok);
|
|
68283
|
+
if (eq) {
|
|
68284
|
+
flagged.push(eq[2]);
|
|
68285
|
+
continue;
|
|
68286
|
+
}
|
|
68287
|
+
if (/^(--from|-p|--package)$/.test(tok)) {
|
|
68288
|
+
if (t[k + 1]) flagged.push(t[++k]);
|
|
68289
|
+
continue;
|
|
68290
|
+
}
|
|
68291
|
+
if (tok.startsWith("-")) continue;
|
|
68292
|
+
positional ??= tok;
|
|
68293
|
+
}
|
|
68294
|
+
const names = flagged.length ? flagged : positional ? [positional] : [];
|
|
68295
|
+
return names.map((n) => bare(n).toLowerCase()).filter(Boolean);
|
|
68296
|
+
}
|
|
68297
|
+
var SHELL_RE = /(?:^|\/)(?:sh|bash|zsh|dash|cmd|pwsh|powershell)(?:\.exe)?$/i;
|
|
68298
|
+
var SHELL_OP_RE = /[;&|`$<>()]/;
|
|
68299
|
+
function launchesOwnPackage(argv, own) {
|
|
68300
|
+
if (own.size === 0) return false;
|
|
68301
|
+
if (SHELL_OP_RE.test(argv) || SHELL_RE.test(argv.split(/\s+/)[0] ?? "")) return false;
|
|
68302
|
+
const l = LAUNCHERS.find((x) => x.re.test(argv));
|
|
68303
|
+
if (!l) return false;
|
|
68304
|
+
const reg = l.registry === "PyPI" ? "pypi" : "npm";
|
|
68305
|
+
const pkgs = launchedPackages(argv);
|
|
68306
|
+
return pkgs.length > 0 && pkgs.every((p) => own.has(`${reg}:${p}`) || own.has(p));
|
|
68307
|
+
}
|
|
68308
|
+
function unpinnedLauncher(argv, own) {
|
|
68309
|
+
if (LOCAL_RUNNER_RE.test(argv)) return null;
|
|
68310
|
+
if (!SHELL_OP_RE.test(argv) && LOCAL_FROM_RE.test(argv)) return null;
|
|
68311
|
+
if (launchesOwnPackage(argv, own)) return null;
|
|
68312
|
+
const image = runImage(argv.split(/\s+/));
|
|
68313
|
+
if (image) {
|
|
68314
|
+
if (image.includes("@sha256:")) return null;
|
|
68315
|
+
const tail = image.slice(image.lastIndexOf("/") + 1);
|
|
68316
|
+
const tag = tail.includes(":") ? tail.slice(tail.indexOf(":") + 1) : "";
|
|
68317
|
+
return !tag || tag === "latest" ? { launcher: "docker run", registry: "the image registry" } : null;
|
|
68318
|
+
}
|
|
68319
|
+
const goMod = /\bgo\s+run\s+(?:-\S+\s+)*([^\s@]+)@(\S+)/.exec(argv);
|
|
68320
|
+
if (goMod)
|
|
68321
|
+
return /^v\d/.test(goMod[2]) || /^[0-9a-f]{40}$/.test(goMod[2]) ? null : { launcher: "go run", registry: "the Go module proxy" };
|
|
68322
|
+
const l = LAUNCHERS.find((x) => x.re.test(argv));
|
|
68323
|
+
if (!l) return null;
|
|
68324
|
+
return /@latest\b/.test(argv) || !PINNED_RE.test(argv) ? { launcher: l.name, registry: l.registry } : null;
|
|
68325
|
+
}
|
|
68326
|
+
function ownPackageNames(manifests) {
|
|
68327
|
+
const out = /* @__PURE__ */ new Set();
|
|
68328
|
+
for (const m of manifests) {
|
|
68329
|
+
if (m.path === "package.json") {
|
|
68330
|
+
try {
|
|
68331
|
+
const pkg = JSON.parse(m.content);
|
|
68332
|
+
if (typeof pkg.name === "string" && pkg.name && pkg.private !== true)
|
|
68333
|
+
out.add(`npm:${pkg.name.toLowerCase()}`);
|
|
68334
|
+
} catch {
|
|
68335
|
+
}
|
|
68336
|
+
} else if (m.path === "pyproject.toml") {
|
|
68337
|
+
const sec = /^\[(project|tool\.poetry)\]\s*$([\s\S]*?)(?=^\[|(?![\s\S]))/m.exec(m.content);
|
|
68338
|
+
const name = sec && /^name\s*=\s*["']([^"']{1,200})["']/m.exec(sec[2]);
|
|
68339
|
+
if (name) {
|
|
68340
|
+
out.add(`pypi:${name[1].toLowerCase()}`);
|
|
68341
|
+
out.add(`pypi:${name[1].toLowerCase().replace(/_/g, "-")}`);
|
|
68342
|
+
}
|
|
68343
|
+
}
|
|
68344
|
+
}
|
|
68345
|
+
return out;
|
|
68346
|
+
}
|
|
68347
|
+
function analyzeMcp(path91, content, own = /* @__PURE__ */ new Set()) {
|
|
67160
68348
|
let cfg;
|
|
67161
68349
|
try {
|
|
67162
68350
|
cfg = JSON.parse(content);
|
|
67163
68351
|
} catch {
|
|
67164
68352
|
return [];
|
|
67165
68353
|
}
|
|
67166
|
-
|
|
68354
|
+
if (!cfg || typeof cfg !== "object") return [];
|
|
68355
|
+
const sections = [];
|
|
68356
|
+
const obj = (x) => x && typeof x === "object" && !Array.isArray(x) ? x : {};
|
|
68357
|
+
sections.push(["mcpServers", obj(cfg.mcpServers)]);
|
|
68358
|
+
sections.push(["servers", obj(cfg.servers)]);
|
|
68359
|
+
const opencode = {};
|
|
68360
|
+
for (const [name, s] of Object.entries(obj(cfg.mcp))) {
|
|
68361
|
+
if (!s || typeof s !== "object") continue;
|
|
68362
|
+
const o = s;
|
|
68363
|
+
const cmd = Array.isArray(o.command) ? o.command.map(String) : [];
|
|
68364
|
+
opencode[name] = {
|
|
68365
|
+
command: cmd[0],
|
|
68366
|
+
args: cmd.slice(1),
|
|
68367
|
+
url: o.url,
|
|
68368
|
+
env: obj(o.environment),
|
|
68369
|
+
disabled: o.enabled === false
|
|
68370
|
+
};
|
|
68371
|
+
}
|
|
68372
|
+
sections.push(["mcp", opencode]);
|
|
68373
|
+
const count = /* @__PURE__ */ new Map();
|
|
68374
|
+
for (const [, sec] of sections)
|
|
68375
|
+
for (const name of Object.keys(sec)) count.set(name, (count.get(name) ?? 0) + 1);
|
|
68376
|
+
const servers = {};
|
|
68377
|
+
for (const [section, sec] of sections)
|
|
68378
|
+
for (const [name, spec] of Object.entries(sec)) {
|
|
68379
|
+
if (!spec || typeof spec !== "object") continue;
|
|
68380
|
+
const key = (count.get(name) ?? 0) > 1 ? `${section}.${name}` : name;
|
|
68381
|
+
servers[key] = { ...spec, anchor: name };
|
|
68382
|
+
}
|
|
68383
|
+
return analyzeMcpServers(servers, path91, content, own);
|
|
67167
68384
|
}
|
|
67168
|
-
function analyzeMcpServers(servers, path91, content = "") {
|
|
68385
|
+
function analyzeMcpServers(servers, path91, content = "", own = /* @__PURE__ */ new Set()) {
|
|
67169
68386
|
const findings = [];
|
|
67170
68387
|
for (const [name, srv] of Object.entries(servers ?? {})) {
|
|
67171
68388
|
if (!srv || srv.disabled) continue;
|
|
67172
68389
|
const argv = [srv.command, ...Array.isArray(srv.args) ? srv.args.map(String) : []].join(" ");
|
|
67173
|
-
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
68390
|
+
const esc = (srv.anchor ?? name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
67174
68391
|
const at = content.search(
|
|
67175
68392
|
new RegExp(`"${esc}"\\s*:|^[ \\t]*\\[mcp_servers\\.(?:"${esc}"|${esc})\\]`, "m")
|
|
67176
68393
|
);
|
|
67177
68394
|
const line = at >= 0 ? lineAtIndex(content, at) : void 0;
|
|
67178
|
-
|
|
68395
|
+
const why = unpinnedLauncher(argv, own);
|
|
68396
|
+
if (why) {
|
|
67179
68397
|
findings.push({
|
|
67180
68398
|
check: "CI-3",
|
|
67181
68399
|
rule: "CI-3.mcp-unpinned",
|
|
@@ -67185,9 +68403,16 @@ function analyzeMcpServers(servers, path91, content = "") {
|
|
|
67185
68403
|
title: `MCP server "${name}" runs an unpinned executable`,
|
|
67186
68404
|
file: path91,
|
|
67187
68405
|
...line ? { line } : {},
|
|
67188
|
-
signals: [
|
|
68406
|
+
signals: [
|
|
68407
|
+
why.launcher === "npx" ? `\`${safeText(argv, 120)}\`: unversioned/@latest npx` : `\`${safeText(argv, 120)}\`: unversioned/latest ${why.launcher}`
|
|
68408
|
+
],
|
|
67189
68409
|
fix: "Pin the MCP server package to an exact version so a PR (or a registry compromise) can\u2019t swap the toolchain.",
|
|
67190
|
-
explain: explainMcp({
|
|
68410
|
+
explain: explainMcp({
|
|
68411
|
+
name,
|
|
68412
|
+
argv,
|
|
68413
|
+
registry: why.registry,
|
|
68414
|
+
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
|
|
68415
|
+
})
|
|
67191
68416
|
});
|
|
67192
68417
|
}
|
|
67193
68418
|
for (const [k, v] of Object.entries(srv.env ?? {})) {
|
|
@@ -67216,7 +68441,7 @@ function analyzeMcpServers(servers, path91, content = "") {
|
|
|
67216
68441
|
|
|
67217
68442
|
// src/ci-check/codex.ts
|
|
67218
68443
|
var import_smol_toml7 = require("smol-toml");
|
|
67219
|
-
function analyzeCodexConfig(path91, content) {
|
|
68444
|
+
function analyzeCodexConfig(path91, content, own = /* @__PURE__ */ new Set()) {
|
|
67220
68445
|
let cfg;
|
|
67221
68446
|
try {
|
|
67222
68447
|
cfg = (0, import_smol_toml7.parse)(content);
|
|
@@ -67224,7 +68449,7 @@ function analyzeCodexConfig(path91, content) {
|
|
|
67224
68449
|
return [];
|
|
67225
68450
|
}
|
|
67226
68451
|
const findings = [];
|
|
67227
|
-
findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path91, content));
|
|
68452
|
+
findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path91, content, own));
|
|
67228
68453
|
const sandbox = typeof cfg.sandbox_mode === "string" ? cfg.sandbox_mode : "";
|
|
67229
68454
|
const approval = typeof cfg.approval_policy === "string" ? cfg.approval_policy : "";
|
|
67230
68455
|
const fullAccess = /danger-full-access/i.test(sandbox);
|
|
@@ -67261,10 +68486,12 @@ var UNREAD_RULES = /* @__PURE__ */ new Set([
|
|
|
67261
68486
|
"CI-1.hook-script.unscanned-size",
|
|
67262
68487
|
"CI-6.skill-script.unscanned-size"
|
|
67263
68488
|
]);
|
|
67264
|
-
var CONCEALMENT = /* @__PURE__ */ new Set(["CI-6.unicode-tag-chars", "CI-6.bidi-override"]);
|
|
68489
|
+
var CONCEALMENT = /* @__PURE__ */ new Set(["CI-6.unicode-tag-chars", "CI-6.bidi-override", "CI-6.homoglyph"]);
|
|
67265
68490
|
function tierOf(f) {
|
|
67266
68491
|
const { rule, severity } = f;
|
|
67267
68492
|
if (rule === "CI-1.skill-allowed-tools") return "review";
|
|
68493
|
+
if (rule === "CI-2.trusted-trigger-reader" || rule === "CI-2.fork-code-on-trust" || rule === "CI-1.skill-untrusted-input")
|
|
68494
|
+
return "alert";
|
|
67268
68495
|
if (UNREAD_RULES.has(rule)) return "review";
|
|
67269
68496
|
if (rule.startsWith("CI-2.") || rule.startsWith("CI-4."))
|
|
67270
68497
|
return severity === "advisory" ? "note" : "alert";
|
|
@@ -67629,12 +68856,54 @@ function scanTree(tree) {
|
|
|
67629
68856
|
}
|
|
67630
68857
|
return named.get(dir).has(docPath);
|
|
67631
68858
|
};
|
|
68859
|
+
const callersOf = /* @__PURE__ */ new Map();
|
|
68860
|
+
const autoLabels = /* @__PURE__ */ new Set();
|
|
68861
|
+
const routeOfFile = (file) => file.route !== void 0 ? file.route : routeOf(file.path, skillDirs, appDirs);
|
|
68862
|
+
const manifests = [];
|
|
68863
|
+
for (const file of tree.files) {
|
|
68864
|
+
if (routeOfFile(file) !== "manifest") continue;
|
|
68865
|
+
try {
|
|
68866
|
+
manifests.push({ path: file.path, content: file.content });
|
|
68867
|
+
} catch {
|
|
68868
|
+
}
|
|
68869
|
+
}
|
|
68870
|
+
const ownPackages = ownPackageNames(manifests);
|
|
68871
|
+
for (const file of tree.files) {
|
|
68872
|
+
if (routeOfFile(file) !== "issue-form") continue;
|
|
68873
|
+
try {
|
|
68874
|
+
for (const l of issueFormLabels(file.content)) autoLabels.add(l);
|
|
68875
|
+
} catch {
|
|
68876
|
+
}
|
|
68877
|
+
}
|
|
68878
|
+
const runSources = /* @__PURE__ */ new Map();
|
|
68879
|
+
for (const file of tree.files) {
|
|
68880
|
+
if (routeOfFile(file) !== "workflow") continue;
|
|
68881
|
+
try {
|
|
68882
|
+
const src = runSourceOf(file.path, file.content, autoLabels);
|
|
68883
|
+
if (src) runSources.set(src.name, (runSources.get(src.name) ?? true) && src.trusted);
|
|
68884
|
+
} catch {
|
|
68885
|
+
}
|
|
68886
|
+
}
|
|
68887
|
+
const trustedRunSources = new Set([...runSources].filter(([, t]) => t).map(([n]) => n));
|
|
68888
|
+
for (const file of tree.files) {
|
|
68889
|
+
if (routeOfFile(file) !== "workflow") continue;
|
|
68890
|
+
try {
|
|
68891
|
+
const content = file.content;
|
|
68892
|
+
for (const { target, caller } of localCallsOf(file.path, content, autoLabels)) {
|
|
68893
|
+
const list = callersOf.get(target) ?? [];
|
|
68894
|
+
list.push(caller);
|
|
68895
|
+
callersOf.set(target, list);
|
|
68896
|
+
}
|
|
68897
|
+
} catch {
|
|
68898
|
+
}
|
|
68899
|
+
}
|
|
67632
68900
|
for (const file of tree.files) {
|
|
67633
68901
|
let route = file.route !== void 0 ? file.route : routeOf(file.path, skillDirs, appDirs);
|
|
67634
68902
|
if (route === "app-doc") {
|
|
67635
68903
|
if (!namedBy(file.path)) continue;
|
|
67636
68904
|
route = "instruction";
|
|
67637
68905
|
}
|
|
68906
|
+
if (route === "issue-form" || route === "manifest") continue;
|
|
67638
68907
|
inspected.push(file.path);
|
|
67639
68908
|
if (file.path === SUPPRESSIONS_FILE) continue;
|
|
67640
68909
|
const testKind = route && CONTENT_ROUTES.has(route) ? testPathKind(file.path, route) : "none";
|
|
@@ -67644,10 +68913,20 @@ function scanTree(tree) {
|
|
|
67644
68913
|
const content = file.content;
|
|
67645
68914
|
switch (route) {
|
|
67646
68915
|
case "workflow": {
|
|
67647
|
-
const
|
|
68916
|
+
const callers = callersOf.get(file.path);
|
|
68917
|
+
const f = analyzeWorkflow(file.path, content, callers, autoLabels, trustedRunSources);
|
|
67648
68918
|
if (f) findings.push(f);
|
|
67649
|
-
const s = analyzeWorkflowSecrets(
|
|
68919
|
+
const s = analyzeWorkflowSecrets(
|
|
68920
|
+
file.path,
|
|
68921
|
+
content,
|
|
68922
|
+
callers,
|
|
68923
|
+
autoLabels,
|
|
68924
|
+
trustedRunSources
|
|
68925
|
+
);
|
|
67650
68926
|
if (s) findings.push(s);
|
|
68927
|
+
findings.push(...analyzeWorkflowPlugins(file.path, content, tree.owner));
|
|
68928
|
+
findings.push(...analyzeTrustedReaders(file.path, content, autoLabels, callers));
|
|
68929
|
+
findings.push(...analyzeForkCodeOnTrust(file.path, content, callers, autoLabels));
|
|
67651
68930
|
break;
|
|
67652
68931
|
}
|
|
67653
68932
|
case "agent-config":
|
|
@@ -67660,10 +68939,10 @@ function scanTree(tree) {
|
|
|
67660
68939
|
findings.push(...analyzeScript(file.path, content, "CI-6.skill-script"));
|
|
67661
68940
|
break;
|
|
67662
68941
|
case "mcp":
|
|
67663
|
-
findings.push(...analyzeMcp(file.path, content));
|
|
68942
|
+
findings.push(...analyzeMcp(file.path, content, ownPackages));
|
|
67664
68943
|
break;
|
|
67665
68944
|
case "codex":
|
|
67666
|
-
findings.push(...analyzeCodexConfig(file.path, content));
|
|
68945
|
+
findings.push(...analyzeCodexConfig(file.path, content, ownPackages));
|
|
67667
68946
|
break;
|
|
67668
68947
|
case "instruction":
|
|
67669
68948
|
findings.push(...analyzeInstructionFile(file.path, content));
|
|
@@ -67671,7 +68950,9 @@ function scanTree(tree) {
|
|
|
67671
68950
|
break;
|
|
67672
68951
|
}
|
|
67673
68952
|
} catch (err2) {
|
|
67674
|
-
notes.push(
|
|
68953
|
+
notes.push(
|
|
68954
|
+
`checker degraded on ${file.path}: ${err2?.message ?? "error"}; results may be INCOMPLETE`
|
|
68955
|
+
);
|
|
67675
68956
|
}
|
|
67676
68957
|
if (testKind === "alerts-only") {
|
|
67677
68958
|
const kept = findings.splice(before).filter((f) => tierOf(f) === "alert");
|