Merge branch 'main' into feature/pypi

This commit is contained in:
Reinier Criel 2025-11-04 06:54:00 -08:00
commit d789491561
26 changed files with 186 additions and 118 deletions

View file

@ -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. - `--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 ```shell
npm install express --safe-chain-logging=silent 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 # Usage in CI/CD

View file

@ -9,7 +9,8 @@
"scripts": { "scripts": {
"test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun", "test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun",
"test:e2e": "npm run test --workspace=test/e2e", "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": { "repository": {
"type": "git", "type": "git",

View file

@ -9,5 +9,4 @@ const packageManagerName = "bun";
initializePackageManager(packageManagerName); initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2)); var exitCode = await main(process.argv.slice(2));
// @ts-expect-error scanCommand can return an empty array in main
process.exit(exitCode); process.exit(exitCode);

View file

@ -9,5 +9,4 @@ const packageManagerName = "bunx";
initializePackageManager(packageManagerName); initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2)); var exitCode = await main(process.argv.slice(2));
// @ts-expect-error scanCommand can return an empty array in main
process.exit(exitCode); process.exit(exitCode);

View file

@ -9,5 +9,4 @@ const packageManagerName = "npm";
initializePackageManager(packageManagerName); initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2)); var exitCode = await main(process.argv.slice(2));
// @ts-expect-error scanCommand can return an empty array in main
process.exit(exitCode); process.exit(exitCode);

View file

@ -9,5 +9,4 @@ const packageManagerName = "npx";
initializePackageManager(packageManagerName); initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2)); var exitCode = await main(process.argv.slice(2));
// @ts-expect-error scanCommand can return an empty array in main
process.exit(exitCode); process.exit(exitCode);

View file

@ -9,5 +9,4 @@ const packageManagerName = "pnpm";
initializePackageManager(packageManagerName); initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2)); var exitCode = await main(process.argv.slice(2));
// @ts-expect-error scanCommand can return an empty array in main
process.exit(exitCode); process.exit(exitCode);

View file

@ -9,5 +9,4 @@ const packageManagerName = "pnpx";
initializePackageManager(packageManagerName); initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2)); var exitCode = await main(process.argv.slice(2));
// @ts-expect-error scanCommand can return an empty array in main
process.exit(exitCode); process.exit(exitCode);

View file

@ -9,5 +9,4 @@ const packageManagerName = "yarn";
initializePackageManager(packageManagerName); initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2)); var exitCode = await main(process.argv.slice(2));
// @ts-expect-error scanCommand can return an empty array in main
process.exit(exitCode); process.exit(exitCode);

View file

@ -7,6 +7,10 @@ export function getLoggingLevel() {
return LOGGING_SILENT; return LOGGING_SILENT;
} }
if (level === LOGGING_VERBOSE) {
return LOGGING_VERBOSE;
}
return LOGGING_NORMAL; return LOGGING_NORMAL;
} }
@ -34,3 +38,4 @@ export function setEcoSystem(setting) {
export const LOGGING_SILENT = "silent"; export const LOGGING_SILENT = "silent";
export const LOGGING_NORMAL = "normal"; export const LOGGING_NORMAL = "normal";
export const LOGGING_VERBOSE = "verbose";

View file

@ -2,12 +2,28 @@
import chalk from "chalk"; import chalk from "chalk";
import ora from "ora"; import ora from "ora";
import { isCi } from "./environment.js"; 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() { function isSilentMode() {
return getLoggingLevel() === LOGGING_SILENT; return getLoggingLevel() === LOGGING_SILENT;
} }
function isVerboseMode() {
return getLoggingLevel() === LOGGING_VERBOSE;
}
function emptyLine() { function emptyLine() {
if (isSilentMode()) return; if (isSilentMode()) return;
@ -22,7 +38,7 @@ function emptyLine() {
function writeInformation(message, ...optionalParams) { function writeInformation(message, ...optionalParams) {
if (isSilentMode()) return; if (isSilentMode()) return;
console.log(message, ...optionalParams); writeOrBuffer(() => console.log(message, ...optionalParams));
} }
/** /**
@ -36,7 +52,7 @@ function writeWarning(message, ...optionalParams) {
if (!isCi()) { if (!isCi()) {
message = chalk.yellow(message); message = chalk.yellow(message);
} }
console.warn(message, ...optionalParams); writeOrBuffer(() => console.warn(message, ...optionalParams));
} }
/** /**
@ -48,7 +64,7 @@ function writeError(message, ...optionalParams) {
if (!isCi()) { if (!isCi()) {
message = chalk.red(message); message = chalk.red(message);
} }
console.error(message, ...optionalParams); writeOrBuffer(() => console.error(message, ...optionalParams));
} }
function writeExitWithoutInstallingMaliciousPackages() { function writeExitWithoutInstallingMaliciousPackages() {
@ -56,7 +72,30 @@ function writeExitWithoutInstallingMaliciousPackages() {
if (!isCi()) { if (!isCi()) {
message = chalk.red(message); 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 = { export const ui = {
writeVerbose,
writeInformation, writeInformation,
writeWarning, writeWarning,
writeError, writeError,
writeExitWithoutInstallingMaliciousPackages, writeExitWithoutInstallingMaliciousPackages,
emptyLine, emptyLine,
startProcess, startProcess,
startBufferingLogs,
writeBufferedLogsAndStopBuffering,
}; };

View file

@ -9,16 +9,18 @@ import chalk from "chalk";
/** /**
* @param {string[]} args * @param {string[]} args
* @returns {Promise<number | never[]>} * @returns {Promise<number>}
*/ */
export async function main(args) { export async function main(args) {
process.on("SIGINT", handleProcessTermination);
process.on("SIGTERM", handleProcessTermination);
const proxy = createSafeChainProxy(); const proxy = createSafeChainProxy();
await proxy.startServer(); await proxy.startServer();
// Global error handlers to log unhandled errors // Global error handlers to log unhandled errors
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`); 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}`); ui.writeVerbose(`Stack trace: ${error.stack}`);
process.exit(1); process.exit(1);
}); });
@ -26,7 +28,6 @@ export async function main(args) {
process.on("unhandledRejection", (reason) => { process.on("unhandledRejection", (reason) => {
ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`); ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`);
if (reason instanceof Error) { if (reason instanceof Error) {
// @ts-expect-error writeVerbose will be added in a future PR
ui.writeVerbose(`Stack trace: ${reason.stack}`); ui.writeVerbose(`Stack trace: ${reason.stack}`);
} }
process.exit(1); 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); const packageManagerResult = await getPackageManager().runCommand(args);
// Write all buffered logs
ui.writeBufferedLogsAndStopBuffering();
if (!proxy.verifyNoMaliciousPackages()) { if (!proxy.verifyNoMaliciousPackages()) {
return 1; return 1;
} }
@ -72,3 +81,7 @@ export async function main(args) {
await proxy.stopServer(); await proxy.stopServer();
} }
} }
function handleProcessTermination() {
ui.writeBufferedLogsAndStopBuffering();
}

View file

@ -39,7 +39,6 @@ async function runBunCommand(command, args) {
try { try {
const result = await safeSpawn(command, args, { const result = await safeSpawn(command, args, {
stdio: "inherit", stdio: "inherit",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env), env: mergeSafeChainProxyEnvironmentVariables(process.env),
}); });
return { status: result.status }; return { status: result.status };

View file

@ -11,7 +11,6 @@ export async function runNpm(args) {
try { try {
const result = await safeSpawn("npm", args, { const result = await safeSpawn("npm", args, {
stdio: "inherit", stdio: "inherit",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env), env: mergeSafeChainProxyEnvironmentVariables(process.env),
}); });
return { status: result.status }; 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 };
}
}
}

View file

@ -11,7 +11,6 @@ export async function runNpx(args) {
try { try {
const result = await safeSpawn("npx", args, { const result = await safeSpawn("npx", args, {
stdio: "inherit", stdio: "inherit",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env), env: mergeSafeChainProxyEnvironmentVariables(process.env),
}); });
return { status: result.status }; return { status: result.status };

View file

@ -13,13 +13,11 @@ export async function runPnpmCommand(args, toolName = "pnpm") {
if (toolName === "pnpm") { if (toolName === "pnpm") {
result = await safeSpawn("pnpm", args, { result = await safeSpawn("pnpm", args, {
stdio: "inherit", stdio: "inherit",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env), env: mergeSafeChainProxyEnvironmentVariables(process.env),
}); });
} else if (toolName === "pnpx") { } else if (toolName === "pnpx") {
result = await safeSpawn("pnpx", args, { result = await safeSpawn("pnpx", args, {
stdio: "inherit", stdio: "inherit",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env), env: mergeSafeChainProxyEnvironmentVariables(process.env),
}); });
} else { } else {

View file

@ -9,7 +9,6 @@ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/reg
*/ */
export async function runYarnCommand(args) { export async function runYarnCommand(args) {
try { try {
// @ts-expect-error values of process.env can be string | undefined
const env = mergeSafeChainProxyEnvironmentVariables(process.env); const env = mergeSafeChainProxyEnvironmentVariables(process.env);
await fixYarnProxyEnvironmentVariables(env); await fixYarnProxyEnvironmentVariables(env);
@ -36,29 +35,7 @@ export async function runYarnCommand(args) {
async function fixYarnProxyEnvironmentVariables(env) { async function fixYarnProxyEnvironmentVariables(env) {
// Yarn ignores standard proxy environment variable HTTPS_PROXY // Yarn ignores standard proxy environment variable HTTPS_PROXY
// It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though. // 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 env.YARN_HTTPS_PROXY = env.HTTPS_PROXY;
// 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();
} }

View file

@ -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"; yarnVersion = "1.22.19";
await runYarnCommand(["add", "lodash"]); await runYarnCommand(["add", "lodash"]);
assert.strictEqual( assert.strictEqual(
capturedEnv.YARN_HTTPS_PROXY, capturedEnv.YARN_HTTPS_PROXY,
undefined, "http://localhost:8080",
"YARN_HTTPS_PROXY should not be set for Yarn v1" "YARN_HTTPS_PROXY should not be set for Yarn v1"
); );
assert.strictEqual( assert.strictEqual(

View file

@ -5,13 +5,17 @@ import { ui } from "../environment/userInteraction.js";
/** /**
* @param {import("http").IncomingMessage} req * @param {import("http").IncomingMessage} req
* @param {import("net").Socket} clientSocket * @param {import("http").ServerResponse} clientSocket
* @param {(target: string) => Promise<boolean>} isAllowed * @param {(target: string) => Promise<boolean>} isAllowed
*/ */
export function mitmConnect(req, clientSocket, 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}`); 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 // NO-OP
// This can happen if the client TCP socket sends RST instead of FIN. // 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. // 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) => { server.on("error", (err) => {
ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`);
// @ts-expect-error Property 'headersSent' does not exist on type 'Socket'
if (!clientSocket.headersSent) { if (!clientSocket.headersSent) {
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
} else if (clientSocket.writable) { } else if (clientSocket.writable) {
@ -51,11 +54,18 @@ function createHttpsServer(hostname, isAllowed) {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function handleRequest(req, res) { 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 pathAndQuery = getRequestPathAndQuery(req.url);
const targetUrl = `https://${hostname}${pathAndQuery}`; const targetUrl = `https://${hostname}${pathAndQuery}`;
if (!(await isAllowed(targetUrl))) { if (!(await isAllowed(targetUrl))) {
ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`);
res.writeHead(403, "Forbidden - blocked by safe-chain"); res.writeHead(403, "Forbidden - blocked by safe-chain");
res.end("Blocked by safe-chain"); res.end("Blocked by safe-chain");
return; return;
@ -96,7 +106,10 @@ function getRequestPathAndQuery(url) {
function forwardRequest(req, hostname, res) { function forwardRequest(req, hostname, res) {
const proxyReq = createProxyRequest(hostname, req, 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.writeHead(502);
res.end("Bad Gateway"); res.end("Bad Gateway");
}); });
@ -111,6 +124,9 @@ function forwardRequest(req, hostname, res) {
}); });
req.on("end", () => { req.on("end", () => {
ui.writeVerbose(
`Safe-chain: Finished proxying request to ${req.url} for ${hostname}`
);
proxyReq.end(); 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); res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res); proxyRes.pipe(res);
}); });

View file

@ -1,5 +1,6 @@
import * as http from "http"; import * as http from "http";
import * as https from "https"; import * as https from "https";
import { ui } from "../environment/userInteraction.js";
/** /**
* @param {import("http").IncomingMessage} req * @param {import("http").IncomingMessage} req
@ -8,7 +9,13 @@ import * as https from "https";
* @returns {void} * @returns {void}
*/ */
export function handleHttpProxyRequest(req, res) { 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); const url = new URL(req.url);
// The protocol for the plainHttpProxy should usually only be http: // The protocol for the plainHttpProxy should usually only be http:
@ -27,11 +34,16 @@ export function handleHttpProxyRequest(req, res) {
const proxyRequest = protocol const proxyRequest = protocol
.request( .request(
// @ts-expect-error req.url might be undefined
req.url, req.url,
{ method: req.method, headers: req.headers }, { method: req.method, headers: req.headers },
(proxyRes) => { (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); res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res); proxyRes.pipe(res);

View file

@ -44,7 +44,7 @@ function getSafeChainProxyEnvironmentVariables() {
} }
/** /**
* @param {Record<string, string>} env * @param {Record<string, string | undefined>} env
* *
* @returns {Record<string, string>} * @returns {Record<string, string>}
*/ */
@ -57,7 +57,7 @@ export function mergeSafeChainProxyEnvironmentVariables(env) {
// So we only copy the variable if it's not already set in a different case // So we only copy the variable if it's not already set in a different case
const upperKey = key.toUpperCase(); const upperKey = key.toUpperCase();
if (!proxyEnv[upperKey]) { if (!proxyEnv[upperKey] && env[key]) {
proxyEnv[key] = env[key]; proxyEnv[key] = env[key];
} }
} }
@ -123,7 +123,7 @@ function stopServer(server) {
/** /**
* @param {import("http").IncomingMessage} req * @param {import("http").IncomingMessage} req
* @param {import("net").Socket} clientSocket * @param {import("http").ServerResponse} clientSocket
* @param {Buffer} head * @param {Buffer} head
* *
* @returns {void} * @returns {void}
@ -146,6 +146,7 @@ function handleConnect(req, clientSocket, head) {
mitmConnect(req, clientSocket, isAllowedUrl); mitmConnect(req, clientSocket, isAllowedUrl);
} else { } else {
// For other hosts, just tunnel the request to the destination tcp socket // 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); tunnelRequest(req, clientSocket, head);
} }
} }

View file

@ -3,7 +3,7 @@ import { ui } from "../environment/userInteraction.js";
/** /**
* @param {import("http").IncomingMessage} req * @param {import("http").IncomingMessage} req
* @param {import("net").Socket} clientSocket * @param {import("http").ServerResponse} clientSocket
* @param {Buffer} head * @param {Buffer} head
* *
* @returns {void} * @returns {void}
@ -30,7 +30,7 @@ export function tunnelRequest(req, clientSocket, head) {
/** /**
* @param {import("http").IncomingMessage} req * @param {import("http").IncomingMessage} req
* @param {import("net").Socket} clientSocket * @param {import("http").ServerResponse} clientSocket
* @param {Buffer} head * @param {Buffer} head
* *
* @returns {void} * @returns {void}
@ -38,13 +38,16 @@ export function tunnelRequest(req, clientSocket, head) {
function tunnelRequestToDestination(req, clientSocket, head) { function tunnelRequestToDestination(req, clientSocket, head) {
const { port, hostname } = new URL(`http://${req.url}`); 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(
const serverSocket = net.connect(port || 443, hostname, () => { Number.parseInt(port) || 443,
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); hostname,
serverSocket.write(head); () => {
serverSocket.pipe(clientSocket); clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
clientSocket.pipe(serverSocket); serverSocket.write(head);
}); serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
}
);
clientSocket.on("error", () => { clientSocket.on("error", () => {
// This can happen if the client TCP socket sends RST instead of FIN. // 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("http").IncomingMessage} req
* @param {import("net").Socket} clientSocket * @param {import("http").ServerResponse} clientSocket
* @param {Buffer} head * @param {Buffer} head
* @param {string} proxyUrl * @param {string} proxyUrl
*/ */
@ -75,10 +78,9 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
const proxy = new URL(proxyUrl); const proxy = new URL(proxyUrl);
// Connect to proxy server // Connect to proxy server
// @ts-expect-error net.connect wants port as number but proxy.port is string
const proxySocket = net.connect({ const proxySocket = net.connect({
host: proxy.hostname, host: proxy.hostname,
port: proxy.port, port: Number.parseInt(proxy.port) || 80,
}); });
proxySocket.on("connect", () => { proxySocket.on("connect", () => {

View file

@ -1,3 +1,4 @@
import { ui } from "../../environment/userInteraction.js";
import { import {
MALWARE_STATUS_MALWARE, MALWARE_STATUS_MALWARE,
openMalwareDatabase, openMalwareDatabase,
@ -40,8 +41,14 @@ export async function auditChanges(changes) {
); );
if (malwarePackage) { if (malwarePackage) {
ui.writeVerbose(
`Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}`
);
disallowedChanges.push({ ...change, reason: malwarePackage.status }); disallowedChanges.push({ ...change, reason: malwarePackage.status });
} else { } else {
ui.writeVerbose(
`Safe-chain: Package ${change.name}@${change.version} is clean`
);
allowedChanges.push(change); allowedChanges.push(change);
} }
} }

View file

@ -21,11 +21,11 @@ export function shouldScanCommand(args) {
/** /**
* @param {string[]} args * @param {string[]} args
* *
* @returns {Promise<number | never[]>} * @returns {Promise<number>}
*/ */
export async function scanCommand(args) { export async function scanCommand(args) {
if (!shouldScanCommand(args)) { if (!shouldScanCommand(args)) {
return []; return 0;
} }
let timedOut = false; let timedOut = false;

View file

@ -91,10 +91,19 @@ async function getMalwareDatabase() {
} }
const { malwareDatabase, version } = await fetchMalwareDatabase(); 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) { } catch (/** @type any */ error) {
if (cachedDatabase) { if (cachedDatabase) {
ui.writeWarning( ui.writeWarning(

View file

@ -67,8 +67,6 @@ function resolveCommandPath(command) {
// Use 'command -v' to find the full path // Use 'command -v' to find the full path
const fullPath = execSync(`command -v ${command}`, { const fullPath = execSync(`command -v ${command}`, {
encoding: "utf8", encoding: "utf8",
// @ts-expect-error shell is a string option
shell: true,
}).trim(); }).trim();
if (!fullPath) { if (!fullPath) {
@ -120,8 +118,12 @@ export async function safeSpawn(command, args, options = {}) {
}); });
child.on("close", (code) => { 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({ resolve({
// @ts-expect-error code can be null
status: code, status: code,
stdout: stdout, stdout: stdout,
stderr: stderr, stderr: stderr,