@aikidosec/safe-chain 1.0.24 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/aikido-bun.js +10 -0
- package/bin/aikido-bunx.js +10 -0
- package/bin/aikido-npm.js +3 -1
- package/bin/aikido-npx.js +3 -1
- package/bin/aikido-pnpm.js +3 -1
- package/bin/aikido-pnpx.js +3 -1
- package/bin/aikido-yarn.js +3 -1
- package/package.json +5 -1
- package/src/main.js +35 -5
- package/src/packagemanager/bun/createBunPackageManager.js +42 -0
- package/src/packagemanager/currentPackageManager.js +8 -0
- package/src/packagemanager/npm/dependencyScanner/dryRunScanner.js +3 -2
- package/src/packagemanager/npm/runNpmCommand.js +26 -10
- package/src/packagemanager/npx/runNpxCommand.js +8 -5
- package/src/packagemanager/pnpm/runPnpmCommand.js +11 -4
- package/src/packagemanager/yarn/runYarnCommand.js +41 -5
- package/src/registryProxy/certUtils.js +114 -0
- package/src/registryProxy/mitmRequestHandler.js +90 -0
- package/src/registryProxy/parsePackageFromUrl.js +48 -0
- package/src/registryProxy/registryProxy.js +158 -0
- package/src/registryProxy/tunnelRequestHandler.js +98 -0
- package/src/scanning/index.js +5 -4
- package/src/scanning/malwareDatabase.js +10 -1
- package/src/shell-integration/helpers.js +7 -6
- package/src/shell-integration/startup-scripts/init-fish.fish +8 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +8 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +8 -0
- package/src/utils/safeSpawn.js +14 -2
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { main } from "../src/main.js";
|
|
4
|
+
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
|
+
|
|
6
|
+
const packageManagerName = "bun";
|
|
7
|
+
initializePackageManager(packageManagerName);
|
|
8
|
+
var exitCode = await main(process.argv.slice(2));
|
|
9
|
+
|
|
10
|
+
process.exit(exitCode);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { main } from "../src/main.js";
|
|
4
|
+
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
|
+
|
|
6
|
+
const packageManagerName = "bunx";
|
|
7
|
+
initializePackageManager(packageManagerName);
|
|
8
|
+
var exitCode = await main(process.argv.slice(2));
|
|
9
|
+
|
|
10
|
+
process.exit(exitCode);
|
package/bin/aikido-npm.js
CHANGED
|
@@ -6,7 +6,9 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa
|
|
|
6
6
|
|
|
7
7
|
const packageManagerName = "npm";
|
|
8
8
|
initializePackageManager(packageManagerName, getNpmVersion());
|
|
9
|
-
await main(process.argv.slice(2));
|
|
9
|
+
var exitCode = await main(process.argv.slice(2));
|
|
10
|
+
|
|
11
|
+
process.exit(exitCode);
|
|
10
12
|
|
|
11
13
|
function getNpmVersion() {
|
|
12
14
|
try {
|
package/bin/aikido-npx.js
CHANGED
|
@@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa
|
|
|
5
5
|
|
|
6
6
|
const packageManagerName = "npx";
|
|
7
7
|
initializePackageManager(packageManagerName, process.versions.node);
|
|
8
|
-
await main(process.argv.slice(2));
|
|
8
|
+
var exitCode = await main(process.argv.slice(2));
|
|
9
|
+
|
|
10
|
+
process.exit(exitCode);
|
package/bin/aikido-pnpm.js
CHANGED
|
@@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa
|
|
|
5
5
|
|
|
6
6
|
const packageManagerName = "pnpm";
|
|
7
7
|
initializePackageManager(packageManagerName, process.versions.node);
|
|
8
|
-
await main(process.argv.slice(2));
|
|
8
|
+
var exitCode = await main(process.argv.slice(2));
|
|
9
|
+
|
|
10
|
+
process.exit(exitCode);
|
package/bin/aikido-pnpx.js
CHANGED
|
@@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa
|
|
|
5
5
|
|
|
6
6
|
const packageManagerName = "pnpx";
|
|
7
7
|
initializePackageManager(packageManagerName, process.versions.node);
|
|
8
|
-
await main(process.argv.slice(2));
|
|
8
|
+
var exitCode = await main(process.argv.slice(2));
|
|
9
|
+
|
|
10
|
+
process.exit(exitCode);
|
package/bin/aikido-yarn.js
CHANGED
|
@@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa
|
|
|
5
5
|
|
|
6
6
|
const packageManagerName = "yarn";
|
|
7
7
|
initializePackageManager(packageManagerName, process.versions.node);
|
|
8
|
-
await main(process.argv.slice(2));
|
|
8
|
+
var exitCode = await main(process.argv.slice(2));
|
|
9
|
+
|
|
10
|
+
process.exit(exitCode);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
|
|
6
6
|
"test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"aikido-yarn": "bin/aikido-yarn.js",
|
|
13
13
|
"aikido-pnpm": "bin/aikido-pnpm.js",
|
|
14
14
|
"aikido-pnpx": "bin/aikido-pnpx.js",
|
|
15
|
+
"aikido-bun": "bin/aikido-bun.js",
|
|
16
|
+
"aikido-bunx": "bin/aikido-bunx.js",
|
|
15
17
|
"safe-chain": "bin/safe-chain.js"
|
|
16
18
|
},
|
|
17
19
|
"type": "module",
|
|
@@ -30,7 +32,9 @@
|
|
|
30
32
|
"dependencies": {
|
|
31
33
|
"abbrev": "3.0.1",
|
|
32
34
|
"chalk": "5.4.1",
|
|
35
|
+
"https-proxy-agent": "7.0.6",
|
|
33
36
|
"make-fetch-happen": "14.0.3",
|
|
37
|
+
"node-forge": "1.3.1",
|
|
34
38
|
"npm-registry-fetch": "18.0.2",
|
|
35
39
|
"ora": "8.2.0",
|
|
36
40
|
"semver": "7.7.2"
|
package/src/main.js
CHANGED
|
@@ -4,20 +4,50 @@ import { scanCommand, shouldScanCommand } from "./scanning/index.js";
|
|
|
4
4
|
import { ui } from "./environment/userInteraction.js";
|
|
5
5
|
import { getPackageManager } from "./packagemanager/currentPackageManager.js";
|
|
6
6
|
import { initializeCliArguments } from "./config/cliArguments.js";
|
|
7
|
+
import { createSafeChainProxy } from "./registryProxy/registryProxy.js";
|
|
8
|
+
import chalk from "chalk";
|
|
7
9
|
|
|
8
10
|
export async function main(args) {
|
|
11
|
+
const proxy = createSafeChainProxy();
|
|
12
|
+
await proxy.startServer();
|
|
13
|
+
|
|
9
14
|
try {
|
|
10
15
|
// This parses all the --safe-chain arguments and removes them from the args array
|
|
11
16
|
args = initializeCliArguments(args);
|
|
12
17
|
|
|
13
18
|
if (shouldScanCommand(args)) {
|
|
14
|
-
await scanCommand(args);
|
|
19
|
+
const commandScanResult = await scanCommand(args);
|
|
20
|
+
|
|
21
|
+
// Returning the exit code back to the caller allows the promise
|
|
22
|
+
// to be awaited in the bin files and return the correct exit code
|
|
23
|
+
if (commandScanResult !== 0) {
|
|
24
|
+
return commandScanResult;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const packageManagerResult = await getPackageManager().runCommand(args);
|
|
29
|
+
|
|
30
|
+
if (!proxy.verifyNoMaliciousPackages()) {
|
|
31
|
+
return 1;
|
|
15
32
|
}
|
|
33
|
+
|
|
34
|
+
ui.emptyLine();
|
|
35
|
+
ui.writeInformation(
|
|
36
|
+
`${chalk.green(
|
|
37
|
+
"✔"
|
|
38
|
+
)} Safe-chain: Command completed, no malicious packages found.`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Returning the exit code back to the caller allows the promise
|
|
42
|
+
// to be awaited in the bin files and return the correct exit code
|
|
43
|
+
return packageManagerResult.status;
|
|
16
44
|
} catch (error) {
|
|
17
45
|
ui.writeError("Failed to check for malicious packages:", error.message);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
46
|
|
|
21
|
-
|
|
22
|
-
|
|
47
|
+
// Returning the exit code back to the caller allows the promise
|
|
48
|
+
// to be awaited in the bin files and return the correct exit code
|
|
49
|
+
return 1;
|
|
50
|
+
} finally {
|
|
51
|
+
await proxy.stopServer();
|
|
52
|
+
}
|
|
23
53
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
4
|
+
|
|
5
|
+
export function createBunPackageManager() {
|
|
6
|
+
return {
|
|
7
|
+
runCommand: (args) => runBunCommand("bun", args),
|
|
8
|
+
|
|
9
|
+
// For bun, we use the proxy-only approach to block package downloads,
|
|
10
|
+
// so we don't need to analyze commands.
|
|
11
|
+
isSupportedCommand: () => false,
|
|
12
|
+
getDependencyUpdatesForCommand: () => [],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createBunxPackageManager() {
|
|
17
|
+
return {
|
|
18
|
+
runCommand: (args) => runBunCommand("bunx", args),
|
|
19
|
+
|
|
20
|
+
// For bunx, we use the proxy-only approach to block package downloads,
|
|
21
|
+
// so we don't need to analyze commands.
|
|
22
|
+
isSupportedCommand: () => false,
|
|
23
|
+
getDependencyUpdatesForCommand: () => [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function runBunCommand(command, args) {
|
|
28
|
+
try {
|
|
29
|
+
const result = await safeSpawn(command, args, {
|
|
30
|
+
stdio: "inherit",
|
|
31
|
+
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
32
|
+
});
|
|
33
|
+
return { status: result.status };
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.status) {
|
|
36
|
+
return { status: error.status };
|
|
37
|
+
} else {
|
|
38
|
+
ui.writeError("Error executing command:", error.message);
|
|
39
|
+
return { status: 1 };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBunPackageManager,
|
|
3
|
+
createBunxPackageManager,
|
|
4
|
+
} from "./bun/createBunPackageManager.js";
|
|
1
5
|
import { createNpmPackageManager } from "./npm/createPackageManager.js";
|
|
2
6
|
import { createNpxPackageManager } from "./npx/createPackageManager.js";
|
|
3
7
|
import {
|
|
@@ -21,6 +25,10 @@ export function initializePackageManager(packageManagerName, version) {
|
|
|
21
25
|
state.packageManagerName = createPnpmPackageManager();
|
|
22
26
|
} else if (packageManagerName === "pnpx") {
|
|
23
27
|
state.packageManagerName = createPnpxPackageManager();
|
|
28
|
+
} else if (packageManagerName === "bun") {
|
|
29
|
+
state.packageManagerName = createBunPackageManager();
|
|
30
|
+
} else if (packageManagerName === "bunx") {
|
|
31
|
+
state.packageManagerName = createBunxPackageManager();
|
|
24
32
|
} else {
|
|
25
33
|
throw new Error("Unsupported package manager: " + packageManagerName);
|
|
26
34
|
}
|
|
@@ -8,6 +8,7 @@ export function dryRunScanner(scannerOptions) {
|
|
|
8
8
|
shouldScan: (args) => shouldScanDependencies(scannerOptions, args),
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
+
|
|
11
12
|
function scanDependencies(scannerOptions, args) {
|
|
12
13
|
let dryRunArgs = args;
|
|
13
14
|
|
|
@@ -31,8 +32,8 @@ function shouldScanDependencies(scannerOptions, args) {
|
|
|
31
32
|
return true;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function checkChangesWithDryRun(args) {
|
|
35
|
-
const dryRunOutput = dryRunNpmCommandAndOutput(args);
|
|
35
|
+
async function checkChangesWithDryRun(args) {
|
|
36
|
+
const dryRunOutput = await dryRunNpmCommandAndOutput(args);
|
|
36
37
|
|
|
37
38
|
// Dry-run can return a non-zero status code in some cases
|
|
38
39
|
// e.g., when running "npm audit fix --dry-run", it returns exit code 1
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
2
1
|
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
3
4
|
|
|
4
|
-
export function runNpm(args) {
|
|
5
|
+
export async function runNpm(args) {
|
|
5
6
|
try {
|
|
6
|
-
const
|
|
7
|
-
|
|
7
|
+
const result = await safeSpawn("npm", args, {
|
|
8
|
+
stdio: "inherit",
|
|
9
|
+
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
10
|
+
});
|
|
11
|
+
return { status: result.status };
|
|
8
12
|
} catch (error) {
|
|
9
13
|
if (error.status) {
|
|
10
14
|
return { status: error.status };
|
|
@@ -13,17 +17,29 @@ export function runNpm(args) {
|
|
|
13
17
|
return { status: 1 };
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
|
-
return { status: 0 };
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
export function dryRunNpmCommandAndOutput(args) {
|
|
22
|
+
export async function dryRunNpmCommandAndOutput(args) {
|
|
20
23
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const result = await safeSpawn(
|
|
25
|
+
"npm",
|
|
26
|
+
[...args, "--ignore-scripts", "--dry-run"],
|
|
27
|
+
{
|
|
28
|
+
stdio: "pipe",
|
|
29
|
+
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
return {
|
|
33
|
+
status: result.status,
|
|
34
|
+
output: result.status === 0 ? result.stdout : result.stderr,
|
|
35
|
+
};
|
|
24
36
|
} catch (error) {
|
|
25
37
|
if (error.status) {
|
|
26
|
-
const output =
|
|
38
|
+
const output =
|
|
39
|
+
error.stdout?.toString() ??
|
|
40
|
+
error.stderr?.toString() ??
|
|
41
|
+
error.message ??
|
|
42
|
+
"";
|
|
27
43
|
return { status: error.status, output };
|
|
28
44
|
} else {
|
|
29
45
|
ui.writeError("Error executing command:", error.message);
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
2
1
|
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
3
4
|
|
|
4
|
-
export function runNpx(args) {
|
|
5
|
+
export async function runNpx(args) {
|
|
5
6
|
try {
|
|
6
|
-
const
|
|
7
|
-
|
|
7
|
+
const result = await safeSpawn("npx", args, {
|
|
8
|
+
stdio: "inherit",
|
|
9
|
+
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
10
|
+
});
|
|
11
|
+
return { status: result.status };
|
|
8
12
|
} catch (error) {
|
|
9
13
|
if (error.status) {
|
|
10
14
|
return { status: error.status };
|
|
@@ -13,5 +17,4 @@ export function runNpx(args) {
|
|
|
13
17
|
return { status: 1 };
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
|
-
return { status: 0 };
|
|
17
20
|
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
-
import {
|
|
2
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
3
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
4
|
|
|
4
|
-
export function runPnpmCommand(args, toolName = "pnpm") {
|
|
5
|
+
export async function runPnpmCommand(args, toolName = "pnpm") {
|
|
5
6
|
try {
|
|
6
7
|
let result;
|
|
7
8
|
if (toolName === "pnpm") {
|
|
8
|
-
result =
|
|
9
|
+
result = await safeSpawn("pnpm", args, {
|
|
10
|
+
stdio: "inherit",
|
|
11
|
+
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
12
|
+
});
|
|
9
13
|
} else if (toolName === "pnpx") {
|
|
10
|
-
result =
|
|
14
|
+
result = await safeSpawn("pnpx", args, {
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
17
|
+
});
|
|
11
18
|
} else {
|
|
12
19
|
throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
|
|
13
20
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
2
1
|
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
3
4
|
|
|
4
|
-
export function runYarnCommand(args) {
|
|
5
|
+
export async function runYarnCommand(args) {
|
|
5
6
|
try {
|
|
6
|
-
const
|
|
7
|
-
|
|
7
|
+
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
|
8
|
+
await fixYarnProxyEnvironmentVariables(env);
|
|
9
|
+
|
|
10
|
+
const result = await safeSpawn("yarn", args, {
|
|
11
|
+
stdio: "inherit",
|
|
12
|
+
env,
|
|
13
|
+
});
|
|
14
|
+
return { status: result.status };
|
|
8
15
|
} catch (error) {
|
|
9
16
|
if (error.status) {
|
|
10
17
|
return { status: error.status };
|
|
@@ -13,5 +20,34 @@ export function runYarnCommand(args) {
|
|
|
13
20
|
return { status: 1 };
|
|
14
21
|
}
|
|
15
22
|
}
|
|
16
|
-
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function fixYarnProxyEnvironmentVariables(env) {
|
|
26
|
+
// Yarn ignores standard proxy environment variables HTTPS_PROXY and NODE_EXTRA_CA_CERTS
|
|
27
|
+
|
|
28
|
+
// Yarn v2/v3 and v4+ use different environment variables for proxy and CA certs
|
|
29
|
+
// When setting all variables, yarn returns an error about conflicting variables
|
|
30
|
+
// - v2/v3: "Usage Error: Unrecognized or legacy configuration settings found: httpsCaFilePath"
|
|
31
|
+
// - v4+: "Usage Error: Unrecognized or legacy configuration settings found: caFilePath"
|
|
32
|
+
|
|
33
|
+
const version = await yarnVersion();
|
|
34
|
+
const majorVersion = parseInt(version.split(".")[0]);
|
|
35
|
+
|
|
36
|
+
if (majorVersion >= 4) {
|
|
37
|
+
env.YARN_HTTPS_PROXY = env.HTTPS_PROXY;
|
|
38
|
+
env.YARN_HTTPS_CA_FILE_PATH = env.NODE_EXTRA_CA_CERTS;
|
|
39
|
+
} else if (majorVersion === 2 || majorVersion === 3) {
|
|
40
|
+
env.YARN_HTTPS_PROXY = env.HTTPS_PROXY;
|
|
41
|
+
env.YARN_CA_FILE_PATH = env.NODE_EXTRA_CA_CERTS;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function yarnVersion() {
|
|
46
|
+
const result = await safeSpawn("yarn", ["--version"], {
|
|
47
|
+
stdio: "pipe",
|
|
48
|
+
});
|
|
49
|
+
if (result.status !== 0) {
|
|
50
|
+
throw new Error("Failed to get yarn version");
|
|
51
|
+
}
|
|
52
|
+
return result.stdout.trim();
|
|
17
53
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import forge from "node-forge";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
const certFolder = path.join(os.homedir(), ".safe-chain", "certs");
|
|
7
|
+
const ca = loadCa();
|
|
8
|
+
|
|
9
|
+
const certCache = new Map();
|
|
10
|
+
|
|
11
|
+
export function getCaCertPath() {
|
|
12
|
+
return path.join(certFolder, "ca-cert.pem");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function generateCertForHost(hostname) {
|
|
16
|
+
let existingCert = certCache.get(hostname);
|
|
17
|
+
if (existingCert) {
|
|
18
|
+
return existingCert;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
22
|
+
const cert = forge.pki.createCertificate();
|
|
23
|
+
cert.publicKey = keys.publicKey;
|
|
24
|
+
cert.serialNumber = "01";
|
|
25
|
+
cert.validity.notBefore = new Date();
|
|
26
|
+
cert.validity.notAfter = new Date();
|
|
27
|
+
cert.validity.notAfter.setHours(cert.validity.notBefore.getHours() + 1);
|
|
28
|
+
|
|
29
|
+
const attrs = [{ name: "commonName", value: hostname }];
|
|
30
|
+
cert.setSubject(attrs);
|
|
31
|
+
cert.setIssuer(ca.certificate.subject.attributes);
|
|
32
|
+
cert.setExtensions([
|
|
33
|
+
{
|
|
34
|
+
name: "subjectAltName",
|
|
35
|
+
altNames: [
|
|
36
|
+
{
|
|
37
|
+
type: 2, // DNS
|
|
38
|
+
value: hostname,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "keyUsage",
|
|
44
|
+
digitalSignature: true,
|
|
45
|
+
keyEncipherment: true,
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
cert.sign(ca.privateKey, forge.md.sha256.create());
|
|
49
|
+
|
|
50
|
+
const result = {
|
|
51
|
+
privateKey: forge.pki.privateKeyToPem(keys.privateKey),
|
|
52
|
+
certificate: forge.pki.certificateToPem(cert),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
certCache.set(hostname, result);
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadCa() {
|
|
61
|
+
const keyPath = path.join(certFolder, "ca-key.pem");
|
|
62
|
+
const certPath = path.join(certFolder, "ca-cert.pem");
|
|
63
|
+
|
|
64
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
65
|
+
const privateKeyPem = fs.readFileSync(keyPath, "utf8");
|
|
66
|
+
const certPem = fs.readFileSync(certPath, "utf8");
|
|
67
|
+
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
|
|
68
|
+
const certificate = forge.pki.certificateFromPem(certPem);
|
|
69
|
+
|
|
70
|
+
// Don't return a cert that is valid for less than 1 hour
|
|
71
|
+
const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000);
|
|
72
|
+
if (certificate.validity.notAfter > oneHourFromNow) {
|
|
73
|
+
return { privateKey, certificate };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { privateKey, certificate } = generateCa();
|
|
78
|
+
fs.mkdirSync(certFolder, { recursive: true });
|
|
79
|
+
fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(privateKey));
|
|
80
|
+
fs.writeFileSync(certPath, forge.pki.certificateToPem(certificate));
|
|
81
|
+
return { privateKey, certificate };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function generateCa() {
|
|
85
|
+
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
86
|
+
const cert = forge.pki.createCertificate();
|
|
87
|
+
cert.publicKey = keys.publicKey;
|
|
88
|
+
cert.serialNumber = "01";
|
|
89
|
+
cert.validity.notBefore = new Date();
|
|
90
|
+
cert.validity.notAfter = new Date();
|
|
91
|
+
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1);
|
|
92
|
+
|
|
93
|
+
const attrs = [{ name: "commonName", value: "safe-chain proxy" }];
|
|
94
|
+
cert.setSubject(attrs);
|
|
95
|
+
cert.setIssuer(attrs);
|
|
96
|
+
cert.setExtensions([
|
|
97
|
+
{
|
|
98
|
+
name: "basicConstraints",
|
|
99
|
+
cA: true,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "keyUsage",
|
|
103
|
+
keyCertSign: true,
|
|
104
|
+
digitalSignature: true,
|
|
105
|
+
keyEncipherment: true,
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
privateKey: keys.privateKey,
|
|
112
|
+
certificate: cert,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import https from "https";
|
|
2
|
+
import { generateCertForHost } from "./certUtils.js";
|
|
3
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
4
|
+
|
|
5
|
+
export function mitmConnect(req, clientSocket, isAllowed) {
|
|
6
|
+
const { hostname } = new URL(`http://${req.url}`);
|
|
7
|
+
|
|
8
|
+
const server = createHttpsServer(hostname, isAllowed);
|
|
9
|
+
|
|
10
|
+
// Establish the connection
|
|
11
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
12
|
+
|
|
13
|
+
// Hand off the socket to the HTTPS server
|
|
14
|
+
server.emit("connection", clientSocket);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createHttpsServer(hostname, isAllowed) {
|
|
18
|
+
const cert = generateCertForHost(hostname);
|
|
19
|
+
|
|
20
|
+
async function handleRequest(req, res) {
|
|
21
|
+
const pathAndQuery = getRequestPathAndQuery(req.url);
|
|
22
|
+
const targetUrl = `https://${hostname}${pathAndQuery}`;
|
|
23
|
+
|
|
24
|
+
if (!(await isAllowed(targetUrl))) {
|
|
25
|
+
res.writeHead(403, "Forbidden - blocked by safe-chain");
|
|
26
|
+
res.end("Blocked by safe-chain");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Collect request body
|
|
31
|
+
forwardRequest(req, hostname, res);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return https.createServer(
|
|
35
|
+
{
|
|
36
|
+
key: cert.privateKey,
|
|
37
|
+
cert: cert.certificate,
|
|
38
|
+
},
|
|
39
|
+
handleRequest
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getRequestPathAndQuery(url) {
|
|
44
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
45
|
+
const parsedUrl = new URL(url);
|
|
46
|
+
return parsedUrl.pathname + parsedUrl.search + parsedUrl.hash;
|
|
47
|
+
}
|
|
48
|
+
return url;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function forwardRequest(req, hostname, res) {
|
|
52
|
+
const proxyReq = createProxyRequest(hostname, req, res);
|
|
53
|
+
|
|
54
|
+
proxyReq.on("error", () => {
|
|
55
|
+
res.writeHead(502);
|
|
56
|
+
res.end("Bad Gateway");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
req.on("data", (chunk) => {
|
|
60
|
+
proxyReq.write(chunk);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
req.on("end", () => {
|
|
64
|
+
proxyReq.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createProxyRequest(hostname, req, res) {
|
|
69
|
+
const options = {
|
|
70
|
+
hostname: hostname,
|
|
71
|
+
port: 443,
|
|
72
|
+
path: req.url,
|
|
73
|
+
method: req.method,
|
|
74
|
+
headers: { ...req.headers },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
delete options.headers.host;
|
|
78
|
+
|
|
79
|
+
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
|
80
|
+
if (httpsProxy) {
|
|
81
|
+
options.agent = new HttpsProxyAgent(httpsProxy);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const proxyReq = https.request(options, (proxyRes) => {
|
|
85
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
86
|
+
proxyRes.pipe(res);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return proxyReq;
|
|
90
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const knownRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"];
|
|
2
|
+
|
|
3
|
+
export function parsePackageFromUrl(url) {
|
|
4
|
+
let packageName, version, registry;
|
|
5
|
+
|
|
6
|
+
for (const knownRegistry of knownRegistries) {
|
|
7
|
+
if (url.includes(knownRegistry)) {
|
|
8
|
+
registry = knownRegistry;
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!registry || !url.endsWith(".tgz")) {
|
|
14
|
+
return { packageName, version };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const registryIndex = url.indexOf(registry);
|
|
18
|
+
const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash
|
|
19
|
+
|
|
20
|
+
const separatorIndex = afterRegistry.indexOf("/-/");
|
|
21
|
+
if (separatorIndex === -1) {
|
|
22
|
+
return { packageName, version };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
packageName = afterRegistry.substring(0, separatorIndex);
|
|
26
|
+
const filename = afterRegistry.substring(
|
|
27
|
+
separatorIndex + 3,
|
|
28
|
+
afterRegistry.length - 4
|
|
29
|
+
); // Remove /-/ and .tgz
|
|
30
|
+
|
|
31
|
+
// Extract version from filename
|
|
32
|
+
// For scoped packages like @babel/core, the filename is core-7.21.4.tgz
|
|
33
|
+
// For regular packages like lodash, the filename is lodash-4.17.21.tgz
|
|
34
|
+
if (packageName.startsWith("@")) {
|
|
35
|
+
const scopedPackageName = packageName.substring(
|
|
36
|
+
packageName.lastIndexOf("/") + 1
|
|
37
|
+
);
|
|
38
|
+
if (filename.startsWith(scopedPackageName + "-")) {
|
|
39
|
+
version = filename.substring(scopedPackageName.length + 1);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
if (filename.startsWith(packageName + "-")) {
|
|
43
|
+
version = filename.substring(packageName.length + 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { packageName, version };
|
|
48
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as http from "http";
|
|
2
|
+
import { tunnelRequest } from "./tunnelRequestHandler.js";
|
|
3
|
+
import { mitmConnect } from "./mitmRequestHandler.js";
|
|
4
|
+
import { getCaCertPath } from "./certUtils.js";
|
|
5
|
+
import { auditChanges } from "../scanning/audit/index.js";
|
|
6
|
+
import { knownRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js";
|
|
7
|
+
import { ui } from "../environment/userInteraction.js";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
|
|
10
|
+
const SERVER_STOP_TIMEOUT_MS = 1000;
|
|
11
|
+
const state = {
|
|
12
|
+
port: null,
|
|
13
|
+
blockedRequests: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createSafeChainProxy() {
|
|
17
|
+
const server = createProxyServer();
|
|
18
|
+
server.on("connect", handleConnect);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
startServer: () => startServer(server),
|
|
22
|
+
stopServer: () => stopServer(server),
|
|
23
|
+
verifyNoMaliciousPackages,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getSafeChainProxyEnvironmentVariables() {
|
|
28
|
+
if (!state.port) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
HTTPS_PROXY: `http://localhost:${state.port}`,
|
|
34
|
+
GLOBAL_AGENT_HTTP_PROXY: `http://localhost:${state.port}`,
|
|
35
|
+
NODE_EXTRA_CA_CERTS: getCaCertPath(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function mergeSafeChainProxyEnvironmentVariables(env) {
|
|
40
|
+
const proxyEnv = getSafeChainProxyEnvironmentVariables();
|
|
41
|
+
|
|
42
|
+
for (const key of Object.keys(env)) {
|
|
43
|
+
// If we were to simply copy all env variables, we might overwrite
|
|
44
|
+
// the proxy settings set by safe-chain when casing varies (e.g. http_proxy vs HTTP_PROXY)
|
|
45
|
+
// So we only copy the variable if it's not already set in a different case
|
|
46
|
+
const upperKey = key.toUpperCase();
|
|
47
|
+
|
|
48
|
+
if (!proxyEnv[upperKey]) {
|
|
49
|
+
proxyEnv[key] = env[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return proxyEnv;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createProxyServer() {
|
|
57
|
+
const server = http.createServer((_, res) => {
|
|
58
|
+
res.writeHead(400, "Bad Request");
|
|
59
|
+
res.write(
|
|
60
|
+
"Safe-chain proxy: Direct http not supported. Only CONNECT requests are allowed."
|
|
61
|
+
);
|
|
62
|
+
res.end();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return server;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function startServer(server) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
// Passing port 0 makes the OS assign an available port
|
|
71
|
+
server.listen(0, () => {
|
|
72
|
+
const address = server.address();
|
|
73
|
+
if (address && typeof address === "object") {
|
|
74
|
+
state.port = address.port;
|
|
75
|
+
resolve();
|
|
76
|
+
} else {
|
|
77
|
+
reject(new Error("Failed to start proxy server"));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
server.on("error", (err) => {
|
|
82
|
+
reject(err);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stopServer(server) {
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
try {
|
|
90
|
+
server.close(() => {
|
|
91
|
+
resolve();
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
resolve();
|
|
95
|
+
}
|
|
96
|
+
setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleConnect(req, clientSocket, head) {
|
|
101
|
+
// CONNECT method is used for HTTPS requests
|
|
102
|
+
// It establishes a tunnel to the server identified by the request URL
|
|
103
|
+
|
|
104
|
+
if (knownRegistries.some((reg) => req.url.includes(reg))) {
|
|
105
|
+
// For npm and yarn registries, we want to intercept and inspect the traffic
|
|
106
|
+
// so we can block packages with malware
|
|
107
|
+
mitmConnect(req, clientSocket, isAllowedUrl);
|
|
108
|
+
} else {
|
|
109
|
+
// For other hosts, just tunnel the request to the destination tcp socket
|
|
110
|
+
tunnelRequest(req, clientSocket, head);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function isAllowedUrl(url) {
|
|
115
|
+
const { packageName, version } = parsePackageFromUrl(url);
|
|
116
|
+
|
|
117
|
+
// packageName and version are undefined when the URL is not a package download
|
|
118
|
+
// In that case, we can allow the request to proceed
|
|
119
|
+
if (!packageName || !version) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const auditResult = await auditChanges([
|
|
124
|
+
{ name: packageName, version, type: "add" },
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
if (!auditResult.isAllowed) {
|
|
128
|
+
state.blockedRequests.push({ packageName, version, url });
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function verifyNoMaliciousPackages() {
|
|
136
|
+
if (state.blockedRequests.length === 0) {
|
|
137
|
+
// No malicious packages were blocked, so nothing to block
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ui.emptyLine();
|
|
142
|
+
|
|
143
|
+
ui.writeInformation(
|
|
144
|
+
`Safe-chain: ${chalk.bold(
|
|
145
|
+
`blocked ${state.blockedRequests.length} malicious package downloads`
|
|
146
|
+
)}:`
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
for (const req of state.blockedRequests) {
|
|
150
|
+
ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ui.emptyLine();
|
|
154
|
+
ui.writeError("Exiting without installing malicious packages.");
|
|
155
|
+
ui.emptyLine();
|
|
156
|
+
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as net from "net";
|
|
2
|
+
import { ui } from "../environment/userInteraction.js";
|
|
3
|
+
|
|
4
|
+
export function tunnelRequest(req, clientSocket, head) {
|
|
5
|
+
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
|
6
|
+
|
|
7
|
+
if (httpsProxy) {
|
|
8
|
+
// If an HTTPS proxy is set, tunnel the request via the proxy
|
|
9
|
+
// This is the system proxy, not the safe-chain proxy
|
|
10
|
+
// The package manager will run via the safe-chain proxy
|
|
11
|
+
// The safe-chain proxy will then send the request to the system proxy
|
|
12
|
+
// Typical flow: package manager -> safe-chain proxy -> system proxy -> destination
|
|
13
|
+
|
|
14
|
+
// There are 2 processes involved in this:
|
|
15
|
+
// 1. Safe-chain process: has HTTPS_PROXY set to system proxy
|
|
16
|
+
// 2. Package manager process: has HTTPS_PROXY set to safe-chain proxy
|
|
17
|
+
|
|
18
|
+
tunnelRequestViaProxy(req, clientSocket, head, httpsProxy);
|
|
19
|
+
} else {
|
|
20
|
+
tunnelRequestToDestination(req, clientSocket, head);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function tunnelRequestToDestination(req, clientSocket, head) {
|
|
25
|
+
const { port, hostname } = new URL(`http://${req.url}`);
|
|
26
|
+
|
|
27
|
+
const serverSocket = net.connect(port || 443, hostname, () => {
|
|
28
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
29
|
+
serverSocket.write(head);
|
|
30
|
+
serverSocket.pipe(clientSocket);
|
|
31
|
+
clientSocket.pipe(serverSocket);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
serverSocket.on("error", (err) => {
|
|
35
|
+
ui.writeError(
|
|
36
|
+
`Safe-chain: error connecting to ${hostname}:${port} - ${err.message}`
|
|
37
|
+
);
|
|
38
|
+
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
|
|
43
|
+
const { port, hostname } = new URL(`http://${req.url}`);
|
|
44
|
+
const proxy = new URL(proxyUrl);
|
|
45
|
+
|
|
46
|
+
// Connect to proxy server
|
|
47
|
+
const proxySocket = net.connect({
|
|
48
|
+
host: proxy.hostname,
|
|
49
|
+
port: proxy.port,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
proxySocket.on("connect", () => {
|
|
53
|
+
// Send CONNECT request to proxy
|
|
54
|
+
const connectRequest = [
|
|
55
|
+
`CONNECT ${hostname}:${port || 443} HTTP/1.1`,
|
|
56
|
+
`Host: ${hostname}:${port || 443}`,
|
|
57
|
+
"",
|
|
58
|
+
"",
|
|
59
|
+
].join("\r\n");
|
|
60
|
+
|
|
61
|
+
proxySocket.write(connectRequest);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let isConnected = false;
|
|
65
|
+
proxySocket.once("data", (data) => {
|
|
66
|
+
const response = data.toString();
|
|
67
|
+
|
|
68
|
+
// Check if CONNECT succeeded (HTTP/1.1 200)
|
|
69
|
+
if (response.startsWith("HTTP/1.1 200")) {
|
|
70
|
+
isConnected = true;
|
|
71
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
72
|
+
proxySocket.write(head);
|
|
73
|
+
proxySocket.pipe(clientSocket);
|
|
74
|
+
clientSocket.pipe(proxySocket);
|
|
75
|
+
} else {
|
|
76
|
+
ui.writeError(
|
|
77
|
+
`Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}`
|
|
78
|
+
);
|
|
79
|
+
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
80
|
+
proxySocket.end();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
proxySocket.on("error", (err) => {
|
|
85
|
+
if (!isConnected) {
|
|
86
|
+
ui.writeError(
|
|
87
|
+
`Safe-chain: error connecting to proxy ${proxy.hostname}:${
|
|
88
|
+
proxy.port || 8080
|
|
89
|
+
} - ${err.message}`
|
|
90
|
+
);
|
|
91
|
+
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
clientSocket.on("error", () => {
|
|
96
|
+
proxySocket.end();
|
|
97
|
+
});
|
|
98
|
+
}
|
package/src/scanning/index.js
CHANGED
|
@@ -61,10 +61,11 @@ export async function scanCommand(args) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
if (!audit || audit.isAllowed) {
|
|
64
|
-
spinner.
|
|
64
|
+
spinner.stop();
|
|
65
|
+
return 0;
|
|
65
66
|
} else {
|
|
66
67
|
printMaliciousChanges(audit.disallowedChanges, spinner);
|
|
67
|
-
await onMalwareFound();
|
|
68
|
+
return await onMalwareFound();
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -88,11 +89,11 @@ async function onMalwareFound() {
|
|
|
88
89
|
|
|
89
90
|
if (continueInstall) {
|
|
90
91
|
ui.writeWarning("Continuing with the installation despite the risks...");
|
|
91
|
-
return;
|
|
92
|
+
return 0;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
ui.writeError("Exiting without installing malicious packages.");
|
|
96
97
|
ui.emptyLine();
|
|
97
|
-
|
|
98
|
+
return 1;
|
|
98
99
|
}
|
|
@@ -8,7 +8,13 @@ import {
|
|
|
8
8
|
} from "../config/configFile.js";
|
|
9
9
|
import { ui } from "../environment/userInteraction.js";
|
|
10
10
|
|
|
11
|
+
let cachedMalwareDatabase = null;
|
|
12
|
+
|
|
11
13
|
export async function openMalwareDatabase() {
|
|
14
|
+
if (cachedMalwareDatabase) {
|
|
15
|
+
return cachedMalwareDatabase;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
const malwareDatabase = await getMalwareDatabase();
|
|
13
19
|
|
|
14
20
|
function getPackageStatus(name, version) {
|
|
@@ -25,13 +31,16 @@ export async function openMalwareDatabase() {
|
|
|
25
31
|
return packageData.reason;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
// This implicitely caches the malware database
|
|
35
|
+
// that's closed over by the getPackageStatus function
|
|
36
|
+
cachedMalwareDatabase = {
|
|
29
37
|
getPackageStatus,
|
|
30
38
|
isMalware: (name, version) => {
|
|
31
39
|
const status = getPackageStatus(name, version);
|
|
32
40
|
return isMalwareStatus(status);
|
|
33
41
|
},
|
|
34
42
|
};
|
|
43
|
+
return cachedMalwareDatabase;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
async function getMalwareDatabase() {
|
|
@@ -9,8 +9,9 @@ export const knownAikidoTools = [
|
|
|
9
9
|
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
|
10
10
|
{ tool: "pnpm", aikidoCommand: "aikido-pnpm" },
|
|
11
11
|
{ tool: "pnpx", aikidoCommand: "aikido-pnpx" },
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
{ tool: "bun", aikidoCommand: "aikido-bun" },
|
|
13
|
+
{ tool: "bunx", aikidoCommand: "aikido-bunx" },
|
|
14
|
+
// When adding a new tool here, also update the documentation for the new tool in the README.md
|
|
14
15
|
];
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -18,15 +19,15 @@ export const knownAikidoTools = [
|
|
|
18
19
|
* Example: "npm, npx, yarn, pnpm, and pnpx commands"
|
|
19
20
|
*/
|
|
20
21
|
export function getPackageManagerList() {
|
|
21
|
-
const tools = knownAikidoTools.map(t => t.tool);
|
|
22
|
+
const tools = knownAikidoTools.map((t) => t.tool);
|
|
22
23
|
if (tools.length <= 1) {
|
|
23
|
-
return `${tools[0] ||
|
|
24
|
+
return `${tools[0] || ""} commands`;
|
|
24
25
|
}
|
|
25
26
|
if (tools.length === 2) {
|
|
26
27
|
return `${tools[0]} and ${tools[1]} commands`;
|
|
27
28
|
}
|
|
28
29
|
const lastTool = tools.pop();
|
|
29
|
-
return `${tools.join(
|
|
30
|
+
return `${tools.join(", ")}, and ${lastTool} commands`;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function doesExecutableExistOnSystem(executableName) {
|
|
@@ -47,7 +48,7 @@ export function removeLinesMatchingPattern(filePath, pattern, eol) {
|
|
|
47
48
|
eol = eol || os.EOL;
|
|
48
49
|
|
|
49
50
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
50
|
-
const lines = fileContent.split(
|
|
51
|
+
const lines = fileContent.split(/\r?\n|\r|\u2028|\u2029/);
|
|
51
52
|
const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern));
|
|
52
53
|
fs.writeFileSync(filePath, updatedLines.join(eol), "utf-8");
|
|
53
54
|
}
|
|
@@ -46,6 +46,14 @@ function pnpx
|
|
|
46
46
|
wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
function bun
|
|
50
|
+
wrapSafeChainCommand "bun" "aikido-bun" $argv
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
function bunx
|
|
54
|
+
wrapSafeChainCommand "bunx" "aikido-bunx" $argv
|
|
55
|
+
end
|
|
56
|
+
|
|
49
57
|
function npm
|
|
50
58
|
# If args is just -v or --version and nothing else, just run the `npm -v` command
|
|
51
59
|
# This is because nvm uses this to check the version of npm
|
|
@@ -42,6 +42,14 @@ function pnpx() {
|
|
|
42
42
|
wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@"
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function bun() {
|
|
46
|
+
wrapSafeChainCommand "bun" "aikido-bun" "$@"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bunx() {
|
|
50
|
+
wrapSafeChainCommand "bunx" "aikido-bunx" "$@"
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
function npm() {
|
|
46
54
|
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
|
|
47
55
|
# If args is just -v or --version and nothing else, just run the npm version command
|
|
@@ -68,6 +68,14 @@ function pnpx {
|
|
|
68
68
|
Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function bun {
|
|
72
|
+
Invoke-WrappedCommand "bun" "aikido-bun" $args
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function bunx {
|
|
76
|
+
Invoke-WrappedCommand "bunx" "aikido-bunx" $args
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
function npm {
|
|
72
80
|
# If args is just -v or --version and nothing else, just run the npm version command
|
|
73
81
|
# This is because nvm uses this to check the version of npm
|
package/src/utils/safeSpawn.js
CHANGED
|
@@ -23,11 +23,23 @@ export async function safeSpawn(command, args, options = {}) {
|
|
|
23
23
|
return new Promise((resolve, reject) => {
|
|
24
24
|
const child = spawn(fullCommand, { ...options, shell: true });
|
|
25
25
|
|
|
26
|
+
// When stdio is piped, we need to collect the output
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
|
|
30
|
+
child.stdout?.on("data", (data) => {
|
|
31
|
+
stdout += data.toString();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.stderr?.on("data", (data) => {
|
|
35
|
+
stderr += data.toString();
|
|
36
|
+
});
|
|
37
|
+
|
|
26
38
|
child.on("close", (code) => {
|
|
27
39
|
resolve({
|
|
28
40
|
status: code,
|
|
29
|
-
stdout:
|
|
30
|
-
stderr:
|
|
41
|
+
stdout: stdout,
|
|
42
|
+
stderr: stderr,
|
|
31
43
|
});
|
|
32
44
|
});
|
|
33
45
|
|