Remove ora dependency

This commit is contained in:
Sander Declerck 2025-11-25 14:22:31 +01:00
parent d158e15c08
commit c8df7566b5
No known key found for this signature in database
5 changed files with 17 additions and 426 deletions

View file

@ -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,
};

View file

@ -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}`);

View file

@ -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" },
]);