AikidoSec-safe-chain/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js
2026-03-19 15:58:42 -07:00

667 lines
23 KiB
JavaScript

import { describe, it, mock } from "node:test";
import assert from "node:assert";
describe("npmInterceptor minimum package age", async () => {
let minimumPackageAgeSettings = 48;
let skipMinimumPackageAgeSetting = false;
let minimumPackageAgeExclusionsSetting = [];
let newlyReleasedPackages = new Set();
mock.module("../../../config/settings.js", {
namedExports: {
ECOSYSTEM_JS: "js",
ECOSYSTEM_PY: "py",
getMinimumPackageAgeHours: () => minimumPackageAgeSettings,
skipMinimumPackageAge: () => skipMinimumPackageAgeSetting,
getNpmCustomRegistries: () => [],
getNpmMinimumPackageAgeExclusions: () => minimumPackageAgeExclusionsSetting,
getEcoSystem: () => "js",
},
});
mock.module("../../../scanning/newPackagesDatabase.js", {
namedExports: {
openNewPackagesDatabase: async () => ({
isNewlyReleasedPackage: (name, version) =>
newlyReleasedPackages.has(`${name}@${version}`),
}),
},
});
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");
});
it("Should suppress too-young versions on metadata requests without directly blocking the request", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = false;
const packageUrl = "https://registry.npmjs.org/lodash";
const interceptor = npmInterceptorForUrl(packageUrl);
const requestHandler = await interceptor.handleRequest(packageUrl);
assert.equal(requestHandler.blockResponse, undefined);
assert.equal(requestHandler.modifiesResponse(), true);
});
it("Should directly block tarball requests when the new packages list marks them as too young", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = false;
newlyReleasedPackages = new Set(["lodash@4.17.21"]);
const packageUrl =
"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz?integrity=sha512-abc123";
const interceptor = npmInterceptorForUrl(packageUrl);
const requestHandler = await interceptor.handleRequest(packageUrl);
assert.ok(requestHandler.blockResponse);
assert.equal(requestHandler.modifiesResponse(), false);
assert.equal(requestHandler.blockResponse.statusCode, 403);
assert.equal(
requestHandler.blockResponse.message,
"Forbidden - blocked by safe-chain minimum package age (lodash@4.17.21)"
);
});
it("Should not block tarball requests when skipMinimumPackageAge is enabled", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = true;
minimumPackageAgeExclusionsSetting = [];
newlyReleasedPackages = new Set(["lodash@4.17.21"]);
const packageUrl =
"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz";
const interceptor = npmInterceptorForUrl(packageUrl);
const requestHandler = await interceptor.handleRequest(packageUrl);
assert.equal(requestHandler.blockResponse, undefined);
assert.equal(requestHandler.modifiesResponse(), false);
});
it("Should not block tarball requests when the package is excluded from minimum age", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = false;
minimumPackageAgeExclusionsSetting = ["lodash"];
newlyReleasedPackages = new Set(["lodash@4.17.21"]);
const packageUrl =
"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz";
const interceptor = npmInterceptorForUrl(packageUrl);
const requestHandler = await interceptor.handleRequest(packageUrl);
assert.equal(requestHandler.blockResponse, undefined);
assert.equal(requestHandler.modifiesResponse(), false);
});
it("Should not filter packages when package is in exclusion list", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = false;
minimumPackageAgeExclusionsSetting = ["lodash"];
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), // Would normally be filtered
},
});
const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
const modifiedJson = JSON.parse(modifiedBody);
// All versions should remain unchanged since lodash is excluded
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"));
assert.equal(modifiedJson["dist-tags"]["latest"], "3.0.0");
});
it("Should filter packages when package is NOT in exclusion list", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = false;
minimumPackageAgeExclusionsSetting = ["react"]; // Different package
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"]: {}, ["3.0.0"]: {} },
time: {
created: getDate(-365 * 24),
modified: getDate(-3),
["1.0.0"]: getDate(-7),
["3.0.0"]: getDate(-3),
},
})
);
const modifiedJson = JSON.parse(modifiedBody);
// lodash should still be filtered since it's not in exclusions
assert.equal(Object.keys(modifiedJson.versions).length, 1);
assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0"));
assert.ok(!Object.keys(modifiedJson.versions).includes("3.0.0"));
});
it("Should handle scoped packages in exclusion list", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = false;
minimumPackageAgeExclusionsSetting = ["@babel/core"];
const packageUrl = "https://registry.npmjs.org/@babel/core";
const originalBody = JSON.stringify({
name: "@babel/core",
["dist-tags"]: { latest: "7.0.0" },
versions: { ["6.0.0"]: {}, ["7.0.0"]: {} },
time: {
created: getDate(-365 * 24),
modified: getDate(-1),
["6.0.0"]: getDate(-100),
["7.0.0"]: getDate(-1), // Would normally be filtered
},
});
const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
const modifiedJson = JSON.parse(modifiedBody);
// All versions should remain for excluded scoped package
assert.equal(Object.keys(modifiedJson.versions).length, 2);
assert.ok(Object.keys(modifiedJson.versions).includes("6.0.0"));
assert.ok(Object.keys(modifiedJson.versions).includes("7.0.0"));
});
it("Should handle multiple packages in exclusion list", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = false;
minimumPackageAgeExclusionsSetting = ["react", "lodash", "@types/node"];
const packageUrl = "https://registry.npmjs.org/lodash";
const originalBody = JSON.stringify({
name: "lodash",
["dist-tags"]: { latest: "2.0.0" },
versions: { ["1.0.0"]: {}, ["2.0.0"]: {} },
time: {
created: getDate(-365 * 24),
modified: getDate(-1),
["1.0.0"]: getDate(-100),
["2.0.0"]: getDate(-1),
},
});
const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
const modifiedJson = JSON.parse(modifiedBody);
// All versions should remain since lodash is in the exclusion list
assert.equal(Object.keys(modifiedJson.versions).length, 2);
});
it("Should exclude packages matching wildcard pattern @scope/*", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = false;
minimumPackageAgeExclusionsSetting = ["@aikidosec/*"];
const packageUrl = "https://registry.npmjs.org/@aikidosec/safe-chain";
const originalBody = JSON.stringify({
name: "@aikidosec/safe-chain",
["dist-tags"]: { latest: "2.0.0" },
versions: { ["1.0.0"]: {}, ["2.0.0"]: {} },
time: {
created: getDate(-365 * 24),
modified: getDate(-1),
["1.0.0"]: getDate(-100),
["2.0.0"]: getDate(-1), // Would normally be filtered
},
});
const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
const modifiedJson = JSON.parse(modifiedBody);
// All versions should remain since @aikidosec/* matches @aikidosec/safe-chain
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"));
});
it("Should NOT exclude packages that don't match wildcard pattern", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = false;
minimumPackageAgeExclusionsSetting = ["@aikidosec/*"];
const packageUrl = "https://registry.npmjs.org/@other/package";
const originalBody = JSON.stringify({
name: "@other/package",
["dist-tags"]: { latest: "2.0.0" },
versions: { ["1.0.0"]: {}, ["2.0.0"]: {} },
time: {
created: getDate(-365 * 24),
modified: getDate(-1),
["1.0.0"]: getDate(-100),
["2.0.0"]: getDate(-1),
},
});
const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
const modifiedJson = JSON.parse(modifiedBody);
// Version 2.0.0 should be filtered since @other/package doesn't match @aikidosec/*
assert.equal(Object.keys(modifiedJson.versions).length, 1);
assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0"));
});
it("Should reset exclusions between tests", async () => {
minimumPackageAgeSettings = 5;
skipMinimumPackageAgeSetting = false;
minimumPackageAgeExclusionsSetting = []; // Reset to empty
newlyReleasedPackages = new Set();
const packageUrl = "https://registry.npmjs.org/lodash";
const modifiedBody = await runModifyNpmInfoRequest(
packageUrl,
JSON.stringify({
name: "lodash",
["dist-tags"]: { latest: "2.0.0" },
versions: { ["1.0.0"]: {}, ["2.0.0"]: {} },
time: {
created: getDate(-365 * 24),
modified: getDate(-1),
["1.0.0"]: getDate(-100),
["2.0.0"]: getDate(-1),
},
})
);
const modifiedJson = JSON.parse(modifiedBody);
// Version 2.0.0 should be filtered since exclusions are empty
assert.equal(Object.keys(modifiedJson.versions).length, 1);
assert.ok(Object.keys(modifiedJson.versions).includes("1.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<string>}
*/
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;
}
});