dependabot-maven 0.399.0 → 0.400.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: 633af390f012c6aa9704de839340e18485655cffa7ce230467f78f8b6ee2541b
4
- data.tar.gz: 7062af128c881f3bfcd9aacffb61957a24700982f86d51db00bf978f8e832d51
3
+ metadata.gz: d916bf0c52cb0878f5ffaa396bb644c5f6529a9be503175c359395a5acb905ff
4
+ data.tar.gz: 0d093a3b4689ba1f54f7e6ef9f97c3a10288252a63ae2da3c122d3f30df30a1a
5
5
  SHA512:
6
- metadata.gz: 5ece07a1b94981cf48b3b1638b18b1d40da3b4a19b90847641b67d226c1a60053335f1a7b2b9ed6c4007d50f733cbec8b4b19900dbde8b57aa6f15cd01ee8c37
7
- data.tar.gz: f7361f3db224e9158d7f6674edeab58f05e4e2e253f366346bad32247886ced22f6dcc8f9948e0894a76cbe4cfa4b3b393ba4e1ce5b18c1bf349acf328f158a2
6
+ metadata.gz: a5d2b4f159ef96b9a9a5dedefd8c01e41943b97b39376d62636dc919fc3bc9d9cb693b9b29dc5528aa657dd97726f24884a13bfc96a159b0fc4afdff7fa23e8d
7
+ data.tar.gz: 071b060c8356276b51e441500e4318b055560b44c5a28b13d1c6e7d43154009aff748cfb56bf4fd39642c4a0888f763aab452ae23c7bb8ea5f8182137e3a5d1a
@@ -3,24 +3,35 @@
3
3
 
4
4
  require "sorbet-runtime"
5
5
 
6
+ require "dependabot/dependency"
7
+ require "dependabot/dependency_requirement"
6
8
  require "dependabot/maven/file_parser"
7
9
  require "dependabot/maven/native_helpers"
10
+ require "dependabot/maven/file_parser/repositories_finder"
11
+ require "dependabot/maven/shared/maven_settings"
8
12
 
9
13
  module Dependabot
10
14
  module Maven
11
15
  class FileParser
16
+ # The `mvn dependency:tree` scan: runs Maven, reads its JSON output into
17
+ # dependencies, and merges them with the XML-declared requirements.
12
18
  class MavenDependencyParser
13
19
  extend T::Sig
14
20
 
15
21
  require "dependabot/file_parsers/base/dependency_set"
16
22
 
17
23
  DEPENDENCY_OUTPUT_FILE = "dependency-tree-output.json"
24
+ REGISTRY_MIRROR_ID = "dependabot-registry-mirror"
18
25
 
26
+ # Runs `mvn dependency:tree` and returns every resolved dependency, or nil when the
27
+ # scan fails so the caller can fall back to the XML-only parse.
19
28
  sig do
20
- params(dependency_files: T::Array[Dependabot::DependencyFile])
21
- .returns(Dependabot::FileParsers::Base::DependencySet)
29
+ params(
30
+ dependency_files: T::Array[Dependabot::DependencyFile],
31
+ credentials: T::Array[Dependabot::Credential]
32
+ ).returns(T.nilable(Dependabot::FileParsers::Base::DependencySet))
22
33
  end
23
- def self.build_dependency_set(dependency_files)
34
+ def self.build_dependency_set(dependency_files, credentials: [])
24
35
  dependency_set = Dependabot::FileParsers::Base::DependencySet.new
25
36
 
26
37
  # Copy only pom.xml files to a temporary directory to
@@ -31,78 +42,69 @@ module Dependabot
31
42
 
32
43
  dependency_files.each do |pom|
33
44
  pom_path = File.join(project_directory, pom.name)
34
- pom_dir = File.dirname(pom_path)
35
- FileUtils.mkdir_p(pom_dir)
45
+ FileUtils.mkdir_p(File.dirname(pom_path))
36
46
  File.write(pom_path, pom.content)
37
47
  end
38
48
 
39
- Dir.chdir(project_directory) do
40
- NativeHelpers.run_mvn_dependency_tree_plugin(DEPENDENCY_OUTPUT_FILE)
41
- end
49
+ Dir.chdir(project_directory) { run_scan(credentials) }
42
50
 
43
51
  # mvn CLI outputs dependency tree for each pom.xml file, collect them
44
52
  # add into single dependency set
45
53
  dependency_files.each do |pom|
46
- pom_path = File.join(project_directory, pom.name)
47
- pom_dir = File.dirname(pom_path)
48
- output_file = File.join(pom_dir, DEPENDENCY_OUTPUT_FILE)
54
+ output_file = File.join(File.dirname(File.join(project_directory, pom.name)), DEPENDENCY_OUTPUT_FILE)
49
55
 
50
56
  # If we run updater from sub-module, parent file might be included in dependency files,
51
57
  # but mvn CLI will not generate dependency tree for it unless we start from the parent.
52
58
  # In that case we can just skip it and focus only on current file and it's sub-modules.
53
- unless File.exist?(output_file)
54
- Dependabot.logger.warn("Dependency tree output file not found: #{output_file}")
55
- next
56
- end
59
+ next unless File.exist?(output_file)
57
60
 
58
- dependency_tree = JSON.parse(File.read(output_file))
59
- extract_dependencies_from_tree(pom, dependency_set, dependency_tree)
61
+ add_tree(dependency_set, pom, JSON.parse(File.read(output_file)))
60
62
  end
61
63
  end
62
64
 
63
65
  dependency_set
66
+ rescue StandardError => e
67
+ # Never fail the job or log Maven output (it can include registry URLs) from here.
68
+ Dependabot.logger.warn("Maven dependency tree scan failed (#{e.class}); using declared dependencies only")
69
+ nil
64
70
  end
65
71
 
72
+ # Merges the requirements of one dependency found by both the XML parse and the scan.
73
+ #
74
+ # Each declared (XML) requirement is paired with the scanned requirement from the same
75
+ # POM. The XML keeps its requirement, file, groups and source, because the file updater
76
+ # matches them against the XML node; the scan only adds metadata Maven resolved.
77
+ # Scanned requirements with no declaration in that POM (inherited or transitive) are
78
+ # kept as they are.
66
79
  sig do
67
- params(
68
- pom: Dependabot::DependencyFile,
69
- dependency_set: Dependabot::FileParsers::Base::DependencySet,
70
- dependency_tree: T::Hash[String, Object]
71
- ).void
80
+ params(requirements: T::Array[Dependabot::DependencyRequirement])
81
+ .returns(T::Array[Dependabot::DependencyRequirement])
72
82
  end
73
- def self.extract_dependencies_from_tree(pom, dependency_set, dependency_tree)
74
- traverse_tree = T.let(-> {}, T.proc.params(node: T::Hash[String, Object]).void)
75
- traverse_tree = lambda do |node|
76
- artifact_id = node["artifactId"]
77
- group_id = node["groupId"]
78
- version = node["version"]
79
- type = node["type"]
80
- classifier = node["classifier"].to_s.empty? ? nil : node["classifier"].to_s
81
- scope = node["scope"]
82
-
83
- groups = scope == "test" ? ["test"] : []
84
- dependency_set << Dependabot::Dependency.new(
85
- name: "#{group_id}:#{artifact_id}",
86
- version: version,
87
- package_manager: "maven",
88
- requirements: [{
89
- requirement: version,
90
- file: nil,
91
- groups: groups,
92
- source: nil,
93
- metadata: {
94
- packaging_type: type,
95
- classifier: classifier,
96
- pom_file: pom.name
97
- }
98
- }]
99
- )
100
-
101
- children = node["children"]
102
- children.each { |child| traverse_tree.call(child) } if children.is_a?(Array)
83
+ def self.merge_requirements(requirements)
84
+ declared, scanned = requirements.partition(&:file)
85
+ merged = declared.map do |declared_req|
86
+ index = scanned.index { |req| req.metadata_string("pom_file") == declared_req.file }
87
+ index ? merge_requirement_pair(declared_req, T.must(scanned.delete_at(index))) : declared_req
103
88
  end
89
+ merged + scanned
90
+ end
104
91
 
105
- traverse_tree.call(dependency_tree)
92
+ # Uses the job's registries the same way Dependabot's own Maven lookups do: a
93
+ # `replaces-base` registry stands in for Central, and other registries are added.
94
+ # Repositories declared in the POMs are left to Maven.
95
+ sig { params(credentials: T::Array[Dependabot::Credential]).void }
96
+ def self.run_scan(credentials)
97
+ finder = RepositoriesFinder.new(pom_fetcher: nil, credentials: credentials)
98
+ base_url = finder.replaces_base_url&.strip&.chomp("/")
99
+ mirror = base_url && Shared::MavenSettings::Mirror.new(
100
+ id: REGISTRY_MIRROR_ID, url: base_url, mirror_of: RepositoriesFinder::CENTRAL_REPO_ID
101
+ )
102
+
103
+ NativeHelpers.run_mvn_dependency_tree_plugin(
104
+ DEPENDENCY_OUTPUT_FILE,
105
+ mirror: mirror,
106
+ repository_urls: finder.urls_from_credentials - [base_url]
107
+ )
106
108
  end
107
109
 
108
110
  sig do
@@ -124,6 +126,98 @@ module Dependabot
124
126
 
125
127
  base_depth_path
126
128
  end
129
+
130
+ # The root node is the module itself and is skipped. Depth-one nodes are direct
131
+ # dependencies, including ones inherited from a parent POM. Deeper nodes are
132
+ # transitive and record the dependency that pulled them in as `pulled_in_by`.
133
+ sig do
134
+ params(
135
+ dependency_set: Dependabot::FileParsers::Base::DependencySet,
136
+ pom: Dependabot::DependencyFile,
137
+ tree: Object
138
+ ).void
139
+ end
140
+ def self.add_tree(dependency_set, pom, tree)
141
+ tree_children(tree).each { |node| add_tree_node(dependency_set, pom, node, pulled_in_by: nil) }
142
+ end
143
+ private_class_method :add_tree
144
+
145
+ sig do
146
+ params(
147
+ dependency_set: Dependabot::FileParsers::Base::DependencySet,
148
+ pom: Dependabot::DependencyFile,
149
+ node: T::Hash[String, Object],
150
+ pulled_in_by: T.nilable(String)
151
+ ).void
152
+ end
153
+ def self.add_tree_node(dependency_set, pom, node, pulled_in_by:)
154
+ group_id = node_string(node, "groupId")
155
+ artifact_id = node_string(node, "artifactId")
156
+ version = node_string(node, "version")
157
+ return unless group_id && artifact_id && version
158
+
159
+ name = "#{group_id}:#{artifact_id}"
160
+ scope = node_string(node, "scope")
161
+ dependency_set << Dependabot::Dependency.new(
162
+ name: name,
163
+ version: version,
164
+ package_manager: "maven",
165
+ requirements: [{
166
+ # Transitive dependencies keep this "declared-looking" shape on purpose:
167
+ # the file updater pins them through `pom_file` (see FileUpdater).
168
+ requirement: version,
169
+ file: nil,
170
+ groups: scope == "test" ? ["test"] : [],
171
+ source: nil,
172
+ metadata: {
173
+ packaging_type: node_string(node, "type"),
174
+ classifier: node_string(node, "classifier"),
175
+ scope: scope,
176
+ pom_file: pom.name,
177
+ pulled_in_by: pulled_in_by
178
+ }.compact
179
+ }]
180
+ )
181
+
182
+ tree_children(node).each { |child| add_tree_node(dependency_set, pom, child, pulled_in_by: name) }
183
+ end
184
+ private_class_method :add_tree_node
185
+
186
+ sig { params(node: Object).returns(T::Array[T::Hash[String, Object]]) }
187
+ def self.tree_children(node)
188
+ return [] unless node.is_a?(Hash)
189
+
190
+ value = node["children"]
191
+ value.is_a?(Array) ? value.grep(Hash) : []
192
+ end
193
+ private_class_method :tree_children
194
+
195
+ sig { params(node: T::Hash[String, Object], key: String).returns(T.nilable(String)) }
196
+ def self.node_string(node, key)
197
+ value = node[key]
198
+ value.is_a?(String) && !value.empty? ? value : nil
199
+ end
200
+ private_class_method :node_string
201
+
202
+ sig do
203
+ params(
204
+ declared: Dependabot::DependencyRequirement,
205
+ scanned: Dependabot::DependencyRequirement
206
+ ).returns(Dependabot::DependencyRequirement)
207
+ end
208
+ def self.merge_requirement_pair(declared, scanned)
209
+ declared_metadata = declared.metadata || {}
210
+ metadata = (scanned.metadata || {}).merge(declared_metadata) { |_key, scan, xml| xml.nil? ? scan : xml }
211
+
212
+ Dependabot::DependencyRequirement.create(
213
+ requirement: declared.requirement,
214
+ file: declared.file,
215
+ groups: declared.groups,
216
+ source: declared.source,
217
+ metadata: metadata
218
+ )
219
+ end
220
+ private_class_method :merge_requirement_pair
127
221
  end
128
222
  end
129
223
  end
@@ -24,6 +24,9 @@ module Dependabot
24
24
 
25
25
  REPOSITORY_SELECTOR = "repositories > repository, " \
26
26
  "pluginRepositories > pluginRepository"
27
+ CREDENTIAL_TYPE = "maven_repository"
28
+ CENTRAL_REPO_ID = "central"
29
+ CENTRAL_REPO_URL = "https://repo.maven.apache.org/maven2"
27
30
  RepositoryEntry = T.type_alias { T::Hash[Symbol, T.nilable(T.any(String, T::Boolean))] }
28
31
 
29
32
  sig do
@@ -51,8 +54,22 @@ module Dependabot
51
54
 
52
55
  sig { returns(String) }
53
56
  def central_repo_url
54
- base = @credentials.find { |cred| cred["type"] == "maven_repository" && cred.replaces_base? }
55
- base ? T.must(base["url"]) : "https://repo.maven.apache.org/maven2"
57
+ replaces_base_url || CENTRAL_REPO_URL
58
+ end
59
+
60
+ # The URL of the credential registry that replaces Maven Central, if any.
61
+ sig { returns(T.nilable(String)) }
62
+ def replaces_base_url
63
+ base = @credentials.find { |cred| cred["type"] == CREDENTIAL_TYPE && cred.replaces_base? }
64
+ base && base["url"]
65
+ end
66
+
67
+ # The URLs of all `maven_repository` credentials, without trailing slashes.
68
+ sig { returns(T::Array[String]) }
69
+ def urls_from_credentials
70
+ @credentials
71
+ .select { |cred| cred["type"] == CREDENTIAL_TYPE }
72
+ .filter_map { |cred| cred["url"]&.strip&.gsub(%r{/$}, "") }
56
73
  end
57
74
 
58
75
  # Collect all repository URLs from this POM and its parents
@@ -90,7 +107,7 @@ module Dependabot
90
107
  # always inherited from.
91
108
  sig { returns(T::Hash[Symbol, String]) }
92
109
  def super_pom
93
- { url: central_repo_url, id: "central" }
110
+ { url: central_repo_url, id: CENTRAL_REPO_ID }
94
111
  end
95
112
 
96
113
  sig { params(entry: Nokogiri::XML::Node).returns(T::Hash[Symbol, T.nilable(String)]) }
@@ -270,13 +287,6 @@ module Dependabot
270
287
  end
271
288
  # rubocop:enable Metrics/PerceivedComplexity
272
289
 
273
- sig { returns(T::Array[String]) }
274
- def urls_from_credentials
275
- @credentials
276
- .select { |cred| cred["type"] == "maven_repository" }
277
- .filter_map { |cred| cred["url"]&.strip&.gsub(%r{/$}, "") }
278
- end
279
-
280
290
  sig { params(value: String).returns(T::Boolean) }
281
291
  def contains_property?(value)
282
292
  value.match?(property_regex)
@@ -46,7 +46,7 @@ module Dependabot
46
46
 
47
47
  sig { override.returns(T::Array[Dependabot::Dependency]) }
48
48
  def parse
49
- if Dependabot::Experiments.enabled?(:maven_transitive_dependencies)
49
+ if scan_dependency_tree?
50
50
  parse_with_transitive_dependencies
51
51
  else
52
52
  parse_standard_dependencies
@@ -67,26 +67,39 @@ module Dependabot
67
67
 
68
68
  private
69
69
 
70
+ # Maven's own reparses (update checker, property updater) pass `skip_dependency_tree: true`:
71
+ # they only need declared dependencies, so they skip the expensive scan.
72
+ sig { returns(T::Boolean) }
73
+ def scan_dependency_tree?
74
+ return false if T.cast(options[:skip_dependency_tree], Object) == true
75
+
76
+ Dependabot::Experiments.enabled?(:maven_transitive_dependencies)
77
+ end
78
+
79
+ # Starts from the XML-only result and adds what `mvn dependency:tree` resolves.
80
+ # If the scan fails, the result is the same as with the experiment off.
70
81
  sig { returns(T::Array[Dependabot::Dependency]) }
71
82
  def parse_with_transitive_dependencies
72
- dependency_set = DependencySet.new
73
- dependency_set += MavenDependencyParser.build_dependency_set(pomfiles)
83
+ declared = parse_standard_dependencies
84
+ tree = MavenDependencyParser.build_dependency_set(pomfiles, credentials: credentials)
85
+ return declared unless tree
74
86
 
75
- pomfiles.each { |pom| dependency_set += pomfile_dependencies(pom) }
76
- extensionfiles.each { |extension| dependency_set += extensionfile_dependencies(extension) }
77
- add_wrapper_dependencies(dependency_set)
87
+ dependency_set = DependencySet.new(declared)
88
+ tree.dependencies.each do |dep|
89
+ next if internal_dependency_names.include?(dep.name)
78
90
 
79
- dependencies = []
80
- dependency_set.dependencies.each do |dep|
81
- requirements = merge_requirements(dep.requirements)
82
- dependencies << Dependabot::Dependency.new(
91
+ tree.all_versions_for_name(dep.name).each { |version| dependency_set << version }
92
+ end
93
+
94
+ dependency_set.dependencies.map do |dep|
95
+ Dependabot::Dependency.new(
83
96
  name: dep.name,
84
97
  version: dep.version,
85
98
  package_manager: "maven",
86
- requirements: requirements
99
+ requirements: MavenDependencyParser.merge_requirements(dep.requirements),
100
+ metadata: dep.metadata
87
101
  )
88
102
  end
89
- dependencies
90
103
  end
91
104
 
92
105
  sig { returns(T::Array[Dependabot::Dependency]) }
@@ -527,132 +540,6 @@ module Dependabot
527
540
  raise "No pom.xml!" unless get_original_file("pom.xml")
528
541
  end
529
542
 
530
- # Merge dependency scan requirements with file parsing requirements.
531
- # Since dependency scan evaluates properties, we need to combine results with XML parsing,
532
- # so we know when certain requirement not a literal value and can differentiate transitive dependencies
533
- # from direct dependencies.
534
- sig do
535
- params(requirements: T::Array[Dependabot::DependencyRequirement])
536
- .returns(T::Array[Dependabot::DependencyRequirement])
537
- end
538
- def merge_requirements(requirements)
539
- return requirements if requirements.length <= 1
540
-
541
- merged = []
542
- used_indices = Set.new
543
- requirements.each_with_index do |dep_scan_req, i|
544
- merge_requirement_at(requirements, dep_scan_req, i, merged, used_indices)
545
- end
546
-
547
- merged
548
- end
549
-
550
- sig do
551
- params(
552
- requirements: T::Array[Dependabot::DependencyRequirement],
553
- dep_scan_req: Dependabot::DependencyRequirement,
554
- index: Integer,
555
- merged: T::Array[Dependabot::DependencyRequirement],
556
- used_indices: T::Set[Integer]
557
- ).void
558
- end
559
- def merge_requirement_at(requirements, dep_scan_req, index, merged, used_indices)
560
- return if used_indices.include?(index)
561
-
562
- pom_file = dep_scan_req.metadata_string("pom_file")
563
- return merged << dep_scan_req if pom_file.nil?
564
-
565
- merge_matching_requirement(requirements, dep_scan_req, index, merged, used_indices)
566
- end
567
-
568
- sig do
569
- params(
570
- requirements: T::Array[Dependabot::DependencyRequirement],
571
- dep_scan_req: Dependabot::DependencyRequirement,
572
- index: Integer,
573
- merged: T::Array[Dependabot::DependencyRequirement],
574
- used_indices: T::Set[Integer]
575
- ).void
576
- end
577
- def merge_matching_requirement(requirements, dep_scan_req, index, merged, used_indices)
578
- # Look for another requirement where pom_file matches property_source
579
- pom_file = T.must(dep_scan_req.metadata_string("pom_file"))
580
- match_index = matching_requirement_index(requirements, pom_file, index, used_indices)
581
- return merge_unmatched_requirement(dep_scan_req, index, merged, used_indices) unless match_index
582
-
583
- parsing_req = T.must(requirements[match_index])
584
- merged << merge_requirement(dep_scan_req, parsing_req)
585
- used_indices.add(index)
586
- used_indices.add(match_index)
587
- end
588
-
589
- sig do
590
- params(
591
- dep_scan_req: Dependabot::DependencyRequirement,
592
- index: Integer,
593
- merged: T::Array[Dependabot::DependencyRequirement],
594
- used_indices: T::Set[Integer]
595
- ).void
596
- end
597
- def merge_unmatched_requirement(dep_scan_req, index, merged, used_indices)
598
- merged << dep_scan_req
599
- used_indices.add(index)
600
- end
601
-
602
- sig do
603
- params(
604
- requirements: T::Array[Dependabot::DependencyRequirement],
605
- pom_file: String,
606
- index: Integer,
607
- used_indices: T::Set[Integer]
608
- ).returns(T.nilable(Integer))
609
- end
610
- def matching_requirement_index(requirements, pom_file, index, used_indices)
611
- requirements.find_index.with_index do |parsing_req, i|
612
- i > index && !used_indices.include?(i) && pom_file == parsing_req.file
613
- end
614
- end
615
-
616
- sig do
617
- params(
618
- dep_scan_req: Dependabot::DependencyRequirement,
619
- parsing_req: Dependabot::DependencyRequirement
620
- ).returns(Dependabot::DependencyRequirement)
621
- end
622
- def merge_requirement(dep_scan_req, parsing_req)
623
- # We prefer file and requirement properties from parsed requirements,
624
- # because they include correct file and not evaluated property value.
625
- merged_req = {
626
- requirement: parsing_req.requirement,
627
- file: parsing_req.file,
628
- groups: [*dep_scan_req.groups, *parsing_req.groups].uniq.compact,
629
- source: dep_scan_req.source,
630
- metadata: merge_metadata(T.must(dep_scan_req.metadata), T.must(parsing_req.metadata))
631
- }
632
-
633
- Dependabot::DependencyRequirement.create(merged_req)
634
- end
635
-
636
- # Merge metadata from two requirements, combining all keys
637
- sig do
638
- params(
639
- metadata1: Dependabot::DependencyRequirement::ObjectHash,
640
- metadata2: Dependabot::DependencyRequirement::ObjectHash
641
- ).returns(Dependabot::DependencyRequirement::ObjectHash)
642
- end
643
- def merge_metadata(metadata1, metadata2)
644
- metadata1.merge(metadata2) do |_key, old_value, new_value|
645
- case [old_value, new_value]
646
- in [nil, new_value] then new_value
647
- in [old_value, nil] then old_value
648
- in [old_value, new_value] if old_value == new_value then old_value
649
- else
650
- # If values differ, combine them
651
- [*old_value, *new_value].uniq
652
- end
653
- end
654
- end
655
-
656
543
  sig { params(doc: Nokogiri::XML::Document).returns(Nokogiri::XML::NodeSet) }
657
544
  def plugin_nodes(doc)
658
545
  doc.css(PLUGIN_SELECTOR, PLUGIN_ARTIFACT_ITEMS_SELECTOR)
@@ -1,15 +1,12 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "fileutils"
5
- require "open3"
6
- require "tempfile"
7
- require "uri"
8
4
  require "sorbet-runtime"
9
5
  require "nokogiri"
10
6
  require "dependabot/errors"
11
7
  require "dependabot/command_helpers"
12
8
  require "dependabot/shared_helpers"
9
+ require "dependabot/maven/shared/maven_settings"
13
10
 
14
11
  module Dependabot
15
12
  module Maven
@@ -49,33 +46,46 @@ module Dependabot
49
46
 
50
47
  DEPENDENCY_PLUGIN_VERSION = T.let(version, T.nilable(String))
51
48
 
49
+ # Inactivity timeout for the dependency tree scan. In batch mode Maven still logs each
50
+ # artifact download as it starts and finishes, and the tree only fetches small POMs,
51
+ # so this only trips when Maven is stuck (e.g. an unreachable registry).
52
+ DEPENDENCY_TREE_TIMEOUT = CommandHelpers::TIMEOUTS::DEFAULT
53
+
54
+ WRAPPER_MIRROR_ID = "dependabot-wrapper-mirror"
55
+ # Every remote repository except localhost and file-based ones.
56
+ WRAPPER_MIRROR_OF = "external:*"
57
+
58
+ # Runs `mvn dependency:tree` in the current directory, writing one JSON tree per module
59
+ # to `output_file`. Registries are set through a generated settings file.
60
+ # Raises `SharedHelpers::HelperSubprocessFailed` on failure or timeout.
52
61
  sig do
53
- params(file_name: String).void
62
+ params(
63
+ output_file: String,
64
+ mirror: T.nilable(Shared::MavenSettings::Mirror),
65
+ repository_urls: T::Array[String]
66
+ ).void
54
67
  end
55
- def self.run_mvn_dependency_tree_plugin(file_name)
68
+ def self.run_mvn_dependency_tree_plugin(output_file, mirror: nil, repository_urls: [])
56
69
  raise DependabotError, "Could not resolve maven-dependency-plugin version" unless DEPENDENCY_PLUGIN_VERSION
57
70
 
58
- proxy_url = URI.parse(ENV.fetch("HTTPS_PROXY"))
59
- stdout, _, status = Open3.capture3(
60
- { "PROXY_HOST" => proxy_url.host },
61
- "mvn",
62
- "dependency:#{DEPENDENCY_PLUGIN_VERSION}:tree",
63
- "-DoutputFile=#{file_name}",
64
- "-DoutputType=json",
65
- "-e"
66
- )
67
- Dependabot.logger.info("mvn dependency:tree output: STDOUT:#{stdout}")
68
- handle_tool_error(stdout) unless status.success?
69
- end
70
-
71
- sig { params(output: String).void }
72
- def self.handle_tool_error(output)
73
- if (match = output.match(TRANSFER_FAILURE_REGEX)) &&
74
- (match[:status_code] == "403" || match[:status_code] == "401")
75
- raise Dependabot::PrivateSourceAuthenticationFailure, match[:repository_url]
71
+ # Without a proxy, the proxy block would point Maven at an unknown host.
72
+ proxy_env = Shared::MavenSettings.proxy_env
73
+ Shared::MavenSettings.with_file(
74
+ mirror: mirror, repository_urls: repository_urls, proxy: proxy_env.any?
75
+ ) do |settings_path|
76
+ command = [
77
+ "mvn",
78
+ "dependency:#{DEPENDENCY_PLUGIN_VERSION}:tree",
79
+ "-DoutputFile=#{output_file}",
80
+ "-DoutputType=json",
81
+ "-B",
82
+ "-s",
83
+ settings_path
84
+ ]
85
+ SharedHelpers.run_shell_command(
86
+ command, env: proxy_env, timeout: DEPENDENCY_TREE_TIMEOUT
87
+ )
76
88
  end
77
-
78
- raise DependabotError, "mvn CLI failed with an unhandled error"
79
89
  end
80
90
 
81
91
  # Runs the Maven Wrapper plugin in the given directory to regenerate
@@ -133,68 +143,37 @@ module Dependabot
133
143
  # resolved version via a generated settings mirror; without it `mvn` falls back to Central
134
144
  # and hangs behind a no-egress registry. Absent when no version resolved, leaving the baked
135
145
  # Central default. The bounded timeout is explained on WRAPPER_DOWNLOAD_TIMEOUT.
136
- settings_file = registry_base && wrapper_settings_file(registry_base)
137
- settings_args = settings_file ? ["-s", T.must(settings_file.path)] : []
138
- begin
139
- output = SharedHelpers.run_shell_command(
146
+ run = lambda do |settings_args|
147
+ SharedHelpers.run_shell_command(
140
148
  cmd + settings_args,
141
149
  env: env,
142
150
  cwd: run_cwd,
143
151
  timeout: WRAPPER_DOWNLOAD_TIMEOUT
144
152
  )
145
- ensure
146
- settings_file&.close! # deletes the temp file
147
153
  end
154
+ output = if registry_base
155
+ Shared::MavenSettings.with_file(mirror: wrapper_mirror(registry_base)) do |path|
156
+ run.call(["-s", path])
157
+ end
158
+ else
159
+ run.call([])
160
+ end
148
161
  Dependabot.logger.info("mvn wrapper output: STDOUT:#{output}")
149
162
  output
150
163
  rescue SharedHelpers::HelperSubprocessFailed => e
151
164
  # `run_shell_command` raises HelperSubprocessFailed on a non-zero exit, and the
152
165
  # updater sanitizes that into an opaque `SubprocessFailed` that only reports the
153
166
  # command and hides the real Maven output. Log the full output and re-raise a
154
- # classified Dependabot error so operators get an actionable message, mirroring
155
- # the `run_mvn_dependency_tree_plugin` path.
167
+ # classified Dependabot error so operators get an actionable message.
156
168
  Dependabot.logger.warn("mvn wrapper command failed:\n#{e.message}")
157
169
  handle_wrapper_error(e)
158
170
  end
159
171
 
160
- # Writes a temporary Maven settings file that keeps the proxy block and adds a
161
- # mirror pointing every external repository at the resolved base. Registry
162
- # auth still flows through the Dependabot proxy; no credentials are written here.
163
- # The caller is responsible for deleting the returned file (via `close!`).
164
- sig { params(registry_base: String).returns(Tempfile) }
165
- def self.wrapper_settings_file(registry_base)
166
- file = Tempfile.new(["dependabot-mvn-wrapper-settings", ".xml"])
167
- file.write(wrapper_settings_xml(registry_base))
168
- file.close
169
- file
170
- end
171
-
172
- # Builds the settings XML used for the wrapper invocation: the same proxy block as
173
- # the baked settings (auth is injected by the proxy) plus a `mirrorOf=external:*`
174
- # mirror so plugin, transitive POM, and distribution resolution all route through
175
- # the resolved base instead of Central.
176
- sig { params(registry_base: String).returns(String) }
177
- def self.wrapper_settings_xml(registry_base)
178
- <<~XML
179
- <settings>
180
- <proxies>
181
- <proxy>
182
- <id>dependabot-proxy</id>
183
- <active>true</active>
184
- <protocol>http</protocol>
185
- <host>${env.PROXY_HOST}</host>
186
- <port>1080</port>
187
- </proxy>
188
- </proxies>
189
- <mirrors>
190
- <mirror>
191
- <id>dependabot-wrapper-mirror</id>
192
- <mirrorOf>external:*</mirrorOf>
193
- <url>#{registry_base.encode(xml: :text)}</url>
194
- </mirror>
195
- </mirrors>
196
- </settings>
197
- XML
172
+ # The wrapper has no repositories of its own, so every external repository (plugin,
173
+ # transitive POMs and the distribution) is routed to the registry that served the version.
174
+ sig { params(registry_base: String).returns(Shared::MavenSettings::Mirror) }
175
+ def self.wrapper_mirror(registry_base)
176
+ Shared::MavenSettings::Mirror.new(id: WRAPPER_MIRROR_ID, url: registry_base, mirror_of: WRAPPER_MIRROR_OF)
198
177
  end
199
178
 
200
179
  # Classifies a failed Maven Wrapper invocation into an actionable Dependabot
@@ -0,0 +1,111 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "tempfile"
5
+ require "uri"
6
+ require "sorbet-runtime"
7
+
8
+ module Dependabot
9
+ module Maven
10
+ module Shared
11
+ # Builds the temporary Maven `settings.xml` files passed to native `mvn` runs with `-s`.
12
+ # They keep the proxy block from the baked `~/.m2/settings.xml` (the Dependabot proxy
13
+ # injects registry auth, so no credentials are written here) and can add a mirror and
14
+ # extra repositories.
15
+ module MavenSettings
16
+ extend T::Sig
17
+
18
+ PROXIES_XML = <<~XML
19
+ <proxies>
20
+ <proxy>
21
+ <id>dependabot-proxy</id>
22
+ <active>true</active>
23
+ <protocol>http</protocol>
24
+ <host>${env.PROXY_HOST}</host>
25
+ <port>1080</port>
26
+ </proxy>
27
+ </proxies>
28
+ XML
29
+
30
+ # A settings `<mirror>` that routes matching repositories to one registry.
31
+ class Mirror < T::ImmutableStruct
32
+ const :id, String
33
+ const :url, String
34
+ const :mirror_of, String
35
+ end
36
+
37
+ # Writes a settings file, yields its path, and deletes it afterwards.
38
+ sig do
39
+ type_parameters(:R)
40
+ .params(
41
+ mirror: T.nilable(Mirror),
42
+ repository_urls: T::Array[String],
43
+ proxy: T::Boolean,
44
+ blk: T.proc.params(path: String).returns(T.type_parameter(:R))
45
+ )
46
+ .returns(T.type_parameter(:R))
47
+ end
48
+ def self.with_file(mirror: nil, repository_urls: [], proxy: true, &blk)
49
+ file = Tempfile.new(["dependabot-mvn-settings", ".xml"])
50
+ file.write(xml(mirror: mirror, repository_urls: repository_urls, proxy: proxy))
51
+ file.close
52
+ yield T.must(file.path)
53
+ ensure
54
+ file&.close!
55
+ end
56
+
57
+ sig do
58
+ params(mirror: T.nilable(Mirror), repository_urls: T::Array[String], proxy: T::Boolean).returns(String)
59
+ end
60
+ def self.xml(mirror: nil, repository_urls: [], proxy: true)
61
+ sections = proxy ? [PROXIES_XML] : []
62
+ sections << mirrors_xml(mirror) if mirror
63
+ sections << repositories_xml(repository_urls) if repository_urls.any?
64
+ "<settings>\n#{sections.join.gsub(/^(?=.)/, ' ')}</settings>\n"
65
+ end
66
+
67
+ # The environment the proxy block reads its host from.
68
+ sig { returns(T::Hash[String, String]) }
69
+ def self.proxy_env
70
+ proxy = ENV.fetch("HTTPS_PROXY", nil)
71
+ proxy ? { "PROXY_HOST" => URI.parse(proxy).host.to_s } : {}
72
+ end
73
+
74
+ sig { params(mirror: Mirror).returns(String) }
75
+ def self.mirrors_xml(mirror)
76
+ <<~XML
77
+ <mirrors>
78
+ <mirror>
79
+ <id>#{mirror.id.encode(xml: :text)}</id>
80
+ <mirrorOf>#{mirror.mirror_of.encode(xml: :text)}</mirrorOf>
81
+ <url>#{mirror.url.encode(xml: :text)}</url>
82
+ </mirror>
83
+ </mirrors>
84
+ XML
85
+ end
86
+ private_class_method :mirrors_xml
87
+
88
+ # Adds each URL as both a repository and a plugin repository, in an always-active profile.
89
+ sig { params(urls: T::Array[String]).returns(String) }
90
+ def self.repositories_xml(urls)
91
+ entries = urls.each_with_index.map do |url, index|
92
+ "<id>dependabot-registry-#{index + 1}</id><url>#{url.encode(xml: :text)}</url>"
93
+ end
94
+ <<~XML
95
+ <profiles>
96
+ <profile>
97
+ <id>dependabot-registries</id>
98
+ <repositories>#{entries.map { |e| "<repository>#{e}</repository>" }.join}</repositories>
99
+ <pluginRepositories>#{entries.map { |e| "<pluginRepository>#{e}</pluginRepository>" }.join}</pluginRepositories>
100
+ </profile>
101
+ </profiles>
102
+ <activeProfiles>
103
+ <activeProfile>dependabot-registries</activeProfile>
104
+ </activeProfiles>
105
+ XML
106
+ end
107
+ private_class_method :repositories_xml
108
+ end
109
+ end
110
+ end
111
+ end
@@ -59,6 +59,7 @@ module Dependabot
59
59
  next false if version_string(dep).nil?
60
60
 
61
61
  next false if includes_property_reference?(updated_version(dep))
62
+ next false unless writable_requirements?(dep)
62
63
 
63
64
  releases = VersionFinder.new(
64
65
  dependency: dep,
@@ -124,13 +125,10 @@ module Dependabot
124
125
  @dependencies_using_property ||= T.let(
125
126
  Maven::FileParser.new(
126
127
  dependency_files: dependency_files,
127
- source: nil
128
+ source: nil,
129
+ options: { skip_dependency_tree: true }
128
130
  ).parse.select do |dep|
129
- dep.requirements.any? do |r|
130
- next unless r.metadata_string("property_name") == property_name
131
-
132
- r.metadata_string("property_source") == property_source
133
- end
131
+ dep.requirements.any? { |requirement| uses_property?(requirement) }
134
132
  end,
135
133
  T.nilable(T::Array[Dependabot::Dependency])
136
134
  )
@@ -233,12 +231,7 @@ module Dependabot
233
231
 
234
232
  sig { params(dep: Dependabot::Dependency).returns(Dependabot::DependencyRequirement) }
235
233
  def declaring_property_requirement(dep)
236
- declaring_requirement =
237
- dep.requirements.find do |r|
238
- next false unless r.metadata_string("property_name") == property_name
239
-
240
- r.metadata_string("property_source") == property_source
241
- end
234
+ declaring_requirement = dep.requirements.find { |requirement| uses_property?(requirement) }
242
235
 
243
236
  return declaring_requirement if declaring_requirement
244
237
 
@@ -246,6 +239,12 @@ module Dependabot
246
239
  "Requirement not found for property #{property_name} from #{property_source || 'unknown source'}"
247
240
  end
248
241
 
242
+ sig { params(requirement: Dependabot::DependencyRequirement).returns(T::Boolean) }
243
+ def uses_property?(requirement)
244
+ requirement.metadata_string("property_name") == property_name &&
245
+ requirement.metadata_string("property_source") == property_source
246
+ end
247
+
249
248
  sig { params(dep: Dependabot::Dependency).returns(T::Array[Dependabot::DependencyRequirement]) }
250
249
  def updated_requirements(dep)
251
250
  @updated_requirements ||= T.let({}, T.nilable(T::Hash[String, T::Array[Dependabot::DependencyRequirement]]))
@@ -255,7 +254,25 @@ module Dependabot
255
254
  latest_version: updated_version(dep),
256
255
  source_url: source_url,
257
256
  properties_to_update: [property_name]
258
- ).updated_requirements
257
+ ).updated_requirements.map.with_index do |requirement, index|
258
+ next requirement unless requirement.metadata_string("property_name")
259
+
260
+ uses_property?(requirement) ? requirement : dep.requirements.fetch(index)
261
+ end
262
+ end
263
+
264
+ sig { params(dep: Dependabot::Dependency).returns(T::Boolean) }
265
+ def writable_requirements?(dep)
266
+ updated = updated_requirements(dep)
267
+ writable = dep.requirements.each_with_index.any? do |requirement, index|
268
+ uses_property?(requirement) && updated.fetch(index).requirement != requirement.requirement
269
+ end
270
+ return true if writable
271
+
272
+ Dependabot.logger.info(
273
+ "Cannot update shared property #{property_name}: no writable requirement changes for #{dep.name}"
274
+ )
275
+ false
259
276
  end
260
277
 
261
278
  sig { returns(Dependabot::Maven::FileParser::PropertyValueFinder) }
@@ -70,12 +70,14 @@ module Dependabot
70
70
  # Maven's version resolution algorithm is very simple: it just uses
71
71
  # the version defined "closest", with the first declaration winning
72
72
  # if two declarations are equally close. As a result, we can just
73
- # return that latest version unless dealing with a property dep.
73
+ # return the latest version with writable requirements unless dealing
74
+ # with a property dep.
74
75
  # https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Transitive_Dependencies
75
76
  return nil if version_comes_from_multi_dependency_property?
76
- return nil if version_comes_from_project_parent_version?
77
+ return nil if version_comes_from_parent_version?
77
78
 
78
- latest_version
79
+ version = latest_version
80
+ version if writable_requirements?(version)
79
81
  end
80
82
 
81
83
  sig { override.returns(T.nilable(Dependabot::Version)) }
@@ -88,7 +90,10 @@ module Dependabot
88
90
 
89
91
  sig { override.returns(T.nilable(Dependabot::Version)) }
90
92
  def lowest_resolvable_security_fix_version
91
- lowest_security_fix_version
93
+ return nil if version_comes_from_parent_version?
94
+
95
+ version = lowest_security_fix_version
96
+ version if writable_requirements?(version)
92
97
  end
93
98
 
94
99
  sig { override.returns(T.nilable(Dependabot::Version)) }
@@ -105,16 +110,7 @@ module Dependabot
105
110
 
106
111
  sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
107
112
  def updated_requirements
108
- property_names =
109
- declarations_using_a_property
110
- .filter_map { |req| req.metadata_string("property_name") }
111
-
112
- RequirementsUpdater.new(
113
- requirements: dependency.requirements,
114
- latest_version: preferred_resolvable_version&.to_s,
115
- source_url: preferred_version_details&.fetch(:source_url),
116
- properties_to_update: property_names
117
- ).updated_requirements
113
+ updated_requirements_for(preferred_resolvable_version)
118
114
  end
119
115
 
120
116
  sig { override.returns(T::Boolean) }
@@ -136,9 +132,40 @@ module Dependabot
136
132
 
137
133
  private
138
134
 
135
+ sig { params(version: T.nilable(Dependabot::Version)).returns(T::Boolean) }
136
+ def writable_requirements?(version)
137
+ return false unless version
138
+ return false unless requirements_unlocked_or_can_be?
139
+
140
+ updated = updated_requirements_for(version)
141
+ return true if updated.map(&:requirement) != dependency.requirements.map(&:requirement)
142
+
143
+ Dependabot.logger.info(
144
+ "Cannot update #{dependency.name} to #{version}: no writable requirement changes " \
145
+ "(requirements may be inherited or ranges)"
146
+ )
147
+ false
148
+ end
149
+
150
+ sig do
151
+ params(version: T.nilable(T.any(String, Gem::Version))).returns(T::Array[Dependabot::DependencyRequirement])
152
+ end
153
+ def updated_requirements_for(version)
154
+ property_names =
155
+ declarations_using_a_property
156
+ .filter_map { |req| req.metadata_string("property_name") }
157
+
158
+ RequirementsUpdater.new(
159
+ requirements: dependency.requirements,
160
+ latest_version: version&.to_s,
161
+ source_url: preferred_version_details&.fetch(:source_url),
162
+ properties_to_update: property_names
163
+ ).updated_requirements
164
+ end
165
+
139
166
  sig { override.returns(T::Boolean) }
140
167
  def latest_version_resolvable_with_full_unlock?
141
- return false if version_comes_from_project_parent_version?
168
+ return false if version_comes_from_parent_version?
142
169
  return false unless version_comes_from_multi_dependency_property?
143
170
 
144
171
  property_updater.update_possible?
@@ -244,16 +271,17 @@ module Dependabot
244
271
  @all_property_based_dependencies ||=
245
272
  Maven::FileParser.new(
246
273
  dependency_files: dependency_files,
247
- source: nil
274
+ source: nil,
275
+ options: { skip_dependency_tree: true }
248
276
  ).parse.select do |dep|
249
277
  dep.requirements.any? { |req| req.metadata_string("property_name") }
250
278
  end
251
279
  end
252
280
 
253
281
  sig { returns(T::Boolean) }
254
- def version_comes_from_project_parent_version?
282
+ def version_comes_from_parent_version?
255
283
  declarations_using_a_property.any? do |requirement|
256
- requirement.metadata_string("property_name") == "project.parent.version"
284
+ requirement.metadata_string("property_name")&.sub(/\A(?:pom|project)\./, "") == "parent.version"
257
285
  end
258
286
  end
259
287
  end
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.399.0
4
+ version: 0.400.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.399.0
18
+ version: 0.400.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.399.0
25
+ version: 0.400.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rexml
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -278,6 +278,7 @@ files:
278
278
  - lib/dependabot/maven/pom.xml
279
279
  - lib/dependabot/maven/requirement.rb
280
280
  - lib/dependabot/maven/shared/base_version_finder.rb
281
+ - lib/dependabot/maven/shared/maven_settings.rb
281
282
  - lib/dependabot/maven/shared/property_value_finding.rb
282
283
  - lib/dependabot/maven/shared/shared_metadata_finder.rb
283
284
  - lib/dependabot/maven/shared/shared_package_details_fetcher.rb
@@ -297,7 +298,7 @@ licenses:
297
298
  - MIT
298
299
  metadata:
299
300
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
300
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.399.0
301
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.400.0
301
302
  rdoc_options: []
302
303
  require_paths:
303
304
  - lib