dependabot-npm_and_yarn 0.195.0 → 0.196.0

Sign up to get free protection for your applications and to get access to all the features.
data/helpers/package.json CHANGED
@@ -12,6 +12,7 @@
12
12
  "@dependabot/yarn-lib": "^1.21.1",
13
13
  "@npmcli/arborist": "^5.2.0",
14
14
  "detect-indent": "^6.1.0",
15
+ "nock": "^13.2.4",
15
16
  "npm": "6.14.17",
16
17
  "semver": "^7.3.7"
17
18
  },
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
  });
@@ -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,80 @@ 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
123
+
124
+ return version_resolver.latest_version_resolvable_with_full_unlock? if dependency.top_level?
125
+
126
+ return false unless transitive_security_updates_enabled? && security_advisories.any?
111
127
 
112
- # No support for full unlocks for subdependencies yet
113
- return false unless dependency.top_level?
128
+ vulnerability_audit["fix_available"]
129
+ end
114
130
 
115
- version_resolver.latest_version_resolvable_with_full_unlock?
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 = FileParser.new(
146
+ dependency_files: dependency_files,
147
+ credentials: credentials,
148
+ source: nil
149
+ ).parse.select(&:top_level?)
150
+
151
+ top_level_dependency_lookup = top_level_dependencies.map { |dep| [dep.name, dep] }.to_h
152
+
153
+ updated_deps = []
154
+ vulnerability_audit["fix_updates"].each do |update|
155
+ dependency_name = update["dependency_name"]
156
+ requirements = top_level_dependency_lookup[dependency_name]&.requirements || []
157
+ conflicting_dep = Dependency.new(
158
+ name: dependency_name,
159
+ package_manager: "npm_and_yarn",
160
+ requirements: requirements
161
+ )
162
+
163
+ updated_deps << build_updated_dependency(
164
+ dependency: conflicting_dep,
165
+ version: update["target_version"],
166
+ previous_version: update["current_version"]
167
+ )
168
+ end
169
+
170
+ # We don't need to update this but need to include it so it's described
171
+ # in the PR and we'll pass validation that this dependency is at a
172
+ # non-vulnerable version.
173
+ if updated_deps.none? { |dep| dep.name == dependency.name }
174
+ updated_deps << build_updated_dependency(
175
+ dependency: dependency,
176
+ version: vulnerability_audit["target_version"],
177
+ previous_version: dependency.version
178
+ )
179
+ end
180
+
181
+ updated_deps
182
+ end
183
+
123
184
  def build_updated_dependency(update_details)
124
185
  original_dep = update_details.fetch(:dependency)
125
186
  version = update_details.fetch(:version).to_s
metadata CHANGED
@@ -1,7 +1,7 @@
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -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.0
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.0
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: