Merge branch 'main' into run-proxy-from-cli

This commit is contained in:
Sander Declerck 2025-12-04 15:32:41 +01:00
commit fbbca73ef6
No known key found for this signature in database
39 changed files with 3745 additions and 934 deletions

View file

@ -7,10 +7,28 @@ on:
permissions:
id-token: write
contents: read
contents: write
jobs:
set-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.tag }}
steps:
- name: Set version number
id: get_version
run: |
version="${{ github.ref_name }}"
echo "tag=$version" >> $GITHUB_OUTPUT
create-binaries:
needs: set-version
uses: ./.github/workflows/create-artifact.yml
with:
version: ${{ needs.set-version.outputs.version }}
build:
needs: [set-version, create-binaries]
runs-on: ubuntu-latest
steps:
@ -30,14 +48,8 @@ jobs:
npm i -g @aikidosec/safe-chain
safe-chain setup-ci
- name: Set version number
id: get_version
run: |
version="${{ github.ref_name }}"
echo "tag=$version" >> $GITHUB_OUTPUT
- name: Set the version in safe-chain package
run: npm --no-git-tag-version version ${{ steps.get_version.outputs.tag }} --workspace=packages/safe-chain
run: npm --no-git-tag-version version ${{ needs.set-version.outputs.version }} --workspace=packages/safe-chain
- name: Install dependencies
run: npm ci
@ -55,3 +67,31 @@ jobs:
run: |
echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM"
npm publish --workspace=packages/safe-chain --access public --provenance
- name: Download all binary artifacts
uses: actions/download-artifact@v4
with:
path: binaries/
pattern: safe-chain-*
merge-multiple: false
- name: Rename binaries to include platform and architecture
run: |
mv binaries/safe-chain-macos-x64/safe-chain binaries/safe-chain-macos-x64/safe-chain-macos-x64
mv binaries/safe-chain-macos-arm64/safe-chain binaries/safe-chain-macos-arm64/safe-chain-macos-arm64
mv binaries/safe-chain-linux-x64/safe-chain binaries/safe-chain-linux-x64/safe-chain-linux-x64
mv binaries/safe-chain-linux-arm64/safe-chain binaries/safe-chain-linux-arm64/safe-chain-linux-arm64
mv binaries/safe-chain-win-x64/safe-chain.exe binaries/safe-chain-win-x64/safe-chain-win-x64.exe
mv binaries/safe-chain-win-arm64/safe-chain.exe binaries/safe-chain-win-arm64/safe-chain-win-arm64.exe
- name: Upload binaries to existing GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload ${{ needs.set-version.outputs.version }} \
binaries/safe-chain-macos-x64/* \
binaries/safe-chain-macos-arm64/* \
binaries/safe-chain-linux-x64/* \
binaries/safe-chain-linux-arm64/* \
binaries/safe-chain-win-x64/* \
binaries/safe-chain-win-arm64/*

82
.github/workflows/create-artifact.yml vendored Normal file
View file

@ -0,0 +1,82 @@
name: Create binaries
on:
pull_request:
workflow_call:
inputs:
version:
description: 'Version to set in package.json'
required: false
type: string
jobs:
create-binaries:
name: Create binary for ${{ matrix.os }}-${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- os: macos
arch: x64
runner: macos-15-intel
target: node20-macos-x64
extension: ""
- os: macos
arch: arm64
runner: macos-latest
target: node20-macos-arm64
extension: ""
- os: linux
arch: x64
runner: ubuntu-latest
target: node20-linux-x64
extension: ""
- os: linux
arch: arm64
runner: ubuntu-24.04-arm
target: node20-linux-arm64
extension: ""
- os: win
arch: x64
runner: windows-latest
target: node20-win-x64
extension: ".exe"
- os: win
arch: arm64
runner: windows-11-arm
target: node20-win-arm64
extension: ".exe"
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: "20.x"
- name: Setup safe-chain
run: |
npm i -g @aikidosec/safe-chain
safe-chain setup-ci
- name: Set the version in safe-chain package
if: inputs.version != ''
run: npm --no-git-tag-version version ${{ inputs.version }} --workspace=packages/safe-chain
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Create binary
run: |
node build.js ${{ matrix.target }}
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: safe-chain-${{ matrix.os }}-${{ matrix.arch }}
path: dist/*

7
.gitignore vendored
View file

@ -144,3 +144,10 @@ vite.config.ts.timestamp-*
Claude.md
.claude
.reference
# Build files
build/
dist/
# Jetbrains IDEs
.idea/**

109
README.md
View file

@ -1,13 +1,16 @@
![Aikido Safe Chain](./docs/banner.svg)
![Aikido Safe Chain](https://raw.githubusercontent.com/AikidoSec/safe-chain/main/docs/banner.svg)
# Aikido Safe Chain
[![NPM Version](https://img.shields.io/npm/v/%40aikidosec%2Fsafe-chain?style=flat-square)](https://www.npmjs.com/package/@aikidosec/safe-chain)
[![NPM Downloads](https://img.shields.io/npm/dw/%40aikidosec%2Fsafe-chain?style=flat-square)](https://www.npmjs.com/package/@aikidosec/safe-chain)
- ✅ **Block malware on developer laptops and CI/CD**
- ✅ **Supports npm and PyPI** more package managers coming
- ✅ **Blocks packages newer than 24 hours** without breaking your build
- ✅ **Tokenless, free, no build data shared**
Aikido Safe Chain works on Node.js version 16 and above and supports the following package managers:
Aikido Safe Chain supports the following package managers:
- 📦 **npm**
- 📦 **npx**
@ -24,29 +27,45 @@ Aikido Safe Chain works on Node.js version 16 and above and supports the followi
## Installation
Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
Installing the Aikido Safe Chain is easy with our one-line installer.
1. **Install the Aikido Safe Chain package globally** using npm:
```shell
npm install -g @aikidosec/safe-chain
```
2. **Setup the shell integration** by running:
> ⚠️ **Already installed via npm?** See the [migration guide](https://github.com/AikidoSec/safe-chain/blob/main/docs/npm-to-binary-migration.md) to switch to the binary version.
```shell
safe-chain setup
```
### Unix/Linux/macOS
To enable Python (pip/pip3/uv) support (beta), use the `--include-python` flag:
**Default installation (JavaScript packages only):**
```shell
safe-chain setup --include-python
```
```shell
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh
```
3. **❗Restart your terminal** to start using the Aikido Safe Chain.
**Include Python support (pip/pip3/uv):**
```shell
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --include-python
```
### Windows (PowerShell)
**Default installation (JavaScript packages only):**
```powershell
iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing)
```
**Include Python support (pip/pip3/uv):**
```powershell
iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -includepython"
```
### Verify the installation
1. **❗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/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available.
4. **Verify the installation** by running one of the following commands:
2. **Verify the installation** by running one of the following commands:
For JavaScript/Node.js:
@ -54,7 +73,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
npm install safe-chain-test
```
For Python (beta):
For Python (if you enabled Python support):
```shell
pip3 install safe-chain-pi-test
@ -92,7 +111,7 @@ The Aikido Safe Chain integrates with your shell to provide a seamless experienc
- ✅ **PowerShell**
- ✅ **PowerShell Core**
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](https://github.com/AikidoSec/safe-chain/blob/main/docs/shell-integration.md).
## Uninstallation
@ -163,23 +182,37 @@ You can set the minimum package age through multiple sources (in order of priori
You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation.
For optimal protection in CI/CD environments, we recommend using **npm >= 10.4.0** as it provides full dependency tree scanning. Other package managers currently offer limited scanning of install command arguments only.
## Installation for CI/CD
## Setup
Use the `--ci` flag to automatically configure Aikido Safe Chain for CI/CD environments. This sets up executable shims in the PATH instead of shell aliases.
To use Aikido Safe Chain in CI/CD environments, run the following command after installing the package:
### Unix/Linux/macOS (GitHub Actions, Azure Pipelines, etc.)
**JavaScript only:**
```shell
safe-chain setup-ci
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci
```
To enable Python (pip/pip3/uv) support (beta) in CI/CD, use the `--include-python` flag:
**With Python support:**
```shell
safe-chain setup-ci --include-python
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python
```
This automatically configures your CI environment to use Aikido Safe Chain for all package manager commands.
### Windows (Azure Pipelines, etc.)
**JavaScript only:**
```powershell
iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci"
```
**With Python support:**
```powershell
iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci -includepython"
```
## Supported Platforms
@ -195,16 +228,15 @@ This automatically configures your CI environment to use Aikido Safe Chain for a
node-version: "22"
cache: "npm"
- name: Setup safe-chain
run: |
npm i -g @aikidosec/safe-chain
safe-chain setup-ci
- name: Install safe-chain
run: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python
- name: Install dependencies
run: |
npm ci
run: npm ci
```
> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv) support.
## Azure DevOps Example
```yaml
@ -213,14 +245,13 @@ This automatically configures your CI environment to use Aikido Safe Chain for a
versionSpec: "22.x"
displayName: "Install Node.js"
- script: |
npm i -g @aikidosec/safe-chain
safe-chain setup-ci
displayName: "Install safe chain"
- script: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python
displayName: "Install safe-chain"
- script: |
npm ci
displayName: "npm install and build"
- script: npm ci
displayName: "Install dependencies"
```
> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv) support.
After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.

139
build.js Normal file
View file

@ -0,0 +1,139 @@
import { build } from "esbuild";
import { mkdir, cp, rm, readFile, writeFile, stat } from "node:fs/promises";
import { spawn } from "node:child_process";
import { resolve } from "node:path";
const target = process.argv[2];
if (!target) {
// eslint-disable-next-line no-console
console.error("Usage: node build.js <target>");
// eslint-disable-next-line no-console
console.error("Example: node build.js node22-macos-arm64");
process.exit(1);
}
(async function main() {
const startBuildTime = performance.now();
await clearOutputFolder();
console.log("- Cleared output folder ✅")
// Esbuild creates a single safe-chain.cjs with all dependencies included
await bundleSafeChain();
console.log("- Bundled safe-chain into safe-chain.cjs (es-build) ✅")
// Copy assets that need to be included in the binary
// - All shell scripts that are used to setup safe-chain
// - Certifi because it contains static root certs for Python
// - Package.json for its metadata (package name, version, ...)
await copyShellScripts();
await copyCertifi();
await copyAndModifyPackageJson();
console.log("- Copied auxiliary resources (shell, package.json,...) ✅")
// Creates a single binary with safe-chain.cjs and the copied assets
await buildSafeChainBinary(target);
console.log(`- Built safe-chain binary for ${target} (pkg) ✅`)
const totalBuildTime = (performance.now() - startBuildTime)/1000;
const totalSizeInMb = (await stat("./dist/safe-chain" + (process.platform === "win32" ? ".exe" : ""))).size / (1024*1024);
console.log(`🏁 Finished build in ${totalBuildTime.toFixed(2)}s, total build size: ${totalSizeInMb.toFixed(2)}MB`);
})();
async function clearOutputFolder() {
await rm("./build", { recursive: true, force: true });
await mkdir("./build");
}
async function bundleSafeChain() {
await build({
entryPoints: ["./packages/safe-chain/bin/safe-chain.js"],
bundle: true,
platform: "node",
target: "node24",
outfile: "./build/bin/safe-chain.cjs",
external: ["certifi"],
});
let bundledContent = await readFile("./build/bin/safe-chain.cjs", "utf-8");
await writeFile("./build/bin/safe-chain.cjs", bundledContent);
}
async function copyShellScripts() {
await mkdir("./build/bin/startup-scripts", { recursive: true });
await cp(
"./packages/safe-chain/src/shell-integration/startup-scripts/",
"./build/bin/startup-scripts",
{ recursive: true }
);
await mkdir("./build/bin/path-wrappers", { recursive: true });
await cp(
"./packages/safe-chain/src/shell-integration/path-wrappers/",
"./build/bin/path-wrappers",
{ recursive: true }
);
}
async function copyCertifi() {
await mkdir("./build/node_modules/certifi", { recursive: true });
await cp("./node_modules/certifi/", "./build/node_modules/certifi", {
recursive: true,
});
}
async function copyAndModifyPackageJson() {
const packageJsonContent = await readFile(
"./packages/safe-chain/package.json",
"utf-8"
);
const packageJson = JSON.parse(packageJsonContent);
delete packageJson.main;
delete packageJson.scripts;
delete packageJson.exports;
delete packageJson.dependencies;
delete packageJson.devDependencies;
packageJson.bin = {
"safe-chain": "bin/safe-chain.cjs",
};
packageJson.type = "commonjs";
packageJson.pkg = {
outputPath: "dist",
assets: [
"node_modules/certifi/**/*",
"bin/startup-scripts/**/*",
"bin/path-wrappers/**/*",
],
};
await writeFile("./build/package.json", JSON.stringify(packageJson, null, 2));
return packageJson;
}
function buildSafeChainBinary(target) {
return new Promise((promiseResolve, reject) => {
// Use .cmd on Windows, resolve to absolute path for cross-platform compatibility
const pkgBin = process.platform === "win32"
? resolve("node_modules/.bin/pkg.cmd")
: resolve("node_modules/.bin/pkg");
let pkgArgs = [];
pkgArgs = pkgArgs.concat(["./build/package.json", "-t", target]);
const pkg = spawn(pkgBin, pkgArgs, {
stdio: "inherit",
shell: true,
});
pkg.on("close", (code) => {
if (code !== 0) {
reject(new Error(`pkg process exited with code ${code}`));
} else {
promiseResolve();
}
});
});
}

View file

@ -0,0 +1,89 @@
# Migrating from npm global tool to binary installation
If you previously installed safe-chain as an npm global package, you need to migrate to the binary installation.
Depending on the version manager you're using, the uninstall process differs:
### Standard npm (no version manager)
1. **Clean up shell aliases:**
```bash
safe-chain teardown
```
2. **Restart your terminal**
3. **Uninstall the npm package:**
```bash
npm uninstall -g @aikidosec/safe-chain
```
4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation))
### nvm (Node Version Manager)
**Important:** nvm installs global packages separately for each Node version, so safe-chain must be uninstalled from each version where it was installed.
1. **Clean up shell aliases:**
```bash
safe-chain teardown
```
2. **Restart your terminal**
3. **Uninstall from all Node versions:**
**Option A** - Automated script (recommended):
```bash
for version in $(nvm list | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'); do nvm use $version && npm uninstall -g @aikidosec/safe-chain; done
```
**Option B** - Manual per version:
```bash
nvm use <version>
npm uninstall -g @aikidosec/safe-chain
```
Repeat for each Node version where safe-chain was installed.
4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation))
### Volta
1. **Clean up shell aliases:**
```bash
safe-chain teardown
```
2. **Restart your terminal**
3. **Uninstall the Volta package:**
```bash
volta uninstall @aikidosec/safe-chain
```
4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation))
## Troubleshooting
### Shell aliases still present after migration
1. Run `safe-chain teardown` (if the binary is installed)
2. Manually remove any safe-chain entries from your shell config files:
- Bash: `~/.bashrc`
- Zsh: `~/.zshrc`
- Fish: `~/.config/fish/config.fish`
- PowerShell: `$PROFILE`
3. Restart your terminal
4. Re-run the install script
### "command not found: safe-chain" after migration
The binary installation directory (`~/.safe-chain/bin`) may not be in your PATH. Restart your terminal. If the problem persists: re-run the installation of safe-chain.

View file

@ -0,0 +1,217 @@
# Downloads and installs safe-chain for Windows
#
# Usage with "iex (iwr {url} -UseBasicParsing)" --> See README.md
param(
[switch]$ci,
[switch]$includepython
)
$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set
$InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin"
$RepoUrl = "https://github.com/AikidoSec/safe-chain"
# Ensure TLS 1.2 is enabled for downloads
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Helper functions
function Write-Info {
param([string]$Message)
Write-Host "[INFO] $Message" -ForegroundColor Green
}
function Write-Warn {
param([string]$Message)
Write-Host "[WARN] $Message" -ForegroundColor Yellow
}
function Write-Error-Custom {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
exit 1
}
# Fetch latest release version tag from GitHub
function Get-LatestVersion {
try {
$response = Invoke-RestMethod -Uri "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" -UseBasicParsing
$latestVersion = $response.tag_name
if ([string]::IsNullOrWhiteSpace($latestVersion)) {
Write-Error-Custom "Failed to fetch latest version from GitHub API. Please set SAFE_CHAIN_VERSION environment variable."
}
return $latestVersion
}
catch {
Write-Error-Custom "Failed to fetch latest version from GitHub API: $($_.Exception.Message). Please set SAFE_CHAIN_VERSION environment variable."
}
}
# Detect architecture
function Get-Architecture {
$arch = $env:PROCESSOR_ARCHITECTURE
switch ($arch) {
"AMD64" { return "x64" }
"ARM64" { return "arm64" }
default { Write-Error-Custom "Unsupported architecture: $arch" }
}
}
# Check and uninstall npm global package if present
function Remove-NpmInstallation {
# Check if npm is available
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
return
}
# Check if safe-chain is installed as an npm global package
npm list -g @aikidosec/safe-chain 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Info "Detected npm global installation of @aikidosec/safe-chain"
Write-Info "Uninstalling npm version before installing binary version..."
npm uninstall -g @aikidosec/safe-chain 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Info "Successfully uninstalled npm version"
}
else {
Write-Warn "Failed to uninstall npm version automatically"
Write-Warn "Please run: npm uninstall -g @aikidosec/safe-chain"
}
}
}
# Check and uninstall Volta-managed package if present
function Remove-VoltaInstallation {
# Check if Volta is available
if (-not (Get-Command volta -ErrorAction SilentlyContinue)) {
return
}
# Volta manages global packages in its own directory
# Check if safe-chain is installed via Volta
volta list safe-chain 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Info "Detected Volta installation of @aikidosec/safe-chain"
Write-Info "Uninstalling Volta version before installing binary version..."
volta uninstall @aikidosec/safe-chain 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Info "Successfully uninstalled Volta version"
}
else {
Write-Warn "Failed to uninstall Volta version automatically"
Write-Warn "Please run: volta uninstall @aikidosec/safe-chain"
}
}
}
# Main installation
function Install-SafeChain {
# Fetch latest version if VERSION is not set
if ([string]::IsNullOrWhiteSpace($Version)) {
Write-Info "Fetching latest release version..."
$Version = Get-LatestVersion
}
# Build installation message
$installMsg = "Installing safe-chain $Version"
if ($includepython) {
$installMsg += " with python"
}
if ($ci) {
$installMsg += " in ci"
}
Write-Info $installMsg
# Check for existing safe-chain installation through npm or volta
Remove-NpmInstallation
Remove-VoltaInstallation
# Detect platform
$arch = Get-Architecture
$binaryName = "safe-chain-win-$arch.exe"
Write-Info "Detected architecture: $arch"
# Create installation directory
if (-not (Test-Path $InstallDir)) {
Write-Info "Creating installation directory: $InstallDir"
try {
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
}
catch {
Write-Error-Custom "Failed to create directory $InstallDir : $_"
}
}
# Download binary
$downloadUrl = "$RepoUrl/releases/download/$Version/$binaryName"
$tempFile = Join-Path $InstallDir $binaryName
Write-Info "Downloading from: $downloadUrl"
try {
# Download with progress suppressed for cleaner output
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile -UseBasicParsing
$ProgressPreference = 'Continue'
}
catch {
Write-Error-Custom "Failed to download from $downloadUrl : $_"
}
# Rename to final location
$finalFile = Join-Path $InstallDir "safe-chain.exe"
try {
# Remove existing file if present (Move-Item -Force doesn't overwrite)
if (Test-Path $finalFile) {
Remove-Item -Path $finalFile -Force
}
Move-Item -Path $tempFile -Destination $finalFile -Force
}
catch {
Write-Error-Custom "Failed to move binary to $finalFile : $_"
}
Write-Info "Binary installed to: $finalFile"
# Build setup command based on parameters
$setupCmd = if ($ci) { "setup-ci" } else { "setup" }
$setupArgs = @()
if ($includepython) {
$setupArgs += "--include-python"
}
# Execute safe-chain setup
Write-Info "Running safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })..."
try {
$env:Path = "$env:Path;$InstallDir"
if ($setupArgs) {
& $finalFile $setupCmd $setupArgs
}
else {
& $finalFile $setupCmd
}
if ($LASTEXITCODE -ne 0) {
Write-Warn "safe-chain was installed but setup encountered issues."
Write-Warn "You can run 'safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })' manually later."
}
}
catch {
Write-Warn "safe-chain was installed but setup encountered issues: $_"
Write-Warn "You can run 'safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })' manually later."
}
}
# Run installation
try {
Install-SafeChain
}
catch {
Write-Error-Custom "Installation failed: $_"
}

View file

@ -0,0 +1,224 @@
#!/bin/sh
# Downloads and installs safe-chain, depending on the operating system and architecture
#
# Usage with "curl -fsSL {url} | sh" --> See README.md
set -e # Exit on error
# Configuration
VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set
INSTALL_DIR="${HOME}/.safe-chain/bin"
REPO_URL="https://github.com/AikidoSec/safe-chain"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Helper functions
info() {
printf "${GREEN}[INFO]${NC} %s\n" "$1"
}
warn() {
printf "${YELLOW}[WARN]${NC} %s\n" "$1"
}
error() {
printf "${RED}[ERROR]${NC} %s\n" "$1" >&2
exit 1
}
# Detect OS
detect_os() {
case "$(uname -s)" in
Linux*) echo "linux" ;;
Darwin*) echo "macos" ;;
*) error "Unsupported operating system: $(uname -s)" ;;
esac
}
# Detect architecture
detect_arch() {
case "$(uname -m)" in
x86_64|amd64) echo "x64" ;;
aarch64|arm64) echo "arm64" ;;
*) error "Unsupported architecture: $(uname -m)" ;;
esac
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Fetch latest release version tag from GitHub
fetch_latest_version() {
# Try using GitHub API to get the latest release tag
if command_exists curl; then
latest_version=$(curl -fsSL "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
elif command_exists wget; then
latest_version=$(wget -qO- "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
else
error "Neither curl nor wget found. Please install one of them or set SAFE_CHAIN_VERSION environment variable."
fi
if [ -z "$latest_version" ]; then
error "Failed to fetch latest version from GitHub API. Please set SAFE_CHAIN_VERSION environment variable."
fi
echo "$latest_version"
}
# Download file
download() {
url="$1"
dest="$2"
if command_exists curl; then
curl -fsSL "$url" -o "$dest" || error "Failed to download from $url"
elif command_exists wget; then
wget -q "$url" -O "$dest" || error "Failed to download from $url"
else
error "Neither curl nor wget found. Please install one of them."
fi
}
# Check and uninstall npm global package if present
remove_npm_installation() {
if ! command_exists npm; then
return
fi
# Check if safe-chain is installed as an npm global package
if npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then
info "Detected npm global installation of @aikidosec/safe-chain"
info "Uninstalling npm version before installing binary version..."
if npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then
info "Successfully uninstalled npm version"
else
warn "Failed to uninstall npm version automatically"
warn "Please run: npm uninstall -g @aikidosec/safe-chain"
fi
fi
}
# Check and uninstall Volta-managed package if present
remove_volta_installation() {
if ! command_exists volta; then
return
fi
# Volta manages global packages in its own directory
# Check if safe-chain is installed via Volta
if volta list safe-chain >/dev/null 2>&1; then
info "Detected Volta installation of @aikidosec/safe-chain"
info "Uninstalling Volta version before installing binary version..."
if volta uninstall @aikidosec/safe-chain >/dev/null 2>&1; then
info "Successfully uninstalled Volta version"
else
warn "Failed to uninstall Volta version automatically"
warn "Please run: volta uninstall @aikidosec/safe-chain"
fi
fi
}
# Parse command-line arguments
parse_arguments() {
for arg in "$@"; do
case "$arg" in
--ci)
USE_CI_SETUP=true
;;
--include-python)
INCLUDE_PYTHON=true
;;
*)
error "Unknown argument: $arg"
;;
esac
done
}
# Main installation
main() {
# Initialize argument flags
USE_CI_SETUP=false
INCLUDE_PYTHON=false
# Parse command-line arguments
parse_arguments "$@"
# Fetch latest version if VERSION is not set
if [ -z "$VERSION" ]; then
info "Fetching latest release version..."
VERSION=$(fetch_latest_version)
fi
# Build installation message
INSTALL_MSG="Installing safe-chain ${VERSION}"
if [ "$INCLUDE_PYTHON" = "true" ]; then
INSTALL_MSG="${INSTALL_MSG} with python"
fi
if [ "$USE_CI_SETUP" = "true" ]; then
INSTALL_MSG="${INSTALL_MSG} in ci"
fi
info "$INSTALL_MSG"
# Check for existing safe-chain installation through npm or volta
remove_npm_installation
remove_volta_installation
# Detect platform
OS=$(detect_os)
ARCH=$(detect_arch)
BINARY_NAME="safe-chain-${OS}-${ARCH}"
info "Detected platform: ${OS}-${ARCH}"
# Create installation directory
if [ ! -d "$INSTALL_DIR" ]; then
info "Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR" || error "Failed to create directory $INSTALL_DIR"
fi
# Download binary
DOWNLOAD_URL="${REPO_URL}/releases/download/${VERSION}/${BINARY_NAME}"
TEMP_FILE="${INSTALL_DIR}/${BINARY_NAME}"
info "Downloading from: $DOWNLOAD_URL"
download "$DOWNLOAD_URL" "$TEMP_FILE"
# Rename and make executable
FINAL_FILE="${INSTALL_DIR}/safe-chain"
mv "$TEMP_FILE" "$FINAL_FILE" || error "Failed to move binary to $FINAL_FILE"
chmod +x "$FINAL_FILE" || error "Failed to make binary executable"
info "Binary installed to: $FINAL_FILE"
# Build setup command based on arguments
SETUP_CMD="setup"
SETUP_ARGS=""
if [ "$USE_CI_SETUP" = "true" ]; then
SETUP_CMD="setup-ci"
fi
if [ "$INCLUDE_PYTHON" = "true" ]; then
SETUP_ARGS="--include-python"
fi
# Execute safe-chain setup
info "Running safe-chain $SETUP_CMD $SETUP_ARGS..."
if ! "$FINAL_FILE" $SETUP_CMD $SETUP_ARGS; then
warn "safe-chain was installed but setup encountered issues."
warn "You can run 'safe-chain $SETUP_CMD $SETUP_ARGS' manually later."
fi
}
main "$@"

2379
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -19,6 +19,8 @@
"author": "Aikido Security",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"oxlint": "^1.22.0"
"oxlint": "^1.22.0",
"esbuild": "^0.27.0",
"@yao-pkg/pkg": "6.10.1"
}
}

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "bun";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "bunx";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "npm";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "npx";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -13,6 +13,8 @@ setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
initializePackageManager(PIP_PACKAGE_MANAGER);
// Pass through only user-supplied pip args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
// Pass through only user-supplied pip args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -14,6 +14,8 @@ setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
// Create package manager
initializePackageManager(PIP_PACKAGE_MANAGER);
// Pass through only user-supplied pip args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
// Pass through only user-supplied pip args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "pnpm";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "pnpx";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -11,7 +11,8 @@ setEcoSystem(ECOSYSTEM_PY);
// Strip nodejs and wrapper script from args
let argv = process.argv.slice(2);
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
(async () => {
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
setEcoSystem(ECOSYSTEM_PY);
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP);
initializePackageManager(PIP_PACKAGE_MANAGER);
@ -21,8 +22,9 @@ if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
var exitCode = await main(argv);
process.exit(exitCode);
} else {
} else {
// Forward to real python binary for non-pip flows
const { spawn } = await import('child_process');
spawn('python', argv, { stdio: 'inherit' });
}
}
})();

View file

@ -11,7 +11,8 @@ setEcoSystem(ECOSYSTEM_PY);
// Strip nodejs and wrapper script from args
let argv = process.argv.slice(2);
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
(async () => {
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
setEcoSystem(ECOSYSTEM_PY);
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP);
initializePackageManager(PIP_PACKAGE_MANAGER);
@ -21,8 +22,9 @@ if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
var exitCode = await main(argv);
process.exit(exitCode);
} else {
} else {
// Forward to real python3 binary for non-pip flows
const { spawn } = await import('child_process');
spawn('python3', argv, { stdio: 'inherit' });
}
}
})();

View file

@ -9,6 +9,8 @@ setEcoSystem(ECOSYSTEM_PY);
initializePackageManager("uv");
// Pass through only user-supplied uv args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
// Pass through only user-supplied uv args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "yarn";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -1,13 +1,38 @@
#!/usr/bin/env node
import chalk from "chalk";
import { createRequire } from "module";
import { ui } from "../src/environment/userInteraction.js";
import { setup } from "../src/shell-integration/setup.js";
import { teardown } from "../src/shell-integration/teardown.js";
import { setupCi } from "../src/shell-integration/setup-ci.js";
import { initializeCliArguments } from "../src/config/cliArguments.js";
import { runProxy } from "../src/run-proxy.js";
import { setEcoSystem } from "../src/config/settings.js";
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
import { main } from "../src/main.js";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";
import { knownAikidoTools } from "../src/shell-integration/helpers.js";
import {
PIP_INVOCATIONS,
PIP_PACKAGE_MANAGER,
setCurrentPipInvocation,
} from "../src/packagemanager/pip/pipSettings.js";
/** @type {string} */
// This checks the current file's dirname in a way that's compatible with:
// - Modulejs (import.meta.url)
// - ES modules (__dirname)
// This is needed because safe-chain's npm package is built using ES modules,
// but building the binaries requires commonjs.
let dirname;
if (import.meta.url) {
const filename = fileURLToPath(import.meta.url);
dirname = path.dirname(filename);
} else {
dirname = __dirname;
}
if (process.argv.length < 3) {
ui.writeError("No command provided. Please provide a command to execute.");
@ -20,8 +45,25 @@ initializeCliArguments(process.argv);
const command = process.argv[2];
if (command === "help" || command === "--help" || command === "-h") {
const tool = knownAikidoTools.find((tool) => tool.tool === command);
if (tool && tool.internalPackageManagerName === PIP_PACKAGE_MANAGER) {
(async function () {
await executePip(tool);
})();
} else if (tool) {
const args = process.argv.slice(3);
setEcoSystem(tool.ecoSystem);
initializePackageManager(tool.internalPackageManagerName);
(async () => {
var exitCode = await main(args);
process.exit(exitCode);
})();
} else if (command === "help" || command === "--help" || command === "-h") {
writeHelp();
process.exit(0);
} else if (command === "setup") {
setup();
} else if (command === "teardown") {
@ -31,7 +73,9 @@ if (command === "help" || command === "--help" || command === "-h") {
} else if (command === "run-proxy") {
await runProxy(process.argv.slice(2));
} else if (command === "--version" || command === "-v" || command === "-v") {
ui.writeInformation(`Current safe-chain version: ${getVersion()}`);
(async () => {
ui.writeInformation(`Current safe-chain version: ${await getVersion()}`);
})();
} else {
ui.writeError(`Unknown command: ${command}.`);
ui.emptyLine();
@ -87,8 +131,63 @@ function writeHelp() {
ui.emptyLine();
}
function getVersion() {
const require = createRequire(import.meta.url);
const packageJson = require("../package.json");
return packageJson.version;
async function getVersion() {
const packageJsonPath = path.join(dirname, "..", "package.json");
const data = await fs.promises.readFile(packageJsonPath);
const json = JSON.parse(data.toString("utf8"));
if (json && json.version) {
return json.version;
}
return "0.0.0";
}
/**
* @param {import("../src/shell-integration/helpers.js").AikidoTool} tool
*/
async function executePip(tool) {
// Scanners for pip / pip3 / python / python3 use a slightly different approach:
// - They all use the same PIP_PACKAGE_MANAGER internally, but need some setup to be able to do so
// - It needs to set which tool to run (pip / pip3 / python / python3)
// - For python and python3, the -m pip/pip3 args are removed and later added again by the package manager
// - Python / python3 skips safe-chain if not being run with -m pip or -m pip3
let args = process.argv.slice(3);
setEcoSystem(tool.ecoSystem);
initializePackageManager(PIP_PACKAGE_MANAGER);
let shouldSkip = false;
if (tool.tool === "pip") {
setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
} else if (tool.tool === "pip3") {
setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
} else if (tool.tool === "python") {
if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) {
setCurrentPipInvocation(
args[1] === "pip3" ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP
);
args = args.slice(2);
} else {
shouldSkip = true;
}
} else if (tool.tool === "python3") {
if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) {
setCurrentPipInvocation(
args[1] === "pip3" ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP
);
args = args.slice(2);
} else {
shouldSkip = true;
}
}
if (shouldSkip) {
const { spawn } = await import("child_process");
spawn(tool.tool, args, { stdio: "inherit" });
} else {
var exitCode = await main(args);
process.exit(exitCode);
}
}

View file

@ -40,9 +40,9 @@
"chalk": "5.4.1",
"https-proxy-agent": "7.0.6",
"ini": "6.0.0",
"make-fetch-happen": "14.0.3",
"node-forge": "1.3.1",
"npm-registry-fetch": "18.0.2",
"make-fetch-happen": "15.0.3",
"node-forge": "1.3.2",
"npm-registry-fetch": "19.1.1",
"semver": "7.7.2"
},
"devDependencies": {
@ -52,6 +52,7 @@
"@types/node-forge": "^1.3.14",
"@types/npm-registry-fetch": "^8.0.9",
"@types/semver": "^7.7.1",
"esbuild": "^0.27.0",
"typescript": "^5.9.3"
},
"main": "src/main.js",

View file

@ -46,6 +46,9 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
* If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges
* their settings with safe-chain's, leaving the original file unchanged.
*
* Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow
* users to read/write persistent config. Only CA environment variables are set for these commands.
*
* @param {string} command - The pip command to execute (e.g., 'pip3')
* @param {string[]} args - Command line arguments to pass to pip
* @returns {Promise<{status: number}>} Exit status of the pip command
@ -59,6 +62,12 @@ export async function runPip(command, args) {
// validates correctly under both MITM'd and tunneled HTTPS.
const combinedCaPath = getCombinedCaBundlePath();
// Commands that need access to persistent config/cache/state files
// These should not have PIP_CONFIG_FILE overridden as it would prevent them from
// reading/writing to the user's actual pip configuration and cache directories
const configRelatedCommands = ['config', 'cache', 'debug', 'completion'];
const isConfigRelatedCommand = args.length > 0 && configRelatedCommands.includes(args[0]);
// https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file)
// will tell pip to use the provided CA bundle for HTTPS verification.
@ -70,6 +79,22 @@ export async function runPip(command, args) {
const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`);
let cleanupConfigPath = null; // Track temp file for cleanup
if (isConfigRelatedCommand) {
ui.writeVerbose(`Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`);
// Still set the fallback CA bundle environment variables to avoid edge cases where a
// plugin or extension triggers a network call during config introspection
// This can do no harm
setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
const result = await safeSpawn(command, args, {
stdio: "inherit",
env,
});
return { status: result.status };
}
// Note: Setting PIP_CONFIG_FILE overrides all pip config levels (Global/User/Site) per pip's loading order
if (!env.PIP_CONFIG_FILE) {
/** @type {{ global: { cert: string, proxy?: string } }} */

View file

@ -62,6 +62,103 @@ describe("runPipCommand environment variable handling", () => {
mock.reset();
});
it("should NOT set PIP_CONFIG_FILE for 'pip config' commands to allow persistent config access", async () => {
const res = await runPip("pip3", ["config", "set", "global.index-url", "https://test.pypi.org/simple"]);
assert.strictEqual(res.status, 0);
assert.ok(capturedArgs, "safeSpawn should have been called");
// PIP_CONFIG_FILE should NOT be set for config commands
assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE,
undefined,
"PIP_CONFIG_FILE should NOT be set for pip config commands"
);
// But CA environment variables should still be set
assert.strictEqual(
capturedArgs.options.env.REQUESTS_CA_BUNDLE,
"/tmp/test-combined-ca.pem",
"REQUESTS_CA_BUNDLE should still be set"
);
assert.strictEqual(
capturedArgs.options.env.SSL_CERT_FILE,
"/tmp/test-combined-ca.pem",
"SSL_CERT_FILE should still be set"
);
assert.strictEqual(
capturedArgs.options.env.PIP_CERT,
"/tmp/test-combined-ca.pem",
"PIP_CERT should still be set"
);
});
it("should NOT set PIP_CONFIG_FILE for 'pip config get' commands", async () => {
const res = await runPip("pip3", ["config", "get", "global.index-url"]);
assert.strictEqual(res.status, 0);
assert.ok(capturedArgs, "safeSpawn should have been called");
assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE,
undefined,
"PIP_CONFIG_FILE should NOT be set for pip config get"
);
});
it("should NOT set PIP_CONFIG_FILE for 'pip config list' commands", async () => {
const res = await runPip("pip3", ["config", "list"]);
assert.strictEqual(res.status, 0);
assert.ok(capturedArgs, "safeSpawn should have been called");
assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE,
undefined,
"PIP_CONFIG_FILE should NOT be set for pip config list"
);
});
it("should NOT set PIP_CONFIG_FILE for 'pip cache' commands", async () => {
const res = await runPip("pip3", ["cache", "dir"]);
assert.strictEqual(res.status, 0);
assert.ok(capturedArgs, "safeSpawn should have been called");
assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE,
undefined,
"PIP_CONFIG_FILE should NOT be set for pip cache commands"
);
// CA env vars should still be set
assert.strictEqual(
capturedArgs.options.env.SSL_CERT_FILE,
"/tmp/test-combined-ca.pem",
"SSL_CERT_FILE should still be set"
);
});
it("should NOT set PIP_CONFIG_FILE for 'pip debug' commands", async () => {
const res = await runPip("pip3", ["debug"]);
assert.strictEqual(res.status, 0);
assert.ok(capturedArgs, "safeSpawn should have been called");
assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE,
undefined,
"PIP_CONFIG_FILE should NOT be set for pip debug"
);
});
it("should NOT set PIP_CONFIG_FILE for 'pip completion' commands", async () => {
const res = await runPip("pip3", ["completion", "--bash"]);
assert.strictEqual(res.status, 0);
assert.ok(capturedArgs, "safeSpawn should have been called");
assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE,
undefined,
"PIP_CONFIG_FILE should NOT be set for pip completion"
);
});
it("should set PIP_CERT env var and create config file", async () => {
const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0);

View file

@ -117,14 +117,16 @@ function forwardRequest(req, hostname, res, requestHandler) {
proxyReq.on("error", (err) => {
ui.writeVerbose(
`Safe-chain: Error occurred while proxying request: ${err.message}`
`Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}`
);
res.writeHead(502);
res.end("Bad Gateway");
});
req.on("error", (err) => {
ui.writeError(`Safe-chain: Error reading client request: ${err.message}`);
ui.writeError(
`Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}`
);
proxyReq.destroy();
});
@ -175,7 +177,7 @@ function createProxyRequest(hostname, req, res, requestHandler) {
const proxyReq = https.request(options, (proxyRes) => {
proxyRes.on("error", (err) => {
ui.writeError(
`Safe-chain: Error reading upstream response: ${err.message}`
`Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}`
);
if (!res.headersSent) {
res.writeHead(502);
@ -184,7 +186,9 @@ function createProxyRequest(hostname, req, res, requestHandler) {
});
if (!proxyRes.statusCode) {
ui.writeError("Safe-chain: Proxy response missing status code");
ui.writeError(
`Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}`
);
res.writeHead(500);
res.end("Internal Server Error");
return;

View file

@ -9,24 +9,85 @@ import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
* @property {string} tool
* @property {string} aikidoCommand
* @property {string} ecoSystem
* @property {string} internalPackageManagerName
*/
/**
* @type {AikidoTool[]}
*/
export const knownAikidoTools = [
{ tool: "npm", aikidoCommand: "aikido-npm", ecoSystem: ECOSYSTEM_JS },
{ tool: "npx", aikidoCommand: "aikido-npx", ecoSystem: ECOSYSTEM_JS },
{ tool: "yarn", aikidoCommand: "aikido-yarn", ecoSystem: ECOSYSTEM_JS },
{ tool: "pnpm", aikidoCommand: "aikido-pnpm", ecoSystem: ECOSYSTEM_JS },
{ tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS },
{ tool: "bun", aikidoCommand: "aikido-bun", 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: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY },
{ tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY },
{ tool: "python3", aikidoCommand: "aikido-python3", ecoSystem: ECOSYSTEM_PY },
{
tool: "npm",
aikidoCommand: "aikido-npm",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "npm",
},
{
tool: "npx",
aikidoCommand: "aikido-npx",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "npx",
},
{
tool: "yarn",
aikidoCommand: "aikido-yarn",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "yarn",
},
{
tool: "pnpm",
aikidoCommand: "aikido-pnpm",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "pnpm",
},
{
tool: "pnpx",
aikidoCommand: "aikido-pnpx",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "pnpx",
},
{
tool: "bun",
aikidoCommand: "aikido-bun",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "bun",
},
{
tool: "bunx",
aikidoCommand: "aikido-bunx",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "bunx",
},
{
tool: "uv",
aikidoCommand: "aikido-uv",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "uv",
},
{
tool: "pip",
aikidoCommand: "aikido-pip",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "pip",
},
{
tool: "pip3",
aikidoCommand: "aikido-pip3",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "pip",
},
{
tool: "python",
aikidoCommand: "aikido-python",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "pip",
},
{
tool: "python3",
aikidoCommand: "aikido-python3",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "pip",
},
// When adding a new tool here, also update the documentation for the new tool in the README.md
];

View file

@ -7,9 +7,9 @@ remove_shim_from_path() {
echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g"
}
if command -v {{AIKIDO_COMMAND}} >/dev/null 2>&1; then
if command -v safe-chain >/dev/null 2>&1; then
# Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
PATH=$(remove_shim_from_path) exec {{AIKIDO_COMMAND}} "$@"
PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
else
# Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory)
original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})

View file

@ -7,10 +7,10 @@ set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims"
call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
REM Check if aikido command is available with clean PATH
set "PATH=%CLEAN_PATH%" & where {{AIKIDO_COMMAND}} >nul 2>&1
set "PATH=%CLEAN_PATH%" & where safe-chain >nul 2>&1
if %errorlevel%==0 (
REM Call aikido command with clean PATH
set "PATH=%CLEAN_PATH%" & {{AIKIDO_COMMAND}} %*
set "PATH=%CLEAN_PATH%" & safe-chain {{PACKAGE_MANAGER}} %*
) else (
REM Find the original command with clean PATH
for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where {{PACKAGE_MANAGER}} 2^>nul') do (

View file

@ -8,6 +8,20 @@ import { fileURLToPath } from "url";
import { includePython } from "../config/cliArguments.js";
import { ECOSYSTEM_PY } from "../config/settings.js";
/** @type {string} */
// This checks the current file's dirname in a way that's compatible with:
// - Modulejs (import.meta.url)
// - ES modules (__dirname)
// This is needed because safe-chain's npm package is built using ES modules,
// but building the binaries requires commonjs.
let dirname;
if (import.meta.url) {
const filename = fileURLToPath(import.meta.url);
dirname = path.dirname(filename);
} else {
dirname = __dirname;
}
/**
* Loops over the detected shells and calls the setup function for each.
*/
@ -19,6 +33,7 @@ export async function setupCi() {
ui.emptyLine();
const shimsDir = path.join(os.homedir(), ".safe-chain", "shims");
const binDir = path.join(os.homedir(), ".safe-chain", "bin");
// Create the shims directory if it doesn't exist
if (!fs.existsSync(shimsDir)) {
fs.mkdirSync(shimsDir, { recursive: true });
@ -26,7 +41,7 @@ export async function setupCi() {
createShims(shimsDir);
ui.writeInformation(`Created shims in ${shimsDir}`);
modifyPathForCi(shimsDir);
modifyPathForCi(shimsDir, binDir);
ui.writeInformation(`Added shims directory to PATH for CI environments.`);
}
@ -37,10 +52,8 @@ export async function setupCi() {
*/
function createUnixShims(shimsDir) {
// Read the template file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const templatePath = path.resolve(
__dirname,
dirname,
"path-wrappers",
"templates",
"unix-wrapper.template.sh"
@ -78,10 +91,8 @@ function createUnixShims(shimsDir) {
*/
function createWindowsShims(shimsDir) {
// Read the template file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const templatePath = path.resolve(
__dirname,
dirname,
"path-wrappers",
"templates",
"windows-wrapper.template.cmd"
@ -124,13 +135,18 @@ function createShims(shimsDir) {
/**
* @param {string} shimsDir
* @param {string} binDir
*
* @returns {void}
*/
function modifyPathForCi(shimsDir) {
function modifyPathForCi(shimsDir, binDir) {
if (process.env.GITHUB_PATH) {
// In GitHub Actions, append the shims directory to GITHUB_PATH
fs.appendFileSync(process.env.GITHUB_PATH, shimsDir + os.EOL, "utf-8");
fs.appendFileSync(
process.env.GITHUB_PATH,
shimsDir + os.EOL + binDir + os.EOL,
"utf-8"
);
ui.writeInformation(
`Added shims directory to GITHUB_PATH for GitHub Actions.`
);
@ -141,6 +157,7 @@ function modifyPathForCi(shimsDir) {
// ##vso[task.prependpath]/path/to/add
// Logging this to stdout will cause the Azure Pipelines agent to pick it up
ui.writeInformation("##vso[task.prependpath]" + shimsDir);
ui.writeInformation("##vso[task.prependpath]" + binDir);
}
}

View file

@ -5,8 +5,22 @@ import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
import fs from "fs";
import os from "os";
import path from "path";
import { fileURLToPath } from "url";
import { includePython } from "../config/cliArguments.js";
import { fileURLToPath } from "url";
/** @type {string} */
// This checks the current file's dirname in a way that's compatible with:
// - Modulejs (import.meta.url)
// - ES modules (__dirname)
// This is needed because safe-chain's npm package is built using ES modules,
// but building the binaries requires commonjs.
let dirname;
if (import.meta.url) {
const filename = fileURLToPath(import.meta.url);
dirname = path.dirname(filename);
} else {
dirname = __dirname;
}
/**
* Loops over the detected shells and calls the setup function for each.
@ -103,10 +117,8 @@ function copyStartupFiles() {
}
// Use absolute path for source
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const sourcePath = path.resolve(
__dirname,
const sourcePath = path.join(
dirname,
includePython() ? "startup-scripts/include-python" : "startup-scripts",
file
);

View file

@ -1,3 +1,67 @@
set -gx PATH $PATH $HOME/.safe-chain/bin
function npx
wrapSafeChainCommand "npx" $argv
end
function yarn
wrapSafeChainCommand "yarn" $argv
end
function pnpm
wrapSafeChainCommand "pnpm" $argv
end
function pnpx
wrapSafeChainCommand "pnpx" $argv
end
function bun
wrapSafeChainCommand "bun" $argv
end
function bunx
wrapSafeChainCommand "bunx" $argv
end
function npm
# If args is just -v or --version and nothing else, just run the `npm -v` command
# This is because nvm uses this to check the version of npm
set argc (count $argv)
if test $argc -eq 1
switch $argv[1]
case "-v" "--version"
command npm $argv
return
end
end
wrapSafeChainCommand "npm" $argv
end
function pip
wrapSafeChainCommand "pip" $argv
end
function pip3
wrapSafeChainCommand "pip3" $argv
end
function uv
wrapSafeChainCommand "uv" $argv
end
# `python -m pip`, `python -m pip3`.
function python
wrapSafeChainCommand "python" $argv
end
# `python3 -m pip`, `python3 -m pip3'.
function python3
wrapSafeChainCommand "python3" $argv
end
function printSafeChainWarning
set original_cmd $argv[1]
@ -17,76 +81,14 @@ end
function wrapSafeChainCommand
set original_cmd $argv[1]
set aikido_cmd $argv[2]
set cmd_args $argv[3..-1]
set cmd_args $argv[2..-1]
if type -q $aikido_cmd
# If the aikido command is available, just run it with the provided arguments
$aikido_cmd $cmd_args
if type -q safe-chain
# If the safe-chain command is available, just run it with the provided arguments
safe-chain $original_cmd $cmd_args
else
# If the aikido command is not available, print a warning and run the original command
# If the safe-chain command is not available, print a warning and run the original command
printSafeChainWarning $original_cmd
command $original_cmd $cmd_args
end
end
function npx
wrapSafeChainCommand "npx" "aikido-npx" $argv
end
function yarn
wrapSafeChainCommand "yarn" "aikido-yarn" $argv
end
function pnpm
wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv
end
function pnpx
wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv
end
function bun
wrapSafeChainCommand "bun" "aikido-bun" $argv
end
function bunx
wrapSafeChainCommand "bunx" "aikido-bunx" $argv
end
function npm
# If args is just -v or --version and nothing else, just run the `npm -v` command
# This is because nvm uses this to check the version of npm
set argc (count $argv)
if test $argc -eq 1
switch $argv[1]
case "-v" "--version"
command npm $argv
return
end
end
wrapSafeChainCommand "npm" "aikido-npm" $argv
end
function pip
wrapSafeChainCommand "pip" "aikido-pip" $argv
end
function pip3
wrapSafeChainCommand "pip3" "aikido-pip3" $argv
end
function uv
wrapSafeChainCommand "uv" "aikido-uv" $argv
end
# `python -m pip`, `python -m pip3`.
function python
wrapSafeChainCommand "python" "aikido-python" $argv
end
# `python3 -m pip`, `python3 -m pip3'.
function python3
wrapSafeChainCommand "python3" "aikido-python3" $argv
end

View file

@ -1,3 +1,62 @@
export PATH="$PATH:$HOME/.safe-chain/bin"
function npx() {
wrapSafeChainCommand "npx" "$@"
}
function yarn() {
wrapSafeChainCommand "yarn" "$@"
}
function pnpm() {
wrapSafeChainCommand "pnpm" "$@"
}
function pnpx() {
wrapSafeChainCommand "pnpx" "$@"
}
function bun() {
wrapSafeChainCommand "bun" "$@"
}
function bunx() {
wrapSafeChainCommand "bunx" "$@"
}
function npm() {
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
command npm "$@"
return
fi
wrapSafeChainCommand "npm" "$@"
}
function pip() {
wrapSafeChainCommand "pip" "$@"
}
function pip3() {
wrapSafeChainCommand "pip3" "$@"
}
function uv() {
wrapSafeChainCommand "uv" "$@"
}
# `python -m pip`, `python -m pip3`.
function python() {
wrapSafeChainCommand "python" "$@"
}
# `python3 -m pip`, `python3 -m pip3'.
function python3() {
wrapSafeChainCommand "python3" "$@"
}
function printSafeChainWarning() {
# \033[43;30m is used to set the background color to yellow and text color to black
@ -9,15 +68,10 @@ function printSafeChainWarning() {
function wrapSafeChainCommand() {
local original_cmd="$1"
local aikido_cmd="$2"
# Remove the first 2 arguments (original_cmd and aikido_cmd) from $@
# so that "$@" now contains only the arguments passed to the original command
shift 2
if command -v "$aikido_cmd" > /dev/null 2>&1; then
if command -v safe-chain > /dev/null 2>&1; then
# If the aikido command is available, just run it with the provided arguments
"$aikido_cmd" "$@"
safe-chain "$@"
else
# If the aikido command is not available, print a warning and run the original command
printSafeChainWarning "$original_cmd"
@ -25,60 +79,3 @@ function wrapSafeChainCommand() {
command "$original_cmd" "$@"
fi
}
function npx() {
wrapSafeChainCommand "npx" "aikido-npx" "$@"
}
function yarn() {
wrapSafeChainCommand "yarn" "aikido-yarn" "$@"
}
function pnpm() {
wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@"
}
function pnpx() {
wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@"
}
function bun() {
wrapSafeChainCommand "bun" "aikido-bun" "$@"
}
function bunx() {
wrapSafeChainCommand "bunx" "aikido-bunx" "$@"
}
function npm() {
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
command npm "$@"
return
fi
wrapSafeChainCommand "npm" "aikido-npm" "$@"
}
function pip() {
wrapSafeChainCommand "pip" "aikido-pip" "$@"
}
function pip3() {
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
}
function uv() {
wrapSafeChainCommand "uv" "aikido-uv" "$@"
}
# `python -m pip`, `python -m pip3`.
function python() {
wrapSafeChainCommand "python" "aikido-python" "$@"
}
# `python3 -m pip`, `python3 -m pip3'.
function python3() {
wrapSafeChainCommand "python3" "aikido-python3" "$@"
}

View file

@ -1,3 +1,66 @@
# Use cross-platform path separator (: on Unix, ; on Windows)
$pathSeparator = if ($IsWindows) { ';' } else { ':' }
$safeChainBin = Join-Path $HOME '.safe-chain' 'bin'
$env:PATH = "$env:PATH$pathSeparator$safeChainBin"
function npx {
Invoke-WrappedCommand "npx" $args
}
function yarn {
Invoke-WrappedCommand "yarn" $args
}
function pnpm {
Invoke-WrappedCommand "pnpm" $args
}
function pnpx {
Invoke-WrappedCommand "pnpx" $args
}
function bun {
Invoke-WrappedCommand "bun" $args
}
function bunx {
Invoke-WrappedCommand "bunx" $args
}
function npm {
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
Invoke-RealCommand "npm" $args
return
}
Invoke-WrappedCommand "npm" $args
}
function pip {
Invoke-WrappedCommand "pip" $args
}
function pip3 {
Invoke-WrappedCommand "pip3" $args
}
function uv {
Invoke-WrappedCommand "uv" $args
}
# `python -m pip`, `python -m pip3`.
function python {
Invoke-WrappedCommand 'python' $args
}
# `python3 -m pip`, `python3 -m pip3'.
function python3 {
Invoke-WrappedCommand 'python3' $args
}
function Write-SafeChainWarning {
param([string]$Command)
@ -39,73 +102,14 @@ function Invoke-RealCommand {
function Invoke-WrappedCommand {
param(
[string]$OriginalCmd,
[string]$AikidoCmd,
[string[]]$Arguments
)
if (Test-CommandAvailable $AikidoCmd) {
& $AikidoCmd @Arguments
if (Test-CommandAvailable "safe-chain") {
& safe-chain $OriginalCmd @Arguments
}
else {
Write-SafeChainWarning $OriginalCmd
Invoke-RealCommand $OriginalCmd $Arguments
}
}
function npx {
Invoke-WrappedCommand "npx" "aikido-npx" $args
}
function yarn {
Invoke-WrappedCommand "yarn" "aikido-yarn" $args
}
function pnpm {
Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args
}
function pnpx {
Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args
}
function bun {
Invoke-WrappedCommand "bun" "aikido-bun" $args
}
function bunx {
Invoke-WrappedCommand "bunx" "aikido-bunx" $args
}
function npm {
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
Invoke-RealCommand "npm" $args
return
}
Invoke-WrappedCommand "npm" "aikido-npm" $args
}
function pip {
Invoke-WrappedCommand "pip" "aikido-pip" $args
}
function pip3 {
Invoke-WrappedCommand "pip3" "aikido-pip3" $args
}
function uv {
Invoke-WrappedCommand "uv" "aikido-uv" $args
}
# `python -m pip`, `python -m pip3`.
function python {
Invoke-WrappedCommand 'python' 'aikido-python' $args
}
# `python3 -m pip`, `python3 -m pip3'.
function python3 {
Invoke-WrappedCommand 'python3' 'aikido-python3' $args
}

View file

@ -1,3 +1,44 @@
set -gx PATH $PATH $HOME/.safe-chain/bin
function npx
wrapSafeChainCommand "npx" $argv
end
function yarn
wrapSafeChainCommand "yarn" $argv
end
function pnpm
wrapSafeChainCommand "pnpm" $argv
end
function pnpx
wrapSafeChainCommand "pnpx" $argv
end
function bun
wrapSafeChainCommand "bun" $argv
end
function bunx
wrapSafeChainCommand "bunx" $argv
end
function npm
# If args is just -v or --version and nothing else, just run the `npm -v` command
# This is because nvm uses this to check the version of npm
set argc (count $argv)
if test $argc -eq 1
switch $argv[1]
case "-v" "--version"
command npm $argv
return
end
end
wrapSafeChainCommand "npm" $argv
end
function printSafeChainWarning
set original_cmd $argv[1]
@ -17,54 +58,14 @@ end
function wrapSafeChainCommand
set original_cmd $argv[1]
set aikido_cmd $argv[2]
set cmd_args $argv[3..-1]
set cmd_args $argv[2..-1]
if type -q $aikido_cmd
# If the aikido command is available, just run it with the provided arguments
$aikido_cmd $cmd_args
if type -q safe-chain
# If the safe-chain command is available, just run it with the provided arguments
safe-chain $original_cmd $cmd_args
else
# If the aikido command is not available, print a warning and run the original command
# If the safe-chain command is not available, print a warning and run the original command
printSafeChainWarning $original_cmd
command $original_cmd $cmd_args
end
end
function npx
wrapSafeChainCommand "npx" "aikido-npx" $argv
end
function yarn
wrapSafeChainCommand "yarn" "aikido-yarn" $argv
end
function pnpm
wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv
end
function pnpx
wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv
end
function bun
wrapSafeChainCommand "bun" "aikido-bun" $argv
end
function bunx
wrapSafeChainCommand "bunx" "aikido-bunx" $argv
end
function npm
# If args is just -v or --version and nothing else, just run the `npm -v` command
# This is because nvm uses this to check the version of npm
set argc (count $argv)
if test $argc -eq 1
switch $argv[1]
case "-v" "--version"
command npm $argv
return
end
end
wrapSafeChainCommand "npm" "aikido-npm" $argv
end

View file

@ -1,3 +1,39 @@
export PATH="$PATH:$HOME/.safe-chain/bin"
function npx() {
wrapSafeChainCommand "npx" "$@"
}
function yarn() {
wrapSafeChainCommand "yarn" "$@"
}
function pnpm() {
wrapSafeChainCommand "pnpm" "$@"
}
function pnpx() {
wrapSafeChainCommand "pnpx" "$@"
}
function bun() {
wrapSafeChainCommand "bun" "$@"
}
function bunx() {
wrapSafeChainCommand "bunx" "$@"
}
function npm() {
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
command npm "$@"
return
fi
wrapSafeChainCommand "npm" "$@"
}
function printSafeChainWarning() {
# \033[43;30m is used to set the background color to yellow and text color to black
@ -9,15 +45,10 @@ function printSafeChainWarning() {
function wrapSafeChainCommand() {
local original_cmd="$1"
local aikido_cmd="$2"
# Remove the first 2 arguments (original_cmd and aikido_cmd) from $@
# so that "$@" now contains only the arguments passed to the original command
shift 2
if command -v "$aikido_cmd" > /dev/null 2>&1; then
if command -v safe-chain > /dev/null 2>&1; then
# If the aikido command is available, just run it with the provided arguments
"$aikido_cmd" "$@"
safe-chain "$@"
else
# If the aikido command is not available, print a warning and run the original command
printSafeChainWarning "$original_cmd"
@ -25,38 +56,3 @@ function wrapSafeChainCommand() {
command "$original_cmd" "$@"
fi
}
function npx() {
wrapSafeChainCommand "npx" "aikido-npx" "$@"
}
function yarn() {
wrapSafeChainCommand "yarn" "aikido-yarn" "$@"
}
function pnpm() {
wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@"
}
function pnpx() {
wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@"
}
function bun() {
wrapSafeChainCommand "bun" "aikido-bun" "$@"
}
function bunx() {
wrapSafeChainCommand "bunx" "aikido-bunx" "$@"
}
function npm() {
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
command npm "$@"
return
fi
wrapSafeChainCommand "npm" "aikido-npm" "$@"
}

View file

@ -1,3 +1,43 @@
# Use cross-platform path separator (: on Unix, ; on Windows)
$pathSeparator = if ($IsWindows) { ';' } else { ':' }
$safeChainBin = Join-Path $HOME '.safe-chain' 'bin'
$env:PATH = "$env:PATH$pathSeparator$safeChainBin"
function npx {
Invoke-WrappedCommand "npx" $args
}
function yarn {
Invoke-WrappedCommand "yarn" $args
}
function pnpm {
Invoke-WrappedCommand "pnpm" $args
}
function pnpx {
Invoke-WrappedCommand "pnpx" $args
}
function bun {
Invoke-WrappedCommand "bun" $args
}
function bunx {
Invoke-WrappedCommand "bunx" $args
}
function npm {
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
Invoke-RealCommand "npm" $args
return
}
Invoke-WrappedCommand "npm" $args
}
function Write-SafeChainWarning {
param([string]$Command)
@ -39,50 +79,14 @@ function Invoke-RealCommand {
function Invoke-WrappedCommand {
param(
[string]$OriginalCmd,
[string]$AikidoCmd,
[string[]]$Arguments
)
if (Test-CommandAvailable $AikidoCmd) {
& $AikidoCmd @Arguments
if (Test-CommandAvailable "safe-chain") {
& safe-chain $OriginalCmd @Arguments
}
else {
Write-SafeChainWarning $OriginalCmd
Invoke-RealCommand $OriginalCmd $Arguments
}
}
function npx {
Invoke-WrappedCommand "npx" "aikido-npx" $args
}
function yarn {
Invoke-WrappedCommand "yarn" "aikido-yarn" $args
}
function pnpm {
Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args
}
function pnpx {
Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args
}
function bun {
Invoke-WrappedCommand "bun" "aikido-bun" $args
}
function bunx {
Invoke-WrappedCommand "bunx" "aikido-bunx" $args
}
function npm {
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
Invoke-RealCommand "npm" $args
return
}
Invoke-WrappedCommand "npm" "aikido-npm" $args
}

View file

@ -336,4 +336,207 @@ describe("E2E: pip coverage", () => {
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`pip3 config set should work and persist configuration`, async () => {
const shell = await container.openShell("zsh");
// Set a config value
const setResult = await shell.runCommand(
"pip3 config set global.timeout 60"
);
assert.ok(
setResult.output.includes("Writing to"),
`pip3 config set should write config. Output was:\n${setResult.output}`
);
// Verify it was persisted by reading it back
const getResult = await shell.runCommand(
"pip3 config get global.timeout"
);
assert.ok(
getResult.output.includes("60"),
`Config value should be 60. Output was:\n${getResult.output}`
);
});
it(`pip3 config list should show user configuration`, async () => {
const shell = await container.openShell("zsh");
// Set a value first
await shell.runCommand("pip3 config set global.timeout 90");
// List config
const listResult = await shell.runCommand("pip3 config list");
assert.ok(
listResult.output.includes("timeout") && listResult.output.includes("90"),
`Config list should show timeout=90. Output was:\n${listResult.output}`
);
});
it(`pip3 config unset should remove configuration`, async () => {
const shell = await container.openShell("zsh");
// Set a value
await shell.runCommand("pip3 config set global.timeout 120");
// Verify it exists
const getResult = await shell.runCommand("pip3 config get global.timeout");
assert.ok(getResult.output.includes("120"));
// Unset it
const unsetResult = await shell.runCommand("pip3 config unset global.timeout");
assert.ok(
unsetResult.output.includes("Writing to"),
`pip3 config unset should write config. Output was:\n${unsetResult.output}`
);
});
it(`pip3 cache dir should return cache directory path`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("pip3 cache dir");
// Should output a directory path
assert.ok(
result.output.includes("/") && result.output.includes("cache"),
`Should output a cache directory path. Output was:\n${result.output}`
);
});
it(`pip3 cache info should show cache information`, async () => {
const shell = await container.openShell("zsh");
// Install something first to populate cache
await shell.runCommand("pip3 install --break-system-packages certifi");
const result = await shell.runCommand("pip3 cache info");
// Output should contain cache-related information
assert.ok(
result.output.match(/cache|wheel|http/i),
`Should output cache information. Output was:\n${result.output}`
);
});
it(`pip3 cache list should list cached packages`, async () => {
const shell = await container.openShell("zsh");
// Download a package to ensure something is in cache
await shell.runCommand("pip3 download certifi");
const result = await shell.runCommand("pip3 cache list certifi");
// Should show either cached wheels or "No locally built wheels"
assert.ok(
result.output.includes("certifi") || result.output.includes("No locally built"),
`Should output cache list information. Output was:\n${result.output}`
);
});
it(`pip3 debug should output debug information`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("pip3 debug");
// Should contain debug information about pip environment
assert.ok(
result.output.match(/pip version|sys\.version|sys\.executable/i),
`Should output debug information. Output was:\n${result.output}`
);
// Should NOT show safe-chain's temporary config file in the debug output
assert.ok(
!result.output.includes("safe-chain-pip-"),
`Debug output should not reference safe-chain temp config. Output was:\n${result.output}`
);
});
it(`pip3 completion should generate shell completion script`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("pip3 completion --zsh");
// Should output shell completion code
assert.ok(
result.output.includes("compdef") || result.output.includes("_pip") || result.output.includes("pip completion"),
`Should output completion code. Output was:\n${result.output}`
);
});
it(`pip3 install still works after config operations`, async () => {
const shell = await container.openShell("zsh");
// Perform config operations
await shell.runCommand("pip3 config set global.timeout 60");
await shell.runCommand("pip3 cache dir");
// Now install should still work with malware protection
const result = await shell.runCommand(
"pip3 install --break-system-packages certifi"
);
assert.ok(
result.output.includes("Successfully installed") ||
result.output.includes("Requirement already satisfied"),
`Install should succeed after config operations. Output was:\n${result.output}`
);
assert.ok(
result.output.includes("no malware found."),
`Should still scan for malware. Output was:\n${result.output}`
);
});
it(`pip3 download works after configuring pip settings`, async () => {
const shell = await container.openShell("zsh");
// Configure pip with timeout and extra index URL
const configTimeout = await shell.runCommand("pip3 config set global.timeout 60");
assert.ok(
configTimeout.output.includes("Writing to"),
`Config set should succeed. Output was:\n${configTimeout.output}`
);
const configIndex = await shell.runCommand(
"pip3 config set global.extra-index-url https://pypi.org/simple"
);
assert.ok(
configIndex.output.includes("Writing to"),
`Config set should succeed. Output was:\n${configIndex.output}`
);
// Verify config persisted
const listConfig = await shell.runCommand("pip3 config list");
assert.ok(
listConfig.output.includes("timeout") && listConfig.output.includes("60"),
`Config should show timeout=60. Output was:\n${listConfig.output}`
);
assert.ok(
listConfig.output.includes("extra-index-url") && listConfig.output.includes("pypi.org"),
`Config should show extra-index-url. Output was:\n${listConfig.output}`
);
// Now download packages with the configured settings
const downloadResult = await shell.runCommand(
"pip3 download -d /tmp/packages requests certifi"
);
assert.ok(
downloadResult.output.includes("no malware found."),
`Should scan for malware. Output was:\n${downloadResult.output}`
);
// Verify downloads succeeded
assert.ok(
downloadResult.output.includes("Saved") || downloadResult.output.includes("requests"),
`Download should succeed with configured settings. Output was:\n${downloadResult.output}`
);
assert.ok(
downloadResult.output.includes("certifi"),
`Should download certifi. Output was:\n${downloadResult.output}`
);
});
});