rogeriochaves / npm-force-resolutions

Force npm to install a specific transitive dependency version

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support for multiple versions of same package

jcardali opened this issue · comments

I've run into the situation several times now when I want to add a resolution for a package, but there are multiple major versions present. For example:

> npm ls y18n
├─┬ @graphql-codegen/cli@1.21.3
│ └─┬ yargs@16.2.0
│   └── y18n@5.0.5
├─┬ concurrently@5.3.0
│ └─┬ yargs@13.3.2
│   └── y18n@4.0.0
└─┬ jest@26.6.3
  └─┬ jest-cli@26.6.3
    └─┬ yargs@15.4.1
      └── y18n@4.0.0  deduped

If I then add the following resolution:

  "resolutions": {
    "y18n": "~4.0.1",
  }

I now get:

 > npm ls y18n
├─┬ @graphql-codegen/cli@1.21.3
│ ├── y18n@4.0.1  extraneous
│ └─┬ yargs@16.2.0
│   └── y18n@4.0.1  invalid
├─┬ concurrently@5.3.0
│ └─┬ yargs@13.3.2
│   └── y18n@4.0.1
└─┬ jest@26.6.3
  └─┬ jest-cli@26.6.3
    └─┬ yargs@15.4.1
      └── y18n@4.0.1

That invalid error is the problem, because yargs@16.20 requires y18n of ^5.0.5. The inverse would happen if the resolution was instead set to ~5.0.5.

It would be great if support could be added for multiple versions in resolutions like so:

  "resolutions": {
    "y18n": "~4.0.1 || ~5.0.5"
  }

Which matches the syntax for dependencies: https://docs.npmjs.com/cli/v7/configuring-npm/package-json#dependencies

OR

  "resolutions": {
    "y18n": ["~4.0.1", "~5.0.5"]
  }

Which in this case would yield:

 > npm ls y18n
├─┬ @graphql-codegen/cli@1.21.3
│ ├── y18n@4.0.1  extraneous
│ └─┬ yargs@16.2.0
│   └── y18n@5.0.5
├─┬ concurrently@5.3.0
│ └─┬ yargs@13.3.2
│   └── y18n@4.0.1 
└─┬ jest@26.6.3
  └─┬ jest-cli@26.6.3
    └─┬ yargs@15.4.1
      └── y18n@4.0.1

When force changing any transitive dependency there is the possibility that we break the parent dependency.

I think we can do better. The README for this module states: "The use case for this is when there is a security vulnerability and you MUST update a nested dependency". So let's keep that in mind and only make the necessary updates to remove the security vulnerability.

Users should be able to enter a minimum version required in the resolutions block. For example, I require version 2.4.0 of package x. If a transitive dependency for package x is 2.1.6 (or anything below 2.4) then it gets bumped to 2.4. Otherwise, if a transitive dependency for package x is 2.7.0 (or anything greater than 2.4) it is not changed. This will keep the blast radius as small as possible!

For example:

> npm ls y18n
├─┬ @graphql-codegen/cli@1.21.3
│ └─┬ yargs@16.2.0
│   └── y18n@5.0.5
├─┬ concurrently@5.3.0
│ └─┬ yargs@13.3.2
│   └── y18n@4.0.0
└─┬ jest@26.6.3
  └─┬ jest-cli@26.6.3
    └─┬ yargs@15.4.1
      └── y18n@4.0.0  deduped

And then I add

"resolutions": {
    "y18n": ">=4.0.1"
}

Which would result in:

> npm ls y18n
├─┬ @graphql-codegen/cli@1.21.3
│ └─┬ yargs@16.2.0
│   └── y18n@5.0.5 <== left alone
├─┬ concurrently@5.3.0
│ └─┬ yargs@13.3.2
│   └── y18n@4.0.1 <== bumped from 4.0.0 to 4.0.1
└─┬ jest@26.6.3
  └─┬ jest-cli@26.6.3
    └─┬ yargs@15.4.1
      └── y18n@4.0.1  <== bumped from 4.0.0 to 4.0.1

@bones418 What if there was a 3.x or 2.x nested version of y18n?

For my particular use case, y18n has vulnerabilities before versions 3.2.2, 4.0.1, and 5.0.5

@jcardali agreed, your project could contain nested versions of a package where multiple major versions are vulnerable and need to be bumped. I also suppose the entirety of some 5.x.x release could be vulnerable while a 4.x.x release is not, which means there'd be cases where you do want to explicitly downgrade the package.

Suppose resolutions is

  "resolutions": {
    "y18n": "~4.0.1 || ~5.0.5"
  }

I imagine that means:

  • nested dependencies < 4.0.1 get bumped to 4.x.x
  • nested dependencies from 5.0.0 up to 5.0.5 get bumped to the latest 5.x.x
  • What about nested dependencies from 6.0.0? Are they downgraded to 5.x.x because 6.x.x isn't listed as safe? This approach would require a lot of vigilance from the user to monitor their nested dependencies and be aware of when dependencies begin using new major versions of nested dependencies

I still think a simple approach of taking max(nested dependency version, minimum required version) as a quick win to reduce blast radius and address 80% of use cases. Perhaps they should be separate issues though.

Since this was built to be similar to yarn's selective dependency resolutions, could it not follow their lead for specifying which packages to update https://github.com/yarnpkg/rfcs/blob/master/implemented/0000-selective-versions-resolutions.md#package-designation

I faced a need for it recently as well.
I think first very easy step could be adding support for a js config file.
For instance the command would run npm-force-resolutions --config resolutions.js
and resolutions.js

module.exports = {
map: function(packageName, currentVersion) {
    if (packageName === lodash) return '4.17.2'
    else if (packageName === 'yargs') {
         if (currentVersion === '5.0.2') return '5.0.2'
         else return '4.0.1'
    }
    else return currentVersion;
}
}

Despite usage is not that convenient, but since implementation and API for that is much simpler, it can be done much faster and therefore be used much sooner

hey folks, what's the update for this? Are we doing this - my use case is also same for this vulnerability - GHSA-76p3-8jx3-jpfq.

The Above JS file scheme is great, will add lot of extensibility?

@jcardali Hi Mate, how did you handled the above use case then, any thoughts?