dependabot-npm_and_yarn 0.212.0 → 0.213.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) 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 +2585 -2386
  6. data/helpers/package.json +4 -4
  7. data/lib/dependabot/npm_and_yarn/file_fetcher.rb +30 -5
  8. data/lib/dependabot/npm_and_yarn/file_parser/lockfile_parser.rb +19 -4
  9. data/lib/dependabot/npm_and_yarn/file_parser.rb +17 -5
  10. data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +35 -21
  11. data/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb +7 -3
  12. data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +2 -2
  13. data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +83 -24
  14. data/lib/dependabot/npm_and_yarn/file_updater.rb +54 -0
  15. data/lib/dependabot/npm_and_yarn/helpers.rb +48 -0
  16. data/lib/dependabot/npm_and_yarn/package_name.rb +2 -2
  17. data/lib/dependabot/npm_and_yarn/requirement.rb +3 -3
  18. data/lib/dependabot/npm_and_yarn/update_checker/latest_version_finder.rb +6 -1
  19. data/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb +16 -3
  20. data/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb +67 -19
  21. data/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb +3 -4
  22. data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +23 -1
  23. data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +3 -3
  24. data/lib/dependabot/npm_and_yarn/update_checker/vulnerability_auditor.rb +33 -8
  25. data/lib/dependabot/npm_and_yarn/update_checker.rb +72 -19
  26. data/lib/dependabot/npm_and_yarn/version.rb +1 -1
  27. metadata +13 -55
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dependabot/experiments"
3
4
  require "dependabot/file_updaters"
4
5
  require "dependabot/file_updaters/base"
6
+ require "dependabot/file_updaters/vendor_updater"
5
7
  require "dependabot/npm_and_yarn/dependency_files_filterer"
6
8
  require "dependabot/npm_and_yarn/sub_dependency_files_filterer"
7
9
 
@@ -53,11 +55,62 @@ module Dependabot
53
55
  )
54
56
  end
55
57
 
58
+ base_dir = updated_files.first.directory
59
+ vendor_updater.updated_vendor_cache_files(base_directory: base_dir).each { |file| updated_files << file }
60
+ install_state_updater.updated_vendor_cache_files(base_directory: base_dir).each do |file|
61
+ updated_files << file
62
+ end
63
+ pnp_updater.updated_vendor_cache_files(base_directory: base_dir).each do |file|
64
+ updated_files << file if file.name == ".pnp.cjs" || file.name == ".pnp.data.json"
65
+ end
66
+
56
67
  updated_files
57
68
  end
58
69
 
59
70
  private
60
71
 
72
+ # Dynamically fetch the vendor cache folder from yarn
73
+ def vendor_cache_dir
74
+ return @vendor_cache_dir if defined?(@vendor_cache_dir)
75
+
76
+ @vendor_cache_dir = if File.exist?(".yarnrc.yml")
77
+ YAML.load_file(".yarnrc.yml").fetch("cacheFolder", "./.yarn/cache")
78
+ else
79
+ "./.yarn/cache"
80
+ end
81
+ end
82
+
83
+ def install_state_path
84
+ return @install_state_path if defined?(@install_state_path)
85
+
86
+ @install_state_path = if File.exist?(".yarnrc.yml")
87
+ YAML.load_file(".yarnrc.yml").fetch("installStatePath", "./.yarn/install-state.gz")
88
+ else
89
+ "./.yarn/install-state.gz"
90
+ end
91
+ end
92
+
93
+ def vendor_updater
94
+ Dependabot::FileUpdaters::VendorUpdater.new(
95
+ repo_contents_path: repo_contents_path,
96
+ vendor_dir: vendor_cache_dir
97
+ )
98
+ end
99
+
100
+ def install_state_updater
101
+ Dependabot::FileUpdaters::VendorUpdater.new(
102
+ repo_contents_path: repo_contents_path,
103
+ vendor_dir: install_state_path
104
+ )
105
+ end
106
+
107
+ def pnp_updater
108
+ Dependabot::FileUpdaters::VendorUpdater.new(
109
+ repo_contents_path: repo_contents_path,
110
+ vendor_dir: "./"
111
+ )
112
+ end
113
+
61
114
  def filtered_dependency_files
62
115
  @filtered_dependency_files ||=
63
116
  if dependencies.select(&:top_level?).any?
@@ -175,6 +228,7 @@ module Dependabot
175
228
  YarnLockfileUpdater.new(
176
229
  dependencies: dependencies,
177
230
  dependency_files: dependency_files,
231
+ repo_contents_path: repo_contents_path,
178
232
  credentials: credentials
179
233
  )
180
234
  end
@@ -15,6 +15,54 @@ module Dependabot
15
15
  rescue JSON::ParserError
16
16
  6
17
17
  end
18
+
19
+ # Run any number of yarn commands while ensuring that `enableScripts` is
20
+ # set to false. Yarn commands should _not_ be ran outside of this helper
21
+ # to ensure that postinstall scripts are never executed, as they could
22
+ # contain malicious code.
23
+ def self.run_yarn_commands(*commands)
24
+ # We never want to execute postinstall scripts
25
+ SharedHelpers.run_shell_command("yarn config set enableScripts false")
26
+ if (http_proxy = ENV.fetch("HTTP_PROXY", false))
27
+ SharedHelpers.run_shell_command("yarn config set httpProxy #{http_proxy}")
28
+ end
29
+ if (https_proxy = ENV.fetch("HTTPS_PROXY", false))
30
+ SharedHelpers.run_shell_command("yarn config set httpsProxy #{https_proxy}")
31
+ end
32
+ if (ca_file_path = ENV.fetch("NODE_EXTRA_CA_CERTS", false))
33
+ output = SharedHelpers.run_shell_command("yarn --version")
34
+ major_version = Version.new(output).major
35
+ if major_version >= 4
36
+ SharedHelpers.run_shell_command("yarn config set httpsCaFilePath #{ca_file_path}")
37
+ else
38
+ SharedHelpers.run_shell_command("yarn config set caFilePath #{ca_file_path}")
39
+ end
40
+ end
41
+ commands.each { |cmd| SharedHelpers.run_shell_command(cmd) }
42
+ end
43
+
44
+ def self.dependencies_with_all_versions_metadata(dependency_set)
45
+ working_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
46
+ dependencies = []
47
+
48
+ names = dependency_set.dependencies.map(&:name)
49
+ names.each do |name|
50
+ all_versions = dependency_set.all_versions_for_name(name)
51
+ all_versions.each do |dep|
52
+ metadata_versions = dep.metadata.fetch(:all_versions, [])
53
+ if metadata_versions.any?
54
+ metadata_versions.each { |a| working_set << a }
55
+ else
56
+ working_set << dep
57
+ end
58
+ end
59
+ dependency = working_set.dependency_for_name(name)
60
+ dependency.metadata[:all_versions] = working_set.all_versions_for_name(name)
61
+ dependencies << dependency
62
+ end
63
+
64
+ dependencies
65
+ end
18
66
  end
19
67
  end
20
68
  end
@@ -18,7 +18,7 @@ module Dependabot
18
18
  [a-z0-9\-\_\.\!\~\*\'\(\)]+ # URL-safe characters
19
19
  )
20
20
  \z # end of string
21
- }xi.freeze # multi-line/case-insensitive
21
+ }xi # multi-line/case-insensitive
22
22
 
23
23
  TYPES_PACKAGE_NAME_REGEX = %r{
24
24
  \A # beginning of string
@@ -26,7 +26,7 @@ module Dependabot
26
26
  ((?<scope>.+)__)? # capture scope
27
27
  (?<name>.+) # capture name
28
28
  \z # end of string
29
- }xi.freeze # multi-line/case-insensitive
29
+ }xi # multi-line/case-insensitive
30
30
 
31
31
  class InvalidPackageName < StandardError; end
32
32
 
@@ -6,8 +6,8 @@ require "dependabot/npm_and_yarn/version"
6
6
  module Dependabot
7
7
  module NpmAndYarn
8
8
  class Requirement < Gem::Requirement
9
- AND_SEPARATOR = /(?<=[a-zA-Z0-9*])\s+(?:&+\s+)?(?!\s*[|-])/.freeze
10
- OR_SEPARATOR = /(?<=[a-zA-Z0-9*])\s*\|+/.freeze
9
+ AND_SEPARATOR = /(?<=[a-zA-Z0-9*])\s+(?:&+\s+)?(?!\s*[|-])/
10
+ OR_SEPARATOR = /(?<=[a-zA-Z0-9*])\s*\|+/
11
11
  LATEST_REQUIREMENT = "latest"
12
12
 
13
13
  # Override the version pattern to allow a 'v' prefix
@@ -15,7 +15,7 @@ module Dependabot
15
15
  version_pattern = "v?#{NpmAndYarn::Version::VERSION_PATTERN}"
16
16
 
17
17
  PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{version_pattern})\\s*"
18
- PATTERN = /\A#{PATTERN_RAW}\z/.freeze
18
+ PATTERN = /\A#{PATTERN_RAW}\z/
19
19
 
20
20
  def self.parse(obj)
21
21
  return ["=", nil] if obj.is_a?(String) && obj.strip == LATEST_REQUIREMENT
@@ -371,7 +371,8 @@ module Dependabot
371
371
  dependency: dependency,
372
372
  credentials: credentials,
373
373
  npmrc_file: npmrc_file,
374
- yarnrc_file: yarnrc_file
374
+ yarnrc_file: yarnrc_file,
375
+ yarnrc_yml_file: yarnrc_yml_file
375
376
  )
376
377
  end
377
378
 
@@ -395,6 +396,10 @@ module Dependabot
395
396
  dependency_files.find { |f| f.name.end_with?(".yarnrc") }
396
397
  end
397
398
 
399
+ def yarnrc_yml_file
400
+ dependency_files.find { |f| f.name.end_with?(".yarnrc.yml") }
401
+ end
402
+
398
403
  # TODO: Remove need for me
399
404
  def git_dependency?
400
405
  # ignored_version/raise_on_ignored are irrelevant.
@@ -8,8 +8,10 @@ module Dependabot
8
8
  module NpmAndYarn
9
9
  class UpdateChecker
10
10
  class LibraryDetector
11
- def initialize(package_json_file:)
11
+ def initialize(package_json_file:, credentials:, dependency_files:)
12
12
  @package_json_file = package_json_file
13
+ @credentials = credentials
14
+ @dependency_files = dependency_files
13
15
  end
14
16
 
15
17
  def library?
@@ -20,7 +22,7 @@ module Dependabot
20
22
 
21
23
  private
22
24
 
23
- attr_reader :package_json_file
25
+ attr_reader :package_json_file, :credentials, :dependency_files
24
26
 
25
27
  def package_json_may_be_for_library?
26
28
  return false unless project_name
@@ -36,7 +38,8 @@ module Dependabot
36
38
  return false unless project_description
37
39
 
38
40
  # Check if the project is listed on npm. If it is, it's a library
39
- @project_npm_response ||= Dependabot::RegistryClient.get(url: "https://registry.npmjs.org/#{escaped_project_name}")
41
+ url = "#{registry.chomp('/')}/#{escaped_project_name}"
42
+ @project_npm_response ||= Dependabot::RegistryClient.get(url: url)
40
43
  return false unless @project_npm_response.status == 200
41
44
 
42
45
  @project_npm_response.body.force_encoding("UTF-8").encode.
@@ -56,6 +59,16 @@ module Dependabot
56
59
  def parsed_package_json
57
60
  @parsed_package_json ||= JSON.parse(package_json_file.content)
58
61
  end
62
+
63
+ def registry
64
+ NpmAndYarn::UpdateChecker::RegistryFinder.new(
65
+ dependency: nil,
66
+ credentials: credentials,
67
+ npmrc_file: dependency_files.find { |f| f.name.end_with?(".npmrc") },
68
+ yarnrc_file: dependency_files.find { |f| f.name.end_with?(".yarnrc") },
69
+ yarnrc_yml_file: dependency_files.find { |f| f.name.end_with?(".yarnrc.yml") }
70
+ ).registry_from_rc(project_name)
71
+ end
59
72
  end
60
73
  end
61
74
  end
@@ -13,19 +13,19 @@ module Dependabot
13
13
  http://registry.npmjs.org
14
14
  https://registry.yarnpkg.com
15
15
  ).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
16
+ NPM_AUTH_TOKEN_REGEX = %r{//(?<registry>.*)/:_authToken=(?<token>.*)$}
17
+ NPM_GLOBAL_REGISTRY_REGEX = /^registry\s*=\s*['"]?(?<registry>.*?)['"]?$/
18
+ YARN_GLOBAL_REGISTRY_REGEX = /^(?:--)?registry\s+((['"](?<registry>.*)['"])|(?<registry>.*))/
19
+ NPM_SCOPED_REGISTRY_REGEX = /^(?<scope>@[^:]+)\s*:registry\s*=\s*['"]?(?<registry>.*?)['"]?$/
20
+ YARN_SCOPED_REGISTRY_REGEX = /['"](?<scope>@[^:]+):registry['"]\s((['"](?<registry>.*)['"])|(?<registry>.*))/
22
21
 
23
22
  def initialize(dependency:, credentials:, npmrc_file: nil,
24
- yarnrc_file: nil)
23
+ yarnrc_file: nil, yarnrc_yml_file: nil)
25
24
  @dependency = dependency
26
25
  @credentials = credentials
27
26
  @npmrc_file = npmrc_file
28
27
  @yarnrc_file = yarnrc_file
28
+ @yarnrc_yml_file = yarnrc_yml_file
29
29
  end
30
30
 
31
31
  def registry
@@ -46,15 +46,24 @@ module Dependabot
46
46
  end
47
47
  end
48
48
 
49
+ def registry_from_rc(dependency_name)
50
+ return global_registry unless dependency_name.start_with?("@") && dependency_name.include?("/")
51
+
52
+ scope = dependency_name.split("/").first
53
+ scoped_registry(scope)
54
+ end
55
+
49
56
  private
50
57
 
51
- attr_reader :dependency, :credentials, :npmrc_file, :yarnrc_file
58
+ attr_reader :dependency, :credentials, :npmrc_file, :yarnrc_file, :yarnrc_yml_file
52
59
 
53
60
  def first_registry_with_dependency_details
54
61
  @first_registry_with_dependency_details ||=
55
62
  known_registries.find do |details|
63
+ url = "#{details['registry'].gsub(%r{/+$}, '')}/#{escaped_dependency_name}"
64
+ url = "https://#{url}" unless url.start_with?("http")
56
65
  response = Dependabot::RegistryClient.get(
57
- url: "https://#{details['registry'].gsub(%r{/+$}, '')}/#{escaped_dependency_name}",
66
+ url: url,
58
67
  headers: auth_header_for(details["token"])
59
68
  )
60
69
  response.status < 400 && JSON.parse(response.body)
@@ -64,10 +73,12 @@ module Dependabot
64
73
  nil
65
74
  end&.fetch("registry")
66
75
 
67
- @first_registry_with_dependency_details ||= global_registry
76
+ @first_registry_with_dependency_details ||= global_registry.sub(%r{/+$}, "").sub(%r{^.*?//}, "")
68
77
  end
69
78
 
70
79
  def registry_url
80
+ return registry if registry.start_with?("http")
81
+
71
82
  protocol =
72
83
  if registry_source_url
73
84
  registry_source_url.split("://").first
@@ -190,26 +201,56 @@ module Dependabot
190
201
  end
191
202
  end
192
203
 
204
+ # rubocop:disable Metrics/PerceivedComplexity
193
205
  def global_registry
206
+ return @global_registry if defined? @global_registry
207
+
194
208
  npmrc_file&.content.to_s.scan(NPM_GLOBAL_REGISTRY_REGEX) do
195
209
  next if Regexp.last_match[:registry].include?("${")
196
210
 
197
- registry = Regexp.last_match[:registry].strip.
198
- sub(%r{/+$}, "").
199
- sub(%r{^.*?//}, "")
200
- return registry
211
+ return @global_registry = Regexp.last_match[:registry].strip
201
212
  end
202
213
 
203
214
  yarnrc_file&.content.to_s.scan(YARN_GLOBAL_REGISTRY_REGEX) do
204
215
  next if Regexp.last_match[:registry].include?("${")
205
216
 
206
- registry = Regexp.last_match[:registry].strip.
207
- sub(%r{/+$}, "").
208
- sub(%r{^.*?//}, "")
209
- return registry
217
+ return @global_registry = Regexp.last_match[:registry].strip
218
+ end
219
+
220
+ if parsed_yarnrc_yml&.key?("npmRegistryServer")
221
+ return @global_registry = parsed_yarnrc_yml["npmRegistryServer"]
222
+ end
223
+
224
+ replaces_base = credentials.find { |cred| cred["type"] == "npm_registry" && cred["replaces-base"] == true }
225
+ if replaces_base
226
+ registry = replaces_base["registry"]
227
+ registry = "https://#{registry}" unless registry.start_with?("http")
228
+ return @global_registry = registry
229
+ end
230
+
231
+ "https://registry.npmjs.org"
232
+ end
233
+ # rubocop:enable Metrics/PerceivedComplexity
234
+
235
+ def scoped_registry(scope)
236
+ npmrc_file&.content.to_s.scan(NPM_SCOPED_REGISTRY_REGEX) do
237
+ next if Regexp.last_match[:registry].include?("${") || Regexp.last_match[:scope] != scope
238
+
239
+ return Regexp.last_match[:registry].strip
240
+ end
241
+
242
+ yarnrc_file&.content.to_s.scan(YARN_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
+ if parsed_yarnrc_yml
249
+ yarn_berry_registry = parsed_yarnrc_yml.dig("npmScopes", scope.delete_prefix("@"), "npmRegistryServer")
250
+ return yarn_berry_registry if yarn_berry_registry
210
251
  end
211
252
 
212
- "registry.npmjs.org"
253
+ global_registry
213
254
  end
214
255
 
215
256
  # npm registries expect slashes to be escaped
@@ -224,6 +265,13 @@ module Dependabot
224
265
 
225
266
  sources.find { |s| s[:type] == "registry" }&.fetch(:url)
226
267
  end
268
+
269
+ def parsed_yarnrc_yml
270
+ return unless yarnrc_yml_file
271
+ return @parsed_yarnrc_yml if defined? @parsed_yarnrc_yml
272
+
273
+ @parsed_yarnrc_yml = YAML.safe_load(yarnrc_yml_file.content)
274
+ end
227
275
  end
228
276
  end
229
277
  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:)
@@ -59,7 +59,9 @@ module Dependabot
59
59
  lockfile_name = Pathname.new(lockfile.name).basename.to_s
60
60
  path = Pathname.new(lockfile.name).dirname.to_s
61
61
 
62
- updated_files = if lockfile.name.end_with?("yarn.lock")
62
+ updated_files = if lockfile.name.end_with?("yarn.lock") && yarn_berry?(lockfile)
63
+ run_yarn_berry_updater(path, lockfile_name)
64
+ elsif lockfile.name.end_with?("yarn.lock")
63
65
  run_yarn_updater(path, lockfile_name)
64
66
  else
65
67
  run_npm_updater(path, lockfile_name, lockfile.content)
@@ -68,6 +70,15 @@ module Dependabot
68
70
  updated_files.fetch(lockfile_name)
69
71
  end
70
72
 
73
+ def yarn_berry?(yarn_lock)
74
+ return false unless Experiments.enabled?(:yarn_berry)
75
+
76
+ yaml = YAML.safe_load(yarn_lock.content)
77
+ yaml.key?("__metadata")
78
+ rescue StandardError
79
+ false
80
+ end
81
+
71
82
  def version_from_updated_lockfiles(updated_lockfiles)
72
83
  updated_files = dependency_files -
73
84
  dependency_files_builder.yarn_locks -
@@ -109,6 +120,17 @@ module Dependabot
109
120
  sleep(rand(3.0..10.0)) && retry
110
121
  end
111
122
 
123
+ def run_yarn_berry_updater(path, lockfile_name)
124
+ SharedHelpers.with_git_configured(credentials: credentials) do
125
+ Dir.chdir(path) do
126
+ Helpers.run_yarn_commands(
127
+ "yarn up -R #{dependency.name}"
128
+ )
129
+ { lockfile_name => File.read(lockfile_name) }
130
+ end
131
+ end
132
+ end
133
+
112
134
  def run_npm_updater(path, lockfile_name, lockfile_content)
113
135
  SharedHelpers.with_git_configured(credentials: credentials) do
114
136
  Dir.chdir(path) do
@@ -36,7 +36,7 @@ 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
40
 
41
41
  # Error message from npm install:
42
42
  # react-dom@15.2.0 requires a peer of react@^15.2.0 \
@@ -46,7 +46,7 @@ module Dependabot
46
46
  (?<requiring_dep>[^\s]+)\s
47
47
  requires\sa\speer\sof\s
48
48
  (?<required_dep>.+?)\sbut\snone\sis\sinstalled.
49
- /x.freeze
49
+ /x
50
50
 
51
51
  # Error message from npm install:
52
52
  # npm ERR! Could not resolve dependency:
@@ -59,7 +59,7 @@ module Dependabot
59
59
  /
60
60
  npm\s(?:WARN|ERR!)\sCould\snot\sresolve\sdependency:\n
61
61
  npm\s(?:WARN|ERR!)\speer\s(?<required_dep>\S+@\S+(\s\S+)?)\sfrom\s(?<requiring_dep>\S+@\S+)
62
- /x.freeze
62
+ /x
63
63
 
64
64
  def initialize(dependency:, credentials:, dependency_files:,
65
65
  latest_allowable_version:, latest_version_finder:)
@@ -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 || {}