dependabot-maven 0.402.0 → 0.404.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: 224f25c480c24fa9c2a118fa908fd4e7d24990e1890e9b7e40cda5eb20bf6d4f
4
- data.tar.gz: 0d093a3b4689ba1f54f7e6ef9f97c3a10288252a63ae2da3c122d3f30df30a1a
3
+ metadata.gz: 882f62fae9fb3288a00474757b26ab2d3d69c4564b5c57fb1934f95e4064a901
4
+ data.tar.gz: cf2b50d9b0fcf12cb1215a37fbf51244a497c2225f57a0508e772004d8e3648b
5
5
  SHA512:
6
- metadata.gz: af889347469fe6e233e51f8c5c7ff9d829534c887200a12aa76bfb64c897eb94026050559a7d0de7db4c41ae1bd9e8c759e41c61cd1db8a4195591ed0a78a19b
7
- data.tar.gz: 071b060c8356276b51e441500e4318b055560b44c5a28b13d1c6e7d43154009aff748cfb56bf4fd39642c4a0888f763aab452ae23c7bb8ea5f8182137e3a5d1a
6
+ metadata.gz: 0ecba86abf4fa974cdc7a096fac2b0df7385f90456502d283f92616f4ecd82f883a93896220f0da2b3df42033ef95b7a0014c972f834485e2444b1d79993f307
7
+ data.tar.gz: 84fd19d904f5785f35a9f676fb9aa9d46cabe7d1480828d510153be50786669db50af500ddfc5c8895996735d6008394839f803d590e6c22331d7d702c780fb5
@@ -199,6 +199,8 @@ module Dependabot
199
199
  end
200
200
  private_class_method :node_string
201
201
 
202
+ # The classifier names which XML entry this is (unclassified means the plain artifact), so
203
+ # it's never taken from the scan, which matches artifacts by name only.
202
204
  sig do
203
205
  params(
204
206
  declared: Dependabot::DependencyRequirement,
@@ -207,7 +209,8 @@ module Dependabot
207
209
  end
208
210
  def self.merge_requirement_pair(declared, scanned)
209
211
  declared_metadata = declared.metadata || {}
210
- metadata = (scanned.metadata || {}).merge(declared_metadata) { |_key, scan, xml| xml.nil? ? scan : xml }
212
+ scanned_metadata = (scanned.metadata || {}).except(:classifier, "classifier")
213
+ metadata = scanned_metadata.merge(declared_metadata) { |_key, scan, xml| xml.nil? ? scan : xml }
211
214
 
212
215
  Dependabot::DependencyRequirement.create(
213
216
  requirement: declared.requirement,
@@ -0,0 +1,208 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "nokogiri"
5
+ require "sorbet-runtime"
6
+
7
+ require "dependabot/dependency_file"
8
+ require "dependabot/maven/file_parser"
9
+
10
+ module Dependabot
11
+ module Maven
12
+ class FileParser
13
+ # Decides whether a `<dependencyManagement>` entry overrides a transitive version.
14
+ #
15
+ # An entry is an override when no POM in its local lineage (itself, its local
16
+ # parents and its local children) declares the artifact in a `<dependencies>`
17
+ # section. Parents count because their dependencies are inherited; sibling
18
+ # modules don't, as the entry doesn't manage their declarations. A local BOM,
19
+ # and every local POM inheriting from it, also manages the lineage of each
20
+ # local POM that imports it.
21
+ class TransitiveOverrideFinder
22
+ extend T::Sig
23
+
24
+ sig do
25
+ params(
26
+ pomfiles: T::Array[Dependabot::DependencyFile],
27
+ declared_keys_by_pom: T::Hash[String, T::Set[String]],
28
+ property_value_finder: PropertyValueFinder
29
+ ).void
30
+ end
31
+ def initialize(pomfiles:, declared_keys_by_pom:, property_value_finder:)
32
+ @pomfiles = pomfiles
33
+ @declared_keys_by_pom = declared_keys_by_pom
34
+ @property_value_finder = property_value_finder
35
+ @lineages = T.let({}, T::Hash[String, T::Array[String]])
36
+ @documents = T.let({}, T::Hash[String, Nokogiri::XML::Document])
37
+ end
38
+
39
+ # `key` is the entry's groupId:artifactId:type:classifier management key.
40
+ sig { params(pom: Dependabot::DependencyFile, key: String).returns(T::Boolean) }
41
+ def override?(pom:, key:)
42
+ lineage(pom.name).none? { |pom_name| @declared_keys_by_pom.fetch(pom_name, Set.new).include?(key) }
43
+ end
44
+
45
+ private
46
+
47
+ sig { params(pom_name: String).returns(T::Array[String]) }
48
+ def lineage(pom_name)
49
+ @lineages[pom_name] ||= begin
50
+ found = T.let([], T::Array[String])
51
+ visited = T.let([], T::Array[String])
52
+ queue = [pom_name]
53
+ while (current = queue.shift)
54
+ next if visited.include?(current)
55
+
56
+ visited << current
57
+ found.concat(inheritance_line(current)).uniq!
58
+ # Importing a BOM also imports what it inherits, so importers of its children count too.
59
+ ([current] + descendants(current)).each do |bom|
60
+ queue.concat(local_importer_names.fetch(bom, []))
61
+ end
62
+ end
63
+ found
64
+ end
65
+ end
66
+
67
+ sig { params(pom_name: String).returns(T::Array[String]) }
68
+ def inheritance_line(pom_name)
69
+ ancestors(pom_name) + [pom_name] + descendants(pom_name)
70
+ end
71
+
72
+ # Maps each local BOM file name to the file names of the local POMs that import it.
73
+ sig { returns(T::Hash[String, T::Array[String]]) }
74
+ def local_importer_names
75
+ @local_importer_names ||= T.let(
76
+ @pomfiles.each_with_object({}) do |pom, importers|
77
+ imported_coordinates(pom).each do |coordinates|
78
+ bom = coordinates_to_pom_name[coordinates]
79
+ next if bom.nil? || bom == pom.name
80
+
81
+ (importers[bom] ||= []) << pom.name
82
+ end
83
+ end,
84
+ T.nilable(T::Hash[String, T::Array[String]])
85
+ )
86
+ end
87
+
88
+ sig { params(pom: Dependabot::DependencyFile).returns(T::Array[String]) }
89
+ def imported_coordinates(pom)
90
+ document(pom).xpath("//dependencyManagement/dependencies/dependency").filter_map do |node|
91
+ import_coordinates(node, pom)
92
+ end
93
+ end
94
+
95
+ sig { params(node: Nokogiri::XML::Element, pom: Dependabot::DependencyFile).returns(T.nilable(String)) }
96
+ def import_coordinates(node, pom)
97
+ return unless child_value(node, "scope", pom) == "import"
98
+
99
+ group_id = child_value(node, "groupId", pom)
100
+ artifact_id = child_value(node, "artifactId", pom)
101
+ return unless group_id && artifact_id
102
+
103
+ "#{group_id}:#{artifact_id}"
104
+ end
105
+
106
+ sig do
107
+ params(node: Nokogiri::XML::Element, name: String, pom: Dependabot::DependencyFile).returns(T.nilable(String))
108
+ end
109
+ def child_value(node, name, pom)
110
+ resolved_text(node.at_xpath("./#{name}"), pom)
111
+ end
112
+
113
+ sig { params(node: T.nilable(Nokogiri::XML::Node), pom: Dependabot::DependencyFile).returns(T.nilable(String)) }
114
+ def resolved_text(node, pom)
115
+ value = T.let(node&.content&.strip, T.nilable(String))
116
+ return value unless value&.match?(PROPERTY_REGEX)
117
+
118
+ value.gsub(PROPERTY_REGEX) do |placeholder|
119
+ resolved_property(T.must(Regexp.last_match)[:property].to_s, pom) || placeholder
120
+ end
121
+ end
122
+
123
+ sig { params(property_name: String, pom: Dependabot::DependencyFile).returns(T.nilable(String)) }
124
+ def resolved_property(property_name, pom)
125
+ details = @property_value_finder.property_details(property_name: property_name, callsite_pom: pom)
126
+ value = details&.fetch(:value)
127
+ value.is_a?(String) ? value : nil
128
+ rescue Dependabot::DependabotError
129
+ nil
130
+ end
131
+
132
+ sig { params(pom_name: String).returns(T::Array[String]) }
133
+ def ancestors(pom_name)
134
+ found = T.let([], T::Array[String])
135
+ current = T.let(local_parent_names[pom_name], T.nilable(String))
136
+ while current && !found.include?(current) && current != pom_name
137
+ found << current
138
+ current = local_parent_names[current]
139
+ end
140
+ found
141
+ end
142
+
143
+ sig { params(pom_name: String).returns(T::Array[String]) }
144
+ def descendants(pom_name)
145
+ found = T.let([], T::Array[String])
146
+ queue = [pom_name]
147
+ while (current = queue.shift)
148
+ local_parent_names.each do |child, parent|
149
+ next unless parent == current
150
+ next if child == pom_name || found.include?(child)
151
+
152
+ found << child
153
+ queue << child
154
+ end
155
+ end
156
+ found
157
+ end
158
+
159
+ # Maps each POM file name to the file name of its parent, when the parent is in the job.
160
+ sig { returns(T::Hash[String, String]) }
161
+ def local_parent_names
162
+ @local_parent_names ||= T.let(
163
+ @pomfiles.each_with_object({}) do |pom, parents|
164
+ coordinates = parent_coordinates(pom)
165
+ parent = coordinates && coordinates_to_pom_name[coordinates]
166
+ parents[pom.name] = parent if parent && parent != pom.name
167
+ end,
168
+ T.nilable(T::Hash[String, String])
169
+ )
170
+ end
171
+
172
+ sig { returns(T::Hash[String, String]) }
173
+ def coordinates_to_pom_name
174
+ @coordinates_to_pom_name ||= T.let(
175
+ @pomfiles.each_with_object({}) do |pom, map|
176
+ doc = document(pom)
177
+ group_id = resolved_text(doc.at_xpath("/project/groupId") || doc.at_xpath("/project/parent/groupId"), pom)
178
+ artifact_id = resolved_text(doc.at_xpath("/project/artifactId"), pom)
179
+ next unless group_id && artifact_id
180
+
181
+ map["#{group_id}:#{artifact_id}"] ||= pom.name
182
+ end,
183
+ T.nilable(T::Hash[String, String])
184
+ )
185
+ end
186
+
187
+ sig { params(pom: Dependabot::DependencyFile).returns(T.nilable(String)) }
188
+ def parent_coordinates(pom)
189
+ doc = document(pom)
190
+ group_id = doc.at_xpath("/project/parent/groupId")
191
+ artifact_id = doc.at_xpath("/project/parent/artifactId")
192
+ return unless group_id && artifact_id
193
+
194
+ "#{group_id.content.strip}:#{artifact_id.content.strip}"
195
+ end
196
+
197
+ sig { params(pom: Dependabot::DependencyFile).returns(Nokogiri::XML::Document) }
198
+ def document(pom)
199
+ @documents[pom.name] ||= begin
200
+ doc = Nokogiri::XML(pom.content)
201
+ doc.remove_namespaces!
202
+ doc
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -24,6 +24,7 @@ module Dependabot
24
24
  require "dependabot/file_parsers/base/dependency_set"
25
25
  require_relative "file_parser/maven_dependency_parser"
26
26
  require_relative "file_parser/property_value_finder"
27
+ require_relative "file_parser/transitive_override_finder"
27
28
  require_relative "file_parser/wrapper_mojo"
28
29
 
29
30
  # The following "dependencies" are candidates for updating:
@@ -36,10 +37,18 @@ module Dependabot
36
37
  "extensions > extension, " \
37
38
  "annotationProcessorPaths > path"
38
39
  PLUGIN_SELECTOR = "plugins > plugin"
39
- EXTENSION_SELECTOR = "extensions > extension"
40
+ # `<dependencies>` under these elements don't declare a project dependency.
41
+ NON_DECLARING_SECTIONS = %w(dependencyManagement plugin).freeze
42
+ EXTENSION_SELECTOR = "extensions > extension"
40
43
  TARGET_SELECTOR = "target > locations > location[type='Maven'] > dependencies > dependency"
41
44
  PLUGIN_ARTIFACT_ITEMS_SELECTOR = "plugins > plugin > executions > execution > " \
42
45
  "configuration > artifactItems > artifactItem"
46
+ # Annotation processor paths and plugin artifact items can take their version
47
+ # from `<dependencyManagement>`, so they count as declarations too.
48
+ DECLARATION_SELECTOR = T.let(
49
+ "dependencies > dependency, annotationProcessorPaths > path, #{PLUGIN_ARTIFACT_ITEMS_SELECTOR}".freeze,
50
+ String
51
+ )
43
52
 
44
53
  # Regex to get the property name from a declaration that uses a property
45
54
  PROPERTY_REGEX = /\$\{(?<property>.*?)\}/
@@ -285,12 +294,85 @@ module Dependabot
285
294
  source: nil,
286
295
  metadata: {
287
296
  packaging_type: packaging_type(pom, dependency_node),
288
- classifier: dependency_classifier(dependency_node, pom)
297
+ classifier: dependency_classifier(dependency_node, pom),
298
+ transitive_override: transitive_override?(pom, dependency_node, is_plugin: is_plugin) || nil
289
299
  }.merge(property_details).compact
290
300
  }]
291
301
  )
292
302
  end
293
303
 
304
+ # A `<dependencyManagement>` entry (not a BOM import) for an artifact that nothing in its
305
+ # POM lineage declares only sets a transitive version. Tagged with the experiment on, so
306
+ # the update checker can leave it alone outside security updates.
307
+ sig do
308
+ params(
309
+ pom: Dependabot::DependencyFile,
310
+ dependency_node: Nokogiri::XML::Element,
311
+ is_plugin: T::Boolean
312
+ ).returns(T::Boolean)
313
+ end
314
+ def transitive_override?(pom, dependency_node, is_plugin:)
315
+ return false if is_plugin
316
+ return false unless Dependabot::Experiments.enabled?(:maven_transitive_dependencies)
317
+ return false unless dependency_management_node?(dependency_node)
318
+ return false if dependency_scope(pom, dependency_node) == "import"
319
+
320
+ key = management_key(dependency_node, pom)
321
+ !key.nil? && transitive_override_finder.override?(pom: pom, key: key)
322
+ end
323
+
324
+ sig { params(node: Nokogiri::XML::Node).returns(T::Boolean) }
325
+ def dependency_management_node?(node)
326
+ node.name == "dependency" && node.ancestors.any? { |a| a.name == "dependencyManagement" }
327
+ end
328
+
329
+ sig { returns(TransitiveOverrideFinder) }
330
+ def transitive_override_finder
331
+ @transitive_override_finder ||= T.let(
332
+ TransitiveOverrideFinder.new(
333
+ pomfiles: pomfiles,
334
+ declared_keys_by_pom: declared_keys_by_pom,
335
+ property_value_finder: property_value_finder
336
+ ),
337
+ T.nilable(TransitiveOverrideFinder)
338
+ )
339
+ end
340
+
341
+ # Management keys of the artifacts each POM declares in a `<dependencies>`
342
+ # section, as an annotation processor path or as a plugin artifact item,
343
+ # excluding `<dependencyManagement>` entries and plugin dependencies.
344
+ sig { returns(T::Hash[String, T::Set[String]]) }
345
+ def declared_keys_by_pom
346
+ pomfiles.to_h do |pom|
347
+ doc = Nokogiri::XML(pom.content)
348
+ doc.remove_namespaces!
349
+ keys = doc.css(DECLARATION_SELECTOR).each_with_object(Set.new) do |node, set|
350
+ next if node.name == "dependency" && node.ancestors.any? { |a| NON_DECLARING_SECTIONS.include?(a.name) }
351
+
352
+ key = declared_key(node, pom)
353
+ set << key if key
354
+ end
355
+ [pom.name, keys]
356
+ end
357
+ end
358
+
359
+ sig { params(node: Nokogiri::XML::Element, pom: Dependabot::DependencyFile).returns(T.nilable(String)) }
360
+ def declared_key(node, pom)
361
+ management_key(node, pom)
362
+ rescue DependencyFileNotEvaluatable
363
+ nil
364
+ end
365
+
366
+ # Maven matches a declaration to a `<dependencyManagement>` entry by groupId, artifactId,
367
+ # type and classifier, so a `test-jar` or classified declaration doesn't use the plain jar entry.
368
+ sig { params(node: Nokogiri::XML::Element, pom: Dependabot::DependencyFile).returns(T.nilable(String)) }
369
+ def management_key(node, pom)
370
+ name = dependency_name(node, pom)
371
+ return unless name
372
+
373
+ [name, packaging_type(pom, node), dependency_classifier(node, pom).to_s].join(":")
374
+ end
375
+
294
376
  sig do
295
377
  params(
296
378
  dependency_node: Nokogiri::XML::Element,
@@ -20,6 +20,17 @@ module Dependabot
20
20
  <path>.*?</path>|
21
21
  <artifactItem>.*?</artifactItem>
22
22
  }mx
23
+
24
+ # The parts of a requirement the XML match depends on. Scanned-only requirements have no XML
25
+ # entry and are pinned per `pom_file`, so they're kept whole.
26
+ sig { params(requirement: Dependabot::DependencyRequirement).returns(Object) }
27
+ def self.match_key(requirement)
28
+ return requirement unless requirement.file
29
+
30
+ [requirement.file, requirement.requirement_string, requirement.groups] +
31
+ %w(packaging_type classifier property_name property_source).map { |key| requirement.metadata_string(key) }
32
+ end
33
+
23
34
  sig { returns(Dependabot::Dependency) }
24
35
  attr_reader :dependency
25
36
 
@@ -137,13 +148,13 @@ module Dependabot
137
148
  type == packaging_type(node)
138
149
  end
139
150
 
151
+ # A classifier names a distinct artifact, so an entry only matches the requirement parsed from
152
+ # an entry with the same classifier (none for the plain artifact).
140
153
  sig { params(node: Nokogiri::XML::Document).returns(T::Boolean) }
141
154
  def classifier_matches?(node)
142
- return true unless node.at_xpath("./*/classifier")
143
-
144
- classifier = evaluated_value(node.at_xpath("./*/classifier").content.strip)
145
- dep_classifier = dependency.requirements.first&.metadata_string("classifier")
146
- classifier == dep_classifier
155
+ classifier_node = node.at_xpath("./*/classifier")
156
+ classifier = classifier_node && evaluated_value(classifier_node.content.strip)
157
+ classifier == declaring_requirement.metadata_string("classifier")
147
158
  end
148
159
 
149
160
  sig { params(node: Nokogiri::XML::Document).returns(T::Boolean) }
@@ -134,13 +134,8 @@ module Dependabot
134
134
  def update_files_for_dependency(original_files:, dependency:)
135
135
  files = original_files.dup
136
136
 
137
- # The UpdateChecker ensures the order of requirements is preserved
138
- # when updating, so we can zip them together in new/old pairs.
139
- reqs = dependency.requirements.zip(dependency.previous_requirements.to_a)
140
- .reject { |new_req, old_req| new_req == old_req }
141
-
142
137
  # Loop through each changed requirement and update the files
143
- reqs.each do |new_req, old_req|
138
+ changed_requirement_pairs(dependency).each do |new_req, old_req|
144
139
  raise "Bad req match" unless new_req.file == T.must(old_req).file
145
140
  next if new_req.requirement == T.must(old_req).requirement
146
141
 
@@ -161,6 +156,15 @@ module Dependabot
161
156
  end
162
157
  # rubocop:enable Metrics/AbcSize
163
158
 
159
+ # Requirement order is preserved, so new/old pairs zip together. One rewrite changes every XML
160
+ # entry that matches it, so pairs that differ only in other metadata (the override tag, or what
161
+ # the dependency tree scan merged into one of them) collapse into one.
162
+ sig { params(dependency: Dependency).returns(T::Array[[DependencyRequirement, T.nilable(DependencyRequirement)]]) }
163
+ def changed_requirement_pairs(dependency)
164
+ dependency.requirements.zip(dependency.previous_requirements.to_a).reject { |new, old| new == old }
165
+ .uniq { |pair| pair.map { |req| req && DeclarationFinder.match_key(req) } }
166
+ end
167
+
164
168
  sig do
165
169
  params(
166
170
  pomfiles: T::Array[Dependabot::DependencyFile],
@@ -112,6 +112,7 @@ module Dependabot
112
112
  headers: headers
113
113
  )
114
114
  check_response(response, url)
115
+ check_unavailable_response(response, url)
115
116
  return unless response.status < 400
116
117
 
117
118
  Nokogiri::XML(response.body)
@@ -223,6 +224,23 @@ module Dependabot
223
224
  forbidden_urls << repository_url
224
225
  end
225
226
 
227
+ # A rate limit or server error means the repository could not be read, not
228
+ # that it has no versions. Raises for the central repo; for other repos the
229
+ # URL is recorded so we can raise if no repository returned any versions.
230
+ sig { params(response: Excon::Response, repository_url: String).void }
231
+ def check_unavailable_response(response, repository_url)
232
+ return unless response.status == 429 || response.status >= 500
233
+
234
+ if base_repository_urls.include?(strip_trailing_slashes(repository_url))
235
+ raise RegistryError.new(
236
+ response.status,
237
+ "RegistryError: #{response.status} response status from #{repository_url}"
238
+ )
239
+ end
240
+
241
+ unavailable_urls << repository_url unless unavailable_urls.include?(repository_url)
242
+ end
243
+
226
244
  # Raises RegistryError for failures hitting the central repo.
227
245
  sig do
228
246
  params(
@@ -289,6 +307,7 @@ module Dependabot
289
307
  sig { returns(T::Array[VersionDetails]) }
290
308
  def versions_details_from_xml
291
309
  forbidden_urls.clear
310
+ unavailable_urls.clear
292
311
  version_details = repositories.flat_map do |repository_details|
293
312
  url = repository_url(repository_details)
294
313
  xml = dependency_metadata(repository_details)
@@ -298,6 +317,7 @@ module Dependabot
298
317
  end
299
318
 
300
319
  raise PrivateSourceAuthenticationFailure, forbidden_urls.first if version_details.none? && forbidden_urls.any?
320
+ raise PrivateSourceBadResponse, unavailable_urls.first if version_details.none? && unavailable_urls.any?
301
321
 
302
322
  version_details
303
323
  end
@@ -390,9 +410,9 @@ module Dependabot
390
410
  end
391
411
 
392
412
  # Both HTTP and HTTPS variants of the central repo URL, for comparison.
393
- sig { returns(T::Array[String]) }
394
- def central_repo_urls
395
- central_url_without_protocol = central_repo_url.gsub(%r{^.*://}, "")
413
+ sig { params(url: String).returns(T::Array[String]) }
414
+ def central_repo_urls(url = central_repo_url)
415
+ central_url_without_protocol = url.gsub(%r{^.*://}, "")
396
416
  %w(http:// https://).map { |p| p + central_url_without_protocol }
397
417
  end
398
418
 
@@ -401,6 +421,24 @@ module Dependabot
401
421
  @forbidden_urls ||= T.let([], T.nilable(T::Array[String]))
402
422
  end
403
423
 
424
+ # Maven Central and any registry that replaces it. Central is kept even when
425
+ # replaced, since a POM may still list it explicitly.
426
+ sig { returns(T::Array[String]) }
427
+ def base_repository_urls
428
+ replaces_base = credentials.filter_map { |c| c[URL_KEY] if c["type"] == REPOSITORY_TYPE && c.replaces_base? }
429
+ (central_repo_urls(DEFAULT_CENTRAL_REPO_URL) + replaces_base).map { |url| strip_trailing_slashes(url) }
430
+ end
431
+
432
+ sig { params(url: String).returns(String) }
433
+ def strip_trailing_slashes(url)
434
+ url.dup.tap { |u| u.delete_suffix!("/") while u.end_with?("/") }
435
+ end
436
+
437
+ sig { returns(T::Array[String]) }
438
+ def unavailable_urls
439
+ @unavailable_urls ||= T.let([], T.nilable(T::Array[String]))
440
+ end
441
+
404
442
  # -- Auth --
405
443
 
406
444
  sig { params(maven_repo_url: String).returns(T::Hash[String, String]) }
@@ -14,6 +14,7 @@ module Dependabot
14
14
  extend T::Sig
15
15
 
16
16
  require_relative "requirements_updater"
17
+ require_relative "transitive_overrides"
17
18
  require_relative "version_finder"
18
19
 
19
20
  sig do
@@ -23,7 +24,8 @@ module Dependabot
23
24
  credentials: T::Array[Dependabot::Credential],
24
25
  ignored_versions: T::Array[String],
25
26
  target_version_details: T.nilable(UpdateChecker::VersionDetails),
26
- update_cooldown: T.nilable(Dependabot::Package::ReleaseCooldownOptions)
27
+ update_cooldown: T.nilable(Dependabot::Package::ReleaseCooldownOptions),
28
+ security_update: T::Boolean
27
29
  ).void
28
30
  end
29
31
  def initialize(
@@ -32,7 +34,8 @@ module Dependabot
32
34
  credentials:,
33
35
  ignored_versions:,
34
36
  target_version_details:,
35
- update_cooldown: nil
37
+ update_cooldown: nil,
38
+ security_update: false
36
39
  )
37
40
  @dependency = dependency
38
41
  @dependency_files = dependency_files
@@ -43,6 +46,7 @@ module Dependabot
43
46
  @target_version = T.let(target_version, T.nilable(Dependabot::Maven::Version))
44
47
  @source_url = T.let(target_version_details&.fetch(:source_url), T.nilable(String))
45
48
  @update_cooldown = update_cooldown
49
+ @security_update = security_update
46
50
  @property_value_finder = T.let(nil, T.nilable(Dependabot::Maven::FileParser::PropertyValueFinder))
47
51
  end
48
52
 
@@ -123,13 +127,21 @@ module Dependabot
123
127
  sig { returns(T::Array[Dependabot::Dependency]) }
124
128
  def dependencies_using_property
125
129
  @dependencies_using_property ||= T.let(
130
+ parsed_dependencies.select do |dep|
131
+ dep.requirements.any? { |requirement| uses_property?(requirement) }
132
+ end,
133
+ T.nilable(T::Array[Dependabot::Dependency])
134
+ )
135
+ end
136
+
137
+ sig { returns(T::Array[Dependabot::Dependency]) }
138
+ def parsed_dependencies
139
+ @parsed_dependencies ||= T.let(
126
140
  Maven::FileParser.new(
127
141
  dependency_files: dependency_files,
128
142
  source: nil,
129
143
  options: { skip_dependency_tree: true }
130
- ).parse.select do |dep|
131
- dep.requirements.any? { |requirement| uses_property?(requirement) }
132
- end,
144
+ ).parse,
133
145
  T.nilable(T::Array[Dependabot::Dependency])
134
146
  )
135
147
  end
@@ -156,7 +168,9 @@ module Dependabot
156
168
 
157
169
  sig { returns(Dependabot::DependencyRequirement) }
158
170
  def property_requirement
159
- property_requirements = dependency.requirements.select { |r| r.metadata_string("property_name") }
171
+ property_requirements = TransitiveOverrides.raisable_property_requirements(
172
+ dependency.requirements, security_update: @security_update
173
+ ) { parsed_dependencies }
160
174
  matching_requirement = property_requirements.find do |requirement|
161
175
  normalized_requirement_version(requirement) == dependency.version
162
176
  end
@@ -255,12 +269,25 @@ module Dependabot
255
269
  source_url: source_url,
256
270
  properties_to_update: [property_name]
257
271
  ).updated_requirements.map.with_index do |requirement, index|
272
+ next dep.requirements.fetch(index) if keep_unchanged?(dep, dep.requirements.fetch(index))
258
273
  next requirement unless requirement.metadata_string("property_name")
259
274
 
260
275
  uses_property?(requirement) ? requirement : dep.requirements.fetch(index)
261
276
  end
262
277
  end
263
278
 
279
+ # A literal transitive override isn't tied to the shared property, so the property update leaves
280
+ # it, and any declaration whose rewrite would also hit it, alone. Only a security update raises
281
+ # it, and only for the vulnerable dependency.
282
+ sig { params(dep: Dependabot::Dependency, requirement: Dependabot::DependencyRequirement).returns(T::Boolean) }
283
+ def keep_unchanged?(dep, requirement)
284
+ return false if @security_update && dep.name == dependency.name
285
+ return false if requirement.metadata_string("property_name")
286
+
287
+ TransitiveOverrides.override?(requirement) ||
288
+ TransitiveOverrides.rewrites_an_override?(requirement, dep.requirements)
289
+ end
290
+
264
291
  sig { params(dep: Dependabot::Dependency).returns(T::Boolean) }
265
292
  def writable_requirements?(dep)
266
293
  updated = updated_requirements(dep)
@@ -0,0 +1,87 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/dependency_requirement"
6
+ require "dependabot/maven/update_checker"
7
+
8
+ module Dependabot
9
+ module Maven
10
+ class UpdateChecker
11
+ # Rules for keeping transitive overrides (tagged by the file parser) out of version updates.
12
+ module TransitiveOverrides
13
+ extend T::Sig
14
+
15
+ sig { params(requirement: Dependabot::DependencyRequirement).returns(T::Boolean) }
16
+ def self.override?(requirement)
17
+ requirement.metadata_boolean("transitive_override") == true
18
+ end
19
+
20
+ # The file updater rewrites every XML entry that matches a declaration's file, literal version,
21
+ # packaging type, scope (ignored for plugins) and classifier. A declaration that matches a kept
22
+ # override that way can't change on its own, so it's held too.
23
+ sig do
24
+ params(
25
+ requirement: Dependabot::DependencyRequirement,
26
+ requirements: T::Array[Dependabot::DependencyRequirement]
27
+ ).returns(T::Boolean)
28
+ end
29
+ def self.rewrites_an_override?(requirement, requirements)
30
+ return false if override?(requirement) || property?(requirement)
31
+
32
+ requirements.any? { |other| override?(other) && rewrites?(requirement, other) }
33
+ end
34
+
35
+ # Outside a security update an override follows a version property only when a declaration
36
+ # also uses it. A property only overrides use is never raised, so it can't drive an update.
37
+ sig do
38
+ params(
39
+ requirements: T::Array[Dependabot::DependencyRequirement],
40
+ security_update: T::Boolean,
41
+ dependencies: T.proc.returns(T::Array[Dependabot::Dependency])
42
+ ).returns(T::Array[Dependabot::DependencyRequirement])
43
+ end
44
+ def self.raisable_property_requirements(requirements, security_update:, &dependencies)
45
+ with_property = requirements.select { |req| property?(req) }
46
+ return with_property if security_update || with_property.none? { |req| override?(req) }
47
+
48
+ declared = (requirements + yield.flat_map(&:requirements)).reject { |req| override?(req) }
49
+ with_property.select do |req|
50
+ !override?(req) || declared.any? { |other| same_property?(other, req) }
51
+ end
52
+ end
53
+
54
+ sig do
55
+ params(
56
+ declaration: Dependabot::DependencyRequirement,
57
+ override: Dependabot::DependencyRequirement
58
+ ).returns(T::Boolean)
59
+ end
60
+ def self.rewrites?(declaration, override)
61
+ return false if property?(override)
62
+ return false unless declaration.file == override.file
63
+ return false unless declaration.requirement_string == override.requirement_string
64
+ return false unless %w(packaging_type classifier).all? do |key|
65
+ declaration.metadata_string(key) == override.metadata_string(key)
66
+ end
67
+
68
+ declaration.groups == ["plugin"] || declaration.groups == override.groups
69
+ end
70
+ private_class_method :rewrites?
71
+
72
+ sig { params(one: Dependabot::DependencyRequirement, other: Dependabot::DependencyRequirement).returns(T::Boolean) }
73
+ def self.same_property?(one, other)
74
+ one.metadata_string("property_name") == other.metadata_string("property_name") &&
75
+ one.metadata_string("property_source") == other.metadata_string("property_source")
76
+ end
77
+ private_class_method :same_property?
78
+
79
+ sig { params(requirement: Dependabot::DependencyRequirement).returns(T::Boolean) }
80
+ def self.property?(requirement)
81
+ !requirement.metadata_string("property_name").nil?
82
+ end
83
+ private_class_method :property?
84
+ end
85
+ end
86
+ end
87
+ end
@@ -11,6 +11,7 @@ module Dependabot
11
11
  require_relative "update_checker/requirements_updater"
12
12
  require_relative "update_checker/version_finder"
13
13
  require_relative "update_checker/property_updater"
14
+ require_relative "update_checker/transitive_overrides"
14
15
 
15
16
  VersionDetails = T.type_alias do
16
17
  {
@@ -57,6 +58,16 @@ module Dependabot
57
58
  @all_property_based_dependencies = T.let(nil, T.nilable(T::Array[Dependabot::Dependency]))
58
59
  end
59
60
 
61
+ # Transitive overrides hold a version on purpose (often a security fix), so only a
62
+ # vulnerable one is updated. A dependency set only by overrides is "up to date" otherwise,
63
+ # which keeps version, grouped and refresh jobs away from it.
64
+ sig { override.returns(T::Boolean) }
65
+ def up_to_date?
66
+ return true if no_updatable_declarations? && !vulnerable?
67
+
68
+ super
69
+ end
70
+
60
71
  sig { override.returns(T.nilable(Dependabot::Version)) }
61
72
  def latest_version
62
73
  version = latest_version_details&.fetch(:version)
@@ -155,12 +166,47 @@ module Dependabot
155
166
  declarations_using_a_property
156
167
  .filter_map { |req| req.metadata_string("property_name") }
157
168
 
158
- RequirementsUpdater.new(
169
+ updated = RequirementsUpdater.new(
159
170
  requirements: dependency.requirements,
160
171
  latest_version: version&.to_s,
161
172
  source_url: preferred_version_details&.fetch(:source_url),
162
173
  properties_to_update: property_names
163
174
  ).updated_requirements
175
+ return updated if vulnerable? || dependency.requirements.none? { |req| transitive_override?(req) }
176
+
177
+ # Outside a security update only declarations change, plus overrides sharing a property being
178
+ # raised (they move with it anyway, and must change so their version suffix is normalized).
179
+ # Other overrides keep their version, and scanned requirements aren't pinned underneath them.
180
+ dependency.requirements.zip(updated).map do |original, updated_req|
181
+ updatable_declaration?(original) || follows_raised_property?(original) ? T.must(updated_req) : original
182
+ end
183
+ end
184
+
185
+ sig { params(requirement: Dependabot::DependencyRequirement).returns(T::Boolean) }
186
+ def follows_raised_property?(requirement)
187
+ transitive_override?(requirement) && declarations_using_a_property.include?(requirement)
188
+ end
189
+
190
+ sig { returns(T::Boolean) }
191
+ def no_updatable_declarations?
192
+ declared = dependency.requirements.select(&:file)
193
+ declared.any? && declared.none? { |req| updatable_declaration?(req) }
194
+ end
195
+
196
+ sig { params(requirement: Dependabot::DependencyRequirement).returns(T::Boolean) }
197
+ def updatable_declaration?(requirement)
198
+ declared_requirement?(requirement) &&
199
+ !TransitiveOverrides.rewrites_an_override?(requirement, dependency.requirements)
200
+ end
201
+
202
+ sig { params(requirement: Dependabot::DependencyRequirement).returns(T::Boolean) }
203
+ def transitive_override?(requirement)
204
+ TransitiveOverrides.override?(requirement)
205
+ end
206
+
207
+ sig { params(requirement: Dependabot::DependencyRequirement).returns(T::Boolean) }
208
+ def declared_requirement?(requirement)
209
+ !requirement.file.nil? && !transitive_override?(requirement)
164
210
  end
165
211
 
166
212
  sig { override.returns(T::Boolean) }
@@ -230,7 +276,8 @@ module Dependabot
230
276
  target_version_details: latest_version_details,
231
277
  credentials: credentials,
232
278
  ignored_versions: ignored_versions,
233
- update_cooldown: update_cooldown
279
+ update_cooldown: update_cooldown,
280
+ security_update: vulnerable?
234
281
  )
235
282
  end
236
283
 
@@ -262,8 +309,9 @@ module Dependabot
262
309
  sig { returns(T::Array[Dependabot::DependencyRequirement]) }
263
310
  def declarations_using_a_property
264
311
  @declarations_using_a_property ||=
265
- dependency.requirements
266
- .select { |req| req.metadata_string("property_name") }
312
+ TransitiveOverrides.raisable_property_requirements(dependency.requirements, security_update: vulnerable?) do
313
+ all_property_based_dependencies
314
+ end
267
315
  end
268
316
 
269
317
  sig { returns(T::Array[Dependabot::Dependency]) }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-maven
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.402.0
4
+ version: 0.404.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.402.0
18
+ version: 0.404.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.402.0
25
+ version: 0.404.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rexml
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -263,6 +263,7 @@ files:
263
263
  - lib/dependabot/maven/file_parser/pom_fetcher.rb
264
264
  - lib/dependabot/maven/file_parser/property_value_finder.rb
265
265
  - lib/dependabot/maven/file_parser/repositories_finder.rb
266
+ - lib/dependabot/maven/file_parser/transitive_override_finder.rb
266
267
  - lib/dependabot/maven/file_parser/wrapper_mojo.rb
267
268
  - lib/dependabot/maven/file_updater.rb
268
269
  - lib/dependabot/maven/file_updater/declaration_finder.rb
@@ -289,6 +290,7 @@ files:
289
290
  - lib/dependabot/maven/update_checker.rb
290
291
  - lib/dependabot/maven/update_checker/property_updater.rb
291
292
  - lib/dependabot/maven/update_checker/requirements_updater.rb
293
+ - lib/dependabot/maven/update_checker/transitive_overrides.rb
292
294
  - lib/dependabot/maven/update_checker/version_finder.rb
293
295
  - lib/dependabot/maven/utils/auth_headers_finder.rb
294
296
  - lib/dependabot/maven/version.rb
@@ -298,7 +300,7 @@ licenses:
298
300
  - MIT
299
301
  metadata:
300
302
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
301
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.402.0
303
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.404.0
302
304
  rdoc_options: []
303
305
  require_paths:
304
306
  - lib