@node9/proxy 2.29.0 → 2.30.1
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 +1407 -151
- package/dist/cli.mjs +1407 -151
- 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) && !(/^workflow_run$/i.test(k) && upstreamRunTrusted(wf, raw)) || /^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,162 @@ 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, trustedRunSources) {
|
|
66550
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
66551
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
66552
|
+
try {
|
|
66553
|
+
return localCallsOfInner(path91, content);
|
|
66554
|
+
} finally {
|
|
66555
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66556
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
66557
|
+
}
|
|
66558
|
+
}
|
|
66559
|
+
function localCallsOfInner(path91, content) {
|
|
66560
|
+
let raw;
|
|
66561
|
+
try {
|
|
66562
|
+
raw = loadWorkflow(content).raw;
|
|
66563
|
+
} catch {
|
|
66564
|
+
return [];
|
|
66565
|
+
}
|
|
66566
|
+
if (!raw || typeof raw !== "object") return [];
|
|
66567
|
+
const wf = raw;
|
|
66568
|
+
CALLERS.delete(wf);
|
|
66569
|
+
const reach = triggerReach(wf, raw);
|
|
66570
|
+
const out = [];
|
|
66571
|
+
for (const job of jobList(wf)) {
|
|
66572
|
+
const m = LOCAL_WORKFLOW_USES_RE.exec(str(job.uses));
|
|
66573
|
+
if (!m) continue;
|
|
66574
|
+
out.push({
|
|
66575
|
+
target: `.github/workflows/${m[1]}`,
|
|
66576
|
+
caller: {
|
|
66577
|
+
file: path91,
|
|
66578
|
+
keys: reach.keys,
|
|
66579
|
+
untrusted: reach.untrusted,
|
|
66580
|
+
secretExposed: reach.secretExposed,
|
|
66581
|
+
privileged: reach.privileged,
|
|
66582
|
+
workflowRun: reach.workflowRun,
|
|
66583
|
+
gated: jobGated(job, wf, raw),
|
|
66584
|
+
dispatched: dispatchedByGatedJob(job, wf, raw),
|
|
66585
|
+
reusable: reach.reusable
|
|
66586
|
+
}
|
|
66587
|
+
});
|
|
66588
|
+
}
|
|
66589
|
+
return out;
|
|
66590
|
+
}
|
|
66591
|
+
function dispatchedByGatedJob(job, wf, raw) {
|
|
66592
|
+
const names = [...str(job.if).matchAll(/needs\.([\w-]{1,100})\.outputs\./g)].map((m) => m[1]);
|
|
66593
|
+
if (!names.length) return false;
|
|
66594
|
+
const jobs = wf.jobs ?? {};
|
|
66595
|
+
const label3 = labelTypeConfigured(wf, raw);
|
|
66596
|
+
const someBranchGated = (x) => {
|
|
66597
|
+
switch (x.k) {
|
|
66598
|
+
case "atom":
|
|
66599
|
+
return exprGated(x, label3);
|
|
66600
|
+
case "not":
|
|
66601
|
+
return false;
|
|
66602
|
+
case "and":
|
|
66603
|
+
case "or":
|
|
66604
|
+
return x.c.some(someBranchGated);
|
|
66605
|
+
}
|
|
66606
|
+
};
|
|
66607
|
+
return names.every((n) => {
|
|
66608
|
+
const up = jobs[n];
|
|
66609
|
+
if (up == null) return false;
|
|
66610
|
+
if (jobGated(up, wf, raw)) return true;
|
|
66611
|
+
const x = parseIf(str(up.if));
|
|
66612
|
+
return x !== null && someBranchGated(x);
|
|
66613
|
+
});
|
|
66614
|
+
}
|
|
66136
66615
|
function allSteps(wf) {
|
|
66137
66616
|
const out = [];
|
|
66138
66617
|
for (const job of jobList(wf)) {
|
|
@@ -66140,11 +66619,47 @@ function allSteps(wf) {
|
|
|
66140
66619
|
}
|
|
66141
66620
|
return out;
|
|
66142
66621
|
}
|
|
66622
|
+
var AGENT_CLI_RE = new RegExp(
|
|
66623
|
+
[
|
|
66624
|
+
// The binary at a command start, then its "do work" flag anywhere on the same command
|
|
66625
|
+
// (`claude --model x -p`, `claude "$Q" --print`, `npx @anthropic-ai/claude-code -p`): the
|
|
66626
|
+
// flag had to come first (review, round 2). Bounded: at most 300 characters per command.
|
|
66627
|
+
String.raw`(?:^|[\s|;&(])(?:claude|@anthropic-ai/claude-code(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\s(?:-p|--print|--continue|--resume)(?=[\s"'=]|$)`,
|
|
66628
|
+
String.raw`(?:^|[\s|;&(])(?:codex|@openai/codex(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\sexec\b`,
|
|
66629
|
+
String.raw`(?:^|[\s|;&(])(?:gemini|@google/gemini-cli(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\s(?:-p|--prompt)(?=[\s"'=]|$)`,
|
|
66630
|
+
String.raw`(?:^|[\s|;&(])copilot(?=\s)[^\n;&|]{0,300}?\s(?:-p|--prompt|--allow-all-tools)(?=[\s"'=]|$)`,
|
|
66631
|
+
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)`,
|
|
66632
|
+
String.raw`api\.devin\.ai/|software-agent-sdk|\bagent_script\.py\b`
|
|
66633
|
+
].join("|"),
|
|
66634
|
+
"i"
|
|
66635
|
+
);
|
|
66636
|
+
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;
|
|
66637
|
+
var EVENT_CONTEXT_RE = /github\.event|context\.payload/;
|
|
66638
|
+
function isRunAgent(step) {
|
|
66639
|
+
return typeof step.run === "string" && !step.uses && AGENT_CLI_RE.test(step.run);
|
|
66640
|
+
}
|
|
66641
|
+
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;
|
|
66642
|
+
var AGENT_PATH_RE = /(?:^|[/_.-])(claude|codex|gemini|opencode|goose|aider|kilo(?:code)?)(?:$|[/_.-])/i;
|
|
66643
|
+
var PROMPT_INPUT_RE = /^(prompt|direct_prompt|prompt_file|prompt-file|agent)$/i;
|
|
66644
|
+
function isWrapperAgent(step) {
|
|
66645
|
+
const uses = str(step.uses);
|
|
66646
|
+
const w = step.with ?? {};
|
|
66647
|
+
if (Object.keys(w).some((k) => FUEL_WITH_KEY_RE.test(k))) return true;
|
|
66648
|
+
const env = { ...step.scopeEnv ?? {}, ...step.env ?? {} };
|
|
66649
|
+
const fuelEnv = Object.keys(env).some((k) => RUN_FUEL_ENV_RE.test(k));
|
|
66650
|
+
if (/^\s*\.\//.test(uses)) return fuelEnv;
|
|
66651
|
+
const pathPart = uses.replace(/^\s*[^/@\s]{1,100}\//, "").replace(/@.*$/, "");
|
|
66652
|
+
return AGENT_PATH_RE.test(pathPart) && (Object.keys(w).some((k) => PROMPT_INPUT_RE.test(k)) || fuelEnv);
|
|
66653
|
+
}
|
|
66143
66654
|
function isAgentStep(step) {
|
|
66144
66655
|
if (step.uses && AGENT_ACTION_RE.test(step.uses)) return true;
|
|
66145
|
-
if (step.
|
|
66656
|
+
if (step.uses && isWrapperAgent(step)) return true;
|
|
66657
|
+
if (typeof step.run !== "string" || !step.run) return false;
|
|
66658
|
+
if (/\b(claude|aider|codex|gemini)\b/i.test(step.run) && /github\.event/.test(step.run))
|
|
66146
66659
|
return true;
|
|
66147
|
-
return false;
|
|
66660
|
+
if (!AGENT_CLI_RE.test(step.run)) return false;
|
|
66661
|
+
const env = { ...step.scopeEnv ?? {}, ...step.env ?? {} };
|
|
66662
|
+
return !!step.scopeEnvEvent || EVENT_CONTEXT_RE.test(str(step.env)) || Object.keys(env).some((k) => RUN_FUEL_ENV_RE.test(k));
|
|
66148
66663
|
}
|
|
66149
66664
|
function allowedToolsFromArgs(claudeArgs) {
|
|
66150
66665
|
let out = "";
|
|
@@ -66164,12 +66679,47 @@ function collectTools(steps) {
|
|
|
66164
66679
|
let s = "";
|
|
66165
66680
|
for (const st of steps) {
|
|
66166
66681
|
const w = st.with ?? {};
|
|
66167
|
-
|
|
66168
|
-
s += " " +
|
|
66682
|
+
const args = str(w["claude_args"]) + " " + str(w["codex-args"]) + " " + str(w["codex_args"]);
|
|
66683
|
+
s += " " + allowedToolsFromArgs(args);
|
|
66684
|
+
s += " " + str(w["allowed_tools"]) + " " + str(w["allowedtools"]);
|
|
66169
66685
|
s += " " + allowedToolsFromSettings(str(w["settings"]));
|
|
66686
|
+
if (PERMISSION_BYPASS_RE.test(args) || /^danger-full-access$/i.test(str(w["sandbox"])))
|
|
66687
|
+
s += " Bash";
|
|
66688
|
+
if (st.run && isRunAgent(st)) {
|
|
66689
|
+
s += " " + allowedToolsFromArgs(st.run);
|
|
66690
|
+
if (PERMISSION_BYPASS_RE.test(st.run)) s += " Bash";
|
|
66691
|
+
s += " " + permissionEnvGrants({ ...st.scopeEnv ?? {}, ...st.env ?? {} });
|
|
66692
|
+
}
|
|
66170
66693
|
}
|
|
66171
66694
|
return s;
|
|
66172
66695
|
}
|
|
66696
|
+
var PERMISSION_BYPASS_RE = /--dangerously-skip-permissions|--permission-mode[=\s]+["']?bypassPermissions|--dangerously-bypass-approvals-and-sandbox|--allow-all-tools|--yolo\b/i;
|
|
66697
|
+
function permissionEnvGrants(env) {
|
|
66698
|
+
let out = "";
|
|
66699
|
+
for (const key of ["OPENCODE_PERMISSION", "KILO_PERMISSION"]) {
|
|
66700
|
+
const raw = str(env[key]);
|
|
66701
|
+
if (!raw || raw.length > 2e4) continue;
|
|
66702
|
+
let cfg;
|
|
66703
|
+
try {
|
|
66704
|
+
cfg = JSON.parse(raw);
|
|
66705
|
+
} catch {
|
|
66706
|
+
continue;
|
|
66707
|
+
}
|
|
66708
|
+
if (!cfg || typeof cfg !== "object") continue;
|
|
66709
|
+
const c = cfg;
|
|
66710
|
+
const bash = c["bash"];
|
|
66711
|
+
if (bash === "allow") out += " Bash";
|
|
66712
|
+
else if (bash && typeof bash === "object") {
|
|
66713
|
+
for (const [pat, v] of Object.entries(bash)) {
|
|
66714
|
+
if (v !== "allow") continue;
|
|
66715
|
+
const cmd = pat.replace(/\*+\s*$/, "").trim();
|
|
66716
|
+
out += cmd ? ` Bash(${cmd}:*)` : " Bash";
|
|
66717
|
+
}
|
|
66718
|
+
}
|
|
66719
|
+
if (c["webfetch"] === "allow") out += " WebFetch";
|
|
66720
|
+
}
|
|
66721
|
+
return out;
|
|
66722
|
+
}
|
|
66173
66723
|
function toolTokens(blob) {
|
|
66174
66724
|
const out = [];
|
|
66175
66725
|
let buf = "";
|
|
@@ -66202,11 +66752,21 @@ function matchedBroadTools(blob) {
|
|
|
66202
66752
|
}
|
|
66203
66753
|
return out;
|
|
66204
66754
|
}
|
|
66755
|
+
var GH_PR_CHECKOUT_RE = /\bgh\s+pr\s+checkout\b/;
|
|
66756
|
+
var FETCH_PULL_RE = /\bgit\s+fetch\b[^\n]{0,200}\bpull\/[^\s/]{1,80}\/(?:head|merge)\b/;
|
|
66757
|
+
var GIT_CHECKOUT_RE = /\bgit\s+(?:checkout|switch)\b/;
|
|
66758
|
+
function runChecksOutHead(run) {
|
|
66759
|
+
if (GH_PR_CHECKOUT_RE.test(run)) return true;
|
|
66760
|
+
const m = FETCH_PULL_RE.exec(run);
|
|
66761
|
+
return m !== null && GIT_CHECKOUT_RE.test(run.slice(m.index));
|
|
66762
|
+
}
|
|
66205
66763
|
function untrustedHeadCheckout(steps) {
|
|
66206
66764
|
for (const step of steps) {
|
|
66765
|
+
if (!step.uses && typeof step.run === "string" && runChecksOutHead(step.run)) return "root";
|
|
66207
66766
|
if (!step.uses || !/actions\/checkout/.test(step.uses)) continue;
|
|
66208
66767
|
const ref = str(step.with?.["ref"]);
|
|
66209
|
-
if (/
|
|
66768
|
+
if (/refs\/pull\//.test(ref)) return step.with?.["path"] ? "subdir" : "root";
|
|
66769
|
+
if (/pull_request\.head|head[._-]sha|head_ref|expected_head|workflow_run\.head|inputs(?:\.|\[\s*['"])[\w-]*head/i.test(
|
|
66210
66770
|
ref
|
|
66211
66771
|
)) {
|
|
66212
66772
|
return step.with?.["path"] ? "subdir" : "root";
|
|
@@ -66216,22 +66776,61 @@ function untrustedHeadCheckout(steps) {
|
|
|
66216
66776
|
}
|
|
66217
66777
|
function promptTakesUntrusted(steps) {
|
|
66218
66778
|
for (const st of steps) {
|
|
66219
|
-
const blob = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + collectTools([st])
|
|
66779
|
+
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.
|
|
66780
|
+
(isRunAgent(st) ? " " + st.run + " " + str(st.env) + " " + str(st.scopeEnv) : "");
|
|
66220
66781
|
if (/github\.event\.(issue|comment|pull_request|review)\.(body|title)/i.test(blob)) return true;
|
|
66221
|
-
if (/pull_request\.number
|
|
66782
|
+
if (/pull_request\.number|(?<![\w./-])\/(?:code-)?review(?![\w-])/i.test(blob)) return true;
|
|
66222
66783
|
}
|
|
66223
66784
|
return false;
|
|
66224
66785
|
}
|
|
66225
66786
|
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
66787
|
var ITEM_HANDLE_RE = /github\.event\.(issue|pull_request|discussion)\.number|github\.event\.number\b|github\.event\.comment\.id/i;
|
|
66227
66788
|
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;
|
|
66789
|
+
var RUN_ARTIFACT_RE = /gh\s+run\s+download\b|\/runs\/\$\{\{\s*github\.event\.workflow_run\.id\s*\}\}\/(?:artifacts|logs)/i;
|
|
66790
|
+
var TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
66791
|
+
var UPSTREAM_TRUSTED = false;
|
|
66792
|
+
function upstreamRunTrusted(wf, raw) {
|
|
66793
|
+
const on = onObject(wf, raw);
|
|
66794
|
+
const run = on?.["workflow_run"];
|
|
66795
|
+
const names = Array.isArray(run?.workflows) ? run.workflows.map(String) : [];
|
|
66796
|
+
return names.length > 0 && names.every((n) => TRUSTED_RUN_SOURCES.has(n));
|
|
66797
|
+
}
|
|
66798
|
+
function runSourceOf(path91, content, autoLabels) {
|
|
66799
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
66800
|
+
try {
|
|
66801
|
+
let raw;
|
|
66802
|
+
try {
|
|
66803
|
+
raw = loadWorkflow(content).raw;
|
|
66804
|
+
} catch {
|
|
66805
|
+
return null;
|
|
66806
|
+
}
|
|
66807
|
+
if (!raw || typeof raw !== "object") return null;
|
|
66808
|
+
const wf = raw;
|
|
66809
|
+
CALLERS.delete(wf);
|
|
66810
|
+
const reach = triggerReach(wf, raw);
|
|
66811
|
+
const name = typeof raw["name"] === "string" && raw["name"] ? raw["name"] : path91;
|
|
66812
|
+
return { name, trusted: !reach.untrusted && !reach.reusable };
|
|
66813
|
+
} finally {
|
|
66814
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66815
|
+
}
|
|
66816
|
+
}
|
|
66817
|
+
function downloadsRunArtifacts(st) {
|
|
66818
|
+
if (UPSTREAM_TRUSTED) return false;
|
|
66819
|
+
if (/actions\/download-artifact/i.test(str(st.uses)))
|
|
66820
|
+
return /workflow_run\.id/.test(str(st.with?.["run-id"]) + " " + str(st.with?.["run_id"]));
|
|
66821
|
+
return RUN_ARTIFACT_RE.test(str(st.run) + " " + str(st.with?.["script"]));
|
|
66822
|
+
}
|
|
66823
|
+
var STEP_OUTPUT_REF_RE = /steps\.([\w-]{1,100})\.outputs\./g;
|
|
66228
66824
|
function jobReadsUntrustedItem(job) {
|
|
66229
66825
|
const steps = job.steps ?? [];
|
|
66230
66826
|
const agent = steps.filter(isAgentStep);
|
|
66231
66827
|
if (!agent.length) return null;
|
|
66232
66828
|
if (ITEM_READ_TOOL_RE.test(collectTools(agent))) return "tool";
|
|
66233
66829
|
for (const st of agent)
|
|
66234
|
-
if (ITEM_HANDLE_RE.test(
|
|
66830
|
+
if (ITEM_HANDLE_RE.test(
|
|
66831
|
+
str(st.with) + " " + str(st.env) + " " + str(job.env) + " " + (isRunAgent(st) ? str(st.run) : "")
|
|
66832
|
+
))
|
|
66833
|
+
return "handle";
|
|
66235
66834
|
for (let i = 0; i < steps.length; i++) {
|
|
66236
66835
|
const st = steps[i];
|
|
66237
66836
|
if (!isAgentStep(st) || !str(st.with?.["prompt_file"] ?? st.with?.["prompt-file"])) continue;
|
|
@@ -66239,6 +66838,20 @@ function jobReadsUntrustedItem(job) {
|
|
|
66239
66838
|
if (earlier.some((p) => PAYLOAD_TEXT_RE.test(str(p.run) + " " + str(p.with) + " " + str(p.env))))
|
|
66240
66839
|
return "prompt-file";
|
|
66241
66840
|
}
|
|
66841
|
+
const firstDownload = steps.findIndex(downloadsRunArtifacts);
|
|
66842
|
+
if (firstDownload >= 0 && steps.slice(firstDownload + 1).some(isAgentStep)) return "artifact";
|
|
66843
|
+
for (let i = 0; i < steps.length; i++) {
|
|
66844
|
+
const st = steps[i];
|
|
66845
|
+
if (!isAgentStep(st)) continue;
|
|
66846
|
+
const input = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + str(st.with?.["prompt_file"] ?? st.with?.["prompt-file"]);
|
|
66847
|
+
const ids = /* @__PURE__ */ new Set();
|
|
66848
|
+
for (const m of input.matchAll(STEP_OUTPUT_REF_RE)) ids.add(m[1]);
|
|
66849
|
+
for (const id of ids) {
|
|
66850
|
+
const src = steps.slice(0, i).find((p) => p.id === id);
|
|
66851
|
+
if (src && PAYLOAD_TEXT_RE.test(str(src.run) + " " + str(src.with) + " " + str(src.env)))
|
|
66852
|
+
return "step-output";
|
|
66853
|
+
}
|
|
66854
|
+
}
|
|
66242
66855
|
return null;
|
|
66243
66856
|
}
|
|
66244
66857
|
var NONCONTAINS_GATE_RE = new RegExp(
|
|
@@ -66251,19 +66864,46 @@ var NONCONTAINS_GATE_RE = new RegExp(
|
|
|
66251
66864
|
].join("|"),
|
|
66252
66865
|
"i"
|
|
66253
66866
|
);
|
|
66254
|
-
var
|
|
66867
|
+
var PERMISSION_OUTPUT_RE = /steps\.[\w-]{1,100}\.outputs\.([\w-]{1,200})\s*==\s*['"]?(?:true|admin|write|maintain)/gi;
|
|
66868
|
+
var PERMISSION_OUTPUT_NAME_RE = /permission|allowed|authoriz|is[_-]?(?:admin|member|maintainer|collaborator)/i;
|
|
66869
|
+
var PERMISSION_OUTPUT_GATE_RE = {
|
|
66870
|
+
test(ifs) {
|
|
66871
|
+
for (const m of ifs.matchAll(PERMISSION_OUTPUT_RE))
|
|
66872
|
+
if (PERMISSION_OUTPUT_NAME_RE.test(m[1])) return true;
|
|
66873
|
+
return false;
|
|
66874
|
+
}
|
|
66875
|
+
};
|
|
66255
66876
|
var CONTAINS_GATE_RE = /contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
|
|
66256
66877
|
var NEGATED_CONTAINS_RE = /!\s*\(?\s*contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
|
|
66257
66878
|
function labelTypeConfigured(wf, raw) {
|
|
66258
66879
|
const on = wf.on ?? raw["on"] ?? raw[true];
|
|
66259
66880
|
const prTypes = (t) => t && Array.isArray(t.types) ? t.types.map(String) : [];
|
|
66260
|
-
|
|
66881
|
+
const pr = prTypes(on?.["pull_request_target"]).includes("labeled") || prTypes(on?.["pull_request"]).includes("labeled");
|
|
66882
|
+
const onObj = onObject(wf, raw) ?? {};
|
|
66883
|
+
const issues = on != null && typeof on === "object" && "issues" in on && triggerKeys(wf, raw).every((k) => /^issues$/i.test(k) || !keyStrangerFirable(onObj, k));
|
|
66884
|
+
return { pr, issues };
|
|
66885
|
+
}
|
|
66886
|
+
var EVENT_LABEL_GATE_RE = /\blabel\.name\s*==\s*'|\b(?:contains|startsWith)\(\s*github\.event\.label\.name\b/i;
|
|
66887
|
+
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;
|
|
66888
|
+
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;
|
|
66889
|
+
var NONWRITE_SET_RE = /contains\(\s*(?:fromjson\(\s*['"]\[[^\]]{0,400}|'[^']{0,400})\b(CONTRIBUTOR|FIRST_TIME_CONTRIBUTOR|FIRST_TIMER|NONE|MANNEQUIN)\b/i;
|
|
66890
|
+
function allIfText(wf) {
|
|
66891
|
+
const s = jobList(wf).map((j) => str(j.if) + " " + (j.steps ?? []).map((st) => str(st.if)).join(" ")).join(" ");
|
|
66892
|
+
return s.length > IF_MAX_LEN ? "" : s;
|
|
66261
66893
|
}
|
|
66262
|
-
|
|
66263
|
-
|
|
66264
|
-
const
|
|
66265
|
-
|
|
66266
|
-
|
|
66894
|
+
function testsAutoLabel(ifs) {
|
|
66895
|
+
if (AUTO_LABELS.size === 0) return false;
|
|
66896
|
+
for (const m of ifs.matchAll(
|
|
66897
|
+
/labels\.\*\.name\s*,\s*'([^']{1,100})'|label\.name\s*==\s*'([^']{1,100})'/gi
|
|
66898
|
+
))
|
|
66899
|
+
if (AUTO_LABELS.has((m[1] ?? m[2]).toLowerCase())) return true;
|
|
66900
|
+
return false;
|
|
66901
|
+
}
|
|
66902
|
+
function ifsAreGated(ifs, label3) {
|
|
66903
|
+
const m = maskLiterals(ifs);
|
|
66904
|
+
const containsGate = CONTAINS_GATE_RE.test(m) && !NEGATED_CONTAINS_RE.test(m) && !NONWRITE_SET_RE.test(ifs);
|
|
66905
|
+
const gated = NONCONTAINS_GATE_RE.test(m) || containsGate;
|
|
66906
|
+
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
66907
|
return gated || labelGated;
|
|
66268
66908
|
}
|
|
66269
66909
|
function hasStepMembershipGate(job) {
|
|
@@ -66284,6 +66924,7 @@ function parseIf(src) {
|
|
|
66284
66924
|
let e = src.trim();
|
|
66285
66925
|
const wrapped = /^\$\{\{([\s\S]*)\}\}$/.exec(e);
|
|
66286
66926
|
if (wrapped) e = wrapped[1].trim();
|
|
66927
|
+
if (e.includes("${{")) return null;
|
|
66287
66928
|
if (!e || e.length > IF_MAX_LEN) return null;
|
|
66288
66929
|
let i = 0;
|
|
66289
66930
|
let depth = 0;
|
|
@@ -66369,12 +67010,21 @@ function stepIfText(steps) {
|
|
|
66369
67010
|
const s = steps.map((st) => str(st.if)).join(" ");
|
|
66370
67011
|
return s.length > IF_MAX_LEN ? "" : s;
|
|
66371
67012
|
}
|
|
67013
|
+
function maskLiterals(ifs) {
|
|
67014
|
+
return ifs.replace(
|
|
67015
|
+
/(==|!=)?(\s*)'(?:[^']|'')*'/g,
|
|
67016
|
+
(m, op, sp) => op ? m : `${sp}'_'`
|
|
67017
|
+
);
|
|
67018
|
+
}
|
|
67019
|
+
var CHAINED_COMPARE_RE = /(?:==|!=)[^=!]*(?:==|!=)/;
|
|
66372
67020
|
function gatedText(ifs, labelConfigured) {
|
|
66373
|
-
|
|
67021
|
+
const masked = maskLiterals(ifs);
|
|
67022
|
+
return ifsAreGated(ifs, labelConfigured) || ASSIGNEE_GATE_RE.test(masked) || PERMISSION_OUTPUT_GATE_RE.test(masked);
|
|
66374
67023
|
}
|
|
66375
67024
|
function exprGated(x, labelConfigured) {
|
|
66376
67025
|
switch (x.k) {
|
|
66377
67026
|
case "atom":
|
|
67027
|
+
if (CHAINED_COMPARE_RE.test(maskLiterals(x.t))) return false;
|
|
66378
67028
|
return gatedText(x.t, labelConfigured) || TRUSTED_EVENT_ATOM_RE.test(x.t);
|
|
66379
67029
|
case "not":
|
|
66380
67030
|
return false;
|
|
@@ -66410,11 +67060,43 @@ function jobOnWorkflowRun(job, workflowRun) {
|
|
|
66410
67060
|
}
|
|
66411
67061
|
function jobActorGate(job, wf, raw) {
|
|
66412
67062
|
const labelConfigured = labelTypeConfigured(wf, raw);
|
|
66413
|
-
const
|
|
66414
|
-
|
|
67063
|
+
const agent = (job.steps ?? []).filter(isAgentStep);
|
|
67064
|
+
const ifTotal = agent.reduce((n, s) => n + str(s.if).length, 0);
|
|
67065
|
+
const agentStepsGated = agent.length > 0 && ifTotal <= IF_MAX_LEN && agent.every((s) => {
|
|
67066
|
+
const x = parseIf(str(s.if));
|
|
67067
|
+
return x !== null && exprGated(x, labelConfigured);
|
|
67068
|
+
});
|
|
67069
|
+
return jobIfGated(str(job.if), labelConfigured) || agentStepsGated || hasStepMembershipGate(job);
|
|
67070
|
+
}
|
|
67071
|
+
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*['"]/;
|
|
67072
|
+
var ACTOR_ALLOWLIST_G = new RegExp(ACTOR_ALLOWLIST_RE.source, "g");
|
|
67073
|
+
var BLOCK_LIST_NAME_RE = /block|deny|ban|spam|abuse|exclu|ignore/i;
|
|
67074
|
+
function scriptAllowsByActor(code2) {
|
|
67075
|
+
if (code2.length > 2e5) return false;
|
|
67076
|
+
for (const m of code2.matchAll(ACTOR_ALLOWLIST_G)) {
|
|
67077
|
+
const at = m.index ?? 0;
|
|
67078
|
+
const before = code2.slice(Math.max(0, at - 80), at);
|
|
67079
|
+
if (m[0].startsWith(".includes")) {
|
|
67080
|
+
const list = /([\w$.]{1,60})$/.exec(before)?.[1] ?? "";
|
|
67081
|
+
if (BLOCK_LIST_NAME_RE.test(list)) continue;
|
|
67082
|
+
}
|
|
67083
|
+
const negated = /!\s{0,3}\(?\s{0,3}[\w$.]{0,60}$/.test(before);
|
|
67084
|
+
const ternary = /^[^?;\n]{0,150}\?\s{0,5}['"`]?(true|false)\b/.exec(code2.slice(at, at + 200));
|
|
67085
|
+
if (!ternary || ternary[1] === "true" !== negated) return true;
|
|
67086
|
+
}
|
|
67087
|
+
return false;
|
|
67088
|
+
}
|
|
67089
|
+
function readsOutputPositively(readerIf, name) {
|
|
67090
|
+
const ref = `needs\\.${escapeRe(name)}\\.outputs\\.[\\w-]{1,100}`;
|
|
67091
|
+
if (!new RegExp(ref).test(readerIf)) return false;
|
|
67092
|
+
const no = `'(?:false|no|0|)'(?![\\w'])`;
|
|
67093
|
+
return !new RegExp(
|
|
67094
|
+
`!\\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})`,
|
|
67095
|
+
"i"
|
|
67096
|
+
).test(readerIf);
|
|
66415
67097
|
}
|
|
66416
67098
|
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]/;
|
|
67099
|
+
var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]|actions\.cancelWorkflowRun\b/;
|
|
66418
67100
|
function needsOf(job) {
|
|
66419
67101
|
return (Array.isArray(job.needs) ? job.needs : job.needs ? [job.needs] : []).map(String);
|
|
66420
67102
|
}
|
|
@@ -66426,13 +67108,18 @@ function upstreamJobGates(name, dep, wf, raw, readerIf) {
|
|
|
66426
67108
|
const label3 = labelTypeConfigured(wf, raw);
|
|
66427
67109
|
if (depIf && jobIfGated(depIf, label3)) return true;
|
|
66428
67110
|
const steps = dep.steps ?? [];
|
|
66429
|
-
|
|
67111
|
+
if (readerIf !== null && readsOutputPositively(readerIf, name) && steps.some((st) => {
|
|
67112
|
+
const code2 = str(st.run) + " " + str(st.with?.["script"]);
|
|
67113
|
+
return MEMBERSHIP_CHECK_RE.test(code2) || scriptAllowsByActor(code2);
|
|
67114
|
+
}))
|
|
67115
|
+
return true;
|
|
67116
|
+
const stepGated = ifsAreGated(stepIfText(steps), label3) || hasStepMembershipGate(dep) || steps.some((st) => MEMBERSHIP_CHECK_RE.test(str(st.with?.["script"])));
|
|
66430
67117
|
if (!stepGated) return false;
|
|
66431
67118
|
if (steps.some(
|
|
66432
67119
|
(st) => FAILING_STEP_RE.test(str(st.run)) || FAILING_STEP_RE.test(str(st.with?.["script"]))
|
|
66433
67120
|
))
|
|
66434
67121
|
return true;
|
|
66435
|
-
return readerIf !== null &&
|
|
67122
|
+
return readerIf !== null && readsOutputPositively(readerIf, name);
|
|
66436
67123
|
}
|
|
66437
67124
|
function gatingAncestors(job, wf, raw) {
|
|
66438
67125
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -66462,16 +67149,23 @@ function jobGated(job, wf, raw) {
|
|
|
66462
67149
|
}
|
|
66463
67150
|
function bypassArmed(steps) {
|
|
66464
67151
|
return steps.some(
|
|
66465
|
-
(s) => str(s.with?.["allowed_non_write_users"])
|
|
67152
|
+
(s) => nonWriteOpen(str(s.with?.["allowed_non_write_users"])) && (!!str(s.with?.["github_token"]) || !!str(s.env?.["OVERRIDE_GITHUB_TOKEN"]))
|
|
66466
67153
|
);
|
|
66467
67154
|
}
|
|
67155
|
+
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;
|
|
67156
|
+
function nonWriteOpen(v) {
|
|
67157
|
+
const t = v.trim();
|
|
67158
|
+
if (t === "*") return true;
|
|
67159
|
+
return t.length <= 1e3 && ACTOR_BOUND_RE.test(t);
|
|
67160
|
+
}
|
|
66468
67161
|
function hasImplicitActorGate(agentSteps, bypassActive, workflowRun = false) {
|
|
66469
67162
|
if (bypassActive) return false;
|
|
66470
67163
|
return agentSteps.some((s) => {
|
|
66471
67164
|
const build = claudeActionBuild(s.uses);
|
|
66472
67165
|
if (!build) return false;
|
|
67166
|
+
if (!build.official) return false;
|
|
66473
67167
|
if (!workflowRun) return true;
|
|
66474
|
-
return
|
|
67168
|
+
return workflowRunChecked(build.ref);
|
|
66475
67169
|
});
|
|
66476
67170
|
}
|
|
66477
67171
|
function claudeActionBuild(uses) {
|
|
@@ -66583,14 +67277,28 @@ function severityFromScore(score) {
|
|
|
66583
67277
|
if (score >= 1) return "advisory";
|
|
66584
67278
|
return null;
|
|
66585
67279
|
}
|
|
66586
|
-
function analyzeWorkflow(path91, content) {
|
|
67280
|
+
function analyzeWorkflow(path91, content, callers, autoLabels, trustedRunSources) {
|
|
67281
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67282
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67283
|
+
try {
|
|
67284
|
+
return analyzeWorkflowInner(path91, content, callers);
|
|
67285
|
+
} finally {
|
|
67286
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67287
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
67288
|
+
UPSTREAM_TRUSTED = false;
|
|
67289
|
+
}
|
|
67290
|
+
}
|
|
67291
|
+
function analyzeWorkflowInner(path91, content, callers) {
|
|
66587
67292
|
let raw;
|
|
66588
67293
|
try {
|
|
66589
|
-
raw =
|
|
67294
|
+
raw = loadWorkflow(content).raw;
|
|
66590
67295
|
} catch {
|
|
66591
67296
|
return null;
|
|
66592
67297
|
}
|
|
66593
67298
|
const wf = raw;
|
|
67299
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67300
|
+
else CALLERS.delete(wf);
|
|
67301
|
+
UPSTREAM_TRUSTED = upstreamRunTrusted(wf, raw);
|
|
66594
67302
|
const steps = allSteps(wf).map((s) => s.step);
|
|
66595
67303
|
const agentSteps = steps.filter(isAgentStep);
|
|
66596
67304
|
const agentJobs = [
|
|
@@ -66609,17 +67317,19 @@ function analyzeWorkflow(path91, content) {
|
|
|
66609
67317
|
} = triggerReach(wf, raw);
|
|
66610
67318
|
const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
|
|
66611
67319
|
const nonWriteStar = bypassArmed(agentSteps);
|
|
66612
|
-
const starWithoutToken = nonWrite
|
|
66613
|
-
const nonWriteList = !!nonWrite && nonWrite
|
|
67320
|
+
const starWithoutToken = nonWriteOpen(nonWrite) && !nonWriteStar;
|
|
67321
|
+
const nonWriteList = !!nonWrite && !nonWriteOpen(nonWrite);
|
|
67322
|
+
const nonWriteActor = nonWrite.trim() !== "*" && nonWriteOpen(nonWrite);
|
|
66614
67323
|
const untrustedTrigger = forkInput || reusable;
|
|
66615
67324
|
const bypassActive = nonWriteStar && untrustedTrigger;
|
|
66616
|
-
const head = untrustedHeadCheckout(steps);
|
|
67325
|
+
const head = agentSteps.length && agentSteps.every(readOnlySandbox) ? null : untrustedHeadCheckout(steps);
|
|
66617
67326
|
const promptUntrusted = promptTakesUntrusted(agentSteps);
|
|
66618
67327
|
const injJobs = injectableJobs(wf, raw, untrustedTrigger);
|
|
66619
67328
|
const powerJobs = injJobs.length ? injJobs : agentJobs;
|
|
66620
67329
|
const powerSteps = injJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep));
|
|
66621
67330
|
const scopedSteps = powerSteps.length ? powerSteps : agentSteps;
|
|
66622
|
-
const
|
|
67331
|
+
const headActionRuns = head === "root" && powerSteps.some((st) => /^\s*\.\//.test(str(st.uses)));
|
|
67332
|
+
const toolsBlob = collectTools(scopedSteps) + (headActionRuns ? " Bash" : "");
|
|
66623
67333
|
const itemRead = forkInput ? injJobs.map(jobReadsUntrustedItem).find(Boolean) ?? null : null;
|
|
66624
67334
|
const reach = untrustedTrigger && injJobs.length ? Math.max(
|
|
66625
67335
|
head === "root" ? 3 : head === "subdir" ? 1 : 0,
|
|
@@ -66667,28 +67377,40 @@ function analyzeWorkflow(path91, content) {
|
|
|
66667
67377
|
severity = "medium";
|
|
66668
67378
|
if (!severity) severity = "advisory";
|
|
66669
67379
|
const signals = [];
|
|
67380
|
+
const openCallers = (callers ?? []).filter((c) => c.untrusted && !c.gated && !c.dispatched);
|
|
67381
|
+
if (openCallers.length)
|
|
67382
|
+
signals.push(
|
|
67383
|
+
`called by ${openCallers.map((c) => `${safeText(c.file, 120)} (${c.keys.map((k) => safeText(k, 40)).join(", ")})`).join("; ")}`
|
|
67384
|
+
);
|
|
66670
67385
|
if (secretExposed)
|
|
66671
67386
|
signals.push(
|
|
66672
|
-
`runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).join(", ")})`
|
|
67387
|
+
`runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).map((t) => safeText(t, 40)).join(", ")})`
|
|
67388
|
+
);
|
|
67389
|
+
else if (forkInput)
|
|
67390
|
+
signals.push(
|
|
67391
|
+
`triggered by untrusted input (${triggers.map((t) => safeText(t, 40)).join(", ")})`
|
|
66673
67392
|
);
|
|
66674
|
-
else if (forkInput) signals.push(`triggered by untrusted input (${triggers.join(", ")})`);
|
|
66675
67393
|
else if (reusable)
|
|
66676
67394
|
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`
|
|
67395
|
+
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
67396
|
);
|
|
66679
67397
|
if (forkInput && !privileged && !reusable)
|
|
66680
67398
|
signals.push(
|
|
66681
67399
|
"triggered by `pull_request`: fork PRs run with a read-only token (lower risk than pull_request_target)"
|
|
66682
67400
|
);
|
|
66683
67401
|
if (head === "root") signals.push("checks out the untrusted PR head into the workspace root");
|
|
67402
|
+
if (headActionRuns)
|
|
67403
|
+
signals.push(
|
|
67404
|
+
"runs the checked-out head's own action (`uses: ./\u2026`): the PR author's code runs with the job's secrets"
|
|
67405
|
+
);
|
|
66684
67406
|
if (head === "subdir") signals.push("checks out the untrusted PR head into an isolated subdir");
|
|
67407
|
+
const thirdParty = agentSteps.map((st) => claudeActionBuild(st.uses)).find((b) => b !== null && !b.official);
|
|
67408
|
+
if (thirdParty && !implicitGate)
|
|
67409
|
+
signals.push(
|
|
67410
|
+
`third-party build of claude-code-action (\`${safeText(thirdParty.owner, 40)}\`): not assumed to check the actor's write access like the official action`
|
|
67411
|
+
);
|
|
66685
67412
|
if (workflowRunJobs.length) {
|
|
66686
67413
|
const builds = workflowRunJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep)).map((st) => claudeActionBuild(st.uses)).filter((b) => b !== null);
|
|
66687
|
-
const thirdParty = builds.find((b) => !b.official);
|
|
66688
|
-
if (thirdParty)
|
|
66689
|
-
signals.push(
|
|
66690
|
-
`third-party build of claude-code-action (\`${safeText(thirdParty.owner, 40)}\`): on workflow_run it is not assumed to check the actor (the official action does from v1.0.185)`
|
|
66691
|
-
);
|
|
66692
67414
|
const old = builds.find((b) => b.official && !workflowRunChecked(b.ref));
|
|
66693
67415
|
if (old)
|
|
66694
67416
|
signals.push(
|
|
@@ -66702,7 +67424,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66702
67424
|
if (promptUntrusted) signals.push("feeds untrusted PR/issue text to the agent");
|
|
66703
67425
|
else if (itemRead)
|
|
66704
67426
|
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)"
|
|
67427
|
+
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
67428
|
);
|
|
66707
67429
|
if (broadTools) {
|
|
66708
67430
|
const names = matchedBroadTools(toolsBlob);
|
|
@@ -66712,7 +67434,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66712
67434
|
}
|
|
66713
67435
|
if (bypassActive)
|
|
66714
67436
|
signals.push(
|
|
66715
|
-
'allowed_non_write_users: "*" with github_token, so any user can trigger the agent'
|
|
67437
|
+
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
67438
|
);
|
|
66717
67439
|
if (elevated) signals.push("elevated permissions (contents/id-token: write)");
|
|
66718
67440
|
if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
|
|
@@ -66806,7 +67528,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66806
67528
|
return finding;
|
|
66807
67529
|
}
|
|
66808
67530
|
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 = /^(
|
|
67531
|
+
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
67532
|
function fuelSecretNames(agentSteps) {
|
|
66811
67533
|
const out = /* @__PURE__ */ new Set();
|
|
66812
67534
|
const add = (v) => {
|
|
@@ -66879,14 +67601,28 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
|
|
|
66879
67601
|
if (reusable && !loadedGun && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
|
|
66880
67602
|
return { severity, secrets, injectable, canReadEnv };
|
|
66881
67603
|
}
|
|
66882
|
-
function analyzeWorkflowSecrets(path91, content) {
|
|
67604
|
+
function analyzeWorkflowSecrets(path91, content, callers, autoLabels, trustedRunSources) {
|
|
67605
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67606
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67607
|
+
try {
|
|
67608
|
+
return analyzeWorkflowSecretsInner(path91, content, callers);
|
|
67609
|
+
} finally {
|
|
67610
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67611
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
67612
|
+
UPSTREAM_TRUSTED = false;
|
|
67613
|
+
}
|
|
67614
|
+
}
|
|
67615
|
+
function analyzeWorkflowSecretsInner(path91, content, callers) {
|
|
66883
67616
|
let raw;
|
|
66884
67617
|
try {
|
|
66885
|
-
raw =
|
|
67618
|
+
raw = loadWorkflow(content).raw;
|
|
66886
67619
|
} catch {
|
|
66887
67620
|
return null;
|
|
66888
67621
|
}
|
|
66889
67622
|
const wf = raw;
|
|
67623
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67624
|
+
else CALLERS.delete(wf);
|
|
67625
|
+
UPSTREAM_TRUSTED = upstreamRunTrusted(wf, raw);
|
|
66890
67626
|
const all = allSteps(wf);
|
|
66891
67627
|
if (!all.some((s) => isAgentStep(s.step))) return null;
|
|
66892
67628
|
const { untrusted: forkInput, reusable } = triggerReach(wf, raw);
|
|
@@ -66918,70 +67654,285 @@ function analyzeWorkflowSecrets(path91, content) {
|
|
|
66918
67654
|
})
|
|
66919
67655
|
};
|
|
66920
67656
|
}
|
|
66921
|
-
|
|
66922
|
-
|
|
66923
|
-
|
|
66924
|
-
|
|
66925
|
-
|
|
66926
|
-
|
|
66927
|
-
|
|
66928
|
-
|
|
66929
|
-
|
|
66930
|
-
if (!close) return null;
|
|
66931
|
-
const block = text.slice(3, 3 + close.index);
|
|
67657
|
+
var COMMIT_PIN_RE = /[#@][0-9a-f]{40}\b/i;
|
|
67658
|
+
function marketplaceOwner(market) {
|
|
67659
|
+
if (/(^|[/\\])\.\.([/\\]|$)/.test(market)) return void 0;
|
|
67660
|
+
return /^(?:https?:\/\/(?:www\.)?|git@|ssh:\/\/git@)?github\.com[:/]([\w.-]{1,100})\/[\w.-]{1,100}(?:\.git)?\/?(?:[#@].*)?$/i.exec(
|
|
67661
|
+
market
|
|
67662
|
+
)?.[1] ?? /^([\w.-]{1,100})\/[\w.-]{1,100}(?:[#@].*)?$/.exec(market)?.[1];
|
|
67663
|
+
}
|
|
67664
|
+
function analyzeWorkflowPlugins(path91, content, owner) {
|
|
67665
|
+
let raw;
|
|
66932
67666
|
try {
|
|
66933
|
-
|
|
66934
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
67667
|
+
raw = loadWorkflow(content).raw;
|
|
66935
67668
|
} catch {
|
|
66936
|
-
return
|
|
67669
|
+
return [];
|
|
67670
|
+
}
|
|
67671
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67672
|
+
const wf = raw;
|
|
67673
|
+
const out = [];
|
|
67674
|
+
const seen = /* @__PURE__ */ new Set();
|
|
67675
|
+
for (const { step } of allSteps(wf)) {
|
|
67676
|
+
if (!claudeActionBuild(step.uses)) continue;
|
|
67677
|
+
if (!str(step.with?.["plugins"]).trim()) continue;
|
|
67678
|
+
for (const m of str(step.with?.["plugin_marketplaces"]).split(/[\s,]+/)) {
|
|
67679
|
+
const market = m.trim();
|
|
67680
|
+
if (!market || COMMIT_PIN_RE.test(market)) continue;
|
|
67681
|
+
if (/^\.{1,2}(\/|$)/.test(market)) continue;
|
|
67682
|
+
const mo = marketplaceOwner(market);
|
|
67683
|
+
if (mo && mo.toLowerCase() === "anthropics") continue;
|
|
67684
|
+
if (owner && mo && mo.toLowerCase() === owner.toLowerCase()) continue;
|
|
67685
|
+
if (seen.has(market)) continue;
|
|
67686
|
+
seen.add(market);
|
|
67687
|
+
out.push({
|
|
67688
|
+
check: "CI-3",
|
|
67689
|
+
rule: "CI-3.plugin-unpinned",
|
|
67690
|
+
locator: safeText(market, 200),
|
|
67691
|
+
dimension: "mcp",
|
|
67692
|
+
severity: "medium",
|
|
67693
|
+
title: "Agent installs plugins from a third-party marketplace on every run",
|
|
67694
|
+
file: path91,
|
|
67695
|
+
signals: [
|
|
67696
|
+
`\`plugin_marketplaces: ${safeText(market, 120)}\` is not pinned to a commit: whoever controls it changes the code and prompts the agent runs`
|
|
67697
|
+
],
|
|
67698
|
+
fix: "Pin the marketplace to a commit (`<url>#<sha>`), or vendor the plugin into this repository."
|
|
67699
|
+
});
|
|
67700
|
+
}
|
|
66937
67701
|
}
|
|
67702
|
+
return out;
|
|
66938
67703
|
}
|
|
66939
|
-
|
|
66940
|
-
|
|
66941
|
-
|
|
66942
|
-
|
|
66943
|
-
|
|
66944
|
-
|
|
66945
|
-
|
|
66946
|
-
|
|
66947
|
-
|
|
66948
|
-
|
|
67704
|
+
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;
|
|
67705
|
+
var WEB_READ_RE = /(^|["\s,])(WebFetch|WebSearch)(\(|\s|,|"|$)/;
|
|
67706
|
+
function readsItemList(text) {
|
|
67707
|
+
if (text.length > 2e5) return ITEM_LIST_RE.test(text);
|
|
67708
|
+
return text.replace(/\\\r?\n/g, " ").split("\n").some((l) => ITEM_LIST_RE.test(l) && !sanitizedListing(l));
|
|
67709
|
+
}
|
|
67710
|
+
function analyzeTrustedReaders(path91, content, autoLabels, callers, trustedRunSources) {
|
|
67711
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67712
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67713
|
+
try {
|
|
67714
|
+
return analyzeTrustedReadersInner(path91, content, callers);
|
|
67715
|
+
} finally {
|
|
67716
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67717
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
67718
|
+
}
|
|
67719
|
+
}
|
|
67720
|
+
function keysWithCallers(keys, callers) {
|
|
67721
|
+
return [.../* @__PURE__ */ new Set([...keys, ...(callers ?? []).flatMap((c) => c.keys)])];
|
|
67722
|
+
}
|
|
67723
|
+
function analyzeTrustedReadersInner(path91, content, callers) {
|
|
67724
|
+
let raw;
|
|
67725
|
+
try {
|
|
67726
|
+
raw = loadWorkflow(content).raw;
|
|
67727
|
+
} catch {
|
|
67728
|
+
return [];
|
|
67729
|
+
}
|
|
67730
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67731
|
+
const wf = raw;
|
|
67732
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67733
|
+
else CALLERS.delete(wf);
|
|
67734
|
+
const reach = triggerReach(wf, raw);
|
|
67735
|
+
if (reach.untrusted || reach.reusable) return [];
|
|
67736
|
+
const keys = keysWithCallers(reach.keys, callers);
|
|
67737
|
+
if (!keys.some((k) => /^schedule$/i.test(k))) return [];
|
|
67738
|
+
for (const job of jobList(wf)) {
|
|
67739
|
+
const jobIf = parseIf(str(job.if));
|
|
67740
|
+
if (jobIf !== null && !exprAllowsEvent(jobIf, "schedule")) continue;
|
|
67741
|
+
const steps = job.steps ?? [];
|
|
67742
|
+
const agentIdx = steps.findIndex(isAgentStep);
|
|
67743
|
+
if (agentIdx < 0) continue;
|
|
67744
|
+
const agent = steps.filter(isAgentStep);
|
|
67745
|
+
const tools = collectTools(agent);
|
|
67746
|
+
const promptText = agent.map(
|
|
67747
|
+
(st) => str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + (isRunAgent(st) ? str(st.run) : "")
|
|
67748
|
+
).join(" ");
|
|
67749
|
+
const earlier = steps.slice(0, agentIdx).filter((st) => !isAgentStep(st)).map((st) => str(st.run) + " " + str(st.with?.["script"]));
|
|
67750
|
+
const reads = [];
|
|
67751
|
+
if (ITEM_READ_TOOL_RE.test(tools) || readsItemList(promptText))
|
|
67752
|
+
reads.push("reads open issues or pull requests");
|
|
67753
|
+
else if (earlier.some(readsItemList))
|
|
67754
|
+
reads.push("an earlier step lists open issues or pull requests for it");
|
|
67755
|
+
if (WEB_READ_RE.test(tools)) reads.push("fetches third-party web content");
|
|
67756
|
+
if (!reads.length) continue;
|
|
67757
|
+
const withheld = agent.every(
|
|
67758
|
+
(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() === ""
|
|
67759
|
+
);
|
|
67760
|
+
const checkouts = steps.slice(0, agentIdx).filter((st) => /actions\/checkout/i.test(str(st.uses)));
|
|
67761
|
+
const persisted = checkouts.some((st) => str(st.with?.["persist-credentials"]) !== "false");
|
|
67762
|
+
const checkoutPat = checkouts.some(
|
|
67763
|
+
(st) => /secrets\.(?!GITHUB_TOKEN\b)[A-Za-z_]/i.test(str(st.with?.["token"]))
|
|
67764
|
+
);
|
|
67765
|
+
const pat = usesPatIn([job]) || checkoutPat;
|
|
67766
|
+
const permWrite = jobsHaveWrite(
|
|
67767
|
+
wf,
|
|
67768
|
+
[job],
|
|
67769
|
+
/["']?(contents|issues|pull-requests)["']?\s*:\s*["']?write/i
|
|
67770
|
+
);
|
|
67771
|
+
const handsToken = agent.some(
|
|
67772
|
+
(st) => !!claudeActionBuild(st.uses) || !!str(st.with?.["github_token"]).trim()
|
|
67773
|
+
);
|
|
67774
|
+
const write2 = pat || permWrite && !withheld && (handsToken || persisted);
|
|
67775
|
+
if (!write2) continue;
|
|
67776
|
+
const writes = pat ? "a static PAT" : "a write token";
|
|
67777
|
+
return [
|
|
67778
|
+
{
|
|
67779
|
+
check: "CI-2",
|
|
67780
|
+
rule: "CI-2.trusted-trigger-reader",
|
|
67781
|
+
dimension: "workflows",
|
|
67782
|
+
severity: "advisory",
|
|
67783
|
+
title: "Agent on a trusted trigger reads content strangers write, with a write token",
|
|
67784
|
+
file: path91,
|
|
67785
|
+
signals: [
|
|
67786
|
+
`runs on ${keys.map((k) => safeText(k, 40)).join(", ")}: no stranger starts it`,
|
|
67787
|
+
...reads.map((r) => `the agent ${r}`),
|
|
67788
|
+
`it holds ${writes}: injected text in that content can act with it`
|
|
67789
|
+
],
|
|
67790
|
+
fix: "Give the agent a read-only token and apply its output in a separate step, or filter the items to trusted authors first."
|
|
67791
|
+
}
|
|
67792
|
+
];
|
|
67793
|
+
}
|
|
67794
|
+
return [];
|
|
67795
|
+
}
|
|
67796
|
+
var FORK_PR_TRIGGER_RE = /^(pull_request_target|issue_comment|workflow_run)$/i;
|
|
67797
|
+
var SAME_REPO_GATE_RE = new RegExp(
|
|
67798
|
+
[
|
|
67799
|
+
String.raw`head(?:\.repo|_repository)\.full_name\s*==\s*github\.(?:repository\b|event\.(?:pull_request\.base\.repo|repository)\.full_name)`,
|
|
67800
|
+
String.raw`github\.repository\s*==\s*github\.event\.(?:pull_request\.head\.repo|workflow_run\.head_repository)\.full_name`,
|
|
67801
|
+
String.raw`head(?:\.repo|_repository)\.fork\s*==\s*false`,
|
|
67802
|
+
String.raw`head(?:\.repo|_repository)\.owner\.login\s*==\s*github\.repository_owner`,
|
|
67803
|
+
String.raw`workflow_run\.pull_requests\[0\]`
|
|
67804
|
+
].join("|"),
|
|
67805
|
+
"i"
|
|
67806
|
+
);
|
|
67807
|
+
var NOT_FORK_ATOM_RE = /^github\.event\.(?:pull_request\.head\.repo|workflow_run\.head_repository)\.fork$/i;
|
|
67808
|
+
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;
|
|
67809
|
+
function requiresAtom(src, re, events, notRe = NOT_FORK_ATOM_RE) {
|
|
67810
|
+
const x = parseIf(src);
|
|
67811
|
+
if (!x) return false;
|
|
67812
|
+
const req = (e) => {
|
|
67813
|
+
if (events?.length && events.every((ev) => !exprAllowsEvent(e, ev))) return true;
|
|
67814
|
+
switch (e.k) {
|
|
67815
|
+
case "atom": {
|
|
67816
|
+
const m = maskLiterals(e.t);
|
|
67817
|
+
return re.test(m) && !CHAINED_COMPARE_RE.test(m);
|
|
67818
|
+
}
|
|
67819
|
+
case "not":
|
|
67820
|
+
return e.c.k === "atom" && notRe.test(e.c.t.trim());
|
|
67821
|
+
case "and":
|
|
67822
|
+
return e.c.some(req);
|
|
67823
|
+
case "or":
|
|
67824
|
+
return e.c.every(req);
|
|
66949
67825
|
}
|
|
66950
|
-
|
|
66951
|
-
|
|
66952
|
-
|
|
67826
|
+
};
|
|
67827
|
+
return req(x);
|
|
67828
|
+
}
|
|
67829
|
+
var PR_EVENTS = [
|
|
67830
|
+
"pull_request_target",
|
|
67831
|
+
"pull_request",
|
|
67832
|
+
"pull_request_review",
|
|
67833
|
+
"pull_request_review_comment"
|
|
67834
|
+
];
|
|
67835
|
+
function headRefEvents(heads) {
|
|
67836
|
+
const evs = /* @__PURE__ */ new Set();
|
|
67837
|
+
for (const st of heads) {
|
|
67838
|
+
if (st.uses == null) return null;
|
|
67839
|
+
const m = /^\s*\$\{\{([\s\S]{1,500})\}\}\s*$/.exec(str(st.with?.["ref"]));
|
|
67840
|
+
if (!m) return null;
|
|
67841
|
+
for (const part of m[1].split("||").map((x) => x.trim())) {
|
|
67842
|
+
if (/^github\.event\.workflow_run\.[\w.]+$/.test(part)) evs.add("workflow_run");
|
|
67843
|
+
else if (/^github\.event\.pull_request\.[\w.]+$/.test(part))
|
|
67844
|
+
PR_EVENTS.forEach((e) => evs.add(e));
|
|
67845
|
+
else if (!/^(?:github\.(?:sha|ref|ref_name|event\.repository\.default_branch)|'[^']*')$/.test(part))
|
|
67846
|
+
return null;
|
|
66953
67847
|
}
|
|
66954
|
-
if (items.length) out[key] = items;
|
|
66955
67848
|
}
|
|
66956
|
-
return
|
|
67849
|
+
return evs.size ? [...evs] : null;
|
|
66957
67850
|
}
|
|
66958
|
-
function
|
|
66959
|
-
const
|
|
66960
|
-
|
|
66961
|
-
|
|
66962
|
-
|
|
66963
|
-
|
|
66964
|
-
|
|
66965
|
-
|
|
66966
|
-
|
|
66967
|
-
|
|
66968
|
-
|
|
67851
|
+
function readOnlySandbox(st) {
|
|
67852
|
+
const args = str(st.with?.["codex-args"]) + " " + str(st.with?.["codex_args"]);
|
|
67853
|
+
return /^read-only$/i.test(str(st.with?.["sandbox"])) && !PERMISSION_BYPASS_RE.test(args);
|
|
67854
|
+
}
|
|
67855
|
+
function requiresTrustedAuthor(src) {
|
|
67856
|
+
const x = parseIf(src);
|
|
67857
|
+
if (!x) return false;
|
|
67858
|
+
const req = (e) => {
|
|
67859
|
+
switch (e.k) {
|
|
67860
|
+
case "atom":
|
|
67861
|
+
return TRUSTED_AUTHOR_GATE_RE.test(e.t) && !NONWRITE_SET_RE.test(e.t) && !CHAINED_COMPARE_RE.test(maskLiterals(e.t));
|
|
67862
|
+
case "not":
|
|
67863
|
+
return false;
|
|
67864
|
+
case "and":
|
|
67865
|
+
return e.c.some(req);
|
|
67866
|
+
case "or":
|
|
67867
|
+
return e.c.every(req);
|
|
66969
67868
|
}
|
|
66970
|
-
|
|
67869
|
+
};
|
|
67870
|
+
return req(x);
|
|
67871
|
+
}
|
|
67872
|
+
function analyzeForkCodeOnTrust(path91, content, callers, autoLabels, trustedRunSources) {
|
|
67873
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67874
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67875
|
+
try {
|
|
67876
|
+
return analyzeForkCodeOnTrustInner(path91, content, callers);
|
|
67877
|
+
} finally {
|
|
67878
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67879
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
66971
67880
|
}
|
|
66972
|
-
if (cur) out.push(cur);
|
|
66973
|
-
return out;
|
|
66974
67881
|
}
|
|
66975
|
-
function
|
|
66976
|
-
|
|
66977
|
-
|
|
66978
|
-
|
|
66979
|
-
|
|
67882
|
+
function analyzeForkCodeOnTrustInner(path91, content, callers) {
|
|
67883
|
+
let raw;
|
|
67884
|
+
try {
|
|
67885
|
+
raw = loadWorkflow(content).raw;
|
|
67886
|
+
} catch {
|
|
67887
|
+
return [];
|
|
67888
|
+
}
|
|
67889
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67890
|
+
const wf = raw;
|
|
67891
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67892
|
+
else CALLERS.delete(wf);
|
|
67893
|
+
const reach = triggerReach(wf, raw);
|
|
67894
|
+
const forkKeys = keysWithCallers(reach.keys, callers).filter((k) => FORK_PR_TRIGGER_RE.test(k));
|
|
67895
|
+
if (!forkKeys.length) return [];
|
|
67896
|
+
const inj = new Set(injectableJobs(wf, raw, reach.untrusted || reach.reusable));
|
|
67897
|
+
for (const job of jobList(wf)) {
|
|
67898
|
+
if (inj.has(job)) continue;
|
|
67899
|
+
const steps = job.steps ?? [];
|
|
67900
|
+
const agent = steps.filter(isAgentStep);
|
|
67901
|
+
if (!agent.length) continue;
|
|
67902
|
+
if (untrustedHeadCheckout(steps) !== "root") continue;
|
|
67903
|
+
if (agent.every(readOnlySandbox)) continue;
|
|
67904
|
+
if (requiresAtom(str(job.if), SAME_REPO_GATE_RE)) continue;
|
|
67905
|
+
if (agent.every((st) => requiresAtom(str(st.if), SAME_REPO_GATE_RE))) continue;
|
|
67906
|
+
const heads = steps.filter((st) => untrustedHeadCheckout([st]) === "root");
|
|
67907
|
+
if (heads.length && heads.every((st) => requiresAtom(str(st.if), SAME_REPO_GATE_RE))) continue;
|
|
67908
|
+
const evs = headRefEvents(heads);
|
|
67909
|
+
if (evs && !forkKeys.some((k) => evs.includes(k.toLowerCase()))) continue;
|
|
67910
|
+
if (evs && requiresAtom(str(job.if), SAME_REPO_GATE_RE, evs)) continue;
|
|
67911
|
+
if (requiresTrustedAuthor(str(job.if))) continue;
|
|
67912
|
+
const holdsSecret = /secrets\./i.test(str(job.steps) + " " + str(job.env));
|
|
67913
|
+
if (!holdsSecret && !jobsHaveWrite(wf, [job], /:\s*["']?write/i)) continue;
|
|
67914
|
+
return [
|
|
67915
|
+
{
|
|
67916
|
+
check: "CI-2",
|
|
67917
|
+
rule: "CI-2.fork-code-on-trust",
|
|
67918
|
+
dimension: "workflows",
|
|
67919
|
+
severity: "advisory",
|
|
67920
|
+
title: "A maintainer's action runs the agent inside the fork's code",
|
|
67921
|
+
file: path91,
|
|
67922
|
+
signals: [
|
|
67923
|
+
`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)`,
|
|
67924
|
+
"checks out the untrusted PR head into the workspace root",
|
|
67925
|
+
"the agent loads the fork's CLAUDE.md, .claude/settings.json hooks, .mcp.json and build scripts next to the job's secrets"
|
|
67926
|
+
],
|
|
67927
|
+
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."
|
|
67928
|
+
}
|
|
67929
|
+
];
|
|
66980
67930
|
}
|
|
66981
67931
|
return [];
|
|
66982
67932
|
}
|
|
66983
67933
|
|
|
66984
67934
|
// src/ci-check/agent-config.ts
|
|
67935
|
+
import path76 from "path";
|
|
66985
67936
|
function asStrings(v) {
|
|
66986
67937
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
66987
67938
|
}
|
|
@@ -66999,11 +67950,32 @@ function hookCommands(hooks) {
|
|
|
66999
67950
|
return out;
|
|
67000
67951
|
}
|
|
67001
67952
|
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
|
|
67953
|
+
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
|
|
67954
|
+
// as Bash, review round 2).
|
|
67955
|
+
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
67956
|
"i"
|
|
67004
67957
|
);
|
|
67005
67958
|
var OPEN_RUNNER_MAX = 300;
|
|
67006
67959
|
var INFO_FLAG_RE = /\s--(version|help)\b/;
|
|
67960
|
+
var EXTRA_RUNNER_RE = new RegExp(
|
|
67961
|
+
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} ?['"]?(?::\*| \*|\*)\)$`,
|
|
67962
|
+
"i"
|
|
67963
|
+
);
|
|
67964
|
+
var EXEC_ESCAPE_RE = new RegExp(
|
|
67965
|
+
String.raw`^Bash\((?:[A-Z_][A-Z0-9_]{0,40}=\S{0,200} )*(?:/[\w./-]{0,80}/)?` + String.raw`(find|make -n)` + String.raw`(?: --?[A-Za-z][\w-]{0,30}){0,4} ?['"]?(?::\*| \*|\*)\)$`,
|
|
67966
|
+
"i"
|
|
67967
|
+
);
|
|
67968
|
+
function execEscape(grant) {
|
|
67969
|
+
if (grant.length > OPEN_RUNNER_MAX) return false;
|
|
67970
|
+
return EXEC_ESCAPE_RE.test(
|
|
67971
|
+
grant.replace(/^PowerShell(?=$|\()/, "Bash").replace(/\s+/g, " ").replace(/\(\s+/, "(")
|
|
67972
|
+
);
|
|
67973
|
+
}
|
|
67974
|
+
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)$/;
|
|
67975
|
+
function extraRunner(grant) {
|
|
67976
|
+
if (grant.length > OPEN_RUNNER_MAX || INFO_FLAG_RE.test(grant)) return false;
|
|
67977
|
+
return EXTRA_RUNNER_RE.test(grant.replace(/\s+/g, " ").replace(/\(\s+/, "(")) || MCP_EXEC_RE.test(grant);
|
|
67978
|
+
}
|
|
67007
67979
|
function openRunner(grant) {
|
|
67008
67980
|
if (grant.length > OPEN_RUNNER_MAX || INFO_FLAG_RE.test(grant)) return false;
|
|
67009
67981
|
return OPEN_RUNNER_RE.test(grant.replace(/\s+/g, " ").replace(/\(\s+/, "("));
|
|
@@ -67011,13 +67983,15 @@ function openRunner(grant) {
|
|
|
67011
67983
|
function gradeBroadGrant(allow, deny, opts = {}) {
|
|
67012
67984
|
const denySupported = opts.denySupported ?? true;
|
|
67013
67985
|
const scope = opts.scope ?? "for everyone who opens this repo with the agent";
|
|
67014
|
-
const
|
|
67015
|
-
|
|
67016
|
-
|
|
67986
|
+
const asBash = (a) => a.replace(/^PowerShell(?=$|\()/, "Bash");
|
|
67987
|
+
const broad = allow.filter((raw) => {
|
|
67988
|
+
const a = asBash(raw);
|
|
67989
|
+
return /^Bash$|^Bash\(\s*\*|^Bash\(git:|^Write\(\s*\*|^Write$|^Edit$/.test(a) || openRunner(a) || opts.settings === true && extraRunner(a);
|
|
67990
|
+
});
|
|
67017
67991
|
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));
|
|
67992
|
+
const hasBackstop = deny.some((d) => /Bash|PowerShell|Write|Edit/.test(d));
|
|
67993
|
+
const bashBackstop = deny.some((d) => /^(?:Bash|PowerShell)\b/.test(d));
|
|
67994
|
+
const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(asBash(a)));
|
|
67021
67995
|
const high = bareShell && !bashBackstop;
|
|
67022
67996
|
const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
|
|
67023
67997
|
if (high)
|
|
@@ -67034,7 +68008,7 @@ function gradeBroadGrant(allow, deny, opts = {}) {
|
|
|
67034
68008
|
);
|
|
67035
68009
|
if (broad.some((a) => /^Write|^Edit$/.test(a)))
|
|
67036
68010
|
signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
|
|
67037
|
-
if (broad.some(openRunner))
|
|
68011
|
+
if (broad.some((a) => openRunner(asBash(a))))
|
|
67038
68012
|
signals.push(
|
|
67039
68013
|
"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
68014
|
);
|
|
@@ -67147,9 +68121,12 @@ function analyzeAgentConfig(path91, content, tree) {
|
|
|
67147
68121
|
}
|
|
67148
68122
|
}
|
|
67149
68123
|
}
|
|
67150
|
-
const allow =
|
|
68124
|
+
const allow = [
|
|
68125
|
+
...asStrings(cfg.permissions?.allow),
|
|
68126
|
+
...asStrings(cfg.permissions?.allowedTools)
|
|
68127
|
+
];
|
|
67151
68128
|
const deny = asStrings(cfg.permissions?.deny);
|
|
67152
|
-
const grade = gradeBroadGrant(allow, deny);
|
|
68129
|
+
const grade = gradeBroadGrant(allow, deny, { settings: true });
|
|
67153
68130
|
if (grade) {
|
|
67154
68131
|
const { high, signals } = grade;
|
|
67155
68132
|
const at = jsonValueIndex(content, grade.broad[0]);
|
|
@@ -67174,9 +68151,47 @@ function analyzeAgentConfig(path91, content, tree) {
|
|
|
67174
68151
|
})
|
|
67175
68152
|
});
|
|
67176
68153
|
}
|
|
68154
|
+
const escapes = allow.filter(execEscape);
|
|
68155
|
+
if (escapes.length) {
|
|
68156
|
+
const at = jsonValueIndex(content, escapes[0]);
|
|
68157
|
+
findings.push({
|
|
68158
|
+
...at >= 0 ? { line: lineAtIndex(content, at) } : {},
|
|
68159
|
+
check: "CI-1",
|
|
68160
|
+
rule: "CI-1.exec-escape-grant",
|
|
68161
|
+
dimension: "toolRules",
|
|
68162
|
+
severity: "medium",
|
|
68163
|
+
title: "Committed agent config pre-approves a command that can run other programs",
|
|
68164
|
+
file: path91,
|
|
68165
|
+
signals: [
|
|
68166
|
+
`pre-approved: ${escapes.slice(0, 5).map((e) => `\`${safeText(e, 80)}\``).join(", ")}`,
|
|
68167
|
+
"`find -exec` and `make -n --eval` run other programs without a prompt"
|
|
68168
|
+
],
|
|
68169
|
+
fix: "Pre-approve the exact command you need (for example `Bash(find . -name *.ts)` or `Bash(make test)`), not every argument."
|
|
68170
|
+
});
|
|
68171
|
+
}
|
|
67177
68172
|
return findings;
|
|
67178
68173
|
}
|
|
67179
68174
|
var GRANT_CARRIER_RE = /(^|\/)[Ss][Kk][Ii][Ll][Ll]\.md$|(^|\/)\.claude\/commands\/.+\.md$/;
|
|
68175
|
+
var ROOT_SURFACE_RE = /^\.(claude|agents|github|codex|cursor|gemini)\//;
|
|
68176
|
+
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/;
|
|
68177
|
+
var PR_TEXT_RE = /gh\s+pr\s+(?:view|diff)\b|gh\s+api\s+[^\n]{0,120}\/pulls\b/;
|
|
68178
|
+
var WEB_TEXT_RE = /\bsearch the web\b|\bfetch (?:the )?(?:url|page)\b/i;
|
|
68179
|
+
function skillReadsUntrusted(path91, content, allow, broad) {
|
|
68180
|
+
if (!ROOT_SURFACE_RE.test(path91)) return null;
|
|
68181
|
+
const body = content.replace(/^---[\s\S]*?\n---/, "");
|
|
68182
|
+
const runsCode = broad.map((a) => a.replace(/^PowerShell(?=$|\()/, "Bash")).some((a) => /^Bash$|^Bash\(\s*\*/.test(a) || openRunner(a) && !/^Bash\(\s*gh\b/.test(a));
|
|
68183
|
+
const webGrant = allow.some((a) => /^(WebFetch|WebSearch)\b/.test(a));
|
|
68184
|
+
const reads = (re) => body.replace(/\\\r?\n/g, " ").split("\n").some((l) => re.test(l) && !sanitizedListing(l));
|
|
68185
|
+
if (runsCode && reads(ITEM_TEXT_RE))
|
|
68186
|
+
return "it reads pull request or issue text that strangers write, with a grant that runs code";
|
|
68187
|
+
if (runsCode && (webGrant || WEB_TEXT_RE.test(body)))
|
|
68188
|
+
return "it reads web content anyone can publish, with a grant that runs code";
|
|
68189
|
+
const writes = allow.some((a) => /^(Write|Edit|MultiEdit)$/.test(a));
|
|
68190
|
+
const ghOrGit = allow.some((a) => /^Bash\(\s*(gh|git)\b/.test(a));
|
|
68191
|
+
if (writes && ghOrGit && reads(PR_TEXT_RE))
|
|
68192
|
+
return "it reads pull request text that strangers write, and may edit files and run git/gh on it";
|
|
68193
|
+
return null;
|
|
68194
|
+
}
|
|
67180
68195
|
function analyzeSkillGrants(path91, content) {
|
|
67181
68196
|
if (!GRANT_CARRIER_RE.test(path91)) return [];
|
|
67182
68197
|
const allow = allowedToolsOf(parseFrontmatter(content));
|
|
@@ -67188,6 +68203,21 @@ function analyzeSkillGrants(path91, content) {
|
|
|
67188
68203
|
if (!grade) return [];
|
|
67189
68204
|
const kind = /commands\//.test(path91) ? "slash command" : "skill";
|
|
67190
68205
|
const at = content.search(/^allowed-tools\s*:/m);
|
|
68206
|
+
const untrusted = skillReadsUntrusted(path91, content, allow, grade.broad);
|
|
68207
|
+
if (untrusted)
|
|
68208
|
+
return [
|
|
68209
|
+
{
|
|
68210
|
+
check: "CI-1",
|
|
68211
|
+
rule: "CI-1.skill-untrusted-input",
|
|
68212
|
+
...at >= 0 ? { line: lineAtIndex(content, at) } : {},
|
|
68213
|
+
dimension: "toolRules",
|
|
68214
|
+
severity: "advisory",
|
|
68215
|
+
title: `Committed ${kind} runs code or writes with text strangers control`,
|
|
68216
|
+
file: path91,
|
|
68217
|
+
signals: [...grade.signals, untrusted],
|
|
68218
|
+
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."
|
|
68219
|
+
}
|
|
68220
|
+
];
|
|
67191
68221
|
return [
|
|
67192
68222
|
{
|
|
67193
68223
|
check: "CI-1",
|
|
@@ -67209,27 +68239,178 @@ function analyzeSkillGrants(path91, content) {
|
|
|
67209
68239
|
|
|
67210
68240
|
// src/ci-check/mcp.ts
|
|
67211
68241
|
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
|
-
|
|
68242
|
+
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;&|`$<>()]+)*$/;
|
|
68243
|
+
var LAUNCHERS = [
|
|
68244
|
+
{ re: /\bnpx\b/, name: "npx", registry: "npm" },
|
|
68245
|
+
{ re: /\bpnpx\b|\bpnpm\s+dlx\b/, name: "pnpm dlx", registry: "npm" },
|
|
68246
|
+
{ re: /\byarn\s+dlx\b/, name: "yarn dlx", registry: "npm" },
|
|
68247
|
+
{ re: /\bnpm\s+(?:exec|x)\b/, name: "npm exec", registry: "npm" },
|
|
68248
|
+
{ re: /\bbunx\b|\bbun\s+x\b/, name: "bunx", registry: "npm" },
|
|
68249
|
+
{ re: /\buvx\b|\buv\s+tool\s+run\b/, name: "uvx", registry: "PyPI" },
|
|
68250
|
+
{ re: /\bpipx\s+run\b/, name: "pipx run", registry: "PyPI" }
|
|
68251
|
+
];
|
|
68252
|
+
var PINNED_RE = /@v?\d|@[0-9a-f]{40}\b|==\s*\d/i;
|
|
68253
|
+
var LOCAL_FROM_RE = /(?:^|\s)--from(?:\s+|=)(?:\.{1,2}(?:\/\S*)?|\/\S*|~\/\S*)(?:\s|$)/;
|
|
68254
|
+
var RUN_VALUE_FLAGS = new Set(
|
|
68255
|
+
[
|
|
68256
|
+
"-a --attach --add-host --annotation --blkio-weight --blkio-weight-device --cap-add",
|
|
68257
|
+
"--cap-drop --cgroup-parent --cgroupns --cgroups --cgroup-conf --cidfile --cpu-period",
|
|
68258
|
+
"--cpu-quota --cpu-rt-period --cpu-rt-runtime -c --cpu-shares --cpus --cpuset-cpus",
|
|
68259
|
+
"--cpuset-mems --device --device-cgroup-rule --device-read-bps --device-read-iops",
|
|
68260
|
+
"--device-write-bps --device-write-iops --dns --dns-option --dns-search --domainname",
|
|
68261
|
+
"--entrypoint -e --env --env-file --expose --gpus --group-add --health-cmd",
|
|
68262
|
+
"--health-interval --health-retries --health-start-interval --health-start-period",
|
|
68263
|
+
"--health-timeout -h --hostname --ip --ip6 --ipc --isolation --kernel-memory -l --label",
|
|
68264
|
+
"--label-file --link --link-local-ip --log-driver --log-opt --mac-address -m --memory",
|
|
68265
|
+
"--memory-reservation --memory-swap --memory-swappiness --mount --name --network --net",
|
|
68266
|
+
"--network-alias --oom-score-adj --pid --pids-limit --platform -p --publish --pull",
|
|
68267
|
+
"--restart --runtime --security-opt --shm-size --stop-signal --stop-timeout",
|
|
68268
|
+
"--storage-opt --sysctl --tmpfs --ulimit -u --user --userns --uts -v --volume",
|
|
68269
|
+
"--volume-driver --volumes-from -w --workdir --secret --pod --arch --os --variant",
|
|
68270
|
+
"--timezone --umask --requires --uidmap --gidmap --subuidname --subgidname --hooks-dir",
|
|
68271
|
+
"--init-path --seccomp-policy --sdnotify --personality --preserve-fds --chrootdirs"
|
|
68272
|
+
].join(" ").split(" ")
|
|
68273
|
+
);
|
|
68274
|
+
function runImage(tokens) {
|
|
68275
|
+
const i = tokens.findIndex(
|
|
68276
|
+
(t, k) => /(^|\/)(docker|podman|nerdctl)$/.test(t) && (tokens[k + 1] === "run" || tokens[k + 1] === "container" && tokens[k + 2] === "run")
|
|
68277
|
+
);
|
|
68278
|
+
if (i < 0) return null;
|
|
68279
|
+
const from = tokens[i + 1] === "run" ? i + 2 : i + 3;
|
|
68280
|
+
for (let k = from; k < tokens.length; k++) {
|
|
68281
|
+
const t = tokens[k];
|
|
68282
|
+
if (t.startsWith("-")) {
|
|
68283
|
+
if (!t.includes("=") && RUN_VALUE_FLAGS.has(t)) k++;
|
|
68284
|
+
continue;
|
|
68285
|
+
}
|
|
68286
|
+
return t;
|
|
68287
|
+
}
|
|
68288
|
+
return null;
|
|
68289
|
+
}
|
|
68290
|
+
function launchedPackages(argv) {
|
|
68291
|
+
const t = argv.split(/\s+/).filter(Boolean);
|
|
68292
|
+
const i = t.findIndex((x) => /(^|\/)(npx|pnpx|bunx|uvx|dlx|exec|x|run)$/.test(x));
|
|
68293
|
+
if (i < 0) return [];
|
|
68294
|
+
const bare = (v) => v.startsWith("@") ? v.replace(/^(@[^/]+\/[^@]+).*$/, "$1") : v.replace(/(==|@).*$/, "");
|
|
68295
|
+
const flagged = [];
|
|
68296
|
+
let positional = null;
|
|
68297
|
+
for (let k = i + 1; k < t.length; k++) {
|
|
68298
|
+
const tok = t[k];
|
|
68299
|
+
const eq = /^(--from|-p|--package)=(.*)$/.exec(tok);
|
|
68300
|
+
if (eq) {
|
|
68301
|
+
flagged.push(eq[2]);
|
|
68302
|
+
continue;
|
|
68303
|
+
}
|
|
68304
|
+
if (/^(--from|-p|--package)$/.test(tok)) {
|
|
68305
|
+
if (t[k + 1]) flagged.push(t[++k]);
|
|
68306
|
+
continue;
|
|
68307
|
+
}
|
|
68308
|
+
if (tok.startsWith("-")) continue;
|
|
68309
|
+
positional ??= tok;
|
|
68310
|
+
}
|
|
68311
|
+
const names = flagged.length ? flagged : positional ? [positional] : [];
|
|
68312
|
+
return names.map((n) => bare(n).toLowerCase()).filter(Boolean);
|
|
68313
|
+
}
|
|
68314
|
+
var SHELL_RE = /(?:^|\/)(?:sh|bash|zsh|dash|cmd|pwsh|powershell)(?:\.exe)?$/i;
|
|
68315
|
+
var SHELL_OP_RE = /[;&|`$<>()]/;
|
|
68316
|
+
function launchesOwnPackage(argv, own) {
|
|
68317
|
+
if (own.size === 0) return false;
|
|
68318
|
+
if (SHELL_OP_RE.test(argv) || SHELL_RE.test(argv.split(/\s+/)[0] ?? "")) return false;
|
|
68319
|
+
const l = LAUNCHERS.find((x) => x.re.test(argv));
|
|
68320
|
+
if (!l) return false;
|
|
68321
|
+
const reg = l.registry === "PyPI" ? "pypi" : "npm";
|
|
68322
|
+
const pkgs = launchedPackages(argv);
|
|
68323
|
+
return pkgs.length > 0 && pkgs.every((p) => own.has(`${reg}:${p}`) || own.has(p));
|
|
68324
|
+
}
|
|
68325
|
+
function unpinnedLauncher(argv, own) {
|
|
68326
|
+
if (LOCAL_RUNNER_RE.test(argv)) return null;
|
|
68327
|
+
if (!SHELL_OP_RE.test(argv) && LOCAL_FROM_RE.test(argv)) return null;
|
|
68328
|
+
if (launchesOwnPackage(argv, own)) return null;
|
|
68329
|
+
const image = runImage(argv.split(/\s+/));
|
|
68330
|
+
if (image) {
|
|
68331
|
+
if (image.includes("@sha256:")) return null;
|
|
68332
|
+
const tail = image.slice(image.lastIndexOf("/") + 1);
|
|
68333
|
+
const tag = tail.includes(":") ? tail.slice(tail.indexOf(":") + 1) : "";
|
|
68334
|
+
return !tag || tag === "latest" ? { launcher: "docker run", registry: "the image registry" } : null;
|
|
68335
|
+
}
|
|
68336
|
+
const goMod = /\bgo\s+run\s+(?:-\S+\s+)*([^\s@]+)@(\S+)/.exec(argv);
|
|
68337
|
+
if (goMod)
|
|
68338
|
+
return /^v\d/.test(goMod[2]) || /^[0-9a-f]{40}$/.test(goMod[2]) ? null : { launcher: "go run", registry: "the Go module proxy" };
|
|
68339
|
+
const l = LAUNCHERS.find((x) => x.re.test(argv));
|
|
68340
|
+
if (!l) return null;
|
|
68341
|
+
return /@latest\b/.test(argv) || !PINNED_RE.test(argv) ? { launcher: l.name, registry: l.registry } : null;
|
|
68342
|
+
}
|
|
68343
|
+
function ownPackageNames(manifests) {
|
|
68344
|
+
const out = /* @__PURE__ */ new Set();
|
|
68345
|
+
for (const m of manifests) {
|
|
68346
|
+
if (m.path === "package.json") {
|
|
68347
|
+
try {
|
|
68348
|
+
const pkg = JSON.parse(m.content);
|
|
68349
|
+
if (typeof pkg.name === "string" && pkg.name && pkg.private !== true)
|
|
68350
|
+
out.add(`npm:${pkg.name.toLowerCase()}`);
|
|
68351
|
+
} catch {
|
|
68352
|
+
}
|
|
68353
|
+
} else if (m.path === "pyproject.toml") {
|
|
68354
|
+
const sec = /^\[(project|tool\.poetry)\]\s*$([\s\S]*?)(?=^\[|(?![\s\S]))/m.exec(m.content);
|
|
68355
|
+
const name = sec && /^name\s*=\s*["']([^"']{1,200})["']/m.exec(sec[2]);
|
|
68356
|
+
if (name) {
|
|
68357
|
+
out.add(`pypi:${name[1].toLowerCase()}`);
|
|
68358
|
+
out.add(`pypi:${name[1].toLowerCase().replace(/_/g, "-")}`);
|
|
68359
|
+
}
|
|
68360
|
+
}
|
|
68361
|
+
}
|
|
68362
|
+
return out;
|
|
68363
|
+
}
|
|
68364
|
+
function analyzeMcp(path91, content, own = /* @__PURE__ */ new Set()) {
|
|
67214
68365
|
let cfg;
|
|
67215
68366
|
try {
|
|
67216
68367
|
cfg = JSON.parse(content);
|
|
67217
68368
|
} catch {
|
|
67218
68369
|
return [];
|
|
67219
68370
|
}
|
|
67220
|
-
|
|
68371
|
+
if (!cfg || typeof cfg !== "object") return [];
|
|
68372
|
+
const sections = [];
|
|
68373
|
+
const obj = (x) => x && typeof x === "object" && !Array.isArray(x) ? x : {};
|
|
68374
|
+
sections.push(["mcpServers", obj(cfg.mcpServers)]);
|
|
68375
|
+
sections.push(["servers", obj(cfg.servers)]);
|
|
68376
|
+
const opencode = {};
|
|
68377
|
+
for (const [name, s] of Object.entries(obj(cfg.mcp))) {
|
|
68378
|
+
if (!s || typeof s !== "object") continue;
|
|
68379
|
+
const o = s;
|
|
68380
|
+
const cmd = Array.isArray(o.command) ? o.command.map(String) : [];
|
|
68381
|
+
opencode[name] = {
|
|
68382
|
+
command: cmd[0],
|
|
68383
|
+
args: cmd.slice(1),
|
|
68384
|
+
url: o.url,
|
|
68385
|
+
env: obj(o.environment),
|
|
68386
|
+
disabled: o.enabled === false
|
|
68387
|
+
};
|
|
68388
|
+
}
|
|
68389
|
+
sections.push(["mcp", opencode]);
|
|
68390
|
+
const count = /* @__PURE__ */ new Map();
|
|
68391
|
+
for (const [, sec] of sections)
|
|
68392
|
+
for (const name of Object.keys(sec)) count.set(name, (count.get(name) ?? 0) + 1);
|
|
68393
|
+
const servers = {};
|
|
68394
|
+
for (const [section, sec] of sections)
|
|
68395
|
+
for (const [name, spec] of Object.entries(sec)) {
|
|
68396
|
+
if (!spec || typeof spec !== "object") continue;
|
|
68397
|
+
const key = (count.get(name) ?? 0) > 1 ? `${section}.${name}` : name;
|
|
68398
|
+
servers[key] = { ...spec, anchor: name };
|
|
68399
|
+
}
|
|
68400
|
+
return analyzeMcpServers(servers, path91, content, own);
|
|
67221
68401
|
}
|
|
67222
|
-
function analyzeMcpServers(servers, path91, content = "") {
|
|
68402
|
+
function analyzeMcpServers(servers, path91, content = "", own = /* @__PURE__ */ new Set()) {
|
|
67223
68403
|
const findings = [];
|
|
67224
68404
|
for (const [name, srv] of Object.entries(servers ?? {})) {
|
|
67225
68405
|
if (!srv || srv.disabled) continue;
|
|
67226
68406
|
const argv = [srv.command, ...Array.isArray(srv.args) ? srv.args.map(String) : []].join(" ");
|
|
67227
|
-
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
68407
|
+
const esc = (srv.anchor ?? name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
67228
68408
|
const at = content.search(
|
|
67229
68409
|
new RegExp(`"${esc}"\\s*:|^[ \\t]*\\[mcp_servers\\.(?:"${esc}"|${esc})\\]`, "m")
|
|
67230
68410
|
);
|
|
67231
68411
|
const line = at >= 0 ? lineAtIndex(content, at) : void 0;
|
|
67232
|
-
|
|
68412
|
+
const why = unpinnedLauncher(argv, own);
|
|
68413
|
+
if (why) {
|
|
67233
68414
|
findings.push({
|
|
67234
68415
|
check: "CI-3",
|
|
67235
68416
|
rule: "CI-3.mcp-unpinned",
|
|
@@ -67239,9 +68420,16 @@ function analyzeMcpServers(servers, path91, content = "") {
|
|
|
67239
68420
|
title: `MCP server "${name}" runs an unpinned executable`,
|
|
67240
68421
|
file: path91,
|
|
67241
68422
|
...line ? { line } : {},
|
|
67242
|
-
signals: [
|
|
68423
|
+
signals: [
|
|
68424
|
+
why.launcher === "npx" ? `\`${safeText(argv, 120)}\`: unversioned/@latest npx` : `\`${safeText(argv, 120)}\`: unversioned/latest ${why.launcher}`
|
|
68425
|
+
],
|
|
67243
68426
|
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({
|
|
68427
|
+
explain: explainMcp({
|
|
68428
|
+
name,
|
|
68429
|
+
argv,
|
|
68430
|
+
registry: why.registry,
|
|
68431
|
+
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
|
|
68432
|
+
})
|
|
67245
68433
|
});
|
|
67246
68434
|
}
|
|
67247
68435
|
for (const [k, v] of Object.entries(srv.env ?? {})) {
|
|
@@ -67270,7 +68458,7 @@ function analyzeMcpServers(servers, path91, content = "") {
|
|
|
67270
68458
|
|
|
67271
68459
|
// src/ci-check/codex.ts
|
|
67272
68460
|
import { parse as parseToml7 } from "smol-toml";
|
|
67273
|
-
function analyzeCodexConfig(path91, content) {
|
|
68461
|
+
function analyzeCodexConfig(path91, content, own = /* @__PURE__ */ new Set()) {
|
|
67274
68462
|
let cfg;
|
|
67275
68463
|
try {
|
|
67276
68464
|
cfg = parseToml7(content);
|
|
@@ -67278,7 +68466,7 @@ function analyzeCodexConfig(path91, content) {
|
|
|
67278
68466
|
return [];
|
|
67279
68467
|
}
|
|
67280
68468
|
const findings = [];
|
|
67281
|
-
findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path91, content));
|
|
68469
|
+
findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path91, content, own));
|
|
67282
68470
|
const sandbox = typeof cfg.sandbox_mode === "string" ? cfg.sandbox_mode : "";
|
|
67283
68471
|
const approval = typeof cfg.approval_policy === "string" ? cfg.approval_policy : "";
|
|
67284
68472
|
const fullAccess = /danger-full-access/i.test(sandbox);
|
|
@@ -67315,10 +68503,13 @@ var UNREAD_RULES = /* @__PURE__ */ new Set([
|
|
|
67315
68503
|
"CI-1.hook-script.unscanned-size",
|
|
67316
68504
|
"CI-6.skill-script.unscanned-size"
|
|
67317
68505
|
]);
|
|
67318
|
-
var CONCEALMENT = /* @__PURE__ */ new Set(["CI-6.unicode-tag-chars", "CI-6.bidi-override"]);
|
|
68506
|
+
var CONCEALMENT = /* @__PURE__ */ new Set(["CI-6.unicode-tag-chars", "CI-6.bidi-override", "CI-6.homoglyph"]);
|
|
67319
68507
|
function tierOf(f) {
|
|
67320
68508
|
const { rule, severity } = f;
|
|
67321
68509
|
if (rule === "CI-1.skill-allowed-tools") return "review";
|
|
68510
|
+
if (rule === "CI-1.exec-escape-grant") return "review";
|
|
68511
|
+
if (rule === "CI-2.trusted-trigger-reader" || rule === "CI-2.fork-code-on-trust" || rule === "CI-1.skill-untrusted-input")
|
|
68512
|
+
return "alert";
|
|
67322
68513
|
if (UNREAD_RULES.has(rule)) return "review";
|
|
67323
68514
|
if (rule.startsWith("CI-2.") || rule.startsWith("CI-4."))
|
|
67324
68515
|
return severity === "advisory" ? "note" : "alert";
|
|
@@ -67344,6 +68535,8 @@ function reviewQuestion(rule) {
|
|
|
67344
68535
|
return "Does this hook exist on every machine that runs it?";
|
|
67345
68536
|
if (/hidden-chars/.test(rule))
|
|
67346
68537
|
return "Why does this script hold invisible characters? A list of them to detect is fine.";
|
|
68538
|
+
if (/exec-escape/.test(rule))
|
|
68539
|
+
return "Does the agent need this command with any arguments, or only one exact form?";
|
|
67347
68540
|
if (/unscanned/.test(rule))
|
|
67348
68541
|
return "node9 did not read this script (too large, or outside the folders it reads). What does it run?";
|
|
67349
68542
|
return "Is this an instruction the agent follows, or text for a person?";
|
|
@@ -67683,12 +68876,59 @@ function scanTree(tree) {
|
|
|
67683
68876
|
}
|
|
67684
68877
|
return named.get(dir).has(docPath);
|
|
67685
68878
|
};
|
|
68879
|
+
const callersOf = /* @__PURE__ */ new Map();
|
|
68880
|
+
const autoLabels = /* @__PURE__ */ new Set();
|
|
68881
|
+
const routeOfFile = (file) => file.route !== void 0 ? file.route : routeOf(file.path, skillDirs, appDirs);
|
|
68882
|
+
const manifests = [];
|
|
68883
|
+
for (const file of tree.files) {
|
|
68884
|
+
if (routeOfFile(file) !== "manifest") continue;
|
|
68885
|
+
try {
|
|
68886
|
+
manifests.push({ path: file.path, content: file.content });
|
|
68887
|
+
} catch {
|
|
68888
|
+
}
|
|
68889
|
+
}
|
|
68890
|
+
const ownPackages = ownPackageNames(manifests);
|
|
68891
|
+
for (const file of tree.files) {
|
|
68892
|
+
if (routeOfFile(file) !== "issue-form") continue;
|
|
68893
|
+
try {
|
|
68894
|
+
for (const l of issueFormLabels(file.content)) autoLabels.add(l);
|
|
68895
|
+
} catch {
|
|
68896
|
+
}
|
|
68897
|
+
}
|
|
68898
|
+
const runSources = /* @__PURE__ */ new Map();
|
|
68899
|
+
for (const file of tree.files) {
|
|
68900
|
+
if (routeOfFile(file) !== "workflow") continue;
|
|
68901
|
+
try {
|
|
68902
|
+
const src = runSourceOf(file.path, file.content, autoLabels);
|
|
68903
|
+
if (src) runSources.set(src.name, (runSources.get(src.name) ?? true) && src.trusted);
|
|
68904
|
+
} catch {
|
|
68905
|
+
}
|
|
68906
|
+
}
|
|
68907
|
+
const trustedRunSources = new Set([...runSources].filter(([, t]) => t).map(([n]) => n));
|
|
68908
|
+
for (const file of tree.files) {
|
|
68909
|
+
if (routeOfFile(file) !== "workflow") continue;
|
|
68910
|
+
try {
|
|
68911
|
+
const content = file.content;
|
|
68912
|
+
for (const { target, caller } of localCallsOf(
|
|
68913
|
+
file.path,
|
|
68914
|
+
content,
|
|
68915
|
+
autoLabels,
|
|
68916
|
+
trustedRunSources
|
|
68917
|
+
)) {
|
|
68918
|
+
const list = callersOf.get(target) ?? [];
|
|
68919
|
+
list.push(caller);
|
|
68920
|
+
callersOf.set(target, list);
|
|
68921
|
+
}
|
|
68922
|
+
} catch {
|
|
68923
|
+
}
|
|
68924
|
+
}
|
|
67686
68925
|
for (const file of tree.files) {
|
|
67687
68926
|
let route = file.route !== void 0 ? file.route : routeOf(file.path, skillDirs, appDirs);
|
|
67688
68927
|
if (route === "app-doc") {
|
|
67689
68928
|
if (!namedBy(file.path)) continue;
|
|
67690
68929
|
route = "instruction";
|
|
67691
68930
|
}
|
|
68931
|
+
if (route === "issue-form" || route === "manifest") continue;
|
|
67692
68932
|
inspected.push(file.path);
|
|
67693
68933
|
if (file.path === SUPPRESSIONS_FILE) continue;
|
|
67694
68934
|
const testKind = route && CONTENT_ROUTES.has(route) ? testPathKind(file.path, route) : "none";
|
|
@@ -67698,10 +68938,24 @@ function scanTree(tree) {
|
|
|
67698
68938
|
const content = file.content;
|
|
67699
68939
|
switch (route) {
|
|
67700
68940
|
case "workflow": {
|
|
67701
|
-
const
|
|
68941
|
+
const callers = callersOf.get(file.path);
|
|
68942
|
+
const f = analyzeWorkflow(file.path, content, callers, autoLabels, trustedRunSources);
|
|
67702
68943
|
if (f) findings.push(f);
|
|
67703
|
-
const s = analyzeWorkflowSecrets(
|
|
68944
|
+
const s = analyzeWorkflowSecrets(
|
|
68945
|
+
file.path,
|
|
68946
|
+
content,
|
|
68947
|
+
callers,
|
|
68948
|
+
autoLabels,
|
|
68949
|
+
trustedRunSources
|
|
68950
|
+
);
|
|
67704
68951
|
if (s) findings.push(s);
|
|
68952
|
+
findings.push(...analyzeWorkflowPlugins(file.path, content, tree.owner));
|
|
68953
|
+
findings.push(
|
|
68954
|
+
...analyzeTrustedReaders(file.path, content, autoLabels, callers, trustedRunSources)
|
|
68955
|
+
);
|
|
68956
|
+
findings.push(
|
|
68957
|
+
...analyzeForkCodeOnTrust(file.path, content, callers, autoLabels, trustedRunSources)
|
|
68958
|
+
);
|
|
67705
68959
|
break;
|
|
67706
68960
|
}
|
|
67707
68961
|
case "agent-config":
|
|
@@ -67714,10 +68968,10 @@ function scanTree(tree) {
|
|
|
67714
68968
|
findings.push(...analyzeScript(file.path, content, "CI-6.skill-script"));
|
|
67715
68969
|
break;
|
|
67716
68970
|
case "mcp":
|
|
67717
|
-
findings.push(...analyzeMcp(file.path, content));
|
|
68971
|
+
findings.push(...analyzeMcp(file.path, content, ownPackages));
|
|
67718
68972
|
break;
|
|
67719
68973
|
case "codex":
|
|
67720
|
-
findings.push(...analyzeCodexConfig(file.path, content));
|
|
68974
|
+
findings.push(...analyzeCodexConfig(file.path, content, ownPackages));
|
|
67721
68975
|
break;
|
|
67722
68976
|
case "instruction":
|
|
67723
68977
|
findings.push(...analyzeInstructionFile(file.path, content));
|
|
@@ -67725,7 +68979,9 @@ function scanTree(tree) {
|
|
|
67725
68979
|
break;
|
|
67726
68980
|
}
|
|
67727
68981
|
} catch (err2) {
|
|
67728
|
-
notes.push(
|
|
68982
|
+
notes.push(
|
|
68983
|
+
`checker degraded on ${file.path}: ${err2?.message ?? "error"}; results may be INCOMPLETE`
|
|
68984
|
+
);
|
|
67729
68985
|
}
|
|
67730
68986
|
if (testKind === "alerts-only") {
|
|
67731
68987
|
const kept = findings.splice(before).filter((f) => tierOf(f) === "alert");
|