dependabot-npm_and_yarn 0.195.0 → 0.196.2

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.
data/helpers/package.json CHANGED
@@ -10,15 +10,16 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@dependabot/yarn-lib": "^1.21.1",
13
- "@npmcli/arborist": "^5.2.0",
13
+ "@npmcli/arborist": "^5.2.3",
14
14
  "detect-indent": "^6.1.0",
15
+ "nock": "^13.2.7",
15
16
  "npm": "6.14.17",
16
17
  "semver": "^7.3.7"
17
18
  },
18
19
  "devDependencies": {
19
20
  "eslint": "^8.18.0",
20
21
  "eslint-config-prettier": "^8.5.0",
21
- "jest": "^28.1.0",
22
+ "jest": "^28.1.1",
22
23
  "prettier": "^2.7.1",
23
24
  "rimraf": "^3.0.2"
24
25
  }
data/helpers/run.js CHANGED
@@ -18,18 +18,13 @@ process.stdin.on("end", () => {
18
18
  process.exit(1);
19
19
  }
20
20
 
21
- try {
22
- func
23
- .apply(null, request.args)
24
- .then((result) => {
25
- output({ result: result });
26
- })
27
- .catch((error) => {
28
- output({ error: error.message });
29
- process.exit(1);
30
- });
31
- } catch (e) {
32
- output({ error: `Error calling function: ${func.name}: ${e}` });
33
- process.exit(1);
34
- }
21
+ func
22
+ .apply(null, request.args)
23
+ .then((result) => {
24
+ output({ result: result });
25
+ })
26
+ .catch((error) => {
27
+ output({ error: error.message });
28
+ process.exit(1);
29
+ });
35
30
  });
@@ -291,7 +291,7 @@ module Dependabot
291
291
 
292
292
  if matches_double_glob && !nested
293
293
  dependency_files +=
294
- expanded_paths(File.join(path, "*")).flat_map do |nested_path|
294
+ find_directories(File.join(path, "*")).flat_map do |nested_path|
295
295
  fetch_lerna_packages_from_path(nested_path, true)
296
296
  end
297
297
  end
@@ -309,34 +309,58 @@ module Dependabot
309
309
  [] # Invalid lerna.json, which must not be in use
310
310
  end
311
311
 
312
- paths_array.flat_map do |path|
313
- # The packages/!(not-this-package) syntax is unique to Yarn
314
- if path.include?("*") || path.include?("!(")
315
- expanded_paths(path)
316
- else
317
- path
318
- end
319
- end
312
+ paths_array.flat_map { |path| recursive_find_directories(path) }
320
313
  end
321
314
 
322
315
  # Only expands globs one level deep, so path/**/* gets expanded to path/
323
- def expanded_paths(path)
324
- ignored_path = path.match?(/!\(.*?\)/) && path.gsub(/(!\((.*?)\))/, '\2')
316
+ def find_directories(glob)
317
+ return [glob] unless glob.include?("*") || yarn_ignored_glob(glob)
318
+
319
+ unglobbed_path =
320
+ glob.gsub(%r{^\./}, "").gsub(/!\(.*?\)/, "*").
321
+ split("*").
322
+ first&.gsub(%r{(?<=/)[^/]*$}, "") || "."
325
323
 
326
324
  dir = directory.gsub(%r{(^/|/$)}, "")
327
- path = path.gsub(%r{^\./}, "").gsub(/!\(.*?\)/, "*")
328
- unglobbed_path = path.split("*").first&.gsub(%r{(?<=/)[^/]*$}, "") ||
329
- "."
330
325
 
331
- results =
326
+ paths =
332
327
  repo_contents(dir: unglobbed_path, raise_errors: false).
333
328
  select { |file| file.type == "dir" }.
334
- map { |f| f.path.gsub(%r{^/?#{Regexp.escape(dir)}/?}, "") }.
335
- select { |filename| File.fnmatch?(path, filename) }
329
+ map { |f| f.path.gsub(%r{^/?#{Regexp.escape(dir)}/?}, "") }
330
+
331
+ matching_paths(glob, paths)
332
+ end
333
+
334
+ def matching_paths(glob, paths)
335
+ ignored_glob = yarn_ignored_glob(glob)
336
+ glob = glob.gsub(%r{^\./}, "").gsub(/!\(.*?\)/, "*")
337
+
338
+ results = paths.select { |filename| File.fnmatch?(glob, filename) }
339
+ return results unless ignored_glob
336
340
 
337
- return results unless ignored_path
341
+ results.reject { |filename| File.fnmatch?(ignored_glob, filename) }
342
+ end
343
+
344
+ def recursive_find_directories(glob, prefix = "")
345
+ return [prefix + glob] unless glob.include?("*") || yarn_ignored_glob(glob)
346
+
347
+ glob = glob.gsub(%r{^\./}, "")
348
+ glob_parts = glob.split("/")
349
+
350
+ paths = find_directories(prefix + glob_parts.first)
351
+ next_parts = glob_parts.drop(1)
352
+ return paths if next_parts.empty?
353
+
354
+ paths = paths.flat_map do |expanded_path|
355
+ recursive_find_directories(next_parts.join("/"), "#{expanded_path}/")
356
+ end
357
+
358
+ matching_paths(prefix + glob, paths)
359
+ end
338
360
 
339
- results.reject { |filename| File.fnmatch?(ignored_path, filename) }
361
+ # The packages/!(not-this-package) syntax is unique to Yarn
362
+ def yarn_ignored_glob(glob)
363
+ glob.match?(/!\(.*?\)/) && glob.gsub(/(!\((.*?)\))/, '\2')
340
364
  end
341
365
 
342
366
  def parsed_package_json
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "dependabot/dependency"
5
+ require "dependabot/errors"
6
+ require "dependabot/logger"
7
+ require "dependabot/npm_and_yarn/file_parser"
8
+ require "dependabot/npm_and_yarn/helpers"
9
+ require "dependabot/npm_and_yarn/native_helpers"
10
+ require "dependabot/npm_and_yarn/update_checker"
11
+ require "dependabot/npm_and_yarn/update_checker/dependency_files_builder"
12
+ require "dependabot/shared_helpers"
13
+
14
+ module Dependabot
15
+ module NpmAndYarn
16
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
17
+ class VulnerabilityAuditor
18
+ def initialize(dependency_files:, credentials:)
19
+ @dependency_files = dependency_files
20
+ @credentials = credentials
21
+ end
22
+
23
+ # Finds any dependencies in the `package-lock.json` or `npm-shrinkwrap.json` that have
24
+ # a subdependency on the given dependency that is locked to a vuln version range.
25
+ #
26
+ # NOTE: yarn is currently not supported.
27
+ #
28
+ # @param dependency [Dependabot::Dependency] the dependency to check
29
+ # @param security_advisories [Array<Dependabot::SecurityAdvisory>] advisories for the dependency
30
+ # @return [Hash<String, [String, Array<Hash<String, String>>]>] the audit results
31
+ # * :dependency_name [String] the name of the dependency
32
+ # * :fix_available [Boolean] whether a fix is available
33
+ # * :current_version [String] the version of the dependency
34
+ # * :target_version [String] the version of the dependency after the fix
35
+ # * :fix_updates [Array<Hash<String, String>>] a list of dependencies to update in order to fix
36
+ # * :dependency_name [String] the name of the blocking dependency
37
+ # * :current_version [String] the current version of the blocking dependency
38
+ # * :target_version [String] the target version of the blocking dependency
39
+ def audit(dependency:, security_advisories:)
40
+ fix_unavailable = {
41
+ "dependency_name" => dependency.name,
42
+ "fix_available" => false
43
+ }
44
+
45
+ SharedHelpers.in_a_temporary_directory do
46
+ dependency_files_builder = DependencyFilesBuilder.new(
47
+ dependency: dependency,
48
+ dependency_files: dependency_files,
49
+ credentials: credentials
50
+ )
51
+ dependency_files_builder.write_temporary_dependency_files
52
+
53
+ # `npm-shrinkwrap.js`, if present, takes precedence over `package-lock.js`.
54
+ # Both files use the same format. See https://bit.ly/3lDIAJV for more.
55
+ lockfile = (dependency_files_builder.shrinkwraps + dependency_files_builder.package_locks).first
56
+ return fix_unavailable unless lockfile
57
+
58
+ vuln_versions = security_advisories.map do |a|
59
+ {
60
+ dependency_name: a.dependency_name,
61
+ affected_versions: a.vulnerable_version_strings
62
+ }
63
+ end
64
+
65
+ audit_result = SharedHelpers.run_helper_subprocess(
66
+ command: NativeHelpers.helper_path,
67
+ function: "npm:vulnerabilityAuditor",
68
+ args: [Dir.pwd, vuln_versions]
69
+ )
70
+ return fix_unavailable unless valid_audit_result?(audit_result, security_advisories)
71
+
72
+ audit_result
73
+ end
74
+ rescue SharedHelpers::HelperSubprocessFailed => e
75
+ log_helper_subprocess_failure(dependency, e)
76
+ fix_unavailable
77
+ end
78
+
79
+ private
80
+
81
+ attr_reader :dependency_files, :credentials
82
+
83
+ def valid_audit_result?(audit_result, security_advisories)
84
+ # we only need to check results that indicate a fix is available
85
+ return true unless audit_result["fix_available"]
86
+
87
+ return false if vulnerable_dependency_removed?(audit_result)
88
+ return false if dependency_still_vulnerable?(audit_result, security_advisories)
89
+ return false if downgrades_dependencies?(audit_result)
90
+
91
+ true
92
+ end
93
+
94
+ def vulnerable_dependency_removed?(audit_result)
95
+ !audit_result["target_version"]
96
+ end
97
+
98
+ def dependency_still_vulnerable?(audit_result, security_advisories)
99
+ version = Version.new(audit_result["target_version"])
100
+ security_advisories.any? { |a| a.vulnerable?(version) }
101
+ end
102
+
103
+ def downgrades_dependencies?(audit_result)
104
+ return true if downgrades_version?(audit_result["current_version"], audit_result["target_version"])
105
+
106
+ audit_result["fix_updates"].any? do |update|
107
+ downgrades_version?(update["current_version"], update["target_version"])
108
+ end
109
+ end
110
+
111
+ def downgrades_version?(current_version, target_version)
112
+ current = Version.new(current_version)
113
+ target = Version.new(target_version)
114
+ current > target
115
+ end
116
+
117
+ def log_helper_subprocess_failure(dependency, error)
118
+ # See `Dependabot::SharedHelpers.run_helper_subprocess` for details on error context
119
+ context = error.error_context || {}
120
+
121
+ builder = ::StringIO.new
122
+ builder << "VulnerabilityAuditor: "
123
+ builder << "#{context[:function]} " if context[:function]
124
+ builder << "failed"
125
+ builder << " after #{context[:time_taken].truncate(2)}s" if context[:time_taken]
126
+ builder << " while auditing #{dependency.name}: "
127
+ builder << error.message
128
+ builder << "\n" << context[:trace]
129
+
130
+ msg = builder.string
131
+ Dependabot.logger.info(msg) # TODO: is this the right log level?
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -14,6 +14,7 @@ module Dependabot
14
14
  require_relative "update_checker/version_resolver"
15
15
  require_relative "update_checker/subdependency_version_resolver"
16
16
  require_relative "update_checker/conflicting_dependency_resolver"
17
+ require_relative "update_checker/vulnerability_auditor"
17
18
 
18
19
  def latest_version
19
20
  @latest_version ||=
@@ -106,20 +107,86 @@ module Dependabot
106
107
 
107
108
  private
108
109
 
110
+ def vulnerability_audit
111
+ @vulnerability_audit ||=
112
+ VulnerabilityAuditor.new(
113
+ dependency_files: dependency_files,
114
+ credentials: credentials
115
+ ).audit(
116
+ dependency: dependency,
117
+ security_advisories: security_advisories
118
+ )
119
+ end
120
+
109
121
  def latest_version_resolvable_with_full_unlock?
110
- return unless latest_version
122
+ return false unless latest_version
111
123
 
112
- # No support for full unlocks for subdependencies yet
113
- return false unless dependency.top_level?
124
+ return version_resolver.latest_version_resolvable_with_full_unlock? if dependency.top_level?
114
125
 
115
- version_resolver.latest_version_resolvable_with_full_unlock?
126
+ return false unless transitive_security_updates_enabled? && security_advisories.any?
127
+
128
+ vulnerability_audit["fix_available"]
129
+ end
130
+
131
+ def transitive_security_updates_enabled?
132
+ options.key?(:npm_transitive_security_updates)
116
133
  end
117
134
 
118
135
  def updated_dependencies_after_full_unlock
136
+ if !dependency.top_level? && transitive_security_updates_enabled? && security_advisories.any?
137
+ return conflicting_updated_dependencies
138
+ end
139
+
119
140
  version_resolver.dependency_updates_from_full_unlock.
120
141
  map { |update_details| build_updated_dependency(update_details) }
121
142
  end
122
143
 
144
+ def conflicting_updated_dependencies
145
+ top_level_dependencies = top_level_dependency_lookup
146
+
147
+ updated_deps = []
148
+ vulnerability_audit["fix_updates"].each do |update|
149
+ dependency_name = update["dependency_name"]
150
+ requirements = top_level_dependencies[dependency_name]&.requirements || []
151
+ conflicting_dep = Dependency.new(
152
+ name: dependency_name,
153
+ package_manager: "npm_and_yarn",
154
+ requirements: requirements
155
+ )
156
+
157
+ updated_deps << build_updated_dependency(
158
+ dependency: conflicting_dep,
159
+ version: update["target_version"],
160
+ previous_version: update["current_version"]
161
+ )
162
+ end
163
+
164
+ # We don't need to update this but need to include it so it's described
165
+ # in the PR and we'll pass validation that this dependency is at a
166
+ # non-vulnerable version.
167
+ if updated_deps.none? { |dep| dep.name == dependency.name }
168
+ updated_deps << build_updated_dependency(
169
+ dependency: dependency,
170
+ version: vulnerability_audit["target_version"],
171
+ previous_version: dependency.version
172
+ )
173
+ end
174
+
175
+ # Target dependency should be first in the result to support rebases
176
+ updated_deps.select { |dep| dep.name == dependency.name } +
177
+ updated_deps.reject { |dep| dep.name == dependency.name }
178
+ end
179
+
180
+ def top_level_dependency_lookup
181
+ top_level_dependencies = FileParser.new(
182
+ dependency_files: dependency_files,
183
+ credentials: credentials,
184
+ source: nil
185
+ ).parse.select(&:top_level?)
186
+
187
+ top_level_dependencies.map { |dep| [dep.name, dep] }.to_h
188
+ end
189
+
123
190
  def build_updated_dependency(update_details)
124
191
  original_dep = update_details.fetch(:dependency)
125
192
  version = update_details.fetch(:version).to_s
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-npm_and_yarn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.195.0
4
+ version: 0.196.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-24 00:00:00.000000000 Z
11
+ date: 2022-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.195.0
19
+ version: 0.196.2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.195.0
26
+ version: 0.196.2
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debase
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -233,6 +233,7 @@ files:
233
233
  - helpers/jest.config.js
234
234
  - helpers/lib/npm/conflicting-dependency-parser.js
235
235
  - helpers/lib/npm/index.js
236
+ - helpers/lib/npm/vulnerability-auditor.js
236
237
  - helpers/lib/npm6/helpers.js
237
238
  - helpers/lib/npm6/index.js
238
239
  - helpers/lib/npm6/peer-dependency-checker.js
@@ -307,6 +308,7 @@ files:
307
308
  - lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb
308
309
  - lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb
309
310
  - lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb
311
+ - lib/dependabot/npm_and_yarn/update_checker/vulnerability_auditor.rb
310
312
  - lib/dependabot/npm_and_yarn/version.rb
311
313
  homepage: https://github.com/dependabot/dependabot-core
312
314
  licenses: