UNPKG

snyk-nodejs-lockfile-parser

Version:
223 lines 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getYarnLockV2ChildNode = exports.yarnLockFileKeyNormalizer = void 0; const core_1 = require("@yarnpkg/core"); const _flatMap = require("lodash.flatmap"); const errors_1 = require("../../errors"); const parsers_1 = require("../../parsers"); const util_1 = require("../util"); const semver = require("semver"); const debugModule = require("debug"); const debug = debugModule('snyk-nodejs-plugin'); const BUILTIN_PLACEHOLDER = 'builtin'; const MULTIPLE_KEYS_REGEXP = / *, */g; const keyNormalizer = (parseDescriptor, parseRange) => (rawDescriptor) => { // See https://yarnpkg.com/features/protocols const descriptors = [rawDescriptor]; const descriptor = parseDescriptor(rawDescriptor); const name = `${descriptor.scope ? '@' + descriptor.scope + '/' : ''}${descriptor.name}`; const range = parseRange(descriptor.range); const protocol = range.protocol; switch (protocol) { case 'npm:': case 'file:': // This is space inneficient but will be kept for now, // Due to how we wish to index using the dependencies map // we want the keys to match name@version but this is handled different // for npm alias and normal install. descriptors.push(`${name}@${range.selector}`); descriptors.push(`${name}@${protocol}${range.selector}`); break; case 'git:': case 'git+ssh:': case 'git+http:': case 'git+https:': case 'github:': if (range.source) { descriptors.push(`${name}@${protocol}${range.source}${range.selector ? '#' + range.selector : ''}`); } else { descriptors.push(`${name}@${protocol}${range.selector}`); } break; case 'patch:': if (range.source && range.selector.indexOf(BUILTIN_PLACEHOLDER) === 0) { descriptors.push(range.source); } else { descriptors.push(`${name}@${protocol}${range.source}${range.selector ? '#' + range.selector : ''}`); } break; case null: case undefined: if (range.source) { descriptors.push(`${name}@${range.source}#${range.selector}`); } else { descriptors.push(`${name}@${range.selector}`); } break; case 'http:': case 'https:': case 'link:': case 'portal:': case 'exec:': case 'workspace:': case 'virtual:': default: // For user defined plugins descriptors.push(`${name}@${protocol}${range.selector}`); break; } return descriptors; }; const yarnLockFileKeyNormalizer = (parseDescriptor, parseRange) => (fullDescriptor) => { const allKeys = fullDescriptor .split(MULTIPLE_KEYS_REGEXP) .map(keyNormalizer(parseDescriptor, parseRange)); return new Set(_flatMap(allKeys)); }; exports.yarnLockFileKeyNormalizer = yarnLockFileKeyNormalizer; const getYarnLockV2ChildNode = (name, depInfo, pkgs, strictOutOfSync, includeOptionalDeps, resolutions, parentNode) => { // First, check if a resolution would be used const resolvedVersionFromResolution = (() => { // Check for scoped resolution (e.g., "parentPackageName/dependencyName") const scopedKey = `${parentNode.name}/${name}`; if (resolutions[scopedKey]) { return resolutions[scopedKey]; } // Check for scoped + versioned resolution (e.g., "parentPkg@npm:version/depName") // These have the format: parentPackageName@versionOrProtocol/dependencyName // The dep name suffix could be scoped (e.g., "@scope/dep"), so we check // if the key ends with `/${name}` to correctly split parent from dep. const suffix = `/${name}`; for (const resKey in resolutions) { if (Object.prototype.hasOwnProperty.call(resolutions, resKey)) { if (!resKey.endsWith(suffix)) continue; const parentPart = resKey.substring(0, resKey.length - suffix.length); // Skip if parentPart is just a plain name (handled by simple scoped check above) if (!parentPart.includes('@') || parentPart === parentNode.name) { continue; } try { const descriptor = core_1.structUtils.parseDescriptor(parentPart); const parentPkgName = core_1.structUtils.stringifyIdent(descriptor); if (parentPkgName !== parentNode.name) continue; // If the resolution key includes a version/range for the parent, // verify the parent's resolved version satisfies it if (descriptor.range && descriptor.range !== 'unknown') { const rangeWithoutProtocol = descriptor.range.replace(/^[a-z]+:/, ''); if (parentNode.version !== rangeWithoutProtocol && !(semver.valid(parentNode.version) && semver.validRange(rangeWithoutProtocol) && semver.satisfies(parentNode.version, rangeWithoutProtocol))) { continue; } } return resolutions[resKey]; } catch (e) { debug(`Error parsing scoped-versioned resolution key(${resKey}): ${e}`); } } } // Check for resolutions matching "packageName@versionOrRangeToOverride" for (const resKey in resolutions) { if (Object.prototype.hasOwnProperty.call(resolutions, resKey)) { try { const descriptor = core_1.structUtils.parseDescriptor(resKey); const resKeyPkgName = core_1.structUtils.stringifyIdent(descriptor); // Check if the resolution key targets the current package name if (resKeyPkgName === name) { if (descriptor.range && descriptor.range !== 'unknown') { // Strip protocol prefix from both sides (e.g., 'npm:^3.5.4' -> '^3.5.4') const versionWithoutProtocol = depInfo.version.replace(/^[a-z]+:/, ''); const rangeWithoutProtocol = descriptor.range.replace(/^[a-z]+:/, ''); // Check if the current dependency's version/range matches or satisfies // the version/range specified in the resolution key. // If the dependency version is a concrete version (e.g., '3.5.4'), // check if it satisfies the resolution range. // If the dependency version is a range (e.g., '^3.5.4'), check for equality. if (versionWithoutProtocol === rangeWithoutProtocol || (semver.valid(versionWithoutProtocol) && semver.satisfies(versionWithoutProtocol, rangeWithoutProtocol))) { return resolutions[resKey]; } } } } catch (e) { debug(`Error parsing resolution key(${resKey}): ${e}$`); } } } // Check for global resolution by package name (e.g., "packageName": "version") if (resolutions[name]) { return resolutions[name]; } return ''; // No resolution applies })(); if (resolvedVersionFromResolution) { // Decode URL-encoded characters in resolution values (e.g., npm%3A -> npm:) // to match the keys extracted from yarn.lock const decodedResolution = decodeURIComponent(resolvedVersionFromResolution); const childNodeKeyFromResolution = `${name}@${decodedResolution}`; if (!pkgs[childNodeKeyFromResolution]) { if (strictOutOfSync && !/^file:/.test(decodedResolution)) { throw new errors_1.OutOfSyncError(childNodeKeyFromResolution, parsers_1.LockfileType.yarn2); } else { return Object.assign({ id: childNodeKeyFromResolution, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: decodedResolution, dependencies: {}, isDev: depInfo.isDev, missingLockFileEntry: true }, (depInfo.alias ? { alias: Object.assign(Object.assign({}, depInfo.alias), { version: decodedResolution }), } : {})); } } const pkgData = pkgs[childNodeKeyFromResolution]; const { version: versionFromResolution, dependencies, optionalDependencies, } = pkgData; const formattedDependencies = (0, util_1.getGraphDependencies)(dependencies || {}, { isDev: depInfo.isDev, }); const formattedOptionalDependencies = includeOptionalDeps ? (0, util_1.getGraphDependencies)(optionalDependencies || {}, { isDev: depInfo.isDev, isOptional: true, }) : {}; return Object.assign({ id: `${name}@${versionFromResolution}`, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: versionFromResolution, dependencies: Object.assign(Object.assign({}, formattedOptionalDependencies), formattedDependencies), isDev: depInfo.isDev }, (depInfo.alias ? { alias: Object.assign(Object.assign({}, depInfo.alias), { version: versionFromResolution }) } : {})); } // No resolutions const childNodeKey = `${name}@${depInfo.version}`; if (!pkgs[childNodeKey]) { if (strictOutOfSync && !/^file:/.test(depInfo.version)) { throw new errors_1.OutOfSyncError(childNodeKey, parsers_1.LockfileType.yarn2); } else { return Object.assign({ id: childNodeKey, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: depInfo.version, dependencies: {}, isDev: depInfo.isDev, missingLockFileEntry: true }, (depInfo.alias ? { alias: Object.assign(Object.assign({}, depInfo.alias), { version: depInfo.version }) } : {})); } } else { const depData = pkgs[childNodeKey]; const dependencies = (0, util_1.getGraphDependencies)(depData.dependencies || {}, { isDev: depInfo.isDev, }); const optionalDependencies = includeOptionalDeps ? (0, util_1.getGraphDependencies)(depData.optionalDependencies || {}, { isDev: depInfo.isDev, isOptional: true, }) : {}; return Object.assign({ id: `${name}@${depData.version}`, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: depData.version, dependencies: Object.assign(Object.assign({}, dependencies), optionalDependencies), isDev: depInfo.isDev }, (depInfo.alias ? { alias: Object.assign(Object.assign({}, depInfo.alias), { version: depData.version }) } : {})); } }; exports.getYarnLockV2ChildNode = getYarnLockV2ChildNode; //# sourceMappingURL=utils.js.map