dependabot-maven 0.85.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/metadata_finders"
5
+ require "dependabot/metadata_finders/base"
6
+ require "dependabot/file_fetchers/base"
7
+ require "dependabot/maven/file_parser"
8
+ require "dependabot/maven/file_parser/repositories_finder"
9
+
10
+ module Dependabot
11
+ module Maven
12
+ class MetadataFinder < Dependabot::MetadataFinders::Base
13
+ DOT_SEPARATOR_REGEX = %r{\.(?:(?!\d+[.\/])+)}.freeze
14
+
15
+ private
16
+
17
+ def look_up_source
18
+ tmp_source = look_up_source_in_pom(dependency_pom_file)
19
+ return tmp_source if tmp_source
20
+
21
+ return unless (parent = parent_pom_file(dependency_pom_file))
22
+
23
+ tmp_source = look_up_source_in_pom(parent)
24
+ return unless tmp_source
25
+
26
+ artifact = dependency.name.split(":").last
27
+ return tmp_source if tmp_source.repo.end_with?(artifact)
28
+ return tmp_source if repo_has_subdir_for_dep?(tmp_source)
29
+ end
30
+
31
+ def repo_has_subdir_for_dep?(tmp_source)
32
+ @repo_has_subdir_for_dep ||= {}
33
+ if @repo_has_subdir_for_dep.key?(tmp_source)
34
+ return @repo_has_subdir_for_dep[tmp_source]
35
+ end
36
+
37
+ artifact = dependency.name.split(":").last
38
+ fetcher =
39
+ FileFetchers::Base.new(source: tmp_source, credentials: credentials)
40
+
41
+ @repo_has_subdir_for_dep[tmp_source] =
42
+ fetcher.send(:repo_contents, raise_errors: false).
43
+ select { |f| f.type == "dir" }.
44
+ any? { |f| artifact.end_with?(f.name) }
45
+ rescue Dependabot::RepoNotFound
46
+ @repo_has_subdir_for_dep[tmp_source] = false
47
+ end
48
+
49
+ def look_up_source_in_pom(pom)
50
+ potential_source_urls = [
51
+ pom.at_css("project > url")&.content,
52
+ pom.at_css("project > scm > url")&.content,
53
+ pom.at_css("project > issueManagement > url")&.content
54
+ ].compact
55
+
56
+ source_url = potential_source_urls.find { |url| Source.from_url(url) }
57
+ source_url ||= source_from_anywhere_in_pom(pom)
58
+ source_url = substitute_property_in_source_url(source_url, pom)
59
+
60
+ Source.from_url(source_url)
61
+ end
62
+
63
+ def substitute_property_in_source_url(source_url, pom)
64
+ return unless source_url
65
+ return source_url unless source_url.include?("${")
66
+
67
+ regex = Maven::FileParser::PROPERTY_REGEX
68
+ property_name = source_url.match(regex).named_captures["property"]
69
+ doc = pom.dup
70
+ doc.remove_namespaces!
71
+ nm = property_name.sub(/^pom\./, "").sub(/^project\./, "")
72
+ property_value =
73
+ loop do
74
+ candidate_node =
75
+ doc.at_xpath("/project/#{nm}") ||
76
+ doc.at_xpath("/project/properties/#{nm}") ||
77
+ doc.at_xpath("/project/profiles/profile/properties/#{nm}")
78
+ break candidate_node.content if candidate_node
79
+ break unless nm.match?(DOT_SEPARATOR_REGEX)
80
+
81
+ nm = nm.sub(DOT_SEPARATOR_REGEX, "/")
82
+ end
83
+
84
+ source_url.gsub("${#{property_name}}", property_value)
85
+ end
86
+
87
+ def source_from_anywhere_in_pom(pom)
88
+ github_urls = []
89
+ pom.to_s.scan(Source::SOURCE_REGEX) do
90
+ github_urls << Regexp.last_match.to_s
91
+ end
92
+
93
+ github_urls.find do |url|
94
+ repo = Source.from_url(url).repo
95
+ repo.end_with?(dependency.name.split(":").last)
96
+ end
97
+ end
98
+
99
+ def dependency_pom_file
100
+ return @dependency_pom_file unless @dependency_pom_file.nil?
101
+
102
+ artifact_id = dependency.name.split(":").last
103
+ response = Excon.get(
104
+ "#{maven_repo_dependency_url}/"\
105
+ "#{dependency.version}/"\
106
+ "#{artifact_id}-#{dependency.version}.pom",
107
+ headers: auth_details,
108
+ idempotent: true,
109
+ **SharedHelpers.excon_defaults
110
+ )
111
+
112
+ @dependency_pom_file = Nokogiri::XML(response.body)
113
+ rescue Excon::Error::Timeout
114
+ @dependency_pom_file = Nokogiri::XML("")
115
+ end
116
+
117
+ def parent_pom_file(pom)
118
+ doc = pom.dup
119
+ doc.remove_namespaces!
120
+ group_id = doc.at_xpath("/project/parent/groupId")&.content&.strip
121
+ artifact_id =
122
+ doc.at_xpath("/project/parent/artifactId")&.content&.strip
123
+ version = doc.at_xpath("/project/parent/version")&.content&.strip
124
+
125
+ return unless artifact_id && group_id && version
126
+
127
+ response = Excon.get(
128
+ "#{maven_repo_url}/#{group_id.tr('.', '/')}/#{artifact_id}/"\
129
+ "#{version}/"\
130
+ "#{artifact_id}-#{version}.pom",
131
+ headers: auth_details,
132
+ idempotent: true,
133
+ **SharedHelpers.excon_defaults
134
+ )
135
+
136
+ Nokogiri::XML(response.body)
137
+ end
138
+
139
+ def maven_repo_url
140
+ source = dependency.requirements.
141
+ find { |r| r&.fetch(:source) }&.fetch(:source)
142
+
143
+ source&.fetch(:url, nil) ||
144
+ source&.fetch("url") ||
145
+ Maven::FileParser::RepositoriesFinder::CENTRAL_REPO_URL
146
+ end
147
+
148
+ def maven_repo_dependency_url
149
+ group_id, artifact_id = dependency.name.split(":")
150
+
151
+ "#{maven_repo_url}/#{group_id.tr('.', '/')}/#{artifact_id}"
152
+ end
153
+
154
+ def auth_details
155
+ cred =
156
+ credentials.select { |c| c["type"] == "maven_repository" }.
157
+ find do |c|
158
+ cred_url = c.fetch("url").gsub(%r{/+$}, "")
159
+ next false unless cred_url == maven_repo_url
160
+
161
+ c.fetch("username", nil)
162
+ end
163
+
164
+ return {} unless cred
165
+
166
+ token = cred.fetch("username") + ":" + cred.fetch("password")
167
+ encoded_token = Base64.encode64(token).delete("\n")
168
+ { "Authorization" => "Basic #{encoded_token}" }
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ Dependabot::MetadataFinders.
175
+ register("maven", Dependabot::Maven::MetadataFinder)
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/utils"
4
+ require "dependabot/maven/version"
5
+
6
+ module Dependabot
7
+ module Maven
8
+ class Requirement < Gem::Requirement
9
+ quoted = OPS.keys.map { |k| Regexp.quote k }.join("|")
10
+ PATTERN_RAW =
11
+ "\\s*(#{quoted})?\\s*(#{Maven::Version::VERSION_PATTERN})\\s*"
12
+ PATTERN = /\A#{PATTERN_RAW}\z/.freeze
13
+
14
+ def self.parse(obj)
15
+ return ["=", Maven::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
16
+
17
+ unless (matches = PATTERN.match(obj.to_s))
18
+ msg = "Illformed requirement [#{obj.inspect}]"
19
+ raise BadRequirementError, msg
20
+ end
21
+
22
+ return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
23
+
24
+ [matches[1] || "=", Maven::Version.new(matches[2])]
25
+ end
26
+
27
+ def self.requirements_array(requirement_string)
28
+ split_java_requirement(requirement_string).map do |str|
29
+ new(str)
30
+ end
31
+ end
32
+
33
+ def initialize(*requirements)
34
+ requirements = requirements.flatten.flat_map do |req_string|
35
+ convert_java_constraint_to_ruby_constraint(req_string)
36
+ end
37
+
38
+ super(requirements)
39
+ end
40
+
41
+ def satisfied_by?(version)
42
+ version = Maven::Version.new(version.to_s)
43
+ super
44
+ end
45
+
46
+ private
47
+
48
+ def self.split_java_requirement(req_string)
49
+ req_string.split(/(?<=\]|\)),/).flat_map do |str|
50
+ next str if str.start_with?("(", "[")
51
+
52
+ exacts, *rest = str.split(/,(?=\[|\()/)
53
+ [*exacts.split(","), *rest]
54
+ end
55
+ end
56
+ private_class_method :split_java_requirement
57
+
58
+ def convert_java_constraint_to_ruby_constraint(req_string)
59
+ return unless req_string
60
+
61
+ if self.class.send(:split_java_requirement, req_string).count > 1
62
+ raise "Can't convert multiple Java reqs to a single Ruby one"
63
+ end
64
+
65
+ if req_string&.include?(",")
66
+ return convert_java_range_to_ruby_range(req_string)
67
+ end
68
+
69
+ convert_java_equals_req_to_ruby(req_string)
70
+ end
71
+
72
+ def convert_java_range_to_ruby_range(req_string)
73
+ lower_b, upper_b = req_string.split(",").map(&:strip)
74
+
75
+ lower_b =
76
+ if ["(", "["].include?(lower_b) then nil
77
+ elsif lower_b.start_with?("(") then "> #{lower_b.sub(/\(\s*/, '')}"
78
+ else ">= #{lower_b.sub(/\[\s*/, '').strip}"
79
+ end
80
+
81
+ upper_b =
82
+ if [")", "]"].include?(upper_b) then nil
83
+ elsif upper_b.end_with?(")") then "< #{upper_b.sub(/\s*\)/, '')}"
84
+ else "<= #{upper_b.sub(/\s*\]/, '').strip}"
85
+ end
86
+
87
+ [lower_b, upper_b].compact
88
+ end
89
+
90
+ def convert_java_equals_req_to_ruby(req_string)
91
+ return convert_wildcard_req(req_string) if req_string&.include?("+")
92
+
93
+ # If a soft requirement is being used, treat it as an equality matcher
94
+ return req_string unless req_string&.start_with?("[")
95
+
96
+ req_string.gsub(/[\[\]\(\)]/, "")
97
+ end
98
+
99
+ def convert_wildcard_req(req_string)
100
+ version = req_string.gsub(/(?:\.|^)\+/, "")
101
+ return ">= 0" if version.empty?
102
+
103
+ "~> #{version}.0"
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ Dependabot::Utils.
110
+ register_requirement_class("maven", Dependabot::Maven::Requirement)
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/maven/file_parser"
4
+ require "dependabot/maven/update_checker"
5
+ require "dependabot/maven/file_updater/declaration_finder"
6
+
7
+ module Dependabot
8
+ module Maven
9
+ class UpdateChecker
10
+ class PropertyUpdater
11
+ require_relative "requirements_updater"
12
+ require_relative "version_finder"
13
+
14
+ def initialize(dependency:, dependency_files:, credentials:,
15
+ target_version_details:, ignored_versions:)
16
+ @dependency = dependency
17
+ @dependency_files = dependency_files
18
+ @credentials = credentials
19
+ @ignored_versions = ignored_versions
20
+ @target_version = target_version_details&.fetch(:version)
21
+ @source_url = target_version_details&.fetch(:source_url)
22
+ end
23
+
24
+ def update_possible?
25
+ return false unless target_version
26
+
27
+ @update_possible ||=
28
+ dependencies_using_property.all? do |dep|
29
+ versions = VersionFinder.new(
30
+ dependency: dep,
31
+ dependency_files: dependency_files,
32
+ credentials: credentials,
33
+ ignored_versions: ignored_versions
34
+ ).versions.map { |v| v.fetch(:version) }
35
+
36
+ versions.include?(target_version) || versions.none?
37
+ end
38
+ end
39
+
40
+ def updated_dependencies
41
+ raise "Update not possible!" unless update_possible?
42
+
43
+ @updated_dependencies ||=
44
+ dependencies_using_property.map do |dep|
45
+ Dependency.new(
46
+ name: dep.name,
47
+ version: updated_version(dep),
48
+ requirements: updated_requirements(dep),
49
+ previous_version: dep.version,
50
+ previous_requirements: dep.requirements,
51
+ package_manager: dep.package_manager
52
+ )
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :dependency, :dependency_files, :target_version,
59
+ :source_url, :credentials, :ignored_versions
60
+
61
+ def dependencies_using_property
62
+ @dependencies_using_property ||=
63
+ Maven::FileParser.new(
64
+ dependency_files: dependency_files,
65
+ source: nil
66
+ ).parse.select do |dep|
67
+ dep.requirements.any? do |r|
68
+ next unless r.dig(:metadata, :property_name) == property_name
69
+
70
+ r.dig(:metadata, :property_source) == property_source
71
+ end
72
+ end
73
+ end
74
+
75
+ def property_name
76
+ @property_name ||= dependency.requirements.
77
+ find { |r| r.dig(:metadata, :property_name) }&.
78
+ dig(:metadata, :property_name)
79
+
80
+ raise "No requirement with a property name!" unless @property_name
81
+
82
+ @property_name
83
+ end
84
+
85
+ def property_source
86
+ @property_source ||=
87
+ dependency.requirements.
88
+ find { |r| r.dig(:metadata, :property_name) == property_name }&.
89
+ dig(:metadata, :property_source)
90
+ end
91
+
92
+ def version_string(dep)
93
+ declaring_requirement =
94
+ dep.requirements.
95
+ find { |r| r.dig(:metadata, :property_name) == property_name }
96
+
97
+ Maven::FileUpdater::DeclarationFinder.new(
98
+ dependency: dep,
99
+ declaring_requirement: declaring_requirement,
100
+ dependency_files: dependency_files
101
+ ).declaration_nodes.first.at_css("version")&.content
102
+ end
103
+
104
+ def pom
105
+ dependency_files.find { |f| f.name == "pom.xml" }
106
+ end
107
+
108
+ def updated_version(dep)
109
+ version_string(dep).gsub("${#{property_name}}", target_version.to_s)
110
+ end
111
+
112
+ def updated_requirements(dep)
113
+ @updated_requirements ||= {}
114
+ @updated_requirements[dep.name] ||=
115
+ RequirementsUpdater.new(
116
+ requirements: dep.requirements,
117
+ latest_version: updated_version(dep),
118
+ source_url: source_url,
119
+ properties_to_update: [property_name]
120
+ ).updated_requirements
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ #######################################################
4
+ # For more details on Maven version constraints, see: #
5
+ # https://maven.apache.org/pom.html#Dependencies #
6
+ #######################################################
7
+
8
+ require "dependabot/maven/update_checker"
9
+ require "dependabot/maven/version"
10
+ require "dependabot/maven/requirement"
11
+
12
+ module Dependabot
13
+ module Maven
14
+ class UpdateChecker
15
+ class RequirementsUpdater
16
+ def initialize(requirements:, latest_version:, source_url:,
17
+ properties_to_update:)
18
+ @requirements = requirements
19
+ @source_url = source_url
20
+ @properties_to_update = properties_to_update
21
+ return unless latest_version
22
+
23
+ @latest_version = version_class.new(latest_version)
24
+ end
25
+
26
+ def updated_requirements
27
+ return requirements unless latest_version
28
+
29
+ # Note: Order is important here. The FileUpdater needs the updated
30
+ # requirement at index `i` to correspond to the previous requirement
31
+ # at the same index.
32
+ requirements.map do |req|
33
+ next req if req.fetch(:requirement).nil?
34
+ next req if req.fetch(:requirement).include?(",")
35
+
36
+ property_name = req.dig(:metadata, :property_name)
37
+ if property_name && !properties_to_update.include?(property_name)
38
+ next req
39
+ end
40
+
41
+ new_req = update_requirement(req[:requirement])
42
+ req.merge(requirement: new_req, source: updated_source)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :requirements, :latest_version, :source_url,
49
+ :properties_to_update
50
+
51
+ def update_requirement(req_string)
52
+ if req_string.include?(".+")
53
+ update_dynamic_requirement(req_string)
54
+ else
55
+ # Since range requirements are excluded this must be exact
56
+ update_exact_requirement(req_string)
57
+ end
58
+ end
59
+
60
+ def update_exact_requirement(req_string)
61
+ old_version = requirement_class.new(req_string).
62
+ requirements.first.last
63
+ req_string.gsub(old_version.to_s, latest_version.to_s)
64
+ end
65
+
66
+ # This is really only a Gradle thing, but Gradle relies on this
67
+ # RequirementsUpdater too
68
+ def update_dynamic_requirement(req_string)
69
+ precision = req_string.split(".").take_while { |s| s != "+" }.count
70
+
71
+ version_parts = latest_version.segments.first(precision)
72
+
73
+ version_parts.join(".") + ".+"
74
+ end
75
+
76
+ def version_class
77
+ Maven::Version
78
+ end
79
+
80
+ def requirement_class
81
+ Maven::Requirement
82
+ end
83
+
84
+ def updated_source
85
+ { type: "maven_repo", url: source_url }
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end