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