@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.js
CHANGED
|
@@ -19442,19 +19442,24 @@ async function postCostBatches(apiUrl, apiKey, machineId, entries) {
|
|
|
19442
19442
|
} catch {
|
|
19443
19443
|
}
|
|
19444
19444
|
if (typeof stored === "number" && stored < batch.length) {
|
|
19445
|
-
|
|
19446
|
-
|
|
19447
|
-
`[cost-sync] dropped ${batch.length - stored} of ${batch.length} rows
|
|
19448
|
-
`
|
|
19449
|
-
);
|
|
19445
|
+
debugLog(`[cost-sync] dropped ${batch.length - stored} of ${batch.length} rows
|
|
19446
|
+
`);
|
|
19450
19447
|
}
|
|
19451
19448
|
}
|
|
19452
19449
|
} catch (err2) {
|
|
19453
|
-
|
|
19450
|
+
debugLog(`[cost-sync] ${safeMessage(err2)}
|
|
19454
19451
|
`);
|
|
19455
19452
|
}
|
|
19456
19453
|
}
|
|
19457
19454
|
}
|
|
19455
|
+
function debugLog(line) {
|
|
19456
|
+
try {
|
|
19457
|
+
import_fs33.default.mkdirSync(import_path34.default.dirname(HOOK_DEBUG_LOG), { recursive: true });
|
|
19458
|
+
import_fs33.default.appendFileSync(HOOK_DEBUG_LOG, line);
|
|
19459
|
+
} catch (err2) {
|
|
19460
|
+
console.error(`[cost-sync] could not write ${HOOK_DEBUG_LOG}: ${safeMessage(err2)}`);
|
|
19461
|
+
}
|
|
19462
|
+
}
|
|
19458
19463
|
async function syncCost() {
|
|
19459
19464
|
const creds = getCredentials();
|
|
19460
19465
|
if (!creds?.apiKey || !creds?.apiUrl) return;
|
|
@@ -32349,7 +32354,7 @@ var require_diagnostics = __commonJS({
|
|
|
32349
32354
|
proxyConnected: diagnosticsChannel.channel("undici:proxy:connected")
|
|
32350
32355
|
};
|
|
32351
32356
|
var isTrackingClientEvents = false;
|
|
32352
|
-
function trackClientEvents(
|
|
32357
|
+
function trackClientEvents(debugLog2 = undiciDebugLog) {
|
|
32353
32358
|
if (isTrackingClientEvents) {
|
|
32354
32359
|
return;
|
|
32355
32360
|
}
|
|
@@ -32364,7 +32369,7 @@ var require_diagnostics = __commonJS({
|
|
|
32364
32369
|
const {
|
|
32365
32370
|
connectParams: { version: version2, protocol, port, host }
|
|
32366
32371
|
} = evt;
|
|
32367
|
-
|
|
32372
|
+
debugLog2(
|
|
32368
32373
|
"connecting to %s%s using %s%s",
|
|
32369
32374
|
host,
|
|
32370
32375
|
port ? `:${port}` : "",
|
|
@@ -32379,7 +32384,7 @@ var require_diagnostics = __commonJS({
|
|
|
32379
32384
|
const {
|
|
32380
32385
|
connectParams: { version: version2, protocol, port, host }
|
|
32381
32386
|
} = evt;
|
|
32382
|
-
|
|
32387
|
+
debugLog2(
|
|
32383
32388
|
"connected to %s%s using %s%s",
|
|
32384
32389
|
host,
|
|
32385
32390
|
port ? `:${port}` : "",
|
|
@@ -32395,7 +32400,7 @@ var require_diagnostics = __commonJS({
|
|
|
32395
32400
|
connectParams: { version: version2, protocol, port, host },
|
|
32396
32401
|
error
|
|
32397
32402
|
} = evt;
|
|
32398
|
-
|
|
32403
|
+
debugLog2(
|
|
32399
32404
|
"connection to %s%s using %s%s errored - %s",
|
|
32400
32405
|
host,
|
|
32401
32406
|
port ? `:${port}` : "",
|
|
@@ -32411,12 +32416,12 @@ var require_diagnostics = __commonJS({
|
|
|
32411
32416
|
const {
|
|
32412
32417
|
request: { method, path: path91, origin }
|
|
32413
32418
|
} = evt;
|
|
32414
|
-
|
|
32419
|
+
debugLog2("sending request to %s %s%s", method, origin, path91);
|
|
32415
32420
|
}
|
|
32416
32421
|
);
|
|
32417
32422
|
}
|
|
32418
32423
|
var isTrackingRequestEvents = false;
|
|
32419
|
-
function trackRequestEvents(
|
|
32424
|
+
function trackRequestEvents(debugLog2 = undiciDebugLog) {
|
|
32420
32425
|
if (isTrackingRequestEvents) {
|
|
32421
32426
|
return;
|
|
32422
32427
|
}
|
|
@@ -32432,7 +32437,7 @@ var require_diagnostics = __commonJS({
|
|
|
32432
32437
|
request: { method, path: path91, origin },
|
|
32433
32438
|
response: { statusCode }
|
|
32434
32439
|
} = evt;
|
|
32435
|
-
|
|
32440
|
+
debugLog2(
|
|
32436
32441
|
"received response to %s %s%s - HTTP %d",
|
|
32437
32442
|
method,
|
|
32438
32443
|
origin,
|
|
@@ -32447,7 +32452,7 @@ var require_diagnostics = __commonJS({
|
|
|
32447
32452
|
const {
|
|
32448
32453
|
request: { method, path: path91, origin }
|
|
32449
32454
|
} = evt;
|
|
32450
|
-
|
|
32455
|
+
debugLog2("trailers received from %s %s%s", method, origin, path91);
|
|
32451
32456
|
}
|
|
32452
32457
|
);
|
|
32453
32458
|
diagnosticsChannel.subscribe(
|
|
@@ -32457,7 +32462,7 @@ var require_diagnostics = __commonJS({
|
|
|
32457
32462
|
request: { method, path: path91, origin },
|
|
32458
32463
|
error
|
|
32459
32464
|
} = evt;
|
|
32460
|
-
|
|
32465
|
+
debugLog2(
|
|
32461
32466
|
"request to %s %s%s errored - %s",
|
|
32462
32467
|
method,
|
|
32463
32468
|
origin,
|
|
@@ -32468,7 +32473,7 @@ var require_diagnostics = __commonJS({
|
|
|
32468
32473
|
);
|
|
32469
32474
|
}
|
|
32470
32475
|
var isTrackingWebSocketEvents = false;
|
|
32471
|
-
function trackWebSocketEvents(
|
|
32476
|
+
function trackWebSocketEvents(debugLog2 = websocketDebuglog) {
|
|
32472
32477
|
if (isTrackingWebSocketEvents) {
|
|
32473
32478
|
return;
|
|
32474
32479
|
}
|
|
@@ -32482,9 +32487,9 @@ var require_diagnostics = __commonJS({
|
|
|
32482
32487
|
(evt) => {
|
|
32483
32488
|
if (evt.address != null) {
|
|
32484
32489
|
const { address, port } = evt.address;
|
|
32485
|
-
|
|
32490
|
+
debugLog2("connection opened %s%s", address, port ? `:${port}` : "");
|
|
32486
32491
|
} else {
|
|
32487
|
-
|
|
32492
|
+
debugLog2("connection opened");
|
|
32488
32493
|
}
|
|
32489
32494
|
}
|
|
32490
32495
|
);
|
|
@@ -32492,7 +32497,7 @@ var require_diagnostics = __commonJS({
|
|
|
32492
32497
|
"undici:websocket:close",
|
|
32493
32498
|
(evt) => {
|
|
32494
32499
|
const { websocket, code: code2, reason } = evt;
|
|
32495
|
-
|
|
32500
|
+
debugLog2(
|
|
32496
32501
|
"closed connection to %s - %s %s",
|
|
32497
32502
|
websocket.url,
|
|
32498
32503
|
code2,
|
|
@@ -32503,19 +32508,19 @@ var require_diagnostics = __commonJS({
|
|
|
32503
32508
|
diagnosticsChannel.subscribe(
|
|
32504
32509
|
"undici:websocket:socket_error",
|
|
32505
32510
|
(err2) => {
|
|
32506
|
-
|
|
32511
|
+
debugLog2("connection errored - %s", err2.message);
|
|
32507
32512
|
}
|
|
32508
32513
|
);
|
|
32509
32514
|
diagnosticsChannel.subscribe(
|
|
32510
32515
|
"undici:websocket:ping",
|
|
32511
32516
|
(evt) => {
|
|
32512
|
-
|
|
32517
|
+
debugLog2("ping received");
|
|
32513
32518
|
}
|
|
32514
32519
|
);
|
|
32515
32520
|
diagnosticsChannel.subscribe(
|
|
32516
32521
|
"undici:websocket:pong",
|
|
32517
32522
|
(evt) => {
|
|
32518
|
-
|
|
32523
|
+
debugLog2("pong received");
|
|
32519
32524
|
}
|
|
32520
32525
|
);
|
|
32521
32526
|
}
|
|
@@ -64565,6 +64570,97 @@ function isZwLegitScript(cp) {
|
|
|
64565
64570
|
return cp >= 3584 && cp <= 3711 || cp >= 3712 && cp <= 3839 || cp >= 4096 && cp <= 4255 || cp >= 6016 && cp <= 6143 || cp >= 12352 && cp <= 12543 || cp >= 13312 && cp <= 40959 || cp >= 44032 && cp <= 55215;
|
|
64566
64571
|
}
|
|
64567
64572
|
var isAsciiWordChar = (ch) => !!ch && /[A-Za-z0-9]/.test(ch);
|
|
64573
|
+
var LOOKALIKE = {
|
|
64574
|
+
\u0430: "a",
|
|
64575
|
+
\u0435: "e",
|
|
64576
|
+
\u043E: "o",
|
|
64577
|
+
\u0440: "p",
|
|
64578
|
+
\u0441: "c",
|
|
64579
|
+
\u0443: "y",
|
|
64580
|
+
\u0445: "x",
|
|
64581
|
+
\u0456: "i",
|
|
64582
|
+
\u0458: "j",
|
|
64583
|
+
\u0455: "s",
|
|
64584
|
+
"\u0501": "d",
|
|
64585
|
+
"\u051B": "q",
|
|
64586
|
+
"\u051D": "w",
|
|
64587
|
+
\u0410: "A",
|
|
64588
|
+
\u0412: "B",
|
|
64589
|
+
\u0415: "E",
|
|
64590
|
+
\u041A: "K",
|
|
64591
|
+
\u041C: "M",
|
|
64592
|
+
\u041D: "H",
|
|
64593
|
+
\u041E: "O",
|
|
64594
|
+
\u0420: "P",
|
|
64595
|
+
\u0421: "C",
|
|
64596
|
+
\u0422: "T",
|
|
64597
|
+
\u0425: "X",
|
|
64598
|
+
\u0406: "I",
|
|
64599
|
+
\u0408: "J",
|
|
64600
|
+
\u0405: "S",
|
|
64601
|
+
"\u050C": "G",
|
|
64602
|
+
"\u051A": "Q",
|
|
64603
|
+
"\u051C": "W",
|
|
64604
|
+
\u03BF: "o",
|
|
64605
|
+
\u03B1: "a",
|
|
64606
|
+
\u03C1: "p",
|
|
64607
|
+
\u03C4: "t",
|
|
64608
|
+
\u03BD: "v",
|
|
64609
|
+
\u03C5: "u",
|
|
64610
|
+
\u03BA: "k",
|
|
64611
|
+
\u03C7: "x",
|
|
64612
|
+
\u03B9: "i",
|
|
64613
|
+
\u0391: "A",
|
|
64614
|
+
\u0392: "B",
|
|
64615
|
+
\u0395: "E",
|
|
64616
|
+
\u0397: "H",
|
|
64617
|
+
\u0399: "I",
|
|
64618
|
+
\u039A: "K",
|
|
64619
|
+
\u039C: "M",
|
|
64620
|
+
\u039D: "N",
|
|
64621
|
+
\u039F: "O",
|
|
64622
|
+
\u03A1: "P",
|
|
64623
|
+
\u03A4: "T",
|
|
64624
|
+
\u03A7: "X",
|
|
64625
|
+
\u03A5: "Y",
|
|
64626
|
+
\u0396: "Z",
|
|
64627
|
+
// Armenian (review, round 2)
|
|
64628
|
+
"\u0585": "o",
|
|
64629
|
+
"\u057D": "u",
|
|
64630
|
+
"\u0570": "h",
|
|
64631
|
+
"\u0578": "n",
|
|
64632
|
+
"\u0581": "g",
|
|
64633
|
+
"\u0566": "q"
|
|
64634
|
+
};
|
|
64635
|
+
var MIXED_WORD_RE = /[A-Za-z\u0370-\u03ff\u0400-\u04ff\u0500-\u052f\u0530-\u058f]{4,60}/g;
|
|
64636
|
+
var FOREIGN_LETTER_RE = /[\u0370-\u03ff\u0400-\u04ff\u0500-\u052f\u0530-\u058f]/;
|
|
64637
|
+
var GREEK_RE = /[\u0370-\u03ff]/;
|
|
64638
|
+
var EXPLAINED_LINE_RE = /cyrillic|greek|armenian|homoglyph|confusable|look-?alike/i;
|
|
64639
|
+
var HOMOGLYPH_MAX_LINE = 2e3;
|
|
64640
|
+
function homoglyphWord(content) {
|
|
64641
|
+
const lines = content.split("\n");
|
|
64642
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64643
|
+
const text = lines[i];
|
|
64644
|
+
if (text.length > HOMOGLYPH_MAX_LINE) continue;
|
|
64645
|
+
const visible = text.replace(/<!--[\s\S]*?(?:-->|$)/g, "");
|
|
64646
|
+
if (EXPLAINED_LINE_RE.test(visible)) continue;
|
|
64647
|
+
for (const m of text.matchAll(MIXED_WORD_RE)) {
|
|
64648
|
+
const chars = [...m[0]];
|
|
64649
|
+
const latin = chars.filter((c) => /[A-Za-z]/.test(c)).length;
|
|
64650
|
+
const foreign = chars.filter((c) => FOREIGN_LETTER_RE.test(c));
|
|
64651
|
+
if (latin < 2 || !foreign.some((c) => c in LOOKALIKE) || foreign.length > latin) continue;
|
|
64652
|
+
const lead = chars.findIndex((c) => !FOREIGN_LETTER_RE.test(c));
|
|
64653
|
+
if (lead > 0 && chars.slice(0, lead).every((c) => GREEK_RE.test(c)) && lead === foreign.length)
|
|
64654
|
+
continue;
|
|
64655
|
+
return {
|
|
64656
|
+
folded: chars.map((c) => LOOKALIKE[c] ?? c).join(""),
|
|
64657
|
+
count: foreign.length,
|
|
64658
|
+
line: i + 1
|
|
64659
|
+
};
|
|
64660
|
+
}
|
|
64661
|
+
}
|
|
64662
|
+
return null;
|
|
64663
|
+
}
|
|
64568
64664
|
function suspiciousZeroWidth(text) {
|
|
64569
64665
|
let n = 0;
|
|
64570
64666
|
let first = -1;
|
|
@@ -64808,6 +64904,21 @@ var lineAt = (text, i) => i >= 0 ? lineAtIndex(text, i) : void 0;
|
|
|
64808
64904
|
function analyzeInstructionFile(path91, content) {
|
|
64809
64905
|
const findings = [];
|
|
64810
64906
|
const decoded = decodeSuspiciousBase64(content);
|
|
64907
|
+
const glyph = homoglyphWord(content);
|
|
64908
|
+
if (glyph)
|
|
64909
|
+
findings.push(
|
|
64910
|
+
mk2(
|
|
64911
|
+
"CI-6.homoglyph",
|
|
64912
|
+
"advisory",
|
|
64913
|
+
"A word in an agent instruction file is spelled with look-alike letters",
|
|
64914
|
+
[
|
|
64915
|
+
`\`${glyph.folded}\` is written with ${glyph.count} non-Latin look-alike letter(s): it reads the same to a reviewer, but a search, a replace or a policy check on the real word misses it`
|
|
64916
|
+
],
|
|
64917
|
+
"Spell the word with plain Latin letters, or explain on the same line why the look-alikes are there.",
|
|
64918
|
+
path91,
|
|
64919
|
+
glyph.line
|
|
64920
|
+
)
|
|
64921
|
+
);
|
|
64811
64922
|
if (TAG_CHARS.test(content))
|
|
64812
64923
|
findings.push(
|
|
64813
64924
|
mk2(
|
|
@@ -64946,13 +65057,17 @@ function analyzeInstructionFile(path91, content) {
|
|
|
64946
65057
|
}
|
|
64947
65058
|
|
|
64948
65059
|
// src/ci-check/route.ts
|
|
65060
|
+
var MCP_CONFIG_RE = /\.mcp\.json$|\.cursor\/mcp\.json$|(^|\/)\.vscode\/mcp\.json$|(^|\/)\.kiro\/settings\/mcp\.json$|(^|\/)\.gemini\/settings\.json$|(^|\/)opencode\.json$|(^|\/)\.opencode\/config\.json$/;
|
|
65061
|
+
var ISSUE_FORM_RE = /^\.github\/ISSUE_TEMPLATE\/[^/]+\.(ya?ml|md)$/i;
|
|
64949
65062
|
function routeOf(path91, skillDirs, appDirs = /* @__PURE__ */ new Set()) {
|
|
64950
65063
|
if (path91 === SUPPRESSIONS_FILE) return "suppressions";
|
|
64951
65064
|
if (/\.github\/workflows\/.+\.ya?ml$/.test(path91)) return "workflow";
|
|
65065
|
+
if (ISSUE_FORM_RE.test(path91)) return "issue-form";
|
|
65066
|
+
if (path91 === "package.json" || path91 === "pyproject.toml") return "manifest";
|
|
64952
65067
|
if (/\.claude\/settings(\.local)?\.json$/.test(path91)) return "agent-config";
|
|
64953
65068
|
if (isHookScript(path91)) return "hook-script";
|
|
64954
65069
|
if (isSkillScript(path91, skillDirs)) return "skill-script";
|
|
64955
|
-
if (
|
|
65070
|
+
if (MCP_CONFIG_RE.test(path91)) return "mcp";
|
|
64956
65071
|
if (/(^|\/)\.codex\/config\.toml$/.test(path91)) return "codex";
|
|
64957
65072
|
if (isInstructionFile(path91, skillDirs, appDirs)) return "instruction";
|
|
64958
65073
|
if (isAppDoc(path91, skillDirs, appDirs)) return "app-doc";
|
|
@@ -64984,6 +65099,11 @@ var SURFACE_FILES = [
|
|
|
64984
65099
|
".claude/settings.local.json",
|
|
64985
65100
|
".mcp.json",
|
|
64986
65101
|
".cursor/mcp.json",
|
|
65102
|
+
".vscode/mcp.json",
|
|
65103
|
+
".kiro/settings/mcp.json",
|
|
65104
|
+
".gemini/settings.json",
|
|
65105
|
+
"opencode.json",
|
|
65106
|
+
".opencode/config.json",
|
|
64987
65107
|
".codex/config.toml",
|
|
64988
65108
|
// CI-6: agent instruction files (auto-loaded into the agent's system prompt).
|
|
64989
65109
|
"CLAUDE.md",
|
|
@@ -64995,7 +65115,7 @@ var SURFACE_FILES = [
|
|
|
64995
65115
|
".clinerules"
|
|
64996
65116
|
];
|
|
64997
65117
|
var WORKFLOW_DIR = ".github/workflows";
|
|
64998
|
-
var CONFIG_FILE_RE = /(^|\/)\.claude\/settings(\.local)?\.json$|(^|\/)\.mcp\.json$|(^|\/)\.cursor\/mcp\.json$|(^|\/)\.codex\/config\.toml$/;
|
|
65118
|
+
var CONFIG_FILE_RE = /(^|\/)\.claude\/settings(\.local)?\.json$|(^|\/)\.mcp\.json$|(^|\/)\.cursor\/mcp\.json$|(^|\/)\.codex\/config\.toml$|(^|\/)\.vscode\/mcp\.json$|(^|\/)\.kiro\/settings\/mcp\.json$|(^|\/)\.gemini\/settings\.json$|(^|\/)opencode\.json$|(^|\/)\.opencode\/config\.json$/;
|
|
64999
65119
|
function selectSurface(paths) {
|
|
65000
65120
|
const skillDirs = skillDirsOf(paths);
|
|
65001
65121
|
const appDirs = appSkillDirsOf(paths, skillDirs);
|
|
@@ -65144,6 +65264,7 @@ async function listWorkflowPaths(owner, repo, notes) {
|
|
|
65144
65264
|
return json.filter((e) => e.type === "file" && /\.ya?ml$/.test(e.name)).map((e) => e.path);
|
|
65145
65265
|
}
|
|
65146
65266
|
var ROOT_WORKFLOW_RE = /^\.github\/workflows\/[^/]+\.ya?ml$/;
|
|
65267
|
+
var MAX_ISSUE_FORMS = 40;
|
|
65147
65268
|
function kindOfMode(mode) {
|
|
65148
65269
|
if (mode === "120000") return "link";
|
|
65149
65270
|
if (/^100[0-7]{3}$/.test(mode)) return "file";
|
|
@@ -65353,8 +65474,15 @@ function planSurface(paths, notes, truncated) {
|
|
|
65353
65474
|
const present = new Set(paths);
|
|
65354
65475
|
const root = SURFACE_FILES.filter((p) => present.has(p));
|
|
65355
65476
|
const workflows = paths.filter((p) => ROOT_WORKFLOW_RE.test(p));
|
|
65477
|
+
const allForms = paths.filter((p) => ISSUE_FORM_RE.test(p));
|
|
65478
|
+
const forms = allForms.slice(0, MAX_ISSUE_FORMS);
|
|
65479
|
+
if (allForms.length > MAX_ISSUE_FORMS)
|
|
65480
|
+
notes.push(
|
|
65481
|
+
`${allForms.length} issue forms; read the first ${MAX_ISSUE_FORMS} (a label an unread form applies is not known): results ${INCOMPLETE}.`
|
|
65482
|
+
);
|
|
65483
|
+
forms.push(...["package.json", "pyproject.toml"].filter((p) => present.has(p)));
|
|
65356
65484
|
const nested = pickSurfacePaths(paths, truncated, notes, { files: Number.POSITIVE_INFINITY });
|
|
65357
|
-
return [.../* @__PURE__ */ new Set([...root, ...workflows, ...nested])];
|
|
65485
|
+
return [.../* @__PURE__ */ new Set([...root, ...workflows, ...forms, ...nested])];
|
|
65358
65486
|
}
|
|
65359
65487
|
function planReads(l, R, opts = {}) {
|
|
65360
65488
|
const notes = [];
|
|
@@ -65420,7 +65548,7 @@ function planReads(l, R, opts = {}) {
|
|
|
65420
65548
|
function capNested(reads, cap, notes) {
|
|
65421
65549
|
if (cap === void 0) return reads;
|
|
65422
65550
|
const rootSet = new Set(SURFACE_FILES);
|
|
65423
|
-
const isRoot = (r) => rootSet.has(r.rel) || ROOT_WORKFLOW_RE.test(r.rel);
|
|
65551
|
+
const isRoot = (r) => rootSet.has(r.rel) || ROOT_WORKFLOW_RE.test(r.rel) || ISSUE_FORM_RE.test(r.rel) || r.rel === "package.json" || r.rel === "pyproject.toml";
|
|
65424
65552
|
const nested = reads.filter((r) => !isRoot(r));
|
|
65425
65553
|
if (nested.length < cap) return reads;
|
|
65426
65554
|
const keep = new Set(nested.slice(0, cap));
|
|
@@ -65516,6 +65644,45 @@ function readLocalFile(abs, rel) {
|
|
|
65516
65644
|
if (fd !== void 0) import_fs74.default.closeSync(fd);
|
|
65517
65645
|
}
|
|
65518
65646
|
}
|
|
65647
|
+
function readSmallFile(p, max) {
|
|
65648
|
+
let fd;
|
|
65649
|
+
try {
|
|
65650
|
+
fd = import_fs74.default.openSync(p, import_fs74.default.constants.O_RDONLY | (import_fs74.default.constants.O_NOFOLLOW ?? 0));
|
|
65651
|
+
const st = import_fs74.default.fstatSync(fd);
|
|
65652
|
+
if (!st.isFile() || st.size > max) return null;
|
|
65653
|
+
const buf = Buffer.alloc(st.size);
|
|
65654
|
+
const n = import_fs74.default.readSync(fd, buf, 0, st.size, 0);
|
|
65655
|
+
return buf.subarray(0, n).toString("utf8");
|
|
65656
|
+
} catch {
|
|
65657
|
+
return null;
|
|
65658
|
+
} finally {
|
|
65659
|
+
if (fd !== void 0) import_fs74.default.closeSync(fd);
|
|
65660
|
+
}
|
|
65661
|
+
}
|
|
65662
|
+
function localGitHubOwner(root) {
|
|
65663
|
+
let cfg = readSmallFile(import_path71.default.join(root, ".git", "config"), 1e6);
|
|
65664
|
+
if (cfg === null) {
|
|
65665
|
+
const pointer = readSmallFile(import_path71.default.join(root, ".git"), 4096);
|
|
65666
|
+
const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
|
|
65667
|
+
if (!m) return void 0;
|
|
65668
|
+
let gitDir = import_path71.default.resolve(root, m[1]);
|
|
65669
|
+
const common = readSmallFile(import_path71.default.join(gitDir, "commondir"), 4096);
|
|
65670
|
+
if (common !== null) gitDir = import_path71.default.resolve(gitDir, common.trim());
|
|
65671
|
+
cfg = readSmallFile(import_path71.default.join(gitDir, "config"), 1e6);
|
|
65672
|
+
if (cfg === null) return void 0;
|
|
65673
|
+
}
|
|
65674
|
+
let inOrigin = false;
|
|
65675
|
+
for (const line of cfg.split(/\r?\n/)) {
|
|
65676
|
+
const sec = /^\s*\[\s*([^\]\s]+)(?:\s+"([^"]*)")?\s*\]/.exec(line);
|
|
65677
|
+
if (sec) {
|
|
65678
|
+
inOrigin = sec[1].toLowerCase() === "remote" && sec[2] === "origin";
|
|
65679
|
+
continue;
|
|
65680
|
+
}
|
|
65681
|
+
const url = inOrigin && /^\s*url\s*=\s*(.+?)\s*$/i.exec(line);
|
|
65682
|
+
if (url) return /github\.com[:/]([\w.-]{1,100})\//i.exec(url[1])?.[1];
|
|
65683
|
+
}
|
|
65684
|
+
return void 0;
|
|
65685
|
+
}
|
|
65519
65686
|
function readLocalTree(dir) {
|
|
65520
65687
|
const root = dir.replace(/^~/, process.env.HOME ?? "~");
|
|
65521
65688
|
const notes = [];
|
|
@@ -65545,8 +65712,10 @@ function readLocalTree(dir) {
|
|
|
65545
65712
|
}
|
|
65546
65713
|
});
|
|
65547
65714
|
}
|
|
65715
|
+
const owner = localGitHubOwner(root);
|
|
65548
65716
|
return {
|
|
65549
65717
|
source: root,
|
|
65718
|
+
...owner ? { owner } : {},
|
|
65550
65719
|
files,
|
|
65551
65720
|
notes,
|
|
65552
65721
|
paths: plan.paths,
|
|
@@ -65659,8 +65828,10 @@ function readGitRefTree(dir, ref) {
|
|
|
65659
65828
|
}
|
|
65660
65829
|
files.push({ path: rel, content, route });
|
|
65661
65830
|
}
|
|
65831
|
+
const owner = localGitHubOwner(root);
|
|
65662
65832
|
return {
|
|
65663
65833
|
source: `${root}@${ref}`,
|
|
65834
|
+
...owner ? { owner } : {},
|
|
65664
65835
|
files,
|
|
65665
65836
|
notes,
|
|
65666
65837
|
paths: plan.paths,
|
|
@@ -65760,7 +65931,13 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65760
65931
|
const listed2 = await listTree(owner, repo, notes);
|
|
65761
65932
|
if (!listed2) {
|
|
65762
65933
|
const workflowPaths = await listWorkflowPaths(owner, repo, notes);
|
|
65763
|
-
const allPaths = [
|
|
65934
|
+
const allPaths = [
|
|
65935
|
+
.../* @__PURE__ */ new Set([...SURFACE_FILES, ...workflowPaths, "package.json", "pyproject.toml"])
|
|
65936
|
+
];
|
|
65937
|
+
if (!notes.includes(RATE_LIMIT_NOTE) && !notes.includes(NETWORK_NOTE))
|
|
65938
|
+
notes.push(
|
|
65939
|
+
`the repository tree could not be listed; only the root files and the workflows were read: nested agent files ${INCOMPLETE}.`
|
|
65940
|
+
);
|
|
65764
65941
|
const fetched2 = await pooled(
|
|
65765
65942
|
allPaths,
|
|
65766
65943
|
FETCH_CONCURRENCY,
|
|
@@ -65768,6 +65945,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65768
65945
|
);
|
|
65769
65946
|
return {
|
|
65770
65947
|
source: `${owner}/${repo}`,
|
|
65948
|
+
owner,
|
|
65771
65949
|
files: fetched2.filter((f) => !!f),
|
|
65772
65950
|
notes
|
|
65773
65951
|
};
|
|
@@ -65823,6 +66001,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65823
66001
|
notes.push(...session.closingNotes());
|
|
65824
66002
|
return {
|
|
65825
66003
|
source: `${owner}/${repo}`,
|
|
66004
|
+
owner,
|
|
65826
66005
|
files: fetched.filter((f) => !!f),
|
|
65827
66006
|
notes,
|
|
65828
66007
|
paths: plan.paths,
|
|
@@ -65833,7 +66012,7 @@ async function fetchGitHubTree(owner, repo, onProgress) {
|
|
|
65833
66012
|
notes.push(
|
|
65834
66013
|
`fetch degraded (${err2?.message ?? "network error"}): results ${INCOMPLETE} (the repo could not be fetched).`
|
|
65835
66014
|
);
|
|
65836
|
-
return { source: `${owner}/${repo}`, files: [], notes };
|
|
66015
|
+
return { source: `${owner}/${repo}`, owner, files: [], notes };
|
|
65837
66016
|
}
|
|
65838
66017
|
}
|
|
65839
66018
|
async function fetchTree(input, onProgress) {
|
|
@@ -65996,7 +66175,15 @@ function explainGrant(f) {
|
|
|
65996
66175
|
const git = f.broad.some((a) => /^Bash\(git:/.test(a));
|
|
65997
66176
|
const files = f.broad.some((a) => /^Write|^Edit$/.test(a));
|
|
65998
66177
|
const template = TEMPLATE_PATH_RE.test(f.file);
|
|
65999
|
-
const
|
|
66178
|
+
const runner = f.broad.some((a) => !/^Bash\(git:|^Write|^Edit$/.test(a));
|
|
66179
|
+
const what = f.bareShell ? "run commands and change files" : sentence(
|
|
66180
|
+
[
|
|
66181
|
+
runner ? "run programs" : "",
|
|
66182
|
+
git ? "run git commands" : "",
|
|
66183
|
+
files ? "change files" : ""
|
|
66184
|
+
].filter(Boolean),
|
|
66185
|
+
true
|
|
66186
|
+
);
|
|
66000
66187
|
const saw = [];
|
|
66001
66188
|
if (template) saw.push("This is a template: every project made from it gets these permissions.");
|
|
66002
66189
|
saw.push(
|
|
@@ -66036,14 +66223,17 @@ function npxPackage(argv) {
|
|
|
66036
66223
|
return null;
|
|
66037
66224
|
}
|
|
66038
66225
|
function explainMcp(f) {
|
|
66039
|
-
const pkg = npxPackage(f.argv);
|
|
66040
|
-
const
|
|
66226
|
+
const pkg = f.pkg ?? npxPackage(f.argv);
|
|
66227
|
+
const registry = f.registry ?? "npm";
|
|
66228
|
+
const image = registry === "the image registry";
|
|
66229
|
+
const what = pkg ? `the newest ${code(pkg)}` : image ? "the newest image" : "the newest version of the package";
|
|
66230
|
+
const example = !pkg ? null : image ? `${pkg}@sha256:\u2026` : registry === "PyPI" ? `${pkg}==x.y.z` : registry === "the Go module proxy" ? `${pkg}@vX.Y.Z` : `${pkg}@x.y.z`;
|
|
66041
66231
|
return {
|
|
66042
66232
|
headline: `MCP server ${code(f.name, 60)} installs whatever version is newest, every time`,
|
|
66043
|
-
happens: `Each run downloads
|
|
66233
|
+
happens: `Each run downloads ${what} from ${registry}. If it is ever taken over, the next version runs on every developer's computer.`,
|
|
66044
66234
|
saw: [`It starts with ${code(f.argv, 120)}.`],
|
|
66045
66235
|
fix: [
|
|
66046
|
-
|
|
66236
|
+
example ? `Pin an exact version, for example ${code(example)}.` : "Pin an exact version."
|
|
66047
66237
|
]
|
|
66048
66238
|
};
|
|
66049
66239
|
}
|
|
@@ -66067,6 +66257,65 @@ function parseYamlStrict(text) {
|
|
|
66067
66257
|
return doc.toJS();
|
|
66068
66258
|
}
|
|
66069
66259
|
|
|
66260
|
+
// src/ci-check/frontmatter.ts
|
|
66261
|
+
function parseFrontmatter(content) {
|
|
66262
|
+
const text = content.replace(/^/, "").replace(/^\s*\n/, "");
|
|
66263
|
+
if (!/^---[ \t]*\r?\n/.test(text)) return null;
|
|
66264
|
+
const close = /\r?\n---[ \t]*(\r?\n|$)/.exec(text.slice(3));
|
|
66265
|
+
if (!close) return null;
|
|
66266
|
+
const block = text.slice(3, 3 + close.index);
|
|
66267
|
+
try {
|
|
66268
|
+
const parsed = parseYamlStrict(block);
|
|
66269
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
66270
|
+
} catch {
|
|
66271
|
+
return salvageTopLevel(block);
|
|
66272
|
+
}
|
|
66273
|
+
}
|
|
66274
|
+
function salvageTopLevel(block) {
|
|
66275
|
+
const out = {};
|
|
66276
|
+
const lines = block.split(/\r?\n/);
|
|
66277
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66278
|
+
const m = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i]);
|
|
66279
|
+
if (!m) continue;
|
|
66280
|
+
const [, key, rest] = m;
|
|
66281
|
+
if (rest.trim()) {
|
|
66282
|
+
out[key] = rest.trim();
|
|
66283
|
+
continue;
|
|
66284
|
+
}
|
|
66285
|
+
const items = [];
|
|
66286
|
+
while (i + 1 < lines.length && /^[ \t]+-[ \t]+/.test(lines[i + 1])) {
|
|
66287
|
+
items.push(lines[++i].replace(/^[ \t]+-[ \t]+/, "").trim());
|
|
66288
|
+
}
|
|
66289
|
+
if (items.length) out[key] = items;
|
|
66290
|
+
}
|
|
66291
|
+
return Object.keys(out).length ? out : null;
|
|
66292
|
+
}
|
|
66293
|
+
function splitGrants(value) {
|
|
66294
|
+
const out = [];
|
|
66295
|
+
let depth = 0;
|
|
66296
|
+
let cur = "";
|
|
66297
|
+
for (const ch of value) {
|
|
66298
|
+
if (ch === "(") depth++;
|
|
66299
|
+
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
66300
|
+
if (depth === 0 && (ch === "," || /\s/.test(ch))) {
|
|
66301
|
+
if (cur) out.push(cur);
|
|
66302
|
+
cur = "";
|
|
66303
|
+
continue;
|
|
66304
|
+
}
|
|
66305
|
+
cur += ch;
|
|
66306
|
+
}
|
|
66307
|
+
if (cur) out.push(cur);
|
|
66308
|
+
return out;
|
|
66309
|
+
}
|
|
66310
|
+
function allowedToolsOf(fm) {
|
|
66311
|
+
const raw = fm?.["allowed-tools"];
|
|
66312
|
+
if (typeof raw === "string") return splitGrants(raw);
|
|
66313
|
+
if (Array.isArray(raw)) {
|
|
66314
|
+
return raw.filter((x) => typeof x === "string").map((x) => x.trim()).filter(Boolean);
|
|
66315
|
+
}
|
|
66316
|
+
return [];
|
|
66317
|
+
}
|
|
66318
|
+
|
|
66070
66319
|
// src/ci-check/types.ts
|
|
66071
66320
|
var SEVERITY_RANK2 = {
|
|
66072
66321
|
critical: 4,
|
|
@@ -66075,11 +66324,28 @@ var SEVERITY_RANK2 = {
|
|
|
66075
66324
|
advisory: 1
|
|
66076
66325
|
};
|
|
66077
66326
|
|
|
66327
|
+
// src/ci-check/listing.ts
|
|
66328
|
+
var SAFE_LIST_FIELD_RE = /^(?:number|url|html_url|id|node_id|sha|state|createdat|updatedat|closedat|mergedat|headrefname|baserefname|isdraft|headrefoid|login|user|author_association)$/i;
|
|
66329
|
+
function sanitizedListing(t) {
|
|
66330
|
+
if (t.length > 5e4) return false;
|
|
66331
|
+
if (/\|\s*grep\s+-[a-zA-Z]*o/.test(t)) return true;
|
|
66332
|
+
const fieldsSafe = (expr, fieldRe) => {
|
|
66333
|
+
const fields = [...expr.matchAll(fieldRe)].map((m) => m[1]);
|
|
66334
|
+
return fields.length > 0 && fields.every((f) => SAFE_LIST_FIELD_RE.test(f));
|
|
66335
|
+
};
|
|
66336
|
+
const jq = [
|
|
66337
|
+
...t.matchAll(/(?:--jq|\s-q)[\s=]+(?:'([^']{0,500})'|"([^"]{0,500})"|([^\s'"]{1,200}))/g)
|
|
66338
|
+
].map((m) => m[1] ?? m[2] ?? m[3]);
|
|
66339
|
+
if (jq.length) return jq.every((e) => fieldsSafe(e, /\.([A-Za-z_]\w{0,60})/g));
|
|
66340
|
+
const json = [...t.matchAll(/--json[\s=]+['"]?([\w,]{1,500})/g)].map((m) => m[1]);
|
|
66341
|
+
return json.length > 0 && json.every((e) => fieldsSafe(e, /([A-Za-z_]\w{0,60})/g));
|
|
66342
|
+
}
|
|
66343
|
+
|
|
66078
66344
|
// src/ci-check/workflows.ts
|
|
66079
66345
|
var AGENT_ACTION_RE = /(anthropics\/claude-code(-base)?-action|anthropics\/claude-code|^\s*[\w.-]{1,100}\/claude-code-action@|openai\/codex|codex-action|run-?aider|aider-?action|google-github-actions\/run-gemini)/i;
|
|
66080
66346
|
var BROAD_TOOL_RE = /(^|["\s,])(Bash|Write|Edit)(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|git push|git commit|git config|git:|rm|sh|bash|eval|npx|pip)/i;
|
|
66081
66347
|
var EXFIL_RCE_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\((curl|wget|sh[\s):]|eval|rm[\s):]|git push)/i;
|
|
66082
|
-
var GH_WRITE_TOOL_RE = /Bash\(\s*(gh api|gh:|gh (pr|issue) (comment|edit|merge|close|review|create|ready|lock|reopen)|git push|git:)/i;
|
|
66348
|
+
var GH_WRITE_TOOL_RE = /Bash\(\s*(gh api|gh:|gh (pr|issue) (comment|edit|merge|close|review|create|ready|lock|reopen)|gh (pr|issue)\s*[:*)]|git push|git:)/i;
|
|
66083
66349
|
var EGRESS_TOOL_RE = /(^|["\s,])Bash(\s|,|"|$)|Bash\(\s*\*|Bash\(\s*(curl|wget|python|python3|node|deno|bun|ruby|perl|php|pip|pipx|npx|nc|ncat|ssh|scp|http|gh api)|WebFetch|WebSearch/i;
|
|
66084
66350
|
var MEMBERSHIP_CHECK_RE = /orgs\/[^/'"\s]+\/memberships\/|teams\/[^/'"\s]+\/memberships\/|getCollaboratorPermissionLevel|collaborators\/[^/'"\s]+\/permission|checkMembershipForUser|getMembershipForUser/i;
|
|
66085
66351
|
var STRANGER_ISSUE_TYPES = /* @__PURE__ */ new Set(["opened", "edited", "reopened", "closed"]);
|
|
@@ -66135,12 +66401,69 @@ function keyStrangerFirable(on, key) {
|
|
|
66135
66401
|
return false;
|
|
66136
66402
|
}
|
|
66137
66403
|
}
|
|
66404
|
+
var AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66405
|
+
function issueFormLabels(content) {
|
|
66406
|
+
const text = content.length > 2e5 ? content.slice(0, 2e5) : content;
|
|
66407
|
+
let value;
|
|
66408
|
+
let parsed = false;
|
|
66409
|
+
if (/^\uFEFF?\s*---[ \t]*\r?\n/.test(text)) {
|
|
66410
|
+
const fm = parseFrontmatter(text);
|
|
66411
|
+
parsed = fm !== null;
|
|
66412
|
+
value = fm?.["labels"];
|
|
66413
|
+
} else {
|
|
66414
|
+
try {
|
|
66415
|
+
const doc = parseYamlStrict(text);
|
|
66416
|
+
parsed = true;
|
|
66417
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc))
|
|
66418
|
+
value = doc["labels"];
|
|
66419
|
+
} catch {
|
|
66420
|
+
}
|
|
66421
|
+
}
|
|
66422
|
+
const raw = parsed ? Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [] : labelLines(text);
|
|
66423
|
+
return raw.filter((l) => typeof l === "string").map(
|
|
66424
|
+
(l) => l.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase()
|
|
66425
|
+
).filter((l) => l && l.length <= 100);
|
|
66426
|
+
}
|
|
66427
|
+
function labelLines(text) {
|
|
66428
|
+
const out = [];
|
|
66429
|
+
const lines = text.split(/\r?\n/);
|
|
66430
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66431
|
+
const m = /^labels:[ \t]*([^#]*)/.exec(lines[i]);
|
|
66432
|
+
if (!m) continue;
|
|
66433
|
+
const inline = m[1].trim();
|
|
66434
|
+
if (inline) {
|
|
66435
|
+
out.push(...inline.replace(/^\[|\]$/g, "").split(","));
|
|
66436
|
+
} else {
|
|
66437
|
+
for (let j = i + 1; j < lines.length && /^\s*-\s/.test(lines[j]); j++)
|
|
66438
|
+
out.push(lines[j].replace(/^\s*-\s/, "").replace(/\s#.*$/, ""));
|
|
66439
|
+
}
|
|
66440
|
+
}
|
|
66441
|
+
return out;
|
|
66442
|
+
}
|
|
66443
|
+
var CALLERS = /* @__PURE__ */ new WeakMap();
|
|
66138
66444
|
function triggerReach(wf, raw) {
|
|
66139
66445
|
const on = onObject(wf, raw) ?? {};
|
|
66140
66446
|
const keys = triggerKeys(wf, raw);
|
|
66141
|
-
const firable = keys.filter(
|
|
66447
|
+
const firable = keys.filter(
|
|
66448
|
+
(k) => keyStrangerFirable(on, k) && !(/^workflow_run$/i.test(k) && upstreamRunTrusted(wf, raw)) || /^issues$/i.test(k) && testsAutoLabel(allIfText(wf))
|
|
66449
|
+
);
|
|
66142
66450
|
const untrusted = firable.length > 0;
|
|
66143
66451
|
const reusable = !untrusted && keys.some((k) => /^workflow_call$/i.test(k));
|
|
66452
|
+
const callers = CALLERS.get(wf);
|
|
66453
|
+
const open = (callers ?? []).filter((c) => c.untrusted && !c.gated && !c.dispatched);
|
|
66454
|
+
const unresolved = (callers ?? []).some(
|
|
66455
|
+
(c) => c.untrusted && !c.gated && c.dispatched || c.reusable && !c.gated
|
|
66456
|
+
);
|
|
66457
|
+
if (reusable && callers?.length && (open.length || !unresolved)) {
|
|
66458
|
+
return {
|
|
66459
|
+
keys: [...new Set(open.flatMap((c) => c.keys))],
|
|
66460
|
+
untrusted: open.length > 0,
|
|
66461
|
+
reusable: false,
|
|
66462
|
+
secretExposed: open.some((c) => c.secretExposed),
|
|
66463
|
+
privileged: open.some((c) => c.privileged),
|
|
66464
|
+
workflowRun: open.some((c) => c.workflowRun)
|
|
66465
|
+
};
|
|
66466
|
+
}
|
|
66144
66467
|
const secretExposed = firable.some((t) => /pull_request_target|workflow_run/i.test(t));
|
|
66145
66468
|
const privileged = untrusted && firable.some(
|
|
66146
66469
|
(t) => /pull_request_target|pull_request_review|workflow_run|issue|discussion/i.test(t)
|
|
@@ -66151,6 +66474,162 @@ function triggerReach(wf, raw) {
|
|
|
66151
66474
|
function jobList(wf) {
|
|
66152
66475
|
return Object.values(wf.jobs ?? {}).filter((j) => j != null);
|
|
66153
66476
|
}
|
|
66477
|
+
var ENV_REF_RE = /\$\{\{\s*env(?:\.([A-Za-z_][A-Za-z0-9_]{0,100})|\[\s*['"]([A-Za-z_][A-Za-z0-9_]{0,100})['"]\s*\])\s*\}\}/g;
|
|
66478
|
+
var ENV_REF_HOPS = 3;
|
|
66479
|
+
var RESOLVED_MAX_LEN = 1e5;
|
|
66480
|
+
function resolveEnvRefs(v, envs) {
|
|
66481
|
+
const holderOf = (name) => envs.find((e) => e != null && typeof e === "object" && name in e);
|
|
66482
|
+
for (let hop = 0; hop < ENV_REF_HOPS && v.includes("env"); hop++) {
|
|
66483
|
+
let projected = v.length;
|
|
66484
|
+
let any = false;
|
|
66485
|
+
for (const m of v.matchAll(ENV_REF_RE)) {
|
|
66486
|
+
const name = m[1] ?? m[2];
|
|
66487
|
+
const holder = holderOf(name);
|
|
66488
|
+
if (holder) {
|
|
66489
|
+
any = true;
|
|
66490
|
+
projected += str(holder[name]).length - m[0].length;
|
|
66491
|
+
}
|
|
66492
|
+
if (projected > RESOLVED_MAX_LEN) return v;
|
|
66493
|
+
}
|
|
66494
|
+
if (!any) break;
|
|
66495
|
+
v = v.replace(ENV_REF_RE, (whole, a, b) => {
|
|
66496
|
+
const name = a ?? b;
|
|
66497
|
+
const holder = holderOf(name);
|
|
66498
|
+
return holder ? str(holder[name]) : whole;
|
|
66499
|
+
});
|
|
66500
|
+
}
|
|
66501
|
+
return v;
|
|
66502
|
+
}
|
|
66503
|
+
function ifNeverTrue(v) {
|
|
66504
|
+
if (v === false) return true;
|
|
66505
|
+
if (typeof v !== "string") return false;
|
|
66506
|
+
const never = (x2) => {
|
|
66507
|
+
switch (x2.k) {
|
|
66508
|
+
case "atom":
|
|
66509
|
+
return /^false$/i.test(x2.t.trim());
|
|
66510
|
+
case "not":
|
|
66511
|
+
return false;
|
|
66512
|
+
case "and":
|
|
66513
|
+
return x2.c.some(never);
|
|
66514
|
+
case "or":
|
|
66515
|
+
return x2.c.every(never);
|
|
66516
|
+
}
|
|
66517
|
+
};
|
|
66518
|
+
const x = parseIf(v);
|
|
66519
|
+
return x !== null && never(x);
|
|
66520
|
+
}
|
|
66521
|
+
function normalizeWorkflow(raw) {
|
|
66522
|
+
const wf = raw;
|
|
66523
|
+
const wfEnv = raw.env;
|
|
66524
|
+
const jobs = wf.jobs;
|
|
66525
|
+
if (!jobs || typeof jobs !== "object") return wf;
|
|
66526
|
+
const wfEnvEvent = EVENT_CONTEXT_RE.test(str(wfEnv));
|
|
66527
|
+
for (const [name, job] of Object.entries(jobs)) {
|
|
66528
|
+
if (job == null || typeof job !== "object") continue;
|
|
66529
|
+
if (ifNeverTrue(job.if)) {
|
|
66530
|
+
delete jobs[name];
|
|
66531
|
+
continue;
|
|
66532
|
+
}
|
|
66533
|
+
if (!Array.isArray(job.steps)) continue;
|
|
66534
|
+
job.steps = job.steps.map(
|
|
66535
|
+
(st) => st != null && typeof st === "object" && !Array.isArray(st) ? { ...st } : st
|
|
66536
|
+
);
|
|
66537
|
+
const scopeEnv = {
|
|
66538
|
+
...wfEnv && typeof wfEnv === "object" ? wfEnv : {},
|
|
66539
|
+
...job.env && typeof job.env === "object" ? job.env : {}
|
|
66540
|
+
};
|
|
66541
|
+
const scopeEnvEvent = wfEnvEvent || EVENT_CONTEXT_RE.test(str(job.env));
|
|
66542
|
+
for (const step of job.steps) {
|
|
66543
|
+
if (step == null || typeof step !== "object") continue;
|
|
66544
|
+
step.scopeEnv = scopeEnv;
|
|
66545
|
+
step.scopeEnvEvent = scopeEnvEvent;
|
|
66546
|
+
if (step.with == null || typeof step.with !== "object") continue;
|
|
66547
|
+
const out = {};
|
|
66548
|
+
for (const [k, v] of Object.entries(step.with)) {
|
|
66549
|
+
const key = k.toLowerCase();
|
|
66550
|
+
if (key in out) continue;
|
|
66551
|
+
out[key] = typeof v === "string" ? resolveEnvRefs(v, [step.env, job.env, wfEnv]) : v;
|
|
66552
|
+
}
|
|
66553
|
+
step.with = out;
|
|
66554
|
+
}
|
|
66555
|
+
}
|
|
66556
|
+
return wf;
|
|
66557
|
+
}
|
|
66558
|
+
var LOADED = null;
|
|
66559
|
+
function loadWorkflow(content) {
|
|
66560
|
+
if (LOADED !== null && LOADED.content === content) return LOADED;
|
|
66561
|
+
const raw = parseYamlStrict(content) ?? {};
|
|
66562
|
+
const wf = raw && typeof raw === "object" ? normalizeWorkflow(raw) : raw;
|
|
66563
|
+
LOADED = { content, raw, wf };
|
|
66564
|
+
return LOADED;
|
|
66565
|
+
}
|
|
66566
|
+
var LOCAL_WORKFLOW_USES_RE = /^\s*\.\/\.github\/workflows\/([\w.-]{1,200}\.ya?ml)\s*$/i;
|
|
66567
|
+
function localCallsOf(path91, content, autoLabels, trustedRunSources) {
|
|
66568
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
66569
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
66570
|
+
try {
|
|
66571
|
+
return localCallsOfInner(path91, content);
|
|
66572
|
+
} finally {
|
|
66573
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66574
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
66575
|
+
}
|
|
66576
|
+
}
|
|
66577
|
+
function localCallsOfInner(path91, content) {
|
|
66578
|
+
let raw;
|
|
66579
|
+
try {
|
|
66580
|
+
raw = loadWorkflow(content).raw;
|
|
66581
|
+
} catch {
|
|
66582
|
+
return [];
|
|
66583
|
+
}
|
|
66584
|
+
if (!raw || typeof raw !== "object") return [];
|
|
66585
|
+
const wf = raw;
|
|
66586
|
+
CALLERS.delete(wf);
|
|
66587
|
+
const reach = triggerReach(wf, raw);
|
|
66588
|
+
const out = [];
|
|
66589
|
+
for (const job of jobList(wf)) {
|
|
66590
|
+
const m = LOCAL_WORKFLOW_USES_RE.exec(str(job.uses));
|
|
66591
|
+
if (!m) continue;
|
|
66592
|
+
out.push({
|
|
66593
|
+
target: `.github/workflows/${m[1]}`,
|
|
66594
|
+
caller: {
|
|
66595
|
+
file: path91,
|
|
66596
|
+
keys: reach.keys,
|
|
66597
|
+
untrusted: reach.untrusted,
|
|
66598
|
+
secretExposed: reach.secretExposed,
|
|
66599
|
+
privileged: reach.privileged,
|
|
66600
|
+
workflowRun: reach.workflowRun,
|
|
66601
|
+
gated: jobGated(job, wf, raw),
|
|
66602
|
+
dispatched: dispatchedByGatedJob(job, wf, raw),
|
|
66603
|
+
reusable: reach.reusable
|
|
66604
|
+
}
|
|
66605
|
+
});
|
|
66606
|
+
}
|
|
66607
|
+
return out;
|
|
66608
|
+
}
|
|
66609
|
+
function dispatchedByGatedJob(job, wf, raw) {
|
|
66610
|
+
const names = [...str(job.if).matchAll(/needs\.([\w-]{1,100})\.outputs\./g)].map((m) => m[1]);
|
|
66611
|
+
if (!names.length) return false;
|
|
66612
|
+
const jobs = wf.jobs ?? {};
|
|
66613
|
+
const label3 = labelTypeConfigured(wf, raw);
|
|
66614
|
+
const someBranchGated = (x) => {
|
|
66615
|
+
switch (x.k) {
|
|
66616
|
+
case "atom":
|
|
66617
|
+
return exprGated(x, label3);
|
|
66618
|
+
case "not":
|
|
66619
|
+
return false;
|
|
66620
|
+
case "and":
|
|
66621
|
+
case "or":
|
|
66622
|
+
return x.c.some(someBranchGated);
|
|
66623
|
+
}
|
|
66624
|
+
};
|
|
66625
|
+
return names.every((n) => {
|
|
66626
|
+
const up = jobs[n];
|
|
66627
|
+
if (up == null) return false;
|
|
66628
|
+
if (jobGated(up, wf, raw)) return true;
|
|
66629
|
+
const x = parseIf(str(up.if));
|
|
66630
|
+
return x !== null && someBranchGated(x);
|
|
66631
|
+
});
|
|
66632
|
+
}
|
|
66154
66633
|
function allSteps(wf) {
|
|
66155
66634
|
const out = [];
|
|
66156
66635
|
for (const job of jobList(wf)) {
|
|
@@ -66158,11 +66637,47 @@ function allSteps(wf) {
|
|
|
66158
66637
|
}
|
|
66159
66638
|
return out;
|
|
66160
66639
|
}
|
|
66640
|
+
var AGENT_CLI_RE = new RegExp(
|
|
66641
|
+
[
|
|
66642
|
+
// The binary at a command start, then its "do work" flag anywhere on the same command
|
|
66643
|
+
// (`claude --model x -p`, `claude "$Q" --print`, `npx @anthropic-ai/claude-code -p`): the
|
|
66644
|
+
// flag had to come first (review, round 2). Bounded: at most 300 characters per command.
|
|
66645
|
+
String.raw`(?:^|[\s|;&(])(?:claude|@anthropic-ai/claude-code(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\s(?:-p|--print|--continue|--resume)(?=[\s"'=]|$)`,
|
|
66646
|
+
String.raw`(?:^|[\s|;&(])(?:codex|@openai/codex(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\sexec\b`,
|
|
66647
|
+
String.raw`(?:^|[\s|;&(])(?:gemini|@google/gemini-cli(?:@[\w.-]{1,40})?)(?=\s)[^\n;&|]{0,300}?\s(?:-p|--prompt)(?=[\s"'=]|$)`,
|
|
66648
|
+
String.raw`(?:^|[\s|;&(])copilot(?=\s)[^\n;&|]{0,300}?\s(?:-p|--prompt|--allow-all-tools)(?=[\s"'=]|$)`,
|
|
66649
|
+
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)`,
|
|
66650
|
+
String.raw`api\.devin\.ai/|software-agent-sdk|\bagent_script\.py\b`
|
|
66651
|
+
].join("|"),
|
|
66652
|
+
"i"
|
|
66653
|
+
);
|
|
66654
|
+
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;
|
|
66655
|
+
var EVENT_CONTEXT_RE = /github\.event|context\.payload/;
|
|
66656
|
+
function isRunAgent(step) {
|
|
66657
|
+
return typeof step.run === "string" && !step.uses && AGENT_CLI_RE.test(step.run);
|
|
66658
|
+
}
|
|
66659
|
+
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;
|
|
66660
|
+
var AGENT_PATH_RE = /(?:^|[/_.-])(claude|codex|gemini|opencode|goose|aider|kilo(?:code)?)(?:$|[/_.-])/i;
|
|
66661
|
+
var PROMPT_INPUT_RE = /^(prompt|direct_prompt|prompt_file|prompt-file|agent)$/i;
|
|
66662
|
+
function isWrapperAgent(step) {
|
|
66663
|
+
const uses = str(step.uses);
|
|
66664
|
+
const w = step.with ?? {};
|
|
66665
|
+
if (Object.keys(w).some((k) => FUEL_WITH_KEY_RE.test(k))) return true;
|
|
66666
|
+
const env = { ...step.scopeEnv ?? {}, ...step.env ?? {} };
|
|
66667
|
+
const fuelEnv = Object.keys(env).some((k) => RUN_FUEL_ENV_RE.test(k));
|
|
66668
|
+
if (/^\s*\.\//.test(uses)) return fuelEnv;
|
|
66669
|
+
const pathPart = uses.replace(/^\s*[^/@\s]{1,100}\//, "").replace(/@.*$/, "");
|
|
66670
|
+
return AGENT_PATH_RE.test(pathPart) && (Object.keys(w).some((k) => PROMPT_INPUT_RE.test(k)) || fuelEnv);
|
|
66671
|
+
}
|
|
66161
66672
|
function isAgentStep(step) {
|
|
66162
66673
|
if (step.uses && AGENT_ACTION_RE.test(step.uses)) return true;
|
|
66163
|
-
if (step.
|
|
66674
|
+
if (step.uses && isWrapperAgent(step)) return true;
|
|
66675
|
+
if (typeof step.run !== "string" || !step.run) return false;
|
|
66676
|
+
if (/\b(claude|aider|codex|gemini)\b/i.test(step.run) && /github\.event/.test(step.run))
|
|
66164
66677
|
return true;
|
|
66165
|
-
return false;
|
|
66678
|
+
if (!AGENT_CLI_RE.test(step.run)) return false;
|
|
66679
|
+
const env = { ...step.scopeEnv ?? {}, ...step.env ?? {} };
|
|
66680
|
+
return !!step.scopeEnvEvent || EVENT_CONTEXT_RE.test(str(step.env)) || Object.keys(env).some((k) => RUN_FUEL_ENV_RE.test(k));
|
|
66166
66681
|
}
|
|
66167
66682
|
function allowedToolsFromArgs(claudeArgs) {
|
|
66168
66683
|
let out = "";
|
|
@@ -66182,12 +66697,47 @@ function collectTools(steps) {
|
|
|
66182
66697
|
let s = "";
|
|
66183
66698
|
for (const st of steps) {
|
|
66184
66699
|
const w = st.with ?? {};
|
|
66185
|
-
|
|
66186
|
-
s += " " +
|
|
66700
|
+
const args = str(w["claude_args"]) + " " + str(w["codex-args"]) + " " + str(w["codex_args"]);
|
|
66701
|
+
s += " " + allowedToolsFromArgs(args);
|
|
66702
|
+
s += " " + str(w["allowed_tools"]) + " " + str(w["allowedtools"]);
|
|
66187
66703
|
s += " " + allowedToolsFromSettings(str(w["settings"]));
|
|
66704
|
+
if (PERMISSION_BYPASS_RE.test(args) || /^danger-full-access$/i.test(str(w["sandbox"])))
|
|
66705
|
+
s += " Bash";
|
|
66706
|
+
if (st.run && isRunAgent(st)) {
|
|
66707
|
+
s += " " + allowedToolsFromArgs(st.run);
|
|
66708
|
+
if (PERMISSION_BYPASS_RE.test(st.run)) s += " Bash";
|
|
66709
|
+
s += " " + permissionEnvGrants({ ...st.scopeEnv ?? {}, ...st.env ?? {} });
|
|
66710
|
+
}
|
|
66188
66711
|
}
|
|
66189
66712
|
return s;
|
|
66190
66713
|
}
|
|
66714
|
+
var PERMISSION_BYPASS_RE = /--dangerously-skip-permissions|--permission-mode[=\s]+["']?bypassPermissions|--dangerously-bypass-approvals-and-sandbox|--allow-all-tools|--yolo\b/i;
|
|
66715
|
+
function permissionEnvGrants(env) {
|
|
66716
|
+
let out = "";
|
|
66717
|
+
for (const key of ["OPENCODE_PERMISSION", "KILO_PERMISSION"]) {
|
|
66718
|
+
const raw = str(env[key]);
|
|
66719
|
+
if (!raw || raw.length > 2e4) continue;
|
|
66720
|
+
let cfg;
|
|
66721
|
+
try {
|
|
66722
|
+
cfg = JSON.parse(raw);
|
|
66723
|
+
} catch {
|
|
66724
|
+
continue;
|
|
66725
|
+
}
|
|
66726
|
+
if (!cfg || typeof cfg !== "object") continue;
|
|
66727
|
+
const c = cfg;
|
|
66728
|
+
const bash = c["bash"];
|
|
66729
|
+
if (bash === "allow") out += " Bash";
|
|
66730
|
+
else if (bash && typeof bash === "object") {
|
|
66731
|
+
for (const [pat, v] of Object.entries(bash)) {
|
|
66732
|
+
if (v !== "allow") continue;
|
|
66733
|
+
const cmd = pat.replace(/\*+\s*$/, "").trim();
|
|
66734
|
+
out += cmd ? ` Bash(${cmd}:*)` : " Bash";
|
|
66735
|
+
}
|
|
66736
|
+
}
|
|
66737
|
+
if (c["webfetch"] === "allow") out += " WebFetch";
|
|
66738
|
+
}
|
|
66739
|
+
return out;
|
|
66740
|
+
}
|
|
66191
66741
|
function toolTokens(blob) {
|
|
66192
66742
|
const out = [];
|
|
66193
66743
|
let buf = "";
|
|
@@ -66220,11 +66770,21 @@ function matchedBroadTools(blob) {
|
|
|
66220
66770
|
}
|
|
66221
66771
|
return out;
|
|
66222
66772
|
}
|
|
66773
|
+
var GH_PR_CHECKOUT_RE = /\bgh\s+pr\s+checkout\b/;
|
|
66774
|
+
var FETCH_PULL_RE = /\bgit\s+fetch\b[^\n]{0,200}\bpull\/[^\s/]{1,80}\/(?:head|merge)\b/;
|
|
66775
|
+
var GIT_CHECKOUT_RE = /\bgit\s+(?:checkout|switch)\b/;
|
|
66776
|
+
function runChecksOutHead(run) {
|
|
66777
|
+
if (GH_PR_CHECKOUT_RE.test(run)) return true;
|
|
66778
|
+
const m = FETCH_PULL_RE.exec(run);
|
|
66779
|
+
return m !== null && GIT_CHECKOUT_RE.test(run.slice(m.index));
|
|
66780
|
+
}
|
|
66223
66781
|
function untrustedHeadCheckout(steps) {
|
|
66224
66782
|
for (const step of steps) {
|
|
66783
|
+
if (!step.uses && typeof step.run === "string" && runChecksOutHead(step.run)) return "root";
|
|
66225
66784
|
if (!step.uses || !/actions\/checkout/.test(step.uses)) continue;
|
|
66226
66785
|
const ref = str(step.with?.["ref"]);
|
|
66227
|
-
if (/
|
|
66786
|
+
if (/refs\/pull\//.test(ref)) return step.with?.["path"] ? "subdir" : "root";
|
|
66787
|
+
if (/pull_request\.head|head[._-]sha|head_ref|expected_head|workflow_run\.head|inputs(?:\.|\[\s*['"])[\w-]*head/i.test(
|
|
66228
66788
|
ref
|
|
66229
66789
|
)) {
|
|
66230
66790
|
return step.with?.["path"] ? "subdir" : "root";
|
|
@@ -66234,22 +66794,61 @@ function untrustedHeadCheckout(steps) {
|
|
|
66234
66794
|
}
|
|
66235
66795
|
function promptTakesUntrusted(steps) {
|
|
66236
66796
|
for (const st of steps) {
|
|
66237
|
-
const blob = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + collectTools([st])
|
|
66797
|
+
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.
|
|
66798
|
+
(isRunAgent(st) ? " " + st.run + " " + str(st.env) + " " + str(st.scopeEnv) : "");
|
|
66238
66799
|
if (/github\.event\.(issue|comment|pull_request|review)\.(body|title)/i.test(blob)) return true;
|
|
66239
|
-
if (/pull_request\.number
|
|
66800
|
+
if (/pull_request\.number|(?<![\w./-])\/(?:code-)?review(?![\w-])/i.test(blob)) return true;
|
|
66240
66801
|
}
|
|
66241
66802
|
return false;
|
|
66242
66803
|
}
|
|
66243
66804
|
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;
|
|
66244
66805
|
var ITEM_HANDLE_RE = /github\.event\.(issue|pull_request|discussion)\.number|github\.event\.number\b|github\.event\.comment\.id/i;
|
|
66245
66806
|
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;
|
|
66807
|
+
var RUN_ARTIFACT_RE = /gh\s+run\s+download\b|\/runs\/\$\{\{\s*github\.event\.workflow_run\.id\s*\}\}\/(?:artifacts|logs)/i;
|
|
66808
|
+
var TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
66809
|
+
var UPSTREAM_TRUSTED = false;
|
|
66810
|
+
function upstreamRunTrusted(wf, raw) {
|
|
66811
|
+
const on = onObject(wf, raw);
|
|
66812
|
+
const run = on?.["workflow_run"];
|
|
66813
|
+
const names = Array.isArray(run?.workflows) ? run.workflows.map(String) : [];
|
|
66814
|
+
return names.length > 0 && names.every((n) => TRUSTED_RUN_SOURCES.has(n));
|
|
66815
|
+
}
|
|
66816
|
+
function runSourceOf(path91, content, autoLabels) {
|
|
66817
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
66818
|
+
try {
|
|
66819
|
+
let raw;
|
|
66820
|
+
try {
|
|
66821
|
+
raw = loadWorkflow(content).raw;
|
|
66822
|
+
} catch {
|
|
66823
|
+
return null;
|
|
66824
|
+
}
|
|
66825
|
+
if (!raw || typeof raw !== "object") return null;
|
|
66826
|
+
const wf = raw;
|
|
66827
|
+
CALLERS.delete(wf);
|
|
66828
|
+
const reach = triggerReach(wf, raw);
|
|
66829
|
+
const name = typeof raw["name"] === "string" && raw["name"] ? raw["name"] : path91;
|
|
66830
|
+
return { name, trusted: !reach.untrusted && !reach.reusable };
|
|
66831
|
+
} finally {
|
|
66832
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
66833
|
+
}
|
|
66834
|
+
}
|
|
66835
|
+
function downloadsRunArtifacts(st) {
|
|
66836
|
+
if (UPSTREAM_TRUSTED) return false;
|
|
66837
|
+
if (/actions\/download-artifact/i.test(str(st.uses)))
|
|
66838
|
+
return /workflow_run\.id/.test(str(st.with?.["run-id"]) + " " + str(st.with?.["run_id"]));
|
|
66839
|
+
return RUN_ARTIFACT_RE.test(str(st.run) + " " + str(st.with?.["script"]));
|
|
66840
|
+
}
|
|
66841
|
+
var STEP_OUTPUT_REF_RE = /steps\.([\w-]{1,100})\.outputs\./g;
|
|
66246
66842
|
function jobReadsUntrustedItem(job) {
|
|
66247
66843
|
const steps = job.steps ?? [];
|
|
66248
66844
|
const agent = steps.filter(isAgentStep);
|
|
66249
66845
|
if (!agent.length) return null;
|
|
66250
66846
|
if (ITEM_READ_TOOL_RE.test(collectTools(agent))) return "tool";
|
|
66251
66847
|
for (const st of agent)
|
|
66252
|
-
if (ITEM_HANDLE_RE.test(
|
|
66848
|
+
if (ITEM_HANDLE_RE.test(
|
|
66849
|
+
str(st.with) + " " + str(st.env) + " " + str(job.env) + " " + (isRunAgent(st) ? str(st.run) : "")
|
|
66850
|
+
))
|
|
66851
|
+
return "handle";
|
|
66253
66852
|
for (let i = 0; i < steps.length; i++) {
|
|
66254
66853
|
const st = steps[i];
|
|
66255
66854
|
if (!isAgentStep(st) || !str(st.with?.["prompt_file"] ?? st.with?.["prompt-file"])) continue;
|
|
@@ -66257,6 +66856,20 @@ function jobReadsUntrustedItem(job) {
|
|
|
66257
66856
|
if (earlier.some((p) => PAYLOAD_TEXT_RE.test(str(p.run) + " " + str(p.with) + " " + str(p.env))))
|
|
66258
66857
|
return "prompt-file";
|
|
66259
66858
|
}
|
|
66859
|
+
const firstDownload = steps.findIndex(downloadsRunArtifacts);
|
|
66860
|
+
if (firstDownload >= 0 && steps.slice(firstDownload + 1).some(isAgentStep)) return "artifact";
|
|
66861
|
+
for (let i = 0; i < steps.length; i++) {
|
|
66862
|
+
const st = steps[i];
|
|
66863
|
+
if (!isAgentStep(st)) continue;
|
|
66864
|
+
const input = str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + str(st.with?.["prompt_file"] ?? st.with?.["prompt-file"]);
|
|
66865
|
+
const ids = /* @__PURE__ */ new Set();
|
|
66866
|
+
for (const m of input.matchAll(STEP_OUTPUT_REF_RE)) ids.add(m[1]);
|
|
66867
|
+
for (const id of ids) {
|
|
66868
|
+
const src = steps.slice(0, i).find((p) => p.id === id);
|
|
66869
|
+
if (src && PAYLOAD_TEXT_RE.test(str(src.run) + " " + str(src.with) + " " + str(src.env)))
|
|
66870
|
+
return "step-output";
|
|
66871
|
+
}
|
|
66872
|
+
}
|
|
66260
66873
|
return null;
|
|
66261
66874
|
}
|
|
66262
66875
|
var NONCONTAINS_GATE_RE = new RegExp(
|
|
@@ -66269,19 +66882,46 @@ var NONCONTAINS_GATE_RE = new RegExp(
|
|
|
66269
66882
|
].join("|"),
|
|
66270
66883
|
"i"
|
|
66271
66884
|
);
|
|
66272
|
-
var
|
|
66885
|
+
var PERMISSION_OUTPUT_RE = /steps\.[\w-]{1,100}\.outputs\.([\w-]{1,200})\s*==\s*['"]?(?:true|admin|write|maintain)/gi;
|
|
66886
|
+
var PERMISSION_OUTPUT_NAME_RE = /permission|allowed|authoriz|is[_-]?(?:admin|member|maintainer|collaborator)/i;
|
|
66887
|
+
var PERMISSION_OUTPUT_GATE_RE = {
|
|
66888
|
+
test(ifs) {
|
|
66889
|
+
for (const m of ifs.matchAll(PERMISSION_OUTPUT_RE))
|
|
66890
|
+
if (PERMISSION_OUTPUT_NAME_RE.test(m[1])) return true;
|
|
66891
|
+
return false;
|
|
66892
|
+
}
|
|
66893
|
+
};
|
|
66273
66894
|
var CONTAINS_GATE_RE = /contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
|
|
66274
66895
|
var NEGATED_CONTAINS_RE = /!\s*\(?\s*contains\((?:[^()]|\([^()]*\))*(login|actor|association|OWNER|MEMBER|COLLABORATOR)/i;
|
|
66275
66896
|
function labelTypeConfigured(wf, raw) {
|
|
66276
66897
|
const on = wf.on ?? raw["on"] ?? raw[true];
|
|
66277
66898
|
const prTypes = (t) => t && Array.isArray(t.types) ? t.types.map(String) : [];
|
|
66278
|
-
|
|
66899
|
+
const pr = prTypes(on?.["pull_request_target"]).includes("labeled") || prTypes(on?.["pull_request"]).includes("labeled");
|
|
66900
|
+
const onObj = onObject(wf, raw) ?? {};
|
|
66901
|
+
const issues = on != null && typeof on === "object" && "issues" in on && triggerKeys(wf, raw).every((k) => /^issues$/i.test(k) || !keyStrangerFirable(onObj, k));
|
|
66902
|
+
return { pr, issues };
|
|
66903
|
+
}
|
|
66904
|
+
var EVENT_LABEL_GATE_RE = /\blabel\.name\s*==\s*'|\b(?:contains|startsWith)\(\s*github\.event\.label\.name\b/i;
|
|
66905
|
+
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;
|
|
66906
|
+
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;
|
|
66907
|
+
var NONWRITE_SET_RE = /contains\(\s*(?:fromjson\(\s*['"]\[[^\]]{0,400}|'[^']{0,400})\b(CONTRIBUTOR|FIRST_TIME_CONTRIBUTOR|FIRST_TIMER|NONE|MANNEQUIN)\b/i;
|
|
66908
|
+
function allIfText(wf) {
|
|
66909
|
+
const s = jobList(wf).map((j) => str(j.if) + " " + (j.steps ?? []).map((st) => str(st.if)).join(" ")).join(" ");
|
|
66910
|
+
return s.length > IF_MAX_LEN ? "" : s;
|
|
66279
66911
|
}
|
|
66280
|
-
|
|
66281
|
-
|
|
66282
|
-
const
|
|
66283
|
-
|
|
66284
|
-
|
|
66912
|
+
function testsAutoLabel(ifs) {
|
|
66913
|
+
if (AUTO_LABELS.size === 0) return false;
|
|
66914
|
+
for (const m of ifs.matchAll(
|
|
66915
|
+
/labels\.\*\.name\s*,\s*'([^']{1,100})'|label\.name\s*==\s*'([^']{1,100})'/gi
|
|
66916
|
+
))
|
|
66917
|
+
if (AUTO_LABELS.has((m[1] ?? m[2]).toLowerCase())) return true;
|
|
66918
|
+
return false;
|
|
66919
|
+
}
|
|
66920
|
+
function ifsAreGated(ifs, label3) {
|
|
66921
|
+
const m = maskLiterals(ifs);
|
|
66922
|
+
const containsGate = CONTAINS_GATE_RE.test(m) && !NEGATED_CONTAINS_RE.test(m) && !NONWRITE_SET_RE.test(ifs);
|
|
66923
|
+
const gated = NONCONTAINS_GATE_RE.test(m) || containsGate;
|
|
66924
|
+
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);
|
|
66285
66925
|
return gated || labelGated;
|
|
66286
66926
|
}
|
|
66287
66927
|
function hasStepMembershipGate(job) {
|
|
@@ -66302,6 +66942,7 @@ function parseIf(src) {
|
|
|
66302
66942
|
let e = src.trim();
|
|
66303
66943
|
const wrapped = /^\$\{\{([\s\S]*)\}\}$/.exec(e);
|
|
66304
66944
|
if (wrapped) e = wrapped[1].trim();
|
|
66945
|
+
if (e.includes("${{")) return null;
|
|
66305
66946
|
if (!e || e.length > IF_MAX_LEN) return null;
|
|
66306
66947
|
let i = 0;
|
|
66307
66948
|
let depth = 0;
|
|
@@ -66387,12 +67028,21 @@ function stepIfText(steps) {
|
|
|
66387
67028
|
const s = steps.map((st) => str(st.if)).join(" ");
|
|
66388
67029
|
return s.length > IF_MAX_LEN ? "" : s;
|
|
66389
67030
|
}
|
|
67031
|
+
function maskLiterals(ifs) {
|
|
67032
|
+
return ifs.replace(
|
|
67033
|
+
/(==|!=)?(\s*)'(?:[^']|'')*'/g,
|
|
67034
|
+
(m, op, sp) => op ? m : `${sp}'_'`
|
|
67035
|
+
);
|
|
67036
|
+
}
|
|
67037
|
+
var CHAINED_COMPARE_RE = /(?:==|!=)[^=!]*(?:==|!=)/;
|
|
66390
67038
|
function gatedText(ifs, labelConfigured) {
|
|
66391
|
-
|
|
67039
|
+
const masked = maskLiterals(ifs);
|
|
67040
|
+
return ifsAreGated(ifs, labelConfigured) || ASSIGNEE_GATE_RE.test(masked) || PERMISSION_OUTPUT_GATE_RE.test(masked);
|
|
66392
67041
|
}
|
|
66393
67042
|
function exprGated(x, labelConfigured) {
|
|
66394
67043
|
switch (x.k) {
|
|
66395
67044
|
case "atom":
|
|
67045
|
+
if (CHAINED_COMPARE_RE.test(maskLiterals(x.t))) return false;
|
|
66396
67046
|
return gatedText(x.t, labelConfigured) || TRUSTED_EVENT_ATOM_RE.test(x.t);
|
|
66397
67047
|
case "not":
|
|
66398
67048
|
return false;
|
|
@@ -66428,11 +67078,43 @@ function jobOnWorkflowRun(job, workflowRun) {
|
|
|
66428
67078
|
}
|
|
66429
67079
|
function jobActorGate(job, wf, raw) {
|
|
66430
67080
|
const labelConfigured = labelTypeConfigured(wf, raw);
|
|
66431
|
-
const
|
|
66432
|
-
|
|
67081
|
+
const agent = (job.steps ?? []).filter(isAgentStep);
|
|
67082
|
+
const ifTotal = agent.reduce((n, s) => n + str(s.if).length, 0);
|
|
67083
|
+
const agentStepsGated = agent.length > 0 && ifTotal <= IF_MAX_LEN && agent.every((s) => {
|
|
67084
|
+
const x = parseIf(str(s.if));
|
|
67085
|
+
return x !== null && exprGated(x, labelConfigured);
|
|
67086
|
+
});
|
|
67087
|
+
return jobIfGated(str(job.if), labelConfigured) || agentStepsGated || hasStepMembershipGate(job);
|
|
67088
|
+
}
|
|
67089
|
+
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*['"]/;
|
|
67090
|
+
var ACTOR_ALLOWLIST_G = new RegExp(ACTOR_ALLOWLIST_RE.source, "g");
|
|
67091
|
+
var BLOCK_LIST_NAME_RE = /block|deny|ban|spam|abuse|exclu|ignore/i;
|
|
67092
|
+
function scriptAllowsByActor(code2) {
|
|
67093
|
+
if (code2.length > 2e5) return false;
|
|
67094
|
+
for (const m of code2.matchAll(ACTOR_ALLOWLIST_G)) {
|
|
67095
|
+
const at = m.index ?? 0;
|
|
67096
|
+
const before = code2.slice(Math.max(0, at - 80), at);
|
|
67097
|
+
if (m[0].startsWith(".includes")) {
|
|
67098
|
+
const list = /([\w$.]{1,60})$/.exec(before)?.[1] ?? "";
|
|
67099
|
+
if (BLOCK_LIST_NAME_RE.test(list)) continue;
|
|
67100
|
+
}
|
|
67101
|
+
const negated = /!\s{0,3}\(?\s{0,3}[\w$.]{0,60}$/.test(before);
|
|
67102
|
+
const ternary = /^[^?;\n]{0,150}\?\s{0,5}['"`]?(true|false)\b/.exec(code2.slice(at, at + 200));
|
|
67103
|
+
if (!ternary || ternary[1] === "true" !== negated) return true;
|
|
67104
|
+
}
|
|
67105
|
+
return false;
|
|
67106
|
+
}
|
|
67107
|
+
function readsOutputPositively(readerIf, name) {
|
|
67108
|
+
const ref = `needs\\.${escapeRe(name)}\\.outputs\\.[\\w-]{1,100}`;
|
|
67109
|
+
if (!new RegExp(ref).test(readerIf)) return false;
|
|
67110
|
+
const no = `'(?:false|no|0|)'(?![\\w'])`;
|
|
67111
|
+
return !new RegExp(
|
|
67112
|
+
`!\\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})`,
|
|
67113
|
+
"i"
|
|
67114
|
+
).test(readerIf);
|
|
66433
67115
|
}
|
|
66434
67116
|
var STATUS_FN_RE = /\b(success|failure|always|cancelled)\s*\(/i;
|
|
66435
|
-
var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]/;
|
|
67117
|
+
var FAILING_STEP_RE = /\bexit\s+[1-9]|core\.setFailed|process\.exit\(\s*[1-9]|actions\.cancelWorkflowRun\b/;
|
|
66436
67118
|
function needsOf(job) {
|
|
66437
67119
|
return (Array.isArray(job.needs) ? job.needs : job.needs ? [job.needs] : []).map(String);
|
|
66438
67120
|
}
|
|
@@ -66444,13 +67126,18 @@ function upstreamJobGates(name, dep, wf, raw, readerIf) {
|
|
|
66444
67126
|
const label3 = labelTypeConfigured(wf, raw);
|
|
66445
67127
|
if (depIf && jobIfGated(depIf, label3)) return true;
|
|
66446
67128
|
const steps = dep.steps ?? [];
|
|
66447
|
-
|
|
67129
|
+
if (readerIf !== null && readsOutputPositively(readerIf, name) && steps.some((st) => {
|
|
67130
|
+
const code2 = str(st.run) + " " + str(st.with?.["script"]);
|
|
67131
|
+
return MEMBERSHIP_CHECK_RE.test(code2) || scriptAllowsByActor(code2);
|
|
67132
|
+
}))
|
|
67133
|
+
return true;
|
|
67134
|
+
const stepGated = ifsAreGated(stepIfText(steps), label3) || hasStepMembershipGate(dep) || steps.some((st) => MEMBERSHIP_CHECK_RE.test(str(st.with?.["script"])));
|
|
66448
67135
|
if (!stepGated) return false;
|
|
66449
67136
|
if (steps.some(
|
|
66450
67137
|
(st) => FAILING_STEP_RE.test(str(st.run)) || FAILING_STEP_RE.test(str(st.with?.["script"]))
|
|
66451
67138
|
))
|
|
66452
67139
|
return true;
|
|
66453
|
-
return readerIf !== null &&
|
|
67140
|
+
return readerIf !== null && readsOutputPositively(readerIf, name);
|
|
66454
67141
|
}
|
|
66455
67142
|
function gatingAncestors(job, wf, raw) {
|
|
66456
67143
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -66480,16 +67167,23 @@ function jobGated(job, wf, raw) {
|
|
|
66480
67167
|
}
|
|
66481
67168
|
function bypassArmed(steps) {
|
|
66482
67169
|
return steps.some(
|
|
66483
|
-
(s) => str(s.with?.["allowed_non_write_users"])
|
|
67170
|
+
(s) => nonWriteOpen(str(s.with?.["allowed_non_write_users"])) && (!!str(s.with?.["github_token"]) || !!str(s.env?.["OVERRIDE_GITHUB_TOKEN"]))
|
|
66484
67171
|
);
|
|
66485
67172
|
}
|
|
67173
|
+
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;
|
|
67174
|
+
function nonWriteOpen(v) {
|
|
67175
|
+
const t = v.trim();
|
|
67176
|
+
if (t === "*") return true;
|
|
67177
|
+
return t.length <= 1e3 && ACTOR_BOUND_RE.test(t);
|
|
67178
|
+
}
|
|
66486
67179
|
function hasImplicitActorGate(agentSteps, bypassActive, workflowRun = false) {
|
|
66487
67180
|
if (bypassActive) return false;
|
|
66488
67181
|
return agentSteps.some((s) => {
|
|
66489
67182
|
const build = claudeActionBuild(s.uses);
|
|
66490
67183
|
if (!build) return false;
|
|
67184
|
+
if (!build.official) return false;
|
|
66491
67185
|
if (!workflowRun) return true;
|
|
66492
|
-
return
|
|
67186
|
+
return workflowRunChecked(build.ref);
|
|
66493
67187
|
});
|
|
66494
67188
|
}
|
|
66495
67189
|
function claudeActionBuild(uses) {
|
|
@@ -66601,14 +67295,28 @@ function severityFromScore(score) {
|
|
|
66601
67295
|
if (score >= 1) return "advisory";
|
|
66602
67296
|
return null;
|
|
66603
67297
|
}
|
|
66604
|
-
function analyzeWorkflow(path91, content) {
|
|
67298
|
+
function analyzeWorkflow(path91, content, callers, autoLabels, trustedRunSources) {
|
|
67299
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67300
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67301
|
+
try {
|
|
67302
|
+
return analyzeWorkflowInner(path91, content, callers);
|
|
67303
|
+
} finally {
|
|
67304
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67305
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
67306
|
+
UPSTREAM_TRUSTED = false;
|
|
67307
|
+
}
|
|
67308
|
+
}
|
|
67309
|
+
function analyzeWorkflowInner(path91, content, callers) {
|
|
66605
67310
|
let raw;
|
|
66606
67311
|
try {
|
|
66607
|
-
raw =
|
|
67312
|
+
raw = loadWorkflow(content).raw;
|
|
66608
67313
|
} catch {
|
|
66609
67314
|
return null;
|
|
66610
67315
|
}
|
|
66611
67316
|
const wf = raw;
|
|
67317
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67318
|
+
else CALLERS.delete(wf);
|
|
67319
|
+
UPSTREAM_TRUSTED = upstreamRunTrusted(wf, raw);
|
|
66612
67320
|
const steps = allSteps(wf).map((s) => s.step);
|
|
66613
67321
|
const agentSteps = steps.filter(isAgentStep);
|
|
66614
67322
|
const agentJobs = [
|
|
@@ -66627,17 +67335,19 @@ function analyzeWorkflow(path91, content) {
|
|
|
66627
67335
|
} = triggerReach(wf, raw);
|
|
66628
67336
|
const nonWrite = str(agentSteps.map((s) => s.with?.["allowed_non_write_users"]).find(Boolean));
|
|
66629
67337
|
const nonWriteStar = bypassArmed(agentSteps);
|
|
66630
|
-
const starWithoutToken = nonWrite
|
|
66631
|
-
const nonWriteList = !!nonWrite && nonWrite
|
|
67338
|
+
const starWithoutToken = nonWriteOpen(nonWrite) && !nonWriteStar;
|
|
67339
|
+
const nonWriteList = !!nonWrite && !nonWriteOpen(nonWrite);
|
|
67340
|
+
const nonWriteActor = nonWrite.trim() !== "*" && nonWriteOpen(nonWrite);
|
|
66632
67341
|
const untrustedTrigger = forkInput || reusable;
|
|
66633
67342
|
const bypassActive = nonWriteStar && untrustedTrigger;
|
|
66634
|
-
const head = untrustedHeadCheckout(steps);
|
|
67343
|
+
const head = agentSteps.length && agentSteps.every(readOnlySandbox) ? null : untrustedHeadCheckout(steps);
|
|
66635
67344
|
const promptUntrusted = promptTakesUntrusted(agentSteps);
|
|
66636
67345
|
const injJobs = injectableJobs(wf, raw, untrustedTrigger);
|
|
66637
67346
|
const powerJobs = injJobs.length ? injJobs : agentJobs;
|
|
66638
67347
|
const powerSteps = injJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep));
|
|
66639
67348
|
const scopedSteps = powerSteps.length ? powerSteps : agentSteps;
|
|
66640
|
-
const
|
|
67349
|
+
const headActionRuns = head === "root" && powerSteps.some((st) => /^\s*\.\//.test(str(st.uses)));
|
|
67350
|
+
const toolsBlob = collectTools(scopedSteps) + (headActionRuns ? " Bash" : "");
|
|
66641
67351
|
const itemRead = forkInput ? injJobs.map(jobReadsUntrustedItem).find(Boolean) ?? null : null;
|
|
66642
67352
|
const reach = untrustedTrigger && injJobs.length ? Math.max(
|
|
66643
67353
|
head === "root" ? 3 : head === "subdir" ? 1 : 0,
|
|
@@ -66685,28 +67395,40 @@ function analyzeWorkflow(path91, content) {
|
|
|
66685
67395
|
severity = "medium";
|
|
66686
67396
|
if (!severity) severity = "advisory";
|
|
66687
67397
|
const signals = [];
|
|
67398
|
+
const openCallers = (callers ?? []).filter((c) => c.untrusted && !c.gated && !c.dispatched);
|
|
67399
|
+
if (openCallers.length)
|
|
67400
|
+
signals.push(
|
|
67401
|
+
`called by ${openCallers.map((c) => `${safeText(c.file, 120)} (${c.keys.map((k) => safeText(k, 40)).join(", ")})`).join("; ")}`
|
|
67402
|
+
);
|
|
66688
67403
|
if (secretExposed)
|
|
66689
67404
|
signals.push(
|
|
66690
|
-
`runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).join(", ")})`
|
|
67405
|
+
`runs with base-repo secrets (${triggers.filter((t) => /target|workflow_run/i.test(t)).map((t) => safeText(t, 40)).join(", ")})`
|
|
67406
|
+
);
|
|
67407
|
+
else if (forkInput)
|
|
67408
|
+
signals.push(
|
|
67409
|
+
`triggered by untrusted input (${triggers.map((t) => safeText(t, 40)).join(", ")})`
|
|
66691
67410
|
);
|
|
66692
|
-
else if (forkInput) signals.push(`triggered by untrusted input (${triggers.join(", ")})`);
|
|
66693
67411
|
else if (reusable)
|
|
66694
67412
|
signals.push(
|
|
66695
|
-
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`
|
|
67413
|
+
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`
|
|
66696
67414
|
);
|
|
66697
67415
|
if (forkInput && !privileged && !reusable)
|
|
66698
67416
|
signals.push(
|
|
66699
67417
|
"triggered by `pull_request`: fork PRs run with a read-only token (lower risk than pull_request_target)"
|
|
66700
67418
|
);
|
|
66701
67419
|
if (head === "root") signals.push("checks out the untrusted PR head into the workspace root");
|
|
67420
|
+
if (headActionRuns)
|
|
67421
|
+
signals.push(
|
|
67422
|
+
"runs the checked-out head's own action (`uses: ./\u2026`): the PR author's code runs with the job's secrets"
|
|
67423
|
+
);
|
|
66702
67424
|
if (head === "subdir") signals.push("checks out the untrusted PR head into an isolated subdir");
|
|
67425
|
+
const thirdParty = agentSteps.map((st) => claudeActionBuild(st.uses)).find((b) => b !== null && !b.official);
|
|
67426
|
+
if (thirdParty && !implicitGate)
|
|
67427
|
+
signals.push(
|
|
67428
|
+
`third-party build of claude-code-action (\`${safeText(thirdParty.owner, 40)}\`): not assumed to check the actor's write access like the official action`
|
|
67429
|
+
);
|
|
66703
67430
|
if (workflowRunJobs.length) {
|
|
66704
67431
|
const builds = workflowRunJobs.flatMap((j) => (j.steps ?? []).filter(isAgentStep)).map((st) => claudeActionBuild(st.uses)).filter((b) => b !== null);
|
|
66705
|
-
const thirdParty = builds.find((b) => !b.official);
|
|
66706
|
-
if (thirdParty)
|
|
66707
|
-
signals.push(
|
|
66708
|
-
`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)`
|
|
66709
|
-
);
|
|
66710
67432
|
const old = builds.find((b) => b.official && !workflowRunChecked(b.ref));
|
|
66711
67433
|
if (old)
|
|
66712
67434
|
signals.push(
|
|
@@ -66720,7 +67442,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66720
67442
|
if (promptUntrusted) signals.push("feeds untrusted PR/issue text to the agent");
|
|
66721
67443
|
else if (itemRead)
|
|
66722
67444
|
signals.push(
|
|
66723
|
-
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)"
|
|
67445
|
+
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)"
|
|
66724
67446
|
);
|
|
66725
67447
|
if (broadTools) {
|
|
66726
67448
|
const names = matchedBroadTools(toolsBlob);
|
|
@@ -66730,7 +67452,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66730
67452
|
}
|
|
66731
67453
|
if (bypassActive)
|
|
66732
67454
|
signals.push(
|
|
66733
|
-
'allowed_non_write_users: "*" with github_token, so any user can trigger the agent'
|
|
67455
|
+
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'
|
|
66734
67456
|
);
|
|
66735
67457
|
if (elevated) signals.push("elevated permissions (contents/id-token: write)");
|
|
66736
67458
|
if (pat) signals.push("a static PAT is exposed to the agent (recoverable via injection)");
|
|
@@ -66824,7 +67546,7 @@ function analyzeWorkflow(path91, content) {
|
|
|
66824
67546
|
return finding;
|
|
66825
67547
|
}
|
|
66826
67548
|
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;
|
|
66827
|
-
var FUEL_INPUT_KEYS = /^(
|
|
67549
|
+
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;
|
|
66828
67550
|
function fuelSecretNames(agentSteps) {
|
|
66829
67551
|
const out = /* @__PURE__ */ new Set();
|
|
66830
67552
|
const add = (v) => {
|
|
@@ -66897,14 +67619,28 @@ function evalAgentJob(job, wf, raw, untrustedTrigger, reusable) {
|
|
|
66897
67619
|
if (reusable && !loadedGun && SEVERITY_RANK2[severity] > SEVERITY_RANK2.medium) severity = "medium";
|
|
66898
67620
|
return { severity, secrets, injectable, canReadEnv };
|
|
66899
67621
|
}
|
|
66900
|
-
function analyzeWorkflowSecrets(path91, content) {
|
|
67622
|
+
function analyzeWorkflowSecrets(path91, content, callers, autoLabels, trustedRunSources) {
|
|
67623
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67624
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67625
|
+
try {
|
|
67626
|
+
return analyzeWorkflowSecretsInner(path91, content, callers);
|
|
67627
|
+
} finally {
|
|
67628
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67629
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
67630
|
+
UPSTREAM_TRUSTED = false;
|
|
67631
|
+
}
|
|
67632
|
+
}
|
|
67633
|
+
function analyzeWorkflowSecretsInner(path91, content, callers) {
|
|
66901
67634
|
let raw;
|
|
66902
67635
|
try {
|
|
66903
|
-
raw =
|
|
67636
|
+
raw = loadWorkflow(content).raw;
|
|
66904
67637
|
} catch {
|
|
66905
67638
|
return null;
|
|
66906
67639
|
}
|
|
66907
67640
|
const wf = raw;
|
|
67641
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67642
|
+
else CALLERS.delete(wf);
|
|
67643
|
+
UPSTREAM_TRUSTED = upstreamRunTrusted(wf, raw);
|
|
66908
67644
|
const all = allSteps(wf);
|
|
66909
67645
|
if (!all.some((s) => isAgentStep(s.step))) return null;
|
|
66910
67646
|
const { untrusted: forkInput, reusable } = triggerReach(wf, raw);
|
|
@@ -66936,70 +67672,285 @@ function analyzeWorkflowSecrets(path91, content) {
|
|
|
66936
67672
|
})
|
|
66937
67673
|
};
|
|
66938
67674
|
}
|
|
66939
|
-
|
|
66940
|
-
|
|
66941
|
-
|
|
66942
|
-
|
|
66943
|
-
|
|
66944
|
-
|
|
66945
|
-
|
|
66946
|
-
|
|
66947
|
-
|
|
66948
|
-
if (!close) return null;
|
|
66949
|
-
const block = text.slice(3, 3 + close.index);
|
|
67675
|
+
var COMMIT_PIN_RE = /[#@][0-9a-f]{40}\b/i;
|
|
67676
|
+
function marketplaceOwner(market) {
|
|
67677
|
+
if (/(^|[/\\])\.\.([/\\]|$)/.test(market)) return void 0;
|
|
67678
|
+
return /^(?:https?:\/\/(?:www\.)?|git@|ssh:\/\/git@)?github\.com[:/]([\w.-]{1,100})\/[\w.-]{1,100}(?:\.git)?\/?(?:[#@].*)?$/i.exec(
|
|
67679
|
+
market
|
|
67680
|
+
)?.[1] ?? /^([\w.-]{1,100})\/[\w.-]{1,100}(?:[#@].*)?$/.exec(market)?.[1];
|
|
67681
|
+
}
|
|
67682
|
+
function analyzeWorkflowPlugins(path91, content, owner) {
|
|
67683
|
+
let raw;
|
|
66950
67684
|
try {
|
|
66951
|
-
|
|
66952
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
67685
|
+
raw = loadWorkflow(content).raw;
|
|
66953
67686
|
} catch {
|
|
66954
|
-
return
|
|
67687
|
+
return [];
|
|
67688
|
+
}
|
|
67689
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67690
|
+
const wf = raw;
|
|
67691
|
+
const out = [];
|
|
67692
|
+
const seen = /* @__PURE__ */ new Set();
|
|
67693
|
+
for (const { step } of allSteps(wf)) {
|
|
67694
|
+
if (!claudeActionBuild(step.uses)) continue;
|
|
67695
|
+
if (!str(step.with?.["plugins"]).trim()) continue;
|
|
67696
|
+
for (const m of str(step.with?.["plugin_marketplaces"]).split(/[\s,]+/)) {
|
|
67697
|
+
const market = m.trim();
|
|
67698
|
+
if (!market || COMMIT_PIN_RE.test(market)) continue;
|
|
67699
|
+
if (/^\.{1,2}(\/|$)/.test(market)) continue;
|
|
67700
|
+
const mo = marketplaceOwner(market);
|
|
67701
|
+
if (mo && mo.toLowerCase() === "anthropics") continue;
|
|
67702
|
+
if (owner && mo && mo.toLowerCase() === owner.toLowerCase()) continue;
|
|
67703
|
+
if (seen.has(market)) continue;
|
|
67704
|
+
seen.add(market);
|
|
67705
|
+
out.push({
|
|
67706
|
+
check: "CI-3",
|
|
67707
|
+
rule: "CI-3.plugin-unpinned",
|
|
67708
|
+
locator: safeText(market, 200),
|
|
67709
|
+
dimension: "mcp",
|
|
67710
|
+
severity: "medium",
|
|
67711
|
+
title: "Agent installs plugins from a third-party marketplace on every run",
|
|
67712
|
+
file: path91,
|
|
67713
|
+
signals: [
|
|
67714
|
+
`\`plugin_marketplaces: ${safeText(market, 120)}\` is not pinned to a commit: whoever controls it changes the code and prompts the agent runs`
|
|
67715
|
+
],
|
|
67716
|
+
fix: "Pin the marketplace to a commit (`<url>#<sha>`), or vendor the plugin into this repository."
|
|
67717
|
+
});
|
|
67718
|
+
}
|
|
66955
67719
|
}
|
|
67720
|
+
return out;
|
|
66956
67721
|
}
|
|
66957
|
-
|
|
66958
|
-
|
|
66959
|
-
|
|
66960
|
-
|
|
66961
|
-
|
|
66962
|
-
|
|
66963
|
-
|
|
66964
|
-
|
|
66965
|
-
|
|
66966
|
-
|
|
67722
|
+
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;
|
|
67723
|
+
var WEB_READ_RE = /(^|["\s,])(WebFetch|WebSearch)(\(|\s|,|"|$)/;
|
|
67724
|
+
function readsItemList(text) {
|
|
67725
|
+
if (text.length > 2e5) return ITEM_LIST_RE.test(text);
|
|
67726
|
+
return text.replace(/\\\r?\n/g, " ").split("\n").some((l) => ITEM_LIST_RE.test(l) && !sanitizedListing(l));
|
|
67727
|
+
}
|
|
67728
|
+
function analyzeTrustedReaders(path91, content, autoLabels, callers, trustedRunSources) {
|
|
67729
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67730
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67731
|
+
try {
|
|
67732
|
+
return analyzeTrustedReadersInner(path91, content, callers);
|
|
67733
|
+
} finally {
|
|
67734
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67735
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
67736
|
+
}
|
|
67737
|
+
}
|
|
67738
|
+
function keysWithCallers(keys, callers) {
|
|
67739
|
+
return [.../* @__PURE__ */ new Set([...keys, ...(callers ?? []).flatMap((c) => c.keys)])];
|
|
67740
|
+
}
|
|
67741
|
+
function analyzeTrustedReadersInner(path91, content, callers) {
|
|
67742
|
+
let raw;
|
|
67743
|
+
try {
|
|
67744
|
+
raw = loadWorkflow(content).raw;
|
|
67745
|
+
} catch {
|
|
67746
|
+
return [];
|
|
67747
|
+
}
|
|
67748
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67749
|
+
const wf = raw;
|
|
67750
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67751
|
+
else CALLERS.delete(wf);
|
|
67752
|
+
const reach = triggerReach(wf, raw);
|
|
67753
|
+
if (reach.untrusted || reach.reusable) return [];
|
|
67754
|
+
const keys = keysWithCallers(reach.keys, callers);
|
|
67755
|
+
if (!keys.some((k) => /^schedule$/i.test(k))) return [];
|
|
67756
|
+
for (const job of jobList(wf)) {
|
|
67757
|
+
const jobIf = parseIf(str(job.if));
|
|
67758
|
+
if (jobIf !== null && !exprAllowsEvent(jobIf, "schedule")) continue;
|
|
67759
|
+
const steps = job.steps ?? [];
|
|
67760
|
+
const agentIdx = steps.findIndex(isAgentStep);
|
|
67761
|
+
if (agentIdx < 0) continue;
|
|
67762
|
+
const agent = steps.filter(isAgentStep);
|
|
67763
|
+
const tools = collectTools(agent);
|
|
67764
|
+
const promptText = agent.map(
|
|
67765
|
+
(st) => str(st.with?.["prompt"]) + " " + str(st.with?.["direct_prompt"]) + " " + (isRunAgent(st) ? str(st.run) : "")
|
|
67766
|
+
).join(" ");
|
|
67767
|
+
const earlier = steps.slice(0, agentIdx).filter((st) => !isAgentStep(st)).map((st) => str(st.run) + " " + str(st.with?.["script"]));
|
|
67768
|
+
const reads = [];
|
|
67769
|
+
if (ITEM_READ_TOOL_RE.test(tools) || readsItemList(promptText))
|
|
67770
|
+
reads.push("reads open issues or pull requests");
|
|
67771
|
+
else if (earlier.some(readsItemList))
|
|
67772
|
+
reads.push("an earlier step lists open issues or pull requests for it");
|
|
67773
|
+
if (WEB_READ_RE.test(tools)) reads.push("fetches third-party web content");
|
|
67774
|
+
if (!reads.length) continue;
|
|
67775
|
+
const withheld = agent.every(
|
|
67776
|
+
(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() === ""
|
|
67777
|
+
);
|
|
67778
|
+
const checkouts = steps.slice(0, agentIdx).filter((st) => /actions\/checkout/i.test(str(st.uses)));
|
|
67779
|
+
const persisted = checkouts.some((st) => str(st.with?.["persist-credentials"]) !== "false");
|
|
67780
|
+
const checkoutPat = checkouts.some(
|
|
67781
|
+
(st) => /secrets\.(?!GITHUB_TOKEN\b)[A-Za-z_]/i.test(str(st.with?.["token"]))
|
|
67782
|
+
);
|
|
67783
|
+
const pat = usesPatIn([job]) || checkoutPat;
|
|
67784
|
+
const permWrite = jobsHaveWrite(
|
|
67785
|
+
wf,
|
|
67786
|
+
[job],
|
|
67787
|
+
/["']?(contents|issues|pull-requests)["']?\s*:\s*["']?write/i
|
|
67788
|
+
);
|
|
67789
|
+
const handsToken = agent.some(
|
|
67790
|
+
(st) => !!claudeActionBuild(st.uses) || !!str(st.with?.["github_token"]).trim()
|
|
67791
|
+
);
|
|
67792
|
+
const write2 = pat || permWrite && !withheld && (handsToken || persisted);
|
|
67793
|
+
if (!write2) continue;
|
|
67794
|
+
const writes = pat ? "a static PAT" : "a write token";
|
|
67795
|
+
return [
|
|
67796
|
+
{
|
|
67797
|
+
check: "CI-2",
|
|
67798
|
+
rule: "CI-2.trusted-trigger-reader",
|
|
67799
|
+
dimension: "workflows",
|
|
67800
|
+
severity: "advisory",
|
|
67801
|
+
title: "Agent on a trusted trigger reads content strangers write, with a write token",
|
|
67802
|
+
file: path91,
|
|
67803
|
+
signals: [
|
|
67804
|
+
`runs on ${keys.map((k) => safeText(k, 40)).join(", ")}: no stranger starts it`,
|
|
67805
|
+
...reads.map((r) => `the agent ${r}`),
|
|
67806
|
+
`it holds ${writes}: injected text in that content can act with it`
|
|
67807
|
+
],
|
|
67808
|
+
fix: "Give the agent a read-only token and apply its output in a separate step, or filter the items to trusted authors first."
|
|
67809
|
+
}
|
|
67810
|
+
];
|
|
67811
|
+
}
|
|
67812
|
+
return [];
|
|
67813
|
+
}
|
|
67814
|
+
var FORK_PR_TRIGGER_RE = /^(pull_request_target|issue_comment|workflow_run)$/i;
|
|
67815
|
+
var SAME_REPO_GATE_RE = new RegExp(
|
|
67816
|
+
[
|
|
67817
|
+
String.raw`head(?:\.repo|_repository)\.full_name\s*==\s*github\.(?:repository\b|event\.(?:pull_request\.base\.repo|repository)\.full_name)`,
|
|
67818
|
+
String.raw`github\.repository\s*==\s*github\.event\.(?:pull_request\.head\.repo|workflow_run\.head_repository)\.full_name`,
|
|
67819
|
+
String.raw`head(?:\.repo|_repository)\.fork\s*==\s*false`,
|
|
67820
|
+
String.raw`head(?:\.repo|_repository)\.owner\.login\s*==\s*github\.repository_owner`,
|
|
67821
|
+
String.raw`workflow_run\.pull_requests\[0\]`
|
|
67822
|
+
].join("|"),
|
|
67823
|
+
"i"
|
|
67824
|
+
);
|
|
67825
|
+
var NOT_FORK_ATOM_RE = /^github\.event\.(?:pull_request\.head\.repo|workflow_run\.head_repository)\.fork$/i;
|
|
67826
|
+
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;
|
|
67827
|
+
function requiresAtom(src, re, events, notRe = NOT_FORK_ATOM_RE) {
|
|
67828
|
+
const x = parseIf(src);
|
|
67829
|
+
if (!x) return false;
|
|
67830
|
+
const req = (e) => {
|
|
67831
|
+
if (events?.length && events.every((ev) => !exprAllowsEvent(e, ev))) return true;
|
|
67832
|
+
switch (e.k) {
|
|
67833
|
+
case "atom": {
|
|
67834
|
+
const m = maskLiterals(e.t);
|
|
67835
|
+
return re.test(m) && !CHAINED_COMPARE_RE.test(m);
|
|
67836
|
+
}
|
|
67837
|
+
case "not":
|
|
67838
|
+
return e.c.k === "atom" && notRe.test(e.c.t.trim());
|
|
67839
|
+
case "and":
|
|
67840
|
+
return e.c.some(req);
|
|
67841
|
+
case "or":
|
|
67842
|
+
return e.c.every(req);
|
|
66967
67843
|
}
|
|
66968
|
-
|
|
66969
|
-
|
|
66970
|
-
|
|
67844
|
+
};
|
|
67845
|
+
return req(x);
|
|
67846
|
+
}
|
|
67847
|
+
var PR_EVENTS = [
|
|
67848
|
+
"pull_request_target",
|
|
67849
|
+
"pull_request",
|
|
67850
|
+
"pull_request_review",
|
|
67851
|
+
"pull_request_review_comment"
|
|
67852
|
+
];
|
|
67853
|
+
function headRefEvents(heads) {
|
|
67854
|
+
const evs = /* @__PURE__ */ new Set();
|
|
67855
|
+
for (const st of heads) {
|
|
67856
|
+
if (st.uses == null) return null;
|
|
67857
|
+
const m = /^\s*\$\{\{([\s\S]{1,500})\}\}\s*$/.exec(str(st.with?.["ref"]));
|
|
67858
|
+
if (!m) return null;
|
|
67859
|
+
for (const part of m[1].split("||").map((x) => x.trim())) {
|
|
67860
|
+
if (/^github\.event\.workflow_run\.[\w.]+$/.test(part)) evs.add("workflow_run");
|
|
67861
|
+
else if (/^github\.event\.pull_request\.[\w.]+$/.test(part))
|
|
67862
|
+
PR_EVENTS.forEach((e) => evs.add(e));
|
|
67863
|
+
else if (!/^(?:github\.(?:sha|ref|ref_name|event\.repository\.default_branch)|'[^']*')$/.test(part))
|
|
67864
|
+
return null;
|
|
66971
67865
|
}
|
|
66972
|
-
if (items.length) out[key] = items;
|
|
66973
67866
|
}
|
|
66974
|
-
return
|
|
67867
|
+
return evs.size ? [...evs] : null;
|
|
66975
67868
|
}
|
|
66976
|
-
function
|
|
66977
|
-
const
|
|
66978
|
-
|
|
66979
|
-
|
|
66980
|
-
|
|
66981
|
-
|
|
66982
|
-
|
|
66983
|
-
|
|
66984
|
-
|
|
66985
|
-
|
|
66986
|
-
|
|
67869
|
+
function readOnlySandbox(st) {
|
|
67870
|
+
const args = str(st.with?.["codex-args"]) + " " + str(st.with?.["codex_args"]);
|
|
67871
|
+
return /^read-only$/i.test(str(st.with?.["sandbox"])) && !PERMISSION_BYPASS_RE.test(args);
|
|
67872
|
+
}
|
|
67873
|
+
function requiresTrustedAuthor(src) {
|
|
67874
|
+
const x = parseIf(src);
|
|
67875
|
+
if (!x) return false;
|
|
67876
|
+
const req = (e) => {
|
|
67877
|
+
switch (e.k) {
|
|
67878
|
+
case "atom":
|
|
67879
|
+
return TRUSTED_AUTHOR_GATE_RE.test(e.t) && !NONWRITE_SET_RE.test(e.t) && !CHAINED_COMPARE_RE.test(maskLiterals(e.t));
|
|
67880
|
+
case "not":
|
|
67881
|
+
return false;
|
|
67882
|
+
case "and":
|
|
67883
|
+
return e.c.some(req);
|
|
67884
|
+
case "or":
|
|
67885
|
+
return e.c.every(req);
|
|
66987
67886
|
}
|
|
66988
|
-
|
|
67887
|
+
};
|
|
67888
|
+
return req(x);
|
|
67889
|
+
}
|
|
67890
|
+
function analyzeForkCodeOnTrust(path91, content, callers, autoLabels, trustedRunSources) {
|
|
67891
|
+
AUTO_LABELS = autoLabels ?? /* @__PURE__ */ new Set();
|
|
67892
|
+
TRUSTED_RUN_SOURCES = trustedRunSources ?? /* @__PURE__ */ new Set();
|
|
67893
|
+
try {
|
|
67894
|
+
return analyzeForkCodeOnTrustInner(path91, content, callers);
|
|
67895
|
+
} finally {
|
|
67896
|
+
AUTO_LABELS = /* @__PURE__ */ new Set();
|
|
67897
|
+
TRUSTED_RUN_SOURCES = /* @__PURE__ */ new Set();
|
|
66989
67898
|
}
|
|
66990
|
-
if (cur) out.push(cur);
|
|
66991
|
-
return out;
|
|
66992
67899
|
}
|
|
66993
|
-
function
|
|
66994
|
-
|
|
66995
|
-
|
|
66996
|
-
|
|
66997
|
-
|
|
67900
|
+
function analyzeForkCodeOnTrustInner(path91, content, callers) {
|
|
67901
|
+
let raw;
|
|
67902
|
+
try {
|
|
67903
|
+
raw = loadWorkflow(content).raw;
|
|
67904
|
+
} catch {
|
|
67905
|
+
return [];
|
|
67906
|
+
}
|
|
67907
|
+
if (!raw || typeof raw !== "object") return [];
|
|
67908
|
+
const wf = raw;
|
|
67909
|
+
if (callers?.length) CALLERS.set(wf, callers);
|
|
67910
|
+
else CALLERS.delete(wf);
|
|
67911
|
+
const reach = triggerReach(wf, raw);
|
|
67912
|
+
const forkKeys = keysWithCallers(reach.keys, callers).filter((k) => FORK_PR_TRIGGER_RE.test(k));
|
|
67913
|
+
if (!forkKeys.length) return [];
|
|
67914
|
+
const inj = new Set(injectableJobs(wf, raw, reach.untrusted || reach.reusable));
|
|
67915
|
+
for (const job of jobList(wf)) {
|
|
67916
|
+
if (inj.has(job)) continue;
|
|
67917
|
+
const steps = job.steps ?? [];
|
|
67918
|
+
const agent = steps.filter(isAgentStep);
|
|
67919
|
+
if (!agent.length) continue;
|
|
67920
|
+
if (untrustedHeadCheckout(steps) !== "root") continue;
|
|
67921
|
+
if (agent.every(readOnlySandbox)) continue;
|
|
67922
|
+
if (requiresAtom(str(job.if), SAME_REPO_GATE_RE)) continue;
|
|
67923
|
+
if (agent.every((st) => requiresAtom(str(st.if), SAME_REPO_GATE_RE))) continue;
|
|
67924
|
+
const heads = steps.filter((st) => untrustedHeadCheckout([st]) === "root");
|
|
67925
|
+
if (heads.length && heads.every((st) => requiresAtom(str(st.if), SAME_REPO_GATE_RE))) continue;
|
|
67926
|
+
const evs = headRefEvents(heads);
|
|
67927
|
+
if (evs && !forkKeys.some((k) => evs.includes(k.toLowerCase()))) continue;
|
|
67928
|
+
if (evs && requiresAtom(str(job.if), SAME_REPO_GATE_RE, evs)) continue;
|
|
67929
|
+
if (requiresTrustedAuthor(str(job.if))) continue;
|
|
67930
|
+
const holdsSecret = /secrets\./i.test(str(job.steps) + " " + str(job.env));
|
|
67931
|
+
if (!holdsSecret && !jobsHaveWrite(wf, [job], /:\s*["']?write/i)) continue;
|
|
67932
|
+
return [
|
|
67933
|
+
{
|
|
67934
|
+
check: "CI-2",
|
|
67935
|
+
rule: "CI-2.fork-code-on-trust",
|
|
67936
|
+
dimension: "workflows",
|
|
67937
|
+
severity: "advisory",
|
|
67938
|
+
title: "A maintainer's action runs the agent inside the fork's code",
|
|
67939
|
+
file: path91,
|
|
67940
|
+
signals: [
|
|
67941
|
+
`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)`,
|
|
67942
|
+
"checks out the untrusted PR head into the workspace root",
|
|
67943
|
+
"the agent loads the fork's CLAUDE.md, .claude/settings.json hooks, .mcp.json and build scripts next to the job's secrets"
|
|
67944
|
+
],
|
|
67945
|
+
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."
|
|
67946
|
+
}
|
|
67947
|
+
];
|
|
66998
67948
|
}
|
|
66999
67949
|
return [];
|
|
67000
67950
|
}
|
|
67001
67951
|
|
|
67002
67952
|
// src/ci-check/agent-config.ts
|
|
67953
|
+
var import_path72 = __toESM(require("path"));
|
|
67003
67954
|
function asStrings(v) {
|
|
67004
67955
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
67005
67956
|
}
|
|
@@ -67017,11 +67968,32 @@ function hookCommands(hooks) {
|
|
|
67017
67968
|
return out;
|
|
67018
67969
|
}
|
|
67019
67970
|
var OPEN_RUNNER_RE = new RegExp(
|
|
67020
|
-
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
|
|
67971
|
+
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
|
|
67972
|
+
// as Bash, review round 2).
|
|
67973
|
+
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} ?['"]?(?::\*| \*|\*)\)$`,
|
|
67021
67974
|
"i"
|
|
67022
67975
|
);
|
|
67023
67976
|
var OPEN_RUNNER_MAX = 300;
|
|
67024
67977
|
var INFO_FLAG_RE = /\s--(version|help)\b/;
|
|
67978
|
+
var EXTRA_RUNNER_RE = new RegExp(
|
|
67979
|
+
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} ?['"]?(?::\*| \*|\*)\)$`,
|
|
67980
|
+
"i"
|
|
67981
|
+
);
|
|
67982
|
+
var EXEC_ESCAPE_RE = new RegExp(
|
|
67983
|
+
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} ?['"]?(?::\*| \*|\*)\)$`,
|
|
67984
|
+
"i"
|
|
67985
|
+
);
|
|
67986
|
+
function execEscape(grant) {
|
|
67987
|
+
if (grant.length > OPEN_RUNNER_MAX) return false;
|
|
67988
|
+
return EXEC_ESCAPE_RE.test(
|
|
67989
|
+
grant.replace(/^PowerShell(?=$|\()/, "Bash").replace(/\s+/g, " ").replace(/\(\s+/, "(")
|
|
67990
|
+
);
|
|
67991
|
+
}
|
|
67992
|
+
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)$/;
|
|
67993
|
+
function extraRunner(grant) {
|
|
67994
|
+
if (grant.length > OPEN_RUNNER_MAX || INFO_FLAG_RE.test(grant)) return false;
|
|
67995
|
+
return EXTRA_RUNNER_RE.test(grant.replace(/\s+/g, " ").replace(/\(\s+/, "(")) || MCP_EXEC_RE.test(grant);
|
|
67996
|
+
}
|
|
67025
67997
|
function openRunner(grant) {
|
|
67026
67998
|
if (grant.length > OPEN_RUNNER_MAX || INFO_FLAG_RE.test(grant)) return false;
|
|
67027
67999
|
return OPEN_RUNNER_RE.test(grant.replace(/\s+/g, " ").replace(/\(\s+/, "("));
|
|
@@ -67029,13 +68001,15 @@ function openRunner(grant) {
|
|
|
67029
68001
|
function gradeBroadGrant(allow, deny, opts = {}) {
|
|
67030
68002
|
const denySupported = opts.denySupported ?? true;
|
|
67031
68003
|
const scope = opts.scope ?? "for everyone who opens this repo with the agent";
|
|
67032
|
-
const
|
|
67033
|
-
|
|
67034
|
-
|
|
68004
|
+
const asBash = (a) => a.replace(/^PowerShell(?=$|\()/, "Bash");
|
|
68005
|
+
const broad = allow.filter((raw) => {
|
|
68006
|
+
const a = asBash(raw);
|
|
68007
|
+
return /^Bash$|^Bash\(\s*\*|^Bash\(git:|^Write\(\s*\*|^Write$|^Edit$/.test(a) || openRunner(a) || opts.settings === true && extraRunner(a);
|
|
68008
|
+
});
|
|
67035
68009
|
if (broad.length === 0) return null;
|
|
67036
|
-
const hasBackstop = deny.some((d) => /Bash|Write|Edit/.test(d));
|
|
67037
|
-
const bashBackstop = deny.some((d) => /^Bash\b/.test(d));
|
|
67038
|
-
const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(a));
|
|
68010
|
+
const hasBackstop = deny.some((d) => /Bash|PowerShell|Write|Edit/.test(d));
|
|
68011
|
+
const bashBackstop = deny.some((d) => /^(?:Bash|PowerShell)\b/.test(d));
|
|
68012
|
+
const bareShell = broad.some((a) => /^Bash$|^Bash\(\s*\*/.test(asBash(a)));
|
|
67039
68013
|
const high = bareShell && !bashBackstop;
|
|
67040
68014
|
const signals = [`broad allow(s): ${broad.slice(0, 5).join(", ")}`];
|
|
67041
68015
|
if (high)
|
|
@@ -67052,7 +68026,7 @@ function gradeBroadGrant(allow, deny, opts = {}) {
|
|
|
67052
68026
|
);
|
|
67053
68027
|
if (broad.some((a) => /^Write|^Edit$/.test(a)))
|
|
67054
68028
|
signals.push("`Write`/`Edit` pre-approve file changes without a prompt");
|
|
67055
|
-
if (broad.some(openRunner))
|
|
68029
|
+
if (broad.some((a) => openRunner(asBash(a))))
|
|
67056
68030
|
signals.push(
|
|
67057
68031
|
"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"
|
|
67058
68032
|
);
|
|
@@ -67165,9 +68139,12 @@ function analyzeAgentConfig(path91, content, tree) {
|
|
|
67165
68139
|
}
|
|
67166
68140
|
}
|
|
67167
68141
|
}
|
|
67168
|
-
const allow =
|
|
68142
|
+
const allow = [
|
|
68143
|
+
...asStrings(cfg.permissions?.allow),
|
|
68144
|
+
...asStrings(cfg.permissions?.allowedTools)
|
|
68145
|
+
];
|
|
67169
68146
|
const deny = asStrings(cfg.permissions?.deny);
|
|
67170
|
-
const grade = gradeBroadGrant(allow, deny);
|
|
68147
|
+
const grade = gradeBroadGrant(allow, deny, { settings: true });
|
|
67171
68148
|
if (grade) {
|
|
67172
68149
|
const { high, signals } = grade;
|
|
67173
68150
|
const at = jsonValueIndex(content, grade.broad[0]);
|
|
@@ -67192,9 +68169,47 @@ function analyzeAgentConfig(path91, content, tree) {
|
|
|
67192
68169
|
})
|
|
67193
68170
|
});
|
|
67194
68171
|
}
|
|
68172
|
+
const escapes = allow.filter(execEscape);
|
|
68173
|
+
if (escapes.length) {
|
|
68174
|
+
const at = jsonValueIndex(content, escapes[0]);
|
|
68175
|
+
findings.push({
|
|
68176
|
+
...at >= 0 ? { line: lineAtIndex(content, at) } : {},
|
|
68177
|
+
check: "CI-1",
|
|
68178
|
+
rule: "CI-1.exec-escape-grant",
|
|
68179
|
+
dimension: "toolRules",
|
|
68180
|
+
severity: "medium",
|
|
68181
|
+
title: "Committed agent config pre-approves a command that can run other programs",
|
|
68182
|
+
file: path91,
|
|
68183
|
+
signals: [
|
|
68184
|
+
`pre-approved: ${escapes.slice(0, 5).map((e) => `\`${safeText(e, 80)}\``).join(", ")}`,
|
|
68185
|
+
"`find -exec` and `make -n --eval` run other programs without a prompt"
|
|
68186
|
+
],
|
|
68187
|
+
fix: "Pre-approve the exact command you need (for example `Bash(find . -name *.ts)` or `Bash(make test)`), not every argument."
|
|
68188
|
+
});
|
|
68189
|
+
}
|
|
67195
68190
|
return findings;
|
|
67196
68191
|
}
|
|
67197
68192
|
var GRANT_CARRIER_RE = /(^|\/)[Ss][Kk][Ii][Ll][Ll]\.md$|(^|\/)\.claude\/commands\/.+\.md$/;
|
|
68193
|
+
var ROOT_SURFACE_RE = /^\.(claude|agents|github|codex|cursor|gemini)\//;
|
|
68194
|
+
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/;
|
|
68195
|
+
var PR_TEXT_RE = /gh\s+pr\s+(?:view|diff)\b|gh\s+api\s+[^\n]{0,120}\/pulls\b/;
|
|
68196
|
+
var WEB_TEXT_RE = /\bsearch the web\b|\bfetch (?:the )?(?:url|page)\b/i;
|
|
68197
|
+
function skillReadsUntrusted(path91, content, allow, broad) {
|
|
68198
|
+
if (!ROOT_SURFACE_RE.test(path91)) return null;
|
|
68199
|
+
const body = content.replace(/^---[\s\S]*?\n---/, "");
|
|
68200
|
+
const runsCode = broad.map((a) => a.replace(/^PowerShell(?=$|\()/, "Bash")).some((a) => /^Bash$|^Bash\(\s*\*/.test(a) || openRunner(a) && !/^Bash\(\s*gh\b/.test(a));
|
|
68201
|
+
const webGrant = allow.some((a) => /^(WebFetch|WebSearch)\b/.test(a));
|
|
68202
|
+
const reads = (re) => body.replace(/\\\r?\n/g, " ").split("\n").some((l) => re.test(l) && !sanitizedListing(l));
|
|
68203
|
+
if (runsCode && reads(ITEM_TEXT_RE))
|
|
68204
|
+
return "it reads pull request or issue text that strangers write, with a grant that runs code";
|
|
68205
|
+
if (runsCode && (webGrant || WEB_TEXT_RE.test(body)))
|
|
68206
|
+
return "it reads web content anyone can publish, with a grant that runs code";
|
|
68207
|
+
const writes = allow.some((a) => /^(Write|Edit|MultiEdit)$/.test(a));
|
|
68208
|
+
const ghOrGit = allow.some((a) => /^Bash\(\s*(gh|git)\b/.test(a));
|
|
68209
|
+
if (writes && ghOrGit && reads(PR_TEXT_RE))
|
|
68210
|
+
return "it reads pull request text that strangers write, and may edit files and run git/gh on it";
|
|
68211
|
+
return null;
|
|
68212
|
+
}
|
|
67198
68213
|
function analyzeSkillGrants(path91, content) {
|
|
67199
68214
|
if (!GRANT_CARRIER_RE.test(path91)) return [];
|
|
67200
68215
|
const allow = allowedToolsOf(parseFrontmatter(content));
|
|
@@ -67206,6 +68221,21 @@ function analyzeSkillGrants(path91, content) {
|
|
|
67206
68221
|
if (!grade) return [];
|
|
67207
68222
|
const kind = /commands\//.test(path91) ? "slash command" : "skill";
|
|
67208
68223
|
const at = content.search(/^allowed-tools\s*:/m);
|
|
68224
|
+
const untrusted = skillReadsUntrusted(path91, content, allow, grade.broad);
|
|
68225
|
+
if (untrusted)
|
|
68226
|
+
return [
|
|
68227
|
+
{
|
|
68228
|
+
check: "CI-1",
|
|
68229
|
+
rule: "CI-1.skill-untrusted-input",
|
|
68230
|
+
...at >= 0 ? { line: lineAtIndex(content, at) } : {},
|
|
68231
|
+
dimension: "toolRules",
|
|
68232
|
+
severity: "advisory",
|
|
68233
|
+
title: `Committed ${kind} runs code or writes with text strangers control`,
|
|
68234
|
+
file: path91,
|
|
68235
|
+
signals: [...grade.signals, untrusted],
|
|
68236
|
+
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."
|
|
68237
|
+
}
|
|
68238
|
+
];
|
|
67209
68239
|
return [
|
|
67210
68240
|
{
|
|
67211
68241
|
check: "CI-1",
|
|
@@ -67227,27 +68257,178 @@ function analyzeSkillGrants(path91, content) {
|
|
|
67227
68257
|
|
|
67228
68258
|
// src/ci-check/mcp.ts
|
|
67229
68259
|
init_dist();
|
|
67230
|
-
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;&|`$<>()]+)*$/;
|
|
67231
|
-
|
|
68260
|
+
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;&|`$<>()]+)*$/;
|
|
68261
|
+
var LAUNCHERS = [
|
|
68262
|
+
{ re: /\bnpx\b/, name: "npx", registry: "npm" },
|
|
68263
|
+
{ re: /\bpnpx\b|\bpnpm\s+dlx\b/, name: "pnpm dlx", registry: "npm" },
|
|
68264
|
+
{ re: /\byarn\s+dlx\b/, name: "yarn dlx", registry: "npm" },
|
|
68265
|
+
{ re: /\bnpm\s+(?:exec|x)\b/, name: "npm exec", registry: "npm" },
|
|
68266
|
+
{ re: /\bbunx\b|\bbun\s+x\b/, name: "bunx", registry: "npm" },
|
|
68267
|
+
{ re: /\buvx\b|\buv\s+tool\s+run\b/, name: "uvx", registry: "PyPI" },
|
|
68268
|
+
{ re: /\bpipx\s+run\b/, name: "pipx run", registry: "PyPI" }
|
|
68269
|
+
];
|
|
68270
|
+
var PINNED_RE = /@v?\d|@[0-9a-f]{40}\b|==\s*\d/i;
|
|
68271
|
+
var LOCAL_FROM_RE = /(?:^|\s)--from(?:\s+|=)(?:\.{1,2}(?:\/\S*)?|\/\S*|~\/\S*)(?:\s|$)/;
|
|
68272
|
+
var RUN_VALUE_FLAGS = new Set(
|
|
68273
|
+
[
|
|
68274
|
+
"-a --attach --add-host --annotation --blkio-weight --blkio-weight-device --cap-add",
|
|
68275
|
+
"--cap-drop --cgroup-parent --cgroupns --cgroups --cgroup-conf --cidfile --cpu-period",
|
|
68276
|
+
"--cpu-quota --cpu-rt-period --cpu-rt-runtime -c --cpu-shares --cpus --cpuset-cpus",
|
|
68277
|
+
"--cpuset-mems --device --device-cgroup-rule --device-read-bps --device-read-iops",
|
|
68278
|
+
"--device-write-bps --device-write-iops --dns --dns-option --dns-search --domainname",
|
|
68279
|
+
"--entrypoint -e --env --env-file --expose --gpus --group-add --health-cmd",
|
|
68280
|
+
"--health-interval --health-retries --health-start-interval --health-start-period",
|
|
68281
|
+
"--health-timeout -h --hostname --ip --ip6 --ipc --isolation --kernel-memory -l --label",
|
|
68282
|
+
"--label-file --link --link-local-ip --log-driver --log-opt --mac-address -m --memory",
|
|
68283
|
+
"--memory-reservation --memory-swap --memory-swappiness --mount --name --network --net",
|
|
68284
|
+
"--network-alias --oom-score-adj --pid --pids-limit --platform -p --publish --pull",
|
|
68285
|
+
"--restart --runtime --security-opt --shm-size --stop-signal --stop-timeout",
|
|
68286
|
+
"--storage-opt --sysctl --tmpfs --ulimit -u --user --userns --uts -v --volume",
|
|
68287
|
+
"--volume-driver --volumes-from -w --workdir --secret --pod --arch --os --variant",
|
|
68288
|
+
"--timezone --umask --requires --uidmap --gidmap --subuidname --subgidname --hooks-dir",
|
|
68289
|
+
"--init-path --seccomp-policy --sdnotify --personality --preserve-fds --chrootdirs"
|
|
68290
|
+
].join(" ").split(" ")
|
|
68291
|
+
);
|
|
68292
|
+
function runImage(tokens) {
|
|
68293
|
+
const i = tokens.findIndex(
|
|
68294
|
+
(t, k) => /(^|\/)(docker|podman|nerdctl)$/.test(t) && (tokens[k + 1] === "run" || tokens[k + 1] === "container" && tokens[k + 2] === "run")
|
|
68295
|
+
);
|
|
68296
|
+
if (i < 0) return null;
|
|
68297
|
+
const from = tokens[i + 1] === "run" ? i + 2 : i + 3;
|
|
68298
|
+
for (let k = from; k < tokens.length; k++) {
|
|
68299
|
+
const t = tokens[k];
|
|
68300
|
+
if (t.startsWith("-")) {
|
|
68301
|
+
if (!t.includes("=") && RUN_VALUE_FLAGS.has(t)) k++;
|
|
68302
|
+
continue;
|
|
68303
|
+
}
|
|
68304
|
+
return t;
|
|
68305
|
+
}
|
|
68306
|
+
return null;
|
|
68307
|
+
}
|
|
68308
|
+
function launchedPackages(argv) {
|
|
68309
|
+
const t = argv.split(/\s+/).filter(Boolean);
|
|
68310
|
+
const i = t.findIndex((x) => /(^|\/)(npx|pnpx|bunx|uvx|dlx|exec|x|run)$/.test(x));
|
|
68311
|
+
if (i < 0) return [];
|
|
68312
|
+
const bare = (v) => v.startsWith("@") ? v.replace(/^(@[^/]+\/[^@]+).*$/, "$1") : v.replace(/(==|@).*$/, "");
|
|
68313
|
+
const flagged = [];
|
|
68314
|
+
let positional = null;
|
|
68315
|
+
for (let k = i + 1; k < t.length; k++) {
|
|
68316
|
+
const tok = t[k];
|
|
68317
|
+
const eq = /^(--from|-p|--package)=(.*)$/.exec(tok);
|
|
68318
|
+
if (eq) {
|
|
68319
|
+
flagged.push(eq[2]);
|
|
68320
|
+
continue;
|
|
68321
|
+
}
|
|
68322
|
+
if (/^(--from|-p|--package)$/.test(tok)) {
|
|
68323
|
+
if (t[k + 1]) flagged.push(t[++k]);
|
|
68324
|
+
continue;
|
|
68325
|
+
}
|
|
68326
|
+
if (tok.startsWith("-")) continue;
|
|
68327
|
+
positional ??= tok;
|
|
68328
|
+
}
|
|
68329
|
+
const names = flagged.length ? flagged : positional ? [positional] : [];
|
|
68330
|
+
return names.map((n) => bare(n).toLowerCase()).filter(Boolean);
|
|
68331
|
+
}
|
|
68332
|
+
var SHELL_RE = /(?:^|\/)(?:sh|bash|zsh|dash|cmd|pwsh|powershell)(?:\.exe)?$/i;
|
|
68333
|
+
var SHELL_OP_RE = /[;&|`$<>()]/;
|
|
68334
|
+
function launchesOwnPackage(argv, own) {
|
|
68335
|
+
if (own.size === 0) return false;
|
|
68336
|
+
if (SHELL_OP_RE.test(argv) || SHELL_RE.test(argv.split(/\s+/)[0] ?? "")) return false;
|
|
68337
|
+
const l = LAUNCHERS.find((x) => x.re.test(argv));
|
|
68338
|
+
if (!l) return false;
|
|
68339
|
+
const reg = l.registry === "PyPI" ? "pypi" : "npm";
|
|
68340
|
+
const pkgs = launchedPackages(argv);
|
|
68341
|
+
return pkgs.length > 0 && pkgs.every((p) => own.has(`${reg}:${p}`) || own.has(p));
|
|
68342
|
+
}
|
|
68343
|
+
function unpinnedLauncher(argv, own) {
|
|
68344
|
+
if (LOCAL_RUNNER_RE.test(argv)) return null;
|
|
68345
|
+
if (!SHELL_OP_RE.test(argv) && LOCAL_FROM_RE.test(argv)) return null;
|
|
68346
|
+
if (launchesOwnPackage(argv, own)) return null;
|
|
68347
|
+
const image = runImage(argv.split(/\s+/));
|
|
68348
|
+
if (image) {
|
|
68349
|
+
if (image.includes("@sha256:")) return null;
|
|
68350
|
+
const tail = image.slice(image.lastIndexOf("/") + 1);
|
|
68351
|
+
const tag = tail.includes(":") ? tail.slice(tail.indexOf(":") + 1) : "";
|
|
68352
|
+
return !tag || tag === "latest" ? { launcher: "docker run", registry: "the image registry" } : null;
|
|
68353
|
+
}
|
|
68354
|
+
const goMod = /\bgo\s+run\s+(?:-\S+\s+)*([^\s@]+)@(\S+)/.exec(argv);
|
|
68355
|
+
if (goMod)
|
|
68356
|
+
return /^v\d/.test(goMod[2]) || /^[0-9a-f]{40}$/.test(goMod[2]) ? null : { launcher: "go run", registry: "the Go module proxy" };
|
|
68357
|
+
const l = LAUNCHERS.find((x) => x.re.test(argv));
|
|
68358
|
+
if (!l) return null;
|
|
68359
|
+
return /@latest\b/.test(argv) || !PINNED_RE.test(argv) ? { launcher: l.name, registry: l.registry } : null;
|
|
68360
|
+
}
|
|
68361
|
+
function ownPackageNames(manifests) {
|
|
68362
|
+
const out = /* @__PURE__ */ new Set();
|
|
68363
|
+
for (const m of manifests) {
|
|
68364
|
+
if (m.path === "package.json") {
|
|
68365
|
+
try {
|
|
68366
|
+
const pkg = JSON.parse(m.content);
|
|
68367
|
+
if (typeof pkg.name === "string" && pkg.name && pkg.private !== true)
|
|
68368
|
+
out.add(`npm:${pkg.name.toLowerCase()}`);
|
|
68369
|
+
} catch {
|
|
68370
|
+
}
|
|
68371
|
+
} else if (m.path === "pyproject.toml") {
|
|
68372
|
+
const sec = /^\[(project|tool\.poetry)\]\s*$([\s\S]*?)(?=^\[|(?![\s\S]))/m.exec(m.content);
|
|
68373
|
+
const name = sec && /^name\s*=\s*["']([^"']{1,200})["']/m.exec(sec[2]);
|
|
68374
|
+
if (name) {
|
|
68375
|
+
out.add(`pypi:${name[1].toLowerCase()}`);
|
|
68376
|
+
out.add(`pypi:${name[1].toLowerCase().replace(/_/g, "-")}`);
|
|
68377
|
+
}
|
|
68378
|
+
}
|
|
68379
|
+
}
|
|
68380
|
+
return out;
|
|
68381
|
+
}
|
|
68382
|
+
function analyzeMcp(path91, content, own = /* @__PURE__ */ new Set()) {
|
|
67232
68383
|
let cfg;
|
|
67233
68384
|
try {
|
|
67234
68385
|
cfg = JSON.parse(content);
|
|
67235
68386
|
} catch {
|
|
67236
68387
|
return [];
|
|
67237
68388
|
}
|
|
67238
|
-
|
|
68389
|
+
if (!cfg || typeof cfg !== "object") return [];
|
|
68390
|
+
const sections = [];
|
|
68391
|
+
const obj = (x) => x && typeof x === "object" && !Array.isArray(x) ? x : {};
|
|
68392
|
+
sections.push(["mcpServers", obj(cfg.mcpServers)]);
|
|
68393
|
+
sections.push(["servers", obj(cfg.servers)]);
|
|
68394
|
+
const opencode = {};
|
|
68395
|
+
for (const [name, s] of Object.entries(obj(cfg.mcp))) {
|
|
68396
|
+
if (!s || typeof s !== "object") continue;
|
|
68397
|
+
const o = s;
|
|
68398
|
+
const cmd = Array.isArray(o.command) ? o.command.map(String) : [];
|
|
68399
|
+
opencode[name] = {
|
|
68400
|
+
command: cmd[0],
|
|
68401
|
+
args: cmd.slice(1),
|
|
68402
|
+
url: o.url,
|
|
68403
|
+
env: obj(o.environment),
|
|
68404
|
+
disabled: o.enabled === false
|
|
68405
|
+
};
|
|
68406
|
+
}
|
|
68407
|
+
sections.push(["mcp", opencode]);
|
|
68408
|
+
const count = /* @__PURE__ */ new Map();
|
|
68409
|
+
for (const [, sec] of sections)
|
|
68410
|
+
for (const name of Object.keys(sec)) count.set(name, (count.get(name) ?? 0) + 1);
|
|
68411
|
+
const servers = {};
|
|
68412
|
+
for (const [section, sec] of sections)
|
|
68413
|
+
for (const [name, spec] of Object.entries(sec)) {
|
|
68414
|
+
if (!spec || typeof spec !== "object") continue;
|
|
68415
|
+
const key = (count.get(name) ?? 0) > 1 ? `${section}.${name}` : name;
|
|
68416
|
+
servers[key] = { ...spec, anchor: name };
|
|
68417
|
+
}
|
|
68418
|
+
return analyzeMcpServers(servers, path91, content, own);
|
|
67239
68419
|
}
|
|
67240
|
-
function analyzeMcpServers(servers, path91, content = "") {
|
|
68420
|
+
function analyzeMcpServers(servers, path91, content = "", own = /* @__PURE__ */ new Set()) {
|
|
67241
68421
|
const findings = [];
|
|
67242
68422
|
for (const [name, srv] of Object.entries(servers ?? {})) {
|
|
67243
68423
|
if (!srv || srv.disabled) continue;
|
|
67244
68424
|
const argv = [srv.command, ...Array.isArray(srv.args) ? srv.args.map(String) : []].join(" ");
|
|
67245
|
-
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
68425
|
+
const esc = (srv.anchor ?? name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
67246
68426
|
const at = content.search(
|
|
67247
68427
|
new RegExp(`"${esc}"\\s*:|^[ \\t]*\\[mcp_servers\\.(?:"${esc}"|${esc})\\]`, "m")
|
|
67248
68428
|
);
|
|
67249
68429
|
const line = at >= 0 ? lineAtIndex(content, at) : void 0;
|
|
67250
|
-
|
|
68430
|
+
const why = unpinnedLauncher(argv, own);
|
|
68431
|
+
if (why) {
|
|
67251
68432
|
findings.push({
|
|
67252
68433
|
check: "CI-3",
|
|
67253
68434
|
rule: "CI-3.mcp-unpinned",
|
|
@@ -67257,9 +68438,16 @@ function analyzeMcpServers(servers, path91, content = "") {
|
|
|
67257
68438
|
title: `MCP server "${name}" runs an unpinned executable`,
|
|
67258
68439
|
file: path91,
|
|
67259
68440
|
...line ? { line } : {},
|
|
67260
|
-
signals: [
|
|
68441
|
+
signals: [
|
|
68442
|
+
why.launcher === "npx" ? `\`${safeText(argv, 120)}\`: unversioned/@latest npx` : `\`${safeText(argv, 120)}\`: unversioned/latest ${why.launcher}`
|
|
68443
|
+
],
|
|
67261
68444
|
fix: "Pin the MCP server package to an exact version so a PR (or a registry compromise) can\u2019t swap the toolchain.",
|
|
67262
|
-
explain: explainMcp({
|
|
68445
|
+
explain: explainMcp({
|
|
68446
|
+
name,
|
|
68447
|
+
argv,
|
|
68448
|
+
registry: why.registry,
|
|
68449
|
+
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
|
|
68450
|
+
})
|
|
67263
68451
|
});
|
|
67264
68452
|
}
|
|
67265
68453
|
for (const [k, v] of Object.entries(srv.env ?? {})) {
|
|
@@ -67288,7 +68476,7 @@ function analyzeMcpServers(servers, path91, content = "") {
|
|
|
67288
68476
|
|
|
67289
68477
|
// src/ci-check/codex.ts
|
|
67290
68478
|
var import_smol_toml7 = require("smol-toml");
|
|
67291
|
-
function analyzeCodexConfig(path91, content) {
|
|
68479
|
+
function analyzeCodexConfig(path91, content, own = /* @__PURE__ */ new Set()) {
|
|
67292
68480
|
let cfg;
|
|
67293
68481
|
try {
|
|
67294
68482
|
cfg = (0, import_smol_toml7.parse)(content);
|
|
@@ -67296,7 +68484,7 @@ function analyzeCodexConfig(path91, content) {
|
|
|
67296
68484
|
return [];
|
|
67297
68485
|
}
|
|
67298
68486
|
const findings = [];
|
|
67299
|
-
findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path91, content));
|
|
68487
|
+
findings.push(...analyzeMcpServers(cfg.mcp_servers ?? {}, path91, content, own));
|
|
67300
68488
|
const sandbox = typeof cfg.sandbox_mode === "string" ? cfg.sandbox_mode : "";
|
|
67301
68489
|
const approval = typeof cfg.approval_policy === "string" ? cfg.approval_policy : "";
|
|
67302
68490
|
const fullAccess = /danger-full-access/i.test(sandbox);
|
|
@@ -67333,10 +68521,13 @@ var UNREAD_RULES = /* @__PURE__ */ new Set([
|
|
|
67333
68521
|
"CI-1.hook-script.unscanned-size",
|
|
67334
68522
|
"CI-6.skill-script.unscanned-size"
|
|
67335
68523
|
]);
|
|
67336
|
-
var CONCEALMENT = /* @__PURE__ */ new Set(["CI-6.unicode-tag-chars", "CI-6.bidi-override"]);
|
|
68524
|
+
var CONCEALMENT = /* @__PURE__ */ new Set(["CI-6.unicode-tag-chars", "CI-6.bidi-override", "CI-6.homoglyph"]);
|
|
67337
68525
|
function tierOf(f) {
|
|
67338
68526
|
const { rule, severity } = f;
|
|
67339
68527
|
if (rule === "CI-1.skill-allowed-tools") return "review";
|
|
68528
|
+
if (rule === "CI-1.exec-escape-grant") return "review";
|
|
68529
|
+
if (rule === "CI-2.trusted-trigger-reader" || rule === "CI-2.fork-code-on-trust" || rule === "CI-1.skill-untrusted-input")
|
|
68530
|
+
return "alert";
|
|
67340
68531
|
if (UNREAD_RULES.has(rule)) return "review";
|
|
67341
68532
|
if (rule.startsWith("CI-2.") || rule.startsWith("CI-4."))
|
|
67342
68533
|
return severity === "advisory" ? "note" : "alert";
|
|
@@ -67362,6 +68553,8 @@ function reviewQuestion(rule) {
|
|
|
67362
68553
|
return "Does this hook exist on every machine that runs it?";
|
|
67363
68554
|
if (/hidden-chars/.test(rule))
|
|
67364
68555
|
return "Why does this script hold invisible characters? A list of them to detect is fine.";
|
|
68556
|
+
if (/exec-escape/.test(rule))
|
|
68557
|
+
return "Does the agent need this command with any arguments, or only one exact form?";
|
|
67365
68558
|
if (/unscanned/.test(rule))
|
|
67366
68559
|
return "node9 did not read this script (too large, or outside the folders it reads). What does it run?";
|
|
67367
68560
|
return "Is this an instruction the agent follows, or text for a person?";
|
|
@@ -67701,12 +68894,59 @@ function scanTree(tree) {
|
|
|
67701
68894
|
}
|
|
67702
68895
|
return named.get(dir).has(docPath);
|
|
67703
68896
|
};
|
|
68897
|
+
const callersOf = /* @__PURE__ */ new Map();
|
|
68898
|
+
const autoLabels = /* @__PURE__ */ new Set();
|
|
68899
|
+
const routeOfFile = (file) => file.route !== void 0 ? file.route : routeOf(file.path, skillDirs, appDirs);
|
|
68900
|
+
const manifests = [];
|
|
68901
|
+
for (const file of tree.files) {
|
|
68902
|
+
if (routeOfFile(file) !== "manifest") continue;
|
|
68903
|
+
try {
|
|
68904
|
+
manifests.push({ path: file.path, content: file.content });
|
|
68905
|
+
} catch {
|
|
68906
|
+
}
|
|
68907
|
+
}
|
|
68908
|
+
const ownPackages = ownPackageNames(manifests);
|
|
68909
|
+
for (const file of tree.files) {
|
|
68910
|
+
if (routeOfFile(file) !== "issue-form") continue;
|
|
68911
|
+
try {
|
|
68912
|
+
for (const l of issueFormLabels(file.content)) autoLabels.add(l);
|
|
68913
|
+
} catch {
|
|
68914
|
+
}
|
|
68915
|
+
}
|
|
68916
|
+
const runSources = /* @__PURE__ */ new Map();
|
|
68917
|
+
for (const file of tree.files) {
|
|
68918
|
+
if (routeOfFile(file) !== "workflow") continue;
|
|
68919
|
+
try {
|
|
68920
|
+
const src = runSourceOf(file.path, file.content, autoLabels);
|
|
68921
|
+
if (src) runSources.set(src.name, (runSources.get(src.name) ?? true) && src.trusted);
|
|
68922
|
+
} catch {
|
|
68923
|
+
}
|
|
68924
|
+
}
|
|
68925
|
+
const trustedRunSources = new Set([...runSources].filter(([, t]) => t).map(([n]) => n));
|
|
68926
|
+
for (const file of tree.files) {
|
|
68927
|
+
if (routeOfFile(file) !== "workflow") continue;
|
|
68928
|
+
try {
|
|
68929
|
+
const content = file.content;
|
|
68930
|
+
for (const { target, caller } of localCallsOf(
|
|
68931
|
+
file.path,
|
|
68932
|
+
content,
|
|
68933
|
+
autoLabels,
|
|
68934
|
+
trustedRunSources
|
|
68935
|
+
)) {
|
|
68936
|
+
const list = callersOf.get(target) ?? [];
|
|
68937
|
+
list.push(caller);
|
|
68938
|
+
callersOf.set(target, list);
|
|
68939
|
+
}
|
|
68940
|
+
} catch {
|
|
68941
|
+
}
|
|
68942
|
+
}
|
|
67704
68943
|
for (const file of tree.files) {
|
|
67705
68944
|
let route = file.route !== void 0 ? file.route : routeOf(file.path, skillDirs, appDirs);
|
|
67706
68945
|
if (route === "app-doc") {
|
|
67707
68946
|
if (!namedBy(file.path)) continue;
|
|
67708
68947
|
route = "instruction";
|
|
67709
68948
|
}
|
|
68949
|
+
if (route === "issue-form" || route === "manifest") continue;
|
|
67710
68950
|
inspected.push(file.path);
|
|
67711
68951
|
if (file.path === SUPPRESSIONS_FILE) continue;
|
|
67712
68952
|
const testKind = route && CONTENT_ROUTES.has(route) ? testPathKind(file.path, route) : "none";
|
|
@@ -67716,10 +68956,24 @@ function scanTree(tree) {
|
|
|
67716
68956
|
const content = file.content;
|
|
67717
68957
|
switch (route) {
|
|
67718
68958
|
case "workflow": {
|
|
67719
|
-
const
|
|
68959
|
+
const callers = callersOf.get(file.path);
|
|
68960
|
+
const f = analyzeWorkflow(file.path, content, callers, autoLabels, trustedRunSources);
|
|
67720
68961
|
if (f) findings.push(f);
|
|
67721
|
-
const s = analyzeWorkflowSecrets(
|
|
68962
|
+
const s = analyzeWorkflowSecrets(
|
|
68963
|
+
file.path,
|
|
68964
|
+
content,
|
|
68965
|
+
callers,
|
|
68966
|
+
autoLabels,
|
|
68967
|
+
trustedRunSources
|
|
68968
|
+
);
|
|
67722
68969
|
if (s) findings.push(s);
|
|
68970
|
+
findings.push(...analyzeWorkflowPlugins(file.path, content, tree.owner));
|
|
68971
|
+
findings.push(
|
|
68972
|
+
...analyzeTrustedReaders(file.path, content, autoLabels, callers, trustedRunSources)
|
|
68973
|
+
);
|
|
68974
|
+
findings.push(
|
|
68975
|
+
...analyzeForkCodeOnTrust(file.path, content, callers, autoLabels, trustedRunSources)
|
|
68976
|
+
);
|
|
67723
68977
|
break;
|
|
67724
68978
|
}
|
|
67725
68979
|
case "agent-config":
|
|
@@ -67732,10 +68986,10 @@ function scanTree(tree) {
|
|
|
67732
68986
|
findings.push(...analyzeScript(file.path, content, "CI-6.skill-script"));
|
|
67733
68987
|
break;
|
|
67734
68988
|
case "mcp":
|
|
67735
|
-
findings.push(...analyzeMcp(file.path, content));
|
|
68989
|
+
findings.push(...analyzeMcp(file.path, content, ownPackages));
|
|
67736
68990
|
break;
|
|
67737
68991
|
case "codex":
|
|
67738
|
-
findings.push(...analyzeCodexConfig(file.path, content));
|
|
68992
|
+
findings.push(...analyzeCodexConfig(file.path, content, ownPackages));
|
|
67739
68993
|
break;
|
|
67740
68994
|
case "instruction":
|
|
67741
68995
|
findings.push(...analyzeInstructionFile(file.path, content));
|
|
@@ -67743,7 +68997,9 @@ function scanTree(tree) {
|
|
|
67743
68997
|
break;
|
|
67744
68998
|
}
|
|
67745
68999
|
} catch (err2) {
|
|
67746
|
-
notes.push(
|
|
69000
|
+
notes.push(
|
|
69001
|
+
`checker degraded on ${file.path}: ${err2?.message ?? "error"}; results may be INCOMPLETE`
|
|
69002
|
+
);
|
|
67747
69003
|
}
|
|
67748
69004
|
if (testKind === "alerts-only") {
|
|
67749
69005
|
const kept = findings.splice(before).filter((f) => tierOf(f) === "alert");
|