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
abbe0480b6
52 changed files with 1603 additions and 1348 deletions
|
|
@ -84,10 +84,14 @@ export class DockerTestContainer {
|
|||
}
|
||||
}
|
||||
|
||||
async openShell(shell) {
|
||||
async openShell(shell, { user } = {}) {
|
||||
const execArgs = user
|
||||
? ["exec", "-it", "-u", user, this.containerName, shell]
|
||||
: ["exec", "-it", this.containerName, shell];
|
||||
|
||||
let ptyProcess = pty.spawn(
|
||||
"docker",
|
||||
["exec", "-it", this.containerName, shell],
|
||||
execArgs,
|
||||
{
|
||||
name: "xterm-color",
|
||||
cols: 80,
|
||||
|
|
|
|||
168
test/e2e/pip-minimum-age.e2e.spec.js
Normal file
168
test/e2e/pip-minimum-age.e2e.spec.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||
|
||||
describe("E2E: pip minimum package age", () => {
|
||||
let container;
|
||||
|
||||
before(async () => {
|
||||
DockerTestContainer.buildImage();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
container = new DockerTestContainer();
|
||||
await container.start();
|
||||
|
||||
const installationShell = await container.openShell("zsh");
|
||||
await installationShell.runCommand("safe-chain setup");
|
||||
await installationShell.runCommand("pip3 cache purge");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (container) {
|
||||
await container.stop();
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to an older PyPI version for flexible constraints", async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
const latestVersion = await getLatestPackageVersion(shell, "openai");
|
||||
const tooYoungTimestamps = getTooYoungReleaseTimestamps();
|
||||
|
||||
await startFeedServer(container, [
|
||||
{
|
||||
source: "pypi",
|
||||
package_name: "openai",
|
||||
version: latestVersion,
|
||||
...tooYoungTimestamps,
|
||||
},
|
||||
]);
|
||||
|
||||
const installResult = await shell.runCommand(
|
||||
'SAFE_CHAIN_MALWARE_LIST_BASE_URL=http://127.0.0.1:8123 pip3 install --break-system-packages "openai>=2.8.0,<3" --safe-chain-logging=verbose'
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
installResult.output.includes(`openai@${latestVersion} is newer than 48 hours and was removed`),
|
||||
`Expected Safe Chain to suppress the latest openai version. Output was:\n${installResult.output}`
|
||||
);
|
||||
assert.ok(
|
||||
!installResult.output.includes("blocked by safe-chain direct download minimum package age"),
|
||||
`Expected fallback during resolution, not a direct-download block. Output was:\n${installResult.output}`
|
||||
);
|
||||
assert.ok(
|
||||
installResult.output.includes("Successfully installed"),
|
||||
`Expected pip install to succeed after fallback. Output was:\n${installResult.output}`
|
||||
);
|
||||
|
||||
const installedVersion = await getInstalledVersion(shell, "openai");
|
||||
assert.notEqual(
|
||||
installedVersion,
|
||||
latestVersion,
|
||||
`Expected fallback to an older openai version, but installed ${latestVersion}.`
|
||||
);
|
||||
});
|
||||
|
||||
it("fails cleanly for exact pinned too-young PyPI versions", async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
const latestVersion = await getLatestPackageVersion(shell, "openai");
|
||||
const tooYoungTimestamps = getTooYoungReleaseTimestamps();
|
||||
|
||||
await startFeedServer(container, [
|
||||
{
|
||||
source: "pypi",
|
||||
package_name: "openai",
|
||||
version: latestVersion,
|
||||
...tooYoungTimestamps,
|
||||
},
|
||||
]);
|
||||
|
||||
const installResult = await shell.runCommand(
|
||||
`SAFE_CHAIN_MALWARE_LIST_BASE_URL=http://127.0.0.1:8123 pip3 install --break-system-packages openai==${latestVersion} --safe-chain-logging=verbose`
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
installResult.output.includes(`openai@${latestVersion} is newer than 48 hours and was removed`),
|
||||
`Expected Safe Chain to suppress the pinned openai version. Output was:\n${installResult.output}`
|
||||
);
|
||||
assert.ok(
|
||||
installResult.output.includes(`No matching distribution found for openai==${latestVersion}`) ||
|
||||
installResult.output.includes(`Could not find a version that satisfies the requirement openai==${latestVersion}`),
|
||||
`Expected pip to fail because the exact version was suppressed. Output was:\n${installResult.output}`
|
||||
);
|
||||
assert.ok(
|
||||
!installResult.output.includes("blocked by safe-chain direct download minimum package age"),
|
||||
`Expected resolver failure for an exact pin, not a direct-download block. Output was:\n${installResult.output}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function getLatestPackageVersion(shell, packageName) {
|
||||
const result = await shell.runCommand(`/usr/bin/pip3 index versions ${packageName}`);
|
||||
const version = result.output.match(new RegExp(`${packageName} \\(([^)]+)\\)`))?.[1];
|
||||
|
||||
assert.ok(
|
||||
version,
|
||||
`Could not determine latest ${packageName} version from pip output:\n${result.output}`
|
||||
);
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
async function getInstalledVersion(shell, packageName) {
|
||||
const result = await shell.runCommand(
|
||||
`python3 - <<'PY'
|
||||
import importlib.metadata
|
||||
print(importlib.metadata.version("${packageName}"))
|
||||
PY`
|
||||
);
|
||||
|
||||
return result.output.trim();
|
||||
}
|
||||
|
||||
async function startFeedServer(container, releases) {
|
||||
const shell = await container.openShell("bash");
|
||||
const releasesJson = JSON.stringify(releases, null, 2);
|
||||
|
||||
await shell.runCommand(`mkdir -p /tmp/safe-chain-feed/releases
|
||||
cat > /tmp/safe-chain-feed/malware_pypi.json <<'EOF'
|
||||
[]
|
||||
EOF
|
||||
cat > /tmp/safe-chain-feed/releases/pypi.json <<'EOF'
|
||||
${releasesJson}
|
||||
EOF`);
|
||||
|
||||
container.dockerExec(
|
||||
"nohup python3 -m http.server 8123 -d /tmp/safe-chain-feed >/tmp/safe-chain-feed.log 2>&1 </dev/null &",
|
||||
true
|
||||
);
|
||||
|
||||
const readinessResult = await shell.runCommand(`i=0
|
||||
while [ "$i" -lt 100 ]; do
|
||||
if curl -fsS http://127.0.0.1:8123/releases/pypi.json >/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
if [ "$i" -ge 100 ]; then
|
||||
echo "feed server did not become ready" >&2
|
||||
cat /tmp/safe-chain-feed.log >&2 || true
|
||||
fi`);
|
||||
|
||||
assert.equal(
|
||||
readinessResult.output.includes("feed server did not become ready"),
|
||||
false,
|
||||
`Expected local feed server to become ready. Output was:\n${readinessResult.output}`
|
||||
);
|
||||
}
|
||||
|
||||
function getTooYoungReleaseTimestamps() {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
return {
|
||||
released_on: now,
|
||||
scraped_on: now,
|
||||
};
|
||||
}
|
||||
132
test/e2e/uvx.e2e.spec.js
Normal file
132
test/e2e/uvx.e2e.spec.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||
import assert from "node:assert";
|
||||
|
||||
describe("E2E: uvx coverage", () => {
|
||||
let container;
|
||||
|
||||
before(async () => {
|
||||
DockerTestContainer.buildImage();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
container = new DockerTestContainer();
|
||||
await container.start();
|
||||
|
||||
const installationShell = await container.openShell("zsh");
|
||||
await installationShell.runCommand("safe-chain setup");
|
||||
|
||||
// Clear uv cache
|
||||
await installationShell.runCommand("uv cache clean");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (container) {
|
||||
await container.stop();
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
|
||||
it(`successfully runs a known safe tool with uvx`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uvx ruff --version --safe-chain-logging=verbose"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || /ruff/i.test(result.output),
|
||||
`Expected safe tool to run successfully. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`safe-chain blocks malicious packages via uvx`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uvx safe-chain-pi-test"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("blocked by safe-chain"),
|
||||
`Expected malicious package to be blocked. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("Exiting without installing malicious packages."),
|
||||
`Expected exit message. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`uvx with --from flag runs a safe tool`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uvx --from ruff ruff --version --safe-chain-logging=verbose"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || /ruff/i.test(result.output),
|
||||
`Expected safe tool to run successfully with --from. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`uvx with --from flag blocks malicious packages`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uvx --from safe-chain-pi-test some-command"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("blocked by safe-chain"),
|
||||
`Expected malicious package to be blocked with --from. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("Exiting without installing malicious packages."),
|
||||
`Expected exit message. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`uvx with specific version runs successfully`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uvx ruff@0.4.0 --version --safe-chain-logging=verbose"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || /ruff/i.test(result.output),
|
||||
`Expected safe tool with version to run. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`uvx with --with flag for additional dependencies`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uvx --with requests ruff --version --safe-chain-logging=verbose"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || /ruff/i.test(result.output),
|
||||
`Expected safe tool with --with dependency to run. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`uvx with --with flag blocks malicious additional dependencies`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uvx --with safe-chain-pi-test ruff --version"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("blocked by safe-chain"),
|
||||
`Expected malicious --with dependency to be blocked. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("Exiting without installing malicious packages."),
|
||||
`Expected exit message. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue