dependabot-maven 0.317.0 → 0.318.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: 21397e0916880a257f48425d1d22031d2f428e3c126be658b2b6081984da8404
4
- data.tar.gz: 425286545ad1da908980dcfb20a7c8313c89862771e6af6c07d0b06ec1ad900c
3
+ metadata.gz: 7dddaacd9f55be6eed5e719f3e4fb005d0e09a1cbcaedaa434a74c3802e8ba4f
4
+ data.tar.gz: f7080365f342e6ecfdc094718c5bb186d2e228b014bb7e824db1648814320302
5
5
  SHA512:
6
- metadata.gz: af0b726b16f926ac7052072253438bbed9cb899e20264dc4d19942c879dfefec73199eabaa3c13d7f89ea4f433365b2335bfb563c31d867072e37e4aaf26aa05
7
- data.tar.gz: 4f2c3c8005a80ac3407939e5f8a94dd398da02b407bc35b41d43b075d2b275a1bff39e0c52732b35252d4342da4c3f86920061f97e3621bfb416725762c0e2b4
6
+ metadata.gz: a4f0f5efb938e2ae503c50e562be4169afaa47a5a058757938f69395dacc3c46693488daaf9db2e6151bdbf0d03c7dd879be15de5f8840aa4dd15abf04913e15
7
+ data.tar.gz: 6308ea5cb891f6f7bed9f9095381bdaceacd75ef3fef890b7d2fcdce0b5ac3b84f800924b0ff718f4fcc8cb35bcdc5c85592a99606c7af35bf77ec598bf5fb0c
@@ -0,0 +1,126 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/maven/file_parser"
7
+ require "dependabot/maven/native_helpers"
8
+
9
+ module Dependabot
10
+ module Maven
11
+ class FileParser
12
+ class MavenDependencyParser
13
+ extend T::Sig
14
+ require "dependabot/file_parsers/base/dependency_set"
15
+
16
+ DEPENDENCY_OUTPUT_FILE = "dependency-tree-output.json"
17
+
18
+ sig do
19
+ params(dependency_files: T::Array[Dependabot::DependencyFile])
20
+ .returns(Dependabot::FileParsers::Base::DependencySet)
21
+ end
22
+ def self.build_dependency_set(dependency_files)
23
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
24
+
25
+ # Copy only pom.xml files to a temporary directory to
26
+ # output the dependency tree without building the project
27
+ SharedHelpers.in_a_temporary_directory do |temp_path|
28
+ # Create a directory structure that maintains relative relationships
29
+ project_directory = create_directory_structure(dependency_files, temp_path.to_s)
30
+
31
+ dependency_files.each do |pom|
32
+ pom_path = File.join(project_directory, pom.name)
33
+ pom_dir = File.dirname(pom_path)
34
+ FileUtils.mkdir_p(pom_dir)
35
+ File.write(pom_path, pom.content)
36
+ end
37
+
38
+ Dir.chdir(project_directory) do
39
+ NativeHelpers.run_mvn_dependency_tree_plugin(DEPENDENCY_OUTPUT_FILE)
40
+ end
41
+
42
+ # mvn CLI outputs dependency tree for each pom.xml file, collect them
43
+ # add into single dependency set
44
+ dependency_files.each do |pom|
45
+ pom_path = File.join(project_directory, pom.name)
46
+ pom_dir = File.dirname(pom_path)
47
+ output_file = File.join(pom_dir, DEPENDENCY_OUTPUT_FILE)
48
+
49
+ # If we run updater from sub-module, parent file might be included in dependency files,
50
+ # but mvn CLI will not generate dependency tree for it unless we start from the parent.
51
+ # In that case we can just skip it and focus only on current file and it's sub-modules.
52
+ unless File.exist?(output_file)
53
+ Dependabot.logger.warn("Dependency tree output file not found: #{output_file}")
54
+ next
55
+ end
56
+
57
+ dependency_tree = JSON.parse(File.read(output_file))
58
+ extract_dependencies_from_tree(pom, dependency_set, dependency_tree)
59
+ end
60
+ end
61
+
62
+ dependency_set
63
+ end
64
+
65
+ sig do
66
+ params(pom: Dependabot::DependencyFile,
67
+ dependency_set: Dependabot::FileParsers::Base::DependencySet,
68
+ dependency_tree: T::Hash[String, T.untyped]).void
69
+ end
70
+ def self.extract_dependencies_from_tree(pom, dependency_set, dependency_tree)
71
+ traverse_tree = T.let(-> {}, T.proc.params(node: T::Hash[String, T.untyped]).void)
72
+ traverse_tree = lambda do |node|
73
+ artifact_id = node["artifactId"]
74
+ group_id = node["groupId"]
75
+ version = node["version"]
76
+ type = node["type"]
77
+ classifier = node["classifier"].to_s.empty? ? nil : node["classifier"]
78
+ scope = node["scope"]
79
+
80
+ groups = scope == "test" ? ["test"] : []
81
+ dependency_set << Dependabot::Dependency.new(
82
+ name: "#{group_id}:#{artifact_id}",
83
+ version: version,
84
+ package_manager: "maven",
85
+ requirements: [{
86
+ requirement: version,
87
+ file: nil,
88
+ groups: groups,
89
+ source: nil,
90
+ metadata: {
91
+ packaging_type: type,
92
+ classifier: classifier,
93
+ pom_file: pom.name
94
+ }
95
+ }]
96
+ )
97
+
98
+ node["children"]&.each(&traverse_tree)
99
+ end
100
+
101
+ traverse_tree.call(dependency_tree)
102
+ end
103
+
104
+ sig do
105
+ params(dependency_files: T::Array[Dependabot::DependencyFile], temp_path: String)
106
+ .returns(String)
107
+ end
108
+ def self.create_directory_structure(dependency_files, temp_path)
109
+ # Find the topmost directory level by finding the minimum number of "../" sequences
110
+ relative_top_depth = dependency_files.map do |pom|
111
+ Pathname.new(pom.name).cleanpath.to_s.scan("../").length
112
+ end.max || 0
113
+
114
+ # Create the base directory structure with the required depth
115
+ base_depth_path = (0...relative_top_depth).reduce(temp_path) do |path, i|
116
+ File.join(path, "l#{i}")
117
+ end
118
+
119
+ FileUtils.mkdir_p(base_depth_path)
120
+
121
+ base_depth_path
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require "nokogiri"
5
5
  require "sorbet-runtime"
6
+ require "open3"
6
7
 
7
8
  require "dependabot/dependency"
8
9
  require "dependabot/file_parsers"
@@ -16,9 +17,11 @@ require "dependabot/errors"
16
17
  # - http://maven.apache.org/pom.html
17
18
  module Dependabot
18
19
  module Maven
20
+ # rubocop:disable Metrics/ClassLength
19
21
  class FileParser < Dependabot::FileParsers::Base
20
22
  extend T::Sig
21
23
  require "dependabot/file_parsers/base/dependency_set"
24
+ require_relative "file_parser/maven_dependency_parser"
22
25
  require_relative "file_parser/property_value_finder"
23
26
 
24
27
  # The following "dependencies" are candidates for updating:
@@ -41,9 +44,30 @@ module Dependabot
41
44
  sig { override.returns(T::Array[Dependabot::Dependency]) }
42
45
  def parse
43
46
  dependency_set = DependencySet.new
44
- pomfiles.each { |pom| dependency_set += pomfile_dependencies(pom) }
45
- extensionfiles.each { |extension| dependency_set += extensionfile_dependencies(extension) }
46
- dependency_set.dependencies
47
+
48
+ dependencies = []
49
+ if Dependabot::Experiments.enabled?(:maven_transitive_dependencies)
50
+ dependency_set += MavenDependencyParser.build_dependency_set(pomfiles)
51
+
52
+ pomfiles.each { |pom| dependency_set += pomfile_dependencies(pom) }
53
+ extensionfiles.each { |extension| dependency_set += extensionfile_dependencies(extension) }
54
+
55
+ dependency_set.dependencies.each do |dep|
56
+ requirements = merge_requirements(dep.requirements)
57
+ dependencies << Dependabot::Dependency.new(
58
+ name: dep.name,
59
+ version: dep.version,
60
+ package_manager: "maven",
61
+ requirements: requirements
62
+ )
63
+ end
64
+ else
65
+ pomfiles.each { |pom| dependency_set += pomfile_dependencies(pom) }
66
+ extensionfiles.each { |extension| dependency_set += extensionfile_dependencies(extension) }
67
+ dependencies = dependency_set.dependencies
68
+ end
69
+
70
+ dependencies
47
71
  end
48
72
 
49
73
  sig { returns(Ecosystem) }
@@ -386,7 +410,75 @@ module Dependabot
386
410
  def check_required_files
387
411
  raise "No pom.xml!" unless get_original_file("pom.xml")
388
412
  end
413
+
414
+ # Merge dependency scan requirements with file parsing requirements.
415
+ # Since dependency scan evaluates properties, we need to combine results with XML parsing,
416
+ # so we know when certain requirement not a literal value and can differentiate transitive dependencies
417
+ # from direct dependencies.
418
+ sig do
419
+ params(requirements: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Array[T::Hash[Symbol, T.untyped]])
420
+ end
421
+ def merge_requirements(requirements)
422
+ return requirements if requirements.length <= 1
423
+
424
+ merged = []
425
+ used_indices = Set.new
426
+ requirements.each_with_index do |dep_scan_req, i|
427
+ next if used_indices.include?(i) || dep_scan_req.dig(:metadata, :pom_file).nil?
428
+
429
+ # Look for another requirement where pom_file matches property_source
430
+ match_index = requirements.find_index.with_index do |parsing_req, j|
431
+ j > i &&
432
+ !used_indices.include?(j) &&
433
+ dep_scan_req.dig(:metadata, :pom_file) == parsing_req.fetch(:file)
434
+ end
435
+
436
+ if match_index
437
+ parsing_req = T.must(requirements[match_index])
438
+
439
+ # Merge the two requirements
440
+ # We prefer file and requirement properties from parsed requirements,
441
+ # because they include correct file and not evaluated property value.
442
+ merged_req = {
443
+ requirement: parsing_req[:requirement],
444
+ file: parsing_req[:file],
445
+ groups: [*dep_scan_req[:groups], *parsing_req[:groups]].uniq.compact,
446
+ source: dep_scan_req[:source],
447
+ metadata: merge_metadata(dep_scan_req[:metadata], parsing_req[:metadata])
448
+ }
449
+
450
+ merged << merged_req
451
+ used_indices.add(i)
452
+ used_indices.add(match_index)
453
+ else
454
+ # No match found, keep the requirement as is
455
+ merged << dep_scan_req
456
+ used_indices.add(i)
457
+ end
458
+ end
459
+
460
+ merged
461
+ end
462
+
463
+ # Merge metadata from two requirements, combining all keys
464
+ sig do
465
+ params(metadata1: T::Hash[Symbol, T.untyped],
466
+ metadata2: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped])
467
+ end
468
+ def merge_metadata(metadata1, metadata2)
469
+ metadata1.merge(metadata2) do |_key, old_value, new_value|
470
+ case [old_value, new_value]
471
+ in [nil, new_value] then new_value
472
+ in [old_value, nil] then old_value
473
+ in [old_value, new_value] if old_value == new_value then old_value
474
+ else
475
+ # If values differ, combine them
476
+ [*old_value, *new_value].uniq
477
+ end
478
+ end
479
+ end
389
480
  end
481
+ # rubocop:enable Metrics/ClassLength
390
482
  end
391
483
  end
392
484
 
@@ -61,7 +61,7 @@ module Dependabot
61
61
 
62
62
  sig { returns(Dependabot::DependencyFile) }
63
63
  def declaring_pom
64
- filename = declaring_requirement.fetch(:file)
64
+ filename = declaring_requirement.fetch(:file) || declaring_requirement.dig(:metadata, :pom_file)
65
65
  declaring_pom = dependency_files.find { |f| f.name == filename }
66
66
  return declaring_pom if declaring_pom
67
67
 
@@ -74,13 +74,14 @@ module Dependabot
74
74
  raise "Bad req match" unless new_req[:file] == T.must(old_req)[:file]
75
75
  next if new_req[:requirement] == T.must(old_req)[:requirement]
76
76
 
77
+ file_name = T.let(new_req.fetch(:file) || new_req.dig(:metadata, :pom_file), String)
77
78
  if new_req.dig(:metadata, :property_name)
78
79
  files = update_pomfiles_for_property_change(files, new_req)
79
- pom = files.find { |f| f.name == new_req.fetch(:file) }
80
+ pom = files.find { |f| f.name == file_name }
80
81
  files[T.must(files.index(pom))] =
81
82
  remove_property_suffix_in_pom(dependency, T.must(pom), T.must(old_req))
82
83
  else
83
- file = files.find { |f| f.name == new_req.fetch(:file) }
84
+ file = files.find { |f| f.name == file_name }
84
85
  files[T.must(files.index(file))] =
85
86
  update_version_in_file(dependency, T.must(file), T.must(old_req), new_req)
86
87
  end
@@ -119,11 +120,20 @@ module Dependabot
119
120
  end
120
121
  def update_version_in_file(dependency, file, previous_req, requirement)
121
122
  updated_content = T.must(file.content)
122
-
123
- original_file_declarations(dependency, previous_req).each do |old_dec|
124
- updated_content = updated_content.gsub(old_dec) do
125
- updated_file_declaration(old_dec, previous_req, requirement)
123
+ original_file_declarations = original_file_declarations(dependency, previous_req)
124
+
125
+ if original_file_declarations.any?
126
+ # If the file already has a declaration for this dependency, we
127
+ # update the existing declaration with the new version.
128
+ original_file_declarations.each do |old_dec|
129
+ updated_content = updated_content.gsub(old_dec) do
130
+ updated_file_declaration(old_dec, previous_req, requirement)
131
+ end
126
132
  end
133
+ else
134
+ # If the file does not have a declaration for this dependency, we
135
+ # add a new declaration for it.
136
+ updated_content = add_new_declaration(updated_content, dependency, requirement)
127
137
  end
128
138
 
129
139
  raise "Expected content to change!" if updated_content == file.content
@@ -131,6 +141,53 @@ module Dependabot
131
141
  updated_file(file: file, content: updated_content)
132
142
  end
133
143
 
144
+ sig do
145
+ params(
146
+ content: String,
147
+ dependency: Dependabot::Dependency,
148
+ requirement: T::Hash[Symbol, T.untyped]
149
+ ).returns(String)
150
+ end
151
+ def add_new_declaration(content, dependency, requirement) # rubocop:disable Metrics/AbcSize
152
+ doc = Nokogiri::XML(content) { |config| config.default_xml.noblanks }
153
+ doc.remove_namespaces!
154
+
155
+ project = doc.at_xpath("//project")
156
+ raise "<project> element not found in the XML content" unless project
157
+
158
+ dependency_management = project.at_xpath("dependencyManagement")
159
+ unless dependency_management
160
+ dependency_management = Nokogiri::XML::Node.new("dependencyManagement", doc)
161
+ dependencies = Nokogiri::XML::Node.new("dependencies", doc)
162
+ dependency_management.add_child(dependencies)
163
+ project.add_child(dependency_management)
164
+ end
165
+
166
+ dependencies = dependency_management.at_xpath("dependencies")
167
+ unless dependencies
168
+ dependencies = Nokogiri::XML::Node.new("dependencies", doc)
169
+ dependency_management.add_child(dependencies)
170
+ end
171
+
172
+ dependency_node = Nokogiri::XML::Node.new("dependency", doc)
173
+
174
+ group_id = Nokogiri::XML::Node.new("groupId", doc)
175
+ group_id.content = dependency.name.split(":").first
176
+ dependency_node.add_child(group_id)
177
+
178
+ artifact_id = Nokogiri::XML::Node.new("artifactId", doc)
179
+ artifact_id.content = dependency.name.split(":").last
180
+ dependency_node.add_child(artifact_id)
181
+
182
+ version = Nokogiri::XML::Node.new("version", doc)
183
+ version.content = requirement.fetch(:requirement)
184
+ dependency_node.add_child(version)
185
+
186
+ dependencies.add_child(dependency_node)
187
+
188
+ doc.to_xml
189
+ end
190
+
134
191
  sig do
135
192
  params(
136
193
  dep: Dependabot::Dependency,
@@ -0,0 +1,41 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "shellwords"
5
+ require "sorbet-runtime"
6
+
7
+ module Dependabot
8
+ module Maven
9
+ module NativeHelpers
10
+ extend T::Sig
11
+
12
+ sig do
13
+ params(file_name: String).void
14
+ end
15
+ def self.run_mvn_dependency_tree_plugin(file_name)
16
+ proxy_url = URI.parse(ENV.fetch("HTTPS_PROXY"))
17
+ stdout, _, status = Open3.capture3(
18
+ { "PROXY_HOST" => proxy_url.host },
19
+ "mvn",
20
+ "dependency:tree",
21
+ "-DoutputFile=#{file_name}",
22
+ "-DoutputType=json",
23
+ "-e"
24
+ )
25
+ Dependabot.logger.info("mvn dependency:tree output: STDOUT:#{stdout}")
26
+ handle_tool_error(stdout) unless status.success?
27
+ end
28
+
29
+ sig { params(output: String).void }
30
+ def self.handle_tool_error(output)
31
+ if (match = output.match(
32
+ %r{Could not transfer artifact (?<artifact>[^ ]+) from/to (?<repository_name>[^ ]+) \((?<repository_url>[^ ]+)\): status code: (?<status_code>[0-9]+)} # rubocop:disable Layout/LineLength
33
+ )) && (match[:status_code] == ("403") || match[:status_code] == ("401"))
34
+ raise Dependabot::PrivateSourceAuthenticationFailure, match[:repository_url]
35
+ end
36
+
37
+ raise DependabotError, "mvn CLI failed with an unhandled error"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -356,7 +356,8 @@ module Dependabot
356
356
  # Returns the POM file for the dependency, if it exists.
357
357
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
358
358
  def pom
359
- filename = dependency.requirements.first&.fetch(:file)
359
+ filename = dependency.requirements.first&.fetch(:file) ||
360
+ dependency.requirements.first&.dig(:metadata, :pom_file)
360
361
  dependency_files.find { |f| f.name == filename }
361
362
  end
362
363
 
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.317.0
4
+ version: 0.318.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.317.0
18
+ version: 0.318.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.317.0
25
+ version: 0.318.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -244,6 +244,7 @@ files:
244
244
  - lib/dependabot/maven.rb
245
245
  - lib/dependabot/maven/file_fetcher.rb
246
246
  - lib/dependabot/maven/file_parser.rb
247
+ - lib/dependabot/maven/file_parser/maven_dependency_parser.rb
247
248
  - lib/dependabot/maven/file_parser/pom_fetcher.rb
248
249
  - lib/dependabot/maven/file_parser/property_value_finder.rb
249
250
  - lib/dependabot/maven/file_parser/repositories_finder.rb
@@ -252,6 +253,7 @@ files:
252
253
  - lib/dependabot/maven/file_updater/property_value_updater.rb
253
254
  - lib/dependabot/maven/language.rb
254
255
  - lib/dependabot/maven/metadata_finder.rb
256
+ - lib/dependabot/maven/native_helpers.rb
255
257
  - lib/dependabot/maven/package/package_details_fetcher.rb
256
258
  - lib/dependabot/maven/package_manager.rb
257
259
  - lib/dependabot/maven/requirement.rb
@@ -268,7 +270,7 @@ licenses:
268
270
  - MIT
269
271
  metadata:
270
272
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
271
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.317.0
273
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.318.0
272
274
  rdoc_options: []
273
275
  require_paths:
274
276
  - lib