diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index cd48c4e..543b1a3 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -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); diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index bc7ffc2..3de600c 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -84,3 +84,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 diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index b06978f..af979dc 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -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}` ); }); diff --git a/test/e2e/pipx.e2e.spec.js b/test/e2e/pipx.e2e.spec.js index a554aa6..332709d 100644 --- a/test/e2e/pipx.e2e.spec.js +++ b/test/e2e/pipx.e2e.spec.js @@ -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( diff --git a/test/e2e/poetry.e2e.spec.js b/test/e2e/poetry.e2e.spec.js index 58b74fd..7d77d9c 100644 --- a/test/e2e/poetry.e2e.spec.js +++ b/test/e2e/poetry.e2e.spec.js @@ -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( diff --git a/test/e2e/safe-chain-cli-python.e2e.spec.js b/test/e2e/safe-chain-cli-python.e2e.spec.js index 15dbf94..cf3fda2 100644 --- a/test/e2e/safe-chain-cli-python.e2e.spec.js +++ b/test/e2e/safe-chain-cli-python.e2e.spec.js @@ -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( diff --git a/test/e2e/utils/malwarelistmirror.mjs b/test/e2e/utils/malwarelistmirror.mjs new file mode 100644 index 0000000..e8091b0 --- /dev/null +++ b/test/e2e/utils/malwarelistmirror.mjs @@ -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; +} diff --git a/test/e2e/uv.e2e.spec.js b/test/e2e/uv.e2e.spec.js index 9d5f3b9..5536e22 100644 --- a/test/e2e/uv.e2e.spec.js +++ b/test/e2e/uv.e2e.spec.js @@ -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( diff --git a/test/e2e/uvx.e2e.spec.js b/test/e2e/uvx.e2e.spec.js index 12dfc0f..61fb924 100644 --- a/test/e2e/uvx.e2e.spec.js +++ b/test/e2e/uvx.e2e.spec.js @@ -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(