dependabot-npm_and_yarn 0.378.0 → 0.379.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 +4 -4
- data/lib/dependabot/npm_and_yarn/dependency_grapher/lockfile_generator.rb +37 -2
- data/lib/dependabot/npm_and_yarn/dependency_grapher/npm_relationship_resolver.rb +134 -0
- data/lib/dependabot/npm_and_yarn/dependency_grapher/pnpm_relationship_resolver.rb +53 -0
- data/lib/dependabot/npm_and_yarn/dependency_grapher/yarn_relationship_resolver.rb +68 -0
- data/lib/dependabot/npm_and_yarn/dependency_grapher.rb +91 -117
- data/lib/dependabot/npm_and_yarn/file_parser/json_lock.rb +37 -3
- data/lib/dependabot/npm_and_yarn/file_parser/lockfile_parser.rb +6 -5
- data/lib/dependabot/npm_and_yarn/file_parser/pnpm_lock.rb +20 -5
- data/lib/dependabot/npm_and_yarn/file_parser/yarn_lock.rb +50 -9
- data/lib/dependabot/npm_and_yarn/file_parser.rb +63 -1
- data/lib/dependabot/npm_and_yarn/file_updater/berry_lockfile_handler.rb +108 -0
- data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +13 -2
- data/lib/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater.rb +10 -0
- data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +80 -0
- data/lib/dependabot/npm_and_yarn/file_updater.rb +2 -1
- data/lib/dependabot/npm_and_yarn/helpers.rb +3 -1
- data/lib/dependabot/npm_and_yarn/package_manager.rb +83 -10
- data/lib/dependabot/npm_and_yarn/update_checker.rb +110 -0
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd08cdfb4fa9a2d7702785f8f1613bc691569c1cbde17dc45be2c540a7bb6de3
|
|
4
|
+
data.tar.gz: ed5b6525cb039e5dadbd06709f0a95a47c6c16236ec79a2c8ecbc555bcf1ddd8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af2f7366ea66b328b4ab910b54bff68773df0b76156e988b8bc1590e8fa97097b4b736f890699718df31b6e001191e0c86e7362e306fd91030df9625e1625923
|
|
7
|
+
data.tar.gz: 49955bd7ee30d88d916ff83ec7ded75784f5d53efc4aeab927e756fde987b4eddaa1788360f8b5f7f13314075908b4a14e7937394ff9b8b16edcbd7b8037707a
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
5
|
|
|
6
6
|
require "dependabot/dependency_file"
|
|
7
|
+
require "dependabot/errors"
|
|
7
8
|
require "dependabot/shared_helpers"
|
|
8
9
|
require "dependabot/npm_and_yarn/helpers"
|
|
9
10
|
require "dependabot/npm_and_yarn/package_manager"
|
|
@@ -15,6 +16,12 @@ module Dependabot
|
|
|
15
16
|
class LockfileGenerator
|
|
16
17
|
extend T::Sig
|
|
17
18
|
|
|
19
|
+
# URL extraction patterns
|
|
20
|
+
GENERIC_URL_REGEX = T.let(%r{(https?://[^\s"']+)}, Regexp)
|
|
21
|
+
NETWORK_ERROR_HOST_REGEX = T.let(/E(?:NOTFOUND|TIMEDOUT)\s+(\S+)/i, Regexp)
|
|
22
|
+
|
|
23
|
+
FALLBACK_SOURCE = "a private registry"
|
|
24
|
+
|
|
18
25
|
sig do
|
|
19
26
|
params(
|
|
20
27
|
dependency_files: T::Array[Dependabot::DependencyFile],
|
|
@@ -202,21 +209,49 @@ module Dependabot
|
|
|
202
209
|
"Failed to generate lockfile with #{package_manager}: #{error.message}"
|
|
203
210
|
)
|
|
204
211
|
|
|
205
|
-
# Log more details for debugging
|
|
206
212
|
if error.message.include?("ERESOLVE")
|
|
207
213
|
Dependabot.logger.error(
|
|
208
214
|
"Dependency resolution failed. This may be due to conflicting peer dependencies."
|
|
209
215
|
)
|
|
216
|
+
raise Dependabot::DependencyFileNotResolvable,
|
|
217
|
+
"Could not resolve dependencies. This may be due to conflicting peer dependencies."
|
|
210
218
|
elsif error.message.include?("ENOTFOUND") || error.message.include?("ETIMEDOUT")
|
|
211
219
|
Dependabot.logger.error(
|
|
212
220
|
"Network error while generating lockfile. Registry may be unreachable."
|
|
213
221
|
)
|
|
214
|
-
|
|
222
|
+
raise Dependabot::PrivateSourceTimedOut, extract_network_error_host(error.message)
|
|
223
|
+
elsif authentication_error?(error.message)
|
|
215
224
|
Dependabot.logger.error(
|
|
216
225
|
"Authentication error. Check that credentials are configured correctly."
|
|
217
226
|
)
|
|
227
|
+
raise Dependabot::PrivateSourceAuthenticationFailure, extract_url(error.message)
|
|
218
228
|
end
|
|
219
229
|
end
|
|
230
|
+
|
|
231
|
+
sig { params(message: String).returns(T::Boolean) }
|
|
232
|
+
def authentication_error?(message)
|
|
233
|
+
message.include?("401") ||
|
|
234
|
+
message.include?("403") ||
|
|
235
|
+
message.include?(NpmAndYarn::AUTHENTICATION_TOKEN_NOT_PROVIDED) ||
|
|
236
|
+
message.include?(NpmAndYarn::AUTHENTICATION_IS_NOT_CONFIGURED) ||
|
|
237
|
+
message.include?(NpmAndYarn::AUTHENTICATION_HEADER_NOT_PROVIDED) ||
|
|
238
|
+
message.match?(NpmAndYarn::AUTH_REQUIRED_ERROR)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
sig { params(message: String).returns(String) }
|
|
242
|
+
def extract_url(message)
|
|
243
|
+
match = message.match(GENERIC_URL_REGEX)
|
|
244
|
+
match ? T.must(match[1]).chomp(":") : FALLBACK_SOURCE
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
sig { params(message: String).returns(String) }
|
|
248
|
+
def extract_network_error_host(message)
|
|
249
|
+
match = message.match(NETWORK_ERROR_HOST_REGEX)
|
|
250
|
+
return T.must(match[1]) if match
|
|
251
|
+
|
|
252
|
+
match = message.match(GENERIC_URL_REGEX)
|
|
253
|
+
match ? T.must(match[1]) : FALLBACK_SOURCE
|
|
254
|
+
end
|
|
220
255
|
end
|
|
221
256
|
end
|
|
222
257
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
|
|
7
|
+
module Dependabot
|
|
8
|
+
module NpmAndYarn
|
|
9
|
+
class DependencyGrapher < Dependabot::DependencyGraphers::Base
|
|
10
|
+
class NpmRelationshipResolver
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(lockfile: Dependabot::DependencyFile).void }
|
|
14
|
+
def initialize(lockfile)
|
|
15
|
+
@lockfile = lockfile
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { returns(T::Hash[String, T::Array[String]]) }
|
|
19
|
+
def relationships
|
|
20
|
+
parsed = JSON.parse(T.must(@lockfile.content))
|
|
21
|
+
packages = parsed.fetch("packages", {})
|
|
22
|
+
|
|
23
|
+
# v3/v2 lockfiles use a flat "packages" section
|
|
24
|
+
return build_v3_relationships(packages) if packages.is_a?(Hash) && !packages.empty?
|
|
25
|
+
|
|
26
|
+
# if packages isn't present, attempt a v1 fallback
|
|
27
|
+
build_v1_relationships(parsed)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
sig { params(packages: T::Hash[String, T.untyped]).returns(T::Hash[String, T::Array[String]]) }
|
|
33
|
+
def build_v3_relationships(packages)
|
|
34
|
+
packages.each_with_object({}) do |(path, details), rels|
|
|
35
|
+
next if path.empty? # skip root package entry
|
|
36
|
+
next unless details.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
children = details.fetch("dependencies", {}).keys
|
|
39
|
+
next if children.empty?
|
|
40
|
+
|
|
41
|
+
package_name = details["name"] || path.split("node_modules/").last
|
|
42
|
+
version = details["version"]
|
|
43
|
+
next if version.nil? || version.to_s.empty?
|
|
44
|
+
|
|
45
|
+
resolved = resolve_v3_children(packages, path, children)
|
|
46
|
+
rels["#{package_name}@#{version}"] = resolved unless resolved.empty?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig do
|
|
51
|
+
params(
|
|
52
|
+
packages: T::Hash[String, T.untyped],
|
|
53
|
+
parent_path: String,
|
|
54
|
+
children: T::Array[String]
|
|
55
|
+
).returns(T::Array[String])
|
|
56
|
+
end
|
|
57
|
+
def resolve_v3_children(packages, parent_path, children)
|
|
58
|
+
children.filter_map do |child_name|
|
|
59
|
+
child_details = resolve_child(packages, parent_path, child_name)
|
|
60
|
+
next unless child_details
|
|
61
|
+
|
|
62
|
+
child_version = child_details["version"]
|
|
63
|
+
next if child_version.nil? || child_version.to_s.empty?
|
|
64
|
+
|
|
65
|
+
# Use the "name" field for aliased packages (real name vs path alias)
|
|
66
|
+
real_name = child_details["name"] || child_name
|
|
67
|
+
"#{real_name}@#{child_version}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Walks up the node_modules tree to resolve a child dependency,
|
|
72
|
+
# matching Node.js module resolution behavior.
|
|
73
|
+
sig do
|
|
74
|
+
params(
|
|
75
|
+
packages: T::Hash[String, T.untyped],
|
|
76
|
+
parent_path: String,
|
|
77
|
+
child_name: String
|
|
78
|
+
).returns(T.nilable(T::Hash[String, T.untyped]))
|
|
79
|
+
end
|
|
80
|
+
def resolve_child(packages, parent_path, child_name)
|
|
81
|
+
# First check directly nested under parent
|
|
82
|
+
candidate = "#{parent_path}/node_modules/#{child_name}"
|
|
83
|
+
return packages[candidate] if packages.key?(candidate)
|
|
84
|
+
|
|
85
|
+
# Walk up the tree: strip trailing node_modules/pkg segments
|
|
86
|
+
segments = parent_path.split("node_modules/")
|
|
87
|
+
segments.pop # remove the current package segment
|
|
88
|
+
|
|
89
|
+
while segments.any?
|
|
90
|
+
candidate = "#{segments.join('node_modules/')}node_modules/#{child_name}"
|
|
91
|
+
return packages[candidate] if packages.key?(candidate)
|
|
92
|
+
|
|
93
|
+
segments.pop
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Top-level fallback
|
|
97
|
+
packages["node_modules/#{child_name}"]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig { params(parsed: T::Hash[String, T.untyped]).returns(T::Hash[String, T::Array[String]]) }
|
|
101
|
+
def build_v1_relationships(parsed)
|
|
102
|
+
dependencies = parsed.fetch("dependencies", {})
|
|
103
|
+
return {} unless dependencies.is_a?(Hash)
|
|
104
|
+
|
|
105
|
+
dependencies.each_with_object({}) do |(name, details), rels|
|
|
106
|
+
next unless details.is_a?(Hash)
|
|
107
|
+
|
|
108
|
+
nested = details.fetch("dependencies", nil)
|
|
109
|
+
next unless nested.is_a?(Hash)
|
|
110
|
+
|
|
111
|
+
version = details["version"]
|
|
112
|
+
next if version.nil? || version.to_s.empty?
|
|
113
|
+
|
|
114
|
+
children = resolve_v1_children(nested)
|
|
115
|
+
rels["#{name}@#{version}"] = children unless children.empty?
|
|
116
|
+
rels.merge!(build_v1_relationships(details))
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { params(nested: T::Hash[String, T.untyped]).returns(T::Array[String]) }
|
|
121
|
+
def resolve_v1_children(nested)
|
|
122
|
+
nested.filter_map do |child_name, child_details|
|
|
123
|
+
next unless child_details.is_a?(Hash)
|
|
124
|
+
|
|
125
|
+
child_version = child_details["version"]
|
|
126
|
+
next if child_version.nil? || child_version.to_s.empty?
|
|
127
|
+
|
|
128
|
+
"#{child_name}@#{child_version}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
|
|
7
|
+
module Dependabot
|
|
8
|
+
module NpmAndYarn
|
|
9
|
+
class DependencyGrapher < Dependabot::DependencyGraphers::Base
|
|
10
|
+
class PnpmRelationshipResolver
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(lockfile: Dependabot::DependencyFile).void }
|
|
14
|
+
def initialize(lockfile)
|
|
15
|
+
@lockfile = lockfile
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { returns(T::Hash[String, T::Array[String]]) }
|
|
19
|
+
def relationships
|
|
20
|
+
parsed = YAML.safe_load(T.must(@lockfile.content)) || {}
|
|
21
|
+
|
|
22
|
+
# v9+ uses "snapshots" for resolved dependency details; v6 uses "packages"
|
|
23
|
+
entries = parsed.fetch("snapshots", nil) || parsed.fetch("packages", {})
|
|
24
|
+
|
|
25
|
+
entries.each_with_object({}) do |(key, details), rels|
|
|
26
|
+
next unless details.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
# Keys are "/name@version" (v6) or "name@version" (v9)
|
|
29
|
+
name_version = key.sub(%r{^/}, "")
|
|
30
|
+
children = details.fetch("dependencies", {})
|
|
31
|
+
|
|
32
|
+
next if children.nil? || children.empty?
|
|
33
|
+
|
|
34
|
+
# Strip any pnpm suffix metadata (e.g., parenthesized peer dep info)
|
|
35
|
+
name_version = name_version.sub(/\(.*\)$/, "")
|
|
36
|
+
|
|
37
|
+
# pnpm dependencies are already resolved: {"name": "version"}
|
|
38
|
+
# Strip any peer metadata suffixes like "7.49.0(react@18.2.0)"
|
|
39
|
+
resolved_children = children.filter_map do |child_name, child_version|
|
|
40
|
+
clean_version = child_version.to_s.sub(/\(.*\)$/, "")
|
|
41
|
+
next if clean_version.empty?
|
|
42
|
+
|
|
43
|
+
"#{child_name}@#{clean_version}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
rels[name_version] ||= []
|
|
47
|
+
rels[name_version].concat(resolved_children).uniq!
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/npm_and_yarn/file_parser/yarn_lock"
|
|
6
|
+
|
|
7
|
+
module Dependabot
|
|
8
|
+
module NpmAndYarn
|
|
9
|
+
class DependencyGrapher < Dependabot::DependencyGraphers::Base
|
|
10
|
+
class YarnRelationshipResolver
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(lockfile: Dependabot::DependencyFile).void }
|
|
14
|
+
def initialize(lockfile)
|
|
15
|
+
@lockfile = lockfile
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { returns(T::Hash[String, T::Array[String]]) }
|
|
19
|
+
def relationships
|
|
20
|
+
parsed = FileParser::YarnLock.new(@lockfile).parsed
|
|
21
|
+
|
|
22
|
+
parsed.each_with_object({}) do |(req, details), rels|
|
|
23
|
+
next unless details.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
version = details["version"]
|
|
26
|
+
parent_name = T.must(req.split(/(?<=\w)\@/).first)
|
|
27
|
+
children = details.fetch("dependencies", {})
|
|
28
|
+
|
|
29
|
+
next if children.nil? || children.empty?
|
|
30
|
+
|
|
31
|
+
key = "#{parent_name}@#{version}"
|
|
32
|
+
resolved_children = resolve_children(children, parsed)
|
|
33
|
+
|
|
34
|
+
rels[key] ||= []
|
|
35
|
+
rels[key].concat(resolved_children).uniq!
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
sig { params(children: T::Hash[String, String], parsed: T::Hash[String, T.untyped]).returns(T::Array[String]) }
|
|
42
|
+
def resolve_children(children, parsed)
|
|
43
|
+
children.filter_map do |child_name, child_req|
|
|
44
|
+
version = resolve_child_version(child_name, child_req, parsed)
|
|
45
|
+
"#{child_name}@#{version}" if version
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { params(child_name: String, child_req: String, parsed: T::Hash[String, T.untyped]).returns(T.nilable(String)) }
|
|
50
|
+
def resolve_child_version(child_name, child_req, parsed)
|
|
51
|
+
# Try exact key first
|
|
52
|
+
child_entry = parsed["#{child_name}@#{child_req}"]
|
|
53
|
+
return child_entry["version"] if child_entry && child_entry["version"]
|
|
54
|
+
|
|
55
|
+
# Yarn groups multiple requirements into single keys like "foo@^1.0.0, foo@^1.2.0"
|
|
56
|
+
target_req = "#{child_name}@#{child_req}"
|
|
57
|
+
grouped_match = parsed.find { |k, _| k.split(", ").include?(target_req) }
|
|
58
|
+
return grouped_match.last["version"] if grouped_match && grouped_match.last["version"]
|
|
59
|
+
|
|
60
|
+
# Fallback: find by name only if there's exactly one candidate
|
|
61
|
+
candidates = parsed.select { |k, _| k.split(/(?<=\w)\@/).first == child_name }
|
|
62
|
+
candidate = candidates.first
|
|
63
|
+
candidate.last["version"] if candidates.size == 1 && candidate
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
|
-
require "yaml"
|
|
6
5
|
require "sorbet-runtime"
|
|
7
6
|
|
|
8
7
|
require "dependabot/dependency_graphers"
|
|
@@ -17,6 +16,9 @@ module Dependabot
|
|
|
17
16
|
extend T::Sig
|
|
18
17
|
|
|
19
18
|
require_relative "dependency_grapher/lockfile_generator"
|
|
19
|
+
require_relative "dependency_grapher/npm_relationship_resolver"
|
|
20
|
+
require_relative "dependency_grapher/yarn_relationship_resolver"
|
|
21
|
+
require_relative "dependency_grapher/pnpm_relationship_resolver"
|
|
20
22
|
|
|
21
23
|
sig { override.returns(Dependabot::DependencyFile) }
|
|
22
24
|
def relevant_dependency_file
|
|
@@ -27,8 +29,37 @@ module Dependabot
|
|
|
27
29
|
lockfile || package_json
|
|
28
30
|
end
|
|
29
31
|
|
|
32
|
+
# Override to expand multi-version dependencies into separate resolved
|
|
33
|
+
# dependency entries. When the same package exists at multiple versions
|
|
34
|
+
# (e.g., is-number@6.0.0 direct + is-number@7.0.0 transitive), each
|
|
35
|
+
# version gets its own entry with correct subdependency edges.
|
|
36
|
+
sig { override.returns(T::Hash[String, Dependabot::DependencyGraphers::ResolvedDependency]) }
|
|
37
|
+
def resolved_dependencies
|
|
38
|
+
prepare! unless prepared
|
|
39
|
+
|
|
40
|
+
@dependencies.each_with_object({}) do |dep, resolved|
|
|
41
|
+
all_versions = dep.metadata[:all_versions] || [dep]
|
|
42
|
+
|
|
43
|
+
all_versions.each do |version_dep|
|
|
44
|
+
purl = build_purl(version_dep)
|
|
45
|
+
next if resolved.key?(purl)
|
|
46
|
+
|
|
47
|
+
resolved[purl] = Dependabot::DependencyGraphers::ResolvedDependency.new(
|
|
48
|
+
package_url: purl,
|
|
49
|
+
direct: version_dep.top_level? || !version_dep.metadata[:alias].nil?,
|
|
50
|
+
runtime: version_dep.production?,
|
|
51
|
+
dependencies: subdependency_purls_for(version_dep)
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
30
57
|
sig { override.void }
|
|
31
58
|
def prepare!
|
|
59
|
+
# Enable alias extraction for graph jobs so aliased packages appear
|
|
60
|
+
# in the dependency graph for security scanning.
|
|
61
|
+
file_parser.dealias_packages!
|
|
62
|
+
|
|
32
63
|
if lockfile.nil?
|
|
33
64
|
Dependabot.logger.info("No lockfile found, generating ephemeral lockfile for dependency graphing")
|
|
34
65
|
generate_ephemeral_lockfile!
|
|
@@ -79,11 +110,14 @@ module Dependabot
|
|
|
79
110
|
|
|
80
111
|
sig { returns(T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)]) }
|
|
81
112
|
def lockfiles_hash
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
113
|
+
@lockfiles_hash ||= T.let(
|
|
114
|
+
{
|
|
115
|
+
npm: dependency_files.find { |f| f.name.end_with?(NpmPackageManager::LOCKFILE_NAME) },
|
|
116
|
+
yarn: dependency_files.find { |f| f.name.end_with?(YarnPackageManager::LOCKFILE_NAME) },
|
|
117
|
+
pnpm: dependency_files.find { |f| f.name.end_with?(PNPMPackageManager::LOCKFILE_NAME) }
|
|
118
|
+
},
|
|
119
|
+
T.nilable(T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)])
|
|
120
|
+
)
|
|
87
121
|
end
|
|
88
122
|
|
|
89
123
|
sig { returns(String) }
|
|
@@ -130,6 +164,7 @@ module Dependabot
|
|
|
130
164
|
|
|
131
165
|
# Clear our cached lockfile reference so it picks up the new one
|
|
132
166
|
remove_instance_variable(:@lockfile) if instance_variable_defined?(:@lockfile)
|
|
167
|
+
remove_instance_variable(:@lockfiles_hash) if instance_variable_defined?(:@lockfiles_hash)
|
|
133
168
|
|
|
134
169
|
# Clear the FileParser's memoized lockfile references so it will
|
|
135
170
|
# find the newly injected lockfile when parse is called
|
|
@@ -156,7 +191,48 @@ module Dependabot
|
|
|
156
191
|
|
|
157
192
|
sig { override.params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
|
|
158
193
|
def fetch_subdependencies(dependency)
|
|
159
|
-
|
|
194
|
+
key = "#{dependency.name}@#{dependency.version}"
|
|
195
|
+
package_relationships.fetch(key, [])
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Builds purl strings for subdependencies directly from the name@version
|
|
199
|
+
# entries in package_relationships, without going through dependencies_by_name
|
|
200
|
+
# which only holds one combined dep per package name.
|
|
201
|
+
sig { params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
|
|
202
|
+
def subdependency_purls_for(dependency)
|
|
203
|
+
return [] if errored_fetching_subdependencies
|
|
204
|
+
|
|
205
|
+
key = "#{dependency.name}@#{dependency.version}"
|
|
206
|
+
children = package_relationships.fetch(key, [])
|
|
207
|
+
|
|
208
|
+
children.filter_map do |child_key|
|
|
209
|
+
child_name, child_version = split_name_version(child_key)
|
|
210
|
+
next unless child_name && child_version
|
|
211
|
+
|
|
212
|
+
purl_name = child_name.sub(/^@/, "%40")
|
|
213
|
+
format(PURL_TEMPLATE, type: "npm", name: purl_name, version: "@#{child_version}")
|
|
214
|
+
end
|
|
215
|
+
rescue StandardError => e
|
|
216
|
+
errored_fetching_subdependencies!
|
|
217
|
+
@subdependency_error = T.let(e, T.nilable(StandardError))
|
|
218
|
+
Dependabot.logger.error("Error fetching subdependencies: #{e.message}")
|
|
219
|
+
[]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Splits a "name@version" string, handling scoped packages like "@scope/pkg@1.0.0"
|
|
223
|
+
sig { params(name_version: String).returns([T.nilable(String), T.nilable(String)]) }
|
|
224
|
+
def split_name_version(name_version)
|
|
225
|
+
# For scoped packages (@scope/name@version), find the second @
|
|
226
|
+
at_index = if name_version.start_with?("@")
|
|
227
|
+
name_version.index("@", 1)
|
|
228
|
+
else
|
|
229
|
+
name_version.index("@")
|
|
230
|
+
end
|
|
231
|
+
return [name_version, nil] unless at_index
|
|
232
|
+
|
|
233
|
+
version = name_version[(at_index + 1)..]
|
|
234
|
+
version = nil if version.nil? || version.empty?
|
|
235
|
+
[name_version[0...at_index], version]
|
|
160
236
|
end
|
|
161
237
|
|
|
162
238
|
sig { returns(T::Hash[String, T::Array[String]]) }
|
|
@@ -165,129 +241,27 @@ module Dependabot
|
|
|
165
241
|
fetch_package_relationships,
|
|
166
242
|
T.nilable(T::Hash[String, T::Array[String]])
|
|
167
243
|
)
|
|
244
|
+
rescue StandardError => e
|
|
245
|
+
errored_fetching_subdependencies!
|
|
246
|
+
@subdependency_error = T.let(e, T.nilable(StandardError))
|
|
247
|
+
Dependabot.logger.error("Error fetching subdependencies: #{e.message}")
|
|
248
|
+
@package_relationships = {}
|
|
168
249
|
end
|
|
169
250
|
|
|
170
251
|
sig { returns(T::Hash[String, T::Array[String]]) }
|
|
171
252
|
def fetch_package_relationships
|
|
172
253
|
case detected_package_manager
|
|
173
254
|
when NpmPackageManager::NAME
|
|
174
|
-
|
|
255
|
+
NpmRelationshipResolver.new(T.must(lockfiles_hash[:npm])).relationships
|
|
175
256
|
when YarnPackageManager::NAME
|
|
176
|
-
|
|
257
|
+
YarnRelationshipResolver.new(T.must(lockfiles_hash[:yarn])).relationships
|
|
177
258
|
when PNPMPackageManager::NAME
|
|
178
|
-
|
|
259
|
+
PnpmRelationshipResolver.new(T.must(lockfiles_hash[:pnpm])).relationships
|
|
179
260
|
else
|
|
180
261
|
{}
|
|
181
262
|
end
|
|
182
263
|
end
|
|
183
264
|
|
|
184
|
-
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
185
|
-
def npm_lockfile
|
|
186
|
-
return @npm_lockfile if defined?(@npm_lockfile)
|
|
187
|
-
|
|
188
|
-
@npm_lockfile = T.let(
|
|
189
|
-
dependency_files.find { |f| f.name.end_with?(NpmPackageManager::LOCKFILE_NAME) },
|
|
190
|
-
T.nilable(Dependabot::DependencyFile)
|
|
191
|
-
)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
195
|
-
def yarn_lockfile
|
|
196
|
-
return @yarn_lockfile if defined?(@yarn_lockfile)
|
|
197
|
-
|
|
198
|
-
@yarn_lockfile = T.let(
|
|
199
|
-
dependency_files.find { |f| f.name.end_with?(YarnPackageManager::LOCKFILE_NAME) },
|
|
200
|
-
T.nilable(Dependabot::DependencyFile)
|
|
201
|
-
)
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
205
|
-
def pnpm_lockfile
|
|
206
|
-
return @pnpm_lockfile if defined?(@pnpm_lockfile)
|
|
207
|
-
|
|
208
|
-
@pnpm_lockfile = T.let(
|
|
209
|
-
dependency_files.find { |f| f.name.end_with?(PNPMPackageManager::LOCKFILE_NAME) },
|
|
210
|
-
T.nilable(Dependabot::DependencyFile)
|
|
211
|
-
)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
sig { returns(T::Hash[String, T::Array[String]]) }
|
|
215
|
-
def fetch_npm_lock_relationships
|
|
216
|
-
parsed = JSON.parse(T.must(T.must(npm_lockfile).content))
|
|
217
|
-
packages = parsed.fetch("packages", {})
|
|
218
|
-
|
|
219
|
-
# v3/v2 lockfiles use a flat "packages" section
|
|
220
|
-
if packages.is_a?(Hash) && !packages.empty?
|
|
221
|
-
return packages.each_with_object({}) do |(path, details), rels|
|
|
222
|
-
next if path.empty? # skip root package entry
|
|
223
|
-
next unless details.is_a?(Hash)
|
|
224
|
-
|
|
225
|
-
children = details.fetch("dependencies", {}).keys
|
|
226
|
-
next if children.empty?
|
|
227
|
-
|
|
228
|
-
rels[path.split("node_modules/").last] = children
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# if packages isn't present, attempt a v1 fallback
|
|
233
|
-
fetch_npm_v1_lock_relationships(parsed)
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
sig { params(parsed: T::Hash[String, T.untyped]).returns(T::Hash[String, T::Array[String]]) }
|
|
237
|
-
def fetch_npm_v1_lock_relationships(parsed)
|
|
238
|
-
dependencies = parsed.fetch("dependencies", {})
|
|
239
|
-
return {} unless dependencies.is_a?(Hash)
|
|
240
|
-
|
|
241
|
-
dependencies.each_with_object({}) do |(name, details), rels|
|
|
242
|
-
next unless details.is_a?(Hash)
|
|
243
|
-
|
|
244
|
-
nested = details.fetch("dependencies", nil)
|
|
245
|
-
next unless nested.is_a?(Hash)
|
|
246
|
-
|
|
247
|
-
children = nested.keys
|
|
248
|
-
rels[name] = children unless children.empty?
|
|
249
|
-
rels.merge!(fetch_npm_v1_lock_relationships(details))
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
sig { returns(T::Hash[String, T::Array[String]]) }
|
|
254
|
-
def fetch_yarn_lock_relationships
|
|
255
|
-
parsed = FileParser::YarnLock.new(T.must(yarn_lockfile)).parsed
|
|
256
|
-
|
|
257
|
-
parsed.each_with_object({}) do |(req, details), rels|
|
|
258
|
-
next unless details.is_a?(Hash)
|
|
259
|
-
|
|
260
|
-
parent_name = T.must(req.split(/(?<=\w)\@/).first)
|
|
261
|
-
children = details.fetch("dependencies", {})&.keys || []
|
|
262
|
-
|
|
263
|
-
next if children.empty?
|
|
264
|
-
|
|
265
|
-
rels[parent_name] ||= []
|
|
266
|
-
rels[parent_name].concat(children).uniq!
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
sig { returns(T::Hash[String, T::Array[String]]) }
|
|
271
|
-
def fetch_pnpm_lock_relationships
|
|
272
|
-
parsed = YAML.safe_load(T.must(T.must(pnpm_lockfile).content)) || {}
|
|
273
|
-
|
|
274
|
-
# v9+ uses "snapshots" for resolved dependency details; v6 uses "packages"
|
|
275
|
-
entries = parsed.fetch("snapshots", nil) || parsed.fetch("packages", {})
|
|
276
|
-
|
|
277
|
-
entries.each_with_object({}) do |(key, details), rels|
|
|
278
|
-
next unless details.is_a?(Hash)
|
|
279
|
-
|
|
280
|
-
# Keys are "/name@version" (v6) or "name@version" (v9)
|
|
281
|
-
parent_name = key.sub(%r{^/}, "").split(/(?<=\w)\@/).first
|
|
282
|
-
children = details.fetch("dependencies", {})&.keys || []
|
|
283
|
-
|
|
284
|
-
next if children.empty?
|
|
285
|
-
|
|
286
|
-
rels[parent_name] ||= []
|
|
287
|
-
rels[parent_name].concat(children).uniq!
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
|
|
291
265
|
sig { override.params(_dependency: Dependabot::Dependency).returns(String) }
|
|
292
266
|
def purl_pkg_for(_dependency)
|
|
293
267
|
"npm"
|
|
@@ -12,9 +12,10 @@ module Dependabot
|
|
|
12
12
|
class JsonLock
|
|
13
13
|
extend T::Sig
|
|
14
14
|
|
|
15
|
-
sig { params(dependency_file: DependencyFile).void }
|
|
16
|
-
def initialize(dependency_file)
|
|
15
|
+
sig { params(dependency_file: DependencyFile, dealias_packages: T::Boolean).void }
|
|
16
|
+
def initialize(dependency_file, dealias_packages: false)
|
|
17
17
|
@dependency_file = dependency_file
|
|
18
|
+
@dealias_packages = dealias_packages
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
sig { returns(T::Hash[String, T.untyped]) }
|
|
@@ -64,7 +65,7 @@ module Dependabot
|
|
|
64
65
|
version = Version.semver_for(details["version"])
|
|
65
66
|
next unless version
|
|
66
67
|
|
|
67
|
-
package_name = name
|
|
68
|
+
package_name = package_name_for(name, details)
|
|
68
69
|
version = version.to_s
|
|
69
70
|
|
|
70
71
|
dependency_args = {
|
|
@@ -74,6 +75,11 @@ module Dependabot
|
|
|
74
75
|
requirements: []
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
if aliased_package?(name, details)
|
|
79
|
+
alias_name = name.split("node_modules/").last
|
|
80
|
+
dependency_args[:metadata] = { alias: alias_name }
|
|
81
|
+
end
|
|
82
|
+
|
|
77
83
|
if details["bundled"]
|
|
78
84
|
dependency_args[:subdependency_metadata] =
|
|
79
85
|
[{ npm_bundled: details["bundled"] }]
|
|
@@ -91,6 +97,29 @@ module Dependabot
|
|
|
91
97
|
dependency_set
|
|
92
98
|
end
|
|
93
99
|
|
|
100
|
+
sig { params(package_path: String, details: T::Hash[String, T.untyped]).returns(String) }
|
|
101
|
+
def package_name_for(package_path, details)
|
|
102
|
+
package_name = T.must(package_path.split("node_modules/").last)
|
|
103
|
+
return package_name unless dealias_packages?
|
|
104
|
+
|
|
105
|
+
real_package_name = details["name"]
|
|
106
|
+
return package_name unless real_package_name.is_a?(String)
|
|
107
|
+
return package_name if real_package_name == package_name
|
|
108
|
+
|
|
109
|
+
real_package_name
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sig { params(package_path: String, details: T::Hash[String, T.untyped]).returns(T::Boolean) }
|
|
113
|
+
def aliased_package?(package_path, details)
|
|
114
|
+
return false unless dealias_packages?
|
|
115
|
+
|
|
116
|
+
real_package_name = details["name"]
|
|
117
|
+
return false unless real_package_name.is_a?(String)
|
|
118
|
+
|
|
119
|
+
package_name = T.must(package_path.split("node_modules/").last)
|
|
120
|
+
real_package_name != package_name
|
|
121
|
+
end
|
|
122
|
+
|
|
94
123
|
sig { params(manifest_name: String, dependency_name: String).returns(String) }
|
|
95
124
|
def node_modules_path(manifest_name, dependency_name)
|
|
96
125
|
return "node_modules/#{dependency_name}" if manifest_name == "package.json"
|
|
@@ -98,6 +127,11 @@ module Dependabot
|
|
|
98
127
|
workspace_path = manifest_name.gsub("/package.json", "")
|
|
99
128
|
File.join(workspace_path, "node_modules", dependency_name)
|
|
100
129
|
end
|
|
130
|
+
|
|
131
|
+
sig { returns(T::Boolean) }
|
|
132
|
+
def dealias_packages?
|
|
133
|
+
@dealias_packages
|
|
134
|
+
end
|
|
101
135
|
end
|
|
102
136
|
end
|
|
103
137
|
end
|