bundler-audit 0.8.0 → 0.9.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug-report.md +44 -0
  3. data/.github/workflows/ruby.yml +14 -1
  4. data/.rubocop.yml +83 -0
  5. data/COPYING.txt +4 -4
  6. data/ChangeLog.md +30 -0
  7. data/Gemfile +7 -3
  8. data/README.md +16 -10
  9. data/Rakefile +7 -3
  10. data/bundler-audit.gemspec +3 -4
  11. data/lib/bundler/audit/advisory.rb +23 -2
  12. data/lib/bundler/audit/cli/formats/json.rb +16 -2
  13. data/lib/bundler/audit/cli/formats/junit.rb +127 -0
  14. data/lib/bundler/audit/cli/formats/text.rb +11 -7
  15. data/lib/bundler/audit/cli/formats.rb +7 -3
  16. data/lib/bundler/audit/cli.rb +32 -15
  17. data/lib/bundler/audit/configuration.rb +7 -4
  18. data/lib/bundler/audit/database.rb +20 -4
  19. data/lib/bundler/audit/results/insecure_source.rb +4 -1
  20. data/lib/bundler/audit/results/unpatched_gem.rb +6 -2
  21. data/lib/bundler/audit/results.rb +1 -1
  22. data/lib/bundler/audit/scanner.rb +8 -2
  23. data/lib/bundler/audit/task.rb +20 -5
  24. data/lib/bundler/audit/version.rb +2 -2
  25. data/lib/bundler/audit.rb +1 -1
  26. data/spec/advisory_spec.rb +9 -1
  27. data/spec/bundle/insecure_sources/Gemfile.lock +69 -71
  28. data/spec/bundle/secure/Gemfile.lock +51 -53
  29. data/spec/cli/formats/json_spec.rb +1 -0
  30. data/spec/cli/formats/junit_spec.rb +284 -0
  31. data/spec/cli/formats/text_spec.rb +87 -17
  32. data/spec/cli_spec.rb +57 -17
  33. data/spec/database_spec.rb +25 -1
  34. data/spec/fixtures/advisory/CVE-2020-1234.yml +1 -0
  35. data/spec/fixtures/lib/bundler/audit/cli/formats/bad.rb +0 -2
  36. data/spec/fixtures/lib/bundler/audit/cli/formats/good.rb +0 -2
  37. data/spec/results/unpatched_gem_spec.rb +2 -2
  38. data/spec/scanner_spec.rb +25 -1
  39. data/spec/spec_helper.rb +5 -1
  40. metadata +10 -6
@@ -0,0 +1,127 @@
1
+ #
2
+ # Copyright (c) 2013-2021 Hal Brodigan (postmodern.mod3 at gmail.com)
3
+ #
4
+ # bundler-audit is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # bundler-audit is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with bundler-audit. If not, see <https://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ require 'thor'
19
+ require 'cgi'
20
+
21
+ module Bundler
22
+ module Audit
23
+ class CLI < ::Thor
24
+ module Formats
25
+ module Junit
26
+ #
27
+ # Prints any findings as an XML junit report.
28
+ #
29
+ # @param [Report] report
30
+ # The results from the {Scanner}.
31
+ #
32
+ # @param [IO, File] output
33
+ # Optional output stream.
34
+ #
35
+ def print_report(report, output=$stdout)
36
+ original_stdout = $stdout
37
+ $stdout = output
38
+
39
+ print_xml_testsuite(report) do
40
+ report.each do |result|
41
+ print_xml_testcase(result)
42
+ end
43
+ end
44
+
45
+ $stdout = original_stdout
46
+ end
47
+
48
+ private
49
+
50
+ def say_xml(*lines)
51
+ say(lines.join($/))
52
+ end
53
+
54
+ def print_xml_testsuite(report)
55
+ say_xml(
56
+ %{<?xml version="1.0" encoding="UTF-8" ?>},
57
+ %{<testsuites id="#{Time.now.to_i}" name="Bundle Audit">},
58
+ %{ <testsuite id="Gemfile" name="Ruby Gemfile" failures="#{report.count}">}
59
+ )
60
+
61
+ yield
62
+
63
+ say_xml(
64
+ %{ </testsuite>},
65
+ %{</testsuites>}
66
+ )
67
+ end
68
+
69
+ def xml(string)
70
+ CGI.escapeHTML(string.to_s)
71
+ end
72
+
73
+ def print_xml_testcase(result)
74
+ case result
75
+ when Results::InsecureSource
76
+ say_xml(
77
+ %{ <testcase id="#{xml(result.source)}" name="Insecure Source URI found: #{xml(result.source)}">},
78
+ %{ <failure message="Insecure Source URI found: #{xml(result.source)}" type="Unknown"></failure>},
79
+ %{ </testcase>}
80
+ )
81
+ when Results::UnpatchedGem
82
+ say_xml(
83
+ %{ <testcase id="#{xml(result.gem.name)}" name="#{xml(bundle_title(result))}">},
84
+ %{ <failure message="#{xml(result.advisory.title)}" type="#{xml(result.advisory.criticality)}">},
85
+ %{ Name: #{xml(result.gem.name)}},
86
+ %{ Version: #{xml(result.gem.version)}},
87
+ %{ Advisory: #{xml(advisory_ref(result.advisory))}},
88
+ %{ Criticality: #{xml(advisory_criticality(result.advisory))}},
89
+ %{ URL: #{xml(result.advisory.url)}},
90
+ %{ Title: #{xml(result.advisory.title)}},
91
+ %{ Solution: #{xml(advisory_solution(result.advisory))}},
92
+ %{ </failure>},
93
+ %{ </testcase>}
94
+ )
95
+ end
96
+ end
97
+
98
+ def bundle_title(result)
99
+ "#{advisory_criticality(result.advisory).upcase} #{result.gem.name}(#{result.gem.version}) #{result.advisory.title}"
100
+ end
101
+
102
+ def advisory_solution(advisory)
103
+ unless advisory.patched_versions.empty?
104
+ "upgrade to #{advisory.patched_versions.join(', ')}"
105
+ else
106
+ "remove or disable this gem until a patch is available!"
107
+ end
108
+ end
109
+
110
+ def advisory_criticality(advisory)
111
+ if advisory.criticality
112
+ advisory.criticality.to_s.capitalize
113
+ else
114
+ "Unknown"
115
+ end
116
+ end
117
+
118
+ def advisory_ref(advisory)
119
+ advisory.identifiers.join(" ")
120
+ end
121
+
122
+ Formats.register :junit, Junit
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -12,7 +12,7 @@
12
12
  # GNU General Public License for more details.
13
13
  #
14
14
  # You should have received a copy of the GNU General Public License
15
- # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
15
+ # along with bundler-audit. If not, see <https://www.gnu.org/licenses/>.
16
16
  #
17
17
 
18
18
  require 'thor'
@@ -21,6 +21,9 @@ module Bundler
21
21
  module Audit
22
22
  class CLI < ::Thor
23
23
  module Formats
24
+ #
25
+ # The plain-text output format.
26
+ #
24
27
  module Text
25
28
  #
26
29
  # Prints any findings as plain-text.
@@ -78,10 +81,12 @@ module Bundler
78
81
 
79
82
  say "Criticality: ", :red
80
83
  case advisory.criticality
81
- when :low then say "Low"
82
- when :medium then say "Medium", :yellow
83
- when :high then say "High", [:red, :bold]
84
- else say "Unknown"
84
+ when :none then say "None"
85
+ when :low then say "Low"
86
+ when :medium then say "Medium", :yellow
87
+ when :high then say "High", [:red, :bold]
88
+ when :critical then say "Critical", [:red, :bold]
89
+ else say "Unknown"
85
90
  end
86
91
 
87
92
  say "URL: ", :red
@@ -91,7 +96,7 @@ module Bundler
91
96
  say "Description:", :red
92
97
  say
93
98
 
94
- print_wrapped advisory.description, :indent => 2
99
+ print_wrapped advisory.description, indent: 2
95
100
  say
96
101
  else
97
102
  say "Title: ", :red
@@ -108,7 +113,6 @@ module Bundler
108
113
 
109
114
  say
110
115
  end
111
-
112
116
  end
113
117
 
114
118
  Formats.register :text, Text
@@ -12,7 +12,7 @@
12
12
  # GNU General Public License for more details.
13
13
  #
14
14
  # You should have received a copy of the GNU General Public License
15
- # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
15
+ # along with bundler-audit. If not, see <https://www.gnu.org/licenses/>.
16
16
  #
17
17
 
18
18
  require 'thor'
@@ -126,15 +126,19 @@ module Bundler
126
126
  #
127
127
  def self.load(name)
128
128
  name = name.to_s
129
+ path = File.join(DIR,File.basename(name))
129
130
 
130
131
  begin
131
- require File.join(DIR,File.basename(name))
132
+ require path
132
133
  rescue LoadError
133
134
  raise(FormatNotFound,"could not load format #{name.inspect}")
134
135
  end
135
136
 
136
- return self[name] || \
137
+ unless (format = self[name])
137
138
  raise(FormatNotFound,"unknown format #{name.inspect}")
139
+ end
140
+
141
+ return format
138
142
  end
139
143
  end
140
144
  end
@@ -12,7 +12,7 @@
12
12
  # GNU General Public License for more details.
13
13
  #
14
14
  # You should have received a copy of the GNU General Public License
15
- # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
15
+ # along with bundler-audit. If not, see <https://www.gnu.org/licenses/>.
16
16
  #
17
17
 
18
18
  require 'bundler/audit/scanner'
@@ -25,21 +25,26 @@ require 'bundler'
25
25
 
26
26
  module Bundler
27
27
  module Audit
28
+ #
29
+ # The `bundle-audit` command.
30
+ #
28
31
  class CLI < ::Thor
29
32
 
30
33
  default_task :check
31
34
  map '--version' => :version
32
35
 
33
36
  desc 'check [DIR]', 'Checks the Gemfile.lock for insecure dependencies'
34
- method_option :quiet, :type => :boolean, :aliases => '-q'
35
- method_option :verbose, :type => :boolean, :aliases => '-v'
36
- method_option :ignore, :type => :array, :aliases => '-i'
37
- method_option :update, :type => :boolean, :aliases => '-u'
38
- method_option :database, :type => :string, :aliases => '-D', :default => Database::USER_PATH
39
- method_option :format, :type => :string, :default => 'text',
40
- :aliases => '-F'
41
- method_option :gemfile_lock, :type => :string, :aliases => '-G', :default => 'Gemfile.lock'
42
- method_option :output, :type => :string, :aliases => '-o'
37
+ method_option :quiet, type: :boolean, aliases: '-q'
38
+ method_option :verbose, type: :boolean, aliases: '-v'
39
+ method_option :ignore, type: :array, aliases: '-i'
40
+ method_option :update, type: :boolean, aliases: '-u'
41
+ method_option :database, type: :string, aliases: '-D',
42
+ default: Database::USER_PATH
43
+ method_option :format, type: :string, default: 'text', aliases: '-F'
44
+ method_option :config, type: :string, aliases: '-c', default: '.bundler-audit.yml'
45
+ method_option :gemfile_lock, type: :string, aliases: '-G',
46
+ default: 'Gemfile.lock'
47
+ method_option :output, type: :string, aliases: '-o'
43
48
 
44
49
  def check(dir=Dir.pwd)
45
50
  unless File.directory?(dir)
@@ -62,12 +67,13 @@ module Bundler
62
67
 
63
68
  database = Database.new(options[:database])
64
69
  scanner = begin
65
- Scanner.new(dir,options[:gemfile_lock],database)
70
+ Scanner.new(dir,options[:gemfile_lock],database, options[:config])
66
71
  rescue Bundler::GemfileLockNotFound => exception
67
72
  say exception.message, :red
68
73
  exit 1
69
74
  end
70
- report = scanner.report(:ignore => options.ignore)
75
+
76
+ report = scanner.report(ignore: options.ignore)
71
77
 
72
78
  output = if options[:output] then File.new(options[:output],'w')
73
79
  else $stdout
@@ -81,7 +87,7 @@ module Bundler
81
87
  end
82
88
 
83
89
  desc 'stats', 'Prints ruby-advisory-db stats'
84
- method_option :quiet, :type => :boolean, :aliases => '-q'
90
+ method_option :quiet, type: :boolean, aliases: '-q'
85
91
 
86
92
  def stats(path=Database.path)
87
93
  database = Database.new(path)
@@ -89,10 +95,14 @@ module Bundler
89
95
  puts "ruby-advisory-db:"
90
96
  puts " advisories:\t#{database.size} advisories"
91
97
  puts " last updated:\t#{database.last_updated_at}"
98
+
99
+ if (commit_id = database.commit_id)
100
+ puts " commit:\t#{commit_id}"
101
+ end
92
102
  end
93
103
 
94
104
  desc 'download', 'Downloads ruby-advisory-db'
95
- method_option :quiet, :type => :boolean, :aliases => '-q'
105
+ method_option :quiet, type: :boolean, aliases: '-q'
96
106
 
97
107
  def download(path=Database.path)
98
108
  if Database.exists?(path)
@@ -113,7 +123,7 @@ module Bundler
113
123
  end
114
124
 
115
125
  desc 'update', 'Updates the ruby-advisory-db'
116
- method_option :quiet, :type => :boolean, :aliases => '-q'
126
+ method_option :quiet, type: :boolean, aliases: '-q'
117
127
 
118
128
  def update(path=Database.path)
119
129
  unless Database.exists?(path)
@@ -150,6 +160,13 @@ module Bundler
150
160
 
151
161
  protected
152
162
 
163
+ #
164
+ # @note Silence deprecation warnings from Thor.
165
+ #
166
+ def self.exit_on_failure?
167
+ true
168
+ end
169
+
153
170
  #
154
171
  # @abstract
155
172
  #
@@ -12,7 +12,7 @@
12
12
  # GNU General Public License for more details.
13
13
  #
14
14
  # You should have received a copy of the GNU General Public License
15
- # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
15
+ # along with bundler-audit. If not, see <https://www.gnu.org/licenses/>.
16
16
  #
17
17
 
18
18
  require 'yaml'
@@ -26,14 +26,17 @@ module Bundler
26
26
  # @since 0.8.0
27
27
  #
28
28
  class Configuration
29
- class InvalidConfigurationError < StandardError; end
30
- class FileNotFound < StandardError; end
29
+ class InvalidConfigurationError < StandardError
30
+ end
31
+
32
+ class FileNotFound < StandardError
33
+ end
31
34
 
32
35
  #
33
36
  # A constructor method for loading configuration from a YAML file.
34
37
  #
35
38
  # @param [String] file_path
36
- # Path to the yaml file holding the configuration.
39
+ # Path to the YAML file holding the configuration.
37
40
  #
38
41
  # @raise [FileNotFound]
39
42
  # Will raise a file not found error when the path to the
@@ -12,7 +12,7 @@
12
12
  # GNU General Public License for more details.
13
13
  #
14
14
  # You should have received a copy of the GNU General Public License
15
- # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
15
+ # along with bundler-audit. If not, see <https://www.gnu.org/licenses/>.
16
16
  #
17
17
 
18
18
  require 'bundler/audit/advisory'
@@ -82,7 +82,7 @@ module Bundler
82
82
  # The given path of the database to check.
83
83
  #
84
84
  # @return [Boolean]
85
- #
85
+ #
86
86
  # @since 0.8.0
87
87
  #
88
88
  def self.exists?(path=DEFAULT_PATH)
@@ -119,7 +119,7 @@ module Bundler
119
119
 
120
120
  path = options.fetch(:path,DEFAULT_PATH)
121
121
 
122
- command = %w(git clone)
122
+ command = %w[git clone]
123
123
  command << '--quiet' if options[:quiet]
124
124
  command << URL << path
125
125
 
@@ -199,7 +199,7 @@ module Bundler
199
199
  def update!(options={})
200
200
  if git?
201
201
  Dir.chdir(@path) do
202
- command = %w(git pull)
202
+ command = %w[git pull]
203
203
  command << '--quiet' if options[:quiet]
204
204
  command << 'origin' << 'master'
205
205
 
@@ -212,6 +212,22 @@ module Bundler
212
212
  end
213
213
  end
214
214
 
215
+ #
216
+ # The last commit ID of the repository.
217
+ #
218
+ # @return [String, nil]
219
+ # The commit hash or `nil` if the database is not a git repository.
220
+ #
221
+ # @since 0.9.0
222
+ #
223
+ def commit_id
224
+ if git?
225
+ Dir.chdir(@path) do
226
+ `git rev-parse HEAD`.chomp
227
+ end
228
+ end
229
+ end
230
+
215
231
  #
216
232
  # Determines the time when the database was last updated.
217
233
  #
@@ -12,7 +12,7 @@
12
12
  # GNU General Public License for more details.
13
13
  #
14
14
  # You should have received a copy of the GNU General Public License
15
- # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
15
+ # along with bundler-audit. If not, see <https://www.gnu.org/licenses/>.
16
16
  #
17
17
 
18
18
  require 'bundler/audit/results/result'
@@ -20,6 +20,9 @@ require 'bundler/audit/results/result'
20
20
  module Bundler
21
21
  module Audit
22
22
  module Results
23
+ #
24
+ # Represents an insecure gem source (ex: `git://...` or `http://...`).
25
+ #
23
26
  class InsecureSource < Result
24
27
 
25
28
  # The insecure `git://` or `http://` URI.
@@ -12,7 +12,7 @@
12
12
  # GNU General Public License for more details.
13
13
  #
14
14
  # You should have received a copy of the GNU General Public License
15
- # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
15
+ # along with bundler-audit. If not, see <https://www.gnu.org/licenses/>.
16
16
  #
17
17
 
18
18
  require 'bundler/audit/results/result'
@@ -22,6 +22,10 @@ require 'uri'
22
22
  module Bundler
23
23
  module Audit
24
24
  module Results
25
+ #
26
+ # Represents a gem version that has known vulnerabilities and needs to be
27
+ # upgraded.
28
+ #
25
29
  class UnpatchedGem < Result
26
30
 
27
31
  # The specification of the vulnerable gem.
@@ -73,7 +77,7 @@ module Bundler
73
77
  end
74
78
 
75
79
  #
76
- # Converts the unpached gem to a Hash.
80
+ # Converts the unpatched gem to a Hash.
77
81
  #
78
82
  # @return [Hash{Symbol => Object}]
79
83
  #