@aikidosec/safe-chain 1.1.3 → 1.1.5
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 +5 -0
- 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/environment/userInteraction.js +1 -0
- 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/mitmRequestHandler.js +6 -0
- package/src/registryProxy/plainHttpProxy.js +69 -0
- package/src/registryProxy/registryProxy.js +10 -8
- package/src/registryProxy/tunnelRequestHandler.js +6 -0
- package/src/utils/safeSpawn.js +1 -6
- 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,11 @@ 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
|
+
```shell
|
|
45
|
+
safe-chain --version
|
|
46
|
+
```
|
|
47
|
+
|
|
43
48
|
## How it works
|
|
44
49
|
|
|
45
50
|
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.
|
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.5",
|
|
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",
|
|
@@ -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`
|
|
@@ -5,6 +5,12 @@ import { HttpsProxyAgent } from "https-proxy-agent";
|
|
|
5
5
|
export function mitmConnect(req, clientSocket, isAllowed) {
|
|
6
6
|
const { hostname } = new URL(`http://${req.url}`);
|
|
7
7
|
|
|
8
|
+
clientSocket.on("error", () => {
|
|
9
|
+
// NO-OP
|
|
10
|
+
// This can happen if the client TCP socket sends RST instead of FIN.
|
|
11
|
+
// Not subscribing to 'close' event will cause node to throw and crash.
|
|
12
|
+
});
|
|
13
|
+
|
|
8
14
|
const server = createHttpsServer(hostname, isAllowed);
|
|
9
15
|
|
|
10
16
|
// Establish the connection
|
|
@@ -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
|
}
|
|
@@ -24,6 +24,12 @@ export function tunnelRequest(req, clientSocket, head) {
|
|
|
24
24
|
function tunnelRequestToDestination(req, clientSocket, head) {
|
|
25
25
|
const { port, hostname } = new URL(`http://${req.url}`);
|
|
26
26
|
|
|
27
|
+
clientSocket.on("error", () => {
|
|
28
|
+
// NO-OP
|
|
29
|
+
// This can happen if the client TCP socket sends RST instead of FIN.
|
|
30
|
+
// Not subscribing to 'close' event will cause node to throw and crash.
|
|
31
|
+
});
|
|
32
|
+
|
|
27
33
|
const serverSocket = net.connect(port || 443, hostname, () => {
|
|
28
34
|
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
29
35
|
serverSocket.write(head);
|
package/src/utils/safeSpawn.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
2
|
|
|
3
3
|
function escapeArg(arg) {
|
|
4
4
|
// If argument contains spaces or quotes, wrap in double quotes and escape double quotes
|
|
@@ -13,11 +13,6 @@ function buildCommand(command, args) {
|
|
|
13
13
|
return `${command} ${escapedArgs.join(" ")}`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function safeSpawnSync(command, args, options = {}) {
|
|
17
|
-
const fullCommand = buildCommand(command, args);
|
|
18
|
-
return spawnSync(fullCommand, { ...options, shell: true });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
export async function safeSpawn(command, args, options = {}) {
|
|
22
17
|
const fullCommand = buildCommand(command, args);
|
|
23
18
|
return new Promise((resolve, reject) => {
|
|
@@ -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
|
-
}
|