@aikidosec/safe-chain 1.1.10 → 1.2.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.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
+ ![Aikido Safe Chain](./docs/banner.svg)
2
+
1
3
  # Aikido Safe Chain
2
4
 
5
+ [![NPM Version](https://img.shields.io/npm/v/%40aikidosec%2Fsafe-chain?style=flat-square)](https://www.npmjs.com/package/@aikidosec/safe-chain)
6
+ [![NPM Downloads](https://img.shields.io/npm/dw/%40aikidosec%2Fsafe-chain?style=flat-square)](https://www.npmjs.com/package/@aikidosec/safe-chain)
7
+
3
8
  - ✅ **Block malware on developer laptops and CI/CD**
4
9
  - ✅ **Supports npm and PyPI** more package managers coming
5
10
  - ✅ **Blocks packages newer than 24 hours** without breaking your build
@@ -22,29 +27,45 @@ Aikido Safe Chain works on Node.js version 16 and above and supports the followi
22
27
 
23
28
  ## Installation
24
29
 
25
- Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
30
+ Installing the Aikido Safe Chain is easy with our one-line installer.
26
31
 
27
- 1. **Install the Aikido Safe Chain package globally** using npm:
28
- ```shell
29
- npm install -g @aikidosec/safe-chain
30
- ```
31
- 2. **Setup the shell integration** by running:
32
+ > ⚠️ **Already installed via npm?** See the [migration guide](docs/npm-to-binary-migration.md) to switch to the binary version.
32
33
 
33
- ```shell
34
- safe-chain setup
35
- ```
34
+ ### Unix/Linux/macOS
36
35
 
37
- To enable Python (pip/pip3/uv) support (beta), use the `--include-python` flag:
36
+ **Default installation (JavaScript packages only):**
38
37
 
39
- ```shell
40
- safe-chain setup --include-python
41
- ```
38
+ ```shell
39
+ curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh
40
+ ```
41
+
42
+ **Include Python support (pip/pip3/uv):**
43
+
44
+ ```shell
45
+ curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --include-python
46
+ ```
47
+
48
+ ### Windows (PowerShell)
49
+
50
+ **Default installation (JavaScript packages only):**
42
51
 
43
- 3. **❗Restart your terminal** to start using the Aikido Safe Chain.
52
+ ```powershell
53
+ iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing)
54
+ ```
55
+
56
+ **Include Python support (pip/pip3/uv):**
57
+
58
+ ```powershell
59
+ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -includepython"
60
+ ```
61
+
62
+ ### Verify the installation
63
+
64
+ 1. **❗Restart your terminal** to start using the Aikido Safe Chain.
44
65
 
45
66
  - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available.
46
67
 
47
- 4. **Verify the installation** by running one of the following commands:
68
+ 2. **Verify the installation** by running one of the following commands:
48
69
 
49
70
  For JavaScript/Node.js:
50
71
 
@@ -52,7 +73,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
52
73
  npm install safe-chain-test
53
74
  ```
54
75
 
55
- For Python (beta):
76
+ For Python (if you enabled Python support):
56
77
 
57
78
  ```shell
58
79
  pip3 install safe-chain-pi-test
@@ -163,21 +184,37 @@ You can protect your CI/CD pipelines from malicious packages by integrating Aiki
163
184
 
164
185
  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.
165
186
 
166
- ## Setup
187
+ ## Installation for CI/CD
188
+
189
+ Use the `--ci` flag to automatically configure Aikido Safe Chain for CI/CD environments. This sets up executable shims in the PATH instead of shell aliases.
190
+
191
+ ### Unix/Linux/macOS (GitHub Actions, Azure Pipelines, etc.)
167
192
 
168
- To use Aikido Safe Chain in CI/CD environments, run the following command after installing the package:
193
+ **JavaScript only:**
169
194
 
170
195
  ```shell
171
- safe-chain setup-ci
196
+ curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci
172
197
  ```
173
198
 
174
- To enable Python (pip/pip3/uv) support (beta) in CI/CD, use the `--include-python` flag:
199
+ **With Python support:**
175
200
 
176
201
  ```shell
177
- safe-chain setup-ci --include-python
202
+ curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python
178
203
  ```
179
204
 
180
- This automatically configures your CI environment to use Aikido Safe Chain for all package manager commands.
205
+ ### Windows (Azure Pipelines, etc.)
206
+
207
+ **JavaScript only:**
208
+
209
+ ```powershell
210
+ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci"
211
+ ```
212
+
213
+ **With Python support:**
214
+
215
+ ```powershell
216
+ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci -includepython"
217
+ ```
181
218
 
182
219
  ## Supported Platforms
183
220
 
@@ -193,16 +230,15 @@ This automatically configures your CI environment to use Aikido Safe Chain for a
193
230
  node-version: "22"
194
231
  cache: "npm"
195
232
 
196
- - name: Setup safe-chain
197
- run: |
198
- npm i -g @aikidosec/safe-chain
199
- safe-chain setup-ci
233
+ - name: Install safe-chain
234
+ run: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python
200
235
 
201
236
  - name: Install dependencies
202
- run: |
203
- npm ci
237
+ run: npm ci
204
238
  ```
205
239
 
240
+ > **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv) support.
241
+
206
242
  ## Azure DevOps Example
207
243
 
208
244
  ```yaml
@@ -211,14 +247,13 @@ This automatically configures your CI environment to use Aikido Safe Chain for a
211
247
  versionSpec: "22.x"
212
248
  displayName: "Install Node.js"
213
249
 
214
- - script: |
215
- npm i -g @aikidosec/safe-chain
216
- safe-chain setup-ci
217
- displayName: "Install safe chain"
250
+ - script: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python
251
+ displayName: "Install safe-chain"
218
252
 
219
- - script: |
220
- npm ci
221
- displayName: "npm install and build"
253
+ - script: npm ci
254
+ displayName: "Install dependencies"
222
255
  ```
223
256
 
257
+ > **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv) support.
258
+
224
259
  After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
package/bin/aikido-bun.js CHANGED
@@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
7
7
  setEcoSystem(ECOSYSTEM_JS);
8
8
  const packageManagerName = "bun";
9
9
  initializePackageManager(packageManagerName);
10
- var exitCode = await main(process.argv.slice(2));
11
10
 
12
- process.exit(exitCode);
11
+ (async () => {
12
+ var exitCode = await main(process.argv.slice(2));
13
+ process.exit(exitCode);
14
+ })();
@@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
7
7
  setEcoSystem(ECOSYSTEM_JS);
8
8
  const packageManagerName = "bunx";
9
9
  initializePackageManager(packageManagerName);
10
- var exitCode = await main(process.argv.slice(2));
11
10
 
12
- process.exit(exitCode);
11
+ (async () => {
12
+ var exitCode = await main(process.argv.slice(2));
13
+ process.exit(exitCode);
14
+ })();
package/bin/aikido-npm.js CHANGED
@@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
7
7
  setEcoSystem(ECOSYSTEM_JS);
8
8
  const packageManagerName = "npm";
9
9
  initializePackageManager(packageManagerName);
10
- var exitCode = await main(process.argv.slice(2));
11
10
 
12
- process.exit(exitCode);
11
+ (async () => {
12
+ var exitCode = await main(process.argv.slice(2));
13
+ process.exit(exitCode);
14
+ })();
package/bin/aikido-npx.js CHANGED
@@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
7
7
  setEcoSystem(ECOSYSTEM_JS);
8
8
  const packageManagerName = "npx";
9
9
  initializePackageManager(packageManagerName);
10
- var exitCode = await main(process.argv.slice(2));
11
10
 
12
- process.exit(exitCode);
11
+ (async () => {
12
+ var exitCode = await main(process.argv.slice(2));
13
+ process.exit(exitCode);
14
+ })();
package/bin/aikido-pip.js CHANGED
@@ -13,6 +13,8 @@ setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
13
13
 
14
14
  initializePackageManager(PIP_PACKAGE_MANAGER);
15
15
 
16
- // Pass through only user-supplied pip args
17
- var exitCode = await main(process.argv.slice(2));
18
- process.exit(exitCode);
16
+ (async () => {
17
+ // Pass through only user-supplied pip args
18
+ var exitCode = await main(process.argv.slice(2));
19
+ process.exit(exitCode);
20
+ })();
@@ -14,6 +14,8 @@ setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
14
14
  // Create package manager
15
15
  initializePackageManager(PIP_PACKAGE_MANAGER);
16
16
 
17
- // Pass through only user-supplied pip args
18
- var exitCode = await main(process.argv.slice(2));
19
- process.exit(exitCode);
17
+ (async () => {
18
+ // Pass through only user-supplied pip args
19
+ var exitCode = await main(process.argv.slice(2));
20
+ process.exit(exitCode);
21
+ })();
@@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
7
7
  setEcoSystem(ECOSYSTEM_JS);
8
8
  const packageManagerName = "pnpm";
9
9
  initializePackageManager(packageManagerName);
10
- var exitCode = await main(process.argv.slice(2));
11
10
 
12
- process.exit(exitCode);
11
+ (async () => {
12
+ var exitCode = await main(process.argv.slice(2));
13
+ process.exit(exitCode);
14
+ })();
@@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
7
7
  setEcoSystem(ECOSYSTEM_JS);
8
8
  const packageManagerName = "pnpx";
9
9
  initializePackageManager(packageManagerName);
10
- var exitCode = await main(process.argv.slice(2));
11
10
 
12
- process.exit(exitCode);
11
+ (async () => {
12
+ var exitCode = await main(process.argv.slice(2));
13
+ process.exit(exitCode);
14
+ })();
@@ -11,18 +11,20 @@ setEcoSystem(ECOSYSTEM_PY);
11
11
  // Strip nodejs and wrapper script from args
12
12
  let argv = process.argv.slice(2);
13
13
 
14
- if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
15
- setEcoSystem(ECOSYSTEM_PY);
16
- setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP);
17
- initializePackageManager(PIP_PACKAGE_MANAGER);
14
+ (async () => {
15
+ if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
16
+ setEcoSystem(ECOSYSTEM_PY);
17
+ setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP);
18
+ initializePackageManager(PIP_PACKAGE_MANAGER);
18
19
 
19
- // Strip off the '-m pip' or '-m pip3' from the args
20
- argv = argv.slice(2);
20
+ // Strip off the '-m pip' or '-m pip3' from the args
21
+ argv = argv.slice(2);
21
22
 
22
- var exitCode = await main(argv);
23
- process.exit(exitCode);
24
- } else {
25
- // Forward to real python binary for non-pip flows
26
- const { spawn } = await import('child_process');
27
- spawn('python', argv, { stdio: 'inherit' });
28
- }
23
+ var exitCode = await main(argv);
24
+ process.exit(exitCode);
25
+ } else {
26
+ // Forward to real python binary for non-pip flows
27
+ const { spawn } = await import('child_process');
28
+ spawn('python', argv, { stdio: 'inherit' });
29
+ }
30
+ })();
@@ -11,18 +11,20 @@ setEcoSystem(ECOSYSTEM_PY);
11
11
  // Strip nodejs and wrapper script from args
12
12
  let argv = process.argv.slice(2);
13
13
 
14
- if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
15
- setEcoSystem(ECOSYSTEM_PY);
16
- setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP);
17
- initializePackageManager(PIP_PACKAGE_MANAGER);
14
+ (async () => {
15
+ if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
16
+ setEcoSystem(ECOSYSTEM_PY);
17
+ setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP);
18
+ initializePackageManager(PIP_PACKAGE_MANAGER);
18
19
 
19
- // Strip off the '-m pip' or '-m pip3' from the args
20
- argv = argv.slice(2);
20
+ // Strip off the '-m pip' or '-m pip3' from the args
21
+ argv = argv.slice(2);
21
22
 
22
- var exitCode = await main(argv);
23
- process.exit(exitCode);
24
- } else {
25
- // Forward to real python3 binary for non-pip flows
26
- const { spawn } = await import('child_process');
27
- spawn('python3', argv, { stdio: 'inherit' });
28
- }
23
+ var exitCode = await main(argv);
24
+ process.exit(exitCode);
25
+ } else {
26
+ // Forward to real python3 binary for non-pip flows
27
+ const { spawn } = await import('child_process');
28
+ spawn('python3', argv, { stdio: 'inherit' });
29
+ }
30
+ })();
package/bin/aikido-uv.js CHANGED
@@ -9,6 +9,8 @@ setEcoSystem(ECOSYSTEM_PY);
9
9
 
10
10
  initializePackageManager("uv");
11
11
 
12
- // Pass through only user-supplied uv args
13
- var exitCode = await main(process.argv.slice(2));
14
- process.exit(exitCode);
12
+ (async () => {
13
+ // Pass through only user-supplied uv args
14
+ var exitCode = await main(process.argv.slice(2));
15
+ process.exit(exitCode);
16
+ })();
@@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
7
7
  setEcoSystem(ECOSYSTEM_JS);
8
8
  const packageManagerName = "yarn";
9
9
  initializePackageManager(packageManagerName);
10
- var exitCode = await main(process.argv.slice(2));
11
10
 
12
- process.exit(exitCode);
11
+ (async () => {
12
+ var exitCode = await main(process.argv.slice(2));
13
+ process.exit(exitCode);
14
+ })();
package/bin/safe-chain.js CHANGED
@@ -1,12 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import chalk from "chalk";
4
- import { createRequire } from "module";
5
4
  import { ui } from "../src/environment/userInteraction.js";
6
5
  import { setup } from "../src/shell-integration/setup.js";
7
6
  import { teardown } from "../src/shell-integration/teardown.js";
8
7
  import { setupCi } from "../src/shell-integration/setup-ci.js";
9
8
  import { initializeCliArguments } from "../src/config/cliArguments.js";
9
+ import { setEcoSystem } from "../src/config/settings.js";
10
+ import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
11
+ import { main } from "../src/main.js";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ import fs from "fs";
15
+ import { knownAikidoTools } from "../src/shell-integration/helpers.js";
16
+ import {
17
+ PIP_INVOCATIONS,
18
+ PIP_PACKAGE_MANAGER,
19
+ setCurrentPipInvocation,
20
+ } from "../src/packagemanager/pip/pipSettings.js";
21
+
22
+ /** @type {string} */
23
+ // This checks the current file's dirname in a way that's compatible with:
24
+ // - Modulejs (import.meta.url)
25
+ // - ES modules (__dirname)
26
+ // This is needed because safe-chain's npm package is built using ES modules,
27
+ // but building the binaries requires commonjs.
28
+ let dirname;
29
+ if (import.meta.url) {
30
+ const filename = fileURLToPath(import.meta.url);
31
+ dirname = path.dirname(filename);
32
+ } else {
33
+ dirname = __dirname;
34
+ }
10
35
 
11
36
  if (process.argv.length < 3) {
12
37
  ui.writeError("No command provided. Please provide a command to execute.");
@@ -19,19 +44,35 @@ initializeCliArguments(process.argv);
19
44
 
20
45
  const command = process.argv[2];
21
46
 
22
- if (command === "help" || command === "--help" || command === "-h") {
47
+ const tool = knownAikidoTools.find((tool) => tool.tool === command);
48
+
49
+ if (tool && tool.internalPackageManagerName === PIP_PACKAGE_MANAGER) {
50
+ (async function () {
51
+ await executePip(tool);
52
+ })();
53
+ } else if (tool) {
54
+ const args = process.argv.slice(3);
55
+
56
+ setEcoSystem(tool.ecoSystem);
57
+ initializePackageManager(tool.internalPackageManagerName);
58
+
59
+ (async () => {
60
+ var exitCode = await main(args);
61
+ process.exit(exitCode);
62
+ })();
63
+ } else if (command === "help" || command === "--help" || command === "-h") {
23
64
  writeHelp();
24
65
  process.exit(0);
25
- }
26
-
27
- if (command === "setup") {
66
+ } else if (command === "setup") {
28
67
  setup();
29
68
  } else if (command === "teardown") {
30
69
  teardown();
31
70
  } else if (command === "setup-ci") {
32
71
  setupCi();
33
72
  } else if (command === "--version" || command === "-v" || command === "-v") {
34
- ui.writeInformation(`Current safe-chain version: ${getVersion()}`);
73
+ (async () => {
74
+ ui.writeInformation(`Current safe-chain version: ${await getVersion()}`);
75
+ })();
35
76
  } else {
36
77
  ui.writeError(`Unknown command: ${command}.`);
37
78
  ui.emptyLine();
@@ -87,8 +128,63 @@ function writeHelp() {
87
128
  ui.emptyLine();
88
129
  }
89
130
 
90
- function getVersion() {
91
- const require = createRequire(import.meta.url);
92
- const packageJson = require("../package.json");
93
- return packageJson.version;
131
+ async function getVersion() {
132
+ const packageJsonPath = path.join(dirname, "..", "package.json");
133
+
134
+ const data = await fs.promises.readFile(packageJsonPath);
135
+ const json = JSON.parse(data.toString("utf8"));
136
+
137
+ if (json && json.version) {
138
+ return json.version;
139
+ }
140
+
141
+ return "0.0.0";
142
+ }
143
+
144
+ /**
145
+ * @param {import("../src/shell-integration/helpers.js").AikidoTool} tool
146
+ */
147
+ async function executePip(tool) {
148
+ // Scanners for pip / pip3 / python / python3 use a slightly different approach:
149
+ // - They all use the same PIP_PACKAGE_MANAGER internally, but need some setup to be able to do so
150
+ // - It needs to set which tool to run (pip / pip3 / python / python3)
151
+ // - For python and python3, the -m pip/pip3 args are removed and later added again by the package manager
152
+ // - Python / python3 skips safe-chain if not being run with -m pip or -m pip3
153
+
154
+ let args = process.argv.slice(3);
155
+ setEcoSystem(tool.ecoSystem);
156
+ initializePackageManager(PIP_PACKAGE_MANAGER);
157
+
158
+ let shouldSkip = false;
159
+ if (tool.tool === "pip") {
160
+ setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
161
+ } else if (tool.tool === "pip3") {
162
+ setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
163
+ } else if (tool.tool === "python") {
164
+ if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) {
165
+ setCurrentPipInvocation(
166
+ args[1] === "pip3" ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP
167
+ );
168
+ args = args.slice(2);
169
+ } else {
170
+ shouldSkip = true;
171
+ }
172
+ } else if (tool.tool === "python3") {
173
+ if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) {
174
+ setCurrentPipInvocation(
175
+ args[1] === "pip3" ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP
176
+ );
177
+ args = args.slice(2);
178
+ } else {
179
+ shouldSkip = true;
180
+ }
181
+ }
182
+
183
+ if (shouldSkip) {
184
+ const { spawn } = await import("child_process");
185
+ spawn(tool.tool, args, { stdio: "inherit" });
186
+ } else {
187
+ var exitCode = await main(args);
188
+ process.exit(exitCode);
189
+ }
94
190
  }