@aikidosec/safe-chain 1.0.21 → 1.0.23

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 CHANGED
@@ -8,12 +8,16 @@ The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [n
8
8
 
9
9
  Aikido Safe Chain works on Node.js version 18 and above and supports the following package managers:
10
10
 
11
- - ✅ **npm**
12
- - **npx**
13
- - **yarn**
14
- - **pnpm**
15
- - **pnpx**
16
- - 🚧 **bun** Coming soon
11
+ - ✅ full coverage: **npm >= 10.4.0**:
12
+ - ⚠️ limited to scanning the install command arguments (broader scanning coming soon):
13
+ - **npm < 10.4.0**
14
+ - **npx**
15
+ - **yarn**
16
+ - **pnpm**
17
+ - **pnpx**
18
+ - 🚧 **bun**: coming soon
19
+
20
+ Note on the limited support for npm < 10.4.0, npx, yarn, pnpm and pnpx: adding **full support for these package managers is a high priority**. In the meantime, we offer limited support already, which means that the Aikido Safe Chain will scan the package names passed as arguments to the install commands. However, it will not scan the full dependency tree of these packages.
17
21
 
18
22
  # Usage
19
23
 
@@ -84,4 +88,4 @@ npm install suspicious-package --safe-chain-malware-action=prompt
84
88
 
85
89
  # Usage in CI/CD
86
90
 
87
- 🚧 Support for CI/CD environments is coming soon...
91
+ [Learn more about Safe Chain CI/CD integration in the Aikido docs.](https://help.aikido.dev/code-scanning/aikido-malware-scanning/malware-scanning-with-safe-chain-in-ci-cd-environments)
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, process.versions.node);
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.21",
3
+ "version": "1.0.23",
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,12 +28,12 @@
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
- "abbrev": "^3.0.1",
33
- "chalk": "^5.4.1",
34
- "npm-registry-fetch": "^18.0.2",
35
- "ora": "^8.2.0",
36
- "semver": "^7.7.2"
31
+ "abbrev": "3.0.1",
32
+ "chalk": "5.4.1",
33
+ "make-fetch-happen": "14.0.3",
34
+ "npm-registry-fetch": "18.0.2",
35
+ "ora": "8.2.0",
36
+ "semver": "7.7.2"
37
37
  },
38
38
  "main": "src/main.js",
39
39
  "bugs": {
package/src/api/aikido.js CHANGED
@@ -1,3 +1,5 @@
1
+ import fetch from "make-fetch-happen";
2
+
1
3
  const malwareDatabaseUrl =
2
4
  "https://malware-list.aikido.dev/malware_predictions.json";
3
5
 
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import ora from "ora";
3
- import { confirm as inquirerConfirm } from "@inquirer/prompts";
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 = {
@@ -14,10 +14,13 @@ import {
14
14
  } from "./utils/npmCommands.js";
15
15
 
16
16
  export function createNpmPackageManager(version) {
17
- const supportedScanners =
18
- getMajorVersion(version) >= 22
19
- ? npm22AndAboveSupportedScanners
20
- : npm21AndBelowSupportedScanners;
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 npm22AndAboveSupportedScanners = {
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 npm21AndBelowSupportedScanners = {
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 warnForLimitedSupport(version) {
63
- if (getMajorVersion(version) >= 22) {
64
- return null;
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) =>
@@ -5,7 +5,6 @@ const scanner = commandArgumentScanner();
5
5
 
6
6
  export function createYarnPackageManager() {
7
7
  return {
8
- getWarningMessage: () => null,
9
8
  runCommand: runYarnCommand,
10
9
  isSupportedCommand: (args) =>
11
10
  matchesCommand(args, "add") ||
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "child_process";
2
2
  import * as os from "os";
3
3
  import fs from "fs";
4
+ import path from "path";
4
5
 
5
6
  export const knownAikidoTools = [
6
7
  { tool: "npm", aikidoCommand: "aikido-npm" },
@@ -22,15 +23,17 @@ export function doesExecutableExistOnSystem(executableName) {
22
23
  }
23
24
  }
24
25
 
25
- export function removeLinesMatchingPattern(filePath, pattern) {
26
+ export function removeLinesMatchingPattern(filePath, pattern, eol) {
26
27
  if (!fs.existsSync(filePath)) {
27
28
  return;
28
29
  }
29
30
 
31
+ eol = eol || os.EOL;
32
+
30
33
  const fileContent = fs.readFileSync(filePath, "utf-8");
31
- const lines = fileContent.split(/[\r\n\u2028\u2029]+/);
34
+ const lines = fileContent.split(/[\r\n\u2028\u2029]/);
32
35
  const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern));
33
- fs.writeFileSync(filePath, updatedLines.join(os.EOL), "utf-8");
36
+ fs.writeFileSync(filePath, updatedLines.join(eol), "utf-8");
34
37
  }
35
38
 
36
39
  const maxLineLength = 100;
@@ -43,12 +46,17 @@ function shouldRemoveLine(line, pattern) {
43
46
 
44
47
  if (line.length > maxLineLength) {
45
48
  // safe-chain only adds lines shorter than maxLineLength
46
- // so if the line is longer, it must be from a different
49
+ // so if the line is longer, it must be from a different
47
50
  // source and could be dangerous to remove
48
51
  return false;
49
52
  }
50
53
 
51
- if (line.includes("\n") || line.includes("\r") || line.includes("\u2028") || line.includes("\u2029")) {
54
+ if (
55
+ line.includes("\n") ||
56
+ line.includes("\r") ||
57
+ line.includes("\u2028") ||
58
+ line.includes("\u2029")
59
+ ) {
52
60
  // If the line contains newlines, something has gone wrong in splitting
53
61
  // \u2028 and \u2029 are Unicode line separator characters (line and paragraph separators)
54
62
  return false;
@@ -57,12 +65,25 @@ function shouldRemoveLine(line, pattern) {
57
65
  return true;
58
66
  }
59
67
 
60
- export function addLineToFile(filePath, line) {
61
- if (!fs.existsSync(filePath)) {
62
- fs.writeFileSync(filePath, "", "utf-8");
63
- }
68
+ export function addLineToFile(filePath, line, eol) {
69
+ createFileIfNotExists(filePath);
70
+
71
+ eol = eol || os.EOL;
64
72
 
65
73
  const fileContent = fs.readFileSync(filePath, "utf-8");
66
- const updatedContent = fileContent + os.EOL + line;
74
+ const updatedContent = fileContent + eol + line + eol;
67
75
  fs.writeFileSync(filePath, updatedContent, "utf-8");
68
76
  }
77
+
78
+ function createFileIfNotExists(filePath) {
79
+ if (fs.existsSync(filePath)) {
80
+ return;
81
+ }
82
+
83
+ const dir = path.dirname(filePath);
84
+ if (!fs.existsSync(dir)) {
85
+ fs.mkdirSync(dir, { recursive: true });
86
+ }
87
+
88
+ fs.writeFileSync(filePath, "", "utf-8");
89
+ }
@@ -56,11 +56,13 @@ export async function setup() {
56
56
  */
57
57
  function setupShell(shell) {
58
58
  let success = false;
59
+ let error;
59
60
  try {
60
61
  shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases
61
62
  success = shell.setup(knownAikidoTools);
62
- } catch {
63
+ } catch (err) {
63
64
  success = false;
65
+ error = err;
64
66
  }
65
67
 
66
68
  if (success) {
@@ -75,6 +77,13 @@ function setupShell(shell) {
75
77
  "Setup failed"
76
78
  )}. Please check your ${shell.name} configuration.`
77
79
  );
80
+ if (error) {
81
+ let message = ` Error: ${error.message}`;
82
+ if (error.code) {
83
+ message += ` (code: ${error.code})`;
84
+ }
85
+ ui.writeError(message);
86
+ }
78
87
  }
79
88
 
80
89
  return success;
@@ -9,6 +9,7 @@ import * as os from "os";
9
9
  const shellName = "Bash";
10
10
  const executableName = "bash";
11
11
  const startupFileCommand = "echo ~/.bashrc";
12
+ const eol = "\n"; // When bash runs on Windows (e.g., Git Bash or WSL), it expects LF line endings.
12
13
 
13
14
  function isInstalled() {
14
15
  return doesExecutableExistOnSystem(executableName);
@@ -19,13 +20,18 @@ function teardown(tools) {
19
20
 
20
21
  for (const { tool } of tools) {
21
22
  // Remove any existing alias for the tool
22
- removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`));
23
+ removeLinesMatchingPattern(
24
+ startupFile,
25
+ new RegExp(`^alias\\s+${tool}=`),
26
+ eol
27
+ );
23
28
  }
24
29
 
25
30
  // Removes the line that sources the safe-chain bash initialization script (~/.aikido/scripts/init-posix.sh)
26
31
  removeLinesMatchingPattern(
27
32
  startupFile,
28
- /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/
33
+ /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,
34
+ eol
29
35
  );
30
36
 
31
37
  return true;
@@ -36,7 +42,8 @@ function setup() {
36
42
 
37
43
  addLineToFile(
38
44
  startupFile,
39
- `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script`
45
+ `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script`,
46
+ eol
40
47
  );
41
48
 
42
49
  return true;
@@ -8,6 +8,7 @@ import { execSync } from "child_process";
8
8
  const shellName = "Fish";
9
9
  const executableName = "fish";
10
10
  const startupFileCommand = "echo ~/.config/fish/config.fish";
11
+ const eol = "\n"; // When fish runs on Windows (e.g., Git Bash or WSL), it expects LF line endings.
11
12
 
12
13
  function isInstalled() {
13
14
  return doesExecutableExistOnSystem(executableName);
@@ -20,14 +21,16 @@ function teardown(tools) {
20
21
  // Remove any existing alias for the tool
21
22
  removeLinesMatchingPattern(
22
23
  startupFile,
23
- new RegExp(`^alias\\s+${tool}\\s+`)
24
+ new RegExp(`^alias\\s+${tool}\\s+`),
25
+ eol
24
26
  );
25
27
  }
26
28
 
27
29
  // Removes the line that sources the safe-chain fish initialization script (~/.safe-chain/scripts/init-fish.fish)
28
30
  removeLinesMatchingPattern(
29
31
  startupFile,
30
- /^source\s+~\/\.safe-chain\/scripts\/init-fish\.fish/
32
+ /^source\s+~\/\.safe-chain\/scripts\/init-fish\.fish/,
33
+ eol
31
34
  );
32
35
 
33
36
  return true;
@@ -38,7 +41,8 @@ function setup() {
38
41
 
39
42
  addLineToFile(
40
43
  startupFile,
41
- `source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script`
44
+ `source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script`,
45
+ eol
42
46
  );
43
47
 
44
48
  return true;
@@ -8,6 +8,7 @@ import { execSync } from "child_process";
8
8
  const shellName = "Zsh";
9
9
  const executableName = "zsh";
10
10
  const startupFileCommand = "echo ${ZDOTDIR:-$HOME}/.zshrc";
11
+ const eol = "\n"; // When zsh runs on Windows (e.g., Git Bash or WSL), it expects LF line endings.
11
12
 
12
13
  function isInstalled() {
13
14
  return doesExecutableExistOnSystem(executableName);
@@ -18,13 +19,18 @@ function teardown(tools) {
18
19
 
19
20
  for (const { tool } of tools) {
20
21
  // Remove any existing alias for the tool
21
- removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`));
22
+ removeLinesMatchingPattern(
23
+ startupFile,
24
+ new RegExp(`^alias\\s+${tool}=`),
25
+ eol
26
+ );
22
27
  }
23
28
 
24
29
  // Removes the line that sources the safe-chain zsh initialization script (~/.aikido/scripts/init-posix.sh)
25
30
  removeLinesMatchingPattern(
26
31
  startupFile,
27
- /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/
32
+ /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,
33
+ eol
28
34
  );
29
35
 
30
36
  return true;
@@ -35,7 +41,8 @@ function setup() {
35
41
 
36
42
  addLineToFile(
37
43
  startupFile,
38
- `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script`
44
+ `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script`,
45
+ eol
39
46
  );
40
47
 
41
48
  return true;