@aikidosec/safe-chain 1.1.4 → 1.1.6
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/README.md +10 -5
- package/bin/aikido-npm.js +1 -12
- package/bin/aikido-npx.js +1 -1
- package/bin/aikido-pnpm.js +1 -1
- package/bin/aikido-pnpx.js +1 -1
- package/bin/aikido-yarn.js +1 -1
- package/bin/safe-chain.js +18 -2
- package/package.json +2 -3
- package/src/api/npmApi.js +4 -0
- package/src/config/cliArguments.js +15 -15
- package/src/config/settings.js +7 -7
- package/src/environment/userInteraction.js +30 -30
- package/src/packagemanager/currentPackageManager.js +2 -2
- package/src/packagemanager/npm/createPackageManager.js +10 -45
- package/src/packagemanager/npm/utils/abbrevs-generated.js +358 -0
- package/src/packagemanager/npm/utils/cmd-list.js +1 -3
- package/src/registryProxy/plainHttpProxy.js +69 -0
- package/src/registryProxy/registryProxy.js +11 -9
- package/src/scanning/index.js +4 -19
- package/src/utils/safeSpawn.js +61 -11
- package/src/packagemanager/npm/dependencyScanner/dryRunScanner.js +0 -67
- package/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js +0 -57
package/README.md
CHANGED
|
@@ -40,6 +40,12 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
|
|
|
40
40
|
|
|
41
41
|
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, or `bunx` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command.
|
|
42
42
|
|
|
43
|
+
You can check the installed version by running:
|
|
44
|
+
|
|
45
|
+
```shell
|
|
46
|
+
safe-chain --version
|
|
47
|
+
```
|
|
48
|
+
|
|
43
49
|
## How it works
|
|
44
50
|
|
|
45
51
|
The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry. When you run npm, npx, yarn, pnpm, pnpx, bun, or bunx commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine.
|
|
@@ -70,17 +76,16 @@ To uninstall the Aikido Safe Chain, you can run the following command:
|
|
|
70
76
|
|
|
71
77
|
# Configuration
|
|
72
78
|
|
|
73
|
-
##
|
|
79
|
+
## Logging
|
|
74
80
|
|
|
75
|
-
You can control
|
|
81
|
+
You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag:
|
|
76
82
|
|
|
77
|
-
- `--safe-chain-
|
|
78
|
-
- `--safe-chain-malware-action=prompt` - Prompts the user to decide whether to continue despite the malware detection
|
|
83
|
+
- `--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.
|
|
79
84
|
|
|
80
85
|
Example usage:
|
|
81
86
|
|
|
82
87
|
```shell
|
|
83
|
-
npm install
|
|
88
|
+
npm install express --safe-chain-logging=silent
|
|
84
89
|
```
|
|
85
90
|
|
|
86
91
|
# Usage in CI/CD
|
package/bin/aikido-npm.js
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execSync } from "child_process";
|
|
4
3
|
import { main } from "../src/main.js";
|
|
5
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
6
5
|
|
|
7
6
|
const packageManagerName = "npm";
|
|
8
|
-
initializePackageManager(packageManagerName
|
|
7
|
+
initializePackageManager(packageManagerName);
|
|
9
8
|
var exitCode = await main(process.argv.slice(2));
|
|
10
9
|
|
|
11
10
|
process.exit(exitCode);
|
|
12
|
-
|
|
13
|
-
function getNpmVersion() {
|
|
14
|
-
try {
|
|
15
|
-
return execSync("npm --version").toString().trim();
|
|
16
|
-
} catch {
|
|
17
|
-
// Default to 0.0.0 if npm is not found
|
|
18
|
-
// That way we don't use any unsupported features
|
|
19
|
-
return "0.0.0";
|
|
20
|
-
}
|
|
21
|
-
}
|
package/bin/aikido-npx.js
CHANGED
|
@@ -4,7 +4,7 @@ import { main } from "../src/main.js";
|
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
5
|
|
|
6
6
|
const packageManagerName = "npx";
|
|
7
|
-
initializePackageManager(packageManagerName
|
|
7
|
+
initializePackageManager(packageManagerName);
|
|
8
8
|
var exitCode = await main(process.argv.slice(2));
|
|
9
9
|
|
|
10
10
|
process.exit(exitCode);
|
package/bin/aikido-pnpm.js
CHANGED
|
@@ -4,7 +4,7 @@ import { main } from "../src/main.js";
|
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
5
|
|
|
6
6
|
const packageManagerName = "pnpm";
|
|
7
|
-
initializePackageManager(packageManagerName
|
|
7
|
+
initializePackageManager(packageManagerName);
|
|
8
8
|
var exitCode = await main(process.argv.slice(2));
|
|
9
9
|
|
|
10
10
|
process.exit(exitCode);
|
package/bin/aikido-pnpx.js
CHANGED
|
@@ -4,7 +4,7 @@ import { main } from "../src/main.js";
|
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
5
|
|
|
6
6
|
const packageManagerName = "pnpx";
|
|
7
|
-
initializePackageManager(packageManagerName
|
|
7
|
+
initializePackageManager(packageManagerName);
|
|
8
8
|
var exitCode = await main(process.argv.slice(2));
|
|
9
9
|
|
|
10
10
|
process.exit(exitCode);
|
package/bin/aikido-yarn.js
CHANGED
|
@@ -4,7 +4,7 @@ import { main } from "../src/main.js";
|
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
5
|
|
|
6
6
|
const packageManagerName = "yarn";
|
|
7
|
-
initializePackageManager(packageManagerName
|
|
7
|
+
initializePackageManager(packageManagerName);
|
|
8
8
|
var exitCode = await main(process.argv.slice(2));
|
|
9
9
|
|
|
10
10
|
process.exit(exitCode);
|
package/bin/safe-chain.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
+
import { createRequire } from "module";
|
|
4
5
|
import { ui } from "../src/environment/userInteraction.js";
|
|
5
6
|
import { setup } from "../src/shell-integration/setup.js";
|
|
6
7
|
import { teardown } from "../src/shell-integration/teardown.js";
|
|
@@ -26,6 +27,8 @@ if (command === "setup") {
|
|
|
26
27
|
teardown();
|
|
27
28
|
} else if (command === "setup-ci") {
|
|
28
29
|
setupCi();
|
|
30
|
+
} else if (command === "--version" || command === "-v" || command === "-v") {
|
|
31
|
+
ui.writeInformation(`Current safe-chain version: ${getVersion()}`);
|
|
29
32
|
} else {
|
|
30
33
|
ui.writeError(`Unknown command: ${command}.`);
|
|
31
34
|
ui.emptyLine();
|
|
@@ -43,13 +46,15 @@ function writeHelp() {
|
|
|
43
46
|
ui.writeInformation(
|
|
44
47
|
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
|
45
48
|
"teardown"
|
|
46
|
-
)}, ${chalk.cyan("help")}
|
|
49
|
+
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan(
|
|
50
|
+
"--version"
|
|
51
|
+
)}`
|
|
47
52
|
);
|
|
48
53
|
ui.emptyLine();
|
|
49
54
|
ui.writeInformation(
|
|
50
55
|
`- ${chalk.cyan(
|
|
51
56
|
"safe-chain setup"
|
|
52
|
-
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm and
|
|
57
|
+
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun and bunx.`
|
|
53
58
|
);
|
|
54
59
|
ui.writeInformation(
|
|
55
60
|
`- ${chalk.cyan(
|
|
@@ -61,5 +66,16 @@ function writeHelp() {
|
|
|
61
66
|
"safe-chain setup-ci"
|
|
62
67
|
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`
|
|
63
68
|
);
|
|
69
|
+
ui.writeInformation(
|
|
70
|
+
`- ${chalk.cyan(
|
|
71
|
+
"safe-chain --version"
|
|
72
|
+
)} (or ${chalk.cyan("-v")}): Display the current version of safe-chain.`
|
|
73
|
+
);
|
|
64
74
|
ui.emptyLine();
|
|
65
75
|
}
|
|
76
|
+
|
|
77
|
+
function getVersion() {
|
|
78
|
+
const require = createRequire(import.meta.url);
|
|
79
|
+
const packageJson = require("../package.json");
|
|
80
|
+
return packageJson.version;
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
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'",
|
|
7
|
-
"lint": "
|
|
7
|
+
"lint": "oxlint --deny-warnings"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
10
|
"aikido-npm": "bin/aikido-npm.js",
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"license": "AGPL-3.0-or-later",
|
|
31
31
|
"description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), and [bunx](https://bun.sh/docs/cli/bunx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, or bunx from downloading or running the malware.",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"abbrev": "3.0.1",
|
|
34
33
|
"chalk": "5.4.1",
|
|
35
34
|
"https-proxy-agent": "7.0.6",
|
|
36
35
|
"make-fetch-happen": "14.0.3",
|
package/src/api/npmApi.js
CHANGED
|
@@ -25,6 +25,10 @@ export async function resolvePackageVersion(packageName, versionRange) {
|
|
|
25
25
|
return distTags[versionRange];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
if (!packageInfo.versions) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
// If the version range is not a dist-tag, we need to resolve the highest version matching the range.
|
|
29
33
|
// This is useful for ranges like "^1.0.0" or "~2.3.4".
|
|
30
34
|
const availableVersions = Object.keys(packageInfo.versions);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const state = {
|
|
2
|
-
|
|
2
|
+
loggingLevel: undefined,
|
|
3
3
|
};
|
|
4
4
|
|
|
5
5
|
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
|
|
6
6
|
|
|
7
7
|
export function initializeCliArguments(args) {
|
|
8
8
|
// Reset state on each call
|
|
9
|
-
state.
|
|
9
|
+
state.loggingLevel = undefined;
|
|
10
10
|
|
|
11
11
|
const safeChainArgs = [];
|
|
12
12
|
const remainingArgs = [];
|
|
@@ -19,21 +19,11 @@ export function initializeCliArguments(args) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
setLoggingLevel(safeChainArgs);
|
|
23
23
|
|
|
24
24
|
return remainingArgs;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function setMalwareAction(args) {
|
|
28
|
-
const safeChainMalwareActionArg = SAFE_CHAIN_ARG_PREFIX + "malware-action=";
|
|
29
|
-
|
|
30
|
-
const action = getLastArgEqualsValue(args, safeChainMalwareActionArg);
|
|
31
|
-
if (!action) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
state.malwareAction = action.toLowerCase();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
27
|
function getLastArgEqualsValue(args, prefix) {
|
|
38
28
|
for (var i = args.length - 1; i >= 0; i--) {
|
|
39
29
|
const arg = args[i];
|
|
@@ -45,6 +35,16 @@ function getLastArgEqualsValue(args, prefix) {
|
|
|
45
35
|
return undefined;
|
|
46
36
|
}
|
|
47
37
|
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
function setLoggingLevel(args) {
|
|
39
|
+
const safeChainLoggingArg = SAFE_CHAIN_ARG_PREFIX + "logging=";
|
|
40
|
+
|
|
41
|
+
const level = getLastArgEqualsValue(args, safeChainLoggingArg);
|
|
42
|
+
if (!level) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
state.loggingLevel = level.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getLoggingLevel() {
|
|
49
|
+
return state.loggingLevel;
|
|
50
50
|
}
|
package/src/config/settings.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as cliArguments from "./cliArguments.js";
|
|
2
2
|
|
|
3
|
-
export function
|
|
4
|
-
const
|
|
3
|
+
export function getLoggingLevel() {
|
|
4
|
+
const level = cliArguments.getLoggingLevel();
|
|
5
5
|
|
|
6
|
-
if (
|
|
7
|
-
return
|
|
6
|
+
if (level === LOGGING_SILENT) {
|
|
7
|
+
return LOGGING_SILENT;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
return
|
|
10
|
+
return LOGGING_NORMAL;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
13
|
+
export const LOGGING_SILENT = "silent";
|
|
14
|
+
export const LOGGING_NORMAL = "normal";
|
|
@@ -1,17 +1,28 @@
|
|
|
1
|
+
// oxlint-disable no-console
|
|
1
2
|
import chalk from "chalk";
|
|
2
3
|
import ora from "ora";
|
|
3
|
-
import { createInterface } from "readline";
|
|
4
4
|
import { isCi } from "./environment.js";
|
|
5
|
+
import { getLoggingLevel, LOGGING_SILENT } from "../config/settings.js";
|
|
6
|
+
|
|
7
|
+
function isSilentMode() {
|
|
8
|
+
return getLoggingLevel() === LOGGING_SILENT;
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
function emptyLine() {
|
|
12
|
+
if (isSilentMode()) return;
|
|
13
|
+
|
|
7
14
|
writeInformation("");
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
function writeInformation(message, ...optionalParams) {
|
|
18
|
+
if (isSilentMode()) return;
|
|
19
|
+
|
|
11
20
|
console.log(message, ...optionalParams);
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
function writeWarning(message, ...optionalParams) {
|
|
24
|
+
if (isSilentMode()) return;
|
|
25
|
+
|
|
15
26
|
if (!isCi()) {
|
|
16
27
|
message = chalk.yellow(message);
|
|
17
28
|
}
|
|
@@ -25,7 +36,24 @@ function writeError(message, ...optionalParams) {
|
|
|
25
36
|
console.error(message, ...optionalParams);
|
|
26
37
|
}
|
|
27
38
|
|
|
39
|
+
function writeExitWithoutInstallingMaliciousPackages() {
|
|
40
|
+
let message = "Safe-chain: Exiting without installing malicious packages.";
|
|
41
|
+
if (!isCi()) {
|
|
42
|
+
message = chalk.red(message);
|
|
43
|
+
}
|
|
44
|
+
console.error(message);
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
function startProcess(message) {
|
|
48
|
+
if (isSilentMode()) {
|
|
49
|
+
return {
|
|
50
|
+
succeed: () => {},
|
|
51
|
+
fail: () => {},
|
|
52
|
+
stop: () => {},
|
|
53
|
+
setText: () => {},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
if (isCi()) {
|
|
30
58
|
return {
|
|
31
59
|
succeed: (message) => {
|
|
@@ -58,39 +86,11 @@ function startProcess(message) {
|
|
|
58
86
|
}
|
|
59
87
|
}
|
|
60
88
|
|
|
61
|
-
async function confirm(config) {
|
|
62
|
-
if (isCi()) {
|
|
63
|
-
return Promise.resolve(config.default);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const rl = createInterface({
|
|
67
|
-
input: process.stdin,
|
|
68
|
-
output: process.stdout,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
return new Promise((resolve) => {
|
|
72
|
-
const defaultText = config.default ? " (Y/n)" : " (y/N)";
|
|
73
|
-
rl.question(`${config.message}${defaultText} `, (answer) => {
|
|
74
|
-
rl.close();
|
|
75
|
-
|
|
76
|
-
const normalizedAnswer = answer.trim().toLowerCase();
|
|
77
|
-
|
|
78
|
-
if (normalizedAnswer === "y" || normalizedAnswer === "yes") {
|
|
79
|
-
resolve(true);
|
|
80
|
-
} else if (normalizedAnswer === "n" || normalizedAnswer === "no") {
|
|
81
|
-
resolve(false);
|
|
82
|
-
} else {
|
|
83
|
-
resolve(config.default);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
89
|
export const ui = {
|
|
90
90
|
writeInformation,
|
|
91
91
|
writeWarning,
|
|
92
92
|
writeError,
|
|
93
|
+
writeExitWithoutInstallingMaliciousPackages,
|
|
93
94
|
emptyLine,
|
|
94
95
|
startProcess,
|
|
95
|
-
confirm,
|
|
96
96
|
};
|
|
@@ -14,9 +14,9 @@ const state = {
|
|
|
14
14
|
packageManagerName: null,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
export function initializePackageManager(packageManagerName
|
|
17
|
+
export function initializePackageManager(packageManagerName) {
|
|
18
18
|
if (packageManagerName === "npm") {
|
|
19
|
-
state.packageManagerName = createNpmPackageManager(
|
|
19
|
+
state.packageManagerName = createNpmPackageManager();
|
|
20
20
|
} else if (packageManagerName === "npx") {
|
|
21
21
|
state.packageManagerName = createNpxPackageManager();
|
|
22
22
|
} else if (packageManagerName === "yarn") {
|
|
@@ -1,34 +1,27 @@
|
|
|
1
1
|
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
|
2
|
-
import { dryRunScanner } from "./dependencyScanner/dryRunScanner.js";
|
|
3
2
|
import { nullScanner } from "./dependencyScanner/nullScanner.js";
|
|
4
3
|
import { runNpm } from "./runNpmCommand.js";
|
|
5
4
|
import {
|
|
6
5
|
getNpmCommandForArgs,
|
|
7
6
|
npmInstallCommand,
|
|
8
|
-
npmCiCommand,
|
|
9
|
-
npmInstallTestCommand,
|
|
10
|
-
npmInstallCiTestCommand,
|
|
11
7
|
npmUpdateCommand,
|
|
12
|
-
npmAuditCommand,
|
|
13
8
|
npmExecCommand,
|
|
14
9
|
} from "./utils/npmCommands.js";
|
|
15
10
|
|
|
16
|
-
export function createNpmPackageManager(
|
|
17
|
-
// From npm v10.4.0 onwards, the npm commands output detailed information
|
|
18
|
-
// when using the --dry-run flag.
|
|
19
|
-
// We use that information to scan for dependency changes.
|
|
20
|
-
// For older versions of npm we have to rely on parsing the command arguments.
|
|
21
|
-
const supportedScanners = isPriorToNpm10_4(version)
|
|
22
|
-
? npm10_3AndBelowSupportedScanners
|
|
23
|
-
: npm10_4AndAboveSupportedScanners;
|
|
24
|
-
|
|
11
|
+
export function createNpmPackageManager() {
|
|
25
12
|
function isSupportedCommand(args) {
|
|
26
|
-
const scanner = findDependencyScannerForCommand(
|
|
13
|
+
const scanner = findDependencyScannerForCommand(
|
|
14
|
+
commandScannerMapping,
|
|
15
|
+
args
|
|
16
|
+
);
|
|
27
17
|
return scanner.shouldScan(args);
|
|
28
18
|
}
|
|
29
19
|
|
|
30
20
|
function getDependencyUpdatesForCommand(args) {
|
|
31
|
-
const scanner = findDependencyScannerForCommand(
|
|
21
|
+
const scanner = findDependencyScannerForCommand(
|
|
22
|
+
commandScannerMapping,
|
|
23
|
+
args
|
|
24
|
+
);
|
|
32
25
|
return scanner.scan(args);
|
|
33
26
|
}
|
|
34
27
|
|
|
@@ -39,40 +32,12 @@ export function createNpmPackageManager(version) {
|
|
|
39
32
|
};
|
|
40
33
|
}
|
|
41
34
|
|
|
42
|
-
const
|
|
43
|
-
[npmInstallCommand]: dryRunScanner(),
|
|
44
|
-
[npmUpdateCommand]: dryRunScanner(),
|
|
45
|
-
[npmCiCommand]: dryRunScanner(),
|
|
46
|
-
[npmAuditCommand]: dryRunScanner({
|
|
47
|
-
skipScanWhen: (args) => !args.includes("fix"),
|
|
48
|
-
}),
|
|
49
|
-
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
|
|
50
|
-
|
|
51
|
-
// Running dry-run on install-test and install-ci-test will install & run tests.
|
|
52
|
-
// We only want to know if there are changes in the dependencies.
|
|
53
|
-
// So we run change the dry-run command to only check the install.
|
|
54
|
-
[npmInstallTestCommand]: dryRunScanner({ dryRunCommand: npmInstallCommand }),
|
|
55
|
-
[npmInstallCiTestCommand]: dryRunScanner({ dryRunCommand: npmCiCommand }),
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const npm10_3AndBelowSupportedScanners = {
|
|
35
|
+
const commandScannerMapping = {
|
|
59
36
|
[npmInstallCommand]: commandArgumentScanner(),
|
|
60
37
|
[npmUpdateCommand]: commandArgumentScanner(),
|
|
61
38
|
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
|
|
62
39
|
};
|
|
63
40
|
|
|
64
|
-
function isPriorToNpm10_4(version) {
|
|
65
|
-
try {
|
|
66
|
-
const [major, minor] = version.split(".").map(Number);
|
|
67
|
-
if (major < 10) return true;
|
|
68
|
-
if (major === 10 && minor < 4) return true;
|
|
69
|
-
return false;
|
|
70
|
-
} catch {
|
|
71
|
-
// Default to true: if version parsing fails, assume it's an older version
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
41
|
function findDependencyScannerForCommand(scanners, args) {
|
|
77
42
|
const command = getNpmCommandForArgs(args);
|
|
78
43
|
if (!command) {
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// This was ran with the abbrev package to generate the abbrevs object below
|
|
2
|
+
// console.log(abbrev(commands.concat(Object.keys(aliases))));
|
|
3
|
+
export const abbrevs = {
|
|
4
|
+
ac: "access",
|
|
5
|
+
acc: "access",
|
|
6
|
+
acce: "access",
|
|
7
|
+
acces: "access",
|
|
8
|
+
access: "access",
|
|
9
|
+
add: "add",
|
|
10
|
+
"add-": "add-user",
|
|
11
|
+
"add-u": "add-user",
|
|
12
|
+
"add-us": "add-user",
|
|
13
|
+
"add-use": "add-user",
|
|
14
|
+
"add-user": "add-user",
|
|
15
|
+
addu: "adduser",
|
|
16
|
+
addus: "adduser",
|
|
17
|
+
adduse: "adduser",
|
|
18
|
+
adduser: "adduser",
|
|
19
|
+
aud: "audit",
|
|
20
|
+
audi: "audit",
|
|
21
|
+
audit: "audit",
|
|
22
|
+
aut: "author",
|
|
23
|
+
auth: "author",
|
|
24
|
+
autho: "author",
|
|
25
|
+
author: "author",
|
|
26
|
+
b: "bugs",
|
|
27
|
+
bu: "bugs",
|
|
28
|
+
bug: "bugs",
|
|
29
|
+
bugs: "bugs",
|
|
30
|
+
c: "c",
|
|
31
|
+
ca: "cache",
|
|
32
|
+
cac: "cache",
|
|
33
|
+
cach: "cache",
|
|
34
|
+
cache: "cache",
|
|
35
|
+
ci: "ci",
|
|
36
|
+
cit: "cit",
|
|
37
|
+
"clean-install": "clean-install",
|
|
38
|
+
"clean-install-": "clean-install-test",
|
|
39
|
+
"clean-install-t": "clean-install-test",
|
|
40
|
+
"clean-install-te": "clean-install-test",
|
|
41
|
+
"clean-install-tes": "clean-install-test",
|
|
42
|
+
"clean-install-test": "clean-install-test",
|
|
43
|
+
com: "completion",
|
|
44
|
+
comp: "completion",
|
|
45
|
+
compl: "completion",
|
|
46
|
+
comple: "completion",
|
|
47
|
+
complet: "completion",
|
|
48
|
+
completi: "completion",
|
|
49
|
+
completio: "completion",
|
|
50
|
+
completion: "completion",
|
|
51
|
+
con: "config",
|
|
52
|
+
conf: "config",
|
|
53
|
+
confi: "config",
|
|
54
|
+
config: "config",
|
|
55
|
+
cr: "create",
|
|
56
|
+
cre: "create",
|
|
57
|
+
crea: "create",
|
|
58
|
+
creat: "create",
|
|
59
|
+
create: "create",
|
|
60
|
+
dd: "ddp",
|
|
61
|
+
ddp: "ddp",
|
|
62
|
+
ded: "dedupe",
|
|
63
|
+
dedu: "dedupe",
|
|
64
|
+
dedup: "dedupe",
|
|
65
|
+
dedupe: "dedupe",
|
|
66
|
+
dep: "deprecate",
|
|
67
|
+
depr: "deprecate",
|
|
68
|
+
depre: "deprecate",
|
|
69
|
+
deprec: "deprecate",
|
|
70
|
+
depreca: "deprecate",
|
|
71
|
+
deprecat: "deprecate",
|
|
72
|
+
deprecate: "deprecate",
|
|
73
|
+
dif: "diff",
|
|
74
|
+
diff: "diff",
|
|
75
|
+
"dist-tag": "dist-tag",
|
|
76
|
+
"dist-tags": "dist-tags",
|
|
77
|
+
docs: "docs",
|
|
78
|
+
doct: "doctor",
|
|
79
|
+
docto: "doctor",
|
|
80
|
+
doctor: "doctor",
|
|
81
|
+
ed: "edit",
|
|
82
|
+
edi: "edit",
|
|
83
|
+
edit: "edit",
|
|
84
|
+
exe: "exec",
|
|
85
|
+
exec: "exec",
|
|
86
|
+
expla: "explain",
|
|
87
|
+
explai: "explain",
|
|
88
|
+
explain: "explain",
|
|
89
|
+
explo: "explore",
|
|
90
|
+
explor: "explore",
|
|
91
|
+
explore: "explore",
|
|
92
|
+
find: "find",
|
|
93
|
+
"find-": "find-dupes",
|
|
94
|
+
"find-d": "find-dupes",
|
|
95
|
+
"find-du": "find-dupes",
|
|
96
|
+
"find-dup": "find-dupes",
|
|
97
|
+
"find-dupe": "find-dupes",
|
|
98
|
+
"find-dupes": "find-dupes",
|
|
99
|
+
fu: "fund",
|
|
100
|
+
fun: "fund",
|
|
101
|
+
fund: "fund",
|
|
102
|
+
g: "get",
|
|
103
|
+
ge: "get",
|
|
104
|
+
get: "get",
|
|
105
|
+
help: "help",
|
|
106
|
+
"help-": "help-search",
|
|
107
|
+
"help-s": "help-search",
|
|
108
|
+
"help-se": "help-search",
|
|
109
|
+
"help-sea": "help-search",
|
|
110
|
+
"help-sear": "help-search",
|
|
111
|
+
"help-searc": "help-search",
|
|
112
|
+
"help-search": "help-search",
|
|
113
|
+
hl: "hlep",
|
|
114
|
+
hle: "hlep",
|
|
115
|
+
hlep: "hlep",
|
|
116
|
+
ho: "home",
|
|
117
|
+
hom: "home",
|
|
118
|
+
home: "home",
|
|
119
|
+
i: "i",
|
|
120
|
+
ic: "ic",
|
|
121
|
+
in: "in",
|
|
122
|
+
inf: "info",
|
|
123
|
+
info: "info",
|
|
124
|
+
ini: "init",
|
|
125
|
+
init: "init",
|
|
126
|
+
inn: "innit",
|
|
127
|
+
inni: "innit",
|
|
128
|
+
innit: "innit",
|
|
129
|
+
ins: "ins",
|
|
130
|
+
inst: "inst",
|
|
131
|
+
insta: "insta",
|
|
132
|
+
instal: "instal",
|
|
133
|
+
install: "install",
|
|
134
|
+
"install-ci": "install-ci-test",
|
|
135
|
+
"install-ci-": "install-ci-test",
|
|
136
|
+
"install-ci-t": "install-ci-test",
|
|
137
|
+
"install-ci-te": "install-ci-test",
|
|
138
|
+
"install-ci-tes": "install-ci-test",
|
|
139
|
+
"install-ci-test": "install-ci-test",
|
|
140
|
+
"install-cl": "install-clean",
|
|
141
|
+
"install-cle": "install-clean",
|
|
142
|
+
"install-clea": "install-clean",
|
|
143
|
+
"install-clean": "install-clean",
|
|
144
|
+
"install-t": "install-test",
|
|
145
|
+
"install-te": "install-test",
|
|
146
|
+
"install-tes": "install-test",
|
|
147
|
+
"install-test": "install-test",
|
|
148
|
+
isnt: "isnt",
|
|
149
|
+
isnta: "isnta",
|
|
150
|
+
isntal: "isntal",
|
|
151
|
+
isntall: "isntall",
|
|
152
|
+
"isntall-": "isntall-clean",
|
|
153
|
+
"isntall-c": "isntall-clean",
|
|
154
|
+
"isntall-cl": "isntall-clean",
|
|
155
|
+
"isntall-cle": "isntall-clean",
|
|
156
|
+
"isntall-clea": "isntall-clean",
|
|
157
|
+
"isntall-clean": "isntall-clean",
|
|
158
|
+
iss: "issues",
|
|
159
|
+
issu: "issues",
|
|
160
|
+
issue: "issues",
|
|
161
|
+
issues: "issues",
|
|
162
|
+
it: "it",
|
|
163
|
+
la: "la",
|
|
164
|
+
lin: "link",
|
|
165
|
+
link: "link",
|
|
166
|
+
lis: "list",
|
|
167
|
+
list: "list",
|
|
168
|
+
ll: "ll",
|
|
169
|
+
ln: "ln",
|
|
170
|
+
logi: "login",
|
|
171
|
+
login: "login",
|
|
172
|
+
logo: "logout",
|
|
173
|
+
logou: "logout",
|
|
174
|
+
logout: "logout",
|
|
175
|
+
ls: "ls",
|
|
176
|
+
og: "ogr",
|
|
177
|
+
ogr: "ogr",
|
|
178
|
+
or: "org",
|
|
179
|
+
org: "org",
|
|
180
|
+
ou: "outdated",
|
|
181
|
+
out: "outdated",
|
|
182
|
+
outd: "outdated",
|
|
183
|
+
outda: "outdated",
|
|
184
|
+
outdat: "outdated",
|
|
185
|
+
outdate: "outdated",
|
|
186
|
+
outdated: "outdated",
|
|
187
|
+
ow: "owner",
|
|
188
|
+
own: "owner",
|
|
189
|
+
owne: "owner",
|
|
190
|
+
owner: "owner",
|
|
191
|
+
pa: "pack",
|
|
192
|
+
pac: "pack",
|
|
193
|
+
pack: "pack",
|
|
194
|
+
pi: "ping",
|
|
195
|
+
pin: "ping",
|
|
196
|
+
ping: "ping",
|
|
197
|
+
pk: "pkg",
|
|
198
|
+
pkg: "pkg",
|
|
199
|
+
pre: "prefix",
|
|
200
|
+
pref: "prefix",
|
|
201
|
+
prefi: "prefix",
|
|
202
|
+
prefix: "prefix",
|
|
203
|
+
pro: "profile",
|
|
204
|
+
prof: "profile",
|
|
205
|
+
profi: "profile",
|
|
206
|
+
profil: "profile",
|
|
207
|
+
profile: "profile",
|
|
208
|
+
pru: "prune",
|
|
209
|
+
prun: "prune",
|
|
210
|
+
prune: "prune",
|
|
211
|
+
pu: "publish",
|
|
212
|
+
pub: "publish",
|
|
213
|
+
publ: "publish",
|
|
214
|
+
publi: "publish",
|
|
215
|
+
publis: "publish",
|
|
216
|
+
publish: "publish",
|
|
217
|
+
q: "query",
|
|
218
|
+
qu: "query",
|
|
219
|
+
que: "query",
|
|
220
|
+
quer: "query",
|
|
221
|
+
query: "query",
|
|
222
|
+
r: "r",
|
|
223
|
+
rb: "rb",
|
|
224
|
+
reb: "rebuild",
|
|
225
|
+
rebu: "rebuild",
|
|
226
|
+
rebui: "rebuild",
|
|
227
|
+
rebuil: "rebuild",
|
|
228
|
+
rebuild: "rebuild",
|
|
229
|
+
rem: "remove",
|
|
230
|
+
remo: "remove",
|
|
231
|
+
remov: "remove",
|
|
232
|
+
remove: "remove",
|
|
233
|
+
rep: "repo",
|
|
234
|
+
repo: "repo",
|
|
235
|
+
res: "restart",
|
|
236
|
+
rest: "restart",
|
|
237
|
+
resta: "restart",
|
|
238
|
+
restar: "restart",
|
|
239
|
+
restart: "restart",
|
|
240
|
+
rm: "rm",
|
|
241
|
+
ro: "root",
|
|
242
|
+
roo: "root",
|
|
243
|
+
root: "root",
|
|
244
|
+
rum: "rum",
|
|
245
|
+
run: "run",
|
|
246
|
+
"run-": "run-script",
|
|
247
|
+
"run-s": "run-script",
|
|
248
|
+
"run-sc": "run-script",
|
|
249
|
+
"run-scr": "run-script",
|
|
250
|
+
"run-scri": "run-script",
|
|
251
|
+
"run-scrip": "run-script",
|
|
252
|
+
"run-script": "run-script",
|
|
253
|
+
s: "s",
|
|
254
|
+
sb: "sbom",
|
|
255
|
+
sbo: "sbom",
|
|
256
|
+
sbom: "sbom",
|
|
257
|
+
se: "se",
|
|
258
|
+
sea: "search",
|
|
259
|
+
sear: "search",
|
|
260
|
+
searc: "search",
|
|
261
|
+
search: "search",
|
|
262
|
+
set: "set",
|
|
263
|
+
sho: "show",
|
|
264
|
+
show: "show",
|
|
265
|
+
shr: "shrinkwrap",
|
|
266
|
+
shri: "shrinkwrap",
|
|
267
|
+
shrin: "shrinkwrap",
|
|
268
|
+
shrink: "shrinkwrap",
|
|
269
|
+
shrinkw: "shrinkwrap",
|
|
270
|
+
shrinkwr: "shrinkwrap",
|
|
271
|
+
shrinkwra: "shrinkwrap",
|
|
272
|
+
shrinkwrap: "shrinkwrap",
|
|
273
|
+
si: "sit",
|
|
274
|
+
sit: "sit",
|
|
275
|
+
star: "star",
|
|
276
|
+
stars: "stars",
|
|
277
|
+
start: "start",
|
|
278
|
+
sto: "stop",
|
|
279
|
+
stop: "stop",
|
|
280
|
+
t: "t",
|
|
281
|
+
tea: "team",
|
|
282
|
+
team: "team",
|
|
283
|
+
tes: "test",
|
|
284
|
+
test: "test",
|
|
285
|
+
to: "token",
|
|
286
|
+
tok: "token",
|
|
287
|
+
toke: "token",
|
|
288
|
+
token: "token",
|
|
289
|
+
ts: "tst",
|
|
290
|
+
tst: "tst",
|
|
291
|
+
ud: "udpate",
|
|
292
|
+
udp: "udpate",
|
|
293
|
+
udpa: "udpate",
|
|
294
|
+
udpat: "udpate",
|
|
295
|
+
udpate: "udpate",
|
|
296
|
+
un: "un",
|
|
297
|
+
und: "undeprecate",
|
|
298
|
+
unde: "undeprecate",
|
|
299
|
+
undep: "undeprecate",
|
|
300
|
+
undepr: "undeprecate",
|
|
301
|
+
undepre: "undeprecate",
|
|
302
|
+
undeprec: "undeprecate",
|
|
303
|
+
undepreca: "undeprecate",
|
|
304
|
+
undeprecat: "undeprecate",
|
|
305
|
+
undeprecate: "undeprecate",
|
|
306
|
+
uni: "uninstall",
|
|
307
|
+
unin: "uninstall",
|
|
308
|
+
unins: "uninstall",
|
|
309
|
+
uninst: "uninstall",
|
|
310
|
+
uninsta: "uninstall",
|
|
311
|
+
uninstal: "uninstall",
|
|
312
|
+
uninstall: "uninstall",
|
|
313
|
+
unl: "unlink",
|
|
314
|
+
unli: "unlink",
|
|
315
|
+
unlin: "unlink",
|
|
316
|
+
unlink: "unlink",
|
|
317
|
+
unp: "unpublish",
|
|
318
|
+
unpu: "unpublish",
|
|
319
|
+
unpub: "unpublish",
|
|
320
|
+
unpubl: "unpublish",
|
|
321
|
+
unpubli: "unpublish",
|
|
322
|
+
unpublis: "unpublish",
|
|
323
|
+
unpublish: "unpublish",
|
|
324
|
+
uns: "unstar",
|
|
325
|
+
unst: "unstar",
|
|
326
|
+
unsta: "unstar",
|
|
327
|
+
unstar: "unstar",
|
|
328
|
+
up: "up",
|
|
329
|
+
upd: "update",
|
|
330
|
+
upda: "update",
|
|
331
|
+
updat: "update",
|
|
332
|
+
update: "update",
|
|
333
|
+
upg: "upgrade",
|
|
334
|
+
upgr: "upgrade",
|
|
335
|
+
upgra: "upgrade",
|
|
336
|
+
upgrad: "upgrade",
|
|
337
|
+
upgrade: "upgrade",
|
|
338
|
+
ur: "urn",
|
|
339
|
+
urn: "urn",
|
|
340
|
+
v: "v",
|
|
341
|
+
veri: "verison",
|
|
342
|
+
veris: "verison",
|
|
343
|
+
veriso: "verison",
|
|
344
|
+
verison: "verison",
|
|
345
|
+
vers: "version",
|
|
346
|
+
versi: "version",
|
|
347
|
+
versio: "version",
|
|
348
|
+
version: "version",
|
|
349
|
+
vi: "view",
|
|
350
|
+
vie: "view",
|
|
351
|
+
view: "view",
|
|
352
|
+
who: "whoami",
|
|
353
|
+
whoa: "whoami",
|
|
354
|
+
whoam: "whoami",
|
|
355
|
+
whoami: "whoami",
|
|
356
|
+
why: "why",
|
|
357
|
+
x: "x",
|
|
358
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Based on https://github.com/npm/cli/blob/latest/lib/utils/cmd-list.js
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { abbrevs } from "./abbrevs-generated.js";
|
|
4
4
|
|
|
5
5
|
const commands = [
|
|
6
6
|
"access",
|
|
@@ -158,8 +158,6 @@ export function deref(c) {
|
|
|
158
158
|
return aliases[c];
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
const abbrevs = abbrev(commands.concat(Object.keys(aliases)));
|
|
162
|
-
|
|
163
161
|
// first deref the abbrev, if there is one
|
|
164
162
|
// then resolve any aliases
|
|
165
163
|
// so `npm install-cl` will resolve to `install-clean` then to `ci`
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as http from "http";
|
|
2
|
+
import * as https from "https";
|
|
3
|
+
|
|
4
|
+
export function handleHttpProxyRequest(req, res) {
|
|
5
|
+
const url = new URL(req.url);
|
|
6
|
+
|
|
7
|
+
// The protocol for the plainHttpProxy should usually only be http:
|
|
8
|
+
// but when the client for some reason sends an https: request directly
|
|
9
|
+
// instead of using the CONNECT method, we should handle it gracefully.
|
|
10
|
+
let protocol;
|
|
11
|
+
if (url.protocol === "http:") {
|
|
12
|
+
protocol = http;
|
|
13
|
+
} else if (url.protocol === "https:") {
|
|
14
|
+
protocol = https;
|
|
15
|
+
} else {
|
|
16
|
+
res.writeHead(502);
|
|
17
|
+
res.end(`Bad Gateway: Unsupported protocol ${url.protocol}`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const proxyRequest = protocol
|
|
22
|
+
.request(
|
|
23
|
+
req.url,
|
|
24
|
+
{ method: req.method, headers: req.headers },
|
|
25
|
+
(proxyRes) => {
|
|
26
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
27
|
+
proxyRes.pipe(res);
|
|
28
|
+
|
|
29
|
+
proxyRes.on("error", () => {
|
|
30
|
+
// Proxy response stream error
|
|
31
|
+
// Clean up client response stream
|
|
32
|
+
if (res.writable) {
|
|
33
|
+
res.end();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
proxyRes.on("close", () => {
|
|
38
|
+
// Clean up if the proxy response stream closes
|
|
39
|
+
if (res.writable) {
|
|
40
|
+
res.end();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
.on("error", (err) => {
|
|
46
|
+
res.writeHead(502);
|
|
47
|
+
res.end(`Bad Gateway: ${err.message}`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
req.on("error", () => {
|
|
51
|
+
// Client request stream error
|
|
52
|
+
// Abort the proxy request
|
|
53
|
+
proxyRequest.destroy();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
res.on("error", () => {
|
|
57
|
+
// Client response stream error (client disconnected)
|
|
58
|
+
// Clean up proxy streams
|
|
59
|
+
proxyRequest.destroy();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
res.on("close", () => {
|
|
63
|
+
// Client disconnected
|
|
64
|
+
// Abort the proxy request to avoid unnecessary work
|
|
65
|
+
proxyRequest.destroy();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
req.pipe(proxyRequest);
|
|
69
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as http from "http";
|
|
2
2
|
import { tunnelRequest } from "./tunnelRequestHandler.js";
|
|
3
3
|
import { mitmConnect } from "./mitmRequestHandler.js";
|
|
4
|
+
import { handleHttpProxyRequest } from "./plainHttpProxy.js";
|
|
4
5
|
import { getCaCertPath } from "./certUtils.js";
|
|
5
6
|
import { auditChanges } from "../scanning/audit/index.js";
|
|
6
7
|
import { knownRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js";
|
|
@@ -15,7 +16,6 @@ const state = {
|
|
|
15
16
|
|
|
16
17
|
export function createSafeChainProxy() {
|
|
17
18
|
const server = createProxyServer();
|
|
18
|
-
server.on("connect", handleConnect);
|
|
19
19
|
|
|
20
20
|
return {
|
|
21
21
|
startServer: () => startServer(server),
|
|
@@ -54,13 +54,15 @@ export function mergeSafeChainProxyEnvironmentVariables(env) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function createProxyServer() {
|
|
57
|
-
const server = http.createServer(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
const server = http.createServer(
|
|
58
|
+
// This handles direct HTTP requests (non-CONNECT requests)
|
|
59
|
+
// This is normally http-only traffic, but we also handle
|
|
60
|
+
// https for clients that don't properly use CONNECT
|
|
61
|
+
handleHttpProxyRequest
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// This handles HTTPS requests via the CONNECT method
|
|
65
|
+
server.on("connect", handleConnect);
|
|
64
66
|
|
|
65
67
|
return server;
|
|
66
68
|
}
|
|
@@ -151,7 +153,7 @@ function verifyNoMaliciousPackages() {
|
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
ui.emptyLine();
|
|
154
|
-
ui.
|
|
156
|
+
ui.writeExitWithoutInstallingMaliciousPackages();
|
|
155
157
|
ui.emptyLine();
|
|
156
158
|
|
|
157
159
|
return false;
|
package/src/scanning/index.js
CHANGED
|
@@ -4,7 +4,6 @@ import { setTimeout } from "timers/promises";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { getPackageManager } from "../packagemanager/currentPackageManager.js";
|
|
6
6
|
import { ui } from "../environment/userInteraction.js";
|
|
7
|
-
import { getMalwareAction, MALWARE_ACTION_PROMPT } from "../config/settings.js";
|
|
8
7
|
|
|
9
8
|
export function shouldScanCommand(args) {
|
|
10
9
|
if (!args || args.length === 0) {
|
|
@@ -65,7 +64,8 @@ export async function scanCommand(args) {
|
|
|
65
64
|
return 0;
|
|
66
65
|
} else {
|
|
67
66
|
printMaliciousChanges(audit.disallowedChanges, spinner);
|
|
68
|
-
|
|
67
|
+
onMalwareFound();
|
|
68
|
+
return 1;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -77,23 +77,8 @@ function printMaliciousChanges(changes, spinner) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
function onMalwareFound() {
|
|
81
81
|
ui.emptyLine();
|
|
82
|
-
|
|
83
|
-
if (getMalwareAction() === MALWARE_ACTION_PROMPT) {
|
|
84
|
-
const continueInstall = await ui.confirm({
|
|
85
|
-
message:
|
|
86
|
-
"Malicious packages were found. Do you want to continue with the installation?",
|
|
87
|
-
default: false,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
if (continueInstall) {
|
|
91
|
-
ui.writeWarning("Continuing with the installation despite the risks...");
|
|
92
|
-
return 0;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
ui.writeError("Exiting without installing malicious packages.");
|
|
82
|
+
ui.writeExitWithoutInstallingMaliciousPackages();
|
|
97
83
|
ui.emptyLine();
|
|
98
|
-
return 1;
|
|
99
84
|
}
|
package/src/utils/safeSpawn.js
CHANGED
|
@@ -1,27 +1,77 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn, execSync } from "child_process";
|
|
2
|
+
import os from "os";
|
|
2
3
|
|
|
3
|
-
function
|
|
4
|
-
// If argument contains
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
function sanitizeShellArgument(arg) {
|
|
5
|
+
// If argument contains shell metacharacters, wrap in double quotes
|
|
6
|
+
// and escape characters that are special even inside double quotes
|
|
7
|
+
if (hasShellMetaChars(arg)) {
|
|
8
|
+
// Inside double quotes, we need to escape: " $ ` \
|
|
9
|
+
return '"' + escapeDoubleQuoteContent(arg) + '"';
|
|
7
10
|
}
|
|
8
11
|
return arg;
|
|
9
12
|
}
|
|
10
13
|
|
|
14
|
+
function hasShellMetaChars(arg) {
|
|
15
|
+
// Shell metacharacters that need escaping
|
|
16
|
+
// These characters have special meaning in shells and need to be quoted
|
|
17
|
+
// Whenever one of these characters is present, we should quote the argument
|
|
18
|
+
// Characters: space, ", &, ', |, ;, <, >, (, ), $, `, \, !, *, ?, [, ], {, }, ~, #
|
|
19
|
+
const shellMetaChars = /[ "&'|;<>()$`\\!*?[\]{}~#]/;
|
|
20
|
+
return shellMetaChars.test(arg);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function escapeDoubleQuoteContent(arg) {
|
|
24
|
+
// Escape special characters for shell safety
|
|
25
|
+
// This escapes ", $, `, and \ by prefixing them with a backslash
|
|
26
|
+
return arg.replace(/(["`$\\])/g, "\\$1");
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
function buildCommand(command, args) {
|
|
12
|
-
|
|
30
|
+
if (args.length === 0) {
|
|
31
|
+
return command;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const escapedArgs = args.map(sanitizeShellArgument);
|
|
35
|
+
|
|
13
36
|
return `${command} ${escapedArgs.join(" ")}`;
|
|
14
37
|
}
|
|
15
38
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
function resolveCommandPath(command) {
|
|
40
|
+
// command will be "npm", "yarn", etc.
|
|
41
|
+
// Use 'command -v' to find the full path
|
|
42
|
+
const fullPath = execSync(`command -v ${command}`, {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
shell: true,
|
|
45
|
+
}).trim();
|
|
46
|
+
|
|
47
|
+
if (!fullPath) {
|
|
48
|
+
throw new Error(`Command not found: ${command}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return fullPath;
|
|
19
52
|
}
|
|
20
53
|
|
|
21
54
|
export async function safeSpawn(command, args, options = {}) {
|
|
22
|
-
|
|
55
|
+
// The command is always one of our supported package managers.
|
|
56
|
+
// It should always be alphanumeric or _ or -
|
|
57
|
+
// Reject any command names with suspicious characters
|
|
58
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(command)) {
|
|
59
|
+
throw new Error(`Invalid command name: ${command}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
23
62
|
return new Promise((resolve, reject) => {
|
|
24
|
-
|
|
63
|
+
// Windows requires shell: true because .bat and .cmd files are not executable
|
|
64
|
+
// without a terminal. On Unix/macOS, we resolve the full path first, then use
|
|
65
|
+
// array args (safer, no escaping needed).
|
|
66
|
+
// See: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
|
|
67
|
+
let child;
|
|
68
|
+
if (os.platform() === "win32") {
|
|
69
|
+
const fullCommand = buildCommand(command, args);
|
|
70
|
+
child = spawn(fullCommand, { ...options, shell: true });
|
|
71
|
+
} else {
|
|
72
|
+
const fullPath = resolveCommandPath(command);
|
|
73
|
+
child = spawn(fullPath, args, options);
|
|
74
|
+
}
|
|
25
75
|
|
|
26
76
|
// When stdio is piped, we need to collect the output
|
|
27
77
|
let stdout = "";
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { parseDryRunOutput } from "../parsing/parseNpmInstallDryRunOutput.js";
|
|
2
|
-
import { dryRunNpmCommandAndOutput } from "../runNpmCommand.js";
|
|
3
|
-
import { hasDryRunArg } from "../utils/npmCommands.js";
|
|
4
|
-
|
|
5
|
-
export function dryRunScanner(scannerOptions) {
|
|
6
|
-
return {
|
|
7
|
-
scan: (args) => scanDependencies(scannerOptions, args),
|
|
8
|
-
shouldScan: (args) => shouldScanDependencies(scannerOptions, args),
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function scanDependencies(scannerOptions, args) {
|
|
13
|
-
let dryRunArgs = args;
|
|
14
|
-
|
|
15
|
-
if (scannerOptions?.dryRunCommand) {
|
|
16
|
-
// Replace the first argument with the dryRunCommand (eg: "install" instead of "install-test")
|
|
17
|
-
dryRunArgs = [scannerOptions.dryRunCommand, ...args.slice(1)];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return checkChangesWithDryRun(dryRunArgs);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function shouldScanDependencies(scannerOptions, args) {
|
|
24
|
-
if (hasDryRunArg(args)) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (scannerOptions?.skipScanWhen && scannerOptions.skipScanWhen(args)) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function checkChangesWithDryRun(args) {
|
|
36
|
-
const dryRunOutput = await dryRunNpmCommandAndOutput(args);
|
|
37
|
-
|
|
38
|
-
// Dry-run can return a non-zero status code in some cases
|
|
39
|
-
// e.g., when running "npm audit fix --dry-run", it returns exit code 1
|
|
40
|
-
// when there are vulnerabilities that can be fixed.
|
|
41
|
-
if (dryRunOutput.status !== 0 && !canCommandReturnNonZeroOnSuccess(args)) {
|
|
42
|
-
throw new Error(
|
|
43
|
-
`Dry-run command failed with exit code ${dryRunOutput.status} and output:\n${dryRunOutput.output}`
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (dryRunOutput.status !== 0 && !dryRunOutput.output) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`Dry-run command failed with exit code ${dryRunOutput.status} and produced no output.`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const parsedOutput = parseDryRunOutput(dryRunOutput.output);
|
|
54
|
-
|
|
55
|
-
// reverse the array to have the top-level packages first
|
|
56
|
-
return parsedOutput.reverse();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function canCommandReturnNonZeroOnSuccess(args) {
|
|
60
|
-
if (args.length < 2) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// `npm audit fix --dry-run` can return exit code 1 when it succesfully ran and
|
|
65
|
-
// there were vulnerabilities that could be fixed
|
|
66
|
-
return args[0] === "audit" && args[1] === "fix";
|
|
67
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
export function parseDryRunOutput(output) {
|
|
2
|
-
const lines = output.split(/\r?\n/);
|
|
3
|
-
const packageChanges = [];
|
|
4
|
-
|
|
5
|
-
for (const line of lines) {
|
|
6
|
-
if (line.startsWith("add ")) {
|
|
7
|
-
packageChanges.push(parseAdd(line));
|
|
8
|
-
} else if (line.startsWith("remove ")) {
|
|
9
|
-
packageChanges.push(parseRemove(line));
|
|
10
|
-
} else if (line.startsWith("change ")) {
|
|
11
|
-
packageChanges.push(parseChange(line));
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return packageChanges;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function parseAdd(line) {
|
|
19
|
-
const splitLine = getLineParts(line);
|
|
20
|
-
const packageName = splitLine[1];
|
|
21
|
-
const packageVersion = splitLine[splitLine.length - 1];
|
|
22
|
-
return addedPackage(packageName, packageVersion);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function addedPackage(name, version) {
|
|
26
|
-
return { type: "add", name, version };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function parseRemove(line) {
|
|
30
|
-
const splitLine = getLineParts(line);
|
|
31
|
-
const packageName = splitLine[1];
|
|
32
|
-
const packageVersion = splitLine[splitLine.length - 1];
|
|
33
|
-
return removedPackage(packageName, packageVersion);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function removedPackage(name, version) {
|
|
37
|
-
return { type: "remove", name, version };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseChange(line) {
|
|
41
|
-
const splitLine = getLineParts(line);
|
|
42
|
-
const packageName = splitLine[1];
|
|
43
|
-
const packageVersion = splitLine[splitLine.length - 1];
|
|
44
|
-
const oldVersion = splitLine[2];
|
|
45
|
-
return changedPackage(packageName, packageVersion, oldVersion);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getLineParts(line) {
|
|
49
|
-
return line
|
|
50
|
-
.split(" ")
|
|
51
|
-
.map((part) => part.trim())
|
|
52
|
-
.filter((part) => part !== "");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function changedPackage(name, version, oldVersion) {
|
|
56
|
-
return { type: "change", name, version, oldVersion };
|
|
57
|
-
}
|