bundler-audit-ng 0.6.1
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.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +11 -0
- data/.gitmodules +3 -0
- data/.rspec +1 -0
- data/.travis.yml +13 -0
- data/.yardopts +1 -0
- data/COPYING.txt +674 -0
- data/ChangeLog.md +129 -0
- data/Gemfile +13 -0
- data/README.md +168 -0
- data/Rakefile +57 -0
- data/bin/bundle-audit +10 -0
- data/bin/bundler-audit +3 -0
- data/bundler-audit.gemspec +67 -0
- data/data/ruby-advisory-db.ts +1 -0
- data/gemspec.yml +14 -0
- data/lib/bundler/audit.rb +19 -0
- data/lib/bundler/audit/advisory.rb +177 -0
- data/lib/bundler/audit/cli.rb +155 -0
- data/lib/bundler/audit/database.rb +248 -0
- data/lib/bundler/audit/scanner.rb +213 -0
- data/lib/bundler/audit/task.rb +31 -0
- data/lib/bundler/audit/version.rb +23 -0
- data/spec/advisory_spec.rb +282 -0
- data/spec/audit_spec.rb +8 -0
- data/spec/bundle/insecure_sources/Gemfile +4 -0
- data/spec/bundle/secure/Gemfile +3 -0
- data/spec/bundle/unpatched_gems/Gemfile +3 -0
- data/spec/cli_spec.rb +99 -0
- data/spec/database_spec.rb +138 -0
- data/spec/fixtures/not_a_hash.yml +2 -0
- data/spec/integration_spec.rb +103 -0
- data/spec/scanner_spec.rb +75 -0
- data/spec/spec_helper.rb +62 -0
- metadata +115 -0
data/spec/audit_spec.rb
ADDED
data/spec/cli_spec.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'bundler/audit/cli'
|
3
|
+
|
4
|
+
describe Bundler::Audit::CLI do
|
5
|
+
describe "#update" do
|
6
|
+
context "not --quiet (the default)" do
|
7
|
+
context "when update succeeds" do
|
8
|
+
|
9
|
+
before { expect(Bundler::Audit::Database).to receive(:update!).and_return(true) }
|
10
|
+
|
11
|
+
it "prints updated message" do
|
12
|
+
expect { subject.update }.to output(/Updated ruby-advisory-db/).to_stdout
|
13
|
+
end
|
14
|
+
|
15
|
+
it "prints total advisory count" do
|
16
|
+
database = double
|
17
|
+
expect(database).to receive(:size).and_return(1234)
|
18
|
+
expect(Bundler::Audit::Database).to receive(:new).and_return(database)
|
19
|
+
|
20
|
+
expect { subject.update }.to output(/ruby-advisory-db: 1234 advisories/).to_stdout
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "when update fails" do
|
25
|
+
|
26
|
+
before { expect(Bundler::Audit::Database).to receive(:update!).and_return(false) }
|
27
|
+
|
28
|
+
it "prints failure message" do
|
29
|
+
expect do
|
30
|
+
begin
|
31
|
+
subject.update
|
32
|
+
rescue SystemExit
|
33
|
+
end
|
34
|
+
end.to output(/Failed updating ruby-advisory-db!/).to_stdout
|
35
|
+
end
|
36
|
+
|
37
|
+
it "exits with error status code" do
|
38
|
+
expect {
|
39
|
+
# Capture output of `update` only to keep spec output clean.
|
40
|
+
# The test regarding specific output is above.
|
41
|
+
expect { subject.update }.to output.to_stdout
|
42
|
+
}.to raise_error(SystemExit) do |error|
|
43
|
+
expect(error.success?).to eq(false)
|
44
|
+
expect(error.status).to eq(1)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "--quiet" do
|
52
|
+
before do
|
53
|
+
allow(subject).to receive(:options).and_return(double("Options", quiet?: true))
|
54
|
+
end
|
55
|
+
|
56
|
+
context "when update succeeds" do
|
57
|
+
|
58
|
+
before do
|
59
|
+
expect(Bundler::Audit::Database).to(
|
60
|
+
receive(:update!).with(quiet: true).and_return(true)
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "does not print any output" do
|
65
|
+
expect { subject.update }.to_not output.to_stdout
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "when update fails" do
|
70
|
+
|
71
|
+
before do
|
72
|
+
expect(Bundler::Audit::Database).to(
|
73
|
+
receive(:update!).with(quiet: true).and_return(false)
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "prints failure message" do
|
78
|
+
expect do
|
79
|
+
begin
|
80
|
+
subject.update
|
81
|
+
rescue SystemExit
|
82
|
+
end
|
83
|
+
end.to output(/Failed updating ruby-advisory-db!/).to_stdout
|
84
|
+
end
|
85
|
+
|
86
|
+
it "exits with error status code" do
|
87
|
+
expect {
|
88
|
+
# Capture output of `update` only to keep spec output clean.
|
89
|
+
# The test regarding specific output is above.
|
90
|
+
expect { subject.update }.to output.to_stdout
|
91
|
+
}.to raise_error(SystemExit) do |error|
|
92
|
+
expect(error.success?).to eq(false)
|
93
|
+
expect(error.status).to eq(1)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'bundler/audit/database'
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
describe Bundler::Audit::Database do
|
6
|
+
let(:vendored_advisories) do
|
7
|
+
Dir[File.join(Bundler::Audit::Database::VENDORED_PATH, 'gems/*/*.yml')].sort
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "path" do
|
11
|
+
subject { described_class.path }
|
12
|
+
|
13
|
+
it "it should be a directory" do
|
14
|
+
expect(File.directory?(subject)).to be_truthy
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should prefer the user repo, iff it's as up to date, or more up to date than the vendored one" do
|
18
|
+
Bundler::Audit::Database.update!(quiet: false)
|
19
|
+
|
20
|
+
Dir.chdir(Bundler::Audit::Database::USER_PATH) do
|
21
|
+
puts "Timestamp:"
|
22
|
+
system 'git log --pretty="%cd" -1'
|
23
|
+
end
|
24
|
+
|
25
|
+
# As up to date...
|
26
|
+
expect(Bundler::Audit::Database.path).to eq mocked_user_path
|
27
|
+
|
28
|
+
# More up to date...
|
29
|
+
fake_a_commit_in_the_user_repo
|
30
|
+
expect(Bundler::Audit::Database.path).to eq mocked_user_path
|
31
|
+
|
32
|
+
roll_user_repo_back(20)
|
33
|
+
expect(Bundler::Audit::Database.path).to eq Bundler::Audit::Database::VENDORED_PATH
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "update!" do
|
38
|
+
it "should create the USER_PATH path as needed" do
|
39
|
+
Bundler::Audit::Database.update!(quiet: false)
|
40
|
+
expect(File.directory?(mocked_user_path)).to be true
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should create the repo, then update it given multple successive calls." do
|
44
|
+
expect_update_to_clone_repo!
|
45
|
+
Bundler::Audit::Database.update!(quiet: false)
|
46
|
+
expect(File.directory?(mocked_user_path)).to be true
|
47
|
+
|
48
|
+
expect_update_to_update_repo!
|
49
|
+
Bundler::Audit::Database.update!(quiet: false)
|
50
|
+
expect(File.directory?(mocked_user_path)).to be true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#initialize" do
|
55
|
+
context "when given no arguments" do
|
56
|
+
subject { described_class.new }
|
57
|
+
|
58
|
+
it "should default path to path" do
|
59
|
+
expect(subject.path).to eq(described_class.path)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "when given a directory" do
|
64
|
+
let(:path ) { Dir.tmpdir }
|
65
|
+
|
66
|
+
subject { described_class.new(path) }
|
67
|
+
|
68
|
+
it "should set #path" do
|
69
|
+
expect(subject.path).to eq(path)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when given an invalid directory" do
|
74
|
+
it "should raise an ArgumentError" do
|
75
|
+
expect {
|
76
|
+
described_class.new('/foo/bar/baz')
|
77
|
+
}.to raise_error(ArgumentError)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#check_gem" do
|
83
|
+
let(:gem) do
|
84
|
+
Gem::Specification.new do |s|
|
85
|
+
s.name = 'actionpack'
|
86
|
+
s.version = '3.1.9'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "when given a block" do
|
91
|
+
it "should yield every advisory effecting the gem" do
|
92
|
+
advisories = []
|
93
|
+
|
94
|
+
subject.check_gem(gem) do |advisory|
|
95
|
+
advisories << advisory
|
96
|
+
end
|
97
|
+
|
98
|
+
expect(advisories).not_to be_empty
|
99
|
+
expect(advisories.all? { |advisory|
|
100
|
+
advisory.kind_of?(Bundler::Audit::Advisory)
|
101
|
+
}).to be_truthy
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "when given no block" do
|
106
|
+
it "should return an Enumerator" do
|
107
|
+
expect(subject.check_gem(gem)).to be_kind_of(Enumerable)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "#size" do
|
113
|
+
it { expect(subject.size).to eq vendored_advisories.count }
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "#advisories" do
|
117
|
+
it "should return a list of all advisories." do
|
118
|
+
actual_advisories = Bundler::Audit::Database.new.
|
119
|
+
advisories.
|
120
|
+
map(&:path).
|
121
|
+
sort
|
122
|
+
|
123
|
+
expect(actual_advisories).to eq vendored_advisories
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "#to_s" do
|
128
|
+
it "should return the Database path" do
|
129
|
+
expect(subject.to_s).to eq(subject.path)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "#inspect" do
|
134
|
+
it "should produce a Ruby-ish instance descriptor" do
|
135
|
+
expect(Bundler::Audit::Database.new.inspect).to eq("#<Bundler::Audit::Database:#{Bundler::Audit::Database::VENDORED_PATH}>")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
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: (High|Medium|Low|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 OSVDB-89026'))
|
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("OSVDB-89026")
|
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
|
67
|
+
end
|
68
|
+
|
69
|
+
context "when auditing a secure bundle" do
|
70
|
+
let(:bundle) { 'secure' }
|
71
|
+
let(:directory) { File.join('spec','bundle',bundle) }
|
72
|
+
|
73
|
+
subject do
|
74
|
+
Dir.chdir(directory) { sh(command) }
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should print nothing when everything is fine" do
|
78
|
+
expect(subject.strip).to eq("No vulnerabilities found")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "update" do
|
83
|
+
|
84
|
+
let(:update_command) { "#{command} update" }
|
85
|
+
let(:bundle) { 'secure' }
|
86
|
+
let(:directory) { File.join('spec','bundle',bundle) }
|
87
|
+
|
88
|
+
subject do
|
89
|
+
Dir.chdir(directory) { sh(update_command) }
|
90
|
+
end
|
91
|
+
|
92
|
+
context "when advisories update successfully" do
|
93
|
+
it "should print status" do
|
94
|
+
expect(subject).not_to include("Fail")
|
95
|
+
expect(subject).to include("Updating ruby-advisory-db ...\n")
|
96
|
+
expect(subject).to include("Updated ruby-advisory-db\n")
|
97
|
+
expect(subject.lines.to_a.last).to match(/ruby-advisory-db: [1-9]\d+ advisories/)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'bundler/audit/scanner'
|
3
|
+
|
4
|
+
describe Scanner do
|
5
|
+
describe "#scan" do
|
6
|
+
let(:bundle) { 'unpatched_gems' }
|
7
|
+
let(:directory) { File.join('spec','bundle',bundle) }
|
8
|
+
|
9
|
+
subject { described_class.new(directory) }
|
10
|
+
|
11
|
+
it "should yield results" do
|
12
|
+
results = []
|
13
|
+
|
14
|
+
subject.scan { |result| results << result }
|
15
|
+
|
16
|
+
expect(results).not_to be_empty
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when not called with a block" do
|
20
|
+
it "should return an Enumerator" do
|
21
|
+
expect(subject.scan).to be_kind_of(Enumerable)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when auditing a bundle with unpatched gems" do
|
27
|
+
let(:bundle) { 'unpatched_gems' }
|
28
|
+
let(:directory) { File.join('spec','bundle',bundle) }
|
29
|
+
let(:scanner) { described_class.new(directory) }
|
30
|
+
|
31
|
+
subject { scanner.scan.to_a }
|
32
|
+
|
33
|
+
it "should match unpatched gems to their advisories" do
|
34
|
+
ids = subject.map { |result| result.advisory.id }
|
35
|
+
expect(ids).to include('OSVDB-89025')
|
36
|
+
expect(subject.all? { |result|
|
37
|
+
result.advisory.vulnerable?(result.gem.version)
|
38
|
+
}).to be_truthy
|
39
|
+
end
|
40
|
+
|
41
|
+
context "when the :ignore option is given" do
|
42
|
+
subject { scanner.scan(:ignore => ['OSVDB-89025']) }
|
43
|
+
|
44
|
+
it "should ignore the specified advisories" do
|
45
|
+
ids = subject.map { |result| result.advisory.id }
|
46
|
+
expect(ids).not_to include('OSVDB-89025')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when auditing a bundle with insecure sources" do
|
52
|
+
let(:bundle) { 'insecure_sources' }
|
53
|
+
let(:directory) { File.join('spec','bundle',bundle) }
|
54
|
+
let(:scanner) { described_class.new(directory) }
|
55
|
+
|
56
|
+
subject { scanner.scan.to_a }
|
57
|
+
|
58
|
+
it "should match unpatched gems to their advisories" do
|
59
|
+
expect(subject[0].source).to eq('git://github.com/rails/jquery-rails.git')
|
60
|
+
expect(subject[1].source).to eq('http://rubygems.org/')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when auditing a secure bundle" do
|
65
|
+
let(:bundle) { 'secure' }
|
66
|
+
let(:directory) { File.join('spec','bundle',bundle) }
|
67
|
+
let(:scanner) { described_class.new(directory) }
|
68
|
+
|
69
|
+
subject { scanner.scan.to_a }
|
70
|
+
|
71
|
+
it "should print nothing when everything is fine" do
|
72
|
+
expect(subject).to be_empty
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
SimpleCov.start
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
require 'bundler/audit/version'
|
6
|
+
require 'bundler/audit/database'
|
7
|
+
|
8
|
+
module Helpers
|
9
|
+
def sh(command, options={})
|
10
|
+
Bundler.with_clean_env do
|
11
|
+
result = `#{command} 2>&1`
|
12
|
+
raise "FAILED #{command}\n#{result}" if $?.success? == !!options[:fail]
|
13
|
+
result
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def decolorize(string)
|
18
|
+
string.gsub(/\e\[\d+m/, "")
|
19
|
+
end
|
20
|
+
|
21
|
+
def mocked_user_path
|
22
|
+
File.expand_path('../../tmp/ruby-advisory-db', __FILE__)
|
23
|
+
end
|
24
|
+
|
25
|
+
def expect_update_to_clone_repo!
|
26
|
+
expect(Bundler::Audit::Database).
|
27
|
+
to receive(:system).
|
28
|
+
with('git', 'clone', Bundler::Audit::Database::VENDORED_PATH, mocked_user_path).
|
29
|
+
and_call_original
|
30
|
+
end
|
31
|
+
|
32
|
+
def expect_update_to_update_repo!
|
33
|
+
expect(Bundler::Audit::Database).
|
34
|
+
to receive(:system).
|
35
|
+
with('git', 'pull', '--no-rebase', 'origin', 'master').
|
36
|
+
and_call_original
|
37
|
+
end
|
38
|
+
|
39
|
+
def fake_a_commit_in_the_user_repo
|
40
|
+
Dir.chdir(mocked_user_path) do
|
41
|
+
system 'git', 'commit', '--allow-empty', '-m', 'Dummy commit.'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def roll_user_repo_back(num_commits)
|
46
|
+
Dir.chdir(mocked_user_path) do
|
47
|
+
system 'git', 'reset', '--hard', "HEAD~#{num_commits}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
include Bundler::Audit
|
53
|
+
|
54
|
+
RSpec.configure do |config|
|
55
|
+
include Helpers
|
56
|
+
|
57
|
+
config.before(:each) do
|
58
|
+
stub_const("Bundler::Audit::Database::URL", Bundler::Audit::Database::VENDORED_PATH)
|
59
|
+
stub_const("Bundler::Audit::Database::USER_PATH", mocked_user_path)
|
60
|
+
FileUtils.rm_rf(mocked_user_path) if File.exist?(mocked_user_path)
|
61
|
+
end
|
62
|
+
end
|