mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge remote-tracking branch 'aikido/main' into feat/pdm-support
This commit is contained in:
commit
8453012f7b
44 changed files with 1311 additions and 202 deletions
|
|
@ -58,12 +58,21 @@ export class DockerTestContainer {
|
|||
`docker run -d --name ${this.containerName} ${imageName} sleep infinity`,
|
||||
{ stdio: "ignore" }
|
||||
);
|
||||
|
||||
await this.startMalwareMirror();
|
||||
|
||||
this.isRunning = true;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to start container: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async startMalwareMirror() {
|
||||
const shell = await this.openShell("zsh");
|
||||
await shell.runCommand("node /utils/malwarelistmirror.mjs &");
|
||||
await shell.runCommand("until curl -sf http://127.0.0.1:5555/ready; do sleep 0.2; done");
|
||||
}
|
||||
|
||||
dockerExec(command, daemon = false) {
|
||||
if (!this.isRunning) {
|
||||
throw new Error("Container is not running");
|
||||
|
|
@ -125,7 +134,7 @@ export class DockerTestContainer {
|
|||
const timeout = setTimeout(() => {
|
||||
// Fallback in case the command doesn't finish in a reasonable time
|
||||
// oxlint-disable-next-line no-console - having this log in CI helps diagnose issues
|
||||
console.log("Command timeout reached");
|
||||
console.log(`Command timeout reached for "${command}"`);
|
||||
resolve({ allData, output: parseShellOutput(allData), command });
|
||||
ptyProcess.removeListener("data", handleInput);
|
||||
}, 15000);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ ARG NODE_VERSION=latest
|
|||
ARG NPM_VERSION=latest
|
||||
ARG YARN_VERSION=latest
|
||||
ARG PNPM_VERSION=latest
|
||||
ARG RUSH_VERSION=latest
|
||||
ARG PYTHON_VERSION=3
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
|
@ -46,6 +47,7 @@ RUN volta install node@${NODE_VERSION}
|
|||
RUN volta install npm@${NPM_VERSION}
|
||||
RUN volta install yarn@${YARN_VERSION}
|
||||
RUN volta install pnpm@${PNPM_VERSION}
|
||||
RUN volta install @microsoft/rush@${RUSH_VERSION}
|
||||
|
||||
# Install Bun
|
||||
RUN curl -fsSL https://bun.sh/install | bash
|
||||
|
|
@ -88,3 +90,5 @@ RUN npm install -g /pkgs/*.tgz
|
|||
WORKDIR /testapp
|
||||
RUN npm init -y
|
||||
|
||||
COPY test/e2e/utils/malwarelistmirror.mjs /utils/malwarelistmirror.mjs
|
||||
ENV SAFE_CHAIN_MALWARE_LIST_BASE_URL=http://127.0.0.1:5555
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ describe("E2E: pip coverage", () => {
|
|||
it(`safe-chain blocks installation of malicious Python packages`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
const result = await shell.runCommand(
|
||||
"pip3 install --break-system-packages safe-chain-pi-test"
|
||||
"pip3 install --break-system-packages numpy==2.4.4 --safe-chain-logging=verbose"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -136,7 +136,7 @@ describe("E2E: pip coverage", () => {
|
|||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("safe_chain_pi_test@0.0.1"),
|
||||
result.output.includes("numpy@2.4.4"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
|
|
@ -146,7 +146,7 @@ describe("E2E: pip coverage", () => {
|
|||
|
||||
const listResult = await shell.runCommand("pip3 list");
|
||||
assert.ok(
|
||||
!listResult.output.includes("safe-chain-pi-test"),
|
||||
!listResult.output.includes("numpy"),
|
||||
`Malicious package was installed despite safe-chain protection. Output of 'pip3 list' was:\n${listResult.output}`
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ describe("E2E: pipx coverage", () => {
|
|||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"pipx install safe-chain-pi-test"
|
||||
"pipx install numpy==2.4.4"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -86,7 +86,7 @@ describe("E2E: pipx coverage", () => {
|
|||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"pipx run safe-chain-pi-test --version"
|
||||
"pipx run numpy==2.4.4 --version"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -122,7 +122,7 @@ describe("E2E: pipx coverage", () => {
|
|||
await shell.runCommand("pipx install ruff");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"pipx runpip ruff install safe-chain-pi-test"
|
||||
"pipx runpip ruff install numpy==2.4.4"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -185,7 +185,7 @@ describe("E2E: pipx coverage", () => {
|
|||
|
||||
await shell.runCommand("pipx install ruff --safe-chain-logging=verbose");
|
||||
const result = await shell.runCommand(
|
||||
"pipx inject ruff safe-chain-pi-test --safe-chain-logging=verbose"
|
||||
"pipx inject ruff numpy==2.4.4 --safe-chain-logging=verbose"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ describe("E2E: poetry coverage", () => {
|
|||
await shell.runCommand("cd /tmp/test-poetry-malware && poetry init --no-interaction");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-poetry-malware && poetry add safe-chain-pi-test"
|
||||
"cd /tmp/test-poetry-malware && poetry add numpy==2.4.4"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -300,7 +300,7 @@ describe("E2E: poetry coverage", () => {
|
|||
|
||||
// Add malware package - this will create lock file and attempt download
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-poetry-install-malware && poetry add safe-chain-pi-test 2>&1"
|
||||
"cd /tmp/test-poetry-install-malware && poetry add numpy==2.4.4 2>&1"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -324,7 +324,7 @@ describe("E2E: poetry coverage", () => {
|
|||
|
||||
// Now try to add malware via add command
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-poetry-update-add && poetry add safe-chain-pi-test 2>&1"
|
||||
"cd /tmp/test-poetry-update-add && poetry add numpy==2.4.4 2>&1"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -345,7 +345,7 @@ describe("E2E: poetry coverage", () => {
|
|||
|
||||
// Try to add malware directly - this is the primary vector
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-poetry-req-malware && poetry add safe-chain-pi-test requests 2>&1"
|
||||
"cd /tmp/test-poetry-req-malware && poetry add numpy==2.4.4 requests 2>&1"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
|
|||
148
test/e2e/rush.e2e.spec.js
Normal file
148
test/e2e/rush.e2e.spec.js
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||
import {
|
||||
buildRushConfig,
|
||||
resolveRushVersions,
|
||||
writeTextFile,
|
||||
} from "./utils/rushtestutils.mjs";
|
||||
import assert from "node:assert";
|
||||
|
||||
// These tests cover safe-chain's Rush wrapper: pre-scanning `rush add` and
|
||||
// blocking malicious packages downloaded during `rush update` via the MITM
|
||||
// proxy. They use a single Rush-internal package manager (pnpm) — see
|
||||
// `utils/rushtestutils.mjs` for why this suite isn't parameterised over the
|
||||
// CI matrix's NPM_VERSION/PNPM_VERSION/YARN_VERSION values.
|
||||
|
||||
describe("E2E: rush coverage", () => {
|
||||
let container;
|
||||
/** @type {{ rushVersion: string, pnpmVersion: string } | undefined} */
|
||||
let resolvedVersions;
|
||||
|
||||
before(async () => {
|
||||
DockerTestContainer.buildImage();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
container = new DockerTestContainer();
|
||||
await container.start();
|
||||
|
||||
const installationShell = await container.openShell("zsh");
|
||||
await installationShell.runCommand("safe-chain setup");
|
||||
|
||||
if (!resolvedVersions) {
|
||||
resolvedVersions = await resolveRushVersions(installationShell);
|
||||
}
|
||||
|
||||
await setupRushWorkspace(installationShell, { resolvedVersions });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (container) {
|
||||
await container.stop();
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
|
||||
it("safe-chain successfully adds safe packages", async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
const result = await shell.runCommand(
|
||||
"cd /testapp/apps/test-app && rush add --package axios@1.13.0 --exact --skip-update --safe-chain-logging=verbose"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found."),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it("safe-chain blocks rush add of malicious packages", async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
const result = await shell.runCommand(
|
||||
"cd /testapp/apps/test-app && rush add --package safe-chain-test --skip-update"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("Malicious changes detected:"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("- safe-chain-test"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("Exiting without installing malicious packages."),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
|
||||
const packageJson = await shell.runCommand(
|
||||
"cat /testapp/apps/test-app/package.json"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!packageJson.output.includes("safe-chain-test"),
|
||||
`Malicious package was added despite safe-chain protection. Output was:\n${packageJson.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it("safe-chain proxy blocks malicious package downloads during rush update", async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
await setupRushWorkspace(shell, {
|
||||
resolvedVersions,
|
||||
packageJson: `{
|
||||
"name": "test-app",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"safe-chain-test": "0.0.1-security"
|
||||
}
|
||||
}`,
|
||||
});
|
||||
|
||||
// `--safe-chain-skip-minimum-package-age` is needed because Rush's
|
||||
// internal pnpm bootstrap (`npm install pnpm@<resolvedVersion>`) goes
|
||||
// through the safe-chain proxy. When the CI matrix selects pnpm
|
||||
// `latest`, the just-released version can be below the minimum age
|
||||
// threshold and Rush's install would otherwise be blocked before our
|
||||
// malicious-download assertion is reached.
|
||||
const result = await shell.runCommand(
|
||||
"cd /testapp/apps/test-app && rush update --safe-chain-skip-minimum-package-age"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
result.output,
|
||||
/blocked \d+ malicious package downloads/,
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("- safe-chain-test"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("Exiting without installing malicious packages."),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function setupRushWorkspace(shell, { resolvedVersions, packageJson }) {
|
||||
const rushConfig = buildRushConfig({
|
||||
rushVersion: resolvedVersions.rushVersion,
|
||||
pnpmVersion: resolvedVersions.pnpmVersion,
|
||||
});
|
||||
|
||||
await shell.runCommand("rm -rf /testapp/common /testapp/apps/test-app");
|
||||
await shell.runCommand("mkdir -p /testapp/apps/test-app");
|
||||
await writeTextFile(
|
||||
shell,
|
||||
"/testapp/rush.json",
|
||||
JSON.stringify(rushConfig, null, 2)
|
||||
);
|
||||
await writeTextFile(
|
||||
shell,
|
||||
"/testapp/apps/test-app/package.json",
|
||||
packageJson ??
|
||||
`{
|
||||
"name": "test-app",
|
||||
"version": "1.0.0"
|
||||
}`
|
||||
);
|
||||
}
|
||||
100
test/e2e/rushx.e2e.spec.js
Normal file
100
test/e2e/rushx.e2e.spec.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||
import {
|
||||
buildRushConfig,
|
||||
resolveRushVersions,
|
||||
writeTextFile,
|
||||
} from "./utils/rushtestutils.mjs";
|
||||
import assert from "node:assert";
|
||||
|
||||
describe("E2E: rushx coverage", () => {
|
||||
let container;
|
||||
/** @type {{ rushVersion: string, pnpmVersion: string } | undefined} */
|
||||
let resolvedVersions;
|
||||
|
||||
before(async () => {
|
||||
DockerTestContainer.buildImage();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
container = new DockerTestContainer();
|
||||
await container.start();
|
||||
|
||||
const installationShell = await container.openShell("zsh");
|
||||
await installationShell.runCommand("safe-chain setup");
|
||||
|
||||
if (!resolvedVersions) {
|
||||
resolvedVersions = await resolveRushVersions(installationShell);
|
||||
}
|
||||
|
||||
await setupRushWorkspace(installationShell, { resolvedVersions });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (container) {
|
||||
await container.stop();
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
|
||||
it("safe-chain successfully scans safe package downloads from rushx scripts", async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
const result = await shell.runCommand(
|
||||
"cd /testapp/apps/test-app && rushx install-safe --safe-chain-logging=verbose"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found."),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it("safe-chain blocks malicious package downloads from rushx scripts", async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
const result = await shell.runCommand(
|
||||
"cd /testapp/apps/test-app && rushx install-malicious"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
result.output,
|
||||
/blocked \d+ malicious package downloads/,
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("- safe-chain-test"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("Exiting without installing malicious packages."),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function setupRushWorkspace(shell, { resolvedVersions }) {
|
||||
const rushConfig = buildRushConfig({
|
||||
rushVersion: resolvedVersions.rushVersion,
|
||||
pnpmVersion: resolvedVersions.pnpmVersion,
|
||||
});
|
||||
|
||||
await shell.runCommand(
|
||||
"mkdir -p /testapp/common/config/rush /testapp/apps/test-app"
|
||||
);
|
||||
await writeTextFile(
|
||||
shell,
|
||||
"/testapp/rush.json",
|
||||
JSON.stringify(rushConfig, null, 2)
|
||||
);
|
||||
await writeTextFile(
|
||||
shell,
|
||||
"/testapp/apps/test-app/package.json",
|
||||
`{
|
||||
"name": "test-app",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"install-safe": "npm install axios@1.13.0",
|
||||
"install-malicious": "npm install safe-chain-test@0.0.1-security"
|
||||
}
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
|
@ -97,7 +97,7 @@ describe("E2E: safe-chain CLI python/pip support", () => {
|
|||
await shell.runCommand("pip3 cache purge");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"safe-chain pip3 install --break-system-packages safe-chain-pi-test"
|
||||
"safe-chain pip3 install --break-system-packages numpy==2.4.4"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
|
|||
79
test/e2e/utils/malwarelistmirror.mjs
Normal file
79
test/e2e/utils/malwarelistmirror.mjs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// Test-only mirror of the malware list. Injects known-safe packages as malicious
|
||||
// to simulate blocking behavior in e2e tests without affecting real data.
|
||||
|
||||
import * as http from "node:http";
|
||||
|
||||
const lists = await downloadLists();
|
||||
const server = http.createServer(handleRequest);
|
||||
server.listen(5555, "127.0.0.1");
|
||||
console.log("listening on http://127.0.0.1:5555");
|
||||
|
||||
function handleRequest(req, res) {
|
||||
if (req.method !== "GET" || !req.url) {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url.startsWith("/ready")) {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const list of lists) {
|
||||
if (req.url.startsWith(list.path)) {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify(list.data));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
|
||||
async function downloadLists() {
|
||||
const lists = [
|
||||
{
|
||||
"path": "/malware_predictions.json",
|
||||
"patchFunc": (data) => data,
|
||||
},
|
||||
{
|
||||
"path": "/malware_pypi.json",
|
||||
"patchFunc": patchPypi,
|
||||
},
|
||||
{
|
||||
"path": "/releases/npm.json",
|
||||
"patchFunc": (data) => data,
|
||||
},
|
||||
{
|
||||
"path": "/releases/pypi.json",
|
||||
"patchFunc": (data) => data,
|
||||
},
|
||||
]
|
||||
|
||||
for (const list of lists) {
|
||||
list.data = list.patchFunc(await downloadList(list.path));
|
||||
}
|
||||
|
||||
return lists;
|
||||
}
|
||||
|
||||
async function downloadList(path) {
|
||||
const baseUrl = "https://malware-list.aikido.dev";
|
||||
const url = `${baseUrl}${path}`;
|
||||
const response = await fetch(url);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
function patchPypi(data) {
|
||||
|
||||
data.push({
|
||||
"package_name": "numpy",
|
||||
"version": "2.4.4",
|
||||
"reason": "MALWARE"
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
69
test/e2e/utils/rushtestutils.mjs
Normal file
69
test/e2e/utils/rushtestutils.mjs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Helpers for the Rush E2E suites.
|
||||
//
|
||||
// What these suites actually test: that safe-chain's shim intercepts `rush`
|
||||
// and `rushx` invocations correctly. The contents of `rush.json` are just
|
||||
// fixture noise needed to make Rush run at all — Rush's schema requires
|
||||
// exact semver for `rushVersion`/`pnpmVersion` and refuses dist-tags like
|
||||
// "latest", so we read both back from the binaries baked into the image.
|
||||
//
|
||||
// * `rushVersion` ← `rush --version` (image installs
|
||||
// `@microsoft/rush@${RUSH_VERSION:-latest}`).
|
||||
// * `pnpmVersion` ← `pnpm --version` (image installs
|
||||
// `pnpm@${PNPM_VERSION:-latest}`). Rush downloads its own copy of this
|
||||
// into `~/.rush/...`; using the same exact version as the system pnpm
|
||||
// just keeps the fixture in lockstep with whatever the CI matrix picks.
|
||||
|
||||
/** Resolves the versions to put into `rush.json`. */
|
||||
export async function resolveRushVersions(shell) {
|
||||
// Sequential: the helper drives a single PTY shell.
|
||||
const rushVersion = await getInstalledVersion(shell, "rush");
|
||||
const pnpmVersion = await getInstalledVersion(shell, "pnpm");
|
||||
return { rushVersion, pnpmVersion };
|
||||
}
|
||||
|
||||
/** Builds the standard `rush.json` body for the e2e fixtures. */
|
||||
export function buildRushConfig({ rushVersion, pnpmVersion, projects }) {
|
||||
return {
|
||||
$schema:
|
||||
"https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json",
|
||||
rushVersion,
|
||||
pnpmVersion,
|
||||
nodeSupportedVersionRange: ">=18.0.0",
|
||||
projectFolderMinDepth: 1,
|
||||
projectFolderMaxDepth: 2,
|
||||
gitPolicy: {},
|
||||
repository: {
|
||||
url: "https://example.com/testapp.git",
|
||||
defaultBranch: "main",
|
||||
},
|
||||
eventHooks: {
|
||||
preRushInstall: [],
|
||||
postRushInstall: [],
|
||||
preRushBuild: [],
|
||||
postRushBuild: [],
|
||||
},
|
||||
projects: projects ?? [
|
||||
{ packageName: "test-app", projectFolder: "apps/test-app" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a UTF-8 text file inside the container, base64-encoding the payload
|
||||
* to avoid shell escaping issues for arbitrary content.
|
||||
*/
|
||||
export async function writeTextFile(shell, filePath, content) {
|
||||
const encoded = Buffer.from(content).toString("base64");
|
||||
await shell.runCommand(`printf '%s' '${encoded}' | base64 -d > ${filePath}`);
|
||||
}
|
||||
|
||||
async function getInstalledVersion(shell, command) {
|
||||
const { output } = await shell.runCommand(`${command} --version`);
|
||||
const match = output.match(/\b(\d+\.\d+\.\d+)\b/);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Could not determine installed ${command} version. Output was:\n${output}`
|
||||
);
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ describe("E2E: uv coverage", () => {
|
|||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uv pip install --system --break-system-packages safe-chain-pi-test"
|
||||
"uv pip install --system --break-system-packages numpy==2.4.4"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -134,7 +134,7 @@ describe("E2E: uv coverage", () => {
|
|||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("safe_chain_pi_test@0.0.1"),
|
||||
result.output.includes("numpy@2.4.4"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
|
|
@ -144,7 +144,7 @@ describe("E2E: uv coverage", () => {
|
|||
|
||||
const listResult = await shell.runCommand("uv pip list --system");
|
||||
assert.ok(
|
||||
!listResult.output.includes("safe-chain-pi-test"),
|
||||
!listResult.output.includes("numpy"),
|
||||
`Malicious package was installed despite safe-chain protection. Output of 'uv pip list' was:\n${listResult.output}`
|
||||
);
|
||||
});
|
||||
|
|
@ -413,7 +413,7 @@ describe("E2E: uv coverage", () => {
|
|||
await shell.runCommand("uv init test-project-malware");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd test-project-malware && uv add safe-chain-pi-test"
|
||||
"cd test-project-malware && uv add numpy==2.4.4"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -421,7 +421,7 @@ describe("E2E: uv coverage", () => {
|
|||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("safe_chain_pi_test@0.0.1"),
|
||||
result.output.includes("numpy@2.4.4"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
|
|
@ -445,14 +445,14 @@ describe("E2E: uv coverage", () => {
|
|||
|
||||
it(`safe-chain blocks malicious packages via uv tool install`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
const result = await shell.runCommand("uv tool install safe-chain-pi-test");
|
||||
const result = await shell.runCommand("uv tool install numpy==2.4.4");
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("blocked 1 malicious package downloads:"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("safe_chain_pi_test@0.0.1"),
|
||||
result.output.includes("numpy@2.4.4"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
|
@ -482,7 +482,7 @@ describe("E2E: uv coverage", () => {
|
|||
await shell.runCommand("echo 'print(\"test\")' > test_script2.py");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uv run --with safe-chain-pi-test test_script2.py"
|
||||
"uv run --with numpy==2.4.4 test_script2.py"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ describe("E2E: uvx coverage", () => {
|
|||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uvx safe-chain-pi-test"
|
||||
"uvx numpy==2.4.4"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -74,7 +74,7 @@ describe("E2E: uvx coverage", () => {
|
|||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uvx --from safe-chain-pi-test some-command"
|
||||
"uvx --from numpy==2.4.4 some-command"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -117,7 +117,7 @@ describe("E2E: uvx coverage", () => {
|
|||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"uvx --with safe-chain-pi-test ruff --version"
|
||||
"uvx --with numpy==2.4.4 ruff --version"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue