@aikidosec/safe-chain 1.0.20 → 1.0.22
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 +16 -1
- package/bin/aikido-npm.js +12 -1
- package/package.json +2 -2
- package/src/api/aikido.js +2 -0
- package/src/config/cliArguments.js +50 -0
- package/src/config/settings.js +14 -0
- package/src/environment/userInteraction.js +23 -6
- package/src/main.js +4 -0
- package/src/packagemanager/npm/createPackageManager.js +18 -17
- package/src/packagemanager/npx/createPackageManager.js +0 -1
- package/src/packagemanager/pnpm/createPackageManager.js +0 -2
- package/src/packagemanager/yarn/createPackageManager.js +0 -1
- package/src/scanning/index.js +16 -14
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Aikido Safe Chain
|
|
2
2
|
|
|
3
|
-
The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm and pnpx.
|
|
3
|
+
The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm and pnpx. It's **free** to use and does not require any token.
|
|
4
4
|
|
|
5
5
|
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/), and [pnpx](https://pnpm.io/cli/dlx) 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 or pnpx from downloading or running the malware.
|
|
6
6
|
|
|
@@ -67,6 +67,21 @@ To uninstall the Aikido Safe Chain, you can run the following command:
|
|
|
67
67
|
```
|
|
68
68
|
3. **❗Restart your terminal** to remove the aliases.
|
|
69
69
|
|
|
70
|
+
# Configuration
|
|
71
|
+
|
|
72
|
+
## Malware Action
|
|
73
|
+
|
|
74
|
+
You can control how Aikido Safe Chain responds when malware is detected using the `--safe-chain-malware-action` flag:
|
|
75
|
+
|
|
76
|
+
- `--safe-chain-malware-action=block` (**default**) - Automatically blocks installation and exits with an error when malware is detected
|
|
77
|
+
- `--safe-chain-malware-action=prompt` - Prompts the user to decide whether to continue despite the malware detection
|
|
78
|
+
|
|
79
|
+
Example usage:
|
|
80
|
+
|
|
81
|
+
```shell
|
|
82
|
+
npm install suspicious-package --safe-chain-malware-action=prompt
|
|
83
|
+
```
|
|
84
|
+
|
|
70
85
|
# Usage in CI/CD
|
|
71
86
|
|
|
72
87
|
🚧 Support for CI/CD environments is coming soon...
|
package/bin/aikido-npm.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { execSync } from "child_process";
|
|
3
4
|
import { main } from "../src/main.js";
|
|
4
5
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
6
|
|
|
6
7
|
const packageManagerName = "npm";
|
|
7
|
-
initializePackageManager(packageManagerName,
|
|
8
|
+
initializePackageManager(packageManagerName, getNpmVersion());
|
|
8
9
|
await main(process.argv.slice(2));
|
|
10
|
+
|
|
11
|
+
function getNpmVersion() {
|
|
12
|
+
try {
|
|
13
|
+
return execSync("npm --version").toString().trim();
|
|
14
|
+
} catch {
|
|
15
|
+
// Default to 0.0.0 if npm is not found
|
|
16
|
+
// That way we don't use any unsupported features
|
|
17
|
+
return "0.0.0";
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
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'",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"license": "AGPL-3.0-or-later",
|
|
29
29
|
"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/), and [pnpx](https://pnpm.io/cli/dlx) 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, or pnpx from downloading or running the malware.",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@inquirer/prompts": "^7.4.1",
|
|
32
31
|
"abbrev": "^3.0.1",
|
|
33
32
|
"chalk": "^5.4.1",
|
|
33
|
+
"make-fetch-happen": "^14.0.3",
|
|
34
34
|
"npm-registry-fetch": "^18.0.2",
|
|
35
35
|
"ora": "^8.2.0",
|
|
36
36
|
"semver": "^7.7.2"
|
package/src/api/aikido.js
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const state = {
|
|
2
|
+
malwareAction: undefined,
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
|
|
6
|
+
|
|
7
|
+
export function initializeCliArguments(args) {
|
|
8
|
+
// Reset state on each call
|
|
9
|
+
state.malwareAction = undefined;
|
|
10
|
+
|
|
11
|
+
const safeChainArgs = [];
|
|
12
|
+
const remainingArgs = [];
|
|
13
|
+
|
|
14
|
+
for (const arg of args) {
|
|
15
|
+
if (arg.toLowerCase().startsWith(SAFE_CHAIN_ARG_PREFIX)) {
|
|
16
|
+
safeChainArgs.push(arg);
|
|
17
|
+
} else {
|
|
18
|
+
remainingArgs.push(arg);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setMalwareAction(safeChainArgs);
|
|
23
|
+
|
|
24
|
+
return remainingArgs;
|
|
25
|
+
}
|
|
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
|
+
function getLastArgEqualsValue(args, prefix) {
|
|
38
|
+
for (var i = args.length - 1; i >= 0; i--) {
|
|
39
|
+
const arg = args[i];
|
|
40
|
+
if (arg.toLowerCase().startsWith(prefix)) {
|
|
41
|
+
return arg.substring(prefix.length);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getMalwareAction() {
|
|
49
|
+
return state.malwareAction;
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as cliArguments from "./cliArguments.js";
|
|
2
|
+
|
|
3
|
+
export function getMalwareAction() {
|
|
4
|
+
const action = cliArguments.getMalwareAction();
|
|
5
|
+
|
|
6
|
+
if (action === MALWARE_ACTION_PROMPT) {
|
|
7
|
+
return MALWARE_ACTION_PROMPT;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return MALWARE_ACTION_BLOCK;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const MALWARE_ACTION_BLOCK = "block";
|
|
14
|
+
export const MALWARE_ACTION_PROMPT = "prompt";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
|
-
import {
|
|
3
|
+
import { createInterface } from "readline";
|
|
4
4
|
import { isCi } from "./environment.js";
|
|
5
5
|
|
|
6
6
|
function emptyLine() {
|
|
@@ -61,12 +61,29 @@ function startProcess(message) {
|
|
|
61
61
|
async function confirm(config) {
|
|
62
62
|
if (isCi()) {
|
|
63
63
|
return Promise.resolve(config.default);
|
|
64
|
-
} else {
|
|
65
|
-
return inquirerConfirm({
|
|
66
|
-
message: config.message,
|
|
67
|
-
default: config.default,
|
|
68
|
-
});
|
|
69
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
|
+
});
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
export const ui = {
|
package/src/main.js
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
import { scanCommand, shouldScanCommand } from "./scanning/index.js";
|
|
4
4
|
import { ui } from "./environment/userInteraction.js";
|
|
5
5
|
import { getPackageManager } from "./packagemanager/currentPackageManager.js";
|
|
6
|
+
import { initializeCliArguments } from "./config/cliArguments.js";
|
|
6
7
|
|
|
7
8
|
export async function main(args) {
|
|
8
9
|
try {
|
|
10
|
+
// This parses all the --safe-chain arguments and removes them from the args array
|
|
11
|
+
args = initializeCliArguments(args);
|
|
12
|
+
|
|
9
13
|
if (shouldScanCommand(args)) {
|
|
10
14
|
await scanCommand(args);
|
|
11
15
|
}
|
|
@@ -14,10 +14,13 @@ import {
|
|
|
14
14
|
} from "./utils/npmCommands.js";
|
|
15
15
|
|
|
16
16
|
export function createNpmPackageManager(version) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
21
24
|
|
|
22
25
|
function isSupportedCommand(args) {
|
|
23
26
|
const scanner = findDependencyScannerForCommand(supportedScanners, args);
|
|
@@ -30,14 +33,13 @@ export function createNpmPackageManager(version) {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
return {
|
|
33
|
-
getWarningMessage: () => warnForLimitedSupport(version),
|
|
34
36
|
runCommand: runNpm,
|
|
35
37
|
isSupportedCommand,
|
|
36
38
|
getDependencyUpdatesForCommand,
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
const
|
|
42
|
+
const npm10_4AndAboveSupportedScanners = {
|
|
41
43
|
[npmInstallCommand]: dryRunScanner(),
|
|
42
44
|
[npmUpdateCommand]: dryRunScanner(),
|
|
43
45
|
[npmCiCommand]: dryRunScanner(),
|
|
@@ -53,23 +55,22 @@ const npm22AndAboveSupportedScanners = {
|
|
|
53
55
|
[npmInstallCiTestCommand]: dryRunScanner({ dryRunCommand: npmCiCommand }),
|
|
54
56
|
};
|
|
55
57
|
|
|
56
|
-
const
|
|
58
|
+
const npm10_3AndBelowSupportedScanners = {
|
|
57
59
|
[npmInstallCommand]: commandArgumentScanner(),
|
|
58
60
|
[npmUpdateCommand]: commandArgumentScanner(),
|
|
59
61
|
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
|
|
60
62
|
};
|
|
61
63
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
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;
|
|
65
73
|
}
|
|
66
|
-
|
|
67
|
-
return `Aikido-npm will only scan the arguments of the install command for Node.js version prior to version 22.
|
|
68
|
-
Please update your Node.js version to 22 or higher for full coverage. Current version: v${version}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function getMajorVersion(version) {
|
|
72
|
-
return parseInt(version.split(".")[0]);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
function findDependencyScannerForCommand(scanners, args) {
|
|
@@ -5,7 +5,6 @@ export function createNpxPackageManager() {
|
|
|
5
5
|
const scanner = commandArgumentScanner();
|
|
6
6
|
|
|
7
7
|
return {
|
|
8
|
-
getWarningMessage: () => null,
|
|
9
8
|
runCommand: runNpx,
|
|
10
9
|
isSupportedCommand: (args) => scanner.shouldScan(args),
|
|
11
10
|
getDependencyUpdatesForCommand: (args) => scanner.scan(args),
|
|
@@ -6,7 +6,6 @@ const scanner = commandArgumentScanner();
|
|
|
6
6
|
|
|
7
7
|
export function createPnpmPackageManager() {
|
|
8
8
|
return {
|
|
9
|
-
getWarningMessage: () => null,
|
|
10
9
|
runCommand: (args) => runPnpmCommand(args, "pnpm"),
|
|
11
10
|
isSupportedCommand: (args) =>
|
|
12
11
|
matchesCommand(args, "add") ||
|
|
@@ -26,7 +25,6 @@ export function createPnpmPackageManager() {
|
|
|
26
25
|
|
|
27
26
|
export function createPnpxPackageManager() {
|
|
28
27
|
return {
|
|
29
|
-
getWarningMessage: () => null,
|
|
30
28
|
runCommand: (args) => runPnpmCommand(args, "pnpx"),
|
|
31
29
|
isSupportedCommand: () => true,
|
|
32
30
|
getDependencyUpdatesForCommand: (args) =>
|
package/src/scanning/index.js
CHANGED
|
@@ -4,6 +4,7 @@ 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";
|
|
7
8
|
|
|
8
9
|
export function shouldScanCommand(args) {
|
|
9
10
|
if (!args || args.length === 0) {
|
|
@@ -59,10 +60,7 @@ export async function scanCommand(args) {
|
|
|
59
60
|
spinner.succeed("No malicious packages detected.");
|
|
60
61
|
} else {
|
|
61
62
|
printMaliciousChanges(audit.disallowedChanges, spinner);
|
|
62
|
-
await
|
|
63
|
-
"Do you want to continue with the installation despite the risks?",
|
|
64
|
-
false
|
|
65
|
-
);
|
|
63
|
+
await onMalwareFound();
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
66
|
|
|
@@ -74,19 +72,23 @@ function printMaliciousChanges(changes, spinner) {
|
|
|
74
72
|
}
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
async function
|
|
75
|
+
async function onMalwareFound() {
|
|
78
76
|
ui.emptyLine();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
|
|
78
|
+
if (getMalwareAction() === MALWARE_ACTION_PROMPT) {
|
|
79
|
+
const continueInstall = await ui.confirm({
|
|
80
|
+
message:
|
|
81
|
+
"Malicious packages were found. Do you want to continue with the installation?",
|
|
82
|
+
default: false,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (continueInstall) {
|
|
86
|
+
ui.writeWarning("Continuing with the installation despite the risks...");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
ui.
|
|
91
|
+
ui.writeError("Exiting without installing malicious packages.");
|
|
90
92
|
ui.emptyLine();
|
|
91
93
|
process.exit(1);
|
|
92
94
|
}
|