Class: Dependabot::Maven::Shared::SharedPackageDetailsFetcher

Inherits:
Object
  • Object
show all
Extended by:
T::Helpers, T::Sig
Defined in:
lib/dependabot/maven/shared/shared_package_details_fetcher.rb

Direct Known Subclasses

Package::PackageDetailsFetcher

Constant Summary collapse

MAVEN_METADATA_XML =
T.let("maven-metadata.xml", String)
REPOSITORY_TYPE =
T.let("maven_repository", String)
URL_KEY =
T.let("url", String)
AUTH_HEADERS_KEY =
T.let("auth_headers", String)
DEFAULT_CENTRAL_REPO_URL =
T.let("https://repo.maven.apache.org/maven2", String)

Instance Method Summary collapse

Instance Method Details

#auth_headers(maven_repo_url) ⇒ Object



397
398
399
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 397

def auth_headers(maven_repo_url)
  auth_headers_finder.auth_headers(maven_repo_url)
end

#auth_headers_finderObject



402
403
404
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 402

def auth_headers_finder
  @auth_headers_finder ||= T.let(Utils::AuthHeadersFinder.new(credentials), T.nilable(Utils::AuthHeadersFinder))
end

#central_repo_urlObject



378
379
380
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 378

def central_repo_url
  DEFAULT_CENTRAL_REPO_URL
end

#central_repo_urlsObject



384
385
386
387
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 384

def central_repo_urls
  central_url_without_protocol = central_repo_url.gsub(%r{^.*://}, "")
  %w(http:// https://).map { |p| p + central_url_without_protocol }
end

#check_response(response, repository_url) ⇒ Object



208
209
210
211
212
213
214
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 208

def check_response(response, repository_url)
  return unless [401, 403].include?(response.status)
  return if forbidden_urls.include?(repository_url)
  return if central_repo_urls.include?(repository_url)

  forbidden_urls << repository_url
end

#credentialsObject



30
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 30

def credentials; end

#credentials_repository_detailsObject



363
364
365
366
367
368
369
370
371
372
373
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 363

def credentials_repository_details
  credentials
    .select { |cred| cred["type"] == REPOSITORY_TYPE && cred[URL_KEY] }
    .map do |cred|
      url_value = cred.fetch(URL_KEY).gsub(%r{/+$}, "")
      {
        URL_KEY => url_value,
        AUTH_HEADERS_KEY => auth_headers(url_value)
      }
    end
end

#dependencyObject



27
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 27

def dependency; end

#dependency_base_url(repository_url) ⇒ Object



58
59
60
61
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 58

def dependency_base_url(repository_url)
  group_path, artifact_id = dependency_parts
  "#{repository_url}/#{group_path}/#{artifact_id}"
end

#dependency_files_url(repository_url, version) ⇒ Object



74
75
76
77
78
79
80
81
82
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 74

def dependency_files_url(repository_url, version)
  _, artifact_id = dependency_parts
  base_url = dependency_base_url(repository_url)
  type = dependency.requirements.first&.dig(:metadata, :packaging_type) || "jar"
  classifier = dependency.requirements.first&.dig(:metadata, :classifier)
  actual_classifier = classifier.nil? ? "" : "-#{classifier}"

  "#{base_url}/#{version}/#{artifact_id}-#{version}#{actual_classifier}.#{type}"
end

#dependency_metadata(repository_details) ⇒ Object



410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 410

def (repository_details)
  @dependency_metadata = T.let(
    @dependency_metadata, T.nilable(T::Hash[T.untyped, Nokogiri::XML::Document])
  )
  @dependency_metadata ||= {}
  repository_key = repository_details.hash
  return @dependency_metadata[repository_key] if @dependency_metadata.key?(repository_key)

  xml_document = (repository_details)
  @dependency_metadata[repository_key] ||= xml_document if xml_document
  @dependency_metadata[repository_key]
end

#dependency_metadata_from_html(repository_details) ⇒ Object



425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 425

def (repository_details)
  @dependency_metadata_from_html = T.let(
    @dependency_metadata_from_html, T.nilable(T::Hash[T.untyped, Nokogiri::HTML::Document])
  )
  @dependency_metadata_from_html ||= {}
  repository_key = repository_details.hash
  return @dependency_metadata_from_html[repository_key] if @dependency_metadata_from_html.key?(repository_key)

  html_document = (repository_details)
  @dependency_metadata_from_html[repository_key] ||= html_document if html_document
  @dependency_metadata_from_html[repository_key]
end

#dependency_metadata_url(repository_url) ⇒ Object



65
66
67
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 65

def (repository_url)
  "#{dependency_base_url(repository_url)}/#{MAVEN_METADATA_XML}"
end

#dependency_partsObject



44
45
46
47
48
49
50
51
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 44

def dependency_parts
  @dependency_parts = T.let(@dependency_parts, T.nilable([String, String]))
  return @dependency_parts if @dependency_parts

  group_id, artifact_id = dependency.name.split(":")
  group_path = T.must(group_id).tr(".", "/")
  @dependency_parts = [group_path, T.must(artifact_id)]
end

#dependency_pom_url(repository_url, version) ⇒ Object



87
88
89
90
91
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 87

def dependency_pom_url(repository_url, version)
  _, artifact_id = dependency_parts
  base_url = dependency_base_url(repository_url)
  "#{base_url}/#{version}/#{artifact_id}-#{version}.pom"
end

#extract_metadata_from_xml(xml, url) ⇒ Object



123
124
125
126
127
128
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 123

def (xml, url)
  xml.css("versions > version")
     .select { |node| version_class.correct?(node.content) }
     .map { |node| version_class.new(node.content) }
     .map { |version| { version: version, source_url: url } }
end


193
194
195
196
197
198
199
200
201
202
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 193

def extract_release_date_from_link(link)
  raw_date_text = link.next&.text.to_s
  date_match = raw_date_text.match(/\b(?:\d{4}-\d{2}-\d{2}|\d{2}-[A-Za-z]{3}-\d{4}) \d{2}:\d{2}\b/)

  return Time.parse(date_match[0]) if date_match

  Time.parse(raw_date_text.strip)
rescue StandardError
  nil
end

#extract_version_details_from_html(html_doc) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 163

def extract_version_details_from_html(html_doc)
  versions_detail_hash = T.let({}, T::Hash[String, T::Hash[Symbol, T.untyped]])

  html_doc.css("a[href]").each do |link|
    version = extract_version_from_link(link)
    next unless version && version_class.correct?(version)

    release_date = extract_release_date_from_link(link)

    versions_detail_hash[version] = { release_date: release_date }
  end

  versions_detail_hash
end


179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 179

def extract_version_from_link(link)
  href = link["href"]&.strip
  return unless href&.end_with?("/")

  identifier = [link["title"], link.text, href].find { |value| value && !value.strip.empty? }
  return unless identifier

  version = identifier.strip.gsub(%r{/$}, "")
  return if ["..", "."].include?(version)

  version
end

#fetch_dependency_metadata(repository_details) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 97

def (repository_details)
  url = repository_details.fetch(URL_KEY)
  headers = repository_details.fetch(AUTH_HEADERS_KEY)
  response = Dependabot::RegistryClient.get(
    url: (url),
    headers: headers
  )
  check_response(response, url)
  return unless response.status < 400

  Nokogiri::XML(response.body)
rescue URI::InvalidURIError
  nil
rescue Excon::Error::Socket, Excon::Error::Timeout,
       Excon::Error::TooManyRedirects => e
  handle_registry_error(url, e, response)
  nil
end

#fetch_dependency_metadata_from_html(repository_details) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 136

def (repository_details)
  url = repository_details.fetch(URL_KEY)
  headers = repository_details.fetch(AUTH_HEADERS_KEY)
  # Add trailing slash for directory listing
  base_directory_url = dependency_base_url(url)
  base_directory_url += "/" unless base_directory_url.end_with?("/")
  response = Dependabot::RegistryClient.get(
    url: base_directory_url,
    headers: headers
  )
  check_response(response, url)
  return unless response.status < 400

  Nokogiri::HTML(response.body)
rescue URI::InvalidURIError
  nil
rescue Excon::Error::Socket, Excon::Error::Timeout,
       Excon::Error::TooManyRedirects => e
  handle_registry_error(url, e, response)
  nil
end

#forbidden_urlsObject



390
391
392
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 390

def forbidden_urls
  @forbidden_urls ||= T.let([], T.nilable(T::Array[String]))
end

#handle_registry_error(url, error, response) ⇒ Object

Raises:

  • (RegistryError)


224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 224

def handle_registry_error(url, error, response)
  return unless central_repo_urls.include?(url)

  response_status = response&.status || 0
  response_body = if response
                    "RegistryError: #{response.status} response status with body #{response.body}"
                  else
                    "RegistryError: #{error.message}"
                  end

  raise RegistryError.new(response_status, response_body)
end

#released?(version) ⇒ Boolean

Returns:

  • (Boolean)


324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 324

def released?(version)
  @released_check = T.let(@released_check, T.nilable(T::Hash[Dependabot::Version, T::Boolean]))
  @released_check ||= {}
  return T.must(@released_check[version]) if @released_check.key?(version)

  @released_check[version] =
    repositories.any? do |repository_details|
      url = repository_details.fetch(URL_KEY)
      headers = repository_details.fetch(AUTH_HEADERS_KEY)
      response = Dependabot::RegistryClient.head(
        url: dependency_files_url(url, version),
        headers: headers
      )
      next true if response.status < 400

      # When the artifact file returns 404, fall back to checking the .pom file.
      # This handles artifacts with non-jar packaging (e.g., aar, bundle) where
      # the consumer POM omits <type>, causing the parser to default to "jar".
      # Only fall back when there's no classifier (classifier artifacts are specific).
      next false unless response.status == 404
      next false if dependency.requirements.first&.dig(:metadata, :classifier)

      pom_response = Dependabot::RegistryClient.head(
        url: dependency_pom_url(url, version),
        headers: headers
      )
      pom_response.status < 400
    rescue Excon::Error::Socket, Excon::Error::Timeout,
           Excon::Error::TooManyRedirects
      false
    rescue URI::InvalidURIError => e
      raise DependencyFileNotResolvable, e.message
    end
end

#repositoriesObject



35
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 35

def repositories; end

#version_classObject



441
442
443
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 441

def version_class
  dependency.version_class
end

#versionsObject



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 243

def versions
  @version_details = T.let(@version_details, T.nilable(T::Array[T::Hash[Symbol, T.untyped]]))
  return @version_details if @version_details

  @version_details = versions_details_from_xml

  begin
    versions_details_hash = versions_details_hash_from_html if @version_details.any?

    if versions_details_hash
      @version_details = @version_details.map do |vd|
        html_details = versions_details_hash[vd[:version].to_s]

        next vd unless html_details

        release_date = html_details[:release_date]

        next vd unless release_date

        vd.merge(
          release_date: html_details[:release_date],
          source_url: vd[:source_url]
        )
      end
    end
  rescue StandardError => e
    Dependabot.logger.error(
      "Error fetching version details from HTML: #{e.message}"
    )
  end

  @version_details = @version_details.sort_by { |d| d.fetch(:version) }
  @version_details
end

#versions_details_from_xmlObject

Raises:

  • (PrivateSourceAuthenticationFailure)


280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 280

def versions_details_from_xml
  forbidden_urls.clear
  version_details = repositories.flat_map do |repository_details|
    url = repository_details.fetch(URL_KEY)
    xml = (repository_details)
    next [] if xml.nil?

    (xml, url)
  end

  raise PrivateSourceAuthenticationFailure, forbidden_urls.first if version_details.none? && forbidden_urls.any?

  version_details
end

#versions_details_hash_from_htmlObject



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/dependabot/maven/shared/shared_package_details_fetcher.rb', line 297

def versions_details_hash_from_html
  forbidden_urls.clear

  versions_detail_hash = T.let(
    {}, T::Hash[String, T::Hash[Symbol, T.untyped]]
  )
  repositories.each do |repository_details|
    html = (repository_details)
    next if html.nil?

    versions_detail_hash = extract_version_details_from_html(html)
    break if versions_detail_hash.any?
  end

  if versions_detail_hash.any? && forbidden_urls.any?
    raise PrivateSourceAuthenticationFailure,
          forbidden_urls.first
  end

  versions_detail_hash
end