Compare commits

...

4 commits

Author SHA1 Message Date
Sander Declerck
6006760b67
Only inherit io when loglevel verbose 2026-01-12 15:39:26 +01:00
Sander Declerck
22580bcf5f
Set correct version 2026-01-12 15:29:43 +01:00
Sander Declerck
f6abfb8a4e
Install script for linux x64 as well 2026-01-12 15:25:40 +01:00
Sander Declerck
9d1f7ac6fd
Use ramaproxy if it's available. 2026-01-12 14:15:30 +01:00
3 changed files with 282 additions and 119 deletions

View file

@ -317,6 +317,35 @@ main() {
info "Binary installed to: $FINAL_FILE" info "Binary installed to: $FINAL_FILE"
# Download safechain-proxy
if [ "$OS" = "macos" ] || [ "$OS" = "linux" ] || [ "$OS" = "linuxstatic" ]; then
info "Downloading safechain-proxy..."
if [ "$OS" = "macos" ]; then
if [ "$ARCH" = "arm64" ]; then
PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.3-linux-proxy-bins/safechain-proxy-darwin-arm64"
else
PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.3-linux-proxy-bins/safechain-proxy-darwin-amd64"
fi
else
# Linux (both linux and linuxstatic)
if [ "$ARCH" = "x64" ]; then
PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.3-linux-proxy-bins/safechain-proxy-linux-amd64"
else
# Skip for non-x64 Linux architectures
info "Skipping safechain-proxy download (not available for linux-${ARCH})"
PROXY_URL=""
fi
fi
if [ -n "$PROXY_URL" ]; then
PROXY_FILE="${INSTALL_DIR}/safechain-proxy"
download "$PROXY_URL" "$PROXY_FILE"
chmod +x "$PROXY_FILE" || error "Failed to make proxy executable"
info "Proxy installed to: $PROXY_FILE"
fi
fi
# Build setup command based on arguments # Build setup command based on arguments
SETUP_CMD="setup" SETUP_CMD="setup"
SETUP_ARGS="" SETUP_ARGS=""

View file

@ -0,0 +1,107 @@
import { ChildProcess, spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { mkdtempSync, readFile, writeFile } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { ui } from "../environment/userInteraction.js";
import { getLoggingLevel, LOGGING_VERBOSE } from "../config/settings.js";
const readFilePromise = promisify(readFile);
const writeFilePromise = promisify(writeFile);
/**
* @typedef {Object} RamaProxyInstance
* @property {ChildProcess} process
* @property {string} proxyAddress
* @property {string} metaAddress
* @property {string} certPath
*/
/**
* @returns {String | null}
*/
export function getRamaPath() {
const executableDir = dirname(process.execPath);
const ramaPath = join(executableDir, "safechain-proxy");
ui.writeWarning(ramaPath);
if (existsSync(ramaPath)) {
return ramaPath;
}
return null;
}
/**
* @param {string} ramaPath
*
* @returns {import("./registryProxy.js").SafeChainProxy} */
export function createRamaProxy(ramaPath) {
const tempDir = mkdtempSync(join(tmpdir(), "safe-chain-proxy-"));
/** @type {RamaProxyInstance | null} */
let ramaInstance = null;
return {
startServer: async () => {
ramaInstance = await startRama(ramaPath, tempDir);
ui.writeInformation(
`Proxy started at address "${ramaInstance.proxyAddress}"`
);
},
stopServer: async () => {
if (ramaInstance) {
ramaInstance.process.kill();
}
return Promise.resolve();
},
verifyNoMaliciousPackages: () => true,
hasSuppressedVersions: () => false,
getServerPort: () => {
if (!ramaInstance) return null;
const url = new URL(`http://${ramaInstance.proxyAddress}`);
return url.port ? parseInt(url.port, 10) : null;
},
getCombinedCaBundlePath: () => ramaInstance?.certPath ?? "",
};
}
/**
* @param {string} ramaPath
* @param {string} dataFolder
* @returns {Promise<RamaProxyInstance>}
*/
async function startRama(ramaPath, dataFolder) {
const args = ["--secrets", "memory", "--data", dataFolder];
const process =
getLoggingLevel() === LOGGING_VERBOSE
? spawn(ramaPath, args, {
stdio: "inherit",
})
: spawn(ramaPath, args);
// wait some time to allow the proxy process to start
await new Promise((resolve) => setTimeout(resolve, 5000));
const proxyAddress = await readFilePromise(
join(dataFolder, "proxy.addr.txt"),
"utf-8"
);
const metaAddress = await readFilePromise(
join(dataFolder, "meta.addr.txt"),
"utf-8"
);
const certResponse = await fetch(`http://${metaAddress}/ca`);
const cert = await certResponse.text();
const certPath = join(dataFolder, "cert.ca");
await writeFilePromise(certPath, cert);
return {
process,
proxyAddress,
metaAddress,
certPath,
};
}

View file

@ -7,37 +7,47 @@ import { ui } from "../environment/userInteraction.js";
import chalk from "chalk"; import chalk from "chalk";
import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js";
import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js"; import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js";
import { createRamaProxy, getRamaPath } from "./createRamaProxy.js";
const SERVER_STOP_TIMEOUT_MS = 1000;
/** /**
* @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}} * @typedef {Object} SafeChainProxy
* @prop {() => Promise<void>} startServer
* @prop {() => Promise<void>} stopServer
* @prop {() => boolean} verifyNoMaliciousPackages
* @prop {() => boolean} hasSuppressedVersions
* @prop {() => Number | null} getServerPort
* @prop {() => string} getCombinedCaBundlePath
*/ */
const state = {
port: null, /** @type {SafeChainProxy} */
blockedRequests: [], let server;
};
export function createSafeChainProxy() { export function createSafeChainProxy() {
const server = createProxyServer(); if (server) {
return server;
}
return { let ramaPath = getRamaPath();
startServer: () => startServer(server), if (ramaPath) {
stopServer: () => stopServer(server), ui.writeInformation("Starting safe-chain rama proxy");
verifyNoMaliciousPackages, server = createRamaProxy(ramaPath);
hasSuppressedVersions: getHasSuppressedVersions, } else {
}; server = createBuiltInProxyServer();
}
return server;
} }
/** /**
* @returns {Record<string, string>} * @returns {Record<string, string>}
*/ */
function getSafeChainProxyEnvironmentVariables() { function getSafeChainProxyEnvironmentVariables() {
if (!state.port) { if (!server || !server.getServerPort()) {
return {}; return {};
} }
const proxyUrl = `http://localhost:${state.port}`; const proxyUrl = `http://localhost:${server.getServerPort()}`;
const caCertPath = getCombinedCaBundlePath(); const caCertPath = server.getCombinedCaBundlePath();
return { return {
HTTPS_PROXY: proxyUrl, HTTPS_PROXY: proxyUrl,
@ -68,7 +78,17 @@ export function mergeSafeChainProxyEnvironmentVariables(env) {
return proxyEnv; return proxyEnv;
} }
function createProxyServer() { /** @returns {SafeChainProxy} */
function createBuiltInProxyServer() {
const SERVER_STOP_TIMEOUT_MS = 1000;
/**
* @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}}
*/
const state = {
port: null,
blockedRequests: [],
};
const server = http.createServer( const server = http.createServer(
// This handles direct HTTP requests (non-CONNECT requests) // This handles direct HTTP requests (non-CONNECT requests)
// This is normally http-only traffic, but we also handle // This is normally http-only traffic, but we also handle
@ -79,15 +99,21 @@ function createProxyServer() {
// This handles HTTPS requests via the CONNECT method // This handles HTTPS requests via the CONNECT method
server.on("connect", handleConnect); server.on("connect", handleConnect);
return server; return {
} startServer: () => startServer(server),
stopServer: () => stopServer(server),
verifyNoMaliciousPackages,
hasSuppressedVersions: getHasSuppressedVersions,
getServerPort: () => state.port,
getCombinedCaBundlePath,
};
/** /**
* @param {import("http").Server} server * @param {import("http").Server} server
* *
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
function startServer(server) { function startServer(server) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Passing port 0 makes the OS assign an available port // Passing port 0 makes the OS assign an available port
server.listen(0, () => { server.listen(0, () => {
@ -104,14 +130,14 @@ function startServer(server) {
reject(err); reject(err);
}); });
}); });
} }
/** /**
* @param {import("http").Server} server * @param {import("http").Server} server
* *
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
function stopServer(server) { function stopServer(server) {
return new Promise((resolve) => { return new Promise((resolve) => {
try { try {
server.close(() => { server.close(() => {
@ -122,16 +148,16 @@ function stopServer(server) {
} }
setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS); setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS);
}); });
} }
/** /**
* @param {import("http").IncomingMessage} req * @param {import("http").IncomingMessage} req
* @param {import("http").ServerResponse} clientSocket * @param {import("http").ServerResponse} clientSocket
* @param {Buffer} head * @param {Buffer} head
* *
* @returns {void} * @returns {void}
*/ */
function handleConnect(req, clientSocket, head) { function handleConnect(req, clientSocket, head) {
// CONNECT method is used for HTTPS requests // CONNECT method is used for HTTPS requests
// It establishes a tunnel to the server identified by the request URL // It establishes a tunnel to the server identified by the request URL
@ -154,19 +180,19 @@ function handleConnect(req, clientSocket, head) {
ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`);
tunnelRequest(req, clientSocket, head); tunnelRequest(req, clientSocket, head);
} }
} }
/** /**
* *
* @param {string} packageName * @param {string} packageName
* @param {string} version * @param {string} version
* @param {string} url * @param {string} url
*/ */
function onMalwareBlocked(packageName, version, url) { function onMalwareBlocked(packageName, version, url) {
state.blockedRequests.push({ packageName, version, url }); state.blockedRequests.push({ packageName, version, url });
} }
function verifyNoMaliciousPackages() { function verifyNoMaliciousPackages() {
if (state.blockedRequests.length === 0) { if (state.blockedRequests.length === 0) {
// No malicious packages were blocked, so nothing to block // No malicious packages were blocked, so nothing to block
return true; return true;
@ -189,4 +215,5 @@ function verifyNoMaliciousPackages() {
ui.emptyLine(); ui.emptyLine();
return false; return false;
}
} }