diff --git a/README.md b/README.md index 1faa1f6..acea710 100644 --- a/README.md +++ b/README.md @@ -92,11 +92,19 @@ You can control the output from Aikido Safe Chain using the `--safe-chain-loggin - `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit. -Example usage: + Example usage: -```shell -npm install express --safe-chain-logging=silent -``` + ```shell + npm install express --safe-chain-logging=silent + ``` + +- `--safe-chain-logging=verbose` - Enables detailed diagnostic output from Aikido Safe Chain. Useful for troubleshooting issues or understanding what Safe Chain is doing behind the scenes. + + Example usage: + + ```shell + npm install express --safe-chain-logging=verbose + ``` # Usage in CI/CD diff --git a/package.json b/package.json index 0193a82..6a5dec3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "scripts": { "test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun", "test:e2e": "npm run test --workspace=test/e2e", - "lint": "npm run lint --workspace=packages/safe-chain" + "lint": "npm run lint --workspace=packages/safe-chain", + "typecheck": "npm run typecheck --workspace=packages/safe-chain" }, "repository": { "type": "git", diff --git a/packages/safe-chain/bin/aikido-bun.js b/packages/safe-chain/bin/aikido-bun.js index d6751fc..c128445 100755 --- a/packages/safe-chain/bin/aikido-bun.js +++ b/packages/safe-chain/bin/aikido-bun.js @@ -9,5 +9,4 @@ const packageManagerName = "bun"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-bunx.js b/packages/safe-chain/bin/aikido-bunx.js index 5ec9646..2e83793 100755 --- a/packages/safe-chain/bin/aikido-bunx.js +++ b/packages/safe-chain/bin/aikido-bunx.js @@ -9,5 +9,4 @@ const packageManagerName = "bunx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-npm.js b/packages/safe-chain/bin/aikido-npm.js index 3e3ad0c..a50d9b5 100755 --- a/packages/safe-chain/bin/aikido-npm.js +++ b/packages/safe-chain/bin/aikido-npm.js @@ -9,5 +9,4 @@ const packageManagerName = "npm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-npx.js b/packages/safe-chain/bin/aikido-npx.js index 20b970a..e1687d3 100755 --- a/packages/safe-chain/bin/aikido-npx.js +++ b/packages/safe-chain/bin/aikido-npx.js @@ -9,5 +9,4 @@ const packageManagerName = "npx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpm.js b/packages/safe-chain/bin/aikido-pnpm.js index 24ed8a9..cf5125e 100755 --- a/packages/safe-chain/bin/aikido-pnpm.js +++ b/packages/safe-chain/bin/aikido-pnpm.js @@ -9,5 +9,4 @@ const packageManagerName = "pnpm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpx.js b/packages/safe-chain/bin/aikido-pnpx.js index 70a5bba..6182810 100755 --- a/packages/safe-chain/bin/aikido-pnpx.js +++ b/packages/safe-chain/bin/aikido-pnpx.js @@ -9,5 +9,4 @@ const packageManagerName = "pnpx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-yarn.js b/packages/safe-chain/bin/aikido-yarn.js index 834b503..eee14e8 100755 --- a/packages/safe-chain/bin/aikido-yarn.js +++ b/packages/safe-chain/bin/aikido-yarn.js @@ -9,5 +9,4 @@ const packageManagerName = "yarn"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index d7ccf87..46cc30c 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -7,6 +7,10 @@ export function getLoggingLevel() { return LOGGING_SILENT; } + if (level === LOGGING_VERBOSE) { + return LOGGING_VERBOSE; + } + return LOGGING_NORMAL; } @@ -34,3 +38,4 @@ export function setEcoSystem(setting) { export const LOGGING_SILENT = "silent"; export const LOGGING_NORMAL = "normal"; +export const LOGGING_VERBOSE = "verbose"; diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 3f690fb..3222874 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -2,12 +2,28 @@ import chalk from "chalk"; import ora from "ora"; import { isCi } from "./environment.js"; -import { getLoggingLevel, LOGGING_SILENT } from "../config/settings.js"; +import { + getLoggingLevel, + LOGGING_SILENT, + LOGGING_VERBOSE, +} from "../config/settings.js"; + +/** + * @type {{ bufferOutput: boolean, bufferedMessages:(() => void)[]}} + */ +const state = { + bufferOutput: false, + bufferedMessages: [], +}; function isSilentMode() { return getLoggingLevel() === LOGGING_SILENT; } +function isVerboseMode() { + return getLoggingLevel() === LOGGING_VERBOSE; +} + function emptyLine() { if (isSilentMode()) return; @@ -22,7 +38,7 @@ function emptyLine() { function writeInformation(message, ...optionalParams) { if (isSilentMode()) return; - console.log(message, ...optionalParams); + writeOrBuffer(() => console.log(message, ...optionalParams)); } /** @@ -36,7 +52,7 @@ function writeWarning(message, ...optionalParams) { if (!isCi()) { message = chalk.yellow(message); } - console.warn(message, ...optionalParams); + writeOrBuffer(() => console.warn(message, ...optionalParams)); } /** @@ -48,7 +64,7 @@ function writeError(message, ...optionalParams) { if (!isCi()) { message = chalk.red(message); } - console.error(message, ...optionalParams); + writeOrBuffer(() => console.error(message, ...optionalParams)); } function writeExitWithoutInstallingMaliciousPackages() { @@ -56,7 +72,30 @@ function writeExitWithoutInstallingMaliciousPackages() { if (!isCi()) { message = chalk.red(message); } - console.error(message); + writeOrBuffer(() => console.error(message)); +} + +/** + * @param {string} message + * @param {...any} optionalParams + * @returns {void} + */ +function writeVerbose(message, ...optionalParams) { + if (!isVerboseMode()) return; + + writeOrBuffer(() => console.log(message, ...optionalParams)); +} + +/** + * + * @param {() => void} messageFunction + */ +function writeOrBuffer(messageFunction) { + if (state.bufferOutput) { + state.bufferedMessages.push(messageFunction); + } else { + messageFunction(); + } } /** @@ -114,11 +153,27 @@ function startProcess(message) { } } +function startBufferingLogs() { + state.bufferOutput = true; + state.bufferedMessages = []; +} + +function writeBufferedLogsAndStopBuffering() { + state.bufferOutput = false; + for (const log of state.bufferedMessages) { + log(); + } + state.bufferedMessages = []; +} + export const ui = { + writeVerbose, writeInformation, writeWarning, writeError, writeExitWithoutInstallingMaliciousPackages, emptyLine, startProcess, + startBufferingLogs, + writeBufferedLogsAndStopBuffering, }; diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index fca1218..3fba24f 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -9,16 +9,18 @@ import chalk from "chalk"; /** * @param {string[]} args - * @returns {Promise} + * @returns {Promise} */ export async function main(args) { + process.on("SIGINT", handleProcessTermination); + process.on("SIGTERM", handleProcessTermination); + const proxy = createSafeChainProxy(); await proxy.startServer(); // Global error handlers to log unhandled errors process.on("uncaughtException", (error) => { ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`); - // @ts-expect-error writeVerbose will be added in a future PR ui.writeVerbose(`Stack trace: ${error.stack}`); process.exit(1); }); @@ -26,7 +28,6 @@ export async function main(args) { process.on("unhandledRejection", (reason) => { ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`); if (reason instanceof Error) { - // @ts-expect-error writeVerbose will be added in a future PR ui.writeVerbose(`Stack trace: ${reason.stack}`); } process.exit(1); @@ -46,8 +47,16 @@ export async function main(args) { } } + // Buffer logs during package manager execution, this avoids interleaving + // of logs from the package manager and safe-chain + // Not doing this could cause bugs to disappear when cursor movement codes + // are written by the package manager while safe-chain is writing logs + ui.startBufferingLogs(); const packageManagerResult = await getPackageManager().runCommand(args); + // Write all buffered logs + ui.writeBufferedLogsAndStopBuffering(); + if (!proxy.verifyNoMaliciousPackages()) { return 1; } @@ -72,3 +81,7 @@ export async function main(args) { await proxy.stopServer(); } } + +function handleProcessTermination() { + ui.writeBufferedLogsAndStopBuffering(); +} diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js index 9716261..037a512 100644 --- a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -39,7 +39,6 @@ async function runBunCommand(command, args) { try { const result = await safeSpawn(command, args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; diff --git a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js index c068240..af57fad 100644 --- a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js +++ b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js @@ -11,7 +11,6 @@ export async function runNpm(args) { try { const result = await safeSpawn("npm", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; @@ -24,37 +23,3 @@ export async function runNpm(args) { } } } - -/** - * @param {string[]} args - * @returns {Promise<{status: number, output?: string}>} - */ -export async function dryRunNpmCommandAndOutput(args) { - try { - const result = await safeSpawn( - "npm", - [...args, "--ignore-scripts", "--dry-run"], - { - stdio: "pipe", - // @ts-expect-error values of process.env can be string | undefined - env: mergeSafeChainProxyEnvironmentVariables(process.env), - } - ); - return { - status: result.status, - output: result.status === 0 ? result.stdout : result.stderr, - }; - } catch (/** @type any */ error) { - if (error.status) { - const output = - error.stdout?.toString() ?? - error.stderr?.toString() ?? - error.message ?? - ""; - return { status: error.status, output }; - } else { - ui.writeError("Error executing command:", error.message); - return { status: 1 }; - } - } -} diff --git a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js index 61adcaa..2501b79 100644 --- a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js +++ b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js @@ -11,7 +11,6 @@ export async function runNpx(args) { try { const result = await safeSpawn("npx", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; diff --git a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js index db4ffa9..d958fb8 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js +++ b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js @@ -13,13 +13,11 @@ export async function runPnpmCommand(args, toolName = "pnpm") { if (toolName === "pnpm") { result = await safeSpawn("pnpm", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); } else if (toolName === "pnpx") { result = await safeSpawn("pnpx", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); } else { diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index 1ba0f5c..2089551 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -9,7 +9,6 @@ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/reg */ export async function runYarnCommand(args) { try { - // @ts-expect-error values of process.env can be string | undefined const env = mergeSafeChainProxyEnvironmentVariables(process.env); await fixYarnProxyEnvironmentVariables(env); @@ -36,29 +35,7 @@ export async function runYarnCommand(args) { async function fixYarnProxyEnvironmentVariables(env) { // Yarn ignores standard proxy environment variable HTTPS_PROXY // It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though. - // Don't use YARN_HTTPS_CA_FILE_PATH though, as it causes to ignore all system CAs + // Don't use YARN_HTTPS_CA_FILE_PATH or YARN_CA_FILE_PATH though, it causes yarn to ignore all system CAs - // Yarn v2/v3 and v4+ use different environment variables for proxy and CA certs - // When setting all variables, yarn returns an error about conflicting variables - // - v2/v3: "Usage Error: Unrecognized or legacy configuration settings found: httpsCaFilePath" - // - v4+: "Usage Error: Unrecognized or legacy configuration settings found: caFilePath" - - const version = await yarnVersion(); - const majorVersion = parseInt(version.split(".")[0]); - - if (majorVersion >= 4) { - env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; - } else if (majorVersion === 2 || majorVersion === 3) { - env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; - } -} - -async function yarnVersion() { - const result = await safeSpawn("yarn", ["--version"], { - stdio: "pipe", - }); - if (result.status !== 0) { - throw new Error("Failed to get yarn version"); - } - return result.stdout.trim(); + env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; } diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js index bd3d04d..21475f9 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js @@ -103,13 +103,13 @@ describe("runYarnCommand", () => { ); }); - it("should not set Yarn-specific proxy vars for Yarn v1", async () => { + it("should set YARN_HTTPS_PROXY for Yarn v1", async () => { yarnVersion = "1.22.19"; await runYarnCommand(["add", "lodash"]); assert.strictEqual( capturedEnv.YARN_HTTPS_PROXY, - undefined, + "http://localhost:8080", "YARN_HTTPS_PROXY should not be set for Yarn v1" ); assert.strictEqual( diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 5bbf38a..6f7b20e 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -5,13 +5,17 @@ import { ui } from "../environment/userInteraction.js"; /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {(target: string) => Promise} isAllowed */ export function mitmConnect(req, clientSocket, isAllowed) { + ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`); const { hostname } = new URL(`http://${req.url}`); - clientSocket.on("error", () => { + clientSocket.on("error", (err) => { + ui.writeVerbose( + `Safe-chain: Client socket error for ${req.url}: ${err.message}` + ); // NO-OP // This can happen if the client TCP socket sends RST instead of FIN. // Not subscribing to 'close' event will cause node to throw and crash. @@ -21,7 +25,6 @@ export function mitmConnect(req, clientSocket, isAllowed) { server.on("error", (err) => { ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); - // @ts-expect-error Property 'headersSent' does not exist on type 'Socket' if (!clientSocket.headersSent) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); } else if (clientSocket.writable) { @@ -51,11 +54,18 @@ function createHttpsServer(hostname, isAllowed) { * @returns {Promise} */ async function handleRequest(req, res) { - // @ts-expect-error req.url might be undefined + if (!req.url) { + ui.writeError("Safe-chain: Request missing URL"); + res.writeHead(400, "Bad Request"); + res.end("Bad Request: Missing URL"); + return; + } + const pathAndQuery = getRequestPathAndQuery(req.url); const targetUrl = `https://${hostname}${pathAndQuery}`; if (!(await isAllowed(targetUrl))) { + ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); res.writeHead(403, "Forbidden - blocked by safe-chain"); res.end("Blocked by safe-chain"); return; @@ -96,7 +106,10 @@ function getRequestPathAndQuery(url) { function forwardRequest(req, hostname, res) { const proxyReq = createProxyRequest(hostname, req, res); - proxyReq.on("error", () => { + proxyReq.on("error", (err) => { + ui.writeVerbose( + `Safe-chain: Error occurred while proxying request: ${err.message}` + ); res.writeHead(502); res.end("Bad Gateway"); }); @@ -111,6 +124,9 @@ function forwardRequest(req, hostname, res) { }); req.on("end", () => { + ui.writeVerbose( + `Safe-chain: Finished proxying request to ${req.url} for ${hostname}` + ); proxyReq.end(); }); } @@ -152,7 +168,13 @@ function createProxyRequest(hostname, req, res) { } }); - // @ts-expect-error statusCode might be undefined + if (!proxyRes.statusCode) { + ui.writeError("Safe-chain: Proxy response missing status code"); + res.writeHead(500); + res.end("Internal Server Error"); + return; + } + res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 16b305a..75b9d77 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -1,5 +1,6 @@ import * as http from "http"; import * as https from "https"; +import { ui } from "../environment/userInteraction.js"; /** * @param {import("http").IncomingMessage} req @@ -8,7 +9,13 @@ import * as https from "https"; * @returns {void} */ export function handleHttpProxyRequest(req, res) { - // @ts-expect-error req.url might be undefined + if (!req.url) { + ui.writeError("Safe-chain: Request missing URL"); + res.writeHead(400, "Bad Request"); + res.end("Bad Request: Missing URL"); + return; + } + const url = new URL(req.url); // The protocol for the plainHttpProxy should usually only be http: @@ -27,11 +34,16 @@ export function handleHttpProxyRequest(req, res) { const proxyRequest = protocol .request( - // @ts-expect-error req.url might be undefined req.url, { method: req.method, headers: req.headers }, (proxyRes) => { - // @ts-expect-error statusCode might be undefined + if (!proxyRes.statusCode) { + ui.writeError("Safe-chain: Proxy response missing status code"); + res.writeHead(500); + res.end("Internal Server Error"); + return; + } + res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 99faa87..c5e272b 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -44,7 +44,7 @@ function getSafeChainProxyEnvironmentVariables() { } /** - * @param {Record} env + * @param {Record} env * * @returns {Record} */ @@ -57,7 +57,7 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { // So we only copy the variable if it's not already set in a different case const upperKey = key.toUpperCase(); - if (!proxyEnv[upperKey]) { + if (!proxyEnv[upperKey] && env[key]) { proxyEnv[key] = env[key]; } } @@ -123,7 +123,7 @@ function stopServer(server) { /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * * @returns {void} @@ -146,6 +146,7 @@ function handleConnect(req, clientSocket, head) { mitmConnect(req, clientSocket, isAllowedUrl); } else { // For other hosts, just tunnel the request to the destination tcp socket + ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); tunnelRequest(req, clientSocket, head); } } diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 452ef15..4b756d7 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -3,7 +3,7 @@ import { ui } from "../environment/userInteraction.js"; /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * * @returns {void} @@ -30,7 +30,7 @@ export function tunnelRequest(req, clientSocket, head) { /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * * @returns {void} @@ -38,13 +38,16 @@ export function tunnelRequest(req, clientSocket, head) { function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); - // @ts-expect-error port from URL is a string but net.connect accepts number - const serverSocket = net.connect(port || 443, hostname, () => { - clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); - serverSocket.write(head); - serverSocket.pipe(clientSocket); - clientSocket.pipe(serverSocket); - }); + const serverSocket = net.connect( + Number.parseInt(port) || 443, + hostname, + () => { + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + } + ); clientSocket.on("error", () => { // This can happen if the client TCP socket sends RST instead of FIN. @@ -66,7 +69,7 @@ function tunnelRequestToDestination(req, clientSocket, head) { /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * @param {string} proxyUrl */ @@ -75,10 +78,9 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { const proxy = new URL(proxyUrl); // Connect to proxy server - // @ts-expect-error net.connect wants port as number but proxy.port is string const proxySocket = net.connect({ host: proxy.hostname, - port: proxy.port, + port: Number.parseInt(proxy.port) || 80, }); proxySocket.on("connect", () => { diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 1514eb8..2d215cb 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -1,3 +1,4 @@ +import { ui } from "../../environment/userInteraction.js"; import { MALWARE_STATUS_MALWARE, openMalwareDatabase, @@ -40,8 +41,14 @@ export async function auditChanges(changes) { ); if (malwarePackage) { + ui.writeVerbose( + `Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}` + ); disallowedChanges.push({ ...change, reason: malwarePackage.status }); } else { + ui.writeVerbose( + `Safe-chain: Package ${change.name}@${change.version} is clean` + ); allowedChanges.push(change); } } diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index d8e817e..44ff57c 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -21,11 +21,11 @@ export function shouldScanCommand(args) { /** * @param {string[]} args * - * @returns {Promise} + * @returns {Promise} */ export async function scanCommand(args) { if (!shouldScanCommand(args)) { - return []; + return 0; } let timedOut = false; diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index b11f8d8..4aba43c 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -91,10 +91,19 @@ async function getMalwareDatabase() { } const { malwareDatabase, version } = await fetchMalwareDatabase(); - // @ts-expect-error version can be undefined - writeDatabaseToLocalCache(malwareDatabase, version); - return malwareDatabase; + if (version) { + // Only cache the malware database when we have a version. + writeDatabaseToLocalCache(malwareDatabase, version); + return malwareDatabase; + } else { + // We received a valid malware database, but the response + // did not contain an etag header with the version + ui.writeWarning( + "The malware database was downloaded, but could not be cached due to a missing version." + ); + return malwareDatabase; + } } catch (/** @type any */ error) { if (cachedDatabase) { ui.writeWarning( diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 489d070..e17bdb5 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -67,8 +67,6 @@ function resolveCommandPath(command) { // Use 'command -v' to find the full path const fullPath = execSync(`command -v ${command}`, { encoding: "utf8", - // @ts-expect-error shell is a string option - shell: true, }).trim(); if (!fullPath) { @@ -120,8 +118,12 @@ export async function safeSpawn(command, args, options = {}) { }); child.on("close", (code) => { + // Code is null if it terminated by a signal. This should never + // happen in our code. If this happens, return 1 error code. + + code = code ?? 1; + resolve({ - // @ts-expect-error code can be null status: code, stdout: stdout, stderr: stderr,