mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Merge branch 'main' into readme-update-intro
This commit is contained in:
commit
3ceed1fc4b
23 changed files with 728 additions and 853 deletions
13
README.md
13
README.md
|
|
@ -16,6 +16,7 @@ Aikido Safe Chain works on Node.js version 16 and above and supports the followi
|
||||||
- 📦 **bunx**
|
- 📦 **bunx**
|
||||||
- 📦 **pip** (beta)
|
- 📦 **pip** (beta)
|
||||||
- 📦 **pip3** (beta)
|
- 📦 **pip3** (beta)
|
||||||
|
- 📦 **uv** (beta)
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
|
|
@ -33,7 +34,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
|
||||||
safe-chain setup
|
safe-chain setup
|
||||||
```
|
```
|
||||||
|
|
||||||
To enable Python (pip/pip3) support (beta), use the `--include-python` flag:
|
To enable Python (pip/pip3/uv) support (beta), use the `--include-python` flag:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
safe-chain setup --include-python
|
safe-chain setup --include-python
|
||||||
|
|
@ -59,7 +60,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
|
||||||
|
|
||||||
- The output should show that Aikido Safe Chain is blocking the installation of these test packages as they are flagged as malware.
|
- The output should show that Aikido Safe Chain is blocking the installation of these test packages as they are flagged as malware.
|
||||||
|
|
||||||
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, or `pip3` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. It also intercepts Python module invocations for pip when available (e.g., `python -m pip install ...`, `python3 -m pip download ...`). If any malware is detected, it will prompt you to exit the command.
|
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `uv`, `pip`, or `pip3` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. It also intercepts Python module invocations for pip when available (e.g., `python -m pip install ...`, `python3 -m pip download ...`). If any malware is detected, it will prompt you to exit the command.
|
||||||
|
|
||||||
You can check the installed version by running:
|
You can check the installed version by running:
|
||||||
|
|
||||||
|
|
@ -71,17 +72,17 @@ safe-chain --version
|
||||||
|
|
||||||
### Malware Blocking
|
### Malware Blocking
|
||||||
|
|
||||||
The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry and PyPI. When you run npm, npx, yarn, pnpm, pnpx, bun, bunx, `pip`, or `pip3` commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine.
|
The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry and PyPI. When you run npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, `pip`, or `pip3` commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine.
|
||||||
|
|
||||||
### Minimum package age (npm only)
|
### Minimum package age (npm only)
|
||||||
|
|
||||||
For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours until they have been validated against malware. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can bypass this protection for specific installs using the `--safe-chain-skip-minimum-package-age` flag.
|
For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours until they have been validated against malware. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can bypass this protection for specific installs using the `--safe-chain-skip-minimum-package-age` flag.
|
||||||
|
|
||||||
⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to PyPI/pip.
|
⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to Python package managers (uv, pip, pip3).
|
||||||
|
|
||||||
### Shell Integration
|
### Shell Integration
|
||||||
|
|
||||||
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which manage the proxy server before executing the original commands. We currently support:
|
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and Python package managers (uv, pip). It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which manage the proxy server before executing the original commands. We currently support:
|
||||||
|
|
||||||
- ✅ **Bash**
|
- ✅ **Bash**
|
||||||
- ✅ **Zsh**
|
- ✅ **Zsh**
|
||||||
|
|
@ -141,7 +142,7 @@ To use Aikido Safe Chain in CI/CD environments, run the following command after
|
||||||
safe-chain setup-ci
|
safe-chain setup-ci
|
||||||
```
|
```
|
||||||
|
|
||||||
To enable Python (pip/pip3) support (beta) in CI/CD, use the `--include-python` flag:
|
To enable Python (pip/pip3/uv) support (beta) in CI/CD, use the `--include-python` flag:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
safe-chain setup-ci --include-python
|
safe-chain setup-ci --include-python
|
||||||
|
|
|
||||||
432
package-lock.json
generated
432
package-lock.json
generated
|
|
@ -19,10 +19,6 @@
|
||||||
"resolved": "packages/safe-chain",
|
"resolved": "packages/safe-chain",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@aikidosec/safe-chain-bun": {
|
|
||||||
"resolved": "packages/safe-chain-bun",
|
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"node_modules/@aikidosec/safe-chain-e2e-tests": {
|
"node_modules/@aikidosec/safe-chain-e2e-tests": {
|
||||||
"resolved": "test/e2e",
|
"resolved": "test/e2e",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -143,160 +139,6 @@
|
||||||
"node": "^18.17.0 || >=20.5.0"
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-darwin-aarch64": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-SihfZ3czKeWz6Z3m5rUDrMlarwOXjnkUg+7tIiSB9VZCFSvWEItMfdAF170eCXxZmEh7A1dw20a3lW37lkmlrA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oven/bun-darwin-x64": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-iXr4y2ap6EmME7/EDoLMxSRKAh9yswKfrHDb9sF+ExHbk1C+XsNGxMY73ckQe2w0SIH6NXz2cRMTORbZ8LNjig==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oven/bun-darwin-x64-baseline": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-3KeslC5z3vpXxluYBqh6EDwojxTSyWJQeYPJFf7y/Z5QJuAN7g33l8jrx072X8P/G8CBzU1lJky14vhhnqWd7A==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oven/bun-linux-aarch64": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-jpUFKGUpim4h4KOqI1VYYgvifZVrWNQZFrmVPfSqGb0ZzF/p5L2qc9Hy2aUL3Lo+zHMPylwbe0iLKElPYk0xoQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oven/bun-linux-aarch64-musl": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-7UoUHKACYDin3iR6kdqUrF1AOCCjTHPTv1xmzlX4rzwNQvFYSAR83AMrY7hkatKGzLYkI8EjXDAvFJpwF+ZxoA==",
|
|
||||||
"cpu": [
|
|
||||||
"aarch64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oven/bun-linux-x64": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-6RuXFaVU2ve0TVw1vfFo7ix/jh9IX7mMAEhwE2odX8EdX/ea55upiivYQ/EKeXt+Ij3STc2bCeV4vvRoEJAHdg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oven/bun-linux-x64-baseline": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-oZ5FUMfeghwbQcL9oxajsKjwVI+1GnVvxcJ3z+pifuXaLMZr25NCr5h0q2j+ZxEFL3RtL/Pyj8/HLfzGEIVAVg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oven/bun-linux-x64-musl": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-ioZjU+2yyLJXaDA8FKoy+tj/fuZKovG9EMp+n9+EG7g3MULbe5nU8gdsS/dET28WzuPlDlSkqF8EUocvg4HajQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oven/bun-linux-x64-musl-baseline": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-0NzMg4XdXgujDM2jZogiV6MgACXW0a0NfB+o6fxwmUzdmMBUk1ZMRzypUi4XKjGUe89mYcPJcVFQRRnNwzTK/Q==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oven/bun-windows-x64": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-DZVCXrZGN/B4JnVnieZin1Kxse1wOkf+Fm2hDGpZHzs27ECbw5xPMFIc0r/oCpxTc/InxuvYO9UGoOmvhFaHsQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oven/bun-windows-x64-baseline": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-sTnkLdThgsa6X8ib6eb3+zgy+CGJOibK6Th4wV2wmZFi5af6TM+digEi9i+q/X3nabGwPXm0V4vBiVpvcFilsA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@oxlint/darwin-arm64": {
|
"node_modules/@oxlint/darwin-arm64": {
|
||||||
"version": "1.22.0",
|
"version": "1.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.22.0.tgz",
|
||||||
|
|
@ -559,41 +401,6 @@
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bun": {
|
|
||||||
"version": "1.2.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/bun/-/bun-1.2.21.tgz",
|
|
||||||
"integrity": "sha512-y0lJ02dS90U3PJm+7KAKY8Se95AQvP5Xm77LouUwrpNOHpv59kBG4SK1+9iE1cAhpUaFipq+0EJ56S6MmE3row==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64",
|
|
||||||
"x64",
|
|
||||||
"aarch64"
|
|
||||||
],
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"os": [
|
|
||||||
"darwin",
|
|
||||||
"linux",
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"bun": "bin/bun.exe",
|
|
||||||
"bunx": "bin/bunx.exe"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@oven/bun-darwin-aarch64": "1.2.21",
|
|
||||||
"@oven/bun-darwin-x64": "1.2.21",
|
|
||||||
"@oven/bun-darwin-x64-baseline": "1.2.21",
|
|
||||||
"@oven/bun-linux-aarch64": "1.2.21",
|
|
||||||
"@oven/bun-linux-aarch64-musl": "1.2.21",
|
|
||||||
"@oven/bun-linux-x64": "1.2.21",
|
|
||||||
"@oven/bun-linux-x64-baseline": "1.2.21",
|
|
||||||
"@oven/bun-linux-x64-musl": "1.2.21",
|
|
||||||
"@oven/bun-linux-x64-musl-baseline": "1.2.21",
|
|
||||||
"@oven/bun-windows-x64": "1.2.21",
|
|
||||||
"@oven/bun-windows-x64-baseline": "1.2.21"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cacache": {
|
"node_modules/cacache": {
|
||||||
"version": "19.0.1",
|
"version": "19.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz",
|
||||||
|
|
@ -659,33 +466,6 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cli-cursor": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"restore-cursor": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cli-spinners": {
|
|
||||||
"version": "2.9.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
|
||||||
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|
@ -909,18 +689,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-east-asian-width": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
|
@ -1128,30 +896,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-interactive": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-unicode-supported": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
|
|
@ -1188,34 +932,6 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/log-symbols": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"is-unicode-supported": "^1.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/log-symbols/node_modules/is-unicode-supported": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "10.4.3",
|
"version": "10.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
|
@ -1274,18 +990,6 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mimic-function": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
|
@ -1515,79 +1219,6 @@
|
||||||
"node": "^18.17.0 || >=20.5.0"
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ora": {
|
|
||||||
"version": "8.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
|
|
||||||
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"cli-cursor": "^5.0.0",
|
|
||||||
"cli-spinners": "^2.9.2",
|
|
||||||
"is-interactive": "^2.0.0",
|
|
||||||
"is-unicode-supported": "^2.0.0",
|
|
||||||
"log-symbols": "^6.0.0",
|
|
||||||
"stdin-discarder": "^0.2.2",
|
|
||||||
"string-width": "^7.2.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ora/node_modules/ansi-regex": {
|
|
||||||
"version": "6.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
|
||||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ora/node_modules/emoji-regex": {
|
|
||||||
"version": "10.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
|
|
||||||
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/ora/node_modules/string-width": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"emoji-regex": "^10.3.0",
|
|
||||||
"get-east-asian-width": "^1.0.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ora/node_modules/strip-ansi": {
|
|
||||||
"version": "7.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
|
||||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": "^6.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/oxlint": {
|
"node_modules/oxlint": {
|
||||||
"version": "1.22.0",
|
"version": "1.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.22.0.tgz",
|
||||||
|
|
@ -1687,37 +1318,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/restore-cursor": {
|
|
||||||
"version": "5.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
|
||||||
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"onetime": "^7.0.0",
|
|
||||||
"signal-exit": "^4.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/restore-cursor/node_modules/onetime": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mimic-function": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/retry": {
|
"node_modules/retry": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||||
|
|
@ -1835,18 +1435,6 @@
|
||||||
"node": "^18.17.0 || >=20.5.0"
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stdin-discarder": {
|
|
||||||
"version": "0.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
|
||||||
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
|
@ -2096,14 +1684,13 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"certifi": "^14.5.15",
|
"certifi": "14.5.15",
|
||||||
"chalk": "5.4.1",
|
"chalk": "5.4.1",
|
||||||
"https-proxy-agent": "7.0.6",
|
"https-proxy-agent": "7.0.6",
|
||||||
"ini": "^6.0.0",
|
"ini": "6.0.0",
|
||||||
"make-fetch-happen": "14.0.3",
|
"make-fetch-happen": "14.0.3",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.1",
|
||||||
"npm-registry-fetch": "18.0.2",
|
"npm-registry-fetch": "18.0.2",
|
||||||
"ora": "8.2.0",
|
|
||||||
"semver": "7.7.2"
|
"semver": "7.7.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -2113,10 +1700,10 @@
|
||||||
"aikido-npx": "bin/aikido-npx.js",
|
"aikido-npx": "bin/aikido-npx.js",
|
||||||
"aikido-pip": "bin/aikido-pip.js",
|
"aikido-pip": "bin/aikido-pip.js",
|
||||||
"aikido-pip3": "bin/aikido-pip3.js",
|
"aikido-pip3": "bin/aikido-pip3.js",
|
||||||
"aikido-python": "bin/aikido-python.js",
|
|
||||||
"aikido-python3": "bin/aikido-python3.js",
|
|
||||||
"aikido-pnpm": "bin/aikido-pnpm.js",
|
"aikido-pnpm": "bin/aikido-pnpm.js",
|
||||||
"aikido-pnpx": "bin/aikido-pnpx.js",
|
"aikido-pnpx": "bin/aikido-pnpx.js",
|
||||||
|
"aikido-python": "bin/aikido-python.js",
|
||||||
|
"aikido-python3": "bin/aikido-python3.js",
|
||||||
"aikido-yarn": "bin/aikido-yarn.js",
|
"aikido-yarn": "bin/aikido-yarn.js",
|
||||||
"safe-chain": "bin/safe-chain.js"
|
"safe-chain": "bin/safe-chain.js"
|
||||||
},
|
},
|
||||||
|
|
@ -2130,17 +1717,6 @@
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/safe-chain-bun": {
|
|
||||||
"name": "@aikidosec/safe-chain-bun",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"dependencies": {
|
|
||||||
"@aikidosec/safe-chain": "file:../safe-chain"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bun": ">=1.2.21"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/safe-chain/node_modules/@types/node": {
|
"packages/safe-chain/node_modules/@types/node": {
|
||||||
"version": "18.19.130",
|
"version": "18.19.130",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"test/e2e"
|
"test/e2e"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun",
|
"test": "npm run test --workspace=packages/safe-chain",
|
||||||
"test:e2e": "npm run test --workspace=test/e2e",
|
"test:e2e": "npm run test --workspace=test/e2e",
|
||||||
"lint": "npm run lint --workspace=packages/safe-chain",
|
"lint": "npm run lint --workspace=packages/safe-chain",
|
||||||
"typecheck": "npm run typecheck --workspace=packages/safe-chain"
|
"typecheck": "npm run typecheck --workspace=packages/safe-chain"
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@aikidosec/safe-chain-bun",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"main": "src/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'"
|
|
||||||
},
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"bun": "./src/index.js",
|
|
||||||
"default": "./src/index.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"keywords": ["bun", "security", "scanner", "malware", "aikido"],
|
|
||||||
"author": "Aikido Security",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"description": "Aikido Security Scanner for Bun package manager - detects malware and security threats during package installation",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/AikidoSec/safe-chain.git",
|
|
||||||
"directory": "packages/safe-chain-bun"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@aikidosec/safe-chain": "file:../safe-chain"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bun": ">=1.2.21"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
// oxlint-disable no-console
|
|
||||||
import { auditChanges } from "@aikidosec/safe-chain/scanning";
|
|
||||||
|
|
||||||
// Bun Security Scanner for Safe-Chain
|
|
||||||
// This is the entry point for Bun's native security scanner integration
|
|
||||||
|
|
||||||
export const scanner = {
|
|
||||||
version: "1", // Our scanner is using version 1 of the bun security scanner API.
|
|
||||||
|
|
||||||
async scan({ packages }) {
|
|
||||||
const advisories = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const changes = packages.map((pkg) => ({
|
|
||||||
name: pkg.name,
|
|
||||||
version: pkg.version,
|
|
||||||
type: "add",
|
|
||||||
}));
|
|
||||||
|
|
||||||
const audit = await auditChanges(changes);
|
|
||||||
|
|
||||||
if (!audit.isAllowed) {
|
|
||||||
for (const change of audit.disallowedChanges) {
|
|
||||||
advisories.push({
|
|
||||||
level: "fatal", // Fatal will block the installation process, this is what we want for packages that contain malware.
|
|
||||||
package: change.name,
|
|
||||||
url: null,
|
|
||||||
description: `Package ${change.name}@${change.version} contains known security threats (${change.reason}). Installation blocked by Safe-Chain.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (/** @type any */ error) {
|
|
||||||
console.warn(`Safe-Chain security scan failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return advisories;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { describe, it, mock } from "node:test";
|
|
||||||
|
|
||||||
describe("Bun Scanner", async () => {
|
|
||||||
const mockAuditChanges = mock.fn();
|
|
||||||
|
|
||||||
// Mock the scanning module
|
|
||||||
mock.module("@aikidosec/safe-chain/scanning", {
|
|
||||||
namedExports: {
|
|
||||||
auditChanges: mockAuditChanges,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { scanner } = await import("./index.js");
|
|
||||||
|
|
||||||
it("should export scanner object with version", () => {
|
|
||||||
assert.strictEqual(scanner.version, "1");
|
|
||||||
assert.strictEqual(typeof scanner.scan, "function");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return empty advisories for clean packages", async () => {
|
|
||||||
mockAuditChanges.mock.mockImplementation(() => ({
|
|
||||||
allowedChanges: [{ name: "express", version: "4.18.2", type: "add" }],
|
|
||||||
disallowedChanges: [],
|
|
||||||
isAllowed: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const packages = [{ name: "express", version: "4.18.2" }];
|
|
||||||
const result = await scanner.scan({ packages });
|
|
||||||
|
|
||||||
assert.deepEqual(result, []);
|
|
||||||
assert.strictEqual(mockAuditChanges.mock.callCount(), 1);
|
|
||||||
assert.deepEqual(mockAuditChanges.mock.calls[0].arguments[0], [
|
|
||||||
{ name: "express", version: "4.18.2", type: "add" },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return fatal advisory for malware packages", async () => {
|
|
||||||
mockAuditChanges.mock.mockImplementation(() => ({
|
|
||||||
allowedChanges: [],
|
|
||||||
disallowedChanges: [
|
|
||||||
{
|
|
||||||
name: "malicious-pkg",
|
|
||||||
version: "1.0.0",
|
|
||||||
type: "add",
|
|
||||||
reason: "MALWARE",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isAllowed: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const packages = [{ name: "malicious-pkg", version: "1.0.0" }];
|
|
||||||
const result = await scanner.scan({ packages });
|
|
||||||
|
|
||||||
assert.strictEqual(result.length, 1);
|
|
||||||
assert.deepEqual(result[0], {
|
|
||||||
level: "fatal",
|
|
||||||
package: "malicious-pkg",
|
|
||||||
url: null,
|
|
||||||
description: "Package malicious-pkg@1.0.0 contains known security threats (MALWARE). Installation blocked by Safe-Chain.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle multiple packages with mixed results", async () => {
|
|
||||||
mockAuditChanges.mock.mockImplementation(() => ({
|
|
||||||
allowedChanges: [{ name: "express", version: "4.18.2", type: "add" }],
|
|
||||||
disallowedChanges: [
|
|
||||||
{
|
|
||||||
name: "malicious-pkg",
|
|
||||||
version: "1.0.0",
|
|
||||||
type: "add",
|
|
||||||
reason: "MALWARE",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "another-bad-pkg",
|
|
||||||
version: "2.1.0",
|
|
||||||
type: "add",
|
|
||||||
reason: "MALWARE",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isAllowed: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const packages = [
|
|
||||||
{ name: "express", version: "4.18.2" },
|
|
||||||
{ name: "malicious-pkg", version: "1.0.0" },
|
|
||||||
{ name: "another-bad-pkg", version: "2.1.0" },
|
|
||||||
];
|
|
||||||
const result = await scanner.scan({ packages });
|
|
||||||
|
|
||||||
assert.strictEqual(result.length, 2);
|
|
||||||
assert.strictEqual(result[0].package, "malicious-pkg");
|
|
||||||
assert.strictEqual(result[0].level, "fatal");
|
|
||||||
assert.strictEqual(result[1].package, "another-bad-pkg");
|
|
||||||
assert.strictEqual(result[1].level, "fatal");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle empty package list", async () => {
|
|
||||||
mockAuditChanges.mock.mockImplementation(() => ({
|
|
||||||
allowedChanges: [],
|
|
||||||
disallowedChanges: [],
|
|
||||||
isAllowed: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const result = await scanner.scan({ packages: [] });
|
|
||||||
|
|
||||||
assert.deepEqual(result, []);
|
|
||||||
assert.deepEqual(
|
|
||||||
mockAuditChanges.mock.calls[mockAuditChanges.mock.callCount() - 1]
|
|
||||||
.arguments[0],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should convert Bun package format to safe-chain format correctly", async () => {
|
|
||||||
mockAuditChanges.mock.mockImplementation(() => ({
|
|
||||||
allowedChanges: [],
|
|
||||||
disallowedChanges: [],
|
|
||||||
isAllowed: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const bunPackages = [
|
|
||||||
{ name: "lodash", version: "4.17.21" },
|
|
||||||
{ name: "@types/node", version: "20.0.0" },
|
|
||||||
];
|
|
||||||
|
|
||||||
await scanner.scan({ packages: bunPackages });
|
|
||||||
|
|
||||||
const expectedChanges = [
|
|
||||||
{ name: "lodash", version: "4.17.21", type: "add" },
|
|
||||||
{ name: "@types/node", version: "20.0.0", type: "add" },
|
|
||||||
];
|
|
||||||
|
|
||||||
assert.deepEqual(
|
|
||||||
mockAuditChanges.mock.calls[mockAuditChanges.mock.callCount() - 1]
|
|
||||||
.arguments[0],
|
|
||||||
expectedChanges
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
14
packages/safe-chain/bin/aikido-uv.js
Executable file
14
packages/safe-chain/bin/aikido-uv.js
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
// Set eco system
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
initializePackageManager("uv");
|
||||||
|
|
||||||
|
// Pass through only user-supplied uv args
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"aikido-pnpx": "bin/aikido-pnpx.js",
|
"aikido-pnpx": "bin/aikido-pnpx.js",
|
||||||
"aikido-bun": "bin/aikido-bun.js",
|
"aikido-bun": "bin/aikido-bun.js",
|
||||||
"aikido-bunx": "bin/aikido-bunx.js",
|
"aikido-bunx": "bin/aikido-bunx.js",
|
||||||
|
"aikido-uv": "bin/aikido-uv.js",
|
||||||
"aikido-pip": "bin/aikido-pip.js",
|
"aikido-pip": "bin/aikido-pip.js",
|
||||||
"aikido-pip3": "bin/aikido-pip3.js",
|
"aikido-pip3": "bin/aikido-pip3.js",
|
||||||
"aikido-python": "bin/aikido-python.js",
|
"aikido-python": "bin/aikido-python.js",
|
||||||
|
|
@ -33,25 +34,24 @@
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Aikido Security",
|
"author": "Aikido Security",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"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/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), and [bunx](https://bun.sh/docs/cli/bunx) 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, pnpx, bun, or bunx from downloading or running the malware.",
|
"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/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) 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, pnpx, bun, bunx, uv, or pip/pip3 from downloading or running the malware.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"certifi": "^14.5.15",
|
"certifi": "14.5.15",
|
||||||
"chalk": "5.4.1",
|
"chalk": "5.4.1",
|
||||||
"https-proxy-agent": "7.0.6",
|
"https-proxy-agent": "7.0.6",
|
||||||
"ini": "^6.0.0",
|
"ini": "6.0.0",
|
||||||
"make-fetch-happen": "14.0.3",
|
"make-fetch-happen": "14.0.3",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.1",
|
||||||
"npm-registry-fetch": "18.0.2",
|
"npm-registry-fetch": "18.0.2",
|
||||||
"ora": "8.2.0",
|
|
||||||
"semver": "7.7.2"
|
"semver": "7.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ini": "^4.1.1",
|
"@types/ini": "^4.1.1",
|
||||||
"@types/make-fetch-happen": "^10.0.4",
|
"@types/make-fetch-happen": "^10.0.4",
|
||||||
"@types/node": "^18.19.130",
|
"@types/node": "^18.19.130",
|
||||||
|
"@types/node-forge": "^1.3.14",
|
||||||
"@types/npm-registry-fetch": "^8.0.9",
|
"@types/npm-registry-fetch": "^8.0.9",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/node-forge": "^1.3.14",
|
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// oxlint-disable no-console
|
// oxlint-disable no-console
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import ora from "ora";
|
|
||||||
import { isCi } from "./environment.js";
|
import { isCi } from "./environment.js";
|
||||||
import {
|
import {
|
||||||
getLoggingLevel,
|
getLoggingLevel,
|
||||||
|
|
@ -98,61 +97,6 @@ function writeOrBuffer(messageFunction) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} Spinner
|
|
||||||
* @property {(message: string) => void} succeed
|
|
||||||
* @property {(message: string) => void} fail
|
|
||||||
* @property {() => void} stop
|
|
||||||
* @property {(message: string) => void} setText
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} message
|
|
||||||
*
|
|
||||||
* @returns {Spinner}
|
|
||||||
*/
|
|
||||||
function startProcess(message) {
|
|
||||||
if (isSilentMode()) {
|
|
||||||
return {
|
|
||||||
succeed: () => {},
|
|
||||||
fail: () => {},
|
|
||||||
stop: () => {},
|
|
||||||
setText: () => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCi()) {
|
|
||||||
return {
|
|
||||||
succeed: (message) => {
|
|
||||||
writeInformation(message);
|
|
||||||
},
|
|
||||||
fail: (message) => {
|
|
||||||
writeError(message);
|
|
||||||
},
|
|
||||||
stop: () => {},
|
|
||||||
setText: (message) => {
|
|
||||||
writeInformation(message);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const spinner = ora(message).start();
|
|
||||||
return {
|
|
||||||
succeed: (message) => {
|
|
||||||
spinner.succeed(message);
|
|
||||||
},
|
|
||||||
fail: (message) => {
|
|
||||||
spinner.fail(message);
|
|
||||||
},
|
|
||||||
stop: () => {
|
|
||||||
spinner.stop();
|
|
||||||
},
|
|
||||||
setText: (message) => {
|
|
||||||
spinner.text = message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startBufferingLogs() {
|
function startBufferingLogs() {
|
||||||
state.bufferOutput = true;
|
state.bufferOutput = true;
|
||||||
state.bufferedMessages = [];
|
state.bufferedMessages = [];
|
||||||
|
|
@ -173,7 +117,6 @@ export const ui = {
|
||||||
writeError,
|
writeError,
|
||||||
writeExitWithoutInstallingMaliciousPackages,
|
writeExitWithoutInstallingMaliciousPackages,
|
||||||
emptyLine,
|
emptyLine,
|
||||||
startProcess,
|
|
||||||
startBufferingLogs,
|
startBufferingLogs,
|
||||||
writeBufferedLogsAndStopBuffering,
|
writeBufferedLogsAndStopBuffering,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from "./pnpm/createPackageManager.js";
|
} from "./pnpm/createPackageManager.js";
|
||||||
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
||||||
import { createPipPackageManager } from "./pip/createPackageManager.js";
|
import { createPipPackageManager } from "./pip/createPackageManager.js";
|
||||||
|
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {{packageManagerName: PackageManager | null}}
|
* @type {{packageManagerName: PackageManager | null}}
|
||||||
|
|
@ -54,6 +55,8 @@ export function initializePackageManager(packageManagerName) {
|
||||||
state.packageManagerName = createBunxPackageManager();
|
state.packageManagerName = createBunxPackageManager();
|
||||||
} else if (packageManagerName === "pip") {
|
} else if (packageManagerName === "pip") {
|
||||||
state.packageManagerName = createPipPackageManager();
|
state.packageManagerName = createPipPackageManager();
|
||||||
|
} else if (packageManagerName === "uv") {
|
||||||
|
state.packageManagerName = createUvPackageManager();
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
throw new Error("Unsupported package manager: " + packageManagerName);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { runUv } from "./runUvCommand.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
*/
|
||||||
|
export function createUvPackageManager() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
*/
|
||||||
|
runCommand: (args) => {
|
||||||
|
return runUv("uv", args);
|
||||||
|
},
|
||||||
|
// For uv, rely solely on MITM
|
||||||
|
isSupportedCommand: () => false,
|
||||||
|
getDependencyUpdatesForCommand: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import { createUvPackageManager } from "./createUvPackageManager.js";
|
||||||
|
|
||||||
|
test("createUvPackageManager", async (t) => {
|
||||||
|
await t.test("should create package manager with required interface", () => {
|
||||||
|
const pm = createUvPackageManager();
|
||||||
|
|
||||||
|
assert.ok(pm);
|
||||||
|
assert.strictEqual(typeof pm.runCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.isSupportedCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
|
||||||
|
});
|
||||||
|
});
|
||||||
71
packages/safe-chain/src/packagemanager/uv/runUvCommand.js
Normal file
71
packages/safe-chain/src/packagemanager/uv/runUvCommand.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { ui } from "../../environment/userInteraction.js";
|
||||||
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||||
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||||
|
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets CA bundle environment variables used by Python libraries and uv.
|
||||||
|
*
|
||||||
|
* @param {NodeJS.ProcessEnv} env - Env object
|
||||||
|
* @param {string} combinedCaPath - Path to the combined CA bundle
|
||||||
|
*/
|
||||||
|
function setUvCaBundleEnvironmentVariables(env, combinedCaPath) {
|
||||||
|
// SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients
|
||||||
|
if (env.SSL_CERT_FILE) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
env.SSL_CERT_FILE = combinedCaPath;
|
||||||
|
|
||||||
|
// REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally)
|
||||||
|
if (env.REQUESTS_CA_BUNDLE) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
env.REQUESTS_CA_BUNDLE = combinedCaPath;
|
||||||
|
|
||||||
|
// PIP_CERT: Some underlying pip operations may respect this
|
||||||
|
if (env.PIP_CERT) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
env.PIP_CERT = combinedCaPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a uv command with safe-chain's certificate bundle and proxy configuration.
|
||||||
|
*
|
||||||
|
* uv respects standard environment variables for proxy and TLS configuration:
|
||||||
|
* - HTTP_PROXY / HTTPS_PROXY: Proxy settings
|
||||||
|
* - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification
|
||||||
|
*
|
||||||
|
* Unlike pip (which requires a temporary config file for cert configuration), uv directly
|
||||||
|
* honors environment variables, so no config/ini file is needed.
|
||||||
|
*
|
||||||
|
* @param {string} command - The uv command to execute (typically 'uv')
|
||||||
|
* @param {string[]} args - Command line arguments to pass to uv
|
||||||
|
* @returns {Promise<{status: number}>} Exit status of the uv command
|
||||||
|
*/
|
||||||
|
export async function runUv(command, args) {
|
||||||
|
try {
|
||||||
|
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
||||||
|
|
||||||
|
const combinedCaPath = getCombinedCaBundlePath();
|
||||||
|
setUvCaBundleEnvironmentVariables(env, combinedCaPath);
|
||||||
|
|
||||||
|
// Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
|
||||||
|
// These are already set by mergeSafeChainProxyEnvironmentVariables
|
||||||
|
|
||||||
|
const result = await safeSpawn(command, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status: result.status };
|
||||||
|
} catch (/** @type any */ error) {
|
||||||
|
if (error.status) {
|
||||||
|
return { status: error.status };
|
||||||
|
} else {
|
||||||
|
ui.writeError(`Error executing command: ${error.message}`);
|
||||||
|
ui.writeError(`Is '${command}' installed and available on your system?`);
|
||||||
|
return { status: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,36 +29,19 @@ export async function scanCommand(args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let timedOut = false;
|
let timedOut = false;
|
||||||
|
|
||||||
const spinner = ui.startProcess(
|
|
||||||
"Safe-chain: Scanning for malicious packages..."
|
|
||||||
);
|
|
||||||
/** @type {import("./audit/index.js").AuditResult | undefined} */
|
/** @type {import("./audit/index.js").AuditResult | undefined} */
|
||||||
let audit;
|
let audit;
|
||||||
|
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
const packageManager = getPackageManager();
|
||||||
const packageManager = getPackageManager();
|
const changes = await packageManager.getDependencyUpdatesForCommand(args);
|
||||||
const changes = await packageManager.getDependencyUpdatesForCommand(
|
|
||||||
args
|
|
||||||
);
|
|
||||||
|
|
||||||
if (timedOut) {
|
if (timedOut) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (changes.length > 0) {
|
|
||||||
spinner.setText(
|
|
||||||
`Safe-chain: Scanning ${changes.length} package(s)...`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
audit = await auditChanges(changes);
|
|
||||||
} catch (/** @type any */ error) {
|
|
||||||
spinner.fail(`Safe-chain: Error while scanning.`);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audit = await auditChanges(changes);
|
||||||
})(),
|
})(),
|
||||||
setTimeout(getScanTimeout()).then(() => {
|
setTimeout(getScanTimeout()).then(() => {
|
||||||
timedOut = true;
|
timedOut = true;
|
||||||
|
|
@ -66,15 +49,13 @@ export async function scanCommand(args) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (timedOut) {
|
if (timedOut) {
|
||||||
spinner.fail("Safe-chain: Timeout exceeded while scanning.");
|
|
||||||
throw new Error("Timeout exceeded while scanning npm install command.");
|
throw new Error("Timeout exceeded while scanning npm install command.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audit || audit.isAllowed) {
|
if (!audit || audit.isAllowed) {
|
||||||
spinner.stop();
|
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
printMaliciousChanges(audit.disallowedChanges, spinner);
|
printMaliciousChanges(audit.disallowedChanges);
|
||||||
onMalwareFound();
|
onMalwareFound();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
@ -82,12 +63,12 @@ export async function scanCommand(args) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("./audit/index.js").PackageChange[]} changes
|
* @param {import("./audit/index.js").PackageChange[]} changes
|
||||||
* @param spinner {import("../environment/userInteraction.js").Spinner}
|
|
||||||
*
|
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
function printMaliciousChanges(changes, spinner) {
|
function printMaliciousChanges(changes) {
|
||||||
spinner.fail("Safe-chain: " + chalk.bold("Malicious changes detected:"));
|
ui.writeInformation(
|
||||||
|
chalk.red("✖") + " Safe-chain: " + chalk.bold("Malicious changes detected:")
|
||||||
|
);
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
ui.writeInformation(` - ${change.name}@${change.version}`);
|
ui.writeInformation(` - ${change.name}@${change.version}`);
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,6 @@ import { setTimeout } from "node:timers/promises";
|
||||||
describe("scanCommand", async () => {
|
describe("scanCommand", async () => {
|
||||||
const getScanTimeoutMock = mock.fn(() => 1000);
|
const getScanTimeoutMock = mock.fn(() => 1000);
|
||||||
const mockGetDependencyUpdatesForCommand = mock.fn();
|
const mockGetDependencyUpdatesForCommand = mock.fn();
|
||||||
const mockStartProcess = mock.fn(() => ({
|
|
||||||
setText: () => {},
|
|
||||||
succeed: () => {},
|
|
||||||
fail: () => {},
|
|
||||||
stop: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// import { getPackageManager } from "../packagemanager/currentPackageManager.js";
|
// import { getPackageManager } from "../packagemanager/currentPackageManager.js";
|
||||||
mock.module("../packagemanager/currentPackageManager.js", {
|
mock.module("../packagemanager/currentPackageManager.js", {
|
||||||
|
|
@ -36,7 +30,6 @@ describe("scanCommand", async () => {
|
||||||
mock.module("../environment/userInteraction.js", {
|
mock.module("../environment/userInteraction.js", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
ui: {
|
ui: {
|
||||||
startProcess: mockStartProcess,
|
|
||||||
writeError: () => {},
|
writeError: () => {},
|
||||||
writeInformation: () => {},
|
writeInformation: () => {},
|
||||||
writeWarning: () => {},
|
writeWarning: () => {},
|
||||||
|
|
@ -75,51 +68,20 @@ describe("scanCommand", async () => {
|
||||||
const { scanCommand } = await import("./index.js");
|
const { scanCommand } = await import("./index.js");
|
||||||
|
|
||||||
it("should succeed when there are no changes", async () => {
|
it("should succeed when there are no changes", async () => {
|
||||||
let progressWasStopped = false;
|
|
||||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
||||||
setText: () => {},
|
|
||||||
succeed: () => {},
|
|
||||||
fail: () => {},
|
|
||||||
stop: () => {
|
|
||||||
progressWasStopped = true;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => []);
|
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => []);
|
||||||
|
|
||||||
await scanCommand(["install", "lodash"]);
|
await scanCommand(["install", "lodash"]);
|
||||||
|
|
||||||
assert.equal(progressWasStopped, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should succeed when changes are not malicious", async () => {
|
it("should succeed when changes are not malicious", async () => {
|
||||||
let progressWasStopped = false;
|
|
||||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
||||||
setText: () => {},
|
|
||||||
succeed: () => {},
|
|
||||||
fail: () => {},
|
|
||||||
stop: () => {
|
|
||||||
progressWasStopped = true;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
||||||
{ name: "lodash", version: "4.17.21" },
|
{ name: "lodash", version: "4.17.21" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await scanCommand(["install", "lodash"]);
|
await scanCommand(["install", "lodash"]);
|
||||||
|
|
||||||
assert.equal(progressWasStopped, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw an error when timing out", async () => {
|
it("should throw an error when timing out", async () => {
|
||||||
let failureMessageWasSet = false;
|
|
||||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
||||||
setText: () => {},
|
|
||||||
succeed: () => {},
|
|
||||||
fail: () => {
|
|
||||||
failureMessageWasSet = true;
|
|
||||||
},
|
|
||||||
stop: () => {},
|
|
||||||
}));
|
|
||||||
getScanTimeoutMock.mock.mockImplementationOnce(() => 100);
|
getScanTimeoutMock.mock.mockImplementationOnce(() => 100);
|
||||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => {
|
mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => {
|
||||||
await setTimeout(150);
|
await setTimeout(150);
|
||||||
|
|
@ -127,83 +89,9 @@ describe("scanCommand", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await assert.rejects(scanCommand(["install", "lodash"]));
|
await assert.rejects(scanCommand(["install", "lodash"]));
|
||||||
|
|
||||||
assert.equal(failureMessageWasSet, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail and return 1 malicious changes are detected", async () => {
|
it("should fail and return 1 malicious changes are detected", async () => {
|
||||||
let failureMessageWasSet = false;
|
|
||||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
||||||
setText: () => {},
|
|
||||||
succeed: () => {},
|
|
||||||
fail: () => {
|
|
||||||
failureMessageWasSet = true;
|
|
||||||
},
|
|
||||||
stop: () => {},
|
|
||||||
}));
|
|
||||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
|
||||||
{ name: "malicious", version: "1.0.0" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await scanCommand(["install", "malicious"]);
|
|
||||||
|
|
||||||
assert.equal(failureMessageWasSet, true);
|
|
||||||
assert.equal(result, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not report a timeout when the user takes a long time to respond (it should not affect the timeout)", async () => {
|
|
||||||
let failureMessages = [];
|
|
||||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
||||||
setText: () => {},
|
|
||||||
succeed: () => {},
|
|
||||||
fail: (message) => {
|
|
||||||
failureMessages.push(message);
|
|
||||||
},
|
|
||||||
stop: () => {},
|
|
||||||
}));
|
|
||||||
getScanTimeoutMock.mock.mockImplementationOnce(() => 100);
|
|
||||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => {
|
|
||||||
return [{ name: "malicious", version: "4.17.21" }];
|
|
||||||
});
|
|
||||||
|
|
||||||
await scanCommand(["install", "malicious"]);
|
|
||||||
|
|
||||||
assert.equal(failureMessages.length, 1);
|
|
||||||
const failureMessage = failureMessages[0];
|
|
||||||
assert.equal(failureMessage.toLowerCase().includes("timeout"), false);
|
|
||||||
assert.equal(failureMessage.toLowerCase().includes("malicious"), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should exit immediately when malicious changes are detected in block mode", async () => {
|
|
||||||
let failureMessageWasSet = false;
|
|
||||||
|
|
||||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
||||||
setText: () => {},
|
|
||||||
succeed: () => {},
|
|
||||||
fail: () => {
|
|
||||||
failureMessageWasSet = true;
|
|
||||||
},
|
|
||||||
stop: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
|
||||||
{ name: "malicious", version: "1.0.0" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await scanCommand(["install", "malicious"]);
|
|
||||||
|
|
||||||
assert.equal(failureMessageWasSet, true);
|
|
||||||
assert.equal(result, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should exit immediately when malicious changes are detected in block mode without prompting", async () => {
|
|
||||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
||||||
setText: () => {},
|
|
||||||
succeed: () => {},
|
|
||||||
fail: () => {},
|
|
||||||
stop: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
||||||
{ name: "malicious", version: "1.0.0" },
|
{ name: "malicious", version: "1.0.0" },
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export const knownAikidoTools = [
|
||||||
{ tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS },
|
{ tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS },
|
||||||
{ tool: "bun", aikidoCommand: "aikido-bun", ecoSystem: ECOSYSTEM_JS },
|
{ tool: "bun", aikidoCommand: "aikido-bun", ecoSystem: ECOSYSTEM_JS },
|
||||||
{ tool: "bunx", aikidoCommand: "aikido-bunx", ecoSystem: ECOSYSTEM_JS },
|
{ tool: "bunx", aikidoCommand: "aikido-bunx", ecoSystem: ECOSYSTEM_JS },
|
||||||
|
{ tool: "uv", aikidoCommand: "aikido-uv", ecoSystem: ECOSYSTEM_PY },
|
||||||
{ tool: "pip", aikidoCommand: "aikido-pip", ecoSystem: ECOSYSTEM_PY },
|
{ tool: "pip", aikidoCommand: "aikido-pip", ecoSystem: ECOSYSTEM_PY },
|
||||||
{ tool: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY },
|
{ tool: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY },
|
||||||
{ tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY },
|
{ tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY },
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,10 @@ function pip3
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip3" $argv
|
wrapSafeChainCommand "pip3" "aikido-pip3" $argv
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function uv
|
||||||
|
wrapSafeChainCommand "uv" "aikido-uv" $argv
|
||||||
|
end
|
||||||
|
|
||||||
# `python -m pip`, `python -m pip3`.
|
# `python -m pip`, `python -m pip3`.
|
||||||
function python
|
function python
|
||||||
wrapSafeChainCommand "python" "aikido-python" $argv
|
wrapSafeChainCommand "python" "aikido-python" $argv
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ function pip3() {
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
|
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uv() {
|
||||||
|
wrapSafeChainCommand "uv" "aikido-uv" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
# `python -m pip`, `python -m pip3`.
|
# `python -m pip`, `python -m pip3`.
|
||||||
function python() {
|
function python() {
|
||||||
wrapSafeChainCommand "python" "aikido-python" "$@"
|
wrapSafeChainCommand "python" "aikido-python" "$@"
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,10 @@ function pip3 {
|
||||||
Invoke-WrappedCommand "pip3" "aikido-pip3" $args
|
Invoke-WrappedCommand "pip3" "aikido-pip3" $args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uv {
|
||||||
|
Invoke-WrappedCommand "uv" "aikido-uv" $args
|
||||||
|
}
|
||||||
|
|
||||||
# `python -m pip`, `python -m pip3`.
|
# `python -m pip`, `python -m pip3`.
|
||||||
function python {
|
function python {
|
||||||
Invoke-WrappedCommand 'python' 'aikido-python' $args
|
Invoke-WrappedCommand 'python' 'aikido-python' $args
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ except Exception as exc:
|
||||||
raise
|
raise
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Install uv
|
||||||
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
||||||
|
echo 'source $HOME/.local/bin/env' >> ~/.bashrc
|
||||||
|
|
||||||
# Copy and install Safe chain
|
# Copy and install Safe chain
|
||||||
COPY --from=builder /app/*.tgz /pkgs/
|
COPY --from=builder /app/*.tgz /pkgs/
|
||||||
RUN npm install -g /pkgs/*.tgz
|
RUN npm install -g /pkgs/*.tgz
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,7 @@ describe("E2E: safe-chain setup-ci command", () => {
|
||||||
const projectShell = await container.openShell(shell);
|
const projectShell = await container.openShell(shell);
|
||||||
const result = await projectShell.runCommand("npm i axios");
|
const result = await projectShell.runCommand("npm i axios");
|
||||||
|
|
||||||
const hasExpectedOutput = result.output.includes(
|
const hasExpectedOutput = result.output.includes("Safe-chain: Scanned");
|
||||||
"Scanning for malicious packages..."
|
|
||||||
);
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
hasExpectedOutput,
|
hasExpectedOutput,
|
||||||
hasExpectedOutput
|
hasExpectedOutput
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,7 @@ describe("E2E: safe-chain setup command", () => {
|
||||||
await projectShell.runCommand("cd /testapp");
|
await projectShell.runCommand("cd /testapp");
|
||||||
const result = await projectShell.runCommand("npm i axios");
|
const result = await projectShell.runCommand("npm i axios");
|
||||||
|
|
||||||
const hasExpectedOutput = result.output.includes(
|
const hasExpectedOutput = result.output.includes("Safe-chain: Scanned");
|
||||||
"Scanning for malicious packages..."
|
|
||||||
);
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
hasExpectedOutput,
|
hasExpectedOutput,
|
||||||
hasExpectedOutput
|
hasExpectedOutput
|
||||||
|
|
|
||||||
561
test/e2e/uv.e2e.spec.js
Normal file
561
test/e2e/uv.e2e.spec.js
Normal file
|
|
@ -0,0 +1,561 @@
|
||||||
|
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||||
|
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
describe("E2E: uv coverage", () => {
|
||||||
|
let container;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
DockerTestContainer.buildImage();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Run a new Docker container for each test
|
||||||
|
container = new DockerTestContainer();
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
const installationShell = await container.openShell("zsh");
|
||||||
|
await installationShell.runCommand("safe-chain setup --include-python");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Stop and clean up the container after each test
|
||||||
|
if (container) {
|
||||||
|
await container.stop();
|
||||||
|
container = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`successfully installs known safe packages with uv pip install`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages requests"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install with specific version`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages requests==2.32.3"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install with version specifiers (>=)`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
'uv pip install --system --break-system-packages "Jinja2>=3.1"'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install with extras such as requests[socks]`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
'uv pip install --system --break-system-packages "requests[socks]==2.32.3"'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install multiple packages`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages requests certifi urllib3"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install from requirements file`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Create a requirements.txt file
|
||||||
|
await shell.runCommand("echo 'requests==2.32.3' > requirements.txt");
|
||||||
|
await shell.runCommand("echo 'certifi>=2024.0.0' >> requirements.txt");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages -r requirements.txt"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip sync with requirements file`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Create a requirements.txt file
|
||||||
|
await shell.runCommand("echo 'requests==2.32.3' > requirements-sync.txt");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip sync --system --break-system-packages requirements-sync.txt"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`safe-chain blocks installation of malicious Python packages via uv`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages safe-chain-pi-test"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("blocked 1 malicious package downloads:"),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("safe_chain_pi_test@0.0.1"),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("Exiting without installing malicious packages."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const listResult = await shell.runCommand("uv pip list --system");
|
||||||
|
assert.ok(
|
||||||
|
!listResult.output.includes("safe-chain-pi-test"),
|
||||||
|
`Malicious package was installed despite safe-chain protection. Output of 'uv pip list' was:\n${listResult.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install from GitHub URL using the CA bundle`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages git+https://github.com/psf/requests.git@v2.32.3"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify installation succeeded (would fail if certificate validation via env CA bundle broke)
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("Installed") ||
|
||||||
|
result.output.includes("installed"),
|
||||||
|
`Installation from GitHub failed - CA bundle may not be working. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip successfully validates certificates for HTTPS downloads`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages certifi"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify successful installation (would fail with SSL/certificate errors if the env CA bundle wasn't working)
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("Installed") ||
|
||||||
|
result.output.includes("installed"),
|
||||||
|
`Installation should succeed with proper certificate validation. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should NOT contain SSL or certificate errors
|
||||||
|
assert.ok(
|
||||||
|
!result.output.match(
|
||||||
|
/SSL|certificate verify failed|CERTIFICATE_VERIFY_FAILED/i
|
||||||
|
),
|
||||||
|
`Should not have SSL/certificate errors. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install from direct HTTPS wheel URL`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("Installed") ||
|
||||||
|
result.output.includes("installed"),
|
||||||
|
`Installation from direct HTTPS URL failed. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install with --upgrade flag`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// First install a package
|
||||||
|
await shell.runCommand("uv pip install --system --break-system-packages requests==2.31.0");
|
||||||
|
|
||||||
|
// Then upgrade it
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages --upgrade requests"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install with --no-deps flag`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages --no-deps requests"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install with --editable flag from local directory`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Create a simple package structure
|
||||||
|
await shell.runCommand("mkdir -p /tmp/test-pkg");
|
||||||
|
await shell.runCommand("echo 'from setuptools import setup' > /tmp/test-pkg/setup.py");
|
||||||
|
await shell.runCommand("echo \"setup(name='test-pkg', version='0.1.0')\" >> /tmp/test-pkg/setup.py");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages -e /tmp/test-pkg"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip compile creates locked requirements`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Create an input requirements file
|
||||||
|
await shell.runCommand("echo 'requests' > requirements.in");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip compile requirements.in"
|
||||||
|
);
|
||||||
|
|
||||||
|
// uv pip compile doesn't install packages, just resolves dependencies
|
||||||
|
// It should complete successfully and output resolved requirements
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("requests==") || result.output.includes("# via"),
|
||||||
|
`Output did not include compiled requirements. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install with --index-url for alternate registry`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages --index-url https://test.pypi.org/simple certifi"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should succeed if CA bundle properly handles tunneled hosts
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("Installed") ||
|
||||||
|
result.output.includes("installed"),
|
||||||
|
`Installation from Test PyPI failed. This may indicate the CA bundle lacks public roots. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install with --safe-chain-logging=verbose`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv pip install --system --break-system-packages requests --safe-chain-logging=verbose"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip install with version range constraint`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
'uv pip install --system --break-system-packages "requests>=2.31.0,<2.33.0"'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv pip list shows installed packages`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Install a package first
|
||||||
|
await shell.runCommand("uv pip install --system --break-system-packages requests");
|
||||||
|
|
||||||
|
// Then list packages - this shouldn't trigger safe-chain scanning
|
||||||
|
const result = await shell.runCommand("uv pip list --system");
|
||||||
|
|
||||||
|
// List command should work without malware scanning
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("requests") || result.output.length > 0,
|
||||||
|
`Output did not show package list. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv add installs package and updates project`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Initialize a new uv project and add package in same command
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv init test-project && cd test-project && uv add requests"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv add with specific version`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Initialize a new uv project
|
||||||
|
await shell.runCommand("uv init test-project-version");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"cd test-project-version && uv add requests==2.32.3"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv add --dev for development dependencies`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Initialize a new uv project
|
||||||
|
await shell.runCommand("uv init test-project-dev");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"cd test-project-dev && uv add --dev pytest"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv add multiple packages at once`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Initialize a new uv project
|
||||||
|
await shell.runCommand("uv init test-project-multi");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"cd test-project-multi && uv add requests certifi urllib3"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`safe-chain blocks malicious packages via uv add`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Initialize a new uv project
|
||||||
|
await shell.runCommand("uv init test-project-malware");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"cd test-project-malware && uv add safe-chain-pi-test"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("blocked 1 malicious package downloads:"),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("safe_chain_pi_test@0.0.1"),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("Exiting without installing malicious packages."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv tool install installs a global tool`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv tool install ruff"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found.") || result.output.includes("Installed"),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`safe-chain blocks malicious packages via uv tool install`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv tool install safe-chain-pi-test"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("blocked 1 malicious package downloads:"),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("safe_chain_pi_test@0.0.1"),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv run --with installs ephemeral dependency`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Create a simple Python script
|
||||||
|
await shell.runCommand("echo 'import requests; print(requests.__version__)' > test_script.py");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv run --with requests test_script.py"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`safe-chain blocks malicious packages via uv run --with`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Create a simple Python script
|
||||||
|
await shell.runCommand("echo 'print(\"test\")' > test_script2.py");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv run --with safe-chain-pi-test test_script2.py"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("blocked 1 malicious package downloads:"),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv sync syncs project dependencies`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Initialize a new uv project, add a dependency, remove venv, and sync in one command chain
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv init test-sync-project && cd test-sync-project && uv add requests && rm -rf .venv && uv sync"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv add from git URL`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Initialize a new uv project
|
||||||
|
await shell.runCommand("uv init test-git-add");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"cd test-git-add && uv add git+https://github.com/psf/requests.git@v2.32.3"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv add with --optional group`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Initialize a new uv project
|
||||||
|
await shell.runCommand("uv init test-optional");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"cd test-optional && uv add --optional dev pytest"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv run --with-requirements installs from requirements file`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Create requirements file and script
|
||||||
|
await shell.runCommand("echo 'requests' > run_requirements.txt");
|
||||||
|
await shell.runCommand("echo 'import requests; print(requests.__version__)' > run_script.py");
|
||||||
|
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv run --with-requirements run_requirements.txt run_script.py"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`uv sync --all-extras syncs all optional dependencies`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
// Initialize project with optional dependency and sync in one command chain
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"uv init test-extras && cd test-extras && uv add --optional dev pytest && uv sync --all-extras"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue