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`,
|
`docker run -d --name ${this.containerName} ${imageName} sleep infinity`,
|
||||||
{ stdio: "ignore" }
|
{ stdio: "ignore" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.startMalwareMirror();
|
||||||
|
|
||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to start container: ${error.message}`);
|
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) {
|
dockerExec(command, daemon = false) {
|
||||||
if (!this.isRunning) {
|
if (!this.isRunning) {
|
||||||
throw new Error("Container is not running");
|
throw new Error("Container is not running");
|
||||||
|
|
@ -125,7 +134,7 @@ export class DockerTestContainer {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
// Fallback in case the command doesn't finish in a reasonable time
|
// 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
|
// 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 });
|
resolve({ allData, output: parseShellOutput(allData), command });
|
||||||
ptyProcess.removeListener("data", handleInput);
|
ptyProcess.removeListener("data", handleInput);
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
|
||||||
|
|
@ -84,3 +84,5 @@ RUN npm install -g /pkgs/*.tgz
|
||||||
WORKDIR /testapp
|
WORKDIR /testapp
|
||||||
RUN npm init -y
|
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 () => {
|
it(`safe-chain blocks installation of malicious Python packages`, async () => {
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
|
|
@ -136,7 +136,7 @@ describe("E2E: pip coverage", () => {
|
||||||
`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(
|
||||||
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}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -146,7 +146,7 @@ describe("E2E: pip coverage", () => {
|
||||||
|
|
||||||
const listResult = await shell.runCommand("pip3 list");
|
const listResult = await shell.runCommand("pip3 list");
|
||||||
assert.ok(
|
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}`
|
`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 shell = await container.openShell("zsh");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
const result = await shell.runCommand(
|
||||||
"pipx install safe-chain-pi-test"
|
"pipx install numpy==2.4.4"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -86,7 +86,7 @@ describe("E2E: pipx coverage", () => {
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
const result = await shell.runCommand(
|
||||||
"pipx run safe-chain-pi-test --version"
|
"pipx run numpy==2.4.4 --version"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -122,7 +122,7 @@ describe("E2E: pipx coverage", () => {
|
||||||
await shell.runCommand("pipx install ruff");
|
await shell.runCommand("pipx install ruff");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
const result = await shell.runCommand(
|
||||||
"pipx runpip ruff install safe-chain-pi-test"
|
"pipx runpip ruff install numpy==2.4.4"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -185,7 +185,7 @@ describe("E2E: pipx coverage", () => {
|
||||||
|
|
||||||
await shell.runCommand("pipx install ruff --safe-chain-logging=verbose");
|
await shell.runCommand("pipx install ruff --safe-chain-logging=verbose");
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ describe("E2E: poetry coverage", () => {
|
||||||
await shell.runCommand("cd /tmp/test-poetry-malware && poetry init --no-interaction");
|
await shell.runCommand("cd /tmp/test-poetry-malware && poetry init --no-interaction");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
|
|
@ -300,7 +300,7 @@ describe("E2E: poetry coverage", () => {
|
||||||
|
|
||||||
// Add malware package - this will create lock file and attempt download
|
// Add malware package - this will create lock file and attempt download
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
|
|
@ -324,7 +324,7 @@ describe("E2E: poetry coverage", () => {
|
||||||
|
|
||||||
// Now try to add malware via add command
|
// Now try to add malware via add command
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
|
|
@ -345,7 +345,7 @@ describe("E2E: poetry coverage", () => {
|
||||||
|
|
||||||
// Try to add malware directly - this is the primary vector
|
// Try to add malware directly - this is the primary vector
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ describe("E2E: safe-chain CLI python/pip support", () => {
|
||||||
await shell.runCommand("pip3 cache purge");
|
await shell.runCommand("pip3 cache purge");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
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 shell = await container.openShell("zsh");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
|
|
@ -134,7 +134,7 @@ describe("E2E: uv coverage", () => {
|
||||||
`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(
|
||||||
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}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -144,7 +144,7 @@ describe("E2E: uv coverage", () => {
|
||||||
|
|
||||||
const listResult = await shell.runCommand("uv pip list --system");
|
const listResult = await shell.runCommand("uv pip list --system");
|
||||||
assert.ok(
|
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}`
|
`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");
|
await shell.runCommand("uv init test-project-malware");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
|
|
@ -421,7 +421,7 @@ describe("E2E: uv coverage", () => {
|
||||||
`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(
|
||||||
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}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -445,14 +445,14 @@ describe("E2E: uv coverage", () => {
|
||||||
|
|
||||||
it(`safe-chain blocks malicious packages via uv tool install`, async () => {
|
it(`safe-chain blocks malicious packages via uv tool install`, async () => {
|
||||||
const shell = await container.openShell("zsh");
|
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(
|
assert.ok(
|
||||||
result.output.includes("blocked 1 malicious package downloads:"),
|
result.output.includes("blocked 1 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(
|
||||||
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}`
|
`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");
|
await shell.runCommand("echo 'print(\"test\")' > test_script2.py");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ describe("E2E: uvx coverage", () => {
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
const result = await shell.runCommand(
|
||||||
"uvx safe-chain-pi-test"
|
"uvx numpy==2.4.4"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -74,7 +74,7 @@ describe("E2E: uvx coverage", () => {
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
const result = await shell.runCommand(
|
||||||
"uvx --from safe-chain-pi-test some-command"
|
"uvx --from numpy==2.4.4 some-command"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
@ -117,7 +117,7 @@ describe("E2E: uvx coverage", () => {
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
const result = await shell.runCommand(
|
||||||
"uvx --with safe-chain-pi-test ruff --version"
|
"uvx --with numpy==2.4.4 ruff --version"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue