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
@@ -76,43 +76,137 @@ describe Bundler::Audit::CLI::Formats::Text do
76
76
  expect(output_lines).to include("Version: #{gem.version}")
77
77
  end
78
78
 
79
- it "must print 'Advisory: CVE-YYYY-NNNN'" do
80
- expect(output_lines).to include("Advisory: CVE-#{advisory.cve}")
79
+ context "when the advisory has a CVE ID" do
80
+ it "must print 'CVE: CVE-YYYY-NNNN'" do
81
+ expect(output_lines).to include("CVE: CVE-#{advisory.cve}")
82
+ end
81
83
  end
82
84
 
83
- context "when Advisory#criticality is :low" do
84
- let(:advisory) do
85
- super().tap do |advisory|
86
- advisory.cvss_v2 = 0.0
87
- end
85
+ context "when the advisory does not have a CVE ID" do
86
+ before { advisory.cve = nil }
87
+
88
+ it "must not print 'CVE: CVE-YYYY-NNNN'" do
89
+ expect(output_lines).to_not include("CVE: CVE-#{advisory.cve}")
90
+ end
91
+ end
92
+
93
+ context "when the advisory has a GHSA ID" do
94
+ it "must print 'GHSA: GHSA-xxxx-xxxx-xxxx'" do
95
+ expect(output_lines).to include("GHSA: GHSA-#{advisory.ghsa}")
88
96
  end
97
+ end
89
98
 
90
- it "must print 'Criticality: Low'" do
91
- expect(output_lines).to include("Criticality: Low")
99
+ context "when the advisory does not have a GHSA ID" do
100
+ before { advisory.ghsa = nil }
101
+
102
+ it "must not print 'GHSA: GHSA-xxxx-xxxx-xxxx'" do
103
+ expect(output_lines).to_not include("GHSA: GHSA-#{advisory.ghsa}")
92
104
  end
93
105
  end
94
106
 
95
- context "when Advisory#criticality is :medium" do
96
- let(:advisory) do
97
- super().tap do |advisory|
98
- advisory.cvss_v2 = 6.9
107
+ context "when CVSS v3 is present" do
108
+ context "when Advisory#criticality is :none (cvss_v3 only)" do
109
+ let(:advisory) do
110
+ super().tap do |advisory|
111
+ advisory.cvss_v3 = 0.0
112
+ end
113
+ end
114
+
115
+ it "must print 'Criticality: None'" do
116
+ expect(output_lines).to include("Criticality: None")
99
117
  end
100
118
  end
101
119
 
102
- it "must print 'Criticality: Medium'" do
103
- expect(output_lines).to include("Criticality: Medium")
120
+ context "when Advisory#criticality is :low" do
121
+ let(:advisory) do
122
+ super().tap do |advisory|
123
+ advisory.cvss_v3 = 0.1
124
+ end
125
+ end
126
+
127
+ it "must print 'Criticality: Low'" do
128
+ expect(output_lines).to include("Criticality: Low")
129
+ end
130
+ end
131
+
132
+ context "when Advisory#criticality is :medium" do
133
+ let(:advisory) do
134
+ super().tap do |advisory|
135
+ advisory.cvss_v3 = 4.0
136
+ end
137
+ end
138
+
139
+ it "must print 'Criticality: Medium'" do
140
+ expect(output_lines).to include("Criticality: Medium")
141
+ end
142
+ end
143
+
144
+ context "when Advisory#criticality is :high" do
145
+ let(:advisory) do
146
+ super().tap do |advisory|
147
+ advisory.cvss_v3 = 7.0
148
+ end
149
+ end
150
+
151
+ it "must print 'Criticality: High'" do
152
+ expect(output_lines).to include("Criticality: High")
153
+ end
154
+ end
155
+
156
+ context "when Advisory#criticality is :critical (cvss_v3 only)" do
157
+ let(:advisory) do
158
+ super().tap do |advisory|
159
+ advisory.cvss_v3 = 9.0
160
+ end
161
+ end
162
+
163
+ it "must print 'Criticality: High'" do
164
+ expect(output_lines).to include("Criticality: Critical")
165
+ end
104
166
  end
105
167
  end
106
168
 
107
- context "when Advisory#criticality is :high" do
169
+ context "when CVSS v2 is present" do
108
170
  let(:advisory) do
109
171
  super().tap do |advisory|
110
- advisory.cvss_v2 = 10.0
172
+ advisory.cvss_v3 = nil
173
+ end
174
+ end
175
+
176
+ context "when Advisory#criticality is :low" do
177
+ let(:advisory) do
178
+ super().tap do |advisory|
179
+ advisory.cvss_v2 = 0.0
180
+ end
181
+ end
182
+
183
+ it "must print 'Criticality: Low'" do
184
+ expect(output_lines).to include("Criticality: Low")
111
185
  end
112
186
  end
113
187
 
114
- it "must print 'Criticality: High'" do
115
- expect(output_lines).to include("Criticality: High")
188
+ context "when Advisory#criticality is :medium" do
189
+ let(:advisory) do
190
+ super().tap do |advisory|
191
+ advisory.cvss_v2 = 4.0
192
+ end
193
+ end
194
+
195
+ it "must print 'Criticality: Medium'" do
196
+ expect(output_lines).to include("Criticality: Medium")
197
+ end
198
+ end
199
+
200
+ context "when Advisory#criticality is :high" do
201
+ let(:advisory) do
202
+ super().tap do |advisory|
203
+ advisory.cvss_v2 = 7.0
204
+ end
205
+ end
206
+
207
+ it "must print 'Criticality: High'" do
208
+ expect(output_lines).to include("Criticality: High")
209
+ end
116
210
  end
117
211
  end
118
212
 
data/spec/cli_spec.rb CHANGED
@@ -15,28 +15,68 @@ describe Bundler::Audit::CLI do
15
15
  end
16
16
  end
17
17
 
18
+ describe "#stats" do
19
+ let(:size) { 1234 }
20
+ let(:last_updated_at) { Time.now }
21
+ let(:commit_id) { 'f0f97c4c493b853319e029d226e96f2c2f0dc539' }
22
+
23
+ let(:database) { double(Bundler::Audit::Database) }
24
+
25
+ before do
26
+ expect(Bundler::Audit::Database).to receive(:new).and_return(database)
27
+
28
+ expect(database).to receive(:size).and_return(size)
29
+ expect(database).to receive(:last_updated_at).and_return(last_updated_at)
30
+ expect(database).to receive(:commit_id).and_return(commit_id)
31
+ end
32
+
33
+ it "prints total advisory count" do
34
+ expect { subject.stats }.to output(
35
+ include(
36
+ "advisories:\t#{size} advisories",
37
+ "last updated:\t#{last_updated_at}",
38
+ "commit:\t#{commit_id}"
39
+ )
40
+ ).to_stdout
41
+ end
42
+ end
43
+
18
44
  describe "#update" do
45
+ let(:database) { double(Bundler::Audit::Database) }
46
+
47
+ before do
48
+ allow(Bundler::Audit::Database).to receive(:new).and_return(database)
49
+ end
50
+
19
51
  context "not --quiet (the default)" do
20
52
  context "when update succeeds" do
53
+ let(:size) { 1234 }
54
+ let(:last_updated_at) { Time.now }
55
+ let(:commit_id) { 'f0f97c4c493b853319e029d226e96f2c2f0dc539' }
56
+
21
57
  before do
22
- expect_any_instance_of(Bundler::Audit::Database).to receive(:update!).and_return(true)
58
+ expect(database).to receive(:update!).and_return(true)
59
+ expect(database).to receive(:size).and_return(size)
60
+ expect(database).to receive(:last_updated_at).and_return(last_updated_at)
61
+ expect(database).to receive(:commit_id).and_return(commit_id)
23
62
  end
24
63
 
25
- it "prints updated message" do
26
- expect { subject.update }.to output(/Updated ruby-advisory-db/).to_stdout
27
- end
28
-
29
- it "prints total advisory count" do
30
- size = 1234
31
- expect_any_instance_of(Bundler::Audit::Database).to receive(:size).and_return(size)
32
-
33
- expect { subject.update }.to output(/advisories:\t#{size} advisories/).to_stdout
64
+ it "prints updated message and then the stats" do
65
+ expect { subject.update }.to output(
66
+ include(
67
+ "Updated ruby-advisory-db",
68
+ "ruby-advisory-db:",
69
+ " advisories:\t#{size} advisories",
70
+ " last updated:\t#{last_updated_at}",
71
+ " commit:\t#{commit_id}"
72
+ )
73
+ ).to_stdout
34
74
  end
35
75
  end
36
76
 
37
77
  context "when update fails" do
38
78
  before do
39
- expect_any_instance_of(Bundler::Audit::Database).to receive(:update!).and_return(false)
79
+ expect(database).to receive(:update!).and_return(false)
40
80
  end
41
81
 
42
82
  it "prints failure message" do
@@ -45,7 +85,7 @@ describe Bundler::Audit::CLI do
45
85
  subject.update
46
86
  rescue SystemExit
47
87
  end
48
- }.to output(/Failed updating ruby-advisory-db!/).to_stdout
88
+ }.to output(/Failed updating ruby-advisory-db!/).to_stderr
49
89
  end
50
90
 
51
91
  it "exits with error status code" do
@@ -58,22 +98,22 @@ describe Bundler::Audit::CLI do
58
98
  expect(error.status).to eq(1)
59
99
  end
60
100
  end
61
-
62
101
  end
63
102
 
64
103
  context "when git is not installed" do
65
104
  before do
66
- expect_any_instance_of(Bundler::Audit::Database).to receive(:update!).and_return(nil)
105
+ expect(database).to receive(:update!).and_return(nil)
106
+
67
107
  expect(Bundler).to receive(:git_present?).and_return(false)
68
108
  end
69
109
 
70
110
  it "prints failure message" do
71
- expect do
111
+ expect {
72
112
  begin
73
113
  subject.update
74
114
  rescue SystemExit
75
115
  end
76
- end.to output(/Git is not installed!/).to_stdout
116
+ }.to output(/Git is not installed!/).to_stderr
77
117
  end
78
118
 
79
119
  it "exits with error status code" do
@@ -90,13 +130,13 @@ describe Bundler::Audit::CLI do
90
130
  end
91
131
 
92
132
  context "--quiet" do
93
- before do
94
- allow(subject).to receive(:options).and_return(double("Options", quiet?: true))
133
+ subject do
134
+ described_class.new([], {quiet: true})
95
135
  end
96
136
 
97
137
  context "when update succeeds" do
98
138
  before do
99
- expect_any_instance_of(Bundler::Audit::Database).to(
139
+ expect(database).to(
100
140
  receive(:update!).with(quiet: true).and_return(true)
101
141
  )
102
142
  end
@@ -108,7 +148,7 @@ describe Bundler::Audit::CLI do
108
148
 
109
149
  context "when update fails" do
110
150
  before do
111
- expect_any_instance_of(Bundler::Audit::Database).to(
151
+ expect(database).to(
112
152
  receive(:update!).with(quiet: true).and_return(false)
113
153
  )
114
154
  end
@@ -119,7 +159,7 @@ describe Bundler::Audit::CLI do
119
159
  subject.update
120
160
  rescue SystemExit
121
161
  end
122
- }.to output(/Failed updating ruby-advisory-db!/).to_stdout
162
+ }.to_not output.to_stderr
123
163
  end
124
164
 
125
165
  it "exits with error status code" do
@@ -22,6 +22,14 @@ describe Bundler::Audit::Configuration do
22
22
  end
23
23
 
24
24
  context "validations" do
25
+ context "when the file is empty" do
26
+ let(:path) { File.join(fixtures_dir,'bad','empty.yml') }
27
+
28
+ it 'raises a validation error' do
29
+ expect { subject }.to raise_error(described_class::InvalidConfigurationError)
30
+ end
31
+ end
32
+
25
33
  context "when ignore is not an array" do
26
34
  let(:path) { File.join(fixtures_dir,'bad','ignore_is_not_an_array.yml') }
27
35
 
@@ -174,7 +174,7 @@ describe Bundler::Audit::Database do
174
174
  end
175
175
 
176
176
  context "when given a directory" do
177
- let(:path ) { Dir.tmpdir }
177
+ let(:path) { Dir.tmpdir }
178
178
 
179
179
  subject { described_class.new(path) }
180
180
 
@@ -263,6 +263,30 @@ describe Bundler::Audit::Database do
263
263
  end
264
264
  end
265
265
 
266
+ describe "#commit_id" do
267
+ context "when the database is a git repository" do
268
+ let(:last_commit) { Fixtures::Database::COMMIT }
269
+
270
+ it "should return the last commit ID" do
271
+ expect(subject.commit_id).to be == last_commit
272
+ end
273
+ end
274
+
275
+ context "when the database is a bare directory" do
276
+ let(:path) { Fixtures.join('mock-database-dir') }
277
+
278
+ before { FileUtils.mkdir(path) }
279
+
280
+ subject { described_class.new(path) }
281
+
282
+ it "should return the mtime of the directory" do
283
+ expect(subject.commit_id).to be(nil)
284
+ end
285
+
286
+ after { FileUtils.rmdir(path) }
287
+ end
288
+ end
289
+
266
290
  describe "#last_updated_at" do
267
291
  context "when the database is a git repository" do
268
292
  let(:last_commit) { Fixtures::Database::COMMIT }
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  gem: test
3
3
  cve: 2020-1234
4
+ ghsa: aaaa-bbbb-cccc
4
5
  url: https://example.com/
5
6
  title: Test advisory
6
7
  date: 2015-06-16
@@ -9,6 +10,7 @@ description: |
9
10
  This is a test advisory.
10
11
 
11
12
  cvss_v2: 10.0
13
+ cvss_v3: 9.8
12
14
 
13
15
  unaffected_versions:
14
16
  - "< 0.1.0"
File without changes
@@ -5,11 +5,9 @@ module Bundler
5
5
  class CLI < ::Thor
6
6
  module Formats
7
7
  module Bad
8
-
9
8
  def print_report(report,output=$stdout)
10
9
  say "I am a bad format!", :red
11
10
  end
12
-
13
11
  end
14
12
 
15
13
  Formats.register :incorrect, Bad
@@ -5,11 +5,9 @@ module Bundler
5
5
  class CLI < ::Thor
6
6
  module Formats
7
7
  module Good
8
-
9
8
  def print_report(report,output=$stdout)
10
9
  say "I am a good format.", :green
11
10
  end
12
-
13
11
  end
14
12
 
15
13
  Formats.register :good, Good
@@ -1,117 +1,31 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe "CLI" do
4
- include Helpers
5
-
6
- let(:command) do
7
- File.expand_path(File.join(File.dirname(__FILE__),'..','bin','bundler-audit'))
8
- end
9
-
10
- context "when auditing a bundle with unpatched gems" do
11
- let(:bundle) { 'unpatched_gems' }
12
- let(:directory) { File.join('spec','bundle',bundle) }
13
-
14
- subject do
15
- Dir.chdir(directory) { sh(command, :fail => true) }
16
- end
17
-
18
- it "should print a warning" do
19
- expect(subject).to include("Vulnerabilities found!")
20
- end
21
-
22
- it "should print advisory information for the vulnerable gems" do
23
- advisory_pattern = %r{(Name: [^\n]+
24
- Version: \d+\.\d+\.\d+(\.\d+)?
25
- Advisory: CVE-[0-9]{4}-[0-9]{4}
26
- Criticality: (Critical|High|Medium|Low|None|Unknown)
27
- URL: https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#!?&//=]*)
28
- Title: [^\n]*?
29
- Solution: upgrade to (~>|>=) \d+\.\d+\.\d+(\.\d+)?(, (~>|>=) \d+\.\d+\.\d+(\.\d+)?)*[\s\n]*?)}
30
-
31
- expect(subject).to match(advisory_pattern)
32
- expect(subject).to include("Vulnerabilities found!")
33
- end
34
- end
35
-
36
- context "when auditing a bundle with ignored gems" do
37
- let(:bundle) { 'unpatched_gems' }
38
- let(:directory) { File.join('spec','bundle',bundle) }
39
-
40
- let(:command) do
41
- File.expand_path(File.join(File.dirname(__FILE__),'..','bin','bundler-audit -i CVE-2013-0156'))
42
- end
43
-
44
- subject do
45
- Dir.chdir(directory) { sh(command, :fail => true) }
46
- end
47
-
48
- it "should not print advisory information for ignored gem" do
49
- expect(subject).not_to include("CVE-2013-0156")
50
- end
51
- end
52
-
53
- context "when auditing a bundle with insecure sources" do
54
- let(:bundle) { 'insecure_sources' }
55
- let(:directory) { File.join('spec','bundle',bundle) }
56
-
57
- subject do
58
- Dir.chdir(directory) { sh(command, :fail => true) }
59
- end
60
-
61
- it "should print warnings about insecure sources" do
62
- expect(subject).to include(%{
63
- Insecure Source URI found: git://github.com/rails/jquery-rails.git
64
- Insecure Source URI found: http://rubygems.org/
65
- }.strip)
66
- end
3
+ describe "bin/bundler-audit" do
4
+ let(:name) { 'bundler-audit' }
5
+ let(:path) do
6
+ File.expand_path(File.join(File.dirname(__FILE__),'..','bin',name))
67
7
  end
68
8
 
69
- context "when auditing a secure bundle" do
70
- let(:bundle) { 'secure' }
71
- let(:directory) { File.join('spec','bundle',bundle) }
9
+ let(:command) { "#{path} version" }
72
10
 
73
- subject do
74
- Dir.chdir(directory) { sh(command) }
75
- end
11
+ subject { sh(command) }
76
12
 
77
- it "should print nothing when everything is fine" do
78
- expect(subject.strip).to eq("No vulnerabilities found")
79
- end
13
+ it "must invoke the CLI class" do
14
+ expect(subject).to eq("bundler-audit #{Bundler::Audit::VERSION}#{$/}")
80
15
  end
16
+ end
81
17
 
82
- context "when auditing a non-existent Gemfile.lock file" do
83
- let(:bundle) { 'secure' }
84
- let(:directory) { File.join('spec','bundle',bundle) }
85
- let(:root) { File.expand_path(directory) }
86
-
87
- let(:gemfile_lock) { 'Gemfile.foo.lock' }
88
- let(:command) { "#{super()} --gemfile-lock #{gemfile_lock}" }
89
-
90
- subject do
91
- Dir.chdir(directory) { sh(command, :fail => true) }
92
- end
93
-
94
- it "should print an error message" do
95
- expect(subject.strip).to eq("Could not find #{gemfile_lock.inspect} in #{root.inspect}")
96
- end
18
+ describe "bin/bundle-audit" do
19
+ let(:name) { 'bundle-audit' }
20
+ let(:path) do
21
+ File.expand_path(File.join(File.dirname(__FILE__),'..','bin',name))
97
22
  end
98
23
 
99
- describe "update" do
100
- let(:update_command) { "#{command} update" }
101
- let(:bundle) { 'secure' }
102
- let(:directory) { File.join('spec','bundle',bundle) }
24
+ let(:command) { "#{path} version" }
103
25
 
104
- subject do
105
- Dir.chdir(directory) { sh(update_command) }
106
- end
26
+ subject { sh(command) }
107
27
 
108
- context "when advisories update successfully" do
109
- it "should print status" do
110
- expect(subject).not_to include("Fail")
111
- expect(subject).to include("Updating ruby-advisory-db ...\n")
112
- expect(subject).to include("Updated ruby-advisory-db\n")
113
- expect(subject).to match(/ruby-advisory-db:\n advisories:\s+[1-9]\d+ advisories/)
114
- end
115
- end
28
+ it "must invoke the CLI class" do
29
+ expect(subject).to eq("bundler-audit #{Bundler::Audit::VERSION}#{$/}")
116
30
  end
117
31
  end