mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge branch 'main' into feature/pypi
This commit is contained in:
commit
d789491561
26 changed files with 186 additions and 118 deletions
16
README.md
16
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.
|
- `--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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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", () => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue