Merge branch 'main' into fix-powershell-install-script-path-separator

This commit is contained in:
Sander Declerck 2025-12-16 13:06:57 +01:00
commit 316922e9a6
No known key found for this signature in database
29 changed files with 379 additions and 563 deletions

View file

@ -5,7 +5,7 @@ on:
workflow_call: workflow_call:
inputs: inputs:
version: version:
description: 'Version to set in package.json' description: "Version to set in package.json"
required: false required: false
type: string type: string
@ -64,13 +64,13 @@ jobs:
npm i -g @aikidosec/safe-chain npm i -g @aikidosec/safe-chain
safe-chain setup-ci 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 - name: Install dependencies
run: npm ci --ignore-scripts run: npm ci --ignore-scripts
- name: Set the version in safe-chain package
if: inputs.version != ''
run: npm --no-git-tag-version version ${{ inputs.version }} --workspace=packages/safe-chain --ignore-scripts
- name: Create binary - name: Create binary
run: | run: |
node build.js ${{ matrix.target }} node build.js ${{ matrix.target }}

View file

@ -19,10 +19,10 @@ Aikido Safe Chain supports the following package managers:
- 📦 **pnpx** - 📦 **pnpx**
- 📦 **bun** - 📦 **bun**
- 📦 **bunx** - 📦 **bunx**
- 📦 **pip** (beta) - 📦 **pip**
- 📦 **pip3** (beta) - 📦 **pip3**
- 📦 **uv** (beta) - 📦 **uv**
- 📦 **poetry** (beta) - 📦 **poetry**
# Usage # Usage
@ -34,32 +34,16 @@ Installing the Aikido Safe Chain is easy with our one-line installer.
### Unix/Linux/macOS ### Unix/Linux/macOS
**Default installation (JavaScript packages only):**
```shell ```shell
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh
``` ```
**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) ### Windows (PowerShell)
**Default installation (JavaScript packages only):**
```powershell ```powershell
iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing) 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 ### Verify the installation
1. **❗Restart your terminal** to start using the Aikido Safe Chain. 1. **❗Restart your terminal** to start using the Aikido Safe Chain.
@ -74,7 +58,7 @@ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/inst
npm install safe-chain-test npm install safe-chain-test
``` ```
For Python (if you enabled Python support): For Python:
```shell ```shell
pip3 install safe-chain-pi-test pip3 install safe-chain-pi-test
@ -193,32 +177,16 @@ Use the `--ci` flag to automatically configure Aikido Safe Chain for CI/CD envir
### Unix/Linux/macOS (GitHub Actions, Azure Pipelines, etc.) ### Unix/Linux/macOS (GitHub Actions, Azure Pipelines, etc.)
**JavaScript only:**
```shell ```shell
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci
``` ```
**With Python support:**
```shell
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python
```
### Windows (Azure Pipelines, etc.) ### Windows (Azure Pipelines, etc.)
**JavaScript only:**
```powershell ```powershell
iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci" 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 ## Supported Platforms
- ✅ **GitHub Actions** - ✅ **GitHub Actions**
@ -234,14 +202,12 @@ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/inst
cache: "npm" cache: "npm"
- name: Install safe-chain - 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 run: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
``` ```
> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv/poetry) support.
## Azure DevOps Example ## Azure DevOps Example
```yaml ```yaml
@ -250,13 +216,11 @@ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/inst
versionSpec: "22.x" versionSpec: "22.x"
displayName: "Install Node.js" displayName: "Install Node.js"
- script: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python - script: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci
displayName: "Install safe-chain" displayName: "Install safe-chain"
- script: npm ci - script: npm ci
displayName: "Install dependencies" displayName: "Install dependencies"
``` ```
> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv/poetry) support.
After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection. After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.

View file

@ -3,8 +3,7 @@
# Usage with "iex (iwr {url} -UseBasicParsing)" --> See README.md # Usage with "iex (iwr {url} -UseBasicParsing)" --> See README.md
param( param(
[switch]$ci, [switch]$ci
[switch]$includepython
) )
$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set $Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set
@ -117,9 +116,6 @@ function Install-SafeChain {
# Build installation message # Build installation message
$installMsg = "Installing safe-chain $Version" $installMsg = "Installing safe-chain $Version"
if ($includepython) {
$installMsg += " with python"
}
if ($ci) { if ($ci) {
$installMsg += " in ci" $installMsg += " in ci"
} }
@ -181,9 +177,6 @@ function Install-SafeChain {
# Build setup command based on parameters # Build setup command based on parameters
$setupCmd = if ($ci) { "setup-ci" } else { "setup" } $setupCmd = if ($ci) { "setup-ci" } else { "setup" }
$setupArgs = @() $setupArgs = @()
if ($includepython) {
$setupArgs += "--include-python"
}
# Execute safe-chain setup # Execute safe-chain setup
Write-Info "Running safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })..." Write-Info "Running safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })..."

View file

@ -134,9 +134,6 @@ parse_arguments() {
--ci) --ci)
USE_CI_SETUP=true USE_CI_SETUP=true
;; ;;
--include-python)
INCLUDE_PYTHON=true
;;
*) *)
error "Unknown argument: $arg" error "Unknown argument: $arg"
;; ;;
@ -148,7 +145,6 @@ parse_arguments() {
main() { main() {
# Initialize argument flags # Initialize argument flags
USE_CI_SETUP=false USE_CI_SETUP=false
INCLUDE_PYTHON=false
# Parse command-line arguments # Parse command-line arguments
parse_arguments "$@" parse_arguments "$@"
@ -161,9 +157,6 @@ main() {
# Build installation message # Build installation message
INSTALL_MSG="Installing safe-chain ${VERSION}" INSTALL_MSG="Installing safe-chain ${VERSION}"
if [ "$INCLUDE_PYTHON" = "true" ]; then
INSTALL_MSG="${INSTALL_MSG} with python"
fi
if [ "$USE_CI_SETUP" = "true" ]; then if [ "$USE_CI_SETUP" = "true" ]; then
INSTALL_MSG="${INSTALL_MSG} in ci" INSTALL_MSG="${INSTALL_MSG} in ci"
fi fi
@ -209,10 +202,6 @@ main() {
SETUP_CMD="setup-ci" SETUP_CMD="setup-ci"
fi fi
if [ "$INCLUDE_PYTHON" = "true" ]; then
SETUP_ARGS="--include-python"
fi
# Execute safe-chain setup # Execute safe-chain setup
info "Running safe-chain $SETUP_CMD $SETUP_ARGS..." info "Running safe-chain $SETUP_CMD $SETUP_ARGS..."
if ! "$FINAL_FILE" $SETUP_CMD $SETUP_ARGS; then if ! "$FINAL_FILE" $SETUP_CMD $SETUP_ARGS; then

View file

@ -3,7 +3,7 @@
import chalk from "chalk"; import chalk from "chalk";
import { ui } from "../src/environment/userInteraction.js"; import { ui } from "../src/environment/userInteraction.js";
import { setup } from "../src/shell-integration/setup.js"; import { setup } from "../src/shell-integration/setup.js";
import { teardown } from "../src/shell-integration/teardown.js"; import { teardown, teardownDirectories } from "../src/shell-integration/teardown.js";
import { setupCi } from "../src/shell-integration/setup-ci.js"; import { setupCi } from "../src/shell-integration/setup-ci.js";
import { initializeCliArguments } from "../src/config/cliArguments.js"; import { initializeCliArguments } from "../src/config/cliArguments.js";
import { setEcoSystem } from "../src/config/settings.js"; import { setEcoSystem } from "../src/config/settings.js";
@ -60,6 +60,7 @@ if (tool) {
} else if (command === "setup") { } else if (command === "setup") {
setup(); setup();
} else if (command === "teardown") { } else if (command === "teardown") {
teardownDirectories();
teardown(); teardown();
} else if (command === "setup-ci") { } else if (command === "setup-ci") {
setupCi(); setupCi();
@ -94,11 +95,6 @@ function writeHelp() {
"safe-chain setup" "safe-chain setup"
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.` )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`
); );
ui.writeInformation(
` ${chalk.yellow(
"--include-python"
)}: Experimental: include Python package managers (pip, pip3) in the setup.`
);
ui.writeInformation( ui.writeInformation(
`- ${chalk.cyan( `- ${chalk.cyan(
"safe-chain teardown" "safe-chain teardown"
@ -109,11 +105,6 @@ function writeHelp() {
"safe-chain setup-ci" "safe-chain setup-ci"
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.` )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`
); );
ui.writeInformation(
` ${chalk.yellow(
"--include-python"
)}: Experimental: include Python package managers (pip, pip3) in the setup.`
);
ui.writeInformation( ui.writeInformation(
`- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan( `- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan(
"-v" "-v"

View file

@ -1,11 +1,10 @@
/** /**
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, includePython: boolean}} * @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined}}
*/ */
const state = { const state = {
loggingLevel: undefined, loggingLevel: undefined,
skipMinimumPackageAge: undefined, skipMinimumPackageAge: undefined,
minimumPackageAgeHours: undefined, minimumPackageAgeHours: undefined,
includePython: false,
}; };
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
@ -34,7 +33,6 @@ export function initializeCliArguments(args) {
setLoggingLevel(safeChainArgs); setLoggingLevel(safeChainArgs);
setSkipMinimumPackageAge(safeChainArgs); setSkipMinimumPackageAge(safeChainArgs);
setMinimumPackageAgeHours(safeChainArgs); setMinimumPackageAgeHours(safeChainArgs);
setIncludePython(args);
return remainingArgs; return remainingArgs;
} }
@ -109,20 +107,6 @@ export function getMinimumPackageAgeHours() {
return state.minimumPackageAgeHours; return state.minimumPackageAgeHours;
} }
/**
* @param {string[]} args
*/
function setIncludePython(args) {
// This flag doesn't have the --safe-chain- prefix because
// it is only used for the safe-chain command itself and
// not when wrapped around package manager commands.
state.includePython = hasFlagArg(args, "--include-python");
}
export function includePython() {
return state.includePython;
}
/** /**
* @param {string[]} args * @param {string[]} args
* @param {string} flagName * @param {string} flagName

View file

@ -67,7 +67,7 @@ function validateMinimumPackageAgeHours(value) {
*/ */
export function getMinimumPackageAgeHours() { export function getMinimumPackageAgeHours() {
const config = readConfigFile(); const config = readConfigFile();
if (config.minimumPackageAgeHours) { if (config.minimumPackageAgeHours !== undefined) {
const validated = validateMinimumPackageAgeHours( const validated = validateMinimumPackageAgeHours(
config.minimumPackageAgeHours config.minimumPackageAgeHours
); );

View file

@ -1,32 +1,24 @@
import { describe, it, beforeEach, afterEach, mock } from "node:test"; import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert"; import assert from "node:assert";
describe("getScanTimeout", () => { let configFileContent = undefined;
mock.module("fs", {
namedExports: {
existsSync: () => configFileContent !== undefined,
readFileSync: () => configFileContent,
writeFileSync: (content) => (configFileContent = content),
mkdirSync: () => {},
},
});
describe("getScanTimeout", async () => {
let originalEnv; let originalEnv;
let fsMock;
let getScanTimeout; const { getScanTimeout } = await import("./configFile.js");
beforeEach(async () => { beforeEach(async () => {
// Save original environment // Save original environment
originalEnv = process.env.AIKIDO_SCAN_TIMEOUT_MS; originalEnv = process.env.AIKIDO_SCAN_TIMEOUT_MS;
// Mock fs module
fsMock = {
existsSync: mock.fn(() => false),
readFileSync: mock.fn(() => "{}"),
writeFileSync: mock.fn(),
mkdirSync: mock.fn(),
};
mock.module("fs", {
namedExports: fsMock,
});
// Re-import the module to get the mocked version
const configFileModule = await import(
`./configFile.js?update=${Date.now()}`
);
getScanTimeout = configFileModule.getScanTimeout;
}); });
afterEach(() => { afterEach(() => {
@ -37,14 +29,12 @@ describe("getScanTimeout", () => {
delete process.env.AIKIDO_SCAN_TIMEOUT_MS; delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
} }
// Reset all mocks configFileContent = undefined;
mock.restoreAll();
}); });
it("should return default timeout of 10000ms when no config or env var is set", () => { it("should return default timeout of 10000ms when no config or env var is set", () => {
delete process.env.AIKIDO_SCAN_TIMEOUT_MS; delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
// Mock: config file doesn't exist configFileContent = undefined;
fsMock.existsSync.mock.mockImplementation(() => false);
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -53,11 +43,7 @@ describe("getScanTimeout", () => {
it("should return timeout from config file when set", () => { it("should return timeout from config file when set", () => {
delete process.env.AIKIDO_SCAN_TIMEOUT_MS; delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
// Mock: config file exists with scanTimeout: 5000 configFileContent = JSON.stringify({ scanTimeout: 5000 });
fsMock.existsSync.mock.mockImplementation(() => true);
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ scanTimeout: 5000 })
);
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -66,11 +52,7 @@ describe("getScanTimeout", () => {
it("should prioritize environment variable over config file", () => { it("should prioritize environment variable over config file", () => {
process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000";
// Mock: config file exists with scanTimeout: 5000 configFileContent = JSON.stringify({ scanTimeout: 5000 });
fsMock.existsSync.mock.mockImplementation(() => true);
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ scanTimeout: 5000 })
);
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -79,11 +61,7 @@ describe("getScanTimeout", () => {
it("should handle invalid environment variable and fall back to config", () => { it("should handle invalid environment variable and fall back to config", () => {
process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid";
// Mock: config file exists with scanTimeout: 7000 configFileContent = JSON.stringify({ scanTimeout: 7000 });
fsMock.existsSync.mock.mockImplementation(() => true);
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ scanTimeout: 7000 })
);
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -91,8 +69,7 @@ describe("getScanTimeout", () => {
}); });
it("should ignore zero and negative values and fall back to default", () => { it("should ignore zero and negative values and fall back to default", () => {
// Mock: config file doesn't exist configFileContent = undefined;
fsMock.existsSync.mock.mockImplementation(() => false);
process.env.AIKIDO_SCAN_TIMEOUT_MS = "0"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "0";
@ -107,11 +84,7 @@ describe("getScanTimeout", () => {
it("should ignore textual non-numeric values in environment variable and fall back to config", () => { it("should ignore textual non-numeric values in environment variable and fall back to config", () => {
process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast";
// Mock: config file exists with scanTimeout: 8000 configFileContent = JSON.stringify({ scanTimeout: 8000 });
fsMock.existsSync.mock.mockImplementation(() => true);
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ scanTimeout: 8000 })
);
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -120,11 +93,7 @@ describe("getScanTimeout", () => {
it("should ignore textual non-numeric values in config file and fall back to default", () => { it("should ignore textual non-numeric values in config file and fall back to default", () => {
delete process.env.AIKIDO_SCAN_TIMEOUT_MS; delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
// Mock: config file exists with scanTimeout: "slow" configFileContent = JSON.stringify({ scanTimeout: "slow" });
fsMock.existsSync.mock.mockImplementation(() => true);
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ scanTimeout: "slow" })
);
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -133,11 +102,7 @@ describe("getScanTimeout", () => {
it("should ignore textual non-numeric values in both env and config, fall back to default", () => { it("should ignore textual non-numeric values in both env and config, fall back to default", () => {
process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick";
// Mock: config file exists with scanTimeout: "medium" configFileContent = JSON.stringify({ scanTimeout: "medium" });
fsMock.existsSync.mock.mockImplementation(() => true);
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ scanTimeout: "medium" })
);
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -146,11 +111,7 @@ describe("getScanTimeout", () => {
it("should ignore mixed alphanumeric strings in environment variable", () => { it("should ignore mixed alphanumeric strings in environment variable", () => {
process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms";
// Mock: config file exists with scanTimeout: 6000 configFileContent = JSON.stringify({ scanTimeout: 6000 });
fsMock.existsSync.mock.mockImplementation(() => true);
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ scanTimeout: 6000 })
);
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -159,11 +120,7 @@ describe("getScanTimeout", () => {
it("should ignore mixed alphanumeric strings in config file", () => { it("should ignore mixed alphanumeric strings in config file", () => {
delete process.env.AIKIDO_SCAN_TIMEOUT_MS; delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
// Mock: config file exists with scanTimeout: "3000ms" configFileContent = JSON.stringify({ scanTimeout: "3000ms" });
fsMock.existsSync.mock.mockImplementation(() => true);
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ scanTimeout: "3000ms" })
);
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -171,37 +128,15 @@ describe("getScanTimeout", () => {
}); });
}); });
describe("getMinimumPackageAgeHours", () => { describe("getMinimumPackageAgeHours", async () => {
let fsMock; const { getMinimumPackageAgeHours } = await import("./configFile.js");
let getMinimumPackageAgeHours;
beforeEach(async () => {
// Mock fs module
fsMock = {
existsSync: mock.fn(() => false),
readFileSync: mock.fn(() => "{}"),
writeFileSync: mock.fn(),
mkdirSync: mock.fn(),
};
mock.module("fs", {
namedExports: fsMock,
});
// Re-import the module to get the mocked version
const configFileModule = await import(
`./configFile.js?update=${Date.now()}`
);
getMinimumPackageAgeHours = configFileModule.getMinimumPackageAgeHours;
});
afterEach(() => { afterEach(() => {
// Reset all mocks configFileContent = undefined;
mock.restoreAll();
}); });
it("should return null when config file doesn't exist", () => { it("should return null when config file doesn't exist", () => {
fsMock.existsSync.mock.mockImplementation(() => false); configFileContent = undefined;
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -209,10 +144,7 @@ describe("getMinimumPackageAgeHours", () => {
}); });
it("should return null when config file exists but minimumPackageAgeHours is not set", () => { it("should return null when config file exists but minimumPackageAgeHours is not set", () => {
fsMock.existsSync.mock.mockImplementation(() => true); configFileContent = JSON.stringify({ scanTimeout: 5000 });
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ scanTimeout: 5000 })
);
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -220,10 +152,7 @@ describe("getMinimumPackageAgeHours", () => {
}); });
it("should return value from config file when set to valid number", () => { it("should return value from config file when set to valid number", () => {
fsMock.existsSync.mock.mockImplementation(() => true); configFileContent = JSON.stringify({ minimumPackageAgeHours: 48 });
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ minimumPackageAgeHours: 48 })
);
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -231,10 +160,7 @@ describe("getMinimumPackageAgeHours", () => {
}); });
it("should handle string numbers in config file", () => { it("should handle string numbers in config file", () => {
fsMock.existsSync.mock.mockImplementation(() => true); configFileContent = JSON.stringify({ minimumPackageAgeHours: "72" });
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ minimumPackageAgeHours: "72" })
);
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -242,10 +168,7 @@ describe("getMinimumPackageAgeHours", () => {
}); });
it("should handle decimal values", () => { it("should handle decimal values", () => {
fsMock.existsSync.mock.mockImplementation(() => true); configFileContent = JSON.stringify({ minimumPackageAgeHours: 1.5 });
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ minimumPackageAgeHours: 1.5 })
);
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -253,21 +176,15 @@ describe("getMinimumPackageAgeHours", () => {
}); });
it("should return null for non-numeric strings", () => { it("should return null for non-numeric strings", () => {
fsMock.existsSync.mock.mockImplementation(() => true); configFileContent = JSON.stringify({ minimumPackageAgeHours: "invalid" });
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ minimumPackageAgeHours: "invalid" })
);
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
assert.strictEqual(hours, undefined); assert.strictEqual(hours, undefined);
}); });
it("should return null for values with units suffix", () => { it("should return undefined for values with units suffix", () => {
fsMock.existsSync.mock.mockImplementation(() => true); configFileContent = JSON.stringify({ minimumPackageAgeHours: "48h" });
fsMock.readFileSync.mock.mockImplementation(() =>
JSON.stringify({ minimumPackageAgeHours: "48h" })
);
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -275,11 +192,42 @@ describe("getMinimumPackageAgeHours", () => {
}); });
it("should handle malformed JSON and return null", () => { it("should handle malformed JSON and return null", () => {
fsMock.existsSync.mock.mockImplementation(() => true); configFileContent = "{ invalid json";
fsMock.readFileSync.mock.mockImplementation(() => "{ invalid json");
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
assert.strictEqual(hours, undefined); assert.strictEqual(hours, undefined);
}); });
it("should return 0 when minimumPackageAgeHours is set to 0", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: 0 });
const hours = getMinimumPackageAgeHours();
assert.strictEqual(hours, 0);
});
it("should return 0 when minimumPackageAgeHours is set to string '0'", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: "0" });
const hours = getMinimumPackageAgeHours();
assert.strictEqual(hours, 0);
});
it("should handle negative numeric values", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: -24 });
const hours = getMinimumPackageAgeHours();
assert.strictEqual(hours, -24);
});
it("should handle negative string values", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: "-48" });
const hours = getMinimumPackageAgeHours();
assert.strictEqual(hours, -48);
});
}); });

View file

@ -81,7 +81,7 @@ function validateMinimumPackageAgeHours(value) {
return undefined; return undefined;
} }
if (numericValue > 0) { if (numericValue >= 0) {
return numericValue; return numericValue;
} }

View file

@ -23,6 +23,7 @@ export async function main(args) {
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`); ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`);
ui.writeVerbose(`Stack trace: ${error.stack}`); ui.writeVerbose(`Stack trace: ${error.stack}`);
ui.writeBufferedLogsAndStopBuffering();
process.exit(1); process.exit(1);
}); });
@ -31,6 +32,7 @@ export async function main(args) {
if (reason instanceof Error) { if (reason instanceof Error) {
ui.writeVerbose(`Stack trace: ${reason.stack}`); ui.writeVerbose(`Stack trace: ${reason.stack}`);
} }
ui.writeBufferedLogsAndStopBuffering();
process.exit(1); process.exit(1);
}); });
@ -89,6 +91,7 @@ export async function main(args) {
return packageManagerResult.status; return packageManagerResult.status;
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
ui.writeError("Failed to check for malicious packages:", error.message); ui.writeError("Failed to check for malicious packages:", error.message);
ui.writeBufferedLogsAndStopBuffering();
// Returning the exit code back to the caller allows the promise // Returning the exit code back to the caller allows the promise
// to be awaited in the bin files and return the correct exit code // to be awaited in the bin files and return the correct exit code

View file

@ -8,6 +8,7 @@ import fsSync from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import ini from "ini"; import ini from "ini";
import { spawn } from "child_process";
/** /**
* Checks if this pip invocation should bypass safe-chain and spawn directly. * Checks if this pip invocation should bypass safe-chain and spawn directly.
@ -16,7 +17,7 @@ import ini from "ini";
* @param {string[]} args - The arguments * @param {string[]} args - The arguments
* @returns {boolean} * @returns {boolean}
*/ */
function shouldBypassSafeChain(command, args) { export function shouldBypassSafeChain(command, args) {
if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) { if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) {
// Check if args start with -m pip // Check if args start with -m pip
if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) { if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) {
@ -77,14 +78,16 @@ export async function runPip(command, args) {
if (shouldBypassSafeChain(command, args)) { if (shouldBypassSafeChain(command, args)) {
ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`); ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`);
// Spawn the ORIGINAL command with ORIGINAL args // Spawn the ORIGINAL command with ORIGINAL args
const { spawn } = await import("child_process");
return new Promise((_resolve) => { return new Promise((_resolve) => {
const proc = spawn(command, args, { stdio: "inherit" }); const proc = spawn(command, args, { stdio: "inherit" });
proc.on("exit", (/** @type {number | null} */ code) => { proc.on("exit", (/** @type {number | null} */ code) => {
ui.writeVerbose(`${command} ${args.join(" ")} exited with status ${code}`);
ui.writeBufferedLogsAndStopBuffering();
process.exit(code ?? 0); process.exit(code ?? 0);
}); });
proc.on("error", (/** @type {Error} */ err) => { proc.on("error", (/** @type {Error} */ err) => {
ui.writeError(`Error executing command: ${err.message}`); ui.writeError(`Error executing command: ${err.message}`);
ui.writeBufferedLogsAndStopBuffering();
process.exit(1); process.exit(1);
}); });
}); });

View file

@ -7,6 +7,7 @@ import ini from "ini";
describe("runPipCommand environment variable handling", () => { describe("runPipCommand environment variable handling", () => {
let runPip; let runPip;
let shouldBypassSafeChain;
let capturedArgs = null; let capturedArgs = null;
let customEnv = null; let customEnv = null;
let capturedConfigContent = null; // Capture config file content before cleanup let capturedConfigContent = null; // Capture config file content before cleanup
@ -56,6 +57,7 @@ describe("runPipCommand environment variable handling", () => {
const mod = await import("./runPipCommand.js"); const mod = await import("./runPipCommand.js");
runPip = mod.runPip; runPip = mod.runPip;
shouldBypassSafeChain = mod.shouldBypassSafeChain;
}); });
afterEach(() => { afterEach(() => {
@ -397,4 +399,21 @@ describe("runPipCommand environment variable handling", () => {
assert.ok(output.includes("proxy found in PIP_CONFIG_FILE"), "Should warn about proxy overwrite in output"); assert.ok(output.includes("proxy found in PIP_CONFIG_FILE"), "Should warn about proxy overwrite in output");
customEnv = null; customEnv = null;
}); });
it("should bypass safe-chain for python correctly", async () => {
assert.strictEqual(shouldBypassSafeChain("python", []), true);
assert.strictEqual(shouldBypassSafeChain("python3", []), true);
assert.strictEqual(shouldBypassSafeChain("python", ["--version"]), true);
assert.strictEqual(shouldBypassSafeChain("python3", ["--version"]), true);
assert.strictEqual(shouldBypassSafeChain("python", ["-m", "http.server"]), true);
assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "http.server"]), true);
assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip"]), false);
assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip"]), false);
assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip3"]), false);
assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip3"]), false);
});
}); });

View file

@ -113,6 +113,20 @@ export function getPackageManagerList() {
return `${tools.join(", ")}, and ${lastTool} commands`; return `${tools.join(", ")}, and ${lastTool} commands`;
} }
/**
* @returns {string}
*/
export function getShimsDir() {
return path.join(os.homedir(), ".safe-chain", "shims");
}
/**
* @returns {string}
*/
export function getScriptsDir() {
return path.join(os.homedir(), ".safe-chain", "scripts");
}
/** /**
* @param {string} executableName * @param {string} executableName
* *

View file

@ -1,12 +1,10 @@
import chalk from "chalk"; import chalk from "chalk";
import { ui } from "../environment/userInteraction.js"; import { ui } from "../environment/userInteraction.js";
import { getPackageManagerList, knownAikidoTools } from "./helpers.js"; import { getPackageManagerList, knownAikidoTools, getShimsDir } from "./helpers.js";
import fs from "fs"; import fs from "fs";
import os from "os"; import os from "os";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { includePython } from "../config/cliArguments.js";
import { ECOSYSTEM_PY } from "../config/settings.js";
/** @type {string} */ /** @type {string} */
// This checks the current file's dirname in a way that's compatible with: // This checks the current file's dirname in a way that's compatible with:
@ -32,7 +30,7 @@ export async function setupCi() {
); );
ui.emptyLine(); ui.emptyLine();
const shimsDir = path.join(os.homedir(), ".safe-chain", "shims"); const shimsDir = getShimsDir();
const binDir = path.join(os.homedir(), ".safe-chain", "bin"); const binDir = path.join(os.homedir(), ".safe-chain", "bin");
// Create the shims directory if it doesn't exist // Create the shims directory if it doesn't exist
if (!fs.existsSync(shimsDir)) { if (!fs.existsSync(shimsDir)) {
@ -162,9 +160,5 @@ function modifyPathForCi(shimsDir, binDir) {
} }
function getToolsToSetup() { function getToolsToSetup() {
if (includePython()) { return knownAikidoTools;
return knownAikidoTools;
} else {
return knownAikidoTools.filter((tool) => tool.ecoSystem !== ECOSYSTEM_PY);
}
} }

View file

@ -50,6 +50,7 @@ describe("Setup CI shell integration", () => {
{ tool: "yarn", aikidoCommand: "aikido-yarn" }, { tool: "yarn", aikidoCommand: "aikido-yarn" },
], ],
getPackageManagerList: () => "npm, yarn", getPackageManagerList: () => "npm, yarn",
getShimsDir: () => mockShimsDir,
}, },
}); });

View file

@ -1,11 +1,9 @@
import chalk from "chalk"; import chalk from "chalk";
import { ui } from "../environment/userInteraction.js"; import { ui } from "../environment/userInteraction.js";
import { detectShells } from "./shellDetection.js"; import { detectShells } from "./shellDetection.js";
import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; import { knownAikidoTools, getPackageManagerList, getScriptsDir } from "./helpers.js";
import fs from "fs"; import fs from "fs";
import os from "os";
import path from "path"; import path from "path";
import { includePython } from "../config/cliArguments.js";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
/** @type {string} */ /** @type {string} */
@ -107,10 +105,10 @@ function setupShell(shell) {
function copyStartupFiles() { function copyStartupFiles() {
const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"]; const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"];
const targetDir = getScriptsDir();
for (const file of startupFiles) { for (const file of startupFiles) {
const targetDir = path.join(os.homedir(), ".safe-chain", "scripts"); const targetPath = path.join(targetDir, file);
const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file);
if (!fs.existsSync(targetDir)) { if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true }); fs.mkdirSync(targetDir, { recursive: true });
@ -119,7 +117,7 @@ function copyStartupFiles() {
// Use absolute path for source // Use absolute path for source
const sourcePath = path.join( const sourcePath = path.join(
dirname, dirname,
includePython() ? "startup-scripts/include-python" : "startup-scripts", "startup-scripts",
file file
); );
fs.copyFileSync(sourcePath, targetPath); fs.copyFileSync(sourcePath, targetPath);

View file

@ -1,98 +0,0 @@
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
function poetry
wrapSafeChainCommand "poetry" $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]
# Fish equivalent of ANSI color codes: yellow background, black text for "Warning:"
set_color -b yellow black
printf "Warning:"
set_color normal
printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd
# Cyan text for the install command
printf "Install safe-chain by using "
set_color cyan
printf "npm install -g @aikidosec/safe-chain"
set_color normal
printf ".\n"
end
function wrapSafeChainCommand
set original_cmd $argv[1]
set cmd_args $argv[2..-1]
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 safe-chain command is not available, print a warning and run the original command
printSafeChainWarning $original_cmd
command $original_cmd $cmd_args
end
end

View file

@ -1,85 +0,0 @@
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" "$@"
}
function poetry() {
wrapSafeChainCommand "poetry" "$@"
}
# `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
# \033[0m is used to reset the text formatting
printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1"
# \033[36m is used to set the text color to cyan
printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n"
}
function wrapSafeChainCommand() {
local original_cmd="$1"
if command -v safe-chain > /dev/null 2>&1; then
# If the aikido command is available, just run it with the provided arguments
safe-chain "$@"
else
# If the aikido command is not available, print a warning and run the original command
printSafeChainWarning "$original_cmd"
command "$original_cmd" "$@"
fi
}

View file

@ -1,121 +0,0 @@
# Use cross-platform path separator (: on Unix, ; on Windows)
# $IsWindows is only available in PowerShell Core 6.0+. If it doesn't exist, assume Windows PowerShell
$isWindowsPlatform = if (Test-Path variable:IsWindows) { $IsWindows } else { $true }
$pathSeparator = if ($isWindowsPlatform) { ';' } else { ':' }
$safeChainBin = Join-Path (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
}
function poetry {
Invoke-WrappedCommand "poetry" $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)
# PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:"
Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline
Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it."
# Cyan text for the install command
Write-Host "Install safe-chain by using " -NoNewline
Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline
Write-Host "."
}
function Test-CommandAvailable {
param([string]$Command)
try {
Get-Command $Command -ErrorAction Stop | Out-Null
return $true
}
catch {
return $false
}
}
function Invoke-RealCommand {
param(
[string]$Command,
[string[]]$Arguments
)
# Find the real executable to avoid calling our wrapped functions
$realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1
if ($realCommand) {
& $realCommand.Source @Arguments
}
}
function Invoke-WrappedCommand {
param(
[string]$OriginalCmd,
[string[]]$Arguments
)
if (Test-CommandAvailable "safe-chain") {
& safe-chain $OriginalCmd @Arguments
}
else {
Write-SafeChainWarning $OriginalCmd
Invoke-RealCommand $OriginalCmd $Arguments
}
}

View file

@ -39,6 +39,33 @@ function npm
wrapSafeChainCommand "npm" $argv wrapSafeChainCommand "npm" $argv
end end
function pip
wrapSafeChainCommand "pip" $argv
end
function pip3
wrapSafeChainCommand "pip3" $argv
end
function uv
wrapSafeChainCommand "uv" $argv
end
function poetry
wrapSafeChainCommand "poetry" $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 function printSafeChainWarning
set original_cmd $argv[1] set original_cmd $argv[1]

View file

@ -35,6 +35,33 @@ function npm() {
wrapSafeChainCommand "npm" "$@" wrapSafeChainCommand "npm" "$@"
} }
function pip() {
wrapSafeChainCommand "pip" "$@"
}
function pip3() {
wrapSafeChainCommand "pip3" "$@"
}
function uv() {
wrapSafeChainCommand "uv" "$@"
}
function poetry() {
wrapSafeChainCommand "poetry" "$@"
}
# `python -m pip`, `python -m pip3`.
function python() {
wrapSafeChainCommand "python" "$@"
}
# `python3 -m pip`, `python3 -m pip3'.
function python3() {
wrapSafeChainCommand "python3" "$@"
}
function printSafeChainWarning() { function printSafeChainWarning() {
# \033[43;30m is used to set the background color to yellow and text color to black # \033[43;30m is used to set the background color to yellow and text color to black
# \033[0m is used to reset the text formatting # \033[0m is used to reset the text formatting

View file

@ -40,6 +40,33 @@ function npm {
Invoke-WrappedCommand "npm" $args Invoke-WrappedCommand "npm" $args
} }
function pip {
Invoke-WrappedCommand "pip" $args
}
function pip3 {
Invoke-WrappedCommand "pip3" $args
}
function uv {
Invoke-WrappedCommand "uv" $args
}
function poetry {
Invoke-WrappedCommand "poetry" $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 { function Write-SafeChainWarning {
param([string]$Command) param([string]$Command)

View file

@ -1,7 +1,8 @@
import chalk from "chalk"; import chalk from "chalk";
import { ui } from "../environment/userInteraction.js"; import { ui } from "../environment/userInteraction.js";
import { detectShells } from "./shellDetection.js"; import { detectShells } from "./shellDetection.js";
import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; import { knownAikidoTools, getPackageManagerList, getShimsDir, getScriptsDir } from "./helpers.js";
import fs from "fs";
/** /**
* @returns {Promise<void>} * @returns {Promise<void>}
@ -62,3 +63,44 @@ export async function teardown() {
return; return;
} }
} }
/**
* Removes directories created by setup-ci and setup commands
* @returns {Promise<void>}
*/
export async function teardownDirectories() {
const shimsDir = getShimsDir();
const scriptsDir = getScriptsDir();
// Remove CI shims directory
if (fs.existsSync(shimsDir)) {
try {
fs.rmSync(shimsDir, { recursive: true, force: true });
ui.writeInformation(
`${chalk.bold("- CI Shims:")} ${chalk.green("Removed successfully")}`
);
} catch (/** @type {any} */ error) {
ui.writeError(
`${chalk.bold("- CI Shims:")} ${chalk.red(
"Failed to remove"
)}. Error: ${error.message}`
);
}
}
// Remove scripts directory
if (fs.existsSync(scriptsDir)) {
try {
fs.rmSync(scriptsDir, { recursive: true, force: true });
ui.writeInformation(
`${chalk.bold("- Scripts:")} ${chalk.green("Removed successfully")}`
);
} catch (/** @type {any} */ error) {
ui.writeError(
`${chalk.bold("- Scripts:")} ${chalk.red(
"Failed to remove"
)}. Error: ${error.message}`
);
}
}
}

View file

@ -231,7 +231,7 @@ describe("E2E: NODE_EXTRA_CA_CERTS merging", () => {
it(`pip install works without NODE_EXTRA_CA_CERTS set`, async () => { it(`pip install works without NODE_EXTRA_CA_CERTS set`, async () => {
const shell = await container.openShell("zsh"); const shell = await container.openShell("zsh");
await shell.runCommand("safe-chain setup --include-python"); await shell.runCommand("safe-chain setup");
await shell.runCommand("unset NODE_EXTRA_CA_CERTS"); await shell.runCommand("unset NODE_EXTRA_CA_CERTS");
const result = await shell.runCommand( const result = await shell.runCommand(
@ -247,7 +247,7 @@ describe("E2E: NODE_EXTRA_CA_CERTS merging", () => {
it(`pip install works with valid NODE_EXTRA_CA_CERTS set`, async () => { it(`pip install works with valid NODE_EXTRA_CA_CERTS set`, async () => {
const shell = await container.openShell("zsh"); const shell = await container.openShell("zsh");
await shell.runCommand("safe-chain setup --include-python"); await shell.runCommand("safe-chain setup");
// Create a temporary valid certificate // Create a temporary valid certificate
await shell.runCommand("cp /etc/ssl/certs/ca-certificates.crt /tmp/pip-valid-certs.pem"); await shell.runCommand("cp /etc/ssl/certs/ca-certificates.crt /tmp/pip-valid-certs.pem");
@ -265,7 +265,7 @@ describe("E2E: NODE_EXTRA_CA_CERTS merging", () => {
it(`pip install handles non-existent NODE_EXTRA_CA_CERTS gracefully`, async () => { it(`pip install handles non-existent NODE_EXTRA_CA_CERTS gracefully`, async () => {
const shell = await container.openShell("zsh"); const shell = await container.openShell("zsh");
await shell.runCommand("safe-chain setup --include-python"); await shell.runCommand("safe-chain setup");
const result = await shell.runCommand( const result = await shell.runCommand(
'export NODE_EXTRA_CA_CERTS="/tmp/nonexistent-pip-certs.pem" && pip3 install --break-system-packages requests' 'export NODE_EXTRA_CA_CERTS="/tmp/nonexistent-pip-certs.pem" && pip3 install --break-system-packages requests'
@ -281,7 +281,7 @@ describe("E2E: NODE_EXTRA_CA_CERTS merging", () => {
it(`pip install handles invalid NODE_EXTRA_CA_CERTS gracefully`, async () => { it(`pip install handles invalid NODE_EXTRA_CA_CERTS gracefully`, async () => {
const shell = await container.openShell("zsh"); const shell = await container.openShell("zsh");
await shell.runCommand("safe-chain setup --include-python"); await shell.runCommand("safe-chain setup");
// Create invalid cert // Create invalid cert
await shell.runCommand( await shell.runCommand(

View file

@ -86,7 +86,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
// Setup safe-chain CI shims // Setup safe-chain CI shims
const installationShell = await container.openShell(shell); const installationShell = await container.openShell(shell);
await installationShell.runCommand( await installationShell.runCommand(
"safe-chain setup-ci --include-python" "safe-chain setup-ci"
); );
// Add $HOME/.safe-chain/shims to PATH for subsequent shells // Add $HOME/.safe-chain/shims to PATH for subsequent shells
@ -115,7 +115,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
it(`setup-ci routes python -m pip through safe-chain for ${shell}`, async () => { it(`setup-ci routes python -m pip through safe-chain for ${shell}`, async () => {
const installationShell = await container.openShell(shell); const installationShell = await container.openShell(shell);
await installationShell.runCommand( await installationShell.runCommand(
"safe-chain setup-ci --include-python" "safe-chain setup-ci"
); );
await installationShell.runCommand( await installationShell.runCommand(
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc"
@ -138,7 +138,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
it(`setup-ci routes python3 -m pip through safe-chain for ${shell}`, async () => { it(`setup-ci routes python3 -m pip through safe-chain for ${shell}`, async () => {
const installationShell = await container.openShell(shell); const installationShell = await container.openShell(shell);
await installationShell.runCommand( await installationShell.runCommand(
"safe-chain setup-ci --include-python" "safe-chain setup-ci"
); );
await installationShell.runCommand( await installationShell.runCommand(
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc"
@ -161,7 +161,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
it(`setup-ci routes pip through safe-chain for ${shell}`, async () => { it(`setup-ci routes pip through safe-chain for ${shell}`, async () => {
const installationShell = await container.openShell(shell); const installationShell = await container.openShell(shell);
await installationShell.runCommand( await installationShell.runCommand(
"safe-chain setup-ci --include-python" "safe-chain setup-ci"
); );
await installationShell.runCommand( await installationShell.runCommand(
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc"
@ -184,7 +184,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
it(`setup-ci routes pip3 through safe-chain for ${shell}`, async () => { it(`setup-ci routes pip3 through safe-chain for ${shell}`, async () => {
const installationShell = await container.openShell(shell); const installationShell = await container.openShell(shell);
await installationShell.runCommand( await installationShell.runCommand(
"safe-chain setup-ci --include-python" "safe-chain setup-ci"
); );
await installationShell.runCommand( await installationShell.runCommand(
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc"

View file

@ -15,7 +15,7 @@ describe("E2E: pip coverage", () => {
await container.start(); await container.start();
const installationShell = await container.openShell("zsh"); const installationShell = await container.openShell("zsh");
await installationShell.runCommand("safe-chain setup --include-python"); await installationShell.runCommand("safe-chain setup");
// Clear pip cache before each test to ensure fresh downloads through proxy // Clear pip cache before each test to ensure fresh downloads through proxy
await installationShell.runCommand("pip3 cache purge"); await installationShell.runCommand("pip3 cache purge");

View file

@ -15,7 +15,7 @@ describe("E2E: poetry coverage", () => {
await container.start(); await container.start();
const installationShell = await container.openShell("zsh"); const installationShell = await container.openShell("zsh");
await installationShell.runCommand("safe-chain setup --include-python"); await installationShell.runCommand("safe-chain setup");
// Clear poetry cache // Clear poetry cache
await installationShell.runCommand("command poetry cache clear pypi --all -n"); await installationShell.runCommand("command poetry cache clear pypi --all -n");

View file

@ -0,0 +1,96 @@
import { describe, it, before, beforeEach, afterEach } from "node:test";
import { DockerTestContainer } from "./DockerTestContainer.js";
import assert from "node:assert";
describe("E2E: safe-chain teardown command", () => {
let container;
before(async () => {
DockerTestContainer.buildImage();
});
beforeEach(async () => {
container = new DockerTestContainer();
await container.start();
});
afterEach(async () => {
if (container) {
await container.stop();
container = null;
}
});
it("safe-chain teardown removes shims directory created by setup-ci", async () => {
const shell = await container.openShell("bash");
// Run setup-ci
await shell.runCommand("safe-chain setup-ci");
// Verify shims directory exists
const checkShimsExist = await shell.runCommand("test -d ~/.safe-chain/shims && echo 'exists' || echo 'missing'");
assert.ok(checkShimsExist.output.includes("exists"), "Shims directory should exist after setup-ci");
// Run teardown
await shell.runCommand("safe-chain teardown");
// Verify shims directory is gone
const checkShimsGone = await shell.runCommand("test -d ~/.safe-chain/shims && echo 'exists' || echo 'missing'");
assert.ok(checkShimsGone.output.includes("missing"), "Shims directory should be removed after teardown");
});
it("safe-chain teardown removes scripts directory created by setup", async () => {
const shell = await container.openShell("bash");
// Run setup
await shell.runCommand("safe-chain setup");
// Verify scripts directory exists
const checkScriptsExist = await shell.runCommand("test -d ~/.safe-chain/scripts && echo 'exists' || echo 'missing'");
assert.ok(checkScriptsExist.output.includes("exists"), "Scripts directory should exist after setup");
// Run teardown
await shell.runCommand("safe-chain teardown");
// Verify scripts directory is gone
const checkScriptsGone = await shell.runCommand("test -d ~/.safe-chain/scripts && echo 'exists' || echo 'missing'");
assert.ok(checkScriptsGone.output.includes("missing"), "Scripts directory should be removed after teardown");
});
it("safe-chain teardown removes shims directory created by setup-ci", async () => {
const shell = await container.openShell("bash");
// Run setup-ci
await shell.runCommand("safe-chain setup-ci");
// Verify shims directory exists
const checkShimsExist = await shell.runCommand("test -d ~/.safe-chain/shims && echo 'exists' || echo 'missing'");
assert.ok(checkShimsExist.output.includes("exists"), "Shims directory should exist after setup-ci");
// Verify Python shims were created
const checkPythonShims = await shell.runCommand("test -f ~/.safe-chain/shims/pip && echo 'exists' || echo 'missing'");
assert.ok(checkPythonShims.output.includes("exists"), "Python shims should exist after setup-ci");
// Run teardown
await shell.runCommand("safe-chain teardown");
// Verify shims directory is gone
const checkShimsGone = await shell.runCommand("test -d ~/.safe-chain/shims && echo 'exists' || echo 'missing'");
assert.ok(checkShimsGone.output.includes("missing"), "Shims directory should be removed after teardown");
});
it("safe-chain teardown removes scripts directory created by setup", async () => {
const shell = await container.openShell("bash");
// Run setup
await shell.runCommand("safe-chain setup");
// Verify scripts directory exists
const checkScriptsExist = await shell.runCommand("test -d ~/.safe-chain/scripts && echo 'exists' || echo 'missing'");
assert.ok(checkScriptsExist.output.includes("exists"), "Scripts directory should exist after setup");
// Run teardown
await shell.runCommand("safe-chain teardown");
// Verify scripts directory is gone
const checkScriptsGone = await shell.runCommand("test -d ~/.safe-chain/scripts && echo 'exists' || echo 'missing'");
assert.ok(checkScriptsGone.output.includes("missing"), "Scripts directory should be removed after teardown");
});
});

View file

@ -15,7 +15,7 @@ describe("E2E: uv coverage", () => {
await container.start(); await container.start();
const installationShell = await container.openShell("zsh"); const installationShell = await container.openShell("zsh");
await installationShell.runCommand("safe-chain setup --include-python"); await installationShell.runCommand("safe-chain setup");
// Clear uv cache // Clear uv cache
await installationShell.runCommand("uv cache clean"); await installationShell.runCommand("uv cache clean");