import { getNpmCustomRegistries, getNpmMinimumPackageAgeExclusions, skipMinimumPackageAge, } from "../../../config/settings.js"; import { isMalwarePackage } from "../../../scanning/audit/index.js"; import { interceptRequests } from "../interceptorBuilder.js"; import { isPackageInfoUrl, matchesExclusionPattern, modifyNpmInfoRequestHeaders, modifyNpmInfoResponse, } from "./modifyNpmInfo.js"; import { parseNpmPackageUrl } from "./parseNpmPackageUrl.js"; import { openNewPackagesDatabase } from "../../../scanning/newPackagesDatabase.js"; const knownJsRegistries = [ "registry.npmjs.org", "registry.yarnpkg.com", "registry.npmjs.com", ]; /** * @param {string} url * @returns {import("../interceptorBuilder.js").Interceptor | undefined} */ export function npmInterceptorForUrl(url) { const registry = [...knownJsRegistries, ...getNpmCustomRegistries()].find( (reg) => url.includes(reg) ); if (registry) { return buildNpmInterceptor(registry); } return undefined; } /** * @param {string} registry * @returns {import("../interceptorBuilder.js").Interceptor} */ function buildNpmInterceptor(registry) { return interceptRequests(async (reqContext) => { const { packageName, version } = parseNpmPackageUrl( reqContext.targetUrl, registry ); const minimumAgeChecksEnabled = !skipMinimumPackageAge(); const packageIsExcludedFromMinimumAgeChecks = packageName && isExcludedFromMinimumPackageAge(packageName); if (await isMalwarePackage(packageName, version)) { reqContext.blockMalware(packageName, version); return; } if (minimumAgeChecksEnabled && isPackageInfoUrl(reqContext.targetUrl)) { reqContext.modifyRequestHeaders(modifyNpmInfoRequestHeaders); reqContext.modifyBody((body, headers) => { const metadataPackageName = getPackageNameFromMetadataResponse( body, headers ); if ( metadataPackageName && isExcludedFromMinimumPackageAge(metadataPackageName) ) { return body; } return modifyNpmInfoResponse(body, headers); }); return; } // For tarball requests the metadata check above is skipped, so we check the // new packages list as a fallback (covers e.g. frozen-lockfile installs). if ( minimumAgeChecksEnabled && packageName && version && !packageIsExcludedFromMinimumAgeChecks ) { const newPackagesDatabase = await openNewPackagesDatabase(); if (newPackagesDatabase.isNewlyReleasedPackage(packageName, version)) { reqContext.blockMinimumAgeRequest( packageName, version, `Forbidden - blocked by safe-chain direct download minimum package age (${packageName}@${version})` ); } } }); } /** * @param {string} packageName * @returns {boolean} */ function isExcludedFromMinimumPackageAge(packageName) { const exclusions = getNpmMinimumPackageAgeExclusions(); return exclusions.some((pattern) => matchesExclusionPattern(packageName, pattern) ); } /** * @param {Buffer} body * @param {NodeJS.Dict | undefined} headers * @returns {string | undefined} */ function getPackageNameFromMetadataResponse(body, headers) { try { const contentType = headers?.["content-type"]; const normalizedContentType = Array.isArray(contentType) ? contentType.join(",") : contentType; if (!normalizedContentType?.toLowerCase().includes("application/json")) { return undefined; } const bodyJson = JSON.parse(body.toString("utf8")); return typeof bodyJson.name === "string" ? bodyJson.name : undefined; } catch { return undefined; } }