dependabot-npm_and_yarn 0.212.0 → 0.214.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/.eslintrc +1 -1
  3. data/helpers/README.md +2 -2
  4. data/helpers/lib/npm/vulnerability-auditor.js +7 -7
  5. data/helpers/package-lock.json +2781 -2547
  6. data/helpers/package.json +5 -5
  7. data/helpers/test/npm6/fixtures/conflicting-dependency-parser/deeply-nested/package-lock.json +3 -3
  8. data/lib/dependabot/npm_and_yarn/file_fetcher/path_dependency_builder.rb +11 -2
  9. data/lib/dependabot/npm_and_yarn/file_fetcher.rb +90 -5
  10. data/lib/dependabot/npm_and_yarn/file_parser/lockfile_parser.rb +15 -4
  11. data/lib/dependabot/npm_and_yarn/file_parser.rb +15 -6
  12. data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +35 -21
  13. data/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb +86 -7
  14. data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +2 -2
  15. data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +96 -32
  16. data/lib/dependabot/npm_and_yarn/file_updater.rb +53 -1
  17. data/lib/dependabot/npm_and_yarn/helpers.rb +94 -0
  18. data/lib/dependabot/npm_and_yarn/package_name.rb +2 -2
  19. data/lib/dependabot/npm_and_yarn/requirement.rb +3 -3
  20. data/lib/dependabot/npm_and_yarn/update_checker/dependency_files_builder.rb +43 -1
  21. data/lib/dependabot/npm_and_yarn/update_checker/latest_version_finder.rb +13 -14
  22. data/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb +16 -3
  23. data/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb +77 -23
  24. data/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb +3 -4
  25. data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +19 -4
  26. data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +74 -30
  27. data/lib/dependabot/npm_and_yarn/update_checker/vulnerability_auditor.rb +33 -8
  28. data/lib/dependabot/npm_and_yarn/update_checker.rb +76 -21
  29. data/lib/dependabot/npm_and_yarn/version.rb +1 -1
  30. data/lib/dependabot/npm_and_yarn.rb +2 -0
  31. metadata +13 -56
  32. data/lib/dependabot/npm_and_yarn/file_parser/yarn_lockfile_parser.rb +0 -59
@@ -12,20 +12,21 @@ module Dependabot
12
12
  https://registry.npmjs.org
13
13
  http://registry.npmjs.org
14
14
  https://registry.yarnpkg.com
15
+ http://registry.yarnpkg.com
15
16
  ).freeze
16
- NPM_AUTH_TOKEN_REGEX =
17
- %r{//(?<registry>.*)/:_authToken=(?<token>.*)$}.freeze
18
- NPM_GLOBAL_REGISTRY_REGEX =
19
- /^registry\s*=\s*['"]?(?<registry>.*?)['"]?$/.freeze
20
- YARN_GLOBAL_REGISTRY_REGEX =
21
- /^(?:--)?registry\s+['"](?<registry>.*)['"]/.freeze
17
+ NPM_AUTH_TOKEN_REGEX = %r{//(?<registry>.*)/:_authToken=(?<token>.*)$}
18
+ NPM_GLOBAL_REGISTRY_REGEX = /^registry\s*=\s*['"]?(?<registry>.*?)['"]?$/
19
+ YARN_GLOBAL_REGISTRY_REGEX = /^(?:--)?registry\s+((['"](?<registry>.*)['"])|(?<registry>.*))/
20
+ NPM_SCOPED_REGISTRY_REGEX = /^(?<scope>@[^:]+)\s*:registry\s*=\s*['"]?(?<registry>.*?)['"]?$/
21
+ YARN_SCOPED_REGISTRY_REGEX = /['"](?<scope>@[^:]+):registry['"]\s((['"](?<registry>.*)['"])|(?<registry>.*))/
22
22
 
23
23
  def initialize(dependency:, credentials:, npmrc_file: nil,
24
- yarnrc_file: nil)
24
+ yarnrc_file: nil, yarnrc_yml_file: nil)
25
25
  @dependency = dependency
26
26
  @credentials = credentials
27
27
  @npmrc_file = npmrc_file
28
28
  @yarnrc_file = yarnrc_file
29
+ @yarnrc_yml_file = yarnrc_yml_file
29
30
  end
30
31
 
31
32
  def registry
@@ -46,15 +47,24 @@ module Dependabot
46
47
  end
47
48
  end
48
49
 
50
+ def registry_from_rc(dependency_name)
51
+ return global_registry unless dependency_name.start_with?("@") && dependency_name.include?("/")
52
+
53
+ scope = dependency_name.split("/").first
54
+ scoped_registry(scope)
55
+ end
56
+
49
57
  private
50
58
 
51
- attr_reader :dependency, :credentials, :npmrc_file, :yarnrc_file
59
+ attr_reader :dependency, :credentials, :npmrc_file, :yarnrc_file, :yarnrc_yml_file
52
60
 
53
61
  def first_registry_with_dependency_details
54
62
  @first_registry_with_dependency_details ||=
55
63
  known_registries.find do |details|
64
+ url = "#{details['registry'].gsub(%r{/+$}, '')}/#{escaped_dependency_name}"
65
+ url = "https://#{url}" unless url.start_with?("http")
56
66
  response = Dependabot::RegistryClient.get(
57
- url: "https://#{details['registry'].gsub(%r{/+$}, '')}/#{escaped_dependency_name}",
67
+ url: url,
58
68
  headers: auth_header_for(details["token"])
59
69
  )
60
70
  response.status < 400 && JSON.parse(response.body)
@@ -64,10 +74,12 @@ module Dependabot
64
74
  nil
65
75
  end&.fetch("registry")
66
76
 
67
- @first_registry_with_dependency_details ||= global_registry
77
+ @first_registry_with_dependency_details ||= global_registry.sub(%r{/+$}, "").sub(%r{^.*?//}, "")
68
78
  end
69
79
 
70
80
  def registry_url
81
+ return registry if registry.start_with?("http")
82
+
71
83
  protocol =
72
84
  if registry_source_url
73
85
  registry_source_url.split("://").first
@@ -134,10 +146,13 @@ module Dependabot
134
146
  npmrc_file.content.scan(NPM_AUTH_TOKEN_REGEX) do
135
147
  next if Regexp.last_match[:registry].include?("${")
136
148
 
149
+ registry = Regexp.last_match[:registry]
150
+ token = Regexp.last_match[:token]&.strip
151
+
137
152
  registries << {
138
153
  "type" => "npm_registry",
139
- "registry" => Regexp.last_match[:registry],
140
- "token" => Regexp.last_match[:token]&.strip
154
+ "registry" => registry.gsub(/\s+/, "%20"),
155
+ "token" => token
141
156
  }
142
157
  end
143
158
 
@@ -146,7 +161,8 @@ module Dependabot
146
161
 
147
162
  registry = Regexp.last_match[:registry].strip.
148
163
  sub(%r{/+$}, "").
149
- sub(%r{^.*?//}, "")
164
+ sub(%r{^.*?//}, "").
165
+ gsub(/\s+/, "%20")
150
166
  next if registries.map { |r| r["registry"] }.include?(registry)
151
167
 
152
168
  registries << {
@@ -168,7 +184,8 @@ module Dependabot
168
184
 
169
185
  registry = Regexp.last_match[:registry].strip.
170
186
  sub(%r{/+$}, "").
171
- sub(%r{^.*?//}, "")
187
+ sub(%r{^.*?//}, "").
188
+ gsub(/\s+/, "%20")
172
189
  registries << {
173
190
  "type" => "npm_registry",
174
191
  "registry" => registry,
@@ -190,26 +207,56 @@ module Dependabot
190
207
  end
191
208
  end
192
209
 
210
+ # rubocop:disable Metrics/PerceivedComplexity
193
211
  def global_registry
212
+ return @global_registry if defined? @global_registry
213
+
194
214
  npmrc_file&.content.to_s.scan(NPM_GLOBAL_REGISTRY_REGEX) do
195
215
  next if Regexp.last_match[:registry].include?("${")
196
216
 
197
- registry = Regexp.last_match[:registry].strip.
198
- sub(%r{/+$}, "").
199
- sub(%r{^.*?//}, "")
200
- return registry
217
+ return @global_registry = Regexp.last_match[:registry].strip
201
218
  end
202
219
 
203
220
  yarnrc_file&.content.to_s.scan(YARN_GLOBAL_REGISTRY_REGEX) do
204
221
  next if Regexp.last_match[:registry].include?("${")
205
222
 
206
- registry = Regexp.last_match[:registry].strip.
207
- sub(%r{/+$}, "").
208
- sub(%r{^.*?//}, "")
209
- return registry
223
+ return @global_registry = Regexp.last_match[:registry].strip
224
+ end
225
+
226
+ if parsed_yarnrc_yml&.key?("npmRegistryServer")
227
+ return @global_registry = parsed_yarnrc_yml["npmRegistryServer"]
228
+ end
229
+
230
+ replaces_base = credentials.find { |cred| cred["type"] == "npm_registry" && cred["replaces-base"] == true }
231
+ if replaces_base
232
+ registry = replaces_base["registry"]
233
+ registry = "https://#{registry}" unless registry.start_with?("http")
234
+ return @global_registry = registry
210
235
  end
211
236
 
212
- "registry.npmjs.org"
237
+ "https://registry.npmjs.org"
238
+ end
239
+ # rubocop:enable Metrics/PerceivedComplexity
240
+
241
+ def scoped_registry(scope)
242
+ npmrc_file&.content.to_s.scan(NPM_SCOPED_REGISTRY_REGEX) do
243
+ next if Regexp.last_match[:registry].include?("${") || Regexp.last_match[:scope] != scope
244
+
245
+ return Regexp.last_match[:registry].strip
246
+ end
247
+
248
+ yarnrc_file&.content.to_s.scan(YARN_SCOPED_REGISTRY_REGEX) do
249
+ next if Regexp.last_match[:registry].include?("${") || Regexp.last_match[:scope] != scope
250
+
251
+ return Regexp.last_match[:registry].strip
252
+ end
253
+
254
+ if parsed_yarnrc_yml
255
+ yarn_berry_registry = parsed_yarnrc_yml.dig("npmScopes", scope.delete_prefix("@"), "npmRegistryServer")
256
+ return yarn_berry_registry if yarn_berry_registry
257
+ end
258
+
259
+ global_registry
213
260
  end
214
261
 
215
262
  # npm registries expect slashes to be escaped
@@ -224,6 +271,13 @@ module Dependabot
224
271
 
225
272
  sources.find { |s| s[:type] == "registry" }&.fetch(:url)
226
273
  end
274
+
275
+ def parsed_yarnrc_yml
276
+ return unless yarnrc_yml_file
277
+ return @parsed_yarnrc_yml if defined? @parsed_yarnrc_yml
278
+
279
+ @parsed_yarnrc_yml = YAML.safe_load(yarnrc_yml_file.content)
280
+ end
227
281
  end
228
282
  end
229
283
  end
@@ -13,10 +13,9 @@ module Dependabot
13
13
  module NpmAndYarn
14
14
  class UpdateChecker
15
15
  class RequirementsUpdater
16
- VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/.freeze
17
- SEPARATOR = /(?<=[a-zA-Z0-9*])[\s|]+(?![\s|-])/.freeze
18
- ALLOWED_UPDATE_STRATEGIES =
19
- %i(widen_ranges bump_versions bump_versions_if_necessary).freeze
16
+ VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/
17
+ SEPARATOR = /(?<=[a-zA-Z0-9*])[\s|]+(?![\s|-])/
18
+ ALLOWED_UPDATE_STRATEGIES = %i(widen_ranges bump_versions bump_versions_if_necessary).freeze
20
19
 
21
20
  def initialize(requirements:, updated_source:, update_strategy:,
22
21
  latest_resolvable_version:)
@@ -19,19 +19,21 @@ module Dependabot
19
19
  class UpdateChecker
20
20
  class SubdependencyVersionResolver
21
21
  def initialize(dependency:, credentials:, dependency_files:,
22
- ignored_versions:, latest_allowable_version:)
22
+ ignored_versions:, latest_allowable_version:, repo_contents_path:)
23
23
  @dependency = dependency
24
24
  @credentials = credentials
25
25
  @dependency_files = dependency_files
26
26
  @ignored_versions = ignored_versions
27
27
  @latest_allowable_version = latest_allowable_version
28
+ @repo_contents_path = repo_contents_path
28
29
  end
29
30
 
30
31
  def latest_resolvable_version
31
32
  raise "Not a subdependency!" if dependency.requirements.any?
32
33
  return if bundled_dependency?
33
34
 
34
- SharedHelpers.in_a_temporary_directory do
35
+ base_dir = dependency_files.first.directory
36
+ SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
35
37
  dependency_files_builder.write_temporary_dependency_files
36
38
 
37
39
  updated_lockfiles = filtered_lockfiles.map do |lockfile|
@@ -53,13 +55,15 @@ module Dependabot
53
55
  private
54
56
 
55
57
  attr_reader :dependency, :credentials, :dependency_files,
56
- :ignored_versions, :latest_allowable_version
58
+ :ignored_versions, :latest_allowable_version, :repo_contents_path
57
59
 
58
60
  def update_subdependency_in_lockfile(lockfile)
59
61
  lockfile_name = Pathname.new(lockfile.name).basename.to_s
60
62
  path = Pathname.new(lockfile.name).dirname.to_s
61
63
 
62
- updated_files = if lockfile.name.end_with?("yarn.lock")
64
+ updated_files = if lockfile.name.end_with?("yarn.lock") && Helpers.yarn_berry?(lockfile)
65
+ run_yarn_berry_updater(path, lockfile_name)
66
+ elsif lockfile.name.end_with?("yarn.lock")
63
67
  run_yarn_updater(path, lockfile_name)
64
68
  else
65
69
  run_npm_updater(path, lockfile_name, lockfile.content)
@@ -109,6 +113,17 @@ module Dependabot
109
113
  sleep(rand(3.0..10.0)) && retry
110
114
  end
111
115
 
116
+ def run_yarn_berry_updater(path, lockfile_name)
117
+ SharedHelpers.with_git_configured(credentials: credentials) do
118
+ Dir.chdir(path) do
119
+ Helpers.run_yarn_commands(
120
+ "yarn up -R #{dependency.name} #{Helpers.yarn_berry_args}".strip
121
+ )
122
+ { lockfile_name => File.read(lockfile_name) }
123
+ end
124
+ end
125
+ end
126
+
112
127
  def run_npm_updater(path, lockfile_name, lockfile_content)
113
128
  SharedHelpers.with_git_configured(credentials: credentials) do
114
129
  Dir.chdir(path) do
@@ -36,7 +36,16 @@ module Dependabot
36
36
  "\s>\s(?<requiring_dep>[^"]+)"\s
37
37
  has\s(incorrect|unmet)\speer\sdependency\s
38
38
  "(?<required_dep>[^"]+)"
39
- /x.freeze
39
+ /x
40
+
41
+ # Error message from yarn add:
42
+ # YN0060: │ eve-roster@workspace:. provides jest (p8d618) \
43
+ # with version 29.3.0, which doesn't satisfy \
44
+ # what ts-jest requests\n
45
+ YARN_BERRY_PEER_DEP_ERROR_REGEX =
46
+ /
47
+ YN0060:\s|\s.+\sprovides\s(?<required_dep>.+?)\s\((?<info_hash>\w+)\).+what\s(?<requiring_dep>.+?)\srequests
48
+ /x
40
49
 
41
50
  # Error message from npm install:
42
51
  # react-dom@15.2.0 requires a peer of react@^15.2.0 \
@@ -46,7 +55,7 @@ module Dependabot
46
55
  (?<requiring_dep>[^\s]+)\s
47
56
  requires\sa\speer\sof\s
48
57
  (?<required_dep>.+?)\sbut\snone\sis\sinstalled.
49
- /x.freeze
58
+ /x
50
59
 
51
60
  # Error message from npm install:
52
61
  # npm ERR! Could not resolve dependency:
@@ -59,10 +68,10 @@ module Dependabot
59
68
  /
60
69
  npm\s(?:WARN|ERR!)\sCould\snot\sresolve\sdependency:\n
61
70
  npm\s(?:WARN|ERR!)\speer\s(?<required_dep>\S+@\S+(\s\S+)?)\sfrom\s(?<requiring_dep>\S+@\S+)
62
- /x.freeze
71
+ /x
63
72
 
64
73
  def initialize(dependency:, credentials:, dependency_files:,
65
- latest_allowable_version:, latest_version_finder:)
74
+ latest_allowable_version:, latest_version_finder:, repo_contents_path:)
66
75
  @dependency = dependency
67
76
  @credentials = credentials
68
77
  @dependency_files = dependency_files
@@ -70,6 +79,7 @@ module Dependabot
70
79
 
71
80
  @latest_version_finder = {}
72
81
  @latest_version_finder[dependency] = latest_version_finder
82
+ @repo_contents_path = repo_contents_path
73
83
  end
74
84
 
75
85
  def latest_resolvable_version
@@ -135,7 +145,7 @@ module Dependabot
135
145
  private
136
146
 
137
147
  attr_reader :dependency, :credentials, :dependency_files,
138
- :latest_allowable_version
148
+ :latest_allowable_version, :repo_contents_path
139
149
 
140
150
  def latest_version_finder(dep)
141
151
  @latest_version_finder[dep] ||=
@@ -307,30 +317,15 @@ module Dependabot
307
317
  # TODO: Add all of the error handling that the FileUpdater does
308
318
  # here (since problematic repos will be resolved here before they're
309
319
  # seen by the FileUpdater)
310
- SharedHelpers.in_a_temporary_directory do
320
+ base_dir = dependency_files.first.directory
321
+ SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
311
322
  dependency_files_builder.write_temporary_dependency_files
312
323
 
313
324
  filtered_package_files.flat_map do |file|
314
325
  path = Pathname.new(file.name).dirname
315
326
  run_checker(path: path, version: version)
316
327
  rescue SharedHelpers::HelperSubprocessFailed => e
317
- errors = []
318
- if e.message.match?(NPM6_PEER_DEP_ERROR_REGEX)
319
- e.message.scan(NPM6_PEER_DEP_ERROR_REGEX) do
320
- errors << Regexp.last_match.named_captures
321
- end
322
- elsif e.message.match?(NPM8_PEER_DEP_ERROR_REGEX)
323
- e.message.scan(NPM8_PEER_DEP_ERROR_REGEX) do
324
- errors << Regexp.last_match.named_captures
325
- end
326
- elsif e.message.match?(YARN_PEER_DEP_ERROR_REGEX)
327
- e.message.scan(YARN_PEER_DEP_ERROR_REGEX) do
328
- errors << Regexp.last_match.named_captures
329
- end
330
- else
331
- raise
332
- end
333
- errors
328
+ handle_peer_dependency_errors(e)
334
329
  end.compact
335
330
  end
336
331
  rescue SharedHelpers::HelperSubprocessFailed
@@ -340,6 +335,30 @@ module Dependabot
340
335
  []
341
336
  end
342
337
 
338
+ def handle_peer_dependency_errors(error)
339
+ errors = []
340
+ if error.message.match?(NPM6_PEER_DEP_ERROR_REGEX)
341
+ error.message.scan(NPM6_PEER_DEP_ERROR_REGEX) do
342
+ errors << Regexp.last_match.named_captures
343
+ end
344
+ elsif error.message.match?(NPM8_PEER_DEP_ERROR_REGEX)
345
+ error.message.scan(NPM8_PEER_DEP_ERROR_REGEX) do
346
+ errors << Regexp.last_match.named_captures
347
+ end
348
+ elsif error.message.match?(YARN_PEER_DEP_ERROR_REGEX)
349
+ error.message.scan(YARN_PEER_DEP_ERROR_REGEX) do
350
+ errors << Regexp.last_match.named_captures
351
+ end
352
+ elsif error.message.match?(YARN_BERRY_PEER_DEP_ERROR_REGEX)
353
+ error.message.scan(YARN_BERRY_PEER_DEP_ERROR_REGEX) do
354
+ errors << Regexp.last_match.named_captures
355
+ end
356
+ else
357
+ raise
358
+ end
359
+ errors
360
+ end
361
+
343
362
  def unmet_peer_dependencies
344
363
  peer_dependency_errors.
345
364
  map { |captures| error_details_from_captures(captures) }
@@ -351,13 +370,14 @@ module Dependabot
351
370
  end
352
371
 
353
372
  def error_details_from_captures(captures)
373
+ required_dep_captures = captures.fetch("required_dep")
374
+ requiring_dep_captures = captures.fetch("requiring_dep")
375
+ return {} unless required_dep_captures && requiring_dep_captures
376
+
354
377
  {
355
- requirement_name:
356
- captures.fetch("required_dep").sub(/@[^@]+$/, ""),
357
- requirement_version:
358
- captures.fetch("required_dep").split("@").last.delete('"'),
359
- requiring_dep_name:
360
- captures.fetch("requiring_dep").sub(/@[^@]+$/, "")
378
+ requirement_name: required_dep_captures.sub(/@[^@]+$/, ""),
379
+ requirement_version: required_dep_captures.split("@").last.delete('"'),
380
+ requiring_dep_name: requiring_dep_captures.sub(/@[^@]+$/, "")
361
381
  }
362
382
  end
363
383
 
@@ -468,13 +488,37 @@ module Dependabot
468
488
  def run_checker(path:, version:)
469
489
  # If there are both yarn lockfiles and npm lockfiles only run the
470
490
  # yarn updater
471
- if lockfiles_for_path(lockfiles: dependency_files_builder.yarn_locks, path: path).any?
491
+ lockfiles = lockfiles_for_path(lockfiles: dependency_files_builder.yarn_locks, path: path)
492
+ if lockfiles.any?
493
+ return run_yarn_berry_checker(path: path, version: version) if Helpers.yarn_berry?(lockfiles.first)
494
+
472
495
  return run_yarn_checker(path: path, version: version)
473
496
  end
474
497
 
475
498
  run_npm_checker(path: path, version: version)
476
499
  end
477
500
 
501
+ def run_yarn_berry_checker(path:, version:)
502
+ # This method mimics calling a native helper in order to comply with the caller's expectations
503
+ # Specifically we add the dependency at the specified updated version
504
+ # then check the output of the add command for Peer Dependency errors (Denoted by YN0060)
505
+ # If we find peer dependency issues, we raise HelperSubprocessFailed as
506
+ # the native helpers do.
507
+ SharedHelpers.with_git_configured(credentials: credentials) do
508
+ Dir.chdir(path) do
509
+ output = Helpers.run_yarn_command(
510
+ "yarn add #{dependency.name}@#{version} #{Helpers.yarn_berry_args}".strip
511
+ )
512
+ if output.include?("YN0060")
513
+ raise SharedHelpers::HelperSubprocessFailed.new(
514
+ message: output,
515
+ error_context: {}
516
+ )
517
+ end
518
+ end
519
+ end
520
+ end
521
+
478
522
  def run_yarn_checker(path:, version:)
479
523
  SharedHelpers.with_git_configured(credentials: credentials) do
480
524
  Dir.chdir(path) do
@@ -21,6 +21,7 @@ module Dependabot
21
21
  @allow_removal = allow_removal
22
22
  end
23
23
 
24
+ # rubocop:disable Metrics/MethodLength
24
25
  # Finds any dependencies in the `package-lock.json` or `npm-shrinkwrap.json` that have
25
26
  # a subdependency on the given dependency that is locked to a vuln version range.
26
27
  #
@@ -41,7 +42,10 @@ module Dependabot
41
42
  # dependency on the blocking dependency
42
43
  # * :top_level_ancestors [Array<String>] the names of all top-level dependencies with a transitive
43
44
  # dependency on the dependency
45
+ # * :explanation [String] an explanation for why the project failed the vulnerability auditor run
44
46
  def audit(dependency:, security_advisories:)
47
+ Dependabot.logger.info("VulnerabilityAuditor: starting audit")
48
+
45
49
  fix_unavailable = {
46
50
  "dependency_name" => dependency.name,
47
51
  "fix_available" => false,
@@ -60,7 +64,10 @@ module Dependabot
60
64
  # `npm-shrinkwrap.js`, if present, takes precedence over `package-lock.js`.
61
65
  # Both files use the same format. See https://bit.ly/3lDIAJV for more.
62
66
  lockfile = (dependency_files_builder.shrinkwraps + dependency_files_builder.package_locks).first
63
- return fix_unavailable unless lockfile
67
+ unless lockfile
68
+ Dependabot.logger.info("VulnerabilityAuditor: missing lockfile")
69
+ return fix_unavailable
70
+ end
64
71
 
65
72
  vuln_versions = security_advisories.map do |a|
66
73
  {
@@ -74,25 +81,37 @@ module Dependabot
74
81
  function: "npm:vulnerabilityAuditor",
75
82
  args: [Dir.pwd, vuln_versions]
76
83
  )
77
- return fix_unavailable unless viable_audit_result?(audit_result, security_advisories)
78
84
 
85
+ validation_result = validate_audit_result(audit_result, security_advisories)
86
+ if validation_result != :viable
87
+ Dependabot.logger.info("VulnerabilityAuditor: audit result not viable: #{validation_result}")
88
+ fix_unavailable["explanation"] = explain_fix_unavailable(validation_result, dependency)
89
+ return fix_unavailable
90
+ end
91
+
92
+ Dependabot.logger.info("VulnerabilityAuditor: audit result viable")
79
93
  audit_result
80
94
  end
81
95
  rescue SharedHelpers::HelperSubprocessFailed => e
82
96
  log_helper_subprocess_failure(dependency, e)
83
97
  fix_unavailable
84
98
  end
99
+ # rubocop:enable Metrics/MethodLength
85
100
 
86
101
  private
87
102
 
88
103
  attr_reader :dependency_files, :credentials
89
104
 
90
- def viable_audit_result?(audit_result, security_advisories)
91
- validation_result = validate_audit_result(audit_result, security_advisories)
92
- return true if validation_result == :viable
93
-
94
- Dependabot.logger.info("VulnerabilityAuditor: audit result not viable: #{validation_result}")
95
- false
105
+ def explain_fix_unavailable(validation_result, dependency)
106
+ case validation_result
107
+ when :fix_unavailable, :dependency_still_vulnerable, :downgrades_dependencies
108
+ "No patched version available for #{dependency.name}"
109
+ when :fix_incomplete
110
+ "The lockfile might be out of sync?"
111
+ when :vulnerable_dependency_removed
112
+ "#{dependency.name} was removed in the update. Dependabot is not able to " \
113
+ "deal with this yet, but you can still upgrade manually."
114
+ end
96
115
  end
97
116
 
98
117
  def validate_audit_result(audit_result, security_advisories)
@@ -100,6 +119,7 @@ module Dependabot
100
119
  return :vulnerable_dependency_removed if !@allow_removal && vulnerable_dependency_removed?(audit_result)
101
120
  return :dependency_still_vulnerable if dependency_still_vulnerable?(audit_result, security_advisories)
102
121
  return :downgrades_dependencies if downgrades_dependencies?(audit_result)
122
+ return :fix_incomplete if fix_incomplete?(audit_result)
103
123
 
104
124
  :viable
105
125
  end
@@ -132,6 +152,11 @@ module Dependabot
132
152
  current > target
133
153
  end
134
154
 
155
+ def fix_incomplete?(audit_result)
156
+ audit_result["fix_updates"].any? { |update| !update.key?("target_version") } ||
157
+ audit_result["fix_updates"].empty?
158
+ end
159
+
135
160
  def log_helper_subprocess_failure(dependency, error)
136
161
  # See `Dependabot::SharedHelpers.run_helper_subprocess` for details on error context
137
162
  context = error.error_context || {}