dependabot-npm_and_yarn 0.195.0 → 0.196.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/helpers/package.json CHANGED
@@ -12,6 +12,7 @@
12
12
  "@dependabot/yarn-lib": "^1.21.1",
13
13
  "@npmcli/arborist": "^5.2.0",
14
14
  "detect-indent": "^6.1.0",
15
+ "nock": "^13.2.4",
15
16
  "npm": "6.14.17",
16
17
  "semver": "^7.3.7"
17
18
  },
data/helpers/run.js CHANGED
@@ -18,18 +18,13 @@ process.stdin.on("end", () => {
18
18
  process.exit(1);
19
19
  }
20
20
 
21
- try {
22
- func
23
- .apply(null, request.args)
24
- .then((result) => {
25
- output({ result: result });
26
- })
27
- .catch((error) => {
28
- output({ error: error.message });
29
- process.exit(1);
30
- });
31
- } catch (e) {
32
- output({ error: `Error calling function: ${func.name}: ${e}` });
33
- process.exit(1);
34
- }
21
+ func
22
+ .apply(null, request.args)
23
+ .then((result) => {
24
+ output({ result: result });
25
+ })
26
+ .catch((error) => {
27
+ output({ error: error.message });
28
+ process.exit(1);
29
+ });
35
30
  });
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "dependabot/dependency"
5
+ require "dependabot/errors"
6
+ require "dependabot/logger"
7
+ require "dependabot/npm_and_yarn/file_parser"
8
+ require "dependabot/npm_and_yarn/helpers"
9
+ require "dependabot/npm_and_yarn/native_helpers"
10
+ require "dependabot/npm_and_yarn/update_checker"
11
+ require "dependabot/npm_and_yarn/update_checker/dependency_files_builder"
12
+ require "dependabot/shared_helpers"
13
+
14
+ module Dependabot
15
+ module NpmAndYarn
16
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
17
+ class VulnerabilityAuditor
18
+ def initialize(dependency_files:, credentials:)
19
+ @dependency_files = dependency_files
20
+ @credentials = credentials
21
+ end
22
+
23
+ # Finds any dependencies in the `package-lock.json` or `npm-shrinkwrap.json` that have
24
+ # a subdependency on the given dependency that is locked to a vuln version range.
25
+ #
26
+ # NOTE: yarn is currently not supported.
27
+ #
28
+ # @param dependency [Dependabot::Dependency] the dependency to check
29
+ # @param security_advisories [Array<Dependabot::SecurityAdvisory>] advisories for the dependency
30
+ # @return [Hash<String, [String, Array<Hash<String, String>>]>] the audit results
31
+ # * :dependency_name [String] the name of the dependency
32
+ # * :fix_available [Boolean] whether a fix is available
33
+ # * :current_version [String] the version of the dependency
34
+ # * :target_version [String] the version of the dependency after the fix
35
+ # * :fix_updates [Array<Hash<String, String>>] a list of dependencies to update in order to fix
36
+ # * :dependency_name [String] the name of the blocking dependency
37
+ # * :current_version [String] the current version of the blocking dependency
38
+ # * :target_version [String] the target version of the blocking dependency
39
+ def audit(dependency:, security_advisories:)
40
+ fix_unavailable = {
41
+ "dependency_name" => dependency.name,
42
+ "fix_available" => false
43
+ }
44
+
45
+ SharedHelpers.in_a_temporary_directory do
46
+ dependency_files_builder = DependencyFilesBuilder.new(
47
+ dependency: dependency,
48
+ dependency_files: dependency_files,
49
+ credentials: credentials
50
+ )
51
+ dependency_files_builder.write_temporary_dependency_files
52
+
53
+ # `npm-shrinkwrap.js`, if present, takes precedence over `package-lock.js`.
54
+ # Both files use the same format. See https://bit.ly/3lDIAJV for more.
55
+ lockfile = (dependency_files_builder.shrinkwraps + dependency_files_builder.package_locks).first
56
+ return fix_unavailable unless lockfile
57
+
58
+ vuln_versions = security_advisories.map do |a|
59
+ {
60
+ dependency_name: a.dependency_name,
61
+ affected_versions: a.vulnerable_version_strings
62
+ }
63
+ end
64
+
65
+ audit_result = SharedHelpers.run_helper_subprocess(
66
+ command: NativeHelpers.helper_path,
67
+ function: "npm:vulnerabilityAuditor",
68
+ args: [Dir.pwd, vuln_versions]
69
+ )
70
+ return fix_unavailable unless valid_audit_result?(audit_result, security_advisories)
71
+
72
+ audit_result
73
+ end
74
+ rescue SharedHelpers::HelperSubprocessFailed => e
75
+ log_helper_subprocess_failure(dependency, e)
76
+ fix_unavailable
77
+ end
78
+
79
+ private
80
+
81
+ attr_reader :dependency_files, :credentials
82
+
83
+ def valid_audit_result?(audit_result, security_advisories)
84
+ # we only need to check results that indicate a fix is available
85
+ return true unless audit_result["fix_available"]
86
+
87
+ return false if vulnerable_dependency_removed?(audit_result)
88
+ return false if dependency_still_vulnerable?(audit_result, security_advisories)
89
+ return false if downgrades_dependencies?(audit_result)
90
+
91
+ true
92
+ end
93
+
94
+ def vulnerable_dependency_removed?(audit_result)
95
+ !audit_result["target_version"]
96
+ end
97
+
98
+ def dependency_still_vulnerable?(audit_result, security_advisories)
99
+ version = Version.new(audit_result["target_version"])
100
+ security_advisories.any? { |a| a.vulnerable?(version) }
101
+ end
102
+
103
+ def downgrades_dependencies?(audit_result)
104
+ return true if downgrades_version?(audit_result["current_version"], audit_result["target_version"])
105
+
106
+ audit_result["fix_updates"].any? do |update|
107
+ downgrades_version?(update["current_version"], update["target_version"])
108
+ end
109
+ end
110
+
111
+ def downgrades_version?(current_version, target_version)
112
+ current = Version.new(current_version)
113
+ target = Version.new(target_version)
114
+ current > target
115
+ end
116
+
117
+ def log_helper_subprocess_failure(dependency, error)
118
+ # See `Dependabot::SharedHelpers.run_helper_subprocess` for details on error context
119
+ context = error.error_context || {}
120
+
121
+ builder = ::StringIO.new
122
+ builder << "VulnerabilityAuditor: "
123
+ builder << "#{context[:function]} " if context[:function]
124
+ builder << "failed"
125
+ builder << " after #{context[:time_taken].truncate(2)}s" if context[:time_taken]
126
+ builder << " while auditing #{dependency.name}: "
127
+ builder << error.message
128
+ builder << "\n" << context[:trace]
129
+
130
+ msg = builder.string
131
+ Dependabot.logger.info(msg) # TODO: is this the right log level?
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -14,6 +14,7 @@ module Dependabot
14
14
  require_relative "update_checker/version_resolver"
15
15
  require_relative "update_checker/subdependency_version_resolver"
16
16
  require_relative "update_checker/conflicting_dependency_resolver"
17
+ require_relative "update_checker/vulnerability_auditor"
17
18
 
18
19
  def latest_version
19
20
  @latest_version ||=
@@ -106,20 +107,80 @@ module Dependabot
106
107
 
107
108
  private
108
109
 
110
+ def vulnerability_audit
111
+ @vulnerability_audit ||=
112
+ VulnerabilityAuditor.new(
113
+ dependency_files: dependency_files,
114
+ credentials: credentials
115
+ ).audit(
116
+ dependency: dependency,
117
+ security_advisories: security_advisories
118
+ )
119
+ end
120
+
109
121
  def latest_version_resolvable_with_full_unlock?
110
- return unless latest_version
122
+ return false unless latest_version
123
+
124
+ return version_resolver.latest_version_resolvable_with_full_unlock? if dependency.top_level?
125
+
126
+ return false unless transitive_security_updates_enabled? && security_advisories.any?
111
127
 
112
- # No support for full unlocks for subdependencies yet
113
- return false unless dependency.top_level?
128
+ vulnerability_audit["fix_available"]
129
+ end
114
130
 
115
- version_resolver.latest_version_resolvable_with_full_unlock?
131
+ def transitive_security_updates_enabled?
132
+ options.key?(:npm_transitive_security_updates)
116
133
  end
117
134
 
118
135
  def updated_dependencies_after_full_unlock
136
+ if !dependency.top_level? && transitive_security_updates_enabled? && security_advisories.any?
137
+ return conflicting_updated_dependencies
138
+ end
139
+
119
140
  version_resolver.dependency_updates_from_full_unlock.
120
141
  map { |update_details| build_updated_dependency(update_details) }
121
142
  end
122
143
 
144
+ def conflicting_updated_dependencies
145
+ top_level_dependencies = FileParser.new(
146
+ dependency_files: dependency_files,
147
+ credentials: credentials,
148
+ source: nil
149
+ ).parse.select(&:top_level?)
150
+
151
+ top_level_dependency_lookup = top_level_dependencies.map { |dep| [dep.name, dep] }.to_h
152
+
153
+ updated_deps = []
154
+ vulnerability_audit["fix_updates"].each do |update|
155
+ dependency_name = update["dependency_name"]
156
+ requirements = top_level_dependency_lookup[dependency_name]&.requirements || []
157
+ conflicting_dep = Dependency.new(
158
+ name: dependency_name,
159
+ package_manager: "npm_and_yarn",
160
+ requirements: requirements
161
+ )
162
+
163
+ updated_deps << build_updated_dependency(
164
+ dependency: conflicting_dep,
165
+ version: update["target_version"],
166
+ previous_version: update["current_version"]
167
+ )
168
+ end
169
+
170
+ # We don't need to update this but need to include it so it's described
171
+ # in the PR and we'll pass validation that this dependency is at a
172
+ # non-vulnerable version.
173
+ if updated_deps.none? { |dep| dep.name == dependency.name }
174
+ updated_deps << build_updated_dependency(
175
+ dependency: dependency,
176
+ version: vulnerability_audit["target_version"],
177
+ previous_version: dependency.version
178
+ )
179
+ end
180
+
181
+ updated_deps
182
+ end
183
+
123
184
  def build_updated_dependency(update_details)
124
185
  original_dep = update_details.fetch(:dependency)
125
186
  version = update_details.fetch(:version).to_s
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-npm_and_yarn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.195.0
4
+ version: 0.196.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.195.0
19
+ version: 0.196.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.195.0
26
+ version: 0.196.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debase
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -233,6 +233,7 @@ files:
233
233
  - helpers/jest.config.js
234
234
  - helpers/lib/npm/conflicting-dependency-parser.js
235
235
  - helpers/lib/npm/index.js
236
+ - helpers/lib/npm/vulnerability-auditor.js
236
237
  - helpers/lib/npm6/helpers.js
237
238
  - helpers/lib/npm6/index.js
238
239
  - helpers/lib/npm6/peer-dependency-checker.js
@@ -307,6 +308,7 @@ files:
307
308
  - lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb
308
309
  - lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb
309
310
  - lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb
311
+ - lib/dependabot/npm_and_yarn/update_checker/vulnerability_auditor.rb
310
312
  - lib/dependabot/npm_and_yarn/version.rb
311
313
  homepage: https://github.com/dependabot/dependabot-core
312
314
  licenses: