-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Support for transitive dependency removal #5549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,7 +41,7 @@ def self.register_name_normaliser(package_manager, name_builder) | |
|
|
||
| def initialize(name:, requirements:, package_manager:, version: nil, | ||
| previous_version: nil, previous_requirements: nil, | ||
| subdependency_metadata: []) | ||
| subdependency_metadata: [], removed: false) | ||
| @name = name | ||
| @version = version | ||
| @requirements = requirements.map { |req| symbolize_keys(req) } | ||
|
|
@@ -53,6 +53,7 @@ def initialize(name:, requirements:, package_manager:, version: nil, | |
| @subdependency_metadata = subdependency_metadata&. | ||
| map { |h| symbolize_keys(h) } | ||
| end | ||
| @removed = removed | ||
|
|
||
| check_values | ||
| end | ||
|
|
@@ -61,6 +62,10 @@ def top_level? | |
| requirements.any? | ||
| end | ||
|
|
||
| def removed? | ||
| @removed | ||
| end | ||
|
|
||
| def to_h | ||
| { | ||
| "name" => name, | ||
|
|
@@ -69,7 +74,8 @@ def to_h | |
| "previous_version" => previous_version, | ||
| "previous_requirements" => previous_requirements, | ||
| "package_manager" => package_manager, | ||
| "subdependency_metadata" => subdependency_metadata | ||
| "subdependency_metadata" => subdependency_metadata, | ||
| "removed" => removed? ? true : nil | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Along the same lines, if we were to have
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When false I'm setting it to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 My next thought would be to make sure we're handling any use of |
||
| }.compact | ||
| end | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,6 +62,9 @@ def fixed_by?(dependency) | |
| # Ignore deps that weren't previously vulnerable | ||
| return false unless affects_version?(dependency.previous_version) | ||
|
|
||
| # Removing a dependency is a way to fix the vulnerability | ||
| return true if dependency.removed? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎨 This reads so well! |
||
|
|
||
| # Select deps that are now fixed | ||
| !affects_version?(dependency.version) | ||
| end | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1531,6 +1531,45 @@ def commits_details(base:, head:) | |
| end | ||
| end | ||
|
|
||
| context "removing a transitive dependency" do | ||
| let(:dependencies) { [removed_dependency, dependency] } | ||
| let(:removed_dependency) do | ||
| Dependabot::Dependency.new( | ||
| name: "statesman", | ||
| previous_version: "1.6.0", | ||
| package_manager: "dummy", | ||
| requirements: [], | ||
| previous_requirements: [], | ||
| removed: true | ||
| ) | ||
| end | ||
|
|
||
| it "includes details of both dependencies" do | ||
| expect(pr_message). | ||
| to eq( | ||
| "Bumps [statesman](https://github.com/gocardless/statesman) "\ | ||
| "and [business](https://github.com/gocardless/business). "\ | ||
| "These dependencies needed to be updated together.\n"\ | ||
| "Removes `statesman`\n"\ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙌 |
||
| "Updates `business` from 1.4.0 to 1.5.0\n"\ | ||
| "<details>\n"\ | ||
| "<summary>Changelog</summary>\n"\ | ||
| "<p><em>Sourced from <a href=\"https://github.com/gocardless/"\ | ||
| "business/blob/master/CHANGELOG.md\">"\ | ||
| "business's changelog</a>.</em></p>\n"\ | ||
| "<blockquote>\n"\ | ||
| "<h2>1.5.0 - June 2, 2015</h2>\n"\ | ||
| "<ul>\n"\ | ||
| "<li>Add 2016 holiday definitions</li>\n"\ | ||
| "</ul>\n"\ | ||
| "</blockquote>\n"\ | ||
| "</details>\n"\ | ||
| "#{commits_details(base: 'v1.4.0', head: 'v1.5.0')}"\ | ||
| "<br />\n" | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| context "with multiple git source requirements", :vcr do | ||
| include_context "with multiple git sources" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,7 +111,8 @@ def vulnerability_audit | |
| @vulnerability_audit ||= | ||
| VulnerabilityAuditor.new( | ||
| dependency_files: dependency_files, | ||
| credentials: credentials | ||
| credentials: credentials, | ||
| allow_removal: @options.key?(:npm_transitive_dependency_removal) | ||
| ).audit( | ||
| dependency: dependency, | ||
| security_advisories: security_advisories | ||
|
|
@@ -141,34 +142,37 @@ def updated_dependencies_after_full_unlock | |
| map { |update_details| build_updated_dependency(update_details) } | ||
| end | ||
|
|
||
| # rubocop:disable Metrics/AbcSize | ||
| def conflicting_updated_dependencies | ||
| top_level_dependencies = top_level_dependency_lookup | ||
|
|
||
| updated_deps = [] | ||
| vulnerability_audit["fix_updates"].each do |update| | ||
| dependency_name = update["dependency_name"] | ||
| requirements = top_level_dependencies[dependency_name]&.requirements || [] | ||
| conflicting_dep = Dependency.new( | ||
| name: dependency_name, | ||
| package_manager: "npm_and_yarn", | ||
| requirements: requirements | ||
| ) | ||
|
|
||
| updated_deps << build_updated_dependency( | ||
| dependency: conflicting_dep, | ||
| dependency: Dependency.new( | ||
| name: dependency_name, | ||
| package_manager: "npm_and_yarn", | ||
| requirements: requirements | ||
| ), | ||
| version: update["target_version"], | ||
| previous_version: update["current_version"] | ||
| ) | ||
| end | ||
| # rubocop:enable Metrics/AbcSize | ||
|
|
||
| # We don't need to update this but need to include it so it's described | ||
| # in the PR and we'll pass validation that this dependency is at a | ||
| # non-vulnerable version. | ||
| if updated_deps.none? { |dep| dep.name == dependency.name } | ||
| target_version = vulnerability_audit["target_version"] | ||
| updated_deps << build_updated_dependency( | ||
| dependency: dependency, | ||
| version: vulnerability_audit["target_version"], | ||
| previous_version: dependency.version | ||
| version: target_version, | ||
| previous_version: dependency.version, | ||
| removed: target_version.nil? | ||
| ) | ||
| end | ||
|
|
||
|
|
@@ -189,7 +193,8 @@ def top_level_dependency_lookup | |
|
|
||
| def build_updated_dependency(update_details) | ||
| original_dep = update_details.fetch(:dependency) | ||
| version = update_details.fetch(:version).to_s | ||
| removed = update_details.fetch(:removed, false) | ||
| version = update_details.fetch(:version).to_s unless removed | ||
|
Comment on lines
+196
to
+197
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 When I read this, I read that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this stage of the code, yes. However, it is valid for a |
||
| previous_version = update_details.fetch(:previous_version)&.to_s | ||
|
|
||
| Dependency.new( | ||
|
|
@@ -203,7 +208,8 @@ def build_updated_dependency(update_details) | |
| ).updated_requirements, | ||
| previous_version: previous_version, | ||
| previous_requirements: original_dep.requirements, | ||
| package_manager: original_dep.package_manager | ||
| package_manager: original_dep.package_manager, | ||
| removed: removed | ||
| ) | ||
| end | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,9 +15,10 @@ module Dependabot | |
| module NpmAndYarn | ||
| class UpdateChecker < Dependabot::UpdateCheckers::Base | ||
| class VulnerabilityAuditor | ||
| def initialize(dependency_files:, credentials:) | ||
| def initialize(dependency_files:, credentials:, allow_removal: false) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How will
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's triggered based on the |
||
| @dependency_files = dependency_files | ||
| @credentials = credentials | ||
| @allow_removal = allow_removal | ||
| end | ||
|
|
||
| # Finds any dependencies in the `package-lock.json` or `npm-shrinkwrap.json` that have | ||
|
|
@@ -96,7 +97,7 @@ def viable_audit_result?(audit_result, security_advisories) | |
|
|
||
| def validate_audit_result(audit_result, security_advisories) | ||
| return :fix_unavailable unless audit_result["fix_available"] | ||
| return :vulnerable_dependency_removed if vulnerable_dependency_removed?(audit_result) | ||
| return :vulnerable_dependency_removed if !@allow_removal && vulnerable_dependency_removed?(audit_result) | ||
| return :dependency_still_vulnerable if dependency_still_vulnerable?(audit_result, security_advisories) | ||
| return :downgrades_dependencies if downgrades_dependencies?(audit_result) | ||
|
|
||
|
|
@@ -108,6 +109,9 @@ def vulnerable_dependency_removed?(audit_result) | |
| end | ||
|
|
||
| def dependency_still_vulnerable?(audit_result, security_advisories) | ||
| # vulnerable depenendency is removed if the target version is nil | ||
| return false unless audit_result["target_version"] | ||
|
|
||
| version = Version.new(audit_result["target_version"]) | ||
| security_advisories.any? { |a| a.vulnerable?(version) } | ||
| end | ||
|
|
@@ -121,6 +125,8 @@ def downgrades_dependencies?(audit_result) | |
| end | ||
|
|
||
| def downgrades_version?(current_version, target_version) | ||
| return false unless target_version | ||
|
|
||
| current = Version.new(current_version) | ||
| target = Version.new(target_version) | ||
| current > target | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 What do you think about this explicitly returning a boolean? I'd expect that based on the
?ending of the method name.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should already return a boolean since I've defaulted it to false at https://github.com/dependabot/dependabot-core/pull/5549/files#diff-75ed2e4bde042ddd4c61aeb79efee8d861ecae1351d692fb94607b581d647f05R44
I just wanted the
?on the accessor. Would you do that another way?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd reach for the
?method as well 😄 I'd personally make sure to returntrue/falsebecause it's a pretty established convention. The other option is!!@removed, but I think Rubocop favors this approach.