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,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/shared_helpers"
5
+ require "dependabot/maven/file_parser/repositories_finder"
6
+ require "dependabot/maven/update_checker"
7
+ require "dependabot/maven/version"
8
+ require "dependabot/maven/requirement"
9
+
10
+ module Dependabot
11
+ module Maven
12
+ class UpdateChecker
13
+ class VersionFinder
14
+ TYPE_SUFFICES = %w(jre android java).freeze
15
+
16
+ def initialize(dependency:, dependency_files:, credentials:,
17
+ ignored_versions:)
18
+ @dependency = dependency
19
+ @dependency_files = dependency_files
20
+ @credentials = credentials
21
+ @ignored_versions = ignored_versions
22
+ @forbidden_urls = []
23
+ end
24
+
25
+ def latest_version_details
26
+ possible_versions = versions
27
+
28
+ unless wants_prerelease?
29
+ possible_versions =
30
+ possible_versions.
31
+ reject { |v| v.fetch(:version).prerelease? }
32
+ end
33
+
34
+ unless wants_date_based_version?
35
+ possible_versions =
36
+ possible_versions.
37
+ reject { |v| v.fetch(:version) > version_class.new(1900) }
38
+ end
39
+
40
+ possible_versions =
41
+ possible_versions.
42
+ select { |v| matches_dependency_version_type?(v.fetch(:version)) }
43
+
44
+ ignored_versions.each do |req|
45
+ ignore_req = Maven::Requirement.new(req.split(","))
46
+ possible_versions =
47
+ possible_versions.
48
+ reject { |v| ignore_req.satisfied_by?(v.fetch(:version)) }
49
+ end
50
+
51
+ possible_versions.reverse.find { |v| released?(v.fetch(:version)) }
52
+ end
53
+
54
+ def versions
55
+ version_details =
56
+ repositories.map do |repository_details|
57
+ url = repository_details.fetch("url")
58
+ dependency_metadata(repository_details).
59
+ css("versions > version").
60
+ select { |node| version_class.correct?(node.content) }.
61
+ map { |node| version_class.new(node.content) }.
62
+ map { |version| { version: version, source_url: url } }
63
+ end.flatten
64
+
65
+ if version_details.none? && forbidden_urls.any?
66
+ raise PrivateSourceAuthenticationFailure, forbidden_urls.first
67
+ end
68
+
69
+ version_details.sort_by { |details| details.fetch(:version) }
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :dependency, :dependency_files, :credentials,
75
+ :ignored_versions, :forbidden_urls
76
+
77
+ def wants_prerelease?
78
+ return false unless dependency.version
79
+ return false unless version_class.correct?(dependency.version)
80
+
81
+ version_class.new(dependency.version).prerelease?
82
+ end
83
+
84
+ def wants_date_based_version?
85
+ return false unless dependency.version
86
+ return false unless version_class.correct?(dependency.version)
87
+
88
+ version_class.new(dependency.version) >= version_class.new(100)
89
+ end
90
+
91
+ def released?(version)
92
+ repositories.any? do |repository_details|
93
+ url = repository_details.fetch("url")
94
+ response = Excon.get(
95
+ dependency_files_url(url, version),
96
+ user: repository_details.fetch("username"),
97
+ password: repository_details.fetch("password"),
98
+ idempotent: true,
99
+ **SharedHelpers.excon_defaults
100
+ )
101
+
102
+ artifact_id = dependency.name.split(":").last
103
+ type = dependency.requirements.first.
104
+ dig(:metadata, :packaging_type)
105
+ response.body.include?("#{artifact_id}-#{version}.#{type}")
106
+ rescue Excon::Error::Socket, Excon::Error::Timeout
107
+ false
108
+ end
109
+ end
110
+
111
+ def dependency_metadata(repository_details)
112
+ @dependency_metadata ||= {}
113
+ @dependency_metadata[repository_details.hash] ||=
114
+ begin
115
+ response = Excon.get(
116
+ dependency_metadata_url(repository_details.fetch("url")),
117
+ user: repository_details.fetch("username"),
118
+ password: repository_details.fetch("password"),
119
+ idempotent: true,
120
+ **SharedHelpers.excon_defaults
121
+ )
122
+ check_response(response, repository_details.fetch("url"))
123
+ Nokogiri::XML(response.body)
124
+ rescue Excon::Error::Socket, Excon::Error::Timeout
125
+ central =
126
+ Maven::FileParser::RepositoriesFinder::CENTRAL_REPO_URL
127
+ raise if repository_details.fetch("url") == central
128
+
129
+ Nokogiri::XML("")
130
+ end
131
+ end
132
+
133
+ def check_response(response, repository_url)
134
+ central =
135
+ Maven::FileParser::RepositoriesFinder::CENTRAL_REPO_URL
136
+
137
+ return unless [401, 403].include?(response.status)
138
+ return if @forbidden_urls.include?(repository_url)
139
+ return if repository_url == central
140
+
141
+ @forbidden_urls << repository_url
142
+ end
143
+
144
+ def repositories
145
+ return @repositories if @repositories
146
+
147
+ details = pom_repository_details + credentials_repository_details
148
+
149
+ @repositories =
150
+ details.reject do |repo|
151
+ next if repo["password"]
152
+
153
+ # Reject this entry if an identical one with a password exists
154
+ details.any? { |r| r["url"] == repo["url"] && r["password"] }
155
+ end
156
+ end
157
+
158
+ def pom_repository_details
159
+ @pom_repository_details ||=
160
+ Maven::FileParser::RepositoriesFinder.
161
+ new(dependency_files: dependency_files).
162
+ repository_urls(pom: pom).
163
+ map do |url|
164
+ { "url" => url, "username" => nil, "password" => nil }
165
+ end
166
+ end
167
+
168
+ def credentials_repository_details
169
+ credentials.
170
+ select { |cred| cred["type"] == "maven_repository" }.
171
+ map do |cred|
172
+ {
173
+ "url" => cred.fetch("url").gsub(%r{/+$}, ""),
174
+ "username" => cred.fetch("username", nil),
175
+ "password" => cred.fetch("password", nil)
176
+ }
177
+ end
178
+ end
179
+
180
+ def matches_dependency_version_type?(comparison_version)
181
+ return true unless dependency.version
182
+
183
+ current_type =
184
+ TYPE_SUFFICES.
185
+ find { |t| dependency.version.split(/[.\-]/).include?(t) }
186
+
187
+ version_type =
188
+ TYPE_SUFFICES.
189
+ find { |t| comparison_version.to_s.split(/[.\-]/).include?(t) }
190
+
191
+ current_type == version_type
192
+ end
193
+
194
+ def pom
195
+ filename = dependency.requirements.first.fetch(:file)
196
+ dependency_files.find { |f| f.name == filename }
197
+ end
198
+
199
+ def dependency_metadata_url(repository_url)
200
+ group_id, artifact_id = dependency.name.split(":")
201
+
202
+ "#{repository_url}/"\
203
+ "#{group_id.tr('.', '/')}/"\
204
+ "#{artifact_id}/"\
205
+ "maven-metadata.xml"
206
+ end
207
+
208
+ def dependency_files_url(repository_url, version)
209
+ group_id, artifact_id = dependency.name.split(":")
210
+
211
+ "#{repository_url}/"\
212
+ "#{group_id.tr('.', '/')}/"\
213
+ "#{artifact_id}/"\
214
+ "#{version}/"
215
+ end
216
+
217
+ def version_class
218
+ Maven::Version
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/update_checkers"
4
+ require "dependabot/update_checkers/base"
5
+ require "dependabot/maven/file_parser/property_value_finder"
6
+
7
+ module Dependabot
8
+ module Maven
9
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
10
+ require_relative "update_checker/requirements_updater"
11
+ require_relative "update_checker/version_finder"
12
+ require_relative "update_checker/property_updater"
13
+
14
+ def latest_version
15
+ latest_version_details&.fetch(:version)
16
+ end
17
+
18
+ def latest_resolvable_version
19
+ # Maven's version resolution algorithm is very simple: it just uses
20
+ # the version defined "closest", with the first declaration winning
21
+ # if two declarations are equally close. As a result, we can just
22
+ # return that latest version unless dealing with a property dep.
23
+ # https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Transitive_Dependencies
24
+ return nil if version_comes_from_multi_dependency_property?
25
+
26
+ latest_version
27
+ end
28
+
29
+ def latest_resolvable_version_with_no_unlock
30
+ # Irrelevant, since Maven has a single dependency file (the pom.xml).
31
+ #
32
+ # For completeness we ought to resolve the pom.xml and return the
33
+ # latest version that satisfies the current constraint AND any
34
+ # constraints placed on it by other dependencies. Seeing as we're
35
+ # never going to take any action as a result, though, we just return
36
+ # nil.
37
+ nil
38
+ end
39
+
40
+ def updated_requirements
41
+ property_names =
42
+ declarations_using_a_property.
43
+ map { |req| req.dig(:metadata, :property_name) }
44
+
45
+ RequirementsUpdater.new(
46
+ requirements: dependency.requirements,
47
+ latest_version: latest_version&.to_s,
48
+ source_url: latest_version_details&.fetch(:source_url),
49
+ properties_to_update: property_names
50
+ ).updated_requirements
51
+ end
52
+
53
+ def requirements_unlocked_or_can_be?
54
+ declarations_using_a_property.none? do |requirement|
55
+ prop_name = requirement.dig(:metadata, :property_name)
56
+ pom = dependency_files.find { |f| f.name == requirement[:file] }
57
+
58
+ declaration_pom_name =
59
+ property_value_finder.
60
+ property_details(property_name: prop_name, callsite_pom: pom)&.
61
+ fetch(:file)
62
+
63
+ declaration_pom_name == "remote_pom.xml" ||
64
+ declaration_pom_name.end_with?("pom_parent.xml")
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def latest_version_resolvable_with_full_unlock?
71
+ return false unless version_comes_from_multi_dependency_property?
72
+
73
+ property_updater.update_possible?
74
+ end
75
+
76
+ def updated_dependencies_after_full_unlock
77
+ property_updater.updated_dependencies
78
+ end
79
+
80
+ def numeric_version_up_to_date?
81
+ return false unless version_class.correct?(dependency.version)
82
+
83
+ super
84
+ end
85
+
86
+ def numeric_version_can_update?(requirements_to_unlock:)
87
+ return false unless version_class.correct?(dependency.version)
88
+
89
+ super
90
+ end
91
+
92
+ def latest_version_details
93
+ @latest_version_details ||= version_finder.latest_version_details
94
+ end
95
+
96
+ def version_finder
97
+ @version_finder ||=
98
+ VersionFinder.new(
99
+ dependency: dependency,
100
+ dependency_files: dependency_files,
101
+ credentials: credentials,
102
+ ignored_versions: ignored_versions
103
+ )
104
+ end
105
+
106
+ def property_updater
107
+ @property_updater ||=
108
+ PropertyUpdater.new(
109
+ dependency: dependency,
110
+ dependency_files: dependency_files,
111
+ target_version_details: latest_version_details,
112
+ credentials: credentials,
113
+ ignored_versions: ignored_versions
114
+ )
115
+ end
116
+
117
+ def property_value_finder
118
+ @property_value_finder ||=
119
+ Maven::FileParser::PropertyValueFinder.
120
+ new(dependency_files: dependency_files)
121
+ end
122
+
123
+ def version_comes_from_multi_dependency_property?
124
+ declarations_using_a_property.any? do |requirement|
125
+ property_name = requirement.fetch(:metadata).fetch(:property_name)
126
+ property_source = requirement.fetch(:metadata).
127
+ fetch(:property_source)
128
+
129
+ all_property_based_dependencies.any? do |dep|
130
+ next false if dep.name == dependency.name
131
+
132
+ dep.requirements.any? do |req|
133
+ next unless req.dig(:metadata, :property_name) == property_name
134
+
135
+ req.dig(:metadata, :property_source) == property_source
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ def declarations_using_a_property
142
+ @declarations_using_a_property ||=
143
+ dependency.requirements.
144
+ select { |req| req.dig(:metadata, :property_name) }
145
+ end
146
+
147
+ def all_property_based_dependencies
148
+ @all_property_based_dependencies ||=
149
+ Maven::FileParser.new(
150
+ dependency_files: dependency_files,
151
+ source: nil
152
+ ).parse.select do |dep|
153
+ dep.requirements.any? { |req| req.dig(:metadata, :property_name) }
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ Dependabot::UpdateCheckers.register("maven", Dependabot::Maven::UpdateChecker)
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/utils"
4
+
5
+ # Java versions use dots and dashes when tokenising their versions.
6
+ # Gem::Version converts a "-" to ".pre.", so we override the `to_s` method.
7
+ #
8
+ # See https://maven.apache.org/pom.html#Version_Order_Specification for details.
9
+
10
+ module Dependabot
11
+ module Maven
12
+ class Version < Gem::Version
13
+ NULL_VALUES = %w(0 final ga).freeze
14
+ PREFIXED_TOKEN_HIERARCHY = {
15
+ "." => { qualifier: 1, number: 4 },
16
+ "-" => { qualifier: 2, number: 3 }
17
+ }.freeze
18
+ NAMED_QUALIFIERS_HIERARCHY = {
19
+ "a" => 1, "alpha" => 1,
20
+ "b" => 2, "beta" => 2,
21
+ "m" => 3, "milestone" => 3,
22
+ "rc" => 4, "cr" => 4,
23
+ "snapshot" => 5,
24
+ "ga" => 6, "" => 6, "final" => 6,
25
+ "sp" => 7
26
+ }.freeze
27
+ VERSION_PATTERN =
28
+ '[0-9a-zA-Z]+(?>\.[0-9a-zA-Z]*)*(-[0-9A-Za-z-]*(\.[0-9A-Za-z-]*)*)?'
29
+ ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/.freeze
30
+
31
+ def self.correct?(version)
32
+ return false if version.nil?
33
+
34
+ version.to_s.match?(ANCHORED_VERSION_PATTERN)
35
+ end
36
+
37
+ def initialize(version)
38
+ @version_string = version.to_s
39
+ super(version.to_s.tr("_", "-"))
40
+ end
41
+
42
+ def to_s
43
+ @version_string
44
+ end
45
+
46
+ def prerelease?
47
+ tokens.any? do |token|
48
+ next false unless NAMED_QUALIFIERS_HIERARCHY[token]
49
+
50
+ NAMED_QUALIFIERS_HIERARCHY[token] < 6
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def tokens
57
+ @tokens ||=
58
+ begin
59
+ version = @version_string.to_s.downcase
60
+ version = fill_tokens(version)
61
+ version = trim_version(version)
62
+ split_into_prefixed_tokens(version).map { |t| t[1..-1] }
63
+ end
64
+ end
65
+
66
+ def <=>(other)
67
+ version = stringify_version(@version_string)
68
+ version = fill_tokens(version)
69
+ version = trim_version(version)
70
+
71
+ other_version = stringify_version(other)
72
+ other_version = fill_tokens(other_version)
73
+ other_version = trim_version(other_version)
74
+
75
+ version, other_version = convert_dates(version, other_version)
76
+
77
+ prefixed_tokens = split_into_prefixed_tokens(version)
78
+ other_prefixed_tokens = split_into_prefixed_tokens(other_version)
79
+
80
+ prefixed_tokens, other_prefixed_tokens =
81
+ pad_for_comparison(prefixed_tokens, other_prefixed_tokens)
82
+
83
+ prefixed_tokens.count.times.each do |index|
84
+ comp = compare_prefixed_token(
85
+ prefix: prefixed_tokens[index][0],
86
+ token: prefixed_tokens[index][1..-1] || "",
87
+ other_prefix: other_prefixed_tokens[index][0],
88
+ other_token: other_prefixed_tokens[index][1..-1] || ""
89
+ )
90
+ return comp unless comp.zero?
91
+ end
92
+
93
+ 0
94
+ end
95
+
96
+ def stringify_version(version)
97
+ version = version.to_s.downcase
98
+
99
+ # Not technically correct, but pragmatic
100
+ version.gsub(/^v(?=\d)/, "")
101
+ end
102
+
103
+ def fill_tokens(version)
104
+ # Add separators when transitioning from digits to characters
105
+ version = version.gsub(/(\d)([A-Za-z])/, '\1-\2')
106
+ version = version.gsub(/([A-Za-z])(\d)/, '\1-\2')
107
+
108
+ # Replace empty tokens with 0
109
+ version = version.gsub(/([\.\-])([\.\-])/, '\10\2')
110
+ version = version.gsub(/^([\.\-])/, '0\1')
111
+ version.gsub(/([\.\-])$/, '\10')
112
+ end
113
+
114
+ def trim_version(version)
115
+ version.split("-").map do |v|
116
+ parts = v.split(".")
117
+ parts = parts[0..-2] while NULL_VALUES.include?(parts&.last)
118
+ parts&.join(".")
119
+ end.compact.reject(&:empty?).join("-")
120
+ end
121
+
122
+ def convert_dates(version, other_version)
123
+ default = [version, other_version]
124
+ return default unless version.match?(/^\d{4}-?\d{2}-?\d{2}$/)
125
+ return default unless other_version.match?(/^\d{4}-?\d{2}-?\d{2}$/)
126
+
127
+ [version.delete("-"), other_version.delete("-")]
128
+ end
129
+
130
+ def split_into_prefixed_tokens(version)
131
+ ".#{version}".split(/(?=[\-\.])/)
132
+ end
133
+
134
+ def pad_for_comparison(prefixed_tokens, other_prefixed_tokens)
135
+ prefixed_tokens = prefixed_tokens.dup
136
+ other_prefixed_tokens = other_prefixed_tokens.dup
137
+
138
+ longest = [prefixed_tokens, other_prefixed_tokens].max_by(&:count)
139
+ shortest = [prefixed_tokens, other_prefixed_tokens].min_by(&:count)
140
+
141
+ longest.count.times do |index|
142
+ next unless shortest[index].nil?
143
+
144
+ shortest[index] = longest[index].start_with?(".") ? ".0" : "-"
145
+ end
146
+
147
+ [prefixed_tokens, other_prefixed_tokens]
148
+ end
149
+
150
+ def compare_prefixed_token(prefix:, token:, other_prefix:, other_token:)
151
+ token_type = token.match?(/^\d+$/) ? :number : :qualifier
152
+ other_token_type = other_token.match?(/^\d+$/) ? :number : :qualifier
153
+
154
+ hierarchy = PREFIXED_TOKEN_HIERARCHY.fetch(prefix).fetch(token_type)
155
+ other_hierarchy =
156
+ PREFIXED_TOKEN_HIERARCHY.fetch(other_prefix).fetch(other_token_type)
157
+
158
+ hierarchy_comparison = hierarchy <=> other_hierarchy
159
+ return hierarchy_comparison unless hierarchy_comparison.zero?
160
+
161
+ compare_token(token: token, other_token: other_token)
162
+ end
163
+
164
+ def compare_token(token:, other_token:)
165
+ if (token_hierarchy = NAMED_QUALIFIERS_HIERARCHY[token])
166
+ return -1 unless NAMED_QUALIFIERS_HIERARCHY[other_token]
167
+
168
+ return token_hierarchy <=> NAMED_QUALIFIERS_HIERARCHY[other_token]
169
+ end
170
+
171
+ return 1 if NAMED_QUALIFIERS_HIERARCHY[other_token]
172
+
173
+ token = token.to_i if token.match?(/^\d+$/)
174
+ other_token = other_token.to_i if other_token.match?(/^\d+$/)
175
+ token <=> other_token
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ Dependabot::Utils.register_version_class("maven", Dependabot::Maven::Version)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # These all need to be required so the various classes can be registered in a
4
+ # lookup table of package manager names to concrete classes.
5
+ require "dependabot/maven/file_fetcher"
6
+ require "dependabot/maven/file_parser"
7
+ require "dependabot/maven/update_checker"
8
+ require "dependabot/maven/file_updater"
9
+ require "dependabot/maven/metadata_finder"
10
+ require "dependabot/maven/requirement"
11
+ require "dependabot/maven/version"