dependabot-npm_and_yarn 0.381.0 → 0.382.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a70ee3c0913d6a9622b867d20d898019ad3453701dd1a18e1c01b62887d52351
4
- data.tar.gz: 80f0ce022e8e6a30f4f72bb7928f0a865abbe05f8868b873d3a84077ab139e02
3
+ metadata.gz: f6b62776c4e4f4808215eb847c97e755f49062eda411ee412828ed1d29569378
4
+ data.tar.gz: 96ce53758c58b319deba6b6611cdff40a9a5673c66b6a0b066eb11410fd5a83d
5
5
  SHA512:
6
- metadata.gz: c56af922ead64141beb7c8fefea2b706d84e90007fd33620f9892fb80d11fcc6e9eb239fa0d0220d0d93b92177de0c780048ccb17dd9ef4ae2409886beb7cda8
7
- data.tar.gz: f3d511e27a4e5ca500c869e0de11729544f4323ea4776c275df99f750b947936b17dad15166571d5515c0e5d524b57eb5c7079876923cebb019a5bf288690612
6
+ metadata.gz: faa9c39442466c97715c626036b1a711217b8e6c4b5dcd1b9438fc63e4009c20acab40a924c52bccf62d56656b3e7f5d3ed79c83e7ebb6a5053407865bd0d883
7
+ data.tar.gz: 3be31e18189e2e9a541fd884cbc91cf4a83e732a887da56b033785539b4f9e0f332f62774548c62487a1b0ac487060f377c9a9cb3138d232fdb33dfff3f15667
@@ -4,6 +4,7 @@
4
4
  require "sorbet-runtime"
5
5
 
6
6
  require "dependabot/npm_and_yarn/file_updater"
7
+ require "dependabot/npm_and_yarn/helpers"
7
8
 
8
9
  module Dependabot
9
10
  module NpmAndYarn
@@ -44,7 +45,7 @@ module Dependabot
44
45
  if npmrc_file then complete_npmrc_from_credentials
45
46
  elsif yarnrc_file then build_npmrc_from_yarnrc
46
47
  else
47
- build_npmrc_content_from_lockfile
48
+ build_npmrc_content_from_lockfile || build_npmrc_content_from_credential_scopes
48
49
  end
49
50
 
50
51
  final_content = initial_content || ""
@@ -98,6 +99,23 @@ module Dependabot
98
99
  "always-auth = true"
99
100
  end
100
101
 
102
+ sig { returns(T.nilable(String)) }
103
+ def build_npmrc_content_from_credential_scopes
104
+ scoped_credentials = registry_credentials.select { |cred| cred.scope && cred["registry"] }
105
+ return if scoped_credentials.empty?
106
+
107
+ lines = T.let([], T::Array[String])
108
+ scoped_credentials.each do |cred|
109
+ registry = cred.fetch("registry")
110
+ registry_url = registry.start_with?("http") ? registry : "https://#{registry}"
111
+ T.must(cred.scope).each do |s|
112
+ lines << "#{Helpers.normalize_npm_scope(s)}:registry=#{registry_url}"
113
+ end
114
+ end
115
+
116
+ lines.join("\n")
117
+ end
118
+
101
119
  sig { returns(T.nilable(String)) }
102
120
  def build_yarnrc_content_from_lockfile
103
121
  return unless yarn_lock || package_lock
@@ -334,11 +352,25 @@ module Dependabot
334
352
  )
335
353
  end
336
354
 
355
+ sig { params(registry: String).returns(T.nilable(T::Array[String])) }
356
+ def credential_scopes_for(registry)
357
+ cred = registry_credentials.find { |c| c["registry"] == registry }
358
+ return unless cred&.scope
359
+
360
+ registry_url = registry.start_with?("http") ? registry : "https://#{registry}"
361
+ T.must(cred.scope).map { |s| "#{Helpers.normalize_npm_scope(s)}:registry=#{registry_url}" }
362
+ end
363
+
337
364
  # rubocop:disable Metrics/PerceivedComplexity
338
365
  sig { params(registry: String).returns(T.nilable(T::Array[String])) }
339
366
  def registry_scopes(registry)
340
367
  # Central registries don't just apply to scopes
341
368
  return if CENTRAL_REGISTRIES.include?(registry)
369
+
370
+ # Use explicit scope from credential if available
371
+ explicit = credential_scopes_for(registry)
372
+ return explicit if explicit
373
+
342
374
  return unless dependency_urls
343
375
 
344
376
  other_regs =
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/npm_and_yarn/helpers"
@@ -6,13 +6,6 @@ require "dependabot/npm_and_yarn/package/registry_finder"
6
6
  require "dependabot/npm_and_yarn/registry_parser"
7
7
  require "dependabot/shared_helpers"
8
8
 
9
- class DependencyRequirement < T::Struct
10
- const :file, String
11
- const :requirement, String
12
- const :groups, T::Array[String]
13
- const :source, T.nilable(T::Hash[Symbol, T.untyped])
14
- end
15
-
16
9
  module Dependabot
17
10
  module NpmAndYarn
18
11
  class FileUpdater
@@ -73,13 +66,13 @@ module Dependabot
73
66
  params(
74
67
  content: String,
75
68
  dependency: Dependabot::Dependency,
76
- old_requirement: DependencyRequirement,
77
- new_requirement: DependencyRequirement
69
+ old_requirement: Dependabot::DependencyRequirement,
70
+ new_requirement: Dependabot::DependencyRequirement
78
71
  ).returns(String)
79
72
  end
80
73
  def replace_version_in_content(content:, dependency:, old_requirement:, new_requirement:)
81
- old_version = old_requirement.requirement
82
- new_version = new_requirement.requirement
74
+ old_version = T.must(old_requirement.requirement)
75
+ new_version = T.must(new_requirement.requirement)
83
76
 
84
77
  pattern = build_replacement_pattern(
85
78
  dependency_name: dependency.name,
@@ -104,37 +97,19 @@ module Dependabot
104
97
  "\\1#{dependency_name}\\1: \\2#{version}\\2"
105
98
  end
106
99
 
107
- sig { params(dependency: Dependabot::Dependency).returns(T::Array[DependencyRequirement]) }
100
+ sig { params(dependency: Dependabot::Dependency).returns(T::Array[Dependabot::DependencyRequirement]) }
108
101
  def new_requirements(dependency)
109
- dependency.requirements
110
- .select { |r| r[:file] == workspace_file.name }
111
- .map do |r|
112
- DependencyRequirement.new(
113
- file: r[:file],
114
- requirement: r[:requirement],
115
- groups: r[:groups],
116
- source: r[:source]
117
- )
118
- end
102
+ dependency.requirements.select { |r| r.file == workspace_file.name }
119
103
  end
120
104
 
121
105
  sig do
122
106
  params(
123
107
  dependency: Dependabot::Dependency,
124
- new_requirement: DependencyRequirement
125
- ).returns(T.nilable(DependencyRequirement))
108
+ new_requirement: Dependabot::DependencyRequirement
109
+ ).returns(T.nilable(Dependabot::DependencyRequirement))
126
110
  end
127
111
  def old_requirement(dependency, new_requirement)
128
- matching_req = T.must(dependency.previous_requirements).find { |r| r[:groups] == new_requirement.groups }
129
-
130
- return nil if matching_req.nil?
131
-
132
- DependencyRequirement.new(
133
- file: matching_req[:file],
134
- requirement: matching_req[:requirement],
135
- groups: matching_req[:groups],
136
- source: matching_req[:source]
137
- )
112
+ T.must(dependency.previous_requirements).find { |r| r.groups == new_requirement.groups }
138
113
  end
139
114
  end
140
115
  end
@@ -634,6 +634,11 @@ module Dependabot
634
634
  def self.corepack_supported_package_manager?(name)
635
635
  SUPPORTED_COREPACK_PACKAGE_MANAGERS.include?(name)
636
636
  end
637
+
638
+ sig { params(scope: String).returns(String) }
639
+ def self.normalize_npm_scope(scope)
640
+ scope.start_with?("@") ? scope : "@#{scope}"
641
+ end
637
642
  end
638
643
  end
639
644
  end
@@ -9,12 +9,14 @@ require "dependabot/metadata_finders"
9
9
  require "dependabot/metadata_finders/base"
10
10
  require "dependabot/registry_client"
11
11
  require "dependabot/npm_and_yarn/package/registry_finder"
12
+ require "dependabot/npm_and_yarn/package/registry_credential_helpers"
12
13
  require "dependabot/npm_and_yarn/version"
13
14
 
14
15
  module Dependabot
15
16
  module NpmAndYarn
16
17
  class MetadataFinder < Dependabot::MetadataFinders::Base
17
18
  extend T::Sig
19
+ include Package::RegistryCredentialHelpers
18
20
 
19
21
  # Lifecycle scripts that run automatically during package installation.
20
22
  # These are security-relevant because they execute with user privileges.
@@ -310,9 +312,11 @@ module Dependabot
310
312
  sig { returns(String) }
311
313
  def dependency_url
312
314
  registry_url =
313
- if new_source.nil?
314
- # Check credentials for a configured registry before falling back to public registry
315
- configured_registry_from_credentials || "https://registry.npmjs.org"
315
+ if (configured_registry = configured_registry_from_credentials)
316
+ # Prioritize replaces-base credential over lockfile source
317
+ configured_registry
318
+ elsif new_source.nil?
319
+ "https://registry.npmjs.org"
316
320
  else
317
321
  new_source&.fetch(:url)
318
322
  end
@@ -332,25 +336,13 @@ module Dependabot
332
336
  { "Authorization" => "Bearer #{auth_token}" }
333
337
  end
334
338
 
335
- sig { returns(T.nilable(String)) }
336
- def configured_registry_from_credentials
337
- # Look for a credential that replaces the base registry (global registry replacement)
338
- replaces_base_cred = credentials.find { |cred| cred["type"] == "npm_registry" && cred.replaces_base? }
339
- return normalize_registry_url(replaces_base_cred["registry"]) if replaces_base_cred
340
-
341
- nil
342
- end
343
-
344
- sig { params(registry: T.nilable(String)).returns(T.nilable(String)) }
345
- def normalize_registry_url(registry)
346
- return nil unless registry
347
- return registry if registry.start_with?("http")
348
-
349
- "https://#{registry}"
350
- end
351
-
352
339
  sig { returns(String) }
353
340
  def dependency_registry
341
+ # Prioritize replaces-base credential over lockfile source
342
+ if (configured_registry = configured_registry_from_credentials)
343
+ return configured_registry.sub(%r{^https?://}, "")
344
+ end
345
+
354
346
  if new_source.nil? then "registry.npmjs.org"
355
347
  else
356
348
  new_source&.fetch(:url)&.gsub("https://", "")&.gsub("http://", "")
@@ -7,12 +7,14 @@ require "time"
7
7
  require "dependabot/package/package_release"
8
8
  require "dependabot/package/package_details"
9
9
  require "dependabot/npm_and_yarn/package/registry_finder"
10
+ require "dependabot/npm_and_yarn/package/registry_credential_helpers"
10
11
 
11
12
  module Dependabot
12
13
  module NpmAndYarn
13
14
  module Package
14
15
  class PackageDetailsFetcher
15
16
  extend T::Sig
17
+ include RegistryCredentialHelpers
16
18
 
17
19
  GLOBAL_REGISTRY = "registry.npmjs.org"
18
20
  NPM_OFFICIAL_WEBSITE = "https://www.npmjs.com"
@@ -63,7 +65,7 @@ module Dependabot
63
65
  sig { returns(Dependabot::Dependency) }
64
66
  attr_reader :dependency
65
67
 
66
- sig { returns(T::Array[Dependabot::Credential]) }
68
+ sig { override.returns(T::Array[Dependabot::Credential]) }
67
69
  attr_reader :credentials
68
70
 
69
71
  sig { returns(T::Array[Dependabot::DependencyFile]) }
@@ -96,6 +98,11 @@ module Dependabot
96
98
 
97
99
  sig { returns(String) }
98
100
  def dependency_url
101
+ if (configured_registry = configured_registry_from_credentials)
102
+ escaped_dependency_name = dependency.name.gsub("/", "%2F")
103
+ return "#{configured_registry}/#{escaped_dependency_name}"
104
+ end
105
+
99
106
  registry_finder.dependency_url
100
107
  end
101
108
 
@@ -381,11 +388,19 @@ module Dependabot
381
388
 
382
389
  sig { returns(T::Hash[String, String]) }
383
390
  def registry_auth_headers
391
+ if (configured_registry = configured_registry_from_credentials)
392
+ return auth_headers_for_registry(configured_registry)
393
+ end
394
+
384
395
  registry_finder.auth_headers
385
396
  end
386
397
 
387
398
  sig { returns(String) }
388
399
  def dependency_registry
400
+ if (configured_registry = configured_registry_from_credentials)
401
+ return configured_registry.sub(%r{^https?://}, "")
402
+ end
403
+
389
404
  registry_finder.registry
390
405
  end
391
406
 
@@ -0,0 +1,64 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "base64"
5
+ require "uri"
6
+
7
+ module Dependabot
8
+ module NpmAndYarn
9
+ module Package
10
+ module RegistryCredentialHelpers
11
+ extend T::Sig
12
+ extend T::Helpers
13
+
14
+ abstract!
15
+
16
+ sig { abstract.returns(T::Array[Dependabot::Credential]) }
17
+ def credentials; end
18
+
19
+ private
20
+
21
+ sig { returns(T.nilable(String)) }
22
+ def configured_registry_from_credentials
23
+ replaces_base_cred = credentials.find { |cred| cred["type"] == "npm_registry" && cred.replaces_base? }
24
+ return unless replaces_base_cred&.fetch("registry", nil)
25
+
26
+ normalize_registry_url(replaces_base_cred["registry"])
27
+ end
28
+
29
+ sig { params(registry: T.nilable(String)).returns(T.nilable(String)) }
30
+ def normalize_registry_url(registry)
31
+ return nil unless registry
32
+
33
+ normalized_registry = registry.start_with?("http") ? registry : "https://#{registry}"
34
+ URI::DEFAULT_PARSER.escape(normalized_registry)&.gsub(%r{/+$}, "")
35
+ end
36
+
37
+ sig { params(registry: String).returns(T::Hash[String, String]) }
38
+ def auth_headers_for_registry(registry)
39
+ token = credentials
40
+ .select { |cred| cred["type"] == "npm_registry" }
41
+ .find { |cred| normalize_registry_url(cred["registry"]) == registry }
42
+ &.fetch("token", nil)
43
+
44
+ return {} unless token
45
+
46
+ auth_header_for(token)
47
+ end
48
+
49
+ sig { params(token: String).returns(T::Hash[String, String]) }
50
+ def auth_header_for(token)
51
+ if token.include?(":")
52
+ encoded_token = Base64.encode64(token).delete("\n")
53
+ { "Authorization" => "Basic #{encoded_token}" }
54
+ elsif Base64.decode64(token).ascii_only? &&
55
+ Base64.decode64(token).include?(":")
56
+ { "Authorization" => "Basic #{token.delete("\n")}" }
57
+ else
58
+ { "Authorization" => "Bearer #{token}" }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "excon"
5
+ require "dependabot/npm_and_yarn/helpers"
5
6
  require "dependabot/npm_and_yarn/update_checker"
6
7
  require "dependabot/registry_client"
7
8
  require "sorbet-runtime"
@@ -60,7 +61,8 @@ module Dependabot
60
61
  def registry
61
62
  return @registry if @registry
62
63
 
63
- @registry = configured_registry || locked_registry || first_registry_with_dependency_details
64
+ @registry = configured_registry || locked_registry || scoped_credential_registry_for_dependency ||
65
+ first_registry_with_dependency_details
64
66
  T.must(@registry)
65
67
  end
66
68
 
@@ -197,19 +199,12 @@ module Dependabot
197
199
  def locked_registry
198
200
  return unless registry_source_url
199
201
 
200
- lockfile_registry =
201
- registry_source_url
202
- &.gsub("https://", "")
203
- &.gsub("http://", "")
202
+ lockfile_registry = registry_source_url&.gsub("https://", "")&.gsub("http://", "")
203
+ return unless lockfile_registry
204
204
 
205
- if lockfile_registry
206
- detailed_registry =
207
- known_registries
208
- .find { |h| h["registry"]&.include?(lockfile_registry) }
209
- &.fetch("registry")
210
- end
211
-
212
- detailed_registry || lockfile_registry
205
+ known_registries
206
+ .find { |h| h["registry"]&.include?(lockfile_registry) }
207
+ &.fetch("registry") || lockfile_registry
213
208
  end
214
209
 
215
210
  sig { returns(T.nilable(String)) }
@@ -288,10 +283,7 @@ module Dependabot
288
283
 
289
284
  sig { returns(String) }
290
285
  def global_registry
291
- return @global_registry if @global_registry
292
-
293
- @global_registry = configured_global_registry || GLOBAL_NPM_REGISTRY
294
- @global_registry
286
+ @global_registry ||= configured_global_registry || GLOBAL_NPM_REGISTRY
295
287
  end
296
288
 
297
289
  # rubocop:disable Metrics/PerceivedComplexity
@@ -342,6 +334,30 @@ module Dependabot
342
334
  nil
343
335
  end
344
336
 
337
+ sig { returns(T.nilable(String)) }
338
+ def scoped_credential_registry_for_dependency
339
+ dep_name = dependency&.name
340
+ return unless dep_name&.start_with?("@") && dep_name.include?("/")
341
+
342
+ scope = T.must(dep_name.split("/").first)
343
+ registry = scoped_credential_registry(scope)
344
+ normalize_configured_registry(registry) if registry
345
+ end
346
+
347
+ sig { params(scope: String).returns(T.nilable(String)) }
348
+ def scoped_credential_registry(scope)
349
+ cred = credentials.find do |c|
350
+ c["type"] == "npm_registry" && c["registry"] && c.scope&.any? do |s|
351
+ Helpers.normalize_npm_scope(s) == scope
352
+ end
353
+ end
354
+ return unless cred
355
+
356
+ reg = T.must(cred["registry"])
357
+ registry = reg.start_with?("http") ? reg : "https://#{reg}"
358
+ prepare_registry_url(registry)
359
+ end
360
+
345
361
  sig do
346
362
  params(
347
363
  file: T.nilable(Dependabot::DependencyFile),
@@ -6,6 +6,8 @@ require "sorbet-runtime"
6
6
  module Dependabot
7
7
  module NpmAndYarn
8
8
  class RegistryParser
9
+ # NOTE: Bun has an equivalent implementation in bun/registry_parser.rb.
10
+ # Keep credential-matching behavior in sync across both ecosystems.
9
11
  extend T::Sig
10
12
 
11
13
  sig { params(resolved_url: String, credentials: T::Array[Dependabot::Credential]).void }
@@ -14,7 +16,7 @@ module Dependabot
14
16
  @credentials = credentials
15
17
  end
16
18
 
17
- sig { params(name: String).returns(T::Hash[Symbol, T.untyped]) }
19
+ sig { params(name: String).returns(T::Hash[Symbol, T.nilable(String)]) }
18
20
  def registry_source_for(name)
19
21
  url =
20
22
  if resolved_url.include?("/~/")
@@ -60,34 +62,68 @@ module Dependabot
60
62
  sig { returns(T::Array[Dependabot::Credential]) }
61
63
  attr_reader :credentials
62
64
 
63
- # rubocop:disable Metrics/PerceivedComplexity
64
65
  sig { returns(T.nilable(String)) }
65
66
  def url_for_relevant_cred
66
- resolved_url_host = URI(resolved_url).host
67
+ resolved_uri = URI(resolved_url)
67
68
 
68
69
  credential_matching_url =
69
70
  credentials
70
71
  .select { |cred| cred["type"] == "npm_registry" && cred["registry"] }
71
72
  .sort_by { |cred| cred.fetch("registry").length }
72
- .find do |details|
73
- next true if resolved_url_host == details["registry"]
74
-
75
- uri = if details["registry"]&.include?("://")
76
- URI(details.fetch("registry"))
77
- else
78
- URI("https://#{details['registry']}")
79
- end
80
- resolved_url_host == uri.host && resolved_url.include?(details.fetch("registry"))
81
- end
73
+ .find { |details| credential_matches?(details, resolved_uri: resolved_uri) }
82
74
 
83
75
  return unless credential_matching_url
84
76
 
85
- # Trim the resolved URL so that it ends at the same point as the
86
- # credential registry
87
77
  reg = credential_matching_url.fetch("registry")
88
- resolved_url.gsub(/#{Regexp.quote(reg)}.*/, "") + reg
78
+ # When the credential registry already includes an explicit scheme, return
79
+ # it directly — the gsub pattern would not match and would produce a
80
+ # malformed string if it ran.
81
+ return reg if reg.include?("://")
82
+
83
+ build_registry_url(registry: reg, resolved_uri: resolved_uri)
84
+ end
85
+
86
+ sig { params(registry: String, resolved_uri: URI::Generic).returns(String) }
87
+ def build_registry_url(registry:, resolved_uri:)
88
+ credential_uri = URI("https://#{registry}")
89
+ normalized_path = credential_uri.path.to_s.chomp("/")
90
+
91
+ "#{resolved_uri.scheme}://#{resolved_uri.authority}#{normalized_path}"
92
+ end
93
+
94
+ # Enforce npm registry credential boundaries by matching on host, optional
95
+ # explicit scheme, and full path segments so sibling paths on the same host
96
+ # cannot inherit credentials configured for a different registry scope.
97
+ sig { params(details: Dependabot::Credential, resolved_uri: URI::Generic).returns(T::Boolean) }
98
+ def credential_matches?(details, resolved_uri:)
99
+ resolved_url_host = resolved_uri.host
100
+ return true if resolved_url_host == details["registry"]
101
+
102
+ registry_has_scheme = details["registry"]&.include?("://")
103
+ uri = if registry_has_scheme
104
+ URI(details.fetch("registry"))
105
+ else
106
+ URI("https://#{details['registry']}")
107
+ end
108
+ return false unless resolved_url_host == uri.host
109
+ # When the credential includes an explicit scheme, require scheme
110
+ # equality so we do not attribute a URL to credentials configured for
111
+ # a different transport protocol.
112
+ return false if registry_has_scheme && resolved_uri.scheme != uri.scheme
113
+
114
+ # Use path-segment-aware matching to prevent credentials configured
115
+ # for one path-scoped registry from being applied to sibling paths
116
+ # on the same host (e.g., /victim-npm should not match /victim-npm-evil).
117
+ credential_path_match?(uri: uri, resolved_url_path: resolved_uri.path.to_s)
118
+ end
119
+
120
+ sig { params(uri: URI::Generic, resolved_url_path: String).returns(T::Boolean) }
121
+ def credential_path_match?(uri:, resolved_url_path:)
122
+ registry_path = uri.path.to_s.chomp("/")
123
+ registry_path.empty? ||
124
+ resolved_url_path.start_with?("#{registry_path}/") ||
125
+ resolved_url_path == registry_path
89
126
  end
90
- # rubocop:enable Metrics/PerceivedComplexity
91
127
  end
92
128
  end
93
129
  end
@@ -68,7 +68,7 @@ module Dependabot
68
68
  end
69
69
  end
70
70
 
71
- sig { params(dep_string: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
71
+ sig { params(dep_string: String).returns(T.nilable(T::Hash[Symbol, T.nilable(String)])) }
72
72
  def self.parse_dep_string(dep_string)
73
73
  stripped = dep_string.strip
74
74
  return nil if stripped.empty?
@@ -106,7 +106,7 @@ module Dependabot
106
106
  validation_result = validate_audit_result(audit_result, security_advisories)
107
107
  if validation_result != :viable
108
108
  Dependabot.logger.info("VulnerabilityAuditor: audit result not viable: #{validation_result}")
109
- fix_unavailable["explanation"] = explain_fix_unavailable(validation_result, dependency)
109
+ fix_unavailable["explanation"] = explain_fix_unavailable(validation_result, dependency, audit_result)
110
110
  return fix_unavailable
111
111
  end
112
112
 
@@ -127,11 +127,27 @@ module Dependabot
127
127
  sig { returns(T::Array[Dependabot::Credential]) }
128
128
  attr_reader :credentials
129
129
 
130
- sig { params(validation_result: Symbol, dependency: Dependabot::Dependency).returns(String) }
131
- def explain_fix_unavailable(validation_result, dependency)
130
+ sig do
131
+ params(
132
+ validation_result: Symbol,
133
+ dependency: Dependabot::Dependency,
134
+ audit_result: T::Hash[String, T.untyped]
135
+ ).returns(String)
136
+ end
137
+ def explain_fix_unavailable(validation_result, dependency, audit_result)
132
138
  case validation_result
133
- when :fix_unavailable, :dependency_still_vulnerable, :downgrades_dependencies
134
- "No patched version available for #{dependency.name}"
139
+ when :fix_unavailable
140
+ fix_unavailable_message(dependency)
141
+ when :dependency_still_vulnerable
142
+ dependency_still_vulnerable_message(dependency, audit_result)
143
+ when :downgrades_dependencies
144
+ downgraded_dependency = audit_result["fix_updates"].find do |update|
145
+ downgrades_version?(update["current_version"], update["target_version"])
146
+ end
147
+
148
+ return downgrades_dependency_message(dependency) unless downgraded_dependency
149
+
150
+ downgraded_dependency_message(dependency, downgraded_dependency)
135
151
  when :fix_incomplete
136
152
  "The lockfile might be out of sync?"
137
153
  else
@@ -193,6 +209,42 @@ module Dependabot
193
209
  current > target
194
210
  end
195
211
 
212
+ sig { params(dependency: Dependabot::Dependency).returns(String) }
213
+ def fix_unavailable_message(dependency)
214
+ "Dependabot could not find a lockfile update that resolves " \
215
+ "#{dependency.name} to a non-vulnerable version."
216
+ end
217
+
218
+ sig do
219
+ params(
220
+ dependency: Dependabot::Dependency,
221
+ audit_result: T::Hash[String, T.untyped]
222
+ ).returns(String)
223
+ end
224
+ def dependency_still_vulnerable_message(dependency, audit_result)
225
+ "A patched version exists for #{dependency.name}, but the available " \
226
+ "update path still resolves it to #{audit_result['target_version']}"
227
+ end
228
+
229
+ sig { params(dependency: Dependabot::Dependency).returns(String) }
230
+ def downgrades_dependency_message(dependency)
231
+ "A patched version exists for #{dependency.name}, but the available " \
232
+ "update path requires downgrading another dependency"
233
+ end
234
+
235
+ sig do
236
+ params(
237
+ dependency: Dependabot::Dependency,
238
+ downgraded_dependency: T::Hash[String, T.untyped]
239
+ ).returns(String)
240
+ end
241
+ def downgraded_dependency_message(dependency, downgraded_dependency)
242
+ "A patched version exists for #{dependency.name}, but the available " \
243
+ "update path would downgrade #{downgraded_dependency['dependency_name']} " \
244
+ "from #{downgraded_dependency['current_version']} to " \
245
+ "#{downgraded_dependency['target_version']}"
246
+ end
247
+
196
248
  sig { params(audit_result: T::Hash[String, T.untyped]).returns(T::Boolean) }
197
249
  def fix_incomplete?(audit_result)
198
250
  audit_result["fix_updates"].any? { |update| !update.key?("target_version") } ||
@@ -51,7 +51,7 @@ module Dependabot
51
51
  )
52
52
  @latest_version = T.let(nil, T.nilable(T.any(String, Gem::Version)))
53
53
  @latest_resolvable_version = T.let(nil, T.nilable(T.any(String, Dependabot::Version)))
54
- @updated_requirements = T.let(nil, T.nilable(T::Array[T::Hash[Symbol, T.untyped]]))
54
+ @updated_requirements = T.let(nil, T.nilable(T::Array[Dependabot::DependencyRequirement]))
55
55
  @vulnerability_audit = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
56
56
  @vulnerable_versions = T.let(nil, T.nilable(T::Array[T.any(String, Gem::Version)]))
57
57
 
@@ -162,7 +162,7 @@ module Dependabot
162
162
  T.unsafe(version_resolver.latest_resolvable_previous_version(updated_version))
163
163
  end
164
164
 
165
- sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
165
+ sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
166
166
  def updated_requirements
167
167
  resolvable_version =
168
168
  if preferred_resolvable_version.is_a?(version_class)
@@ -177,12 +177,14 @@ module Dependabot
177
177
  end
178
178
 
179
179
  @updated_requirements ||=
180
- RequirementsUpdater.new(
181
- requirements: dependency.requirements,
182
- updated_source: updated_source,
183
- latest_resolvable_version: resolvable_version,
184
- update_strategy: T.must(requirements_update_strategy)
185
- ).updated_requirements
180
+ wrap_requirements(
181
+ RequirementsUpdater.new(
182
+ requirements: dependency.requirements,
183
+ updated_source: updated_source,
184
+ latest_resolvable_version: resolvable_version,
185
+ update_strategy: T.must(requirements_update_strategy)
186
+ ).updated_requirements
187
+ )
186
188
  end
187
189
 
188
190
  sig { returns(T::Boolean) }
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.381.0
4
+ version: 0.382.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.381.0
18
+ version: 0.382.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.381.0
25
+ version: 0.382.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -349,6 +349,7 @@ files:
349
349
  - lib/dependabot/npm_and_yarn/native_helpers.rb
350
350
  - lib/dependabot/npm_and_yarn/npm_package_manager.rb
351
351
  - lib/dependabot/npm_and_yarn/package/package_details_fetcher.rb
352
+ - lib/dependabot/npm_and_yarn/package/registry_credential_helpers.rb
352
353
  - lib/dependabot/npm_and_yarn/package/registry_finder.rb
353
354
  - lib/dependabot/npm_and_yarn/package_manager.rb
354
355
  - lib/dependabot/npm_and_yarn/package_name.rb
@@ -374,7 +375,7 @@ licenses:
374
375
  - MIT
375
376
  metadata:
376
377
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
377
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.381.0
378
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.382.0
378
379
  rdoc_options: []
379
380
  require_paths:
380
381
  - lib