Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/safe-chain/bin/safe-chain.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#!/usr/bin/env node

// Strip PKG_EXECPATH from the environment so any child process safe-chain
// spawns (npm, uv, pip, …) doesn't inherit it. If it leaks into a subsequent
// safe-chain invocation (e.g. via a shim) the yao-pkg bootstrap would treat
// argv[1] as a script path and fail with MODULE_NOT_FOUND.
delete process.env.PKG_EXECPATH;

import chalk from "chalk";
import { ui } from "../src/environment/userInteraction.js";
import { setup } from "../src/shell-integration/setup.js";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ remove_shim_from_path() {
}

if command -v safe-chain >/dev/null 2>&1; then
# Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
# Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops.
# Unset PKG_EXECPATH so the yao-pkg bootstrap inside the safe-chain binary doesn't
# mistake argv[1] for a script path and try to resolve "{{PACKAGE_MANAGER}}" against cwd.
unset PKG_EXECPATH
PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
else
# safe-chain is not reachable — warn the user so they know protection is inactive
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..", "..");

describe("PKG_EXECPATH cleanup", () => {
it("unix shim template unsets PKG_EXECPATH before invoking safe-chain", () => {
const file = path.join(
repoRoot,
"src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh",
);
const content = fs.readFileSync(file, "utf-8");
assert.match(
content,
/unset PKG_EXECPATH[\s\S]*exec safe-chain/,
"unix-wrapper.template.sh must `unset PKG_EXECPATH` before `exec safe-chain`",
);
});

it("posix shell function unsets PKG_EXECPATH before invoking safe-chain", () => {
const file = path.join(
repoRoot,
"src/shell-integration/startup-scripts/init-posix.sh",
);
const content = fs.readFileSync(file, "utf-8");
// Scoped subshell so we don't mutate the user's interactive env.
assert.match(
content,
/\(unset PKG_EXECPATH;\s*safe-chain "\$@"\)/,
"init-posix.sh must invoke safe-chain in a subshell that unsets PKG_EXECPATH",
);
});

it("fish shell function unsets PKG_EXECPATH before invoking safe-chain", () => {
const file = path.join(
repoRoot,
"src/shell-integration/startup-scripts/init-fish.fish",
);
const content = fs.readFileSync(file, "utf-8");
assert.match(
content,
/env -u PKG_EXECPATH safe-chain/,
"init-fish.fish must invoke safe-chain via `env -u PKG_EXECPATH`",
);
});

it("safe-chain entry point deletes PKG_EXECPATH from process.env", () => {
const file = path.join(repoRoot, "bin/safe-chain.js");
const content = fs.readFileSync(file, "utf-8");
assert.match(
content,
/delete process\.env\.PKG_EXECPATH/,
"bin/safe-chain.js must delete process.env.PKG_EXECPATH so spawned children don't inherit it",
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,10 @@ function wrapSafeChainCommand
end

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
# If the safe-chain command is available, just run it with the provided arguments.
# Unset PKG_EXECPATH for this invocation so the yao-pkg bootstrap inside the
# safe-chain binary doesn't mistake argv[1] for a script path to resolve against cwd.
env -u PKG_EXECPATH 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,10 @@ function wrapSafeChainCommand() {
fi

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 "$@"
# If the aikido command is available, just run it with the provided arguments.
# Unset PKG_EXECPATH so the yao-pkg bootstrap inside the safe-chain binary doesn't
# mistake argv[1] for a script path and try to resolve it against cwd.
(unset PKG_EXECPATH; safe-chain "$@")
else
# If the aikido command is not available, print a warning and run the original command
printSafeChainWarning "$original_cmd"
Expand Down
10 changes: 6 additions & 4 deletions test/e2e/bun.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ describe("E2E: bun coverage", () => {

var result = await shell.runCommand("bun install");

assert.ok(
result.output.includes("blocked 1 malicious package downloads"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand All @@ -65,8 +66,9 @@ describe("E2E: bun coverage", () => {

const result = await shell.runCommand("bunx safe-chain-test");

assert.ok(
result.output.includes("blocked 1 malicious package downloads"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand Down
5 changes: 3 additions & 2 deletions test/e2e/npm.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ describe("E2E: npm coverage", () => {

var result = await shell.runCommand("npm install");

assert.ok(
result.output.includes("blocked 1 malicious package downloads"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand Down
5 changes: 3 additions & 2 deletions test/e2e/pip.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,9 @@ describe("E2E: pip coverage", () => {
"pip3 install --break-system-packages numpy==2.4.4 --safe-chain-logging=verbose"
);

assert.ok(
result.output.includes("blocked 1 malicious package downloads:"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads:/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand Down
5 changes: 3 additions & 2 deletions test/e2e/pnpm.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ describe("E2E: pnpm coverage", () => {

var result = await shell.runCommand("pnpm install");

assert.ok(
result.output.includes("blocked 1 malicious package downloads"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/rush.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe("E2E: rush coverage", () => {

assert.match(
result.output,
/blocked \d+ malicious package downloads/,
/blocked [1-9]\d* malicious package downloads/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/rushx.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("E2E: rushx coverage", () => {

assert.match(
result.output,
/blocked \d+ malicious package downloads/,
/blocked [1-9]\d* malicious package downloads/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand Down
5 changes: 3 additions & 2 deletions test/e2e/safe-chain-cli-python.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ describe("E2E: safe-chain CLI python/pip support", () => {
"safe-chain pip3 install --break-system-packages numpy==2.4.4"
);

assert.ok(
result.output.includes("blocked 1 malicious package downloads"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads/,
`Should have blocked malware. Output was:\n${result.output}`
);
});
Expand Down
20 changes: 12 additions & 8 deletions test/e2e/uv.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ describe("E2E: uv coverage", () => {
"uv pip install --system --break-system-packages numpy==2.4.4"
);

assert.ok(
result.output.includes("blocked 1 malicious package downloads:"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads:/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand Down Expand Up @@ -416,8 +417,9 @@ describe("E2E: uv coverage", () => {
"cd test-project-malware && uv add numpy==2.4.4"
);

assert.ok(
result.output.includes("blocked 1 malicious package downloads:"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads:/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand Down Expand Up @@ -447,8 +449,9 @@ describe("E2E: uv coverage", () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("uv tool install numpy==2.4.4");

assert.ok(
result.output.includes("blocked 1 malicious package downloads:"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads:/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand Down Expand Up @@ -485,8 +488,9 @@ describe("E2E: uv coverage", () => {
"uv run --with numpy==2.4.4 test_script2.py"
);

assert.ok(
result.output.includes("blocked 1 malicious package downloads:"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads:/,
`Output did not include expected text. Output was:\n${result.output}`
);
});
Expand Down
5 changes: 3 additions & 2 deletions test/e2e/yarn.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ describe("E2E: yarn coverage", () => {

var result = await shell.runCommand("yarn");

assert.ok(
result.output.includes("blocked 1 malicious package downloads"),
assert.match(
result.output,
/blocked [1-9]\d* malicious package downloads/,
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
Expand Down
Loading