mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Fix ranges issue
This commit is contained in:
parent
15785fad73
commit
6b2db6dace
6 changed files with 128 additions and 4 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
import { ui } from "../../environment/userInteraction.js";
|
import { ui } from "../../environment/userInteraction.js";
|
||||||
import { safeSpawn } from "../../utils/safeSpawn.js";
|
import { safeSpawnPy } from "../../utils/safeSpawn.js";
|
||||||
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||||
|
|
||||||
|
|
||||||
export async function runPip(command, args) {
|
export async function runPip(command, args) {
|
||||||
try {
|
try {
|
||||||
const result = await safeSpawn(command, args, {
|
const result = await safeSpawnPy(command, args, {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
||||||
});
|
});
|
||||||
|
|
@ -24,7 +24,7 @@ export async function dryRunPipCommandAndOutput(command, args) {
|
||||||
try {
|
try {
|
||||||
// Note: pip supports --dry-run for the "install" command only; "download" and "wheel" do not.
|
// Note: pip supports --dry-run for the "install" command only; "download" and "wheel" do not.
|
||||||
// We don't mutate args here — callers should include --dry-run when appropriate.
|
// We don't mutate args here — callers should include --dry-run when appropriate.
|
||||||
const result = await safeSpawn(
|
const result = await safeSpawnPy(
|
||||||
command,
|
command,
|
||||||
args,
|
args,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export async function auditChanges(changes) {
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
|
console.log("**** Auditing change:", change);
|
||||||
const malwarePackage = malwarePackages.find(
|
const malwarePackage = malwarePackages.find(
|
||||||
(pkg) => pkg.name === change.name && pkg.version === change.version
|
(pkg) => pkg.name === change.name && pkg.version === change.version
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,8 @@ async function getMalwareDatabase() {
|
||||||
|
|
||||||
function isMalwareStatus(status) {
|
function isMalwareStatus(status) {
|
||||||
let malwareStatus = status.toUpperCase();
|
let malwareStatus = status.toUpperCase();
|
||||||
return malwareStatus === MALWARE_STATUS_MALWARE;
|
return malwareStatus === MALWARE_STATUS_MALWARE
|
||||||
|
|| malwareStatus === MALWARE_STATUS_TELEMETRY;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MALWARE_STATUS_OK = "OK";
|
export const MALWARE_STATUS_OK = "OK";
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,35 @@ export async function safeSpawn(command, args, options = {}) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To avoid any regression issues on the JS ecosystem,
|
||||||
|
* a py-friendly safeSpawn that avoids shell interpolation
|
||||||
|
* issues (e.g., '<', '>' in version specs).
|
||||||
|
*
|
||||||
|
* TL;DR: add support for shell::false
|
||||||
|
*/
|
||||||
|
export async function safeSpawnPy(command, args, options = {}) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const child = spawn(command, args, { ...options, shell: false });
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
child.stdout?.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
resolve({ status: code, stdout, stderr });
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", (error) => {
|
||||||
|
resolve({ status: 1, stdout: "", stderr: error.message || String(error) });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,3 +87,73 @@ describe("safeSpawn", () => {
|
||||||
assert.strictEqual(spawnCalls[0].options.shell, true);
|
assert.strictEqual(spawnCalls[0].options.shell, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("safeSpawnPy", () => {
|
||||||
|
let safeSpawnPy;
|
||||||
|
let spawnCalls = [];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
spawnCalls = [];
|
||||||
|
|
||||||
|
// Mock child_process for argument-array spawn signature
|
||||||
|
mock.module("child_process", {
|
||||||
|
namedExports: {
|
||||||
|
spawn: (command, args = [], options = {}) => {
|
||||||
|
spawnCalls.push({ command, args, options });
|
||||||
|
const stdoutListeners = [];
|
||||||
|
const stderrListeners = [];
|
||||||
|
const stdout = { on: (event, cb) => { if (event === "data") stdoutListeners.push(cb); } };
|
||||||
|
const stderr = { on: (event, cb) => { if (event === "data") stderrListeners.push(cb); } };
|
||||||
|
const obj = {
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
on: (event, callback) => {
|
||||||
|
if (event === 'close') {
|
||||||
|
// Emit one chunk to stdout and stderr to verify piping works, then close with success
|
||||||
|
setTimeout(() => {
|
||||||
|
stdoutListeners.forEach((cb) => cb(Buffer.from("STDOUT-TEST")));
|
||||||
|
stderrListeners.forEach((cb) => cb(Buffer.from("")));
|
||||||
|
callback(0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import after mocking; use a query to avoid ESM cache collisions with previous import
|
||||||
|
const safeSpawnModule = await import("./safeSpawn.js?py");
|
||||||
|
safeSpawnPy = safeSpawnModule.safeSpawnPy;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("spawns without a shell and preserves args (inherit)", async () => {
|
||||||
|
const result = await safeSpawnPy("pip3", ["install", "Jinja2>=3.1,<3.2"], { stdio: "inherit" });
|
||||||
|
|
||||||
|
// Verifies no throw and status 0
|
||||||
|
assert.strictEqual(result.status, 0);
|
||||||
|
|
||||||
|
// Verify spawn signature
|
||||||
|
assert.strictEqual(spawnCalls.length, 1);
|
||||||
|
assert.strictEqual(spawnCalls[0].command, "pip3");
|
||||||
|
assert.deepStrictEqual(spawnCalls[0].args, ["install", "Jinja2>=3.1,<3.2"]);
|
||||||
|
assert.strictEqual(spawnCalls[0].options.shell, false);
|
||||||
|
assert.strictEqual(spawnCalls[0].options.stdio, "inherit");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures stdout when stdio=pipe", async () => {
|
||||||
|
const result = await safeSpawnPy("pip3", ["install", "idna!=3.5,>=3.0", "--dry-run"], { stdio: "pipe" });
|
||||||
|
|
||||||
|
assert.strictEqual(result.status, 0);
|
||||||
|
assert.match(result.stdout || "", /STDOUT-TEST/);
|
||||||
|
|
||||||
|
assert.strictEqual(spawnCalls.length, 1);
|
||||||
|
assert.strictEqual(spawnCalls[0].options.shell, false);
|
||||||
|
assert.strictEqual(spawnCalls[0].options.stdio, "pipe");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -65,4 +65,24 @@ 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}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`pip3 install with extras such as requests[socks]`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand('pip3 install "requests[socks]==2.32.3"');
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malicious packages found."),
|
||||||
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`pip3 install with range version specifier`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand('pip3 install "Jinja2>=3.1,<3.2"');
|
||||||
|
|
||||||
|
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