dependabot-npm_and_yarn 0.303.0 → 0.304.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: ad45cf35ecbd2646efac000b10b1e75788d1b14717df839f7cecbdd8b0f528da
4
- data.tar.gz: a5263324b67553166c24f348da59a0845fec2d9c97562fc4db80474fd936feaf
3
+ metadata.gz: 03ceb27ff40ce2354675588a932c77c0aba9957d7ada32b82c73edcd4566c470
4
+ data.tar.gz: 9670f9f1102fe62f0b2d838aa359300040f486b93a181f768bb96869f514b15e
5
5
  SHA512:
6
- metadata.gz: f7e9fc7913ca0366e0eb18b31ab9add1e7007d575993b1e85f10401b66ffcd003a5d517de9c31ab0e39628d3f00aad1a904076fe0ca1de297c3b3985c4071734
7
- data.tar.gz: 04e7fa3cc8372cd13778b191bd68e5e9aecd5cf33989f8e2c7e10bbb1b98f3daa10b3da59d5b4bbf6d2c6cb04d7a63a0e665712bbf623822d68cf7f755f1e0c5
6
+ metadata.gz: c1891b008fe1683848ee2cbc5e0d2b28276e88e6cf728a5c5233bbf1f13b6dfc1c01ea48c575297e26a9ce9b0e89dbe3f98bb85055fcb4f17c6c31583363c4cb
7
+ data.tar.gz: e4e953398df4515cc1f81a894a4d24173e3a453f482ee3b33cc12eedd76bf7160131704ddf25f16bc9bff6ee27da15d813905c10dc93505879d46211aaf37b57
@@ -14,6 +14,29 @@ module Dependabot
14
14
  class PackageDetailsFetcher
15
15
  extend T::Sig
16
16
 
17
+ GLOBAL_REGISTRY = "registry.npmjs.org"
18
+ NPM_OFFICIAL_WEBSITE = "https://www.npmjs.com"
19
+
20
+ API_AUTHORIZATION_KEY = "Authorization"
21
+ API_AUTHORIZATION_VALUE_BASIC_PREFIX = "Basic"
22
+ API_RESPONSE_STATUS_SUCCESS_PREFIX = "2"
23
+
24
+ RELEASE_TIME_KEY = "time"
25
+ RELEASE_VERSIONS_KEY = "versions"
26
+ RELEASE_DIST_TAGS_KEY = "dist-tags"
27
+ RELEASE_DIST_TAGS_LATEST_KEY = "latest"
28
+ RELEASE_ENGINES_KEY = "engines"
29
+ RELEASE_LANGUAGE_KEY = "node"
30
+ RELEASE_DEPRECATION_KEY = "deprecated"
31
+ RELEASE_REPOSITORY_KEY = "repository"
32
+ RELEASE_PACKAGE_TYPE_KEY = "type"
33
+ RELEASE_PACKAGE_TYPE_GIT = "git"
34
+ RELEASE_PACKAGE_TYPE_NPM = "npm"
35
+
36
+ REGISTRY_FILE_NPMRC = ".npmrc"
37
+ REGISTRY_FILE_YARNRC = ".yarnrc"
38
+ REGISTRY_FILE_YARNRC_YML = ".yarnrc.yml"
39
+
17
40
  sig do
18
41
  params(
19
42
  dependency: Dependabot::Dependency,
@@ -33,6 +56,8 @@ module Dependabot
33
56
  @npm_details = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
34
57
  @dist_tags = T.let(nil, T.nilable(T::Hash[String, String]))
35
58
  @registry_finder = T.let(nil, T.nilable(Package::RegistryFinder))
59
+ @version_endpoint_working = T.let(nil, T.nilable(T::Boolean))
60
+ @yanked = T.let({}, T::Hash[Gem::Version, T.nilable(T::Boolean)])
36
61
  end
37
62
 
38
63
  sig { returns(Dependabot::Dependency) }
@@ -46,7 +71,7 @@ module Dependabot
46
71
 
47
72
  sig { returns(T.nilable(Dependabot::Package::PackageDetails)) }
48
73
  def fetch
49
- package_data = fetch_npm_details
74
+ package_data = npm_details
50
75
  Dependabot::Package::PackageDetails.new(
51
76
  dependency: @dependency,
52
77
  releases: package_data ? parse_versions(package_data) : [],
@@ -64,27 +89,91 @@ module Dependabot
64
89
  @npm_details ||= fetch_npm_details
65
90
  end
66
91
 
92
+ sig { returns(T::Boolean) }
93
+ def custom_registry?
94
+ registry_finder.custom_registry?
95
+ end
96
+
97
+ sig { returns(String) }
98
+ def dependency_url
99
+ registry_finder.dependency_url
100
+ end
101
+
102
+ sig { params(version: Gem::Version).returns(T::Boolean) }
103
+ def yanked?(version)
104
+ return @yanked[version] || false if @yanked.key?(version)
105
+
106
+ @yanked[version] =
107
+ begin
108
+ if dependency_registry == GLOBAL_REGISTRY
109
+ status = Dependabot::RegistryClient.head(
110
+ url: registry_finder.tarball_url(version),
111
+ headers: registry_auth_headers
112
+ ).status
113
+ else
114
+ status = Dependabot::RegistryClient.get(
115
+ url: dependency_url + "/#{version}",
116
+ headers: registry_auth_headers
117
+ ).status
118
+
119
+ if status == 404
120
+ # Some registries don't handle escaped package names properly
121
+ status = Dependabot::RegistryClient.get(
122
+ url: dependency_url.gsub("%2F", "/") + "/#{version}",
123
+ headers: registry_auth_headers
124
+ ).status
125
+ end
126
+ end
127
+
128
+ version_not_found = status == 404
129
+ version_not_found && version_endpoint_working?
130
+ rescue Excon::Error::Timeout, Excon::Error::Socket
131
+ # Give the benefit of the doubt if the registry is playing up
132
+ false
133
+ end
134
+
135
+ @yanked[version] || false
136
+ end
137
+
67
138
  private
68
139
 
140
+ sig { returns(T.nilable(T::Boolean)) }
141
+ def version_endpoint_working?
142
+ return true if dependency_registry == GLOBAL_REGISTRY
143
+
144
+ return @version_endpoint_working if @version_endpoint_working
145
+
146
+ @version_endpoint_working =
147
+ begin
148
+ Dependabot::RegistryClient.get(
149
+ url: dependency_url + "/#{RELEASE_DIST_TAGS_LATEST_KEY}",
150
+ headers: registry_auth_headers
151
+ ).status < 400
152
+ rescue Excon::Error::Timeout, Excon::Error::Socket
153
+ # Give the benefit of the doubt if the registry is playing up
154
+ true
155
+ end
156
+ @version_endpoint_working
157
+ end
158
+
69
159
  sig do
70
160
  params(
71
161
  npm_data: T::Hash[String, T.untyped]
72
162
  ).returns(T::Array[Dependabot::Package::PackageRelease])
73
163
  end
74
164
  def parse_versions(npm_data)
75
- time_data = npm_data["time"] || {}
76
- versions_data = npm_data["versions"] || {}
165
+ time_data = fetch_value_from_hash(npm_data, RELEASE_TIME_KEY) || {}
166
+ versions_data = fetch_value_from_hash(npm_data, RELEASE_VERSIONS_KEY) || {}
77
167
 
78
- latest_version = npm_data.dig("dist-tags", "latest")
168
+ dist_tags = fetch_value_from_hash(npm_data, RELEASE_DIST_TAGS_KEY)
169
+ latest_version = fetch_value_from_hash(dist_tags, RELEASE_DIST_TAGS_LATEST_KEY)
79
170
 
80
171
  versions_data.filter_map do |version, details|
81
172
  next unless Dependabot::NpmAndYarn::Version.correct?(version)
82
173
 
83
- package_type = details.dig("repository", "type")
84
-
85
- deprecated = details["deprecated"]
174
+ package_type = infer_package_type(details)
86
175
 
87
- puts "version: #{version}, #{latest_version}"
176
+ deprecated = fetch_value_from_hash(details, RELEASE_DEPRECATION_KEY)
88
177
 
89
178
  Dependabot::Package::PackageRelease.new(
90
179
  version: Version.new(version),
@@ -95,7 +184,8 @@ module Dependabot
95
184
  latest: latest_version.to_s == version,
96
185
  url: package_version_url(version),
97
186
  package_type: package_type,
98
- language: package_language(details)
187
+ language: package_language(details),
188
+ details: details
99
189
  )
100
190
  end.sort_by(&:version).reverse
101
191
  end
@@ -110,13 +200,16 @@ module Dependabot
110
200
  .returns(T.nilable(Dependabot::Package::PackageLanguage))
111
201
  end
112
202
  def package_language(version_details)
113
- node_requirement = version_details.dig("engines", "node")
203
+ # Fetch the engines hash from the version details
204
+ engines = version_details.is_a?(Hash) ? version_details[RELEASE_ENGINES_KEY] : nil
205
+ # Check if engines is a hash and fetch the node requirement
206
+ node_requirement = engines.is_a?(Hash) ? engines.fetch(RELEASE_LANGUAGE_KEY, nil) : nil
114
207
 
115
208
  return nil unless node_requirement
116
209
 
117
210
  if node_requirement
118
211
  Dependabot::Package::PackageLanguage.new(
119
- name: "node",
212
+ name: RELEASE_LANGUAGE_KEY,
120
213
  version: nil,
121
214
  requirement: Requirement.new(node_requirement)
122
215
  )
@@ -127,7 +220,7 @@ module Dependabot
127
220
 
128
221
  sig { returns(T.nilable(T::Hash[String, String])) }
129
222
  def dist_tags
130
- @dist_tags ||= npm_details&.fetch("dist-tags", nil)
223
+ @dist_tags ||= fetch_value_from_hash(npm_details, RELEASE_DIST_TAGS_KEY)
131
224
  end
132
225
 
133
226
  sig { returns(T.nilable(T::Hash[String, T.untyped])) }
@@ -136,9 +229,11 @@ module Dependabot
136
229
  check_npm_response(npm_response) if npm_response
137
230
  JSON.parse(npm_response.body)
138
231
  rescue JSON::ParserError, Excon::Error::Timeout, Excon::Error::Socket, RegistryError => e
139
- return nil if git_dependency?
140
-
141
- raise_npm_details_error(e)
232
+ if git_dependency?
233
+ nil
234
+ else
235
+ raise_npm_details_error(e)
236
+ end
142
237
  end
143
238
 
144
239
  sig { returns(Excon::Response) }
@@ -149,19 +244,20 @@ module Dependabot
149
244
  )
150
245
 
151
246
  # If response is successful, return it
152
- return response if response.status.to_s.start_with?("2")
247
+ return response if response.status.to_s.start_with?(API_RESPONSE_STATUS_SUCCESS_PREFIX)
153
248
 
154
249
  # If the registry is public (not explicitly private) and the request fails, return the response as is
155
- return response if dependency_registry == "registry.npmjs.org"
250
+ return response if dependency_registry == GLOBAL_REGISTRY
156
251
 
157
252
  # If a private registry returns a 500 error, check authentication
158
253
  return response unless response.status == 500
159
- return response unless registry_auth_headers["Authorization"]
160
254
 
161
- auth = registry_auth_headers["Authorization"]
162
- return response unless auth&.start_with?("Basic")
255
+ auth = fetch_value_from_hash(registry_auth_headers, API_AUTHORIZATION_KEY)
256
+ return response unless auth
163
257
 
164
- decoded_token = Base64.decode64(auth.gsub("Basic ", "")).strip
258
+ return response unless auth&.start_with?(API_AUTHORIZATION_VALUE_BASIC_PREFIX)
259
+
260
+ decoded_token = Base64.decode64(auth.gsub("#{API_AUTHORIZATION_VALUE_BASIC_PREFIX} ", "")).strip
165
261
 
166
262
  # Ensure decoded token is not empty and contains a colon
167
263
  if decoded_token.empty? || !decoded_token.include?(":")
@@ -181,6 +277,29 @@ module Dependabot
181
277
  raise DependencyFileNotResolvable, e.message
182
278
  end
183
279
 
280
+ sig do
281
+ params(
282
+ details: T::Hash[String, T.untyped],
283
+ git_dependency: T::Boolean
284
+ )
285
+ .returns(String)
286
+ end
287
+ def infer_package_type(details, git_dependency: false)
288
+ return RELEASE_PACKAGE_TYPE_GIT if git_dependency
289
+
290
+ repository = fetch_value_from_hash(details, RELEASE_REPOSITORY_KEY)
291
+
292
+ case repository
293
+ when String
294
+ return repository.start_with?("git+") ? RELEASE_PACKAGE_TYPE_GIT : RELEASE_PACKAGE_TYPE_NPM
295
+ when Hash
296
+ type = fetch_value_from_hash(repository, RELEASE_PACKAGE_TYPE_KEY)
297
+ return RELEASE_PACKAGE_TYPE_GIT if type == RELEASE_PACKAGE_TYPE_GIT
298
+ end
299
+
300
+ RELEASE_PACKAGE_TYPE_NPM
301
+ end
302
+
184
303
  sig { params(npm_response: Excon::Response).void }
185
304
  def check_npm_response(npm_response)
186
305
  return if git_dependency?
@@ -199,13 +318,13 @@ module Dependabot
199
318
  status = npm_response.status
200
319
 
201
320
  # handles issue when status 200 is returned from registry but with an invalid JSON object
202
- if status.to_s.start_with?("2") && response_invalid_json?(npm_response)
321
+ if status.to_s.start_with?(API_RESPONSE_STATUS_SUCCESS_PREFIX) && response_invalid_json?(npm_response)
203
322
  msg = "Invalid JSON object returned from registry #{dependency_registry}."
204
323
  Dependabot.logger.warn("#{msg} Response body (truncated) : #{npm_response.body[0..500]}...")
205
324
  raise DependencyFileNotResolvable, msg
206
325
  end
207
326
 
208
- return if status.to_s.start_with?("2")
327
+ return if status.to_s.start_with?(API_RESPONSE_STATUS_SUCCESS_PREFIX)
209
328
 
210
329
  # Ignore 404s from the registry for updates where a lockfile doesn't
211
330
  # need to be generated. The 404 won't cause problems later.
@@ -217,7 +336,7 @@ module Dependabot
217
336
 
218
337
  sig { params(error: StandardError).void }
219
338
  def raise_npm_details_error(error)
220
- raise if dependency_registry == "registry.npmjs.org"
339
+ raise if dependency_registry == GLOBAL_REGISTRY
221
340
  raise unless error.is_a?(Excon::Error::Timeout)
222
341
 
223
342
  raise PrivateSourceTimedOut, dependency_registry
@@ -229,10 +348,10 @@ module Dependabot
229
348
  return false unless [401, 402, 403, 404].include?(npm_response.status)
230
349
 
231
350
  # Check whether this dependency is (likely to be) private
232
- if dependency_registry == "registry.npmjs.org"
351
+ if dependency_registry == GLOBAL_REGISTRY
233
352
  return false unless dependency.name.start_with?("@")
234
353
 
235
- web_response = Dependabot::RegistryClient.get(url: "https://www.npmjs.com/package/#{dependency.name}")
354
+ web_response = Dependabot::RegistryClient.get(url: "#{NPM_OFFICIAL_WEBSITE}/package/#{dependency.name}")
236
355
  # NOTE: returns 429 when the login page is rate limited
237
356
  return web_response.body.include?("Forgot password?") ||
238
357
  web_response.status == 429
@@ -244,8 +363,10 @@ module Dependabot
244
363
  sig { params(npm_response: Excon::Response).returns(T::Boolean) }
245
364
  def private_dependency_server_error?(npm_response)
246
365
  if [500, 501, 502, 503].include?(npm_response.status)
247
- Dependabot.logger.warn("#{dependency_registry} returned code #{npm_response.status} with " \
248
- "body #{npm_response.body}.")
366
+ Dependabot.logger.warn(
367
+ "#{dependency_registry} returned code #{npm_response.status} " \
368
+ "with body #{npm_response.body}."
369
+ )
249
370
  return true
250
371
  end
251
372
  false
@@ -260,11 +381,6 @@ module Dependabot
260
381
  true
261
382
  end
262
383
 
263
- sig { returns(String) }
264
- def dependency_url
265
- registry_finder.dependency_url
266
- end
267
-
268
384
  sig { returns(T::Hash[String, String]) }
269
385
  def registry_auth_headers
270
386
  registry_finder.auth_headers
@@ -288,17 +404,17 @@ module Dependabot
288
404
 
289
405
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
290
406
  def npmrc_file
291
- dependency_files.find { |f| f.name.end_with?(".npmrc") }
407
+ dependency_files.find { |f| f.name.end_with?(REGISTRY_FILE_NPMRC) }
292
408
  end
293
409
 
294
410
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
295
411
  def yarnrc_file
296
- dependency_files.find { |f| f.name.end_with?(".yarnrc") }
412
+ dependency_files.find { |f| f.name.end_with?(REGISTRY_FILE_YARNRC) }
297
413
  end
298
414
 
299
415
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
300
416
  def yarnrc_yml_file
301
- dependency_files.find { |f| f.name.end_with?(".yarnrc.yml") }
417
+ dependency_files.find { |f| f.name.end_with?(REGISTRY_FILE_YARNRC_YML) }
302
418
  end
303
419
 
304
420
  sig { returns(T::Boolean) }
@@ -309,6 +425,15 @@ module Dependabot
309
425
  credentials: credentials
310
426
  ).git_dependency?
311
427
  end
428
+
429
+ # This function safely retrieves a value for a given key from a Hash.
430
+ # If the hash is valid and the key exists, it will return the value, otherwise nil.
431
+ sig { params(hash: T.untyped, key: T.untyped).returns(T.untyped) }
432
+ def fetch_value_from_hash(hash, key)
433
+ return nil unless hash.is_a?(Hash) # Return nil if the hash is not a Hash
434
+
435
+ hash.fetch(key, nil) # Fetch the value for the given key, defaulting to nil if not found
436
+ end
312
437
  end
313
438
  end
314
439
  end