import { describe, it, mock } from "node:test"; import assert from "node:assert"; describe("npmInterceptor minimum package age", async () => { let minimumPackageAgeSettings = 48; let skipMinimumPackageAgeSetting = false; mock.module("../../../config/settings.js", { namedExports: { getMinimumPackageAgeHours: () => minimumPackageAgeSettings, skipMinimumPackageAge: () => skipMinimumPackageAgeSetting, }, }); mock.module("../../../scanning/audit/index.js", { namedExports: { isMalwarePackage: async () => { return false; }, }, }); mock.module("../../../environment/userInteraction.js", { namedExports: { ui: { startProcess: () => {}, writeError: () => {}, writeInformation: () => {}, writeWarning: () => {}, writeVerbose: () => {}, writeExitWithoutInstallingMaliciousPackages: () => {}, emptyLine: () => {}, }, }, }); const { npmInterceptorForUrl } = await import("./npmInterceptor.js"); for (const packageInfoUrl of [ // Basic package metadata "https://registry.npmjs.org/lodash", "https://registry.npmjs.org/express", // Scoped packages "https://registry.npmjs.org/@vercel/functions", "https://registry.npmjs.org/@babel/core", "https://registry.npmjs.org/@types/node", // With query parameters "https://registry.npmjs.org/lodash?write=true", "https://registry.npmjs.org/@babel/core?param=value&other=test", // With fragments "https://registry.npmjs.org/lodash#readme", "https://registry.npmjs.org/@babel/core#installation", // Version-specific metadata "https://registry.npmjs.org/lodash/4.17.21", "https://registry.npmjs.org/lodash/latest", "https://registry.npmjs.org/@babel/core/7.21.4", // URL-encoded scoped packages "https://registry.npmjs.org/@types%2Fnode", "https://registry.npmjs.org/@babel%2Fcore", // With trailing slashes "https://registry.npmjs.org/lodash/", "https://registry.npmjs.org/@babel/core/", ]) { it(`modifyResponse should be true for package info requests: ${packageInfoUrl}`, async () => { const interceptor = npmInterceptorForUrl(packageInfoUrl); const requestInterceptor = await interceptor.handleRequest( packageInfoUrl ); assert.equal(requestInterceptor.modifiesResponse(), true); }); } for (const packageUrl of [ // Regular package tarballs "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "https://registry.npmjs.org/express/-/express-4.18.2.tgz", // Scoped package tarballs "https://registry.npmjs.org/@babel/core/-/core-8.0.0-alpha.1.tgz", "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", // Tarballs with query parameters (integrity checks) "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz?integrity=sha512-abc123", "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz?integrity=sha512-def456&cache=false", // Tarballs with fragments "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#sha512-abc123", "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz#hash", // Prerelease versions "https://registry.npmjs.org/react/-/react-18.3.0-canary-abc123.tgz", "https://registry.npmjs.org/lodash/-/lodash-5.0.0-beta.1.tgz", ]) { it(`modifyResponse should be false for package downloads: ${packageUrl}`, async () => { const interceptor = npmInterceptorForUrl(packageUrl); const requestInterceptor = await interceptor.handleRequest(packageUrl); assert.equal(requestInterceptor.modifiesResponse(), false); }); } for (const specialEndpoint of [ // Security advisory endpoints "https://registry.npmjs.org/-/npm/v1/security/advisories/bulk", "https://registry.npmjs.org/-/npm/v1/security/audits", "https://registry.npmjs.org/-/npm/v1/security/audits/quick", // Search endpoints "https://registry.npmjs.org/-/v1/search?text=lodash&size=20", "https://registry.npmjs.org/-/v1/search?text=react&from=0", // Package access/collaboration endpoints "https://registry.npmjs.org/-/package/lodash/access", "https://registry.npmjs.org/-/package/@babel/core/collaborators", "https://registry.npmjs.org/-/package/lodash/dist-tags", "https://registry.npmjs.org/-/package/@babel/core/dist-tags/latest", // User/organization endpoints "https://registry.npmjs.org/-/user/org.couchdb.user:username", "https://registry.npmjs.org/-/org/myorg/package", // Anonymous metrics "https://registry.npmjs.org/-/npm/anon-metrics/v1/", // Ping/health check "https://registry.npmjs.org/-/ping", ]) { it(`modifyResponse should be false for special endpoints: ${specialEndpoint}`, async () => { const interceptor = npmInterceptorForUrl(specialEndpoint); const requestInterceptor = await interceptor.handleRequest( specialEndpoint ); assert.equal(requestInterceptor.modifiesResponse(), false); }); } it("Should remove packages older than the treshold", async () => { minimumPackageAgeSettings = 5; const packageUrl = "https://registry.npmjs.org/lodash"; const modifiedBody = await runModifyNpmInfoRequest( packageUrl, JSON.stringify({ name: "lodash", ["dist-tags"]: { latest: "3.0.0", }, versions: { ["1.0.0"]: {}, ["2.0.0"]: {}, ["3.0.0"]: {}, }, time: { created: getDate(-365 * 24), modified: getDate(-3), ["1.0.0"]: getDate(-7), // cutoff-date here ["2.0.0"]: getDate(-4), ["3.0.0"]: getDate(-3), }, }) ); const modifiedJson = JSON.parse(modifiedBody); assert.equal(Object.keys(modifiedJson.time).length, 3); assert.equal(Object.keys(modifiedJson.versions).length, 1); assert.ok(Object.keys(modifiedJson.time).includes("1.0.0")); assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); assert.ok(!Object.keys(modifiedJson.time).includes("2.0.0")); assert.ok(!Object.keys(modifiedJson.versions).includes("2.0.0")); assert.ok(!Object.keys(modifiedJson.time).includes("3.0.0")); assert.ok(!Object.keys(modifiedJson.versions).includes("3.0.0")); }); it("Should set the package to the new latest non-preview release", async () => { minimumPackageAgeSettings = 5; const packageUrl = "https://registry.npmjs.org/lodash"; const modifiedBody = await runModifyNpmInfoRequest( packageUrl, JSON.stringify({ name: "lodash", ["dist-tags"]: { latest: "3.0.0", }, versions: { ["1.0.0"]: {}, ["2.0.0"]: {}, ["3.0.0"]: {}, }, time: { created: getDate(-365 * 24), modified: getDate(-3), ["1.0.0"]: getDate(-7), ["0.0.1"]: getDate(-8), // package order: this package is older than 1.0.0, it should not be considered latest ["2.0.0-alpha"]: getDate(-6), //package is a pre-release, it should not be latest // cutoff-date here ["2.0.0"]: getDate(-4), ["3.0.0"]: getDate(-3), }, }) ); const modifiedJson = JSON.parse(modifiedBody); assert.equal(modifiedJson["dist-tags"]["latest"], "1.0.0"); }); it("Should remove dist-tags if version was removed", async () => { minimumPackageAgeSettings = 5; const packageUrl = "https://registry.npmjs.org/lodash"; const modifiedBody = await runModifyNpmInfoRequest( packageUrl, JSON.stringify({ name: "lodash", ["dist-tags"]: { latest: "3.0.0", alpha: "2.0.0-alpha", }, versions: { ["1.0.0"]: {}, ["2.0.0"]: {}, ["3.0.0"]: {}, }, time: { created: getDate(-365 * 24), modified: getDate(-4), ["1.0.0"]: getDate(-7), // cutoff-date here ["2.0.0-alpha"]: getDate(-4), }, }) ); const modifiedJson = JSON.parse(modifiedBody); console.log(modifiedJson); assert.equal(modifiedJson["dist-tags"]["alpha"], undefined); }); it("Should not filter packages when skipMinimumPackageAge is enabled", async () => { minimumPackageAgeSettings = 5; skipMinimumPackageAgeSetting = true; const packageUrl = "https://registry.npmjs.org/lodash"; const originalBody = JSON.stringify({ name: "lodash", ["dist-tags"]: { latest: "3.0.0", }, versions: { ["1.0.0"]: {}, ["2.0.0"]: {}, ["3.0.0"]: {}, }, time: { created: getDate(-365 * 24), modified: getDate(-3), ["1.0.0"]: getDate(-7), // cutoff-date here ["2.0.0"]: getDate(-4), ["3.0.0"]: getDate(-3), }, }); const modifiedBody = await runModifyNpmInfoRequest( packageUrl, originalBody ); const modifiedJson = JSON.parse(modifiedBody); // All versions should remain unchanged assert.equal(Object.keys(modifiedJson.versions).length, 3); assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); assert.ok(Object.keys(modifiedJson.versions).includes("2.0.0")); assert.ok(Object.keys(modifiedJson.versions).includes("3.0.0")); // Latest should remain unchanged assert.equal(modifiedJson["dist-tags"]["latest"], "3.0.0"); }); it("Should use custom minimum package age of 48 hours", async () => { minimumPackageAgeSettings = 48; skipMinimumPackageAgeSetting = false; const packageUrl = "https://registry.npmjs.org/lodash"; const modifiedBody = await runModifyNpmInfoRequest( packageUrl, JSON.stringify({ name: "lodash", ["dist-tags"]: { latest: "4.0.0", }, versions: { ["1.0.0"]: {}, ["2.0.0"]: {}, ["3.0.0"]: {}, ["4.0.0"]: {}, }, time: { created: getDate(-365 * 24), modified: getDate(-24), ["1.0.0"]: getDate(-72), // 3 days old - should remain ["2.0.0"]: getDate(-50), // ~2 days old - should remain // 48-hour cutoff here ["3.0.0"]: getDate(-40), // ~1.7 days old - should be removed ["4.0.0"]: getDate(-24), // 1 day old - should be removed }, }) ); const modifiedJson = JSON.parse(modifiedBody); // Versions older than 48 hours should remain assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); assert.ok(Object.keys(modifiedJson.versions).includes("2.0.0")); // Versions newer than 48 hours should be removed assert.ok(!Object.keys(modifiedJson.versions).includes("3.0.0")); assert.ok(!Object.keys(modifiedJson.versions).includes("4.0.0")); // Latest should be recalculated to 2.0.0 assert.equal(modifiedJson["dist-tags"]["latest"], "2.0.0"); assert.equal(Object.keys(modifiedJson.versions).length, 2); }); it("Should use very small minimum package age of 1 hour", async () => { minimumPackageAgeSettings = 1; skipMinimumPackageAgeSetting = false; const packageUrl = "https://registry.npmjs.org/lodash"; const modifiedBody = await runModifyNpmInfoRequest( packageUrl, JSON.stringify({ name: "lodash", ["dist-tags"]: { latest: "3.0.0", }, versions: { ["1.0.0"]: {}, ["2.0.0"]: {}, ["3.0.0"]: {}, }, time: { created: getDate(-48), modified: getDate(0), ["1.0.0"]: getDate(-3), // 3 hours old - should remain ["2.0.0"]: getDate(-2), // 2 hours old - should remain // 1-hour cutoff here ["3.0.0"]: getDate(0), // just published - should be removed }, }) ); const modifiedJson = JSON.parse(modifiedBody); assert.equal(Object.keys(modifiedJson.versions).length, 2); assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); assert.ok(Object.keys(modifiedJson.versions).includes("2.0.0")); assert.ok(!Object.keys(modifiedJson.versions).includes("3.0.0")); assert.equal(modifiedJson["dist-tags"]["latest"], "2.0.0"); }); function getDate(plusHours) { const date = new Date(); date.setHours(date.getHours() + plusHours); return date; } /** * @param {import("../interceptorBuilder.js").Interceptor} interceptor * @param {string} body * @returns {Promise} */ async function runModifyNpmInfoRequest(url, body) { const interceptor = npmInterceptorForUrl(url); const requestHandler = await interceptor.handleRequest(url); if (requestHandler.modifiesResponse()) { const modifiedBuffer = requestHandler.modifyBody(Buffer.from(body), { ["content-type"]: "application/json", }); return modifiedBuffer.toString("utf8"); } return body; } });