@aikidosec/safe-chain 1.0.23 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +58 -2
  2. package/bin/aikido-bun.js +10 -0
  3. package/bin/aikido-bunx.js +10 -0
  4. package/bin/aikido-npm.js +3 -1
  5. package/bin/aikido-npx.js +3 -1
  6. package/bin/aikido-pnpm.js +3 -1
  7. package/bin/aikido-pnpx.js +3 -1
  8. package/bin/aikido-yarn.js +3 -1
  9. package/bin/safe-chain.js +8 -0
  10. package/docs/safe-package-manager-demo.png +0 -0
  11. package/docs/shell-integration.md +42 -27
  12. package/package.json +5 -1
  13. package/src/main.js +35 -4
  14. package/src/packagemanager/bun/createBunPackageManager.js +42 -0
  15. package/src/packagemanager/currentPackageManager.js +8 -0
  16. package/src/packagemanager/npm/dependencyScanner/dryRunScanner.js +23 -6
  17. package/src/packagemanager/npm/runNpmCommand.js +26 -10
  18. package/src/packagemanager/npx/runNpxCommand.js +8 -5
  19. package/src/packagemanager/pnpm/runPnpmCommand.js +18 -11
  20. package/src/packagemanager/yarn/runYarnCommand.js +41 -5
  21. package/src/registryProxy/certUtils.js +114 -0
  22. package/src/registryProxy/mitmRequestHandler.js +90 -0
  23. package/src/registryProxy/parsePackageFromUrl.js +48 -0
  24. package/src/registryProxy/registryProxy.js +158 -0
  25. package/src/registryProxy/tunnelRequestHandler.js +98 -0
  26. package/src/scanning/index.js +14 -9
  27. package/src/scanning/malwareDatabase.js +10 -1
  28. package/src/shell-integration/helpers.js +20 -3
  29. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
  30. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
  31. package/src/shell-integration/setup-ci.js +123 -0
  32. package/src/shell-integration/setup.js +2 -2
  33. package/src/shell-integration/startup-scripts/init-fish.fish +17 -5
  34. package/src/shell-integration/startup-scripts/init-posix.sh +8 -0
  35. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +8 -0
  36. package/src/shell-integration/teardown.js +2 -2
  37. package/src/utils/safeSpawn.js +50 -0
package/README.md CHANGED
@@ -4,7 +4,7 @@ The Aikido Safe Chain **prevents developers from installing malware** on their w
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
 
7
- ![demo](https://aikido-production-staticfiles-public.s3.eu-west-1.amazonaws.com/safe-pkg.gif)
7
+ ![demo](./docs/safe-package-manager-demo.png)
8
8
 
9
9
  Aikido Safe Chain works on Node.js version 18 and above and supports the following package managers:
10
10
 
@@ -88,4 +88,60 @@ npm install suspicious-package --safe-chain-malware-action=prompt
88
88
 
89
89
  # Usage in CI/CD
90
90
 
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)
91
+ You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation.
92
+
93
+ For optimal protection in CI/CD environments, we recommend using **npm >= 10.4.0** as it provides full dependency tree scanning. Other package managers currently offer limited scanning of install command arguments only.
94
+
95
+ ## Setup
96
+
97
+ To use Aikido Safe Chain in CI/CD environments, run the following command after installing the package:
98
+
99
+ ```shell
100
+ safe-chain setup-ci
101
+ ```
102
+
103
+ This automatically configures your CI environment to use Aikido Safe Chain for all package manager commands.
104
+
105
+ ## Supported Platforms
106
+
107
+ - ✅ **GitHub Actions**
108
+ - ✅ **Azure Pipelines**
109
+
110
+ ## GitHub Actions Example
111
+
112
+ ```yaml
113
+ - name: Setup Node.js
114
+ uses: actions/setup-node@v4
115
+ with:
116
+ node-version: "22"
117
+ cache: "npm"
118
+
119
+ - name: Setup safe-chain
120
+ run: |
121
+ npm i -g @aikidosec/safe-chain
122
+ safe-chain setup-ci
123
+
124
+ - name: Install dependencies
125
+ run: |
126
+ npm ci
127
+ ```
128
+
129
+ ## Azure DevOps Example
130
+
131
+ ```yaml
132
+ - task: NodeTool@0
133
+ inputs:
134
+ versionSpec: "22.x"
135
+ displayName: "Install Node.js"
136
+
137
+ - script: |
138
+ npm i -g @aikidosec/safe-chain
139
+ safe-chain setup-ci
140
+ displayName: "Install safe chain"
141
+
142
+ - script: |
143
+ npm ci
144
+ displayName: "npm install and build"
145
+ ```
146
+
147
+ After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/main.js";
4
+ import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
5
+
6
+ const packageManagerName = "bun";
7
+ initializePackageManager(packageManagerName);
8
+ var exitCode = await main(process.argv.slice(2));
9
+
10
+ process.exit(exitCode);
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/main.js";
4
+ import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
5
+
6
+ const packageManagerName = "bunx";
7
+ initializePackageManager(packageManagerName);
8
+ var exitCode = await main(process.argv.slice(2));
9
+
10
+ process.exit(exitCode);
package/bin/aikido-npm.js CHANGED
@@ -6,7 +6,9 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa
6
6
 
7
7
  const packageManagerName = "npm";
8
8
  initializePackageManager(packageManagerName, getNpmVersion());
9
- await main(process.argv.slice(2));
9
+ var exitCode = await main(process.argv.slice(2));
10
+
11
+ process.exit(exitCode);
10
12
 
11
13
  function getNpmVersion() {
12
14
  try {
package/bin/aikido-npx.js CHANGED
@@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa
5
5
 
6
6
  const packageManagerName = "npx";
7
7
  initializePackageManager(packageManagerName, process.versions.node);
8
- await main(process.argv.slice(2));
8
+ var exitCode = await main(process.argv.slice(2));
9
+
10
+ process.exit(exitCode);
@@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa
5
5
 
6
6
  const packageManagerName = "pnpm";
7
7
  initializePackageManager(packageManagerName, process.versions.node);
8
- await main(process.argv.slice(2));
8
+ var exitCode = await main(process.argv.slice(2));
9
+
10
+ process.exit(exitCode);
@@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa
5
5
 
6
6
  const packageManagerName = "pnpx";
7
7
  initializePackageManager(packageManagerName, process.versions.node);
8
- await main(process.argv.slice(2));
8
+ var exitCode = await main(process.argv.slice(2));
9
+
10
+ process.exit(exitCode);
@@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa
5
5
 
6
6
  const packageManagerName = "yarn";
7
7
  initializePackageManager(packageManagerName, process.versions.node);
8
- await main(process.argv.slice(2));
8
+ var exitCode = await main(process.argv.slice(2));
9
+
10
+ process.exit(exitCode);
package/bin/safe-chain.js CHANGED
@@ -4,6 +4,7 @@ import chalk from "chalk";
4
4
  import { ui } from "../src/environment/userInteraction.js";
5
5
  import { setup } from "../src/shell-integration/setup.js";
6
6
  import { teardown } from "../src/shell-integration/teardown.js";
7
+ import { setupCi } from "../src/shell-integration/setup-ci.js";
7
8
 
8
9
  if (process.argv.length < 3) {
9
10
  ui.writeError("No command provided. Please provide a command to execute.");
@@ -23,6 +24,8 @@ if (command === "setup") {
23
24
  setup();
24
25
  } else if (command === "teardown") {
25
26
  teardown();
27
+ } else if (command === "setup-ci") {
28
+ setupCi();
26
29
  } else {
27
30
  ui.writeError(`Unknown command: ${command}.`);
28
31
  ui.emptyLine();
@@ -53,5 +56,10 @@ function writeHelp() {
53
56
  "safe-chain teardown"
54
57
  )}: This will remove safe-chain aliases from your shell configuration.`
55
58
  );
59
+ ui.writeInformation(
60
+ `- ${chalk.cyan(
61
+ "safe-chain setup-ci"
62
+ )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`
63
+ );
56
64
  ui.emptyLine();
57
65
  }
@@ -2,21 +2,21 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`) with Aikido's security scanning functionality. This is achieved by adding shell aliases that redirect these commands to their Aikido-wrapped equivalents.
5
+ The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`) with Aikido's security scanning functionality. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents.
6
6
 
7
7
  ## Supported Shells
8
8
 
9
9
  Aikido Safe Chain supports integration with the following shells.
10
10
 
11
- | Shell | Startup File | Alias Format |
12
- | ---------------------- | ---------------------------- | -------------------------- |
13
- | **Bash** | `~/.bashrc` | `alias npm='aikido-npm'` |
14
- | **Zsh** | `~/.zshrc` | `alias npm='aikido-npm'` |
15
- | **Fish** | `~/.config/fish/config.fish` | `alias npm "aikido-npm"` |
16
- | **PowerShell Core** | `$PROFILE` | `Set-Alias npm aikido-npm` |
17
- | **Windows PowerShell** | `$PROFILE` | `Set-Alias npm aikido-npm` |
11
+ | Shell | Startup File |
12
+ | ---------------------- | ---------------------------- |
13
+ | **Bash** | `~/.bashrc` |
14
+ | **Zsh** | `~/.zshrc` |
15
+ | **Fish** | `~/.config/fish/config.fish` |
16
+ | **PowerShell Core** | `$PROFILE` |
17
+ | **Windows PowerShell** | `$PROFILE` |
18
18
 
19
- ## Commands
19
+ ## Setup Commands
20
20
 
21
21
  ### Setup Shell Integration
22
22
 
@@ -26,10 +26,11 @@ safe-chain setup
26
26
 
27
27
  This command:
28
28
 
29
+ - Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`)
29
30
  - Detects all supported shells on your system
30
- - Adds aliases for `npm`, `npx`, `yarn`, `pnpm` and `pnpx` to each shell's startup file
31
+ - Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, and `pnpx`
31
32
 
32
- ❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the aliases are loaded correctly.
33
+ ❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the startup scripts are sourced correctly.
33
34
 
34
35
  ### Remove Shell Integration
35
36
 
@@ -40,13 +41,13 @@ safe-chain teardown
40
41
  This command:
41
42
 
42
43
  - Detects all supported shells on your system
43
- - Removes Aikido aliases from each shell's startup file
44
+ - Removes the Safe Chain scripts from each shell's startup file, restoring the original commands
44
45
 
45
46
  ❗ After running this command, **you must restart your terminal** to restore the original commands.
46
47
 
47
48
  ## File Locations
48
49
 
49
- The system modifies the following files based on your shell configuration:
50
+ The system modifies the following files to source Safe Chain startup scripts:
50
51
 
51
52
  ### Unix/Linux/macOS
52
53
 
@@ -64,15 +65,16 @@ The system modifies the following files based on your shell configuration:
64
65
 
65
66
  ### Common Issues
66
67
 
67
- **Aliases not working after setup:**
68
+ **Shell functions not working after setup:**
68
69
 
69
70
  - Make sure to restart your terminal
70
- - Check that the startup file was actually modified
71
+ - Check that the startup file was modified to source Safe Chain scripts
72
+ - Check the sourced file exists at `~/.safe-chain/scripts/`
71
73
  - Verify your shell is reading the correct startup file
72
74
 
73
75
  **Getting 'command not found: aikido-npm' error:**
74
76
 
75
- This means the aliases are working but the Aikido commands aren't installed or available in your PATH:
77
+ This means the shell functions are working but the Aikido commands aren't installed or available in your PATH:
76
78
 
77
79
  - Make sure Aikido Safe Chain is properly installed on your system
78
80
  - Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` and `aikido-pnpx` commands exist
@@ -82,27 +84,40 @@ This means the aliases are working but the Aikido commands aren't installed or a
82
84
 
83
85
  To verify the integration is working, follow these steps:
84
86
 
85
- 1. **Check if aliases were added to your shell startup file:**
87
+ 1. **Check if startup scripts were sourced in your shell startup file:**
86
88
 
87
89
  - **For Bash**: Open `~/.bashrc` in your text editor
88
90
  - **For Zsh**: Open `~/.zshrc` in your text editor
89
91
  - **For Fish**: Open `~/.config/fish/config.fish` in your text editor
90
92
  - **For PowerShell**: Open your PowerShell profile file (run `$PROFILE` in PowerShell to see the path)
91
93
 
92
- Look for lines like:
94
+ Look for lines that source the Safe Chain startup scripts from `~/.safe-chain/scripts/`
93
95
 
94
- - `alias npm='aikido-npm'` (Bash/Zsh)
95
- - `alias npm "aikido-npm"` (Fish)
96
- - `Set-Alias npm aikido-npm` (PowerShell)
97
-
98
- 2. **Test that aliases are active in your terminal:**
96
+ 2. **Test that shell functions are active in your terminal:**
99
97
 
100
98
  After restarting your terminal, run these commands:
101
99
 
102
- - `which npm` - Should show the path to `aikido-npm` instead of the original npm
103
100
  - `npm --version` - Should show output from the Aikido-wrapped version
104
- - `type npm` - Alternative way to check what command `npm` resolves to
101
+ - `type npm` - Should show that `npm` is a function
102
+
103
+ 3. **If you need to remove the integration manually:**
104
+
105
+ Edit the same startup file from step 1 and delete any lines that source Safe Chain scripts from `~/.safe-chain/scripts/`.
106
+
107
+ ## Manual Setup
105
108
 
106
- 3. **If you need to remove aliases manually:**
109
+ For advanced users who prefer manual configuration, you can create wrapper functions directly in your shell's startup file. Shell functions take precedence over commands in PATH, so defining an `npm` function will intercept all `npm` calls:
110
+
111
+ ```bash
112
+ # Example for Bash/Zsh
113
+ npm() {
114
+ if command -v aikido-npm > /dev/null 2>&1; then
115
+ aikido-npm "$@"
116
+ else
117
+ echo "Warning: safe-chain is not installed. npm will run without protection."
118
+ command npm "$@"
119
+ fi
120
+ }
121
+ ```
107
122
 
108
- Edit the same startup file from step 1 and delete any lines containing `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` or `aikido-pnpx`.
123
+ Repeat this pattern for `npx`, `yarn`, `pnpm`, and `pnpx` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.0.23",
3
+ "version": "1.1.0",
4
4
  "scripts": {
5
5
  "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
6
6
  "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
@@ -12,6 +12,8 @@
12
12
  "aikido-yarn": "bin/aikido-yarn.js",
13
13
  "aikido-pnpm": "bin/aikido-pnpm.js",
14
14
  "aikido-pnpx": "bin/aikido-pnpx.js",
15
+ "aikido-bun": "bin/aikido-bun.js",
16
+ "aikido-bunx": "bin/aikido-bunx.js",
15
17
  "safe-chain": "bin/safe-chain.js"
16
18
  },
17
19
  "type": "module",
@@ -30,7 +32,9 @@
30
32
  "dependencies": {
31
33
  "abbrev": "3.0.1",
32
34
  "chalk": "5.4.1",
35
+ "https-proxy-agent": "7.0.6",
33
36
  "make-fetch-happen": "14.0.3",
37
+ "node-forge": "1.3.1",
34
38
  "npm-registry-fetch": "18.0.2",
35
39
  "ora": "8.2.0",
36
40
  "semver": "7.7.2"
package/src/main.js CHANGED
@@ -4,19 +4,50 @@ import { scanCommand, shouldScanCommand } from "./scanning/index.js";
4
4
  import { ui } from "./environment/userInteraction.js";
5
5
  import { getPackageManager } from "./packagemanager/currentPackageManager.js";
6
6
  import { initializeCliArguments } from "./config/cliArguments.js";
7
+ import { createSafeChainProxy } from "./registryProxy/registryProxy.js";
8
+ import chalk from "chalk";
7
9
 
8
10
  export async function main(args) {
11
+ const proxy = createSafeChainProxy();
12
+ await proxy.startServer();
13
+
9
14
  try {
10
15
  // This parses all the --safe-chain arguments and removes them from the args array
11
16
  args = initializeCliArguments(args);
12
17
 
13
18
  if (shouldScanCommand(args)) {
14
- await scanCommand(args);
19
+ const commandScanResult = await scanCommand(args);
20
+
21
+ // Returning the exit code back to the caller allows the promise
22
+ // to be awaited in the bin files and return the correct exit code
23
+ if (commandScanResult !== 0) {
24
+ return commandScanResult;
25
+ }
26
+ }
27
+
28
+ const packageManagerResult = await getPackageManager().runCommand(args);
29
+
30
+ if (!proxy.verifyNoMaliciousPackages()) {
31
+ return 1;
15
32
  }
33
+
34
+ ui.emptyLine();
35
+ ui.writeInformation(
36
+ `${chalk.green(
37
+ "✔"
38
+ )} Safe-chain: Command completed, no malicious packages found.`
39
+ );
40
+
41
+ // Returning the exit code back to the caller allows the promise
42
+ // to be awaited in the bin files and return the correct exit code
43
+ return packageManagerResult.status;
16
44
  } catch (error) {
17
45
  ui.writeError("Failed to check for malicious packages:", error.message);
18
- }
19
46
 
20
- var result = getPackageManager().runCommand(args);
21
- process.exit(result.status);
47
+ // Returning the exit code back to the caller allows the promise
48
+ // to be awaited in the bin files and return the correct exit code
49
+ return 1;
50
+ } finally {
51
+ await proxy.stopServer();
52
+ }
22
53
  }
@@ -0,0 +1,42 @@
1
+ import { ui } from "../../environment/userInteraction.js";
2
+ import { safeSpawn } from "../../utils/safeSpawn.js";
3
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
4
+
5
+ export function createBunPackageManager() {
6
+ return {
7
+ runCommand: (args) => runBunCommand("bun", args),
8
+
9
+ // For bun, we use the proxy-only approach to block package downloads,
10
+ // so we don't need to analyze commands.
11
+ isSupportedCommand: () => false,
12
+ getDependencyUpdatesForCommand: () => [],
13
+ };
14
+ }
15
+
16
+ export function createBunxPackageManager() {
17
+ return {
18
+ runCommand: (args) => runBunCommand("bunx", args),
19
+
20
+ // For bunx, we use the proxy-only approach to block package downloads,
21
+ // so we don't need to analyze commands.
22
+ isSupportedCommand: () => false,
23
+ getDependencyUpdatesForCommand: () => [],
24
+ };
25
+ }
26
+
27
+ async function runBunCommand(command, args) {
28
+ try {
29
+ const result = await safeSpawn(command, args, {
30
+ stdio: "inherit",
31
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
32
+ });
33
+ return { status: result.status };
34
+ } catch (error) {
35
+ if (error.status) {
36
+ return { status: error.status };
37
+ } else {
38
+ ui.writeError("Error executing command:", error.message);
39
+ return { status: 1 };
40
+ }
41
+ }
42
+ }
@@ -1,3 +1,7 @@
1
+ import {
2
+ createBunPackageManager,
3
+ createBunxPackageManager,
4
+ } from "./bun/createBunPackageManager.js";
1
5
  import { createNpmPackageManager } from "./npm/createPackageManager.js";
2
6
  import { createNpxPackageManager } from "./npx/createPackageManager.js";
3
7
  import {
@@ -21,6 +25,10 @@ export function initializePackageManager(packageManagerName, version) {
21
25
  state.packageManagerName = createPnpmPackageManager();
22
26
  } else if (packageManagerName === "pnpx") {
23
27
  state.packageManagerName = createPnpxPackageManager();
28
+ } else if (packageManagerName === "bun") {
29
+ state.packageManagerName = createBunPackageManager();
30
+ } else if (packageManagerName === "bunx") {
31
+ state.packageManagerName = createBunxPackageManager();
24
32
  } else {
25
33
  throw new Error("Unsupported package manager: " + packageManagerName);
26
34
  }
@@ -1,4 +1,3 @@
1
- import { ui } from "../../../environment/userInteraction.js";
2
1
  import { parseDryRunOutput } from "../parsing/parseNpmInstallDryRunOutput.js";
3
2
  import { dryRunNpmCommandAndOutput } from "../runNpmCommand.js";
4
3
  import { hasDryRunArg } from "../utils/npmCommands.js";
@@ -9,6 +8,7 @@ export function dryRunScanner(scannerOptions) {
9
8
  shouldScan: (args) => shouldScanDependencies(scannerOptions, args),
10
9
  };
11
10
  }
11
+
12
12
  function scanDependencies(scannerOptions, args) {
13
13
  let dryRunArgs = args;
14
14
 
@@ -32,15 +32,22 @@ function shouldScanDependencies(scannerOptions, args) {
32
32
  return true;
33
33
  }
34
34
 
35
- function checkChangesWithDryRun(args) {
36
- const dryRunOutput = dryRunNpmCommandAndOutput(args);
35
+ async function checkChangesWithDryRun(args) {
36
+ const dryRunOutput = await dryRunNpmCommandAndOutput(args);
37
37
 
38
38
  // Dry-run can return a non-zero status code in some cases
39
39
  // e.g., when running "npm audit fix --dry-run", it returns exit code 1
40
- // when there are vulnurabilities that can be fixed.
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
+
41
47
  if (dryRunOutput.status !== 0 && !dryRunOutput.output) {
42
- ui.writeError("Detecting changes failed.");
43
- return [];
48
+ throw new Error(
49
+ `Dry-run command failed with exit code ${dryRunOutput.status} and produced no output.`
50
+ );
44
51
  }
45
52
 
46
53
  const parsedOutput = parseDryRunOutput(dryRunOutput.output);
@@ -48,3 +55,13 @@ function checkChangesWithDryRun(args) {
48
55
  // reverse the array to have the top-level packages first
49
56
  return parsedOutput.reverse();
50
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,10 +1,14 @@
1
- import { execSync } from "child_process";
2
1
  import { ui } from "../../environment/userInteraction.js";
2
+ import { safeSpawn } from "../../utils/safeSpawn.js";
3
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
3
4
 
4
- export function runNpm(args) {
5
+ export async function runNpm(args) {
5
6
  try {
6
- const npmCommand = `npm ${args.join(" ")}`;
7
- execSync(npmCommand, { stdio: "inherit" });
7
+ const result = await safeSpawn("npm", args, {
8
+ stdio: "inherit",
9
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
10
+ });
11
+ return { status: result.status };
8
12
  } catch (error) {
9
13
  if (error.status) {
10
14
  return { status: error.status };
@@ -13,17 +17,29 @@ export function runNpm(args) {
13
17
  return { status: 1 };
14
18
  }
15
19
  }
16
- return { status: 0 };
17
20
  }
18
21
 
19
- export function dryRunNpmCommandAndOutput(args) {
22
+ export async function dryRunNpmCommandAndOutput(args) {
20
23
  try {
21
- const npmCommand = `npm ${args.join(" ")} --dry-run`;
22
- const output = execSync(npmCommand, { stdio: "pipe" });
23
- return { status: 0, output: output.toString() };
24
+ const result = await safeSpawn(
25
+ "npm",
26
+ [...args, "--ignore-scripts", "--dry-run"],
27
+ {
28
+ stdio: "pipe",
29
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
30
+ }
31
+ );
32
+ return {
33
+ status: result.status,
34
+ output: result.status === 0 ? result.stdout : result.stderr,
35
+ };
24
36
  } catch (error) {
25
37
  if (error.status) {
26
- const output = error.stdout ? error.stdout.toString() : "";
38
+ const output =
39
+ error.stdout?.toString() ??
40
+ error.stderr?.toString() ??
41
+ error.message ??
42
+ "";
27
43
  return { status: error.status, output };
28
44
  } else {
29
45
  ui.writeError("Error executing command:", error.message);
@@ -1,10 +1,14 @@
1
- import { execSync } from "child_process";
2
1
  import { ui } from "../../environment/userInteraction.js";
2
+ import { safeSpawn } from "../../utils/safeSpawn.js";
3
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
3
4
 
4
- export function runNpx(args) {
5
+ export async function runNpx(args) {
5
6
  try {
6
- const npxCommand = `npx ${args.join(" ")}`;
7
- execSync(npxCommand, { stdio: "inherit" });
7
+ const result = await safeSpawn("npx", args, {
8
+ stdio: "inherit",
9
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
10
+ });
11
+ return { status: result.status };
8
12
  } catch (error) {
9
13
  if (error.status) {
10
14
  return { status: error.status };
@@ -13,5 +17,4 @@ export function runNpx(args) {
13
17
  return { status: 1 };
14
18
  }
15
19
  }
16
- return { status: 0 };
17
20
  }
@@ -1,24 +1,31 @@
1
- import { spawnSync } from "child_process";
2
1
  import { ui } from "../../environment/userInteraction.js";
2
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
3
+ import { safeSpawn } from "../../utils/safeSpawn.js";
3
4
 
4
- export function runPnpmCommand(args, toolName = "pnpm") {
5
+ export async function runPnpmCommand(args, toolName = "pnpm") {
5
6
  try {
6
7
  let result;
7
-
8
8
  if (toolName === "pnpm") {
9
- result = spawnSync("pnpm", args, { stdio: "inherit" });
9
+ result = await safeSpawn("pnpm", args, {
10
+ stdio: "inherit",
11
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
12
+ });
10
13
  } else if (toolName === "pnpx") {
11
- result = spawnSync("pnpx", args, { stdio: "inherit" });
14
+ result = await safeSpawn("pnpx", args, {
15
+ stdio: "inherit",
16
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
17
+ });
12
18
  } else {
13
19
  throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
14
20
  }
15
21
 
16
- if (result.status !== null) {
17
- return { status: result.status };
18
- }
22
+ return { status: result.status };
19
23
  } catch (error) {
20
- ui.writeError("Error executing command:", error.message);
21
- return { status: 1 };
24
+ if (error.status) {
25
+ return { status: error.status };
26
+ } else {
27
+ ui.writeError("Error executing command:", error.message);
28
+ return { status: 1 };
29
+ }
22
30
  }
23
- return { status: 0 };
24
31
  }