mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Adapt per review
This commit is contained in:
parent
9dacf5cff3
commit
190607de92
27 changed files with 191 additions and 114 deletions
16
README.md
16
README.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing in the Python ecosystem (through pip or pip3) or in the Javascript ecosystem (through npm, npx, yarn, pnpm, pnpx, bun and bunx). It's **free** to use and does not require any token.
|
The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing in the Python ecosystem (through pip or pip3) or in the Javascript ecosystem (through npm, npx, yarn, pnpm, pnpx, bun and bunx). It's **free** to use and does not require any token.
|
||||||
|
|
||||||
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), 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, or pip from downloading or running the malware.
|
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), 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, or pip/pip3 from downloading or running the malware.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -16,6 +16,7 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi
|
||||||
- ✅ **bun**
|
- ✅ **bun**
|
||||||
- ✅ **bunx**
|
- ✅ **bunx**
|
||||||
- ✅ **pip**
|
- ✅ **pip**
|
||||||
|
- ✅ **pip3**
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
|
|
@ -32,14 +33,14 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
|
||||||
safe-chain setup
|
safe-chain setup
|
||||||
```
|
```
|
||||||
3. **❗Restart your terminal** to start using the Aikido Safe Chain.
|
3. **❗Restart your terminal** to start using the Aikido Safe Chain.
|
||||||
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
- 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.
|
||||||
4. **Verify the installation** by running:
|
4. **Verify the installation** by running:
|
||||||
```shell
|
```shell
|
||||||
npm install safe-chain-test
|
npm install safe-chain-test
|
||||||
```
|
```
|
||||||
- The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware.
|
- The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware.
|
||||||
|
|
||||||
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, or `pip` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command.
|
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. 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:
|
||||||
```shell
|
```shell
|
||||||
|
|
@ -48,7 +49,7 @@ safe-chain --version
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
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, or pip 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, `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 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 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:
|
||||||
|
|
||||||
|
|
@ -60,13 +61,6 @@ The Aikido Safe Chain integrates with your shell to provide a seamless experienc
|
||||||
|
|
||||||
More information about the shell integration can be found in the [shell integration documentation](docs/shell-integration.md).
|
More information about the shell integration can be found in the [shell integration documentation](docs/shell-integration.md).
|
||||||
|
|
||||||
### Python support
|
|
||||||
|
|
||||||
- Supports `pip` and `pip3` commands.
|
|
||||||
- Scans Python packages fetched by `pip install`, `pip download`, and `pip wheel`.
|
|
||||||
- Intercepts downloads from PyPI and checks them against Aikido's malware intelligence before they reach your machine.
|
|
||||||
- Included automatically when you run `safe-chain setup` (shell integration); **CI integration is not yet available for pip/pip3**.
|
|
||||||
|
|
||||||
## Uninstallation
|
## Uninstallation
|
||||||
|
|
||||||
To uninstall the Aikido Safe Chain, you can run the following command:
|
To uninstall the Aikido Safe Chain, you can run the following command:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`) with Aikido's security scanning functionality. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents.
|
The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`) with Aikido's security scanning functionality. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents.
|
||||||
|
|
||||||
## Supported Shells
|
## Supported Shells
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ This command:
|
||||||
|
|
||||||
- Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`)
|
- Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`)
|
||||||
- Detects all supported shells on your system
|
- Detects all supported shells on your system
|
||||||
- Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, and `bunx`
|
- Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3`
|
||||||
|
|
||||||
❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the startup scripts are sourced correctly.
|
❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the startup scripts are sourced correctly.
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ The system modifies the following files to source Safe Chain startup scripts:
|
||||||
This means the shell functions are working but the Aikido commands aren't installed or available in your PATH:
|
This means the shell functions are working but the Aikido commands aren't installed or available in your PATH:
|
||||||
|
|
||||||
- Make sure Aikido Safe Chain is properly installed on your system
|
- Make sure Aikido Safe Chain is properly installed on your system
|
||||||
- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, and `aikido-bunx` commands exist
|
- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, and `aikido-pip3` commands exist
|
||||||
- Check that these commands are in your system's PATH
|
- Check that these commands are in your system's PATH
|
||||||
|
|
||||||
### Manual Verification
|
### Manual Verification
|
||||||
|
|
@ -120,4 +120,4 @@ npm() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, and `bunx` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes.
|
Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "bun";
|
const packageManagerName = "bun";
|
||||||
initializePackageManager(packageManagerName);
|
initializePackageManager(packageManagerName);
|
||||||
var exitCode = await main(process.argv.slice(2));
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "bunx";
|
const packageManagerName = "bunx";
|
||||||
initializePackageManager(packageManagerName);
|
initializePackageManager(packageManagerName);
|
||||||
var exitCode = await main(process.argv.slice(2));
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "npm";
|
const packageManagerName = "npm";
|
||||||
initializePackageManager(packageManagerName);
|
initializePackageManager(packageManagerName);
|
||||||
var exitCode = await main(process.argv.slice(2));
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "npx";
|
const packageManagerName = "npx";
|
||||||
initializePackageManager(packageManagerName);
|
initializePackageManager(packageManagerName);
|
||||||
var exitCode = await main(process.argv.slice(2));
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,16 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
import { setEcoSystem } from "../src/config/settings.js";
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
let packageManagerName = "pip";
|
let packageManagerName = "pip";
|
||||||
let targetVersionMajor;
|
// Pass through user args as-is
|
||||||
|
|
||||||
// Copy argv so we can modify it
|
|
||||||
const argv = process.argv.slice(2);
|
const argv = process.argv.slice(2);
|
||||||
|
|
||||||
for (let i = 0; i < argv.length; i++) {
|
|
||||||
const a = argv[i];
|
|
||||||
|
|
||||||
// --target-version-major tells us which pip version is being used (2 or 3)
|
|
||||||
if (a === "--target-version-major" && i + 1 < argv.length) {
|
|
||||||
targetVersionMajor = argv[i + 1];
|
|
||||||
argv.splice(i, 2);
|
|
||||||
i -= 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user explicitly called python3, prefer pip3
|
|
||||||
if (targetVersionMajor && String(targetVersionMajor).trim() === "3") {
|
|
||||||
packageManagerName = "pip3";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set eco system
|
// Set eco system
|
||||||
// This can be used in other parts of the code to determine which eco system we are working with
|
// This can be used in other parts of the code to determine which eco system we are working with
|
||||||
setEcoSystem("py");
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
initializePackageManager(packageManagerName);
|
initializePackageManager(packageManagerName);
|
||||||
const exitCode = await main(argv);
|
const exitCode = await main(argv);
|
||||||
|
|
|
||||||
19
packages/safe-chain/bin/aikido-pip3.js
Normal file
19
packages/safe-chain/bin/aikido-pip3.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/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";
|
||||||
|
|
||||||
|
// Explicit pip3 entrypoint
|
||||||
|
const packageManagerName = "pip3";
|
||||||
|
|
||||||
|
// Copy argv as-is
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
|
||||||
|
// Set ecosystem to Python
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
initializePackageManager(packageManagerName);
|
||||||
|
const exitCode = await main(argv);
|
||||||
|
|
||||||
|
process.exit(exitCode);
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "pnpm";
|
const packageManagerName = "pnpm";
|
||||||
initializePackageManager(packageManagerName);
|
initializePackageManager(packageManagerName);
|
||||||
var exitCode = await main(process.argv.slice(2));
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "pnpx";
|
const packageManagerName = "pnpx";
|
||||||
initializePackageManager(packageManagerName);
|
initializePackageManager(packageManagerName);
|
||||||
var exitCode = await main(process.argv.slice(2));
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "yarn";
|
const packageManagerName = "yarn";
|
||||||
initializePackageManager(packageManagerName);
|
initializePackageManager(packageManagerName);
|
||||||
var exitCode = await main(process.argv.slice(2));
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"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-pip": "bin/aikido-pip.js",
|
"aikido-pip": "bin/aikido-pip.js",
|
||||||
|
"aikido-pip3": "bin/aikido-pip3.js",
|
||||||
"safe-chain": "bin/safe-chain.js"
|
"safe-chain": "bin/safe-chain.js"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import fetch from "make-fetch-happen";
|
import fetch from "make-fetch-happen";
|
||||||
import { getEcoSystem } from "../config/settings.js";
|
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
||||||
|
|
||||||
const malwareDatabaseUrls = {
|
const malwareDatabaseUrls = {
|
||||||
js: "https://malware-list.aikido.dev/malware_predictions.json",
|
[ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json",
|
||||||
py: "https://malware-list.aikido.dev/malware_pypi.json",
|
[ECOSYSTEM_PY]: "https://malware-list.aikido.dev/malware_pypi.json",
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchMalwareDatabase() {
|
export async function fetchMalwareDatabase() {
|
||||||
const ecosystem = getEcoSystem() || "js";
|
const ecosystem = getEcoSystem();
|
||||||
const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem];
|
const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem];
|
||||||
const response = await fetch(malwareDatabaseUrl);
|
const response = await fetch(malwareDatabaseUrl);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -26,8 +26,7 @@ export async function fetchMalwareDatabase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMalwareDatabaseVersion() {
|
export async function fetchMalwareDatabaseVersion() {
|
||||||
const ecosystem = getEcoSystem() || "js";
|
const ecosystem = getEcoSystem();
|
||||||
|
|
||||||
const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem];
|
const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem];
|
||||||
const response = await fetch(malwareDatabaseUrl, {
|
const response = await fetch(malwareDatabaseUrl, {
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,13 @@ function readConfigFile() {
|
||||||
|
|
||||||
function getDatabasePath() {
|
function getDatabasePath() {
|
||||||
const aikidoDir = getAikidoDirectory();
|
const aikidoDir = getAikidoDirectory();
|
||||||
const ecosystem = getEcoSystem() || "js";
|
const ecosystem = getEcoSystem();
|
||||||
return path.join(aikidoDir, `malwareDatabase_${ecosystem}.json`);
|
return path.join(aikidoDir, `malwareDatabase_${ecosystem}.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDatabaseVersionPath() {
|
function getDatabaseVersionPath() {
|
||||||
const aikidoDir = getAikidoDirectory();
|
const aikidoDir = getAikidoDirectory();
|
||||||
const ecosystem = getEcoSystem() || "js";
|
const ecosystem = getEcoSystem();
|
||||||
return path.join(aikidoDir, `version_${ecosystem}.txt`);
|
return path.join(aikidoDir, `version_${ecosystem}.txt`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,12 @@ export function getMalwareAction() {
|
||||||
export const MALWARE_ACTION_BLOCK = "block";
|
export const MALWARE_ACTION_BLOCK = "block";
|
||||||
export const MALWARE_ACTION_PROMPT = "prompt";
|
export const MALWARE_ACTION_PROMPT = "prompt";
|
||||||
|
|
||||||
|
export const ECOSYSTEM_JS = "js";
|
||||||
|
export const ECOSYSTEM_PY = "py";
|
||||||
|
|
||||||
// Default to JavaScript ecosystem
|
// Default to JavaScript ecosystem
|
||||||
const ecosystemSettings = {
|
const ecosystemSettings = {
|
||||||
ecoSystem: "js",
|
ecoSystem: ECOSYSTEM_JS,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getEcoSystem() {
|
export function getEcoSystem() {
|
||||||
|
|
|
||||||
|
|
@ -19,34 +19,3 @@ export async function runPip(command, args) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function dryRunPipCommandAndOutput(command, args) {
|
|
||||||
try {
|
|
||||||
// Note: pip supports --dry-run for the "install" command only; "download" and "wheel" do not.
|
|
||||||
// We don't mutate args here — callers should include --dry-run when appropriate.
|
|
||||||
const result = await safeSpawnPy(
|
|
||||||
command,
|
|
||||||
args,
|
|
||||||
{
|
|
||||||
stdio: "pipe",
|
|
||||||
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
status: result.status,
|
|
||||||
output: result.status === 0 ? result.stdout : result.stderr,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status) {
|
|
||||||
const output =
|
|
||||||
error.stdout?.toString() ??
|
|
||||||
error.stderr?.toString() ??
|
|
||||||
error.message ??
|
|
||||||
"";
|
|
||||||
return { status: error.status, output };
|
|
||||||
} else {
|
|
||||||
ui.writeError("Error executing command:", error.message);
|
|
||||||
return { status: 1 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,28 @@
|
||||||
|
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
||||||
|
|
||||||
export const knownJsRegistries = ["registry.npmjs.org","registry.yarnpkg.com"];
|
export const knownJsRegistries = ["registry.npmjs.org","registry.yarnpkg.com"];
|
||||||
export const knownPipRegistries = ["files.pythonhosted.org", "pypi.org", "pypi.python.org", "pythonhosted.org"];
|
export const knownPipRegistries = ["files.pythonhosted.org", "pypi.org", "pypi.python.org", "pythonhosted.org"];
|
||||||
|
|
||||||
export function parsePackageFromUrl(url) {
|
export function parsePackageFromUrl(url) {
|
||||||
|
const ecosystem = getEcoSystem();
|
||||||
let registry;
|
let registry;
|
||||||
|
|
||||||
|
// Only check registries that match the current ecosystem
|
||||||
|
if (ecosystem === ECOSYSTEM_JS) {
|
||||||
for (const knownRegistry of knownJsRegistries) {
|
for (const knownRegistry of knownJsRegistries) {
|
||||||
if (url.includes(knownRegistry)) {
|
if (url.includes(knownRegistry)) {
|
||||||
registry = knownRegistry;
|
registry = knownRegistry;
|
||||||
return parseJsPackageFromUrl(url, registry);
|
return parseJsPackageFromUrl(url, registry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (ecosystem === ECOSYSTEM_PY) {
|
||||||
for (const knownRegistry of knownPipRegistries) {
|
for (const knownRegistry of knownPipRegistries) {
|
||||||
if (url.includes(knownRegistry)) {
|
if (url.includes(knownRegistry)) {
|
||||||
registry = knownRegistry;
|
registry = knownRegistry;
|
||||||
return parsePipPackageFromUrl(url, registry);
|
return parsePipPackageFromUrl(url, registry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If no known registry matched, return { packageName: undefined, version: undefined }
|
// If no known registry matched, return { packageName: undefined, version: undefined }
|
||||||
return { packageName: undefined, version: undefined };
|
return { packageName: undefined, version: undefined };
|
||||||
|
|
@ -70,21 +76,25 @@ function parsePipPackageFromUrl(url, registry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quick sanity check on the URL + parse
|
// Quick sanity check on the URL + parse
|
||||||
let u;
|
let urlObj;
|
||||||
try {
|
try {
|
||||||
u = new URL(url);
|
urlObj = new URL(url);
|
||||||
} catch {
|
} catch {
|
||||||
return { packageName, version};
|
return { packageName, version};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the last path segment (filename) and decode it (strip query & fragment automatically)
|
// Get the last path segment (filename) and decode it (strip query & fragment automatically)
|
||||||
const lastSegment = u.pathname.split("/").filter(Boolean).pop();
|
const lastSegment = urlObj.pathname.split("/").filter(Boolean).pop();
|
||||||
if (!lastSegment){
|
if (!lastSegment){
|
||||||
return { packageName, version};
|
return { packageName, version};
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = decodeURIComponent(lastSegment);
|
const filename = decodeURIComponent(lastSegment);
|
||||||
|
|
||||||
|
// Parse Python package downloads from PyPI/pythonhosted.org
|
||||||
|
// Example wheel: https://files.pythonhosted.org/packages/xx/yy/requests-2.28.1-py3-none-any.whl
|
||||||
|
// Example sdist: https://files.pythonhosted.org/packages/xx/yy/requests-2.28.1.tar.gz
|
||||||
|
|
||||||
// Wheel (.whl)
|
// Wheel (.whl)
|
||||||
if (filename.endsWith(".whl")) {
|
if (filename.endsWith(".whl")) {
|
||||||
const base = filename.slice(0, -4); // remove ".whl"
|
const base = filename.slice(0, -4); // remove ".whl"
|
||||||
|
|
@ -96,6 +106,9 @@ function parsePipPackageFromUrl(url, registry) {
|
||||||
const rawVersion = secondDash >= 0 ? rest.slice(0, secondDash) : rest;
|
const rawVersion = secondDash >= 0 ? rest.slice(0, secondDash) : rest;
|
||||||
packageName = dist; // preserve underscores
|
packageName = dist; // preserve underscores
|
||||||
version = rawVersion;
|
version = rawVersion;
|
||||||
|
// Reject "latest" as it's a placeholder, not a real version
|
||||||
|
// When version is "latest", this signals the URL doesn't contain actual version info
|
||||||
|
// Returning undefined allows the request (see registryProxy.js isAllowedUrl)
|
||||||
if (version === "latest" || !packageName || !version) {
|
if (version === "latest" || !packageName || !version) {
|
||||||
return { packageName: undefined, version: undefined };
|
return { packageName: undefined, version: undefined };
|
||||||
}
|
}
|
||||||
|
|
@ -111,6 +124,9 @@ function parsePipPackageFromUrl(url, registry) {
|
||||||
if (lastDash > 0 && lastDash < base.length - 1) {
|
if (lastDash > 0 && lastDash < base.length - 1) {
|
||||||
packageName = base.slice(0, lastDash);
|
packageName = base.slice(0, lastDash);
|
||||||
version = base.slice(lastDash + 1);
|
version = base.slice(lastDash + 1);
|
||||||
|
// Reject "latest" as it's a placeholder, not a real version
|
||||||
|
// When version is "latest", this signals the URL doesn't contain actual version info
|
||||||
|
// Returning undefined allows the request (see registryProxy.js isAllowedUrl)
|
||||||
if (version === "latest" || !packageName || !version) {
|
if (version === "latest" || !packageName || !version) {
|
||||||
return { packageName: undefined, version: undefined };
|
return { packageName: undefined, version: undefined };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
import { describe, it } from "node:test";
|
import { describe, it, beforeEach } from "node:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { parsePackageFromUrl } from "./parsePackageFromUrl.js";
|
import { parsePackageFromUrl } from "./parsePackageFromUrl.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
||||||
|
|
||||||
describe("parsePackageFromUrl", () => {
|
describe("parsePackageFromUrl", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
|
});
|
||||||
|
|
||||||
const testCases = [
|
const testCases = [
|
||||||
// Regular packages
|
// Regular packages
|
||||||
{
|
{
|
||||||
|
|
@ -114,6 +119,10 @@ describe("parsePackageFromUrl", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("parsePackageFromUrl - pip URLs", () => {
|
describe("parsePackageFromUrl - pip URLs", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
});
|
||||||
|
|
||||||
const pipTestCases = [
|
const pipTestCases = [
|
||||||
// Valid pip URLs
|
// Valid pip URLs
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,32 @@ describe("registryProxy.connectTunnel", () => {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should use destination's real certificate (not safe-chain's self-signed CA)", async () => {
|
||||||
|
const socket = await connectToProxy(proxyHost, proxyPort);
|
||||||
|
await establishHttpsTunnel(socket, "postman-echo.com", 443);
|
||||||
|
|
||||||
|
// Verifies that tunnel requests pass through the destination's real certificate
|
||||||
|
// without interception by the safe-chain MITM proxy.
|
||||||
|
const certInfo = await getTlsCertificateInfo(
|
||||||
|
socket,
|
||||||
|
new URL("https://postman-echo.com")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the certificate is NOT issued by our safe-chain CA
|
||||||
|
// Our self-signed CA would have issuer: "Safe-Chain Proxy CA"
|
||||||
|
assert.ok(certInfo.issuer !== undefined, "Certificate should have an issuer");
|
||||||
|
assert.ok(
|
||||||
|
!certInfo.issuer.includes("Safe-Chain"),
|
||||||
|
`Tunnel should use destination's real certificate, not safe-chain CA. Issuer: ${certInfo.issuer}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify it's a real certificate with proper hostname
|
||||||
|
assert.strictEqual(certInfo.subject.includes("postman-echo.com"), true,
|
||||||
|
`Certificate subject should include postman-echo.com, got: ${certInfo.subject}`);
|
||||||
|
|
||||||
|
socket.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
describe("Error Handling", () => {
|
describe("Error Handling", () => {
|
||||||
it("should return 502 Bad Gateway for invalid hostname", async () => {
|
it("should return 502 Bad Gateway for invalid hostname", async () => {
|
||||||
const socket = await connectToProxy(proxyHost, proxyPort);
|
const socket = await connectToProxy(proxyHost, proxyPort);
|
||||||
|
|
@ -141,7 +167,7 @@ function establishHttpsTunnel(socket, targetHost, targetPort) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendHttpsRequestThroughTunnel(socket, verb, url) {
|
function sendHttpsRequestThroughTunnel(socket, verb, url, rejectUnauthorized = false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tlsSocket = tls.connect(
|
const tlsSocket = tls.connect(
|
||||||
{
|
{
|
||||||
|
|
@ -149,7 +175,7 @@ function sendHttpsRequestThroughTunnel(socket, verb, url) {
|
||||||
servername: url.hostname,
|
servername: url.hostname,
|
||||||
// Tests should focus on tunnel behavior, not system CA state;
|
// Tests should focus on tunnel behavior, not system CA state;
|
||||||
// disable CA verification to avoid flakiness on machines without full roots.
|
// disable CA verification to avoid flakiness on machines without full roots.
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: rejectUnauthorized,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
tlsSocket.write(
|
tlsSocket.write(
|
||||||
|
|
@ -173,3 +199,35 @@ function sendHttpsRequestThroughTunnel(socket, verb, url) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTlsCertificateInfo(socket, url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tlsSocket = tls.connect(
|
||||||
|
{
|
||||||
|
socket: socket,
|
||||||
|
servername: url.hostname,
|
||||||
|
// Don't reject unauthorized to avoid system CA issues in CI
|
||||||
|
// We just want to inspect the certificate
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const cert = tlsSocket.getPeerCertificate();
|
||||||
|
|
||||||
|
// Extract issuer and subject information
|
||||||
|
const issuer = cert.issuer ?
|
||||||
|
Object.entries(cert.issuer).map(([k, v]) => `${k}=${v}`).join(", ") :
|
||||||
|
"unknown";
|
||||||
|
const subject = cert.subject ?
|
||||||
|
Object.entries(cert.subject).map(([k, v]) => `${k}=${v}`).join(", ") :
|
||||||
|
"unknown";
|
||||||
|
|
||||||
|
tlsSocket.end();
|
||||||
|
resolve({ issuer, subject });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
tlsSocket.on("error", (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { handleHttpProxyRequest } from "./plainHttpProxy.js";
|
||||||
import { getCaCertPath } from "./certUtils.js";
|
import { getCaCertPath } from "./certUtils.js";
|
||||||
import { auditChanges } from "../scanning/audit/index.js";
|
import { auditChanges } from "../scanning/audit/index.js";
|
||||||
import { knownJsRegistries, knownPipRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js";
|
import { knownJsRegistries, knownPipRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js";
|
||||||
|
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
|
@ -111,8 +112,17 @@ function handleConnect(req, clientSocket, head) {
|
||||||
// CONNECT method is used for HTTPS requests
|
// CONNECT method is used for HTTPS requests
|
||||||
// It establishes a tunnel to the server identified by the request URL
|
// It establishes a tunnel to the server identified by the request URL
|
||||||
|
|
||||||
if ((knownJsRegistries.some((reg) => req.url.includes(reg)))
|
const ecosystem = getEcoSystem();
|
||||||
|| (knownPipRegistries.some((reg) => req.url.includes(reg)))) {
|
const url = req.url || "";
|
||||||
|
|
||||||
|
let isKnownRegistry = false;
|
||||||
|
if (ecosystem === ECOSYSTEM_JS) {
|
||||||
|
isKnownRegistry = knownJsRegistries.some((reg) => url.includes(reg));
|
||||||
|
} else if (ecosystem === ECOSYSTEM_PY) {
|
||||||
|
isKnownRegistry = knownPipRegistries.some((reg) => url.includes(reg));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isKnownRegistry) {
|
||||||
mitmConnect(req, clientSocket, isAllowedUrl);
|
mitmConnect(req, clientSocket, isAllowedUrl);
|
||||||
} else {
|
} else {
|
||||||
// For other hosts, just tunnel the request to the destination tcp socket
|
// For other hosts, just tunnel the request to the destination tcp socket
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
mergeSafeChainProxyEnvironmentVariables,
|
mergeSafeChainProxyEnvironmentVariables,
|
||||||
} from "./registryProxy.js";
|
} from "./registryProxy.js";
|
||||||
import { getCaCertPath } from "./certUtils.js";
|
import { getCaCertPath } from "./certUtils.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
describe("registryProxy.mitm", () => {
|
describe("registryProxy.mitm", () => {
|
||||||
|
|
@ -19,6 +20,8 @@ describe("registryProxy.mitm", () => {
|
||||||
const proxyUrl = new URL(envVars.HTTPS_PROXY);
|
const proxyUrl = new URL(envVars.HTTPS_PROXY);
|
||||||
proxyHost = proxyUrl.hostname;
|
proxyHost = proxyUrl.hostname;
|
||||||
proxyPort = parseInt(proxyUrl.port, 10);
|
proxyPort = parseInt(proxyUrl.port, 10);
|
||||||
|
// Default to JS ecosystem for JS registry tests
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
|
|
@ -151,6 +154,8 @@ describe("registryProxy.mitm", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should intercept HTTPS requests to pypi.org for pip package", async () => {
|
it("should intercept HTTPS requests to pypi.org for pip package", async () => {
|
||||||
|
// Switch to Python ecosystem for pip registry MITM tests
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
const response = await makeRegistryRequest(
|
const response = await makeRegistryRequest(
|
||||||
proxyHost,
|
proxyHost,
|
||||||
proxyPort,
|
proxyPort,
|
||||||
|
|
@ -162,6 +167,8 @@ describe("registryProxy.mitm", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should intercept HTTPS requests to files.pythonhosted.org for pip wheel", async () => {
|
it("should intercept HTTPS requests to files.pythonhosted.org for pip wheel", async () => {
|
||||||
|
// Ensure Python ecosystem
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
const response = await makeRegistryRequest(
|
const response = await makeRegistryRequest(
|
||||||
proxyHost,
|
proxyHost,
|
||||||
proxyPort,
|
proxyPort,
|
||||||
|
|
@ -173,6 +180,8 @@ describe("registryProxy.mitm", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle pip package with a1 version", async () => {
|
it("should handle pip package with a1 version", async () => {
|
||||||
|
// Ensure Python ecosystem
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
const response = await makeRegistryRequest(
|
const response = await makeRegistryRequest(
|
||||||
proxyHost,
|
proxyHost,
|
||||||
proxyPort,
|
proxyPort,
|
||||||
|
|
@ -184,6 +193,8 @@ describe("registryProxy.mitm", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle pip package with latest version (should not block)", async () => {
|
it("should handle pip package with latest version (should not block)", async () => {
|
||||||
|
// Ensure Python ecosystem
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
const response = await makeRegistryRequest(
|
const response = await makeRegistryRequest(
|
||||||
proxyHost,
|
proxyHost,
|
||||||
proxyPort,
|
proxyPort,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
writeDatabaseToLocalCache,
|
writeDatabaseToLocalCache,
|
||||||
} from "../config/configFile.js";
|
} from "../config/configFile.js";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { getEcoSystem } from "../config/settings.js";
|
import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js";
|
||||||
|
|
||||||
let cachedMalwareDatabase = null;
|
let cachedMalwareDatabase = null;
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ let cachedMalwareDatabase = null;
|
||||||
*/
|
*/
|
||||||
function normalizePackageName(name) {
|
function normalizePackageName(name) {
|
||||||
const ecosystem = getEcoSystem();
|
const ecosystem = getEcoSystem();
|
||||||
if (ecosystem === "py") {
|
if (ecosystem === ECOSYSTEM_PY) {
|
||||||
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,7 @@ function createUnixShims(shimsDir) {
|
||||||
|
|
||||||
const template = fs.readFileSync(templatePath, "utf-8");
|
const template = fs.readFileSync(templatePath, "utf-8");
|
||||||
|
|
||||||
// Create a shim for each tool except pip for now.
|
// Create a shim for each tool except pip (CI support not yet implemented)
|
||||||
// TODO(pip): Enable pip and pip3 CI support
|
|
||||||
let created = 0;
|
let created = 0;
|
||||||
for (const toolInfo of knownAikidoTools) {
|
for (const toolInfo of knownAikidoTools) {
|
||||||
if (toolInfo.tool === "pip") {
|
if (toolInfo.tool === "pip") {
|
||||||
|
|
@ -89,8 +88,7 @@ function createWindowsShims(shimsDir) {
|
||||||
|
|
||||||
const template = fs.readFileSync(templatePath, "utf-8");
|
const template = fs.readFileSync(templatePath, "utf-8");
|
||||||
|
|
||||||
// Create a shim for each tool except pip for now.
|
// Create a shim for each tool except pip (CI support not yet implemented)
|
||||||
// TODO(pip): Enable pip and pip3 CI support
|
|
||||||
let created = 0;
|
let created = 0;
|
||||||
for (const toolInfo of knownAikidoTools) {
|
for (const toolInfo of knownAikidoTools) {
|
||||||
if (toolInfo.tool === "pip") {
|
if (toolInfo.tool === "pip") {
|
||||||
|
|
|
||||||
|
|
@ -70,11 +70,9 @@ function npm
|
||||||
end
|
end
|
||||||
|
|
||||||
function pip
|
function pip
|
||||||
# Default to Python 2 major version when explicitly calling pip
|
wrapSafeChainCommand "pip" "aikido-pip" $argv
|
||||||
wrapSafeChainCommand "pip" "aikido-pip" --target-version-major "2" $argv
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function pip3
|
function pip3
|
||||||
# Route to Python 3 when calling pip3
|
wrapSafeChainCommand "pip3" "aikido-pip3" $argv
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip" --target-version-major "3" $argv
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,9 @@ function npm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function pip() {
|
function pip() {
|
||||||
wrapSafeChainCommand "pip" "aikido-pip" --target-version-major "2" "$@"
|
wrapSafeChainCommand "pip" "aikido-pip" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
function pip3() {
|
function pip3() {
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip" --target-version-major "3" "$@"
|
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,13 +88,9 @@ function npm {
|
||||||
}
|
}
|
||||||
|
|
||||||
function pip {
|
function pip {
|
||||||
# Default to Python 2 major version when explicitly calling pip
|
Invoke-WrappedCommand "pip" "aikido-pip" $args
|
||||||
$forward = @("--target-version-major", "2") + $args
|
|
||||||
Invoke-WrappedCommand "pip" "aikido-pip" $forward
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pip3 {
|
function pip3 {
|
||||||
# Route to Python 3 when calling pip3
|
Invoke-WrappedCommand "pip3" "aikido-pip3" $args
|
||||||
$forward = @("--target-version-major", "3") + $args
|
|
||||||
Invoke-WrappedCommand "pip3" "aikido-pip" $forward
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,4 +85,5 @@ describe("E2E: pip coverage", () => {
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue