@aikidosec/safe-chain 1.0.22 → 1.0.24
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 +68 -8
- package/bin/safe-chain.js +8 -0
- package/docs/safe-package-manager-demo.png +0 -0
- package/docs/shell-integration.md +42 -27
- package/package.json +7 -7
- package/src/main.js +1 -0
- package/src/packagemanager/npm/dependencyScanner/dryRunScanner.js +20 -4
- package/src/packagemanager/npm/runNpmCommand.js +1 -1
- package/src/packagemanager/pnpm/runPnpmCommand.js +10 -10
- package/src/scanning/index.js +10 -6
- package/src/shell-integration/helpers.js +47 -10
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
- package/src/shell-integration/setup-ci.js +123 -0
- package/src/shell-integration/setup.js +12 -3
- package/src/shell-integration/startup-scripts/init-fish.fish +9 -5
- package/src/shell-integration/supported-shells/bash.js +10 -3
- package/src/shell-integration/supported-shells/fish.js +7 -3
- package/src/shell-integration/supported-shells/zsh.js +10 -3
- package/src/shell-integration/teardown.js +2 -2
- package/src/utils/safeSpawn.js +38 -0
package/README.md
CHANGED
|
@@ -4,16 +4,20 @@ 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
|
-

|
|
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
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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,60 @@ npm install suspicious-package --safe-chain-malware-action=prompt
|
|
|
84
88
|
|
|
85
89
|
# Usage in CI/CD
|
|
86
90
|
|
|
87
|
-
|
|
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.
|
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
|
}
|
|
Binary file
|
|
@@ -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
|
|
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 |
|
|
12
|
-
| ---------------------- | ---------------------------- |
|
|
13
|
-
| **Bash** | `~/.bashrc` |
|
|
14
|
-
| **Zsh** | `~/.zshrc` |
|
|
15
|
-
| **Fish** | `~/.config/fish/config.fish` |
|
|
16
|
-
| **PowerShell Core** | `$PROFILE` |
|
|
17
|
-
| **Windows PowerShell** | `$PROFILE` |
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
**
|
|
68
|
+
**Shell functions not working after setup:**
|
|
68
69
|
|
|
69
70
|
- Make sure to restart your terminal
|
|
70
|
-
- Check that the startup file was
|
|
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
|
|
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
|
|
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
|
|
94
|
+
Look for lines that source the Safe Chain startup scripts from `~/.safe-chain/scripts/`
|
|
93
95
|
|
|
94
|
-
|
|
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` -
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.24",
|
|
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
|
-
"abbrev": "
|
|
32
|
-
"chalk": "
|
|
33
|
-
"make-fetch-happen": "
|
|
34
|
-
"npm-registry-fetch": "
|
|
35
|
-
"ora": "
|
|
36
|
-
"semver": "
|
|
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/main.js
CHANGED
|
@@ -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";
|
|
@@ -37,10 +36,17 @@ function checkChangesWithDryRun(args) {
|
|
|
37
36
|
|
|
38
37
|
// Dry-run can return a non-zero status code in some cases
|
|
39
38
|
// e.g., when running "npm audit fix --dry-run", it returns exit code 1
|
|
40
|
-
// when there are
|
|
39
|
+
// when there are vulnerabilities that can be fixed.
|
|
40
|
+
if (dryRunOutput.status !== 0 && !canCommandReturnNonZeroOnSuccess(args)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Dry-run command failed with exit code ${dryRunOutput.status} and output:\n${dryRunOutput.output}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
if (dryRunOutput.status !== 0 && !dryRunOutput.output) {
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Dry-run command failed with exit code ${dryRunOutput.status} and produced no output.`
|
|
49
|
+
);
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
const parsedOutput = parseDryRunOutput(dryRunOutput.output);
|
|
@@ -48,3 +54,13 @@ function checkChangesWithDryRun(args) {
|
|
|
48
54
|
// reverse the array to have the top-level packages first
|
|
49
55
|
return parsedOutput.reverse();
|
|
50
56
|
}
|
|
57
|
+
|
|
58
|
+
function canCommandReturnNonZeroOnSuccess(args) {
|
|
59
|
+
if (args.length < 2) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// `npm audit fix --dry-run` can return exit code 1 when it succesfully ran and
|
|
64
|
+
// there were vulnerabilities that could be fixed
|
|
65
|
+
return args[0] === "audit" && args[1] === "fix";
|
|
66
|
+
}
|
|
@@ -18,7 +18,7 @@ export function runNpm(args) {
|
|
|
18
18
|
|
|
19
19
|
export function dryRunNpmCommandAndOutput(args) {
|
|
20
20
|
try {
|
|
21
|
-
const npmCommand = `npm ${args.join(" ")} --dry-run`;
|
|
21
|
+
const npmCommand = `npm ${args.join(" ")} --ignore-scripts --dry-run`;
|
|
22
22
|
const output = execSync(npmCommand, { stdio: "pipe" });
|
|
23
23
|
return { status: 0, output: output.toString() };
|
|
24
24
|
} catch (error) {
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { spawnSync } from "child_process";
|
|
2
1
|
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { safeSpawnSync } from "../../utils/safeSpawn.js";
|
|
3
3
|
|
|
4
4
|
export function runPnpmCommand(args, toolName = "pnpm") {
|
|
5
5
|
try {
|
|
6
6
|
let result;
|
|
7
|
-
|
|
8
7
|
if (toolName === "pnpm") {
|
|
9
|
-
result =
|
|
8
|
+
result = safeSpawnSync("pnpm", args, { stdio: "inherit" });
|
|
10
9
|
} else if (toolName === "pnpx") {
|
|
11
|
-
result =
|
|
10
|
+
result = safeSpawnSync("pnpx", args, { stdio: "inherit" });
|
|
12
11
|
} else {
|
|
13
12
|
throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
return { status: result.status };
|
|
18
|
-
}
|
|
15
|
+
return { status: result.status };
|
|
19
16
|
} catch (error) {
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
if (error.status) {
|
|
18
|
+
return { status: error.status };
|
|
19
|
+
} else {
|
|
20
|
+
ui.writeError("Error executing command:", error.message);
|
|
21
|
+
return { status: 1 };
|
|
22
|
+
}
|
|
22
23
|
}
|
|
23
|
-
return { status: 0 };
|
|
24
24
|
}
|
package/src/scanning/index.js
CHANGED
|
@@ -21,7 +21,9 @@ export async function scanCommand(args) {
|
|
|
21
21
|
|
|
22
22
|
let timedOut = false;
|
|
23
23
|
|
|
24
|
-
const spinner = ui.startProcess(
|
|
24
|
+
const spinner = ui.startProcess(
|
|
25
|
+
"Safe-chain: Scanning for malicious packages..."
|
|
26
|
+
);
|
|
25
27
|
let audit;
|
|
26
28
|
|
|
27
29
|
await Promise.race([
|
|
@@ -37,12 +39,14 @@ export async function scanCommand(args) {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
if (changes.length > 0) {
|
|
40
|
-
spinner.setText(
|
|
42
|
+
spinner.setText(
|
|
43
|
+
`Safe-chain: Scanning ${changes.length} package(s)...`
|
|
44
|
+
);
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
audit = await auditChanges(changes);
|
|
44
48
|
} catch (error) {
|
|
45
|
-
spinner.fail(`Error while scanning
|
|
49
|
+
spinner.fail(`Safe-chain: Error while scanning.`);
|
|
46
50
|
throw error;
|
|
47
51
|
}
|
|
48
52
|
})(),
|
|
@@ -52,12 +56,12 @@ export async function scanCommand(args) {
|
|
|
52
56
|
]);
|
|
53
57
|
|
|
54
58
|
if (timedOut) {
|
|
55
|
-
spinner.fail("Timeout exceeded while scanning.");
|
|
59
|
+
spinner.fail("Safe-chain: Timeout exceeded while scanning.");
|
|
56
60
|
throw new Error("Timeout exceeded while scanning npm install command.");
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
if (!audit || audit.isAllowed) {
|
|
60
|
-
spinner.succeed("No malicious packages detected.");
|
|
64
|
+
spinner.succeed("Safe-chain: No malicious packages detected.");
|
|
61
65
|
} else {
|
|
62
66
|
printMaliciousChanges(audit.disallowedChanges, spinner);
|
|
63
67
|
await onMalwareFound();
|
|
@@ -65,7 +69,7 @@ export async function scanCommand(args) {
|
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
function printMaliciousChanges(changes, spinner) {
|
|
68
|
-
spinner.fail(chalk.bold("Malicious changes detected:"));
|
|
72
|
+
spinner.fail("Safe-chain: " + chalk.bold("Malicious changes detected:"));
|
|
69
73
|
|
|
70
74
|
for (const change of changes) {
|
|
71
75
|
ui.writeInformation(` - ${change.name}@${change.version}`);
|
|
@@ -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" },
|
|
@@ -12,6 +13,22 @@ export const knownAikidoTools = [
|
|
|
12
13
|
// and add the documentation for the new tool in the README.md
|
|
13
14
|
];
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Returns a formatted string listing all supported package managers.
|
|
18
|
+
* Example: "npm, npx, yarn, pnpm, and pnpx commands"
|
|
19
|
+
*/
|
|
20
|
+
export function getPackageManagerList() {
|
|
21
|
+
const tools = knownAikidoTools.map(t => t.tool);
|
|
22
|
+
if (tools.length <= 1) {
|
|
23
|
+
return `${tools[0] || ''} commands`;
|
|
24
|
+
}
|
|
25
|
+
if (tools.length === 2) {
|
|
26
|
+
return `${tools[0]} and ${tools[1]} commands`;
|
|
27
|
+
}
|
|
28
|
+
const lastTool = tools.pop();
|
|
29
|
+
return `${tools.join(', ')}, and ${lastTool} commands`;
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
export function doesExecutableExistOnSystem(executableName) {
|
|
16
33
|
if (os.platform() === "win32") {
|
|
17
34
|
const result = spawnSync("where", [executableName], { stdio: "ignore" });
|
|
@@ -22,15 +39,17 @@ export function doesExecutableExistOnSystem(executableName) {
|
|
|
22
39
|
}
|
|
23
40
|
}
|
|
24
41
|
|
|
25
|
-
export function removeLinesMatchingPattern(filePath, pattern) {
|
|
42
|
+
export function removeLinesMatchingPattern(filePath, pattern, eol) {
|
|
26
43
|
if (!fs.existsSync(filePath)) {
|
|
27
44
|
return;
|
|
28
45
|
}
|
|
29
46
|
|
|
47
|
+
eol = eol || os.EOL;
|
|
48
|
+
|
|
30
49
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
31
|
-
const lines = fileContent.split(/[\r\n\u2028\u2029]
|
|
50
|
+
const lines = fileContent.split(/[\r\n\u2028\u2029]/);
|
|
32
51
|
const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern));
|
|
33
|
-
fs.writeFileSync(filePath, updatedLines.join(
|
|
52
|
+
fs.writeFileSync(filePath, updatedLines.join(eol), "utf-8");
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
const maxLineLength = 100;
|
|
@@ -43,12 +62,17 @@ function shouldRemoveLine(line, pattern) {
|
|
|
43
62
|
|
|
44
63
|
if (line.length > maxLineLength) {
|
|
45
64
|
// safe-chain only adds lines shorter than maxLineLength
|
|
46
|
-
// so if the line is longer, it must be from a different
|
|
65
|
+
// so if the line is longer, it must be from a different
|
|
47
66
|
// source and could be dangerous to remove
|
|
48
67
|
return false;
|
|
49
68
|
}
|
|
50
69
|
|
|
51
|
-
if (
|
|
70
|
+
if (
|
|
71
|
+
line.includes("\n") ||
|
|
72
|
+
line.includes("\r") ||
|
|
73
|
+
line.includes("\u2028") ||
|
|
74
|
+
line.includes("\u2029")
|
|
75
|
+
) {
|
|
52
76
|
// If the line contains newlines, something has gone wrong in splitting
|
|
53
77
|
// \u2028 and \u2029 are Unicode line separator characters (line and paragraph separators)
|
|
54
78
|
return false;
|
|
@@ -57,12 +81,25 @@ function shouldRemoveLine(line, pattern) {
|
|
|
57
81
|
return true;
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
export function addLineToFile(filePath, line) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
export function addLineToFile(filePath, line, eol) {
|
|
85
|
+
createFileIfNotExists(filePath);
|
|
86
|
+
|
|
87
|
+
eol = eol || os.EOL;
|
|
64
88
|
|
|
65
89
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
66
|
-
const updatedContent = fileContent +
|
|
90
|
+
const updatedContent = fileContent + eol + line + eol;
|
|
67
91
|
fs.writeFileSync(filePath, updatedContent, "utf-8");
|
|
68
92
|
}
|
|
93
|
+
|
|
94
|
+
function createFileIfNotExists(filePath) {
|
|
95
|
+
if (fs.existsSync(filePath)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const dir = path.dirname(filePath);
|
|
100
|
+
if (!fs.existsSync(dir)) {
|
|
101
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fs.writeFileSync(filePath, "", "utf-8");
|
|
105
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain
|
|
3
|
+
# This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments
|
|
4
|
+
|
|
5
|
+
# Function to remove shim from PATH (POSIX-compliant)
|
|
6
|
+
remove_shim_from_path() {
|
|
7
|
+
echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if command -v {{AIKIDO_COMMAND}} >/dev/null 2>&1; then
|
|
11
|
+
# Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
|
|
12
|
+
PATH=$(remove_shim_from_path) exec {{AIKIDO_COMMAND}} "$@"
|
|
13
|
+
else
|
|
14
|
+
# Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory)
|
|
15
|
+
original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})
|
|
16
|
+
if [ -n "$original_cmd" ]; then
|
|
17
|
+
exec "$original_cmd" "$@"
|
|
18
|
+
else
|
|
19
|
+
echo "Error: Could not find original {{PACKAGE_MANAGER}}" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
fi
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain
|
|
3
|
+
REM This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments
|
|
4
|
+
|
|
5
|
+
REM Remove shim directory from PATH to prevent infinite loops
|
|
6
|
+
set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims"
|
|
7
|
+
call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
|
|
8
|
+
|
|
9
|
+
REM Check if aikido command is available with clean PATH
|
|
10
|
+
set "PATH=%CLEAN_PATH%" & where {{AIKIDO_COMMAND}} >nul 2>&1
|
|
11
|
+
if %errorlevel%==0 (
|
|
12
|
+
REM Call aikido command with clean PATH
|
|
13
|
+
set "PATH=%CLEAN_PATH%" & {{AIKIDO_COMMAND}} %*
|
|
14
|
+
) else (
|
|
15
|
+
REM Find the original command with clean PATH
|
|
16
|
+
for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where {{PACKAGE_MANAGER}} 2^>nul') do (
|
|
17
|
+
"%%i" %*
|
|
18
|
+
goto :eof
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
REM If we get here, original command was not found
|
|
22
|
+
echo Error: Could not find original {{PACKAGE_MANAGER}} >&2
|
|
23
|
+
exit /b 1
|
|
24
|
+
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ui } from "../environment/userInteraction.js";
|
|
3
|
+
import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Loops over the detected shells and calls the setup function for each.
|
|
11
|
+
*/
|
|
12
|
+
export async function setupCi() {
|
|
13
|
+
ui.writeInformation(
|
|
14
|
+
chalk.bold("Setting up shell aliases.") +
|
|
15
|
+
` This will wrap safe-chain around ${getPackageManagerList()}.`
|
|
16
|
+
);
|
|
17
|
+
ui.emptyLine();
|
|
18
|
+
|
|
19
|
+
const shimsDir = path.join(os.homedir(), ".safe-chain", "shims");
|
|
20
|
+
// Create the shims directory if it doesn't exist
|
|
21
|
+
if (!fs.existsSync(shimsDir)) {
|
|
22
|
+
fs.mkdirSync(shimsDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
createShims(shimsDir);
|
|
26
|
+
ui.writeInformation(`Created shims in ${shimsDir}`);
|
|
27
|
+
modifyPathForCi(shimsDir);
|
|
28
|
+
ui.writeInformation(`Added shims directory to PATH for CI environments.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createUnixShims(shimsDir) {
|
|
32
|
+
// Read the template file
|
|
33
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
34
|
+
const __dirname = path.dirname(__filename);
|
|
35
|
+
const templatePath = path.resolve(
|
|
36
|
+
__dirname,
|
|
37
|
+
"path-wrappers",
|
|
38
|
+
"templates",
|
|
39
|
+
"unix-wrapper.template.sh"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(templatePath)) {
|
|
43
|
+
ui.writeError(`Template file not found: ${templatePath}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const template = fs.readFileSync(templatePath, "utf-8");
|
|
48
|
+
|
|
49
|
+
// Create a shim for each tool
|
|
50
|
+
for (const toolInfo of knownAikidoTools) {
|
|
51
|
+
const shimContent = template
|
|
52
|
+
.replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
|
|
53
|
+
.replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
|
|
54
|
+
|
|
55
|
+
const shimPath = path.join(shimsDir, toolInfo.tool);
|
|
56
|
+
fs.writeFileSync(shimPath, shimContent, "utf-8");
|
|
57
|
+
|
|
58
|
+
// Make the shim executable on Unix systems
|
|
59
|
+
fs.chmodSync(shimPath, 0o755);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ui.writeInformation(
|
|
63
|
+
`Created ${knownAikidoTools.length} Unix shim(s) in ${shimsDir}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createWindowsShims(shimsDir) {
|
|
68
|
+
// Read the template file
|
|
69
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
70
|
+
const __dirname = path.dirname(__filename);
|
|
71
|
+
const templatePath = path.resolve(
|
|
72
|
+
__dirname,
|
|
73
|
+
"path-wrappers",
|
|
74
|
+
"templates",
|
|
75
|
+
"windows-wrapper.template.cmd"
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(templatePath)) {
|
|
79
|
+
ui.writeError(`Windows template file not found: ${templatePath}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const template = fs.readFileSync(templatePath, "utf-8");
|
|
84
|
+
|
|
85
|
+
// Create a shim for each tool
|
|
86
|
+
for (const toolInfo of knownAikidoTools) {
|
|
87
|
+
const shimContent = template
|
|
88
|
+
.replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
|
|
89
|
+
.replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
|
|
90
|
+
|
|
91
|
+
const shimPath = path.join(shimsDir, `${toolInfo.tool}.cmd`);
|
|
92
|
+
fs.writeFileSync(shimPath, shimContent, "utf-8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
ui.writeInformation(
|
|
96
|
+
`Created ${knownAikidoTools.length} Windows shim(s) in ${shimsDir}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createShims(shimsDir) {
|
|
101
|
+
if (os.platform() === "win32") {
|
|
102
|
+
createWindowsShims(shimsDir);
|
|
103
|
+
} else {
|
|
104
|
+
createUnixShims(shimsDir);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function modifyPathForCi(shimsDir) {
|
|
109
|
+
if (process.env.GITHUB_PATH) {
|
|
110
|
+
// In GitHub Actions, append the shims directory to GITHUB_PATH
|
|
111
|
+
fs.appendFileSync(process.env.GITHUB_PATH, shimsDir + os.EOL, "utf-8");
|
|
112
|
+
ui.writeInformation(
|
|
113
|
+
`Added shims directory to GITHUB_PATH for GitHub Actions.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (process.env.TF_BUILD) {
|
|
118
|
+
// In Azure Pipelines, prepending the path is done via a logging command:
|
|
119
|
+
// ##vso[task.prependpath]/path/to/add
|
|
120
|
+
// Logging this to stdout will cause the Azure Pipelines agent to pick it up
|
|
121
|
+
ui.writeInformation("##vso[task.prependpath]" + shimsDir);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { ui } from "../environment/userInteraction.js";
|
|
3
3
|
import { detectShells } from "./shellDetection.js";
|
|
4
|
-
import { knownAikidoTools } from "./helpers.js";
|
|
4
|
+
import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import os from "os";
|
|
7
7
|
import path from "path";
|
|
@@ -13,7 +13,7 @@ import { fileURLToPath } from "url";
|
|
|
13
13
|
export async function setup() {
|
|
14
14
|
ui.writeInformation(
|
|
15
15
|
chalk.bold("Setting up shell aliases.") +
|
|
16
|
-
|
|
16
|
+
` This will wrap safe-chain around ${getPackageManagerList()}.`
|
|
17
17
|
);
|
|
18
18
|
ui.emptyLine();
|
|
19
19
|
|
|
@@ -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;
|
|
@@ -47,11 +47,15 @@ function pnpx
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
function npm
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
# If args is just -v or --version and nothing else, just run the `npm -v` command
|
|
51
|
+
# This is because nvm uses this to check the version of npm
|
|
52
|
+
set argc (count $argv)
|
|
53
|
+
if test $argc -eq 1
|
|
54
|
+
switch $argv[1]
|
|
55
|
+
case "-v" "--version"
|
|
56
|
+
command npm $argv
|
|
57
|
+
return
|
|
58
|
+
end
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
wrapSafeChainCommand "npm" "aikido-npm" $argv
|
|
@@ -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(
|
|
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(
|
|
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;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { ui } from "../environment/userInteraction.js";
|
|
3
3
|
import { detectShells } from "./shellDetection.js";
|
|
4
|
-
import { knownAikidoTools } from "./helpers.js";
|
|
4
|
+
import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
|
|
5
5
|
|
|
6
6
|
export async function teardown() {
|
|
7
7
|
ui.writeInformation(
|
|
8
8
|
chalk.bold("Removing shell aliases.") +
|
|
9
|
-
|
|
9
|
+
` This will remove safe-chain aliases for ${getPackageManagerList()}.`
|
|
10
10
|
);
|
|
11
11
|
ui.emptyLine();
|
|
12
12
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { spawnSync, spawn } from "child_process";
|
|
2
|
+
|
|
3
|
+
function escapeArg(arg) {
|
|
4
|
+
// If argument contains spaces or quotes, wrap in double quotes and escape double quotes
|
|
5
|
+
if (arg.includes(" ") || arg.includes('"') || arg.includes("'")) {
|
|
6
|
+
return '"' + arg.replaceAll('"', '\\"') + '"';
|
|
7
|
+
}
|
|
8
|
+
return arg;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildCommand(command, args) {
|
|
12
|
+
const escapedArgs = args.map(escapeArg);
|
|
13
|
+
return `${command} ${escapedArgs.join(" ")}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function safeSpawnSync(command, args, options = {}) {
|
|
17
|
+
const fullCommand = buildCommand(command, args);
|
|
18
|
+
return spawnSync(fullCommand, { ...options, shell: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function safeSpawn(command, args, options = {}) {
|
|
22
|
+
const fullCommand = buildCommand(command, args);
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const child = spawn(fullCommand, { ...options, shell: true });
|
|
25
|
+
|
|
26
|
+
child.on("close", (code) => {
|
|
27
|
+
resolve({
|
|
28
|
+
status: code,
|
|
29
|
+
stdout: Buffer.from(""),
|
|
30
|
+
stderr: Buffer.from(""),
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.on("error", (error) => {
|
|
35
|
+
reject(error);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|