mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge pull request #436 from AikidoSec/mirror-malware-list-in-e2e-tests
Mirror malware list in e2e tests to mock malware in a harmless way
This commit is contained in:
commit
bf2bf24343
9 changed files with 114 additions and 24 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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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