mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Remove ora dependency
This commit is contained in:
parent
d158e15c08
commit
c8df7566b5
5 changed files with 17 additions and 426 deletions
|
|
@ -35,23 +35,22 @@
|
|||
"license": "AGPL-3.0-or-later",
|
||||
"description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), and [bunx](https://bun.sh/docs/cli/bunx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, or bunx from downloading or running the malware.",
|
||||
"dependencies": {
|
||||
"certifi": "^14.5.15",
|
||||
"certifi": "14.5.15",
|
||||
"chalk": "5.4.1",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"ini": "^6.0.0",
|
||||
"ini": "6.0.0",
|
||||
"make-fetch-happen": "14.0.3",
|
||||
"node-forge": "1.3.1",
|
||||
"npm-registry-fetch": "18.0.2",
|
||||
"ora": "8.2.0",
|
||||
"semver": "7.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ini": "^4.1.1",
|
||||
"@types/make-fetch-happen": "^10.0.4",
|
||||
"@types/node": "^18.19.130",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/npm-registry-fetch": "^8.0.9",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"main": "src/main.js",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// oxlint-disable no-console
|
||||
import chalk from "chalk";
|
||||
import ora from "ora";
|
||||
import { isCi } from "./environment.js";
|
||||
import {
|
||||
getLoggingLevel,
|
||||
|
|
@ -98,61 +97,6 @@ function writeOrBuffer(messageFunction) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} Spinner
|
||||
* @property {(message: string) => void} succeed
|
||||
* @property {(message: string) => void} fail
|
||||
* @property {() => void} stop
|
||||
* @property {(message: string) => void} setText
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*
|
||||
* @returns {Spinner}
|
||||
*/
|
||||
function startProcess(message) {
|
||||
if (isSilentMode()) {
|
||||
return {
|
||||
succeed: () => {},
|
||||
fail: () => {},
|
||||
stop: () => {},
|
||||
setText: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
if (isCi()) {
|
||||
return {
|
||||
succeed: (message) => {
|
||||
writeInformation(message);
|
||||
},
|
||||
fail: (message) => {
|
||||
writeError(message);
|
||||
},
|
||||
stop: () => {},
|
||||
setText: (message) => {
|
||||
writeInformation(message);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const spinner = ora(message).start();
|
||||
return {
|
||||
succeed: (message) => {
|
||||
spinner.succeed(message);
|
||||
},
|
||||
fail: (message) => {
|
||||
spinner.fail(message);
|
||||
},
|
||||
stop: () => {
|
||||
spinner.stop();
|
||||
},
|
||||
setText: (message) => {
|
||||
spinner.text = message;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function startBufferingLogs() {
|
||||
state.bufferOutput = true;
|
||||
state.bufferedMessages = [];
|
||||
|
|
@ -173,7 +117,6 @@ export const ui = {
|
|||
writeError,
|
||||
writeExitWithoutInstallingMaliciousPackages,
|
||||
emptyLine,
|
||||
startProcess,
|
||||
startBufferingLogs,
|
||||
writeBufferedLogsAndStopBuffering,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,36 +29,19 @@ export async function scanCommand(args) {
|
|||
}
|
||||
|
||||
let timedOut = false;
|
||||
|
||||
const spinner = ui.startProcess(
|
||||
"Safe-chain: Scanning for malicious packages..."
|
||||
);
|
||||
/** @type {import("./audit/index.js").AuditResult | undefined} */
|
||||
let audit;
|
||||
|
||||
await Promise.race([
|
||||
(async () => {
|
||||
try {
|
||||
const packageManager = getPackageManager();
|
||||
const changes = await packageManager.getDependencyUpdatesForCommand(
|
||||
args
|
||||
);
|
||||
const packageManager = getPackageManager();
|
||||
const changes = await packageManager.getDependencyUpdatesForCommand(args);
|
||||
|
||||
if (timedOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
spinner.setText(
|
||||
`Safe-chain: Scanning ${changes.length} package(s)...`
|
||||
);
|
||||
}
|
||||
|
||||
audit = await auditChanges(changes);
|
||||
} catch (/** @type any */ error) {
|
||||
spinner.fail(`Safe-chain: Error while scanning.`);
|
||||
throw error;
|
||||
if (timedOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
audit = await auditChanges(changes);
|
||||
})(),
|
||||
setTimeout(getScanTimeout()).then(() => {
|
||||
timedOut = true;
|
||||
|
|
@ -66,15 +49,13 @@ export async function scanCommand(args) {
|
|||
]);
|
||||
|
||||
if (timedOut) {
|
||||
spinner.fail("Safe-chain: Timeout exceeded while scanning.");
|
||||
throw new Error("Timeout exceeded while scanning npm install command.");
|
||||
}
|
||||
|
||||
if (!audit || audit.isAllowed) {
|
||||
spinner.stop();
|
||||
return 0;
|
||||
} else {
|
||||
printMaliciousChanges(audit.disallowedChanges, spinner);
|
||||
printMaliciousChanges(audit.disallowedChanges);
|
||||
onMalwareFound();
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -82,12 +63,12 @@ export async function scanCommand(args) {
|
|||
|
||||
/**
|
||||
* @param {import("./audit/index.js").PackageChange[]} changes
|
||||
* @param spinner {import("../environment/userInteraction.js").Spinner}
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
function printMaliciousChanges(changes, spinner) {
|
||||
spinner.fail("Safe-chain: " + chalk.bold("Malicious changes detected:"));
|
||||
function printMaliciousChanges(changes) {
|
||||
ui.writeInformation(
|
||||
chalk.red("✖") + " Safe-chain: " + chalk.bold("Malicious changes detected:")
|
||||
);
|
||||
|
||||
for (const change of changes) {
|
||||
ui.writeInformation(` - ${change.name}@${change.version}`);
|
||||
|
|
|
|||
|
|
@ -5,12 +5,6 @@ import { setTimeout } from "node:timers/promises";
|
|||
describe("scanCommand", async () => {
|
||||
const getScanTimeoutMock = mock.fn(() => 1000);
|
||||
const mockGetDependencyUpdatesForCommand = mock.fn();
|
||||
const mockStartProcess = mock.fn(() => ({
|
||||
setText: () => {},
|
||||
succeed: () => {},
|
||||
fail: () => {},
|
||||
stop: () => {},
|
||||
}));
|
||||
|
||||
// import { getPackageManager } from "../packagemanager/currentPackageManager.js";
|
||||
mock.module("../packagemanager/currentPackageManager.js", {
|
||||
|
|
@ -36,7 +30,6 @@ describe("scanCommand", async () => {
|
|||
mock.module("../environment/userInteraction.js", {
|
||||
namedExports: {
|
||||
ui: {
|
||||
startProcess: mockStartProcess,
|
||||
writeError: () => {},
|
||||
writeInformation: () => {},
|
||||
writeWarning: () => {},
|
||||
|
|
@ -75,51 +68,20 @@ describe("scanCommand", async () => {
|
|||
const { scanCommand } = await import("./index.js");
|
||||
|
||||
it("should succeed when there are no changes", async () => {
|
||||
let progressWasStopped = false;
|
||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
||||
setText: () => {},
|
||||
succeed: () => {},
|
||||
fail: () => {},
|
||||
stop: () => {
|
||||
progressWasStopped = true;
|
||||
},
|
||||
}));
|
||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => []);
|
||||
|
||||
await scanCommand(["install", "lodash"]);
|
||||
|
||||
assert.equal(progressWasStopped, true);
|
||||
});
|
||||
|
||||
it("should succeed when changes are not malicious", async () => {
|
||||
let progressWasStopped = false;
|
||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
||||
setText: () => {},
|
||||
succeed: () => {},
|
||||
fail: () => {},
|
||||
stop: () => {
|
||||
progressWasStopped = true;
|
||||
},
|
||||
}));
|
||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
||||
{ name: "lodash", version: "4.17.21" },
|
||||
]);
|
||||
|
||||
await scanCommand(["install", "lodash"]);
|
||||
|
||||
assert.equal(progressWasStopped, true);
|
||||
});
|
||||
|
||||
it("should throw an error when timing out", async () => {
|
||||
let failureMessageWasSet = false;
|
||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
||||
setText: () => {},
|
||||
succeed: () => {},
|
||||
fail: () => {
|
||||
failureMessageWasSet = true;
|
||||
},
|
||||
stop: () => {},
|
||||
}));
|
||||
getScanTimeoutMock.mock.mockImplementationOnce(() => 100);
|
||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => {
|
||||
await setTimeout(150);
|
||||
|
|
@ -127,83 +89,9 @@ describe("scanCommand", async () => {
|
|||
});
|
||||
|
||||
await assert.rejects(scanCommand(["install", "lodash"]));
|
||||
|
||||
assert.equal(failureMessageWasSet, true);
|
||||
});
|
||||
|
||||
it("should fail and return 1 malicious changes are detected", async () => {
|
||||
let failureMessageWasSet = false;
|
||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
||||
setText: () => {},
|
||||
succeed: () => {},
|
||||
fail: () => {
|
||||
failureMessageWasSet = true;
|
||||
},
|
||||
stop: () => {},
|
||||
}));
|
||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
||||
{ name: "malicious", version: "1.0.0" },
|
||||
]);
|
||||
|
||||
const result = await scanCommand(["install", "malicious"]);
|
||||
|
||||
assert.equal(failureMessageWasSet, true);
|
||||
assert.equal(result, 1);
|
||||
});
|
||||
|
||||
it("should not report a timeout when the user takes a long time to respond (it should not affect the timeout)", async () => {
|
||||
let failureMessages = [];
|
||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
||||
setText: () => {},
|
||||
succeed: () => {},
|
||||
fail: (message) => {
|
||||
failureMessages.push(message);
|
||||
},
|
||||
stop: () => {},
|
||||
}));
|
||||
getScanTimeoutMock.mock.mockImplementationOnce(() => 100);
|
||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => {
|
||||
return [{ name: "malicious", version: "4.17.21" }];
|
||||
});
|
||||
|
||||
await scanCommand(["install", "malicious"]);
|
||||
|
||||
assert.equal(failureMessages.length, 1);
|
||||
const failureMessage = failureMessages[0];
|
||||
assert.equal(failureMessage.toLowerCase().includes("timeout"), false);
|
||||
assert.equal(failureMessage.toLowerCase().includes("malicious"), true);
|
||||
});
|
||||
|
||||
it("should exit immediately when malicious changes are detected in block mode", async () => {
|
||||
let failureMessageWasSet = false;
|
||||
|
||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
||||
setText: () => {},
|
||||
succeed: () => {},
|
||||
fail: () => {
|
||||
failureMessageWasSet = true;
|
||||
},
|
||||
stop: () => {},
|
||||
}));
|
||||
|
||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
||||
{ name: "malicious", version: "1.0.0" },
|
||||
]);
|
||||
|
||||
const result = await scanCommand(["install", "malicious"]);
|
||||
|
||||
assert.equal(failureMessageWasSet, true);
|
||||
assert.equal(result, 1);
|
||||
});
|
||||
|
||||
it("should exit immediately when malicious changes are detected in block mode without prompting", async () => {
|
||||
mockStartProcess.mock.mockImplementationOnce(() => ({
|
||||
setText: () => {},
|
||||
succeed: () => {},
|
||||
fail: () => {},
|
||||
stop: () => {},
|
||||
}));
|
||||
|
||||
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
||||
{ name: "malicious", version: "1.0.0" },
|
||||
]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue