mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge branch 'main' into feat/pdm-support
This commit is contained in:
commit
bf2d37d114
14 changed files with 113 additions and 29 deletions
|
|
@ -1,5 +1,11 @@
|
||||||
#!/usr/bin/env node
|
#!/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 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";
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@ remove_shim_from_path() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if command -v safe-chain >/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
|
# 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}} "$@"
|
PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
|
||||||
else
|
else
|
||||||
# safe-chain is not reachable — warn the user so they know protection is inactive
|
# safe-chain is not reachable — warn the user so they know protection is inactive
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -124,8 +124,10 @@ function wrapSafeChainCommand
|
||||||
end
|
end
|
||||||
|
|
||||||
if type -q safe-chain
|
if type -q safe-chain
|
||||||
# If the safe-chain command is available, just run it with the provided arguments
|
# If the safe-chain command is available, just run it with the provided arguments.
|
||||||
safe-chain $original_cmd $cmd_args
|
# 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
|
else
|
||||||
# If the safe-chain 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
|
printSafeChainWarning $original_cmd
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,10 @@ function wrapSafeChainCommand() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v safe-chain > /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
|
# If the aikido command is available, just run it with the provided arguments.
|
||||||
safe-chain "$@"
|
# 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
|
else
|
||||||
# If the aikido command is not available, print a warning and run the original command
|
# If the aikido command is not available, print a warning and run the original command
|
||||||
printSafeChainWarning "$original_cmd"
|
printSafeChainWarning "$original_cmd"
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,9 @@ describe("E2E: bun coverage", () => {
|
||||||
|
|
||||||
var result = await shell.runCommand("bun install");
|
var result = await shell.runCommand("bun install");
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked") && result.output.includes("malicious package downloads"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads/,
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -65,8 +66,9 @@ describe("E2E: bun coverage", () => {
|
||||||
|
|
||||||
const result = await shell.runCommand("bunx safe-chain-test");
|
const result = await shell.runCommand("bunx safe-chain-test");
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked") && result.output.includes("malicious package downloads"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads/,
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,9 @@ describe("E2E: npm coverage", () => {
|
||||||
|
|
||||||
var result = await shell.runCommand("npm install");
|
var result = await shell.runCommand("npm install");
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked 1 malicious package downloads"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads/,
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
|
||||||
|
|
@ -131,8 +131,9 @@ describe("E2E: pip coverage", () => {
|
||||||
"pip3 install --break-system-packages numpy==2.4.4 --safe-chain-logging=verbose"
|
"pip3 install --break-system-packages numpy==2.4.4 --safe-chain-logging=verbose"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked 1 malicious package downloads:"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads:/,
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,9 @@ describe("E2E: pnpm coverage", () => {
|
||||||
|
|
||||||
var result = await shell.runCommand("pnpm install");
|
var result = await shell.runCommand("pnpm install");
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked 1 malicious package downloads"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads/,
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ describe("E2E: rush coverage", () => {
|
||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
result.output,
|
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}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ describe("E2E: rushx coverage", () => {
|
||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
result.output,
|
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}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
|
||||||
|
|
@ -100,8 +100,9 @@ describe("E2E: safe-chain CLI python/pip support", () => {
|
||||||
"safe-chain pip3 install --break-system-packages numpy==2.4.4"
|
"safe-chain pip3 install --break-system-packages numpy==2.4.4"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked 1 malicious package downloads"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads/,
|
||||||
`Should have blocked malware. Output was:\n${result.output}`
|
`Should have blocked malware. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -129,8 +129,9 @@ describe("E2E: uv coverage", () => {
|
||||||
"uv pip install --system --break-system-packages numpy==2.4.4"
|
"uv pip install --system --break-system-packages numpy==2.4.4"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked 1 malicious package downloads:"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads:/,
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -416,8 +417,9 @@ describe("E2E: uv coverage", () => {
|
||||||
"cd test-project-malware && uv add numpy==2.4.4"
|
"cd test-project-malware && uv add numpy==2.4.4"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked 1 malicious package downloads:"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads:/,
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -447,8 +449,9 @@ describe("E2E: uv coverage", () => {
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
const result = await shell.runCommand("uv tool install numpy==2.4.4");
|
const result = await shell.runCommand("uv tool install numpy==2.4.4");
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked 1 malicious package downloads:"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads:/,
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -485,8 +488,9 @@ describe("E2E: uv coverage", () => {
|
||||||
"uv run --with numpy==2.4.4 test_script2.py"
|
"uv run --with numpy==2.4.4 test_script2.py"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked 1 malicious package downloads:"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads:/,
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,9 @@ describe("E2E: yarn coverage", () => {
|
||||||
|
|
||||||
var result = await shell.runCommand("yarn");
|
var result = await shell.runCommand("yarn");
|
||||||
|
|
||||||
assert.ok(
|
assert.match(
|
||||||
result.output.includes("blocked 1 malicious package downloads"),
|
result.output,
|
||||||
|
/blocked [1-9]\d* malicious package downloads/,
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue