bundler-audit 0.8.0.rc1 → 0.9.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/ISSUE_TEMPLATE/bug-report.md +44 -0
  4. data/.github/workflows/ruby.yml +16 -2
  5. data/.rubocop.yml +83 -0
  6. data/COPYING.txt +4 -4
  7. data/ChangeLog.md +45 -11
  8. data/Gemfile +7 -3
  9. data/README.md +20 -15
  10. data/Rakefile +7 -3
  11. data/bundler-audit.gemspec +3 -4
  12. data/gemspec.yml +2 -2
  13. data/lib/bundler/audit/advisory.rb +24 -3
  14. data/lib/bundler/audit/cli/formats/json.rb +17 -3
  15. data/lib/bundler/audit/cli/formats/junit.rb +127 -0
  16. data/lib/bundler/audit/cli/formats/text.rb +19 -13
  17. data/lib/bundler/audit/cli/formats.rb +8 -4
  18. data/lib/bundler/audit/cli/thor_ext/shell/basic/say_error.rb +33 -0
  19. data/lib/bundler/audit/cli.rb +41 -29
  20. data/lib/bundler/audit/configuration.rb +12 -5
  21. data/lib/bundler/audit/database.rb +21 -5
  22. data/lib/bundler/audit/results/insecure_source.rb +5 -2
  23. data/lib/bundler/audit/results/unpatched_gem.rb +7 -3
  24. data/lib/bundler/audit/results.rb +2 -2
  25. data/lib/bundler/audit/scanner.rb +9 -3
  26. data/lib/bundler/audit/task.rb +20 -5
  27. data/lib/bundler/audit/version.rb +3 -3
  28. data/lib/bundler/audit.rb +2 -2
  29. data/spec/advisory_spec.rb +9 -1
  30. data/spec/bundle/insecure_sources/Gemfile.lock +73 -71
  31. data/spec/bundle/secure/Gemfile.lock +55 -53
  32. data/spec/cli/formats/json_spec.rb +1 -0
  33. data/spec/cli/formats/junit_spec.rb +284 -0
  34. data/spec/cli/formats/text_spec.rb +113 -19
  35. data/spec/cli_spec.rb +61 -21
  36. data/spec/configuration_spec.rb +8 -0
  37. data/spec/database_spec.rb +25 -1
  38. data/spec/fixtures/advisory/CVE-2020-1234.yml +2 -0
  39. data/spec/fixtures/config/bad/empty.yml +0 -0
  40. data/spec/fixtures/lib/bundler/audit/cli/formats/bad.rb +0 -2
  41. data/spec/fixtures/lib/bundler/audit/cli/formats/good.rb +0 -2
  42. data/spec/integration_spec.rb +17 -103
  43. data/spec/results/unpatched_gem_spec.rb +2 -2
  44. data/spec/scanner_spec.rb +25 -1
  45. data/spec/spec_helper.rb +5 -1
  46. metadata +18 -17
data/gemspec.yml CHANGED
@@ -6,9 +6,9 @@ authors: Postmodern
6
6
  email: postmodern.mod3@gmail.com
7
7
  homepage: https://github.com/rubysec/bundler-audit#readme
8
8
 
9
- required_ruby_version: ">= 1.9.3"
9
+ required_ruby_version: ">= 2.0.0"
10
10
  required_rubygems_version: ">= 1.8.0"
11
11
 
12
12
  dependencies:
13
- thor: ">= 0.18, < 2"
13
+ thor: "~> 1.0"
14
14
  bundler: ">= 1.2.0, < 3"
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2013-2020 Hal Brodigan (postmodern.mod3 at gmail.com)
2
+ # Copyright (c) 2013-2021 Hal Brodigan (postmodern.mod3 at gmail.com)
3
3
  #
4
4
  # bundler-audit is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -12,13 +12,16 @@
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'
19
19
 
20
20
  module Bundler
21
21
  module Audit
22
+ #
23
+ # Represents an advisory loaded from the {Database}.
24
+ #
22
25
  class Advisory < Struct.new(:path,
23
26
  :id,
24
27
  :url,
@@ -45,7 +48,14 @@ module Bundler
45
48
  #
46
49
  def self.load(path)
47
50
  id = File.basename(path).chomp('.yml')
48
- data = YAML.load_file(path)
51
+ data = File.open(path) do |yaml|
52
+ if Psych::VERSION >= '3.1.0'
53
+ YAML.safe_load(yaml, permitted_classes: [Date])
54
+ else
55
+ # XXX: psych < 3.1.0 YAML.safe_load calling convention
56
+ YAML.safe_load(yaml, [Date])
57
+ end
58
+ end
49
59
 
50
60
  unless data.kind_of?(Hash)
51
61
  raise("advisory data in #{path.dump} was not a Hash")
@@ -200,6 +210,17 @@ module Bundler
200
210
  id == other.id
201
211
  end
202
212
 
213
+ #
214
+ # Converts the advisory to a Hash.
215
+ #
216
+ # @return [Hash{Symbol => Object}]
217
+ #
218
+ def to_h
219
+ super.merge({
220
+ criticality: criticality
221
+ })
222
+ end
223
+
203
224
  alias to_s id
204
225
 
205
226
  end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2013-2020 Hal Brodigan (postmodern.mod3 at gmail.com)
2
+ # Copyright (c) 2013-2021 Hal Brodigan (postmodern.mod3 at gmail.com)
3
3
  #
4
4
  # bundler-audit is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -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'
@@ -22,6 +22,9 @@ module Bundler
22
22
  module Audit
23
23
  class CLI < ::Thor
24
24
  module Formats
25
+ #
26
+ # The JSON output format.
27
+ #
25
28
  module JSON
26
29
  #
27
30
  # Outputs the report as JSON. Will pretty-print JSON if `output`
@@ -37,11 +40,22 @@ module Bundler
37
40
  hash = report.to_h
38
41
 
39
42
  if output.tty?
40
- output.puts ::JSON.pretty_generate(hash)
43
+ output.puts(::JSON.pretty_generate(hash))
41
44
  else
42
45
  output.write(::JSON.generate(hash))
43
46
  end
44
47
  end
48
+
49
+ def criticality_label(advisory)
50
+ case advisory.criticality
51
+ when :none then "none"
52
+ when :low then "low"
53
+ when :medium then "medium"
54
+ when :high then "high"
55
+ when :critical then "critical"
56
+ else "unknown"
57
+ end
58
+ end
45
59
  end
46
60
 
47
61
  Formats.register :json, JSON
@@ -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
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2013-2020 Hal Brodigan (postmodern.mod3 at gmail.com)
2
+ # Copyright (c) 2013-2021 Hal Brodigan (postmodern.mod3 at gmail.com)
3
3
  #
4
4
  # bundler-audit is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -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.
@@ -66,20 +69,24 @@ module Bundler
66
69
  say "Version: ", :red
67
70
  say gem.version
68
71
 
69
- say "Advisory: ", :red
70
-
71
72
  if advisory.cve
72
- say "CVE-#{advisory.cve}"
73
- elsif advisory.osvdb
74
- say advisory.osvdb
73
+ say "CVE: ", :red
74
+ say advisory.cve_id
75
+ end
76
+
77
+ if advisory.ghsa
78
+ say "GHSA: ", :red
79
+ say advisory.ghsa_id
75
80
  end
76
81
 
77
82
  say "Criticality: ", :red
78
83
  case advisory.criticality
79
- when :low then say "Low"
80
- when :medium then say "Medium", :yellow
81
- when :high then say "High", [:red, :bold]
82
- 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"
83
90
  end
84
91
 
85
92
  say "URL: ", :red
@@ -89,7 +96,7 @@ module Bundler
89
96
  say "Description:", :red
90
97
  say
91
98
 
92
- print_wrapped advisory.description, :indent => 2
99
+ print_wrapped advisory.description, indent: 2
93
100
  say
94
101
  else
95
102
  say "Title: ", :red
@@ -106,7 +113,6 @@ module Bundler
106
113
 
107
114
  say
108
115
  end
109
-
110
116
  end
111
117
 
112
118
  Formats.register :text, Text
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2013-2020 Hal Brodigan (postmodern.mod3 at gmail.com)
2
+ # Copyright (c) 2013-2021 Hal Brodigan (postmodern.mod3 at gmail.com)
3
3
  #
4
4
  # bundler-audit is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -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
@@ -0,0 +1,33 @@
1
+ class Thor
2
+ module Shell
3
+ class Basic
4
+ #
5
+ # Prints an error message to `stderr`.
6
+ #
7
+ # @param [String] message
8
+ # The message to print to `stderr`.
9
+ #
10
+ # @param [Symbol, nil] color
11
+ # Optional ANSI color.
12
+ #
13
+ # @param [Boolean] force_new_line
14
+ # Controls whether a newline character will be appended to the output.
15
+ #
16
+ def say_error(message,color=nil,force_new_line=(message.to_s !~ /( |\t)\Z/))
17
+ return if quiet?
18
+
19
+ buffer = prepare_message(message,*color)
20
+ buffer << $/ if force_new_line && !message.to_s.end_with?($/)
21
+
22
+ stderr.print(buffer)
23
+ stderr.flush
24
+ end
25
+ end
26
+
27
+ module_eval <<-METHOD, __FILE__, __LINE__ + 1
28
+ def say_error(*args,&block)
29
+ shell.say_error(*args,&block)
30
+ end
31
+ METHOD
32
+ end
33
+ end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2013-2020 Hal Brodigan (postmodern.mod3 at gmail.com)
2
+ # Copyright (c) 2013-2021 Hal Brodigan (postmodern.mod3 at gmail.com)
3
3
  #
4
4
  # bundler-audit is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -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'
@@ -20,40 +20,46 @@ require 'bundler/audit/version'
20
20
  require 'bundler/audit/cli/formats'
21
21
 
22
22
  require 'thor'
23
+ require 'bundler/audit/cli/thor_ext/shell/basic/say_error'
23
24
  require 'bundler'
24
25
 
25
26
  module Bundler
26
27
  module Audit
28
+ #
29
+ # The `bundle-audit` command.
30
+ #
27
31
  class CLI < ::Thor
28
32
 
29
33
  default_task :check
30
34
  map '--version' => :version
31
35
 
32
36
  desc 'check [DIR]', 'Checks the Gemfile.lock for insecure dependencies'
33
- method_option :quiet, :type => :boolean, :aliases => '-q'
34
- method_option :verbose, :type => :boolean, :aliases => '-v'
35
- method_option :ignore, :type => :array, :aliases => '-i'
36
- method_option :update, :type => :boolean, :aliases => '-u'
37
- method_option :database, :type => :string, :aliases => '-D', :default => Database::USER_PATH
38
- method_option :format, :type => :string, :default => 'text',
39
- :aliases => '-F'
40
- method_option :gemfile_lock, :type => :string, :aliases => '-G', :default => 'Gemfile.lock'
41
- 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'
42
48
 
43
49
  def check(dir=Dir.pwd)
44
50
  unless File.directory?(dir)
45
- say "No such file or directory: #{dir}", :red
51
+ say_error "No such file or directory: #{dir}", :red
46
52
  exit 1
47
53
  end
48
54
 
49
55
  begin
50
56
  extend Formats.load(options[:format])
51
57
  rescue Formats::FormatNotFound
52
- say "Unknown format: #{options[:format]}", :red
58
+ say_error "Unknown format: #{options[:format]}", :red
53
59
  exit 1
54
60
  end
55
61
 
56
- if !Database.exists?
62
+ if !Database.exists?(options[:database])
57
63
  download(options[:database])
58
64
  elsif options[:update]
59
65
  update(options[:database])
@@ -61,12 +67,13 @@ module Bundler
61
67
 
62
68
  database = Database.new(options[:database])
63
69
  scanner = begin
64
- Scanner.new(dir,options[:gemfile_lock],database)
70
+ Scanner.new(dir,options[:gemfile_lock],database, options[:config])
65
71
  rescue Bundler::GemfileLockNotFound => exception
66
72
  say exception.message, :red
67
73
  exit 1
68
74
  end
69
- report = scanner.report(:ignore => options.ignore)
75
+
76
+ report = scanner.report(ignore: options.ignore)
70
77
 
71
78
  output = if options[:output] then File.new(options[:output],'w')
72
79
  else $stdout
@@ -80,7 +87,7 @@ module Bundler
80
87
  end
81
88
 
82
89
  desc 'stats', 'Prints ruby-advisory-db stats'
83
- method_option :quiet, :type => :boolean, :aliases => '-q'
90
+ method_option :quiet, type: :boolean, aliases: '-q'
84
91
 
85
92
  def stats(path=Database.path)
86
93
  database = Database.new(path)
@@ -88,10 +95,14 @@ module Bundler
88
95
  puts "ruby-advisory-db:"
89
96
  puts " advisories:\t#{database.size} advisories"
90
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
91
102
  end
92
103
 
93
104
  desc 'download', 'Downloads ruby-advisory-db'
94
- method_option :quiet, :type => :boolean, :aliases => '-q'
105
+ method_option :quiet, type: :boolean, aliases: '-q'
95
106
 
96
107
  def download(path=Database.path)
97
108
  if Database.exists?(path)
@@ -112,7 +123,7 @@ module Bundler
112
123
  end
113
124
 
114
125
  desc 'update', 'Updates the ruby-advisory-db'
115
- method_option :quiet, :type => :boolean, :aliases => '-q'
126
+ method_option :quiet, type: :boolean, aliases: '-q'
116
127
 
117
128
  def update(path=Database.path)
118
129
  unless Database.exists?(path)
@@ -128,13 +139,14 @@ module Bundler
128
139
  when true
129
140
  say("Updated ruby-advisory-db", :green) unless options.quiet?
130
141
  when false
131
- say "Failed updating ruby-advisory-db!", :red
142
+ say_error "Failed updating ruby-advisory-db!", :red
132
143
  exit 1
133
144
  when nil
134
145
  unless Bundler.git_present?
135
- say "Git is not installed!", :red
146
+ say_error "Git is not installed!", :red
136
147
  exit 1
137
148
  end
149
+
138
150
  say "Skipping update", :yellow
139
151
  end
140
152
 
@@ -143,13 +155,18 @@ module Bundler
143
155
 
144
156
  desc 'version', 'Prints the bundler-audit version'
145
157
  def version
146
- database = Database.new
147
-
148
- puts "#{File.basename($0)} #{VERSION} (advisories: #{database.size})"
158
+ puts "bundler-audit #{VERSION}"
149
159
  end
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
  #
@@ -157,11 +174,6 @@ module Bundler
157
174
  raise(NotImplementedError,"#{self.class}##{__method__} not defined")
158
175
  end
159
176
 
160
- def say(message="", color=nil)
161
- color = nil unless $stdout.tty?
162
- super(message.to_s, color)
163
- end
164
-
165
177
  end
166
178
  end
167
179
  end