mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Implement e2e tests
This commit is contained in:
parent
f817bf887a
commit
059cba06bc
17 changed files with 163 additions and 293 deletions
34
package-lock.json
generated
34
package-lock.json
generated
|
|
@ -154,8 +154,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-darwin-x64": {
|
"node_modules/@oven/bun-darwin-x64": {
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
|
|
@ -168,8 +167,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-darwin-x64-baseline": {
|
"node_modules/@oven/bun-darwin-x64-baseline": {
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
|
|
@ -182,8 +180,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-linux-aarch64": {
|
"node_modules/@oven/bun-linux-aarch64": {
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
|
|
@ -196,8 +193,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-linux-aarch64-musl": {
|
"node_modules/@oven/bun-linux-aarch64-musl": {
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
|
|
@ -210,8 +206,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-linux-x64": {
|
"node_modules/@oven/bun-linux-x64": {
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
|
|
@ -224,8 +219,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-linux-x64-baseline": {
|
"node_modules/@oven/bun-linux-x64-baseline": {
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
|
|
@ -238,8 +232,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-linux-x64-musl": {
|
"node_modules/@oven/bun-linux-x64-musl": {
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
|
|
@ -252,8 +245,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-linux-x64-musl-baseline": {
|
"node_modules/@oven/bun-linux-x64-musl-baseline": {
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
|
|
@ -266,8 +258,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-windows-x64": {
|
"node_modules/@oven/bun-windows-x64": {
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
|
|
@ -280,8 +271,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oven/bun-windows-x64-baseline": {
|
"node_modules/@oven/bun-windows-x64-baseline": {
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
|
|
@ -294,8 +284,7 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@oxlint/darwin-arm64": {
|
"node_modules/@oxlint/darwin-arm64": {
|
||||||
"version": "1.22.0",
|
"version": "1.22.0",
|
||||||
|
|
@ -1735,6 +1724,7 @@
|
||||||
"aikido-bunx": "bin/aikido-bunx.js",
|
"aikido-bunx": "bin/aikido-bunx.js",
|
||||||
"aikido-npm": "bin/aikido-npm.js",
|
"aikido-npm": "bin/aikido-npm.js",
|
||||||
"aikido-npx": "bin/aikido-npx.js",
|
"aikido-npx": "bin/aikido-npx.js",
|
||||||
|
"aikido-pip": "bin/aikido-pip.js",
|
||||||
"aikido-pnpm": "bin/aikido-pnpm.js",
|
"aikido-pnpm": "bin/aikido-pnpm.js",
|
||||||
"aikido-pnpx": "bin/aikido-pnpx.js",
|
"aikido-pnpx": "bin/aikido-pnpx.js",
|
||||||
"aikido-yarn": "bin/aikido-yarn.js",
|
"aikido-yarn": "bin/aikido-yarn.js",
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,19 @@ import { setEcoSystem } from "../src/config/settings.js";
|
||||||
let packageManagerName = "pip";
|
let packageManagerName = "pip";
|
||||||
let targetVersionMajor;
|
let targetVersionMajor;
|
||||||
|
|
||||||
// Copy argv so we can mutate while parsing
|
// Copy argv so we can modify it
|
||||||
const argv = process.argv.slice(2);
|
const argv = process.argv.slice(2);
|
||||||
|
|
||||||
for (let i = 0; i < argv.length; i++) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
const a = argv[i];
|
const a = argv[i];
|
||||||
|
|
||||||
// --target-version-major
|
// --target-version-major tells us which pip version is being used (2 or 3)
|
||||||
if (a === "--target-version-major" && i + 1 < argv.length) {
|
if (a === "--target-version-major" && i + 1 < argv.length) {
|
||||||
console.log("Setting targetVersionMajor from CLI arg:", argv[i + 1]);
|
targetVersionMajor = argv[i + 1];
|
||||||
targetVersionMajor = argv[i + 1];
|
argv.splice(i, 2);
|
||||||
argv.splice(i, 2);
|
i -= 1;
|
||||||
i -= 1;
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user explicitly called python3, prefer pip3
|
// If the user explicitly called python3, prefer pip3
|
||||||
|
|
@ -29,12 +28,10 @@ if (targetVersionMajor && String(targetVersionMajor).trim() === "3") {
|
||||||
packageManagerName = "pip3";
|
packageManagerName = "pip3";
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("** aikido-pip ** Final arguments (after processing):", argv);
|
|
||||||
|
|
||||||
// Set eco system
|
// Set eco system
|
||||||
setEcoSystem("py");
|
setEcoSystem("py");
|
||||||
|
|
||||||
initializePackageManager(packageManagerName);
|
initializePackageManager(packageManagerName);
|
||||||
var exitCode = await main(argv);
|
const exitCode = await main(argv);
|
||||||
|
|
||||||
process.exit(exitCode);
|
process.exit(exitCode);
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,22 @@ import { getEcoSystem } from "../config/settings.js";
|
||||||
|
|
||||||
const malwareDatabaseUrls = {
|
const malwareDatabaseUrls = {
|
||||||
js: "https://malware-list.aikido.dev/malware_predictions.json",
|
js: "https://malware-list.aikido.dev/malware_predictions.json",
|
||||||
python: "https://malware-list.aikido.dev/malware_predictions_python.json",
|
py: "https://malware-list.aikido.dev/malware_predictions_python.json",
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchMalwareDatabase() {
|
export async function fetchMalwareDatabase() {
|
||||||
const ecosystem = getEcoSystem() || "js";
|
const ecosystem = getEcoSystem() || "js";
|
||||||
if (ecosystem === "py") {
|
|
||||||
console.log("**aikido.js** Using 'python' ecosystem for malware database fetch");
|
|
||||||
}
|
|
||||||
const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem];
|
const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem];
|
||||||
const response = await fetch(malwareDatabaseUrl);
|
const response = await fetch(malwareDatabaseUrl);
|
||||||
|
|
||||||
|
// Python malware database doesn't exist yet, return empty database
|
||||||
|
if (!response.ok && ecosystem === "py" && response.status === 403) {
|
||||||
|
return {
|
||||||
|
malwareDatabase: [],
|
||||||
|
version: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`);
|
throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
@ -30,14 +36,17 @@ export async function fetchMalwareDatabase() {
|
||||||
|
|
||||||
export async function fetchMalwareDatabaseVersion() {
|
export async function fetchMalwareDatabaseVersion() {
|
||||||
const ecosystem = getEcoSystem() || "js";
|
const ecosystem = getEcoSystem() || "js";
|
||||||
if (ecosystem === "py") {
|
|
||||||
console.log("**aikido.js** Using 'python' ecosystem for malware database fetch");
|
|
||||||
}
|
|
||||||
|
|
||||||
const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem];
|
const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem];
|
||||||
const response = await fetch(malwareDatabaseUrl, {
|
const response = await fetch(malwareDatabaseUrl, {
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Python malware database doesn't exist yet, return undefined
|
||||||
|
if (!response.ok && ecosystem === "py" && response.status === 403) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error fetching ${ecosystem} malware database version: ${response.statusText}`
|
`Error fetching ${ecosystem} malware database version: ${response.statusText}`
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ export async function main(args) {
|
||||||
await proxy.startServer();
|
await proxy.startServer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(chalk.blueBright.bold("main.js: Scanning for malicious packages..."));
|
|
||||||
// This parses all the --safe-chain arguments and removes them from the args array
|
// This parses all the --safe-chain arguments and removes them from the args array
|
||||||
args = initializeCliArguments(args);
|
args = initializeCliArguments(args);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ const state = {
|
||||||
packageManagerName: null,
|
packageManagerName: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PIP_COMMANDS = new Set(["pip", "pip3"]);
|
|
||||||
|
|
||||||
export function initializePackageManager(packageManagerName) {
|
export function initializePackageManager(packageManagerName) {
|
||||||
if (packageManagerName === "npm") {
|
if (packageManagerName === "npm") {
|
||||||
state.packageManagerName = createNpmPackageManager();
|
state.packageManagerName = createNpmPackageManager();
|
||||||
|
|
@ -32,7 +30,7 @@ export function initializePackageManager(packageManagerName) {
|
||||||
state.packageManagerName = createBunPackageManager();
|
state.packageManagerName = createBunPackageManager();
|
||||||
} else if (packageManagerName === "bunx") {
|
} else if (packageManagerName === "bunx") {
|
||||||
state.packageManagerName = createBunxPackageManager();
|
state.packageManagerName = createBunxPackageManager();
|
||||||
} else if (PIP_COMMANDS.has(packageManagerName)) {
|
} else if (packageManagerName === "pip" || packageManagerName === "pip3") {
|
||||||
state.packageManagerName = createPipPackageManager(packageManagerName);
|
state.packageManagerName = createPipPackageManager(packageManagerName);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
throw new Error("Unsupported package manager: " + packageManagerName);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
||||||
import { nullScanner } from "./dependencyScanner/nullScanner.js";
|
|
||||||
import { runPip } from "./runPipCommand.js";
|
import { runPip } from "./runPipCommand.js";
|
||||||
import {
|
import {
|
||||||
getPipCommandForArgs,
|
getPipCommandForArgs,
|
||||||
|
|
@ -9,8 +8,7 @@ import {
|
||||||
} from "./utils/pipCommands.js";
|
} from "./utils/pipCommands.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a package manager interface for Python's pip package installer
|
* Creates a package manager
|
||||||
*
|
|
||||||
* @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip"
|
* @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip"
|
||||||
*/
|
*/
|
||||||
export function createPipPackageManager(command = "pip") {
|
export function createPipPackageManager(command = "pip") {
|
||||||
|
|
@ -41,15 +39,20 @@ const commandScannerMapping = {
|
||||||
[pipInstallCommand]: commandArgumentScanner(),
|
[pipInstallCommand]: commandArgumentScanner(),
|
||||||
[pipDownloadCommand]: commandArgumentScanner(), // download also fetches packages from PyPI
|
[pipDownloadCommand]: commandArgumentScanner(), // download also fetches packages from PyPI
|
||||||
[pipWheelCommand]: commandArgumentScanner(), // wheel downloads and builds packages
|
[pipWheelCommand]: commandArgumentScanner(), // wheel downloads and builds packages
|
||||||
// Other commands (uninstall, list, etc.) will use nullScanner() by default
|
// Other commands return null scanner by default
|
||||||
|
};
|
||||||
|
|
||||||
|
const NULL_SCANNER = {
|
||||||
|
shouldScan: () => false,
|
||||||
|
scan: () => [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function findDependencyScannerForCommand(scanners, args) {
|
function findDependencyScannerForCommand(scanners, args) {
|
||||||
const command = getPipCommandForArgs(args);
|
const command = getPipCommandForArgs(args);
|
||||||
if (!command) {
|
if (!command) {
|
||||||
return nullScanner();
|
return NULL_SCANNER;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scanner = scanners[command];
|
const scanner = scanners[command];
|
||||||
return scanner ? scanner : nullScanner();
|
return scanner || NULL_SCANNER;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import assert from "node:assert";
|
||||||
import { createPipPackageManager } from "./createPackageManager.js";
|
import { createPipPackageManager } from "./createPackageManager.js";
|
||||||
|
|
||||||
test("createPipPackageManager", async (t) => {
|
test("createPipPackageManager", async (t) => {
|
||||||
await t.test("should create package manager with default pip command", () => {
|
await t.test("should create package manager with required interface", () => {
|
||||||
const pm = createPipPackageManager();
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
assert.ok(pm);
|
assert.ok(pm);
|
||||||
|
|
@ -12,106 +12,49 @@ test("createPipPackageManager", async (t) => {
|
||||||
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
|
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should create package manager with custom pip3 command", () => {
|
await t.test("should accept pip3 as command parameter", () => {
|
||||||
const pm = createPipPackageManager("pip3");
|
const pm = createPipPackageManager("pip3");
|
||||||
|
|
||||||
assert.ok(pm);
|
assert.ok(pm);
|
||||||
assert.strictEqual(typeof pm.runCommand, "function");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should recognize install command as supported", () => {
|
await t.test("should support install, download, and wheel commands", () => {
|
||||||
const pm = createPipPackageManager();
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
// Note: Currently returns false because commandArgumentScanner is not yet implemented
|
assert.strictEqual(pm.isSupportedCommand(["install", "requests"]), true);
|
||||||
// When implemented, this should return true
|
assert.strictEqual(pm.isSupportedCommand(["download", "requests"]), true);
|
||||||
const result = pm.isSupportedCommand(["install", "requests"]);
|
assert.strictEqual(pm.isSupportedCommand(["wheel", "requests"]), true);
|
||||||
assert.strictEqual(typeof result, "boolean");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should recognize download command as supported", () => {
|
await t.test("should not support uninstall and info commands", () => {
|
||||||
const pm = createPipPackageManager();
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
const result = pm.isSupportedCommand(["download", "requests"]);
|
assert.strictEqual(pm.isSupportedCommand(["uninstall", "requests"]), false);
|
||||||
assert.strictEqual(typeof result, "boolean");
|
assert.strictEqual(pm.isSupportedCommand(["list"]), false);
|
||||||
|
assert.strictEqual(pm.isSupportedCommand(["show", "requests"]), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should recognize wheel command as supported", () => {
|
await t.test("should extract packages from install command", () => {
|
||||||
const pm = createPipPackageManager();
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
const result = pm.isSupportedCommand(["wheel", "requests"]);
|
|
||||||
assert.strictEqual(typeof result, "boolean");
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should not support uninstall command", () => {
|
|
||||||
const pm = createPipPackageManager();
|
|
||||||
|
|
||||||
const result = pm.isSupportedCommand(["uninstall", "requests"]);
|
|
||||||
assert.strictEqual(result, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should not support list command", () => {
|
|
||||||
const pm = createPipPackageManager();
|
|
||||||
|
|
||||||
const result = pm.isSupportedCommand(["list"]);
|
|
||||||
assert.strictEqual(result, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should not support show command", () => {
|
|
||||||
const pm = createPipPackageManager();
|
|
||||||
|
|
||||||
const result = pm.isSupportedCommand(["show", "requests"]);
|
|
||||||
assert.strictEqual(result, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should return empty array for getDependencyUpdatesForCommand on install", () => {
|
|
||||||
const pm = createPipPackageManager();
|
|
||||||
|
|
||||||
// Note: Currently returns [] because commandArgumentScanner is not yet implemented
|
|
||||||
const result = pm.getDependencyUpdatesForCommand(["install", "requests==2.28.0"]);
|
const result = pm.getDependencyUpdatesForCommand(["install", "requests==2.28.0"]);
|
||||||
assert.ok(Array.isArray(result));
|
assert.ok(Array.isArray(result));
|
||||||
});
|
assert.strictEqual(result.length, 1);
|
||||||
|
assert.strictEqual(result[0].name, "requests");
|
||||||
await t.test("should return empty array for getDependencyUpdatesForCommand on download", () => {
|
assert.strictEqual(result[0].version, "2.28.0");
|
||||||
const pm = createPipPackageManager();
|
|
||||||
|
|
||||||
const result = pm.getDependencyUpdatesForCommand(["download", "flask"]);
|
|
||||||
assert.ok(Array.isArray(result));
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should return empty array for getDependencyUpdatesForCommand on wheel", () => {
|
|
||||||
const pm = createPipPackageManager();
|
|
||||||
|
|
||||||
const result = pm.getDependencyUpdatesForCommand(["wheel", "django"]);
|
|
||||||
assert.ok(Array.isArray(result));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should return empty array for unsupported commands", () => {
|
await t.test("should return empty array for unsupported commands", () => {
|
||||||
const pm = createPipPackageManager();
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
const result = pm.getDependencyUpdatesForCommand(["uninstall", "requests"]);
|
const result = pm.getDependencyUpdatesForCommand(["uninstall", "requests"]);
|
||||||
assert.strictEqual(Array.isArray(result), true);
|
assert.ok(Array.isArray(result));
|
||||||
assert.strictEqual(result.length, 0);
|
assert.strictEqual(result.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should handle empty args array", () => {
|
await t.test("should handle empty args gracefully", () => {
|
||||||
const pm = createPipPackageManager();
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
const supported = pm.isSupportedCommand([]);
|
assert.strictEqual(pm.isSupportedCommand([]), false);
|
||||||
assert.strictEqual(supported, false);
|
assert.deepStrictEqual(pm.getDependencyUpdatesForCommand([]), []);
|
||||||
|
|
||||||
const deps = pm.getDependencyUpdatesForCommand([]);
|
|
||||||
assert.ok(Array.isArray(deps));
|
|
||||||
assert.strictEqual(deps.length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should handle args with only flags", () => {
|
|
||||||
const pm = createPipPackageManager();
|
|
||||||
|
|
||||||
const supported = pm.isSupportedCommand(["--version"]);
|
|
||||||
assert.strictEqual(supported, false);
|
|
||||||
|
|
||||||
const deps = pm.getDependencyUpdatesForCommand(["-h", "--help"]);
|
|
||||||
assert.ok(Array.isArray(deps));
|
|
||||||
assert.strictEqual(deps.length, 0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js";
|
import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js";
|
||||||
import { hasDryRunArg } from "../utils/pipCommands.js";
|
import { hasDryRunArg } from "../utils/pipCommands.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* Scanner for pip command arguments to detect package installations
|
|
||||||
*
|
|
||||||
* @param {Object} options - Scanner options
|
|
||||||
* @param {boolean} [options.ignoreDryRun=false] - Whether to ignore dry-run flag
|
|
||||||
* @returns {Object} Scanner interface
|
|
||||||
*/
|
|
||||||
export function commandArgumentScanner(options = {}) {
|
export function commandArgumentScanner(options = {}) {
|
||||||
const { ignoreDryRun = false } = options;
|
const { ignoreDryRun = false } = options;
|
||||||
|
|
||||||
|
|
@ -33,15 +26,6 @@ function scanDependencies(args) {
|
||||||
return checkChangesFromArgs(args);
|
return checkChangesFromArgs(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts package changes from pip command arguments
|
|
||||||
*
|
|
||||||
* Unlike npm, pip's parser already returns exact versions (== or ===)
|
|
||||||
* or "latest" for unversioned packages, so no version resolution is needed.
|
|
||||||
*
|
|
||||||
* @param {string[]} args - Command line arguments
|
|
||||||
* @returns {Array<{name: string, version: string, type: string}>} Package changes
|
|
||||||
*/
|
|
||||||
export function checkChangesFromArgs(args) {
|
export function checkChangesFromArgs(args) {
|
||||||
const packageUpdates = parsePackagesFromInstallArgs(args);
|
const packageUpdates = parsePackagesFromInstallArgs(args);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,14 @@ import { test } from "node:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { commandArgumentScanner, checkChangesFromArgs } from "./commandArgumentScanner.js";
|
import { commandArgumentScanner, checkChangesFromArgs } from "./commandArgumentScanner.js";
|
||||||
|
|
||||||
test("commandArgumentScanner", async (t) => {
|
test("commandArgumentScanner factory", async (t) => {
|
||||||
await t.test("should create scanner with default options", () => {
|
await t.test("should create scanner with required interface", () => {
|
||||||
const scanner = commandArgumentScanner();
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
assert.ok(scanner);
|
assert.ok(scanner);
|
||||||
assert.strictEqual(typeof scanner.shouldScan, "function");
|
assert.strictEqual(typeof scanner.shouldScan, "function");
|
||||||
assert.strictEqual(typeof scanner.scan, "function");
|
assert.strictEqual(typeof scanner.scan, "function");
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should create scanner with ignoreDryRun option", () => {
|
|
||||||
const scanner = commandArgumentScanner({ ignoreDryRun: true });
|
|
||||||
|
|
||||||
assert.ok(scanner);
|
|
||||||
assert.strictEqual(typeof scanner.shouldScan, "function");
|
|
||||||
assert.strictEqual(typeof scanner.scan, "function");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shouldScan", async (t) => {
|
test("shouldScan", async (t) => {
|
||||||
|
|
@ -41,20 +33,6 @@ test("shouldScan", async (t) => {
|
||||||
const result = scanner.shouldScan(["install", "--dry-run", "requests"]);
|
const result = scanner.shouldScan(["install", "--dry-run", "requests"]);
|
||||||
assert.strictEqual(result, true);
|
assert.strictEqual(result, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should return true for download command", () => {
|
|
||||||
const scanner = commandArgumentScanner();
|
|
||||||
|
|
||||||
const result = scanner.shouldScan(["download", "flask"]);
|
|
||||||
assert.strictEqual(result, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should return true for wheel command", () => {
|
|
||||||
const scanner = commandArgumentScanner();
|
|
||||||
|
|
||||||
const result = scanner.shouldScan(["wheel", "django"]);
|
|
||||||
assert.strictEqual(result, true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("scan", async (t) => {
|
test("scan", async (t) => {
|
||||||
|
|
@ -129,46 +107,6 @@ test("scan", async (t) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should work with download command", () => {
|
|
||||||
const scanner = commandArgumentScanner();
|
|
||||||
|
|
||||||
const result = scanner.scan(["download", "django==4.2.0"]);
|
|
||||||
assert.strictEqual(result.length, 1);
|
|
||||||
assert.deepEqual(result[0], {
|
|
||||||
name: "django",
|
|
||||||
version: "4.2.0",
|
|
||||||
type: "add",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should work with wheel command", () => {
|
|
||||||
const scanner = commandArgumentScanner();
|
|
||||||
|
|
||||||
const result = scanner.scan(["wheel", "numpy==1.24.0"]);
|
|
||||||
assert.strictEqual(result.length, 1);
|
|
||||||
assert.deepEqual(result[0], {
|
|
||||||
name: "numpy",
|
|
||||||
version: "1.24.0",
|
|
||||||
type: "add",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should parse packages even for unsupported commands", () => {
|
|
||||||
const scanner = commandArgumentScanner();
|
|
||||||
|
|
||||||
// Note: The parser treats the first non-flag arg as the command and skips it
|
|
||||||
// So "uninstall" is treated as the command, and "requests" is parsed as a package
|
|
||||||
// The scanner itself doesn't filter by command type - that's done at a higher level
|
|
||||||
const result = scanner.scan(["uninstall", "requests"]);
|
|
||||||
assert.ok(Array.isArray(result));
|
|
||||||
assert.strictEqual(result.length, 1);
|
|
||||||
assert.deepEqual(result[0], {
|
|
||||||
name: "requests",
|
|
||||||
version: "latest",
|
|
||||||
type: "add",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should handle === exact version specifier", () => {
|
await t.test("should handle === exact version specifier", () => {
|
||||||
const scanner = commandArgumentScanner();
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
|
|
@ -182,8 +120,8 @@ test("scan", async (t) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("checkChangesFromArgs", async (t) => {
|
test("checkChangesFromArgs helper", async (t) => {
|
||||||
await t.test("should extract changes from install args", () => {
|
await t.test("should extract packages from args", () => {
|
||||||
const result = checkChangesFromArgs(["install", "requests==2.28.0", "flask"]);
|
const result = checkChangesFromArgs(["install", "requests==2.28.0", "flask"]);
|
||||||
|
|
||||||
assert.strictEqual(result.length, 2);
|
assert.strictEqual(result.length, 2);
|
||||||
|
|
@ -199,17 +137,8 @@ test("checkChangesFromArgs", async (t) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should return empty array for commands with no packages", () => {
|
|
||||||
const result = checkChangesFromArgs(["install", "-r", "requirements.txt"]);
|
|
||||||
|
|
||||||
assert.ok(Array.isArray(result));
|
|
||||||
assert.strictEqual(result.length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
await t.test("should handle empty args", () => {
|
await t.test("should handle empty args", () => {
|
||||||
const result = checkChangesFromArgs([]);
|
const result = checkChangesFromArgs([]);
|
||||||
|
assert.deepStrictEqual(result, []);
|
||||||
assert.ok(Array.isArray(result));
|
|
||||||
assert.strictEqual(result.length, 0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
/**
|
|
||||||
* Null scanner that returns no dependencies
|
|
||||||
* Used when a command is not supported for scanning
|
|
||||||
*/
|
|
||||||
export function nullScanner() {
|
|
||||||
return {
|
|
||||||
shouldScan: () => false,
|
|
||||||
scan: () => [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +1,18 @@
|
||||||
/**
|
/**
|
||||||
* Parses package specifications from pip install arguments
|
|
||||||
*
|
|
||||||
* Only returns packages with exact version specifiers (== or ===) to ensure
|
|
||||||
* we can check specific versions against the malware database.
|
|
||||||
*
|
|
||||||
* Supported formats that will be returned:
|
* Supported formats that will be returned:
|
||||||
* - package_name (no version)
|
* - package_name (no version)
|
||||||
* - package_name==version (exact version)
|
* - package_name==version (exact version)
|
||||||
* - package_name===version (exact version, PEP 440)
|
* - package_name===version (exact version, PEP 440)
|
||||||
*
|
*
|
||||||
* Skipped formats (won't be returned):
|
* "Ranges". Because they don't specify an exact version, the following formats are skipped and we will rely solely on the mitm scanner:
|
||||||
* - package_name>=version (range specifier)
|
* - package_name>=version
|
||||||
* - package_name<=version (range specifier)
|
* - package_name<=version
|
||||||
* - package_name>version (range specifier)
|
* - package_name>version
|
||||||
* - package_name<version (range specifier)
|
* - package_name<version
|
||||||
* - package_name~=version (compatible release)
|
* - package_name~=version
|
||||||
* - package_name!=version (exclusion)
|
* - package_name!=version
|
||||||
* - git+https://... (VCS URLs - returned without version)
|
* - git+https://... (VCS URLs - returned without version)
|
||||||
* - -r requirements.txt (handled by flag skipping)
|
* - -r requirements.txt (handled by flag skipping)
|
||||||
*
|
|
||||||
* @param {string[]} args - pip install command arguments
|
|
||||||
* @returns {Array<{name: string, version?: string, type: string}>} Array of package specifications with exact versions only
|
|
||||||
*/
|
*/
|
||||||
export function parsePackagesFromInstallArgs(args) {
|
export function parsePackagesFromInstallArgs(args) {
|
||||||
const packages = [];
|
const packages = [];
|
||||||
|
|
@ -34,14 +26,13 @@ export function parsePackagesFromInstallArgs(args) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip the command itself (install, uninstall, etc.)
|
// Skip the command itself (install, etc.)
|
||||||
if (i === 0 && !arg.startsWith("-")) {
|
if (i === 0 && !arg.startsWith("-")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip flags and their values
|
// Skip flags and their values
|
||||||
if (arg.startsWith("-")) {
|
if (arg.startsWith("-")) {
|
||||||
// Flags that take a value - skip the next arg for those
|
|
||||||
if (isPipOptionWithParameter(arg)) {
|
if (isPipOptionWithParameter(arg)) {
|
||||||
skipNext = true;
|
skipNext = true;
|
||||||
}
|
}
|
||||||
|
|
@ -57,8 +48,10 @@ export function parsePackagesFromInstallArgs(args) {
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a pip flag takes a parameter
|
|
||||||
function isPipOptionWithParameter(arg) {
|
function isPipOptionWithParameter(arg) {
|
||||||
|
|
||||||
|
// Check if a pip flag takes a parameter
|
||||||
|
// TODO it would be better to query pip itself for this info
|
||||||
const optionsWithParameters = [
|
const optionsWithParameters = [
|
||||||
// Install options
|
// Install options
|
||||||
"-r",
|
"-r",
|
||||||
|
|
@ -107,10 +100,7 @@ function isPipOptionWithParameter(arg) {
|
||||||
return optionsWithParameters.includes(arg);
|
return optionsWithParameters.includes(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse a single pip requirement spec
|
|
||||||
// Always returns { name, version } where version defaults to "latest" if not specified
|
|
||||||
function parsePipSpec(spec) {
|
function parsePipSpec(spec) {
|
||||||
|
|
||||||
// Ignore obvious URLs and paths
|
// Ignore obvious URLs and paths
|
||||||
// These cannot be scanned from the malware database
|
// These cannot be scanned from the malware database
|
||||||
const lower = spec.toLowerCase();
|
const lower = spec.toLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,7 @@ import { ui } from "../../environment/userInteraction.js";
|
||||||
import { safeSpawn } from "../../utils/safeSpawn.js";
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||||
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs a pip command with the specified arguments
|
|
||||||
*
|
|
||||||
* @param {string} command - The pip command to use (e.g., "pip", "pip3")
|
|
||||||
* @param {string[]} args - Command arguments
|
|
||||||
* @returns {Promise<{status: number}>} Result object with status code
|
|
||||||
*/
|
|
||||||
export async function runPip(command, args) {
|
export async function runPip(command, args) {
|
||||||
try {
|
try {
|
||||||
const result = await safeSpawn(command, args, {
|
const result = await safeSpawn(command, args, {
|
||||||
|
|
@ -26,18 +20,10 @@ export async function runPip(command, args) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs a pip command in dry-run mode and captures output
|
|
||||||
* Note: pip doesn't have a native --dry-run flag, so this may need adjustment
|
|
||||||
*
|
|
||||||
* @param {string} command - The pip command to use
|
|
||||||
* @param {string[]} args - Command arguments
|
|
||||||
* @returns {Promise<{status: number, output: string}>} Result with status and output
|
|
||||||
*/
|
|
||||||
export async function dryRunPipCommandAndOutput(command, args) {
|
export async function dryRunPipCommandAndOutput(command, args) {
|
||||||
try {
|
try {
|
||||||
// Note: pip doesn't have a --dry-run flag like npm
|
// Note: pip supports --dry-run for the "install" command only; "download" and "wheel" do not.
|
||||||
// This would need to be implemented differently if dry-run functionality is needed
|
// We don't mutate args here — callers should include --dry-run when appropriate.
|
||||||
const result = await safeSpawn(
|
const result = await safeSpawn(
|
||||||
command,
|
command,
|
||||||
args,
|
args,
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,7 @@
|
||||||
/**
|
|
||||||
* Pip command constants
|
|
||||||
*
|
|
||||||
* Note: Unlike npm, pip does not support command aliases or abbreviations.
|
|
||||||
* Commands must be spelled out fully (e.g., "install", not "i" or "add").
|
|
||||||
*/
|
|
||||||
export const pipInstallCommand = "install";
|
export const pipInstallCommand = "install";
|
||||||
export const pipDownloadCommand = "download";
|
export const pipDownloadCommand = "download";
|
||||||
export const pipWheelCommand = "wheel";
|
export const pipWheelCommand = "wheel";
|
||||||
export const pipUninstallCommand = "uninstall";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the pip command from the arguments array
|
|
||||||
*
|
|
||||||
* @param {string[]} args - Command line arguments
|
|
||||||
* @returns {string|null} The pip command or null if not found
|
|
||||||
*/
|
|
||||||
export function getPipCommandForArgs(args) {
|
export function getPipCommandForArgs(args) {
|
||||||
if (!args || args.length === 0) {
|
if (!args || args.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -30,12 +17,6 @@ export function getPipCommandForArgs(args) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the arguments contain the --dry-run flag
|
|
||||||
*
|
|
||||||
* @param {string[]} args - Command line arguments
|
|
||||||
* @returns {boolean} True if --dry-run is present
|
|
||||||
*/
|
|
||||||
export function hasDryRunArg(args) {
|
export function hasDryRunArg(args) {
|
||||||
return args.some((arg) => arg === "--dry-run");
|
return args.some((arg) => arg === "--dry-run");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
pipInstallCommand,
|
pipInstallCommand,
|
||||||
pipDownloadCommand,
|
pipDownloadCommand,
|
||||||
pipWheelCommand,
|
pipWheelCommand,
|
||||||
pipUninstallCommand,
|
|
||||||
} from "./pipCommands.js";
|
} from "./pipCommands.js";
|
||||||
|
|
||||||
test("getPipCommandForArgs", async (t) => {
|
test("getPipCommandForArgs", async (t) => {
|
||||||
|
|
@ -81,8 +80,4 @@ test("command constants", async (t) => {
|
||||||
await t.test("should have correct wheel command", () => {
|
await t.test("should have correct wheel command", () => {
|
||||||
assert.strictEqual(pipWheelCommand, "wheel");
|
assert.strictEqual(pipWheelCommand, "wheel");
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.test("should have correct uninstall command", () => {
|
|
||||||
assert.strictEqual(pipUninstallCommand, "uninstall");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ export async function auditChanges(changes) {
|
||||||
const allowedChanges = [];
|
const allowedChanges = [];
|
||||||
const disallowedChanges = [];
|
const disallowedChanges = [];
|
||||||
|
|
||||||
console.log("**audit/index.js** Auditing changes:", changes);
|
|
||||||
var malwarePackages = await getPackagesWithMalware(
|
var malwarePackages = await getPackagesWithMalware(
|
||||||
changes.filter(
|
changes.filter(
|
||||||
(change) => change.type === "add" || change.type === "change"
|
(change) => change.type === "add" || change.type === "change"
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ ARG NODE_VERSION=latest
|
||||||
ARG NPM_VERSION=latest
|
ARG NPM_VERSION=latest
|
||||||
ARG YARN_VERSION=latest
|
ARG YARN_VERSION=latest
|
||||||
ARG PNPM_VERSION=latest
|
ARG PNPM_VERSION=latest
|
||||||
|
ARG PYTHON_VERSION=3
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-c"]
|
SHELL ["/bin/bash", "-c"]
|
||||||
ENV BASH_ENV=~/.bashrc
|
ENV BASH_ENV=~/.bashrc
|
||||||
|
|
@ -49,6 +50,11 @@ RUN volta install pnpm@${PNPM_VERSION}
|
||||||
# Install Bun
|
# Install Bun
|
||||||
RUN curl -fsSL https://bun.sh/install | bash
|
RUN curl -fsSL https://bun.sh/install | bash
|
||||||
|
|
||||||
|
# Install Python and pip (pip3)
|
||||||
|
RUN apt-get update && apt-get install -y python${PYTHON_VERSION} python3-pip && \
|
||||||
|
ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python3 && \
|
||||||
|
ln -sf /usr/bin/pip3 /usr/local/bin/pip3
|
||||||
|
|
||||||
# Copy and install Safe chain
|
# Copy and install Safe chain
|
||||||
COPY --from=builder /app/*.tgz /pkgs/
|
COPY --from=builder /app/*.tgz /pkgs/
|
||||||
RUN npm install -g /pkgs/*.tgz
|
RUN npm install -g /pkgs/*.tgz
|
||||||
|
|
|
||||||
71
test/e2e/pip.e2e.spec.js
Normal file
71
test/e2e/pip.e2e.spec.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||||
|
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
// Note: These tests require Docker. If Docker isn't available locally,
|
||||||
|
// they will be skipped by the runner or fail to build the image.
|
||||||
|
describe("E2E: pip coverage", () => {
|
||||||
|
let container;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
DockerTestContainer.buildImage();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Run a new Docker container for each test
|
||||||
|
container = new DockerTestContainer();
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
const installationShell = await container.openShell("zsh");
|
||||||
|
await installationShell.runCommand("safe-chain setup");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Stop and clean up the container after each test
|
||||||
|
if (container) {
|
||||||
|
await container.stop();
|
||||||
|
container = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`safe-chain successfully installs safe packages with pip3`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand("pip3 install requests");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malicious packages found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`pip3 download works with safe-chain proxy`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand("pip3 download requests");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malicious packages found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`pip3 wheel works with safe-chain proxy`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand("pip3 wheel requests");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malicious packages found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`pip3 install --dry-run is respected by scanner`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand("pip3 install --dry-run requests");
|
||||||
|
|
||||||
|
// Scanner intentionally skips when --dry-run is present for install
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malicious packages found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue