chelsea 0.0.3 → 0.0.4
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 +4 -4
- data/Gemfile.lock +19 -5
- data/README.md +6 -3
- data/bin/chelsea +25 -1
- data/chelsea.gemspec +3 -1
- data/lib/chelsea/cli.rb +49 -53
- data/lib/chelsea/dependency_exception.rb +8 -0
- data/lib/chelsea/deps.rb +171 -0
- data/lib/chelsea/formatters/factory.rb +16 -0
- data/lib/chelsea/formatters/formatter.rb +12 -0
- data/lib/chelsea/formatters/json.rb +18 -0
- data/lib/chelsea/formatters/text.rb +63 -0
- data/lib/chelsea/formatters/xml.rb +60 -0
- data/lib/chelsea/gems.rb +72 -230
- data/lib/chelsea/version.rb +1 -1
- metadata +39 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e8ddb15d0a891dbfa7ce2c666d01ef1cda63727631496efc44fb71202931c485
|
4
|
+
data.tar.gz: 982bdf90ff3ed1851c6eea78519f7dbab3b40c60c23e5b21949035437bc9680c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77205e26a996f43726400fa0ab2ac96299829c75546a3b2d8b153df476d9065defbae40007e1095046fa1f917655fe57c0a919e2986ff28d66fb7b0fcbf3b196
|
7
|
+
data.tar.gz: 45499656d17b88a75584c687488af409770dffcdd331bc55292f52cad2aa572e3b9ff757ba880006928b8da386ba8900e65d72d54a3fb03121b745c777b17027
|
data/Gemfile.lock
CHANGED
@@ -1,30 +1,38 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
chelsea (0.0.
|
4
|
+
chelsea (0.0.3)
|
5
5
|
bundler (>= 1.2.0, < 3)
|
6
|
+
ox (~> 2.13.2)
|
6
7
|
pastel (~> 0.7.2)
|
7
8
|
rest-client (~> 2.0.2)
|
8
|
-
slop (~> 4.8.
|
9
|
+
slop (~> 4.8.1)
|
9
10
|
tty-font (~> 0.5.0)
|
10
11
|
tty-spinner (~> 0.9.3)
|
11
12
|
|
12
13
|
GEM
|
13
14
|
remote: https://rubygems.org/
|
14
15
|
specs:
|
16
|
+
addressable (2.7.0)
|
17
|
+
public_suffix (>= 2.0.2, < 5.0)
|
18
|
+
crack (0.4.3)
|
19
|
+
safe_yaml (~> 1.0.0)
|
15
20
|
diff-lcs (1.3)
|
16
21
|
domain_name (0.5.20190701)
|
17
22
|
unf (>= 0.0.5, < 1.0.0)
|
18
23
|
equatable (0.6.1)
|
24
|
+
hashdiff (1.0.1)
|
19
25
|
http-cookie (1.0.3)
|
20
26
|
domain_name (~> 0.5)
|
21
27
|
mime-types (3.3.1)
|
22
28
|
mime-types-data (~> 3.2015)
|
23
29
|
mime-types-data (3.2019.1009)
|
24
30
|
netrc (0.11.0)
|
31
|
+
ox (2.13.2)
|
25
32
|
pastel (0.7.3)
|
26
33
|
equatable (~> 0.6)
|
27
34
|
tty-color (~> 0.5)
|
35
|
+
public_suffix (4.0.3)
|
28
36
|
rake (10.5.0)
|
29
37
|
rest-client (2.0.2)
|
30
38
|
http-cookie (>= 1.0.2, < 2.0)
|
@@ -45,7 +53,8 @@ GEM
|
|
45
53
|
rspec-support (3.9.2)
|
46
54
|
rspec_junit_formatter (0.4.1)
|
47
55
|
rspec-core (>= 2, < 4, != 2.12.0)
|
48
|
-
|
56
|
+
safe_yaml (1.0.5)
|
57
|
+
slop (4.8.1)
|
49
58
|
tty-color (0.5.1)
|
50
59
|
tty-cursor (0.7.1)
|
51
60
|
tty-font (0.5.0)
|
@@ -53,7 +62,11 @@ GEM
|
|
53
62
|
tty-cursor (~> 0.7)
|
54
63
|
unf (0.1.4)
|
55
64
|
unf_ext
|
56
|
-
unf_ext (0.0.7.
|
65
|
+
unf_ext (0.0.7.7)
|
66
|
+
webmock (3.8.3)
|
67
|
+
addressable (>= 2.3.6)
|
68
|
+
crack (>= 0.3.2)
|
69
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
57
70
|
|
58
71
|
PLATFORMS
|
59
72
|
ruby
|
@@ -63,6 +76,7 @@ DEPENDENCIES
|
|
63
76
|
rake (~> 10.0)
|
64
77
|
rspec (~> 3.0)
|
65
78
|
rspec_junit_formatter (~> 0.4.1)
|
79
|
+
webmock (~> 3.8.3)
|
66
80
|
|
67
81
|
BUNDLED WITH
|
68
|
-
2.
|
82
|
+
2.1.2
|
data/README.md
CHANGED
@@ -30,13 +30,16 @@ $ chelsea
|
|
30
30
|
\____/|_| |_| \___||_||___/ \___| \__,_|
|
31
31
|
|
32
32
|
|
33
|
-
Version: 0.0.
|
33
|
+
Version: 0.0.3
|
34
34
|
|
35
35
|
usage: chelsea [options] ...
|
36
36
|
|
37
37
|
Options:
|
38
|
-
-
|
39
|
-
--
|
38
|
+
-h, --help show usage
|
39
|
+
-q, --quiet make chelsea only output vulnerable third party dependencies for text output (default: false)
|
40
|
+
-t, --format choose what type of format you want your report in (default: text) (options: text, json, xml)
|
41
|
+
-f, --file path to your Gemfile.lock
|
42
|
+
--version print the version
|
40
43
|
```
|
41
44
|
|
42
45
|
Most basic usage is:
|
data/bin/chelsea
CHANGED
@@ -1,5 +1,29 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
require_relative "../lib/chelsea"
|
4
|
+
require 'slop'
|
5
|
+
opts =
|
6
|
+
begin
|
7
|
+
Slop.parse do |o|
|
8
|
+
o.string '-f', '--file', 'do the dang thing'
|
9
|
+
o.bool '-q', '--quiet', 'make chelsea only output vulnerable third party dependencies for text output (default: false)', default: false
|
10
|
+
o.string '-t', '--format', 'choose what type of format you want your report in (default: text) (options: text, json, xml)', default: 'text'
|
11
|
+
o.on '--version', 'print the version' do
|
12
|
+
puts Chelsea::VERSION
|
13
|
+
exit
|
14
|
+
end
|
15
|
+
o.on '-h', '--help', 'show usage' do
|
16
|
+
puts(o)
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
end
|
20
|
+
rescue Slop::Error => e
|
21
|
+
puts(e.message + ' (try --help)')
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
if opts.arguments.count.positive?
|
25
|
+
puts("extraneous arguments: #{opts.arguments.join(' ')} (try --help)")
|
26
|
+
exit 1
|
27
|
+
end
|
4
28
|
|
5
|
-
Chelsea::CLI.new.
|
29
|
+
Chelsea::CLI.new(opts).process!
|
data/chelsea.gemspec
CHANGED
@@ -37,12 +37,14 @@ Gem::Specification.new do |spec|
|
|
37
37
|
|
38
38
|
spec.add_dependency "tty-font", "~> 0.5.0"
|
39
39
|
spec.add_dependency "tty-spinner", "~> 0.9.3"
|
40
|
-
spec.add_dependency "slop", "~> 4.8.
|
40
|
+
spec.add_dependency "slop", "~> 4.8.1"
|
41
41
|
spec.add_dependency "pastel", "~> 0.7.2"
|
42
42
|
spec.add_dependency "rest-client", "~> 2.0.2"
|
43
43
|
spec.add_dependency "bundler", ">= 1.2.0", "< 3"
|
44
|
+
spec.add_dependency "ox", "~> 2.13.2"
|
44
45
|
|
45
46
|
spec.add_development_dependency "rake", "~> 10.0"
|
46
47
|
spec.add_development_dependency "rspec", "~> 3.0"
|
47
48
|
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4.1"
|
49
|
+
spec.add_development_dependency "webmock", "~> 3.8.3"
|
48
50
|
end
|
data/lib/chelsea/cli.rb
CHANGED
@@ -1,33 +1,49 @@
|
|
1
1
|
require 'slop'
|
2
2
|
require 'pastel'
|
3
|
+
require 'tty-font'
|
4
|
+
|
3
5
|
require_relative 'version'
|
6
|
+
require_relative 'gems'
|
4
7
|
|
5
8
|
module Chelsea
|
9
|
+
##
|
10
|
+
# This class provides an interface to the oss index, gems and deps
|
6
11
|
class CLI
|
7
|
-
def main(command_line_options=ARGV)
|
8
|
-
puts show_logo()
|
9
|
-
parser = Slop::Parser.new cli_flags()
|
10
|
-
arguments = parse_arguments(command_line_options, parser)
|
11
|
-
validate_arguments arguments
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
def initialize(opts)
|
14
|
+
@opts = opts
|
15
|
+
@pastel = Pastel.new
|
16
|
+
_validate_arguments
|
17
|
+
_show_logo
|
18
|
+
end
|
19
|
+
|
20
|
+
def process!
|
21
|
+
if @opts.file?
|
22
|
+
@gems = Chelsea::Gems.new(file: @opts[:file])
|
23
|
+
@gems.execute
|
24
|
+
elsif @opts.help?
|
25
|
+
puts _cli_flags
|
26
|
+
|
17
27
|
end
|
18
28
|
end
|
19
29
|
|
20
|
-
|
21
|
-
|
30
|
+
# this is how you do static methods in ruby, because in a test we want to
|
31
|
+
# check for version without opts, and heck, we don't even want a dang object!
|
32
|
+
def self.version
|
33
|
+
Chelsea::VERSION
|
22
34
|
end
|
23
35
|
|
24
|
-
|
36
|
+
protected
|
37
|
+
|
38
|
+
def _cli_flags
|
25
39
|
opts = Slop::Options.new
|
26
40
|
opts.banner = "usage: chelsea [options] ..."
|
27
41
|
opts.separator ""
|
28
42
|
opts.separator 'Options:'
|
29
|
-
opts.bool '-h', '--help', 'show usage'
|
30
|
-
opts.
|
43
|
+
opts.bool '-h', '--help', 'show usage'
|
44
|
+
opts.bool '-q', '--quiet', 'make chelsea only output vulnerable third party dependencies for text output (default: false)', default: false
|
45
|
+
opts.string '-t', '--format', 'choose what type of format you want your report in (default: text) (options: text, json, xml)', default: 'text'
|
46
|
+
opts.string '-f', '--file', 'path to your Gemfile.lock'
|
31
47
|
opts.on '--version', 'print the version' do
|
32
48
|
puts version()
|
33
49
|
exit
|
@@ -36,56 +52,36 @@ module Chelsea
|
|
36
52
|
opts
|
37
53
|
end
|
38
54
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
def number_of_required_flags_set(arguments)
|
46
|
-
minimum_flags = flags
|
47
|
-
valid_flags = minimum_flags.collect {|a| arguments.fetch(a) }.compact
|
48
|
-
valid_flags.count
|
49
|
-
end
|
50
|
-
|
51
|
-
def flags
|
52
|
-
[:file, :help]
|
53
|
-
end
|
54
|
-
|
55
|
-
def flags_error
|
56
|
-
switches = flags.collect {|f| "--#{f}"}
|
57
|
-
puts cli_flags
|
55
|
+
def _flags_error
|
56
|
+
# should be custom exception!
|
57
|
+
switches = _flags.collect {|f| "--#{f}"}
|
58
|
+
puts _cli_flags
|
58
59
|
puts
|
59
60
|
abort "please set one of #{switches}"
|
60
61
|
end
|
61
62
|
|
62
|
-
def
|
63
|
-
|
64
|
-
|
63
|
+
def _validate_arguments
|
64
|
+
if !_flags_set? && !@opts.file?
|
65
|
+
## require at least one argument
|
66
|
+
_flags_error
|
67
|
+
end
|
65
68
|
end
|
66
69
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
rescue Slop::UnknownOption
|
73
|
-
# print help
|
74
|
-
puts cli_flags()
|
75
|
-
exit
|
76
|
-
end
|
70
|
+
def _flags_set?
|
71
|
+
# I'm still unsure what this is trying to express
|
72
|
+
valid_flags = _flags.collect {|arg| @opts[arg] }.compact
|
73
|
+
valid_flags.count > 1
|
77
74
|
end
|
78
75
|
|
79
|
-
def
|
80
|
-
|
76
|
+
def _flags
|
77
|
+
# Seems wrong, should all be handled by bin
|
78
|
+
[:file, :help]
|
81
79
|
end
|
82
|
-
|
83
|
-
def
|
84
|
-
@pastel = Pastel.new
|
85
|
-
require 'tty-font'
|
80
|
+
|
81
|
+
def _show_logo()
|
86
82
|
font = TTY::Font.new(:doom)
|
87
83
|
puts @pastel.green(font.write("Chelsea"))
|
88
|
-
puts @pastel.green("Version: " + version
|
84
|
+
puts @pastel.green("Version: " + CLI::version)
|
89
85
|
end
|
90
86
|
end
|
91
87
|
end
|
data/lib/chelsea/deps.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'bundler/lockfile_parser'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rubygems/commands/dependency_command'
|
5
|
+
require_relative 'dependency_exception'
|
6
|
+
require 'json'
|
7
|
+
require 'rest-client'
|
8
|
+
require 'pstore'
|
9
|
+
|
10
|
+
|
11
|
+
module Chelsea
|
12
|
+
class Deps
|
13
|
+
attr_reader :server_response, :reverse_dependencies, :coordinates
|
14
|
+
|
15
|
+
def initialize(path: , quiet: false)
|
16
|
+
@path, @quiet = path, quiet
|
17
|
+
|
18
|
+
begin
|
19
|
+
@lockfile = Bundler::LockfileParser.new(
|
20
|
+
File.read(@path)
|
21
|
+
)
|
22
|
+
rescue
|
23
|
+
raise "Gemfile.lock not parseable, please check file or that it's path is valid"
|
24
|
+
end
|
25
|
+
|
26
|
+
@dependencies = {}
|
27
|
+
@reverse_dependencies = {}
|
28
|
+
@dependencies_versions = {}
|
29
|
+
@coordinates = { 'coordinates' => [] }
|
30
|
+
@server_response = []
|
31
|
+
@store = PStore.new(_get_db_store_location())
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_h(reverse: false)
|
35
|
+
if reverse
|
36
|
+
@reverse_dependencies
|
37
|
+
else
|
38
|
+
@dependencies
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def nil?
|
43
|
+
@dependencies.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.to_purl(name, version)
|
47
|
+
return "pkg:gem/#{name}@#{version}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def user_agent
|
51
|
+
"chelsea/#{Chelsea::VERSION}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_dependencies
|
55
|
+
@lockfile.specs.each do |gem|\
|
56
|
+
begin
|
57
|
+
@dependencies[gem.name] = [gem.name, gem.version]
|
58
|
+
rescue StandardError => e
|
59
|
+
raise Chelsea::DependencyException e, "Parsing dependency line #{gem} failed."
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_reverse_dependencies
|
65
|
+
begin
|
66
|
+
reverse = Gem::Commands::DependencyCommand.new
|
67
|
+
reverse.options[:reverse_dependencies] = true
|
68
|
+
@reverse_dependencies = reverse.reverse_dependencies(@lockfile.specs).to_h
|
69
|
+
rescue => e
|
70
|
+
raise Chelsea::DependencyException e, "ReverseDependencyException"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_dependencies_versions_as_coordinates
|
75
|
+
@dependencies.each do |p, r|
|
76
|
+
o = r[0]
|
77
|
+
v = r[1].to_s
|
78
|
+
if v.split('.').length == 1 then
|
79
|
+
v = v + ".0.0"
|
80
|
+
elsif v.split('.').length == 2 then
|
81
|
+
v = v + ".0"
|
82
|
+
end
|
83
|
+
@dependencies_versions[p] = v
|
84
|
+
end
|
85
|
+
|
86
|
+
@dependencies_versions.each do |p, v|
|
87
|
+
@coordinates["coordinates"] << self.class.to_purl(p,v);
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Makes multiple REST calls
|
92
|
+
def get_vulns()
|
93
|
+
_check_db_for_cached_values()
|
94
|
+
|
95
|
+
if @coordinates["coordinates"].count() > 0
|
96
|
+
chunked = Hash.new()
|
97
|
+
@coordinates["coordinates"].each_slice(128).to_a.each do |coords|
|
98
|
+
# Won't this return the first successful slice?
|
99
|
+
chunked["coordinates"] = coords
|
100
|
+
r = RestClient.post "https://ossindex.sonatype.org/api/v3/component-report", chunked.to_json,
|
101
|
+
{ content_type: :json, accept: :json, 'User-Agent': user_agent }
|
102
|
+
if r.code == 200
|
103
|
+
@server_response = @server_response.concat(JSON.parse(r.body))
|
104
|
+
_save_values_to_db(JSON.parse(r.body))
|
105
|
+
@server_response.count()
|
106
|
+
else
|
107
|
+
0
|
108
|
+
end
|
109
|
+
end
|
110
|
+
else
|
111
|
+
#IDGI
|
112
|
+
@server_response.count()
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
# This method will take an array of values, and save them to a pstore database
|
118
|
+
# and as well set a TTL of Time.now to be checked later
|
119
|
+
def _save_values_to_db(values)
|
120
|
+
values.each do |val|
|
121
|
+
if _get_cached_value_from_db(val["coordinates"]).nil?
|
122
|
+
new_val = val.dup
|
123
|
+
new_val["ttl"] = Time.now
|
124
|
+
@store.transaction do
|
125
|
+
@store[new_val["coordinates"]] = new_val
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def _get_db_store_location()
|
132
|
+
initial_path = File.join("#{Dir.home}", ".ossindex")
|
133
|
+
Dir.mkdir(initial_path) unless File.exists? initial_path
|
134
|
+
path = File.join(initial_path, "chelsea.pstore")
|
135
|
+
end
|
136
|
+
|
137
|
+
# Checks pstore to see if a coordinate exists, and if it does also
|
138
|
+
# checks to see if it's ttl has expired. Returns nil unless a record
|
139
|
+
# is valid in the cache (ttl has not expired) and found
|
140
|
+
def _get_cached_value_from_db(coordinate)
|
141
|
+
record = @store.transaction { @store[coordinate] }
|
142
|
+
if !record.nil?
|
143
|
+
diff = (Time.now - record['ttl']) / 3600
|
144
|
+
if diff > 12
|
145
|
+
return nil
|
146
|
+
else
|
147
|
+
return record
|
148
|
+
end
|
149
|
+
else
|
150
|
+
return nil
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Goes through the list of @coordinates and checks pstore for them, if it finds a valid coord
|
155
|
+
# it will add it to the server response. If it does not, it will append the coord to a new hash
|
156
|
+
# and eventually set @coordinates to the new hash, so we query OSS Index on only coords not in cache
|
157
|
+
def _check_db_for_cached_values()
|
158
|
+
new_coords = Hash.new
|
159
|
+
new_coords["coordinates"] = Array.new
|
160
|
+
@coordinates["coordinates"].each do |coord|
|
161
|
+
record = _get_cached_value_from_db(coord)
|
162
|
+
if !record.nil?
|
163
|
+
@server_response << record
|
164
|
+
else
|
165
|
+
new_coords["coordinates"].push(coord)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
@coordinates = new_coords
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'json'
|
2
|
+
require_relative 'xml'
|
3
|
+
require_relative 'text'
|
4
|
+
|
5
|
+
class FormatterFactory
|
6
|
+
def get_formatter(format: 'text', options: {})
|
7
|
+
case format
|
8
|
+
when 'text'
|
9
|
+
Chelsea::TextFormatter.new(options)
|
10
|
+
when 'json'
|
11
|
+
Chelsea::JsonFormatter.new(options)
|
12
|
+
when 'xml'
|
13
|
+
Chelsea::XMLFormatter.new(options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'formatter'
|
3
|
+
|
4
|
+
module Chelsea
|
5
|
+
class JsonFormatter < Formatter
|
6
|
+
def initialize(options)
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_results(server_response, reverse_deps)
|
11
|
+
server_response.to_json
|
12
|
+
end
|
13
|
+
|
14
|
+
def do_print(result)
|
15
|
+
puts result
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'pastel'
|
2
|
+
require_relative 'formatter'
|
3
|
+
|
4
|
+
module Chelsea
|
5
|
+
class TextFormatter < Formatter
|
6
|
+
def initialize(quiet: false)
|
7
|
+
@quiet = quiet
|
8
|
+
@pastel = Pastel.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_results(dependencies)
|
12
|
+
response = String.new
|
13
|
+
if !@quiet
|
14
|
+
response += "\n"\
|
15
|
+
"Audit Results\n"\
|
16
|
+
"=============\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
i = 0
|
20
|
+
count = dependencies.server_response.count()
|
21
|
+
dependencies.server_response.each do |r|
|
22
|
+
i += 1
|
23
|
+
package = r["coordinates"]
|
24
|
+
vulnerable = r["vulnerabilities"].length() > 0
|
25
|
+
coord = r["coordinates"].sub("pkg:gem/", "")
|
26
|
+
name = coord.split('@')[0]
|
27
|
+
version = coord.split('@')[1]
|
28
|
+
reverse_deps = dependencies.reverse_dependencies["#{name}-#{version}"]
|
29
|
+
if vulnerable
|
30
|
+
response += @pastel.red("[#{i}/#{count}] - #{package} ") + @pastel.red.bold("Vulnerable.\n")
|
31
|
+
response += _get_reverse_deps(reverse_deps, name)
|
32
|
+
r["vulnerabilities"].each do |k, v|
|
33
|
+
response += @pastel.red.bold(" #{k}:#{v}\n")
|
34
|
+
end
|
35
|
+
else
|
36
|
+
if !@quiet
|
37
|
+
response += @pastel.white("[#{i}/#{count}] - #{package} ") + @pastel.green.bold("No vulnerabilities found!\n")
|
38
|
+
response += _get_reverse_deps(reverse_deps, name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
response
|
44
|
+
end
|
45
|
+
|
46
|
+
def do_print(results)
|
47
|
+
puts results
|
48
|
+
end
|
49
|
+
|
50
|
+
# Right now this looks at all Ruby deps, so it might find some in your Library, but that don't belong to your project
|
51
|
+
def _get_reverse_deps(coord, name)
|
52
|
+
response = String.new
|
53
|
+
coord.each do |dep|
|
54
|
+
dep.each do |gran|
|
55
|
+
if gran.class == String && !gran.include?(name)
|
56
|
+
response += "\tRequired by: #{gran}\n"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
response
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'ox'
|
2
|
+
require_relative 'formatter'
|
3
|
+
module Chelsea
|
4
|
+
class XMLFormatter < Formatter
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_results(server_response, reverse_deps)
|
10
|
+
doc = Ox::Document.new
|
11
|
+
instruct = Ox::Instruct.new(:xml)
|
12
|
+
instruct[:version] = '1.0'
|
13
|
+
instruct[:encoding] = 'UTF-8'
|
14
|
+
instruct[:standalone] = 'yes'
|
15
|
+
doc << instruct
|
16
|
+
|
17
|
+
testsuite = Ox::Element.new('testsuite')
|
18
|
+
testsuite[:name] = "purl"
|
19
|
+
testsuite[:tests] = server_response.count()
|
20
|
+
doc << testsuite
|
21
|
+
|
22
|
+
server_response.each do |coord|
|
23
|
+
testcase = Ox::Element.new('testcase')
|
24
|
+
testcase[:classname] = coord["coordinates"]
|
25
|
+
testcase[:name] = coord["coordinates"]
|
26
|
+
|
27
|
+
if coord["vulnerabilities"].length() > 0
|
28
|
+
failure = Ox::Element.new('failure')
|
29
|
+
failure[:type] = "Vulnerable Dependency"
|
30
|
+
failure << get_vulnerability_block(coord["vulnerabilities"])
|
31
|
+
testcase << failure
|
32
|
+
end
|
33
|
+
|
34
|
+
testsuite << testcase
|
35
|
+
end
|
36
|
+
|
37
|
+
doc
|
38
|
+
end
|
39
|
+
|
40
|
+
def do_print(results)
|
41
|
+
puts Ox.dump(results)
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_vulnerability_block(vulnerabilities)
|
45
|
+
vulnBlock = String.new
|
46
|
+
vulnerabilities.each do |vuln|
|
47
|
+
vulnBlock += "Vulnerability Title: #{vuln["title"]}\n"\
|
48
|
+
"ID: #{vuln["id"]}\n"\
|
49
|
+
"Description: #{vuln["description"]}\n"\
|
50
|
+
"CVSS Score: #{vuln["cvssScore"]}\n"\
|
51
|
+
"CVSS Vector: #{vuln["cvssVector"]}\n"\
|
52
|
+
"CVE: #{vuln["cve"]}\n"\
|
53
|
+
"Reference: #{vuln["reference"]}"\
|
54
|
+
"\n"
|
55
|
+
end
|
56
|
+
|
57
|
+
vulnBlock
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/chelsea/gems.rb
CHANGED
@@ -4,273 +4,115 @@ require 'pastel'
|
|
4
4
|
require 'tty-spinner'
|
5
5
|
require 'bundler'
|
6
6
|
require 'bundler/lockfile_parser'
|
7
|
-
require_relative 'version'
|
8
7
|
require 'rubygems'
|
9
8
|
require 'rubygems/commands/dependency_command'
|
10
|
-
|
9
|
+
require_relative 'version'
|
10
|
+
require_relative 'formatters/factory'
|
11
|
+
require_relative 'deps'
|
12
|
+
|
11
13
|
|
12
14
|
module Chelsea
|
13
15
|
class Gems
|
14
|
-
def initialize(file, options)
|
15
|
-
@file = file
|
16
|
-
@options = options
|
17
|
-
@pastel = Pastel.new
|
18
|
-
@dependencies = Hash.new()
|
19
|
-
@dependencies_versions = Hash.new()
|
20
|
-
@coordinates = Hash.new()
|
21
|
-
@coordinates["coordinates"] = Array.new()
|
22
|
-
@server_response = Array.new()
|
23
|
-
@reverse_deps = Hash.new()
|
24
|
-
@store = PStore.new(get_db_store_location())
|
16
|
+
def initialize(file:, quiet: false, options: {})
|
17
|
+
@file, @quiet, @options = file, quiet, options
|
25
18
|
|
26
|
-
if not
|
27
|
-
|
19
|
+
if not _gemfile_lock_file_exists? or file.nil?
|
20
|
+
raise "Gemfile.lock not found, check --file path"
|
28
21
|
end
|
29
|
-
|
30
|
-
|
31
|
-
@
|
32
|
-
File.read(path)
|
33
|
-
)
|
34
|
-
end
|
35
|
-
|
36
|
-
def get_db_store_location()
|
37
|
-
initial_path = File.join("#{Dir.home}", ".ossindex")
|
38
|
-
Dir.mkdir(initial_path) unless File.exists? initial_path
|
39
|
-
path = File.join(initial_path, "chelsea.pstore")
|
22
|
+
@pastel = Pastel.new
|
23
|
+
@formatter = FormatterFactory.new.get_formatter(@options)
|
24
|
+
@deps = Chelsea::Deps.new({path: Pathname.new(@file)})
|
40
25
|
end
|
41
26
|
|
42
|
-
def execute(input: $stdin, output: $stdout)
|
43
|
-
|
44
|
-
if
|
45
|
-
|
27
|
+
def execute(input: $stdin, output: $stdout)
|
28
|
+
audit
|
29
|
+
if @deps.nil?
|
30
|
+
_print_err "No dependencies retrieved. Exiting."
|
46
31
|
return
|
47
32
|
end
|
48
|
-
|
49
|
-
|
50
|
-
n = get_vulns()
|
51
|
-
if n == 0
|
52
|
-
print_err "No vulnerability data retrieved from server. Exiting."
|
33
|
+
if !@deps.server_response.count
|
34
|
+
_print_err "No vulnerability data retrieved from server. Exiting."
|
53
35
|
return
|
54
36
|
end
|
55
|
-
|
37
|
+
@formatter.do_print(@formatter.get_results(@deps))
|
56
38
|
end
|
57
39
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
61
|
-
else
|
62
|
-
path = Pathname.new(@file)
|
63
|
-
return true
|
40
|
+
def audit
|
41
|
+
unless @quiet
|
42
|
+
spinner = _spin_msg "Parsing dependencies"
|
64
43
|
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def get_dependencies()
|
68
|
-
format = "[#{@pastel.green(':spinner')}] " + @pastel.white("Parsing dependencies")
|
69
|
-
spinner = TTY::Spinner.new(format, success_mark: @pastel.green('+'), hide_cursor: true)
|
70
|
-
spinner.auto_spin()
|
71
|
-
|
72
|
-
reverse = Gem::Commands::DependencyCommand.new
|
73
|
-
reverse.options[:reverse_dependencies] = true
|
74
|
-
@reverse_deps = reverse.reverse_dependencies(@lockfile.specs)
|
75
44
|
|
76
|
-
|
77
|
-
@
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
end
|
82
|
-
|
83
|
-
c = @dependencies.count()
|
84
|
-
spinner.success("...done. Parsed #{c} dependencies.")
|
85
|
-
c
|
86
|
-
end
|
87
|
-
|
88
|
-
def get_dependencies_versions()
|
89
|
-
format = "[#{@pastel.green(':spinner')}] " + @pastel.white("Parsing versions")
|
90
|
-
spinner = TTY::Spinner.new(format, success_mark: @pastel.green('+'), hide_cursor: true)
|
91
|
-
spinner.auto_spin()
|
92
|
-
@dependencies.each do |p, r|
|
93
|
-
o = r[0]
|
94
|
-
v = r[1].to_s
|
95
|
-
if v.split('.').length == 1 then
|
96
|
-
v = v + ".0.0"
|
97
|
-
elsif v.split('.').length == 2 then
|
98
|
-
v = v + ".0"
|
45
|
+
begin
|
46
|
+
@deps.get_dependencies
|
47
|
+
rescue StandardError => e
|
48
|
+
unless @quiet
|
49
|
+
spinner.stop
|
99
50
|
end
|
100
|
-
|
51
|
+
_print_err "Parsing dependency line #{gem} failed."
|
101
52
|
end
|
102
|
-
c = @dependencies_versions.count()
|
103
|
-
spinner.success("...done.")
|
104
|
-
c
|
105
|
-
end
|
106
|
-
|
107
|
-
def get_coordinates()
|
108
|
-
@dependencies_versions.each do |p, v|
|
109
|
-
@coordinates["coordinates"] << "pkg:gem/#{p}@#{v}";
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def get_vulns()
|
114
|
-
require 'json'
|
115
|
-
require 'rest-client'
|
116
|
-
format = "[#{@pastel.green(':spinner')}] " + @pastel.white("Making request to OSS Index server")
|
117
|
-
spinner = TTY::Spinner.new(format, success_mark: @pastel.green('+'), hide_cursor: true)
|
118
|
-
spinner.auto_spin()
|
119
53
|
|
120
|
-
|
54
|
+
@deps.get_reverse_dependencies
|
121
55
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
{content_type: :json, accept: :json, 'User-Agent': get_user_agent()}
|
128
|
-
|
129
|
-
if r.code == 200
|
130
|
-
@server_response = @server_response.concat(JSON.parse(r.body))
|
131
|
-
save_values_to_db(JSON.parse(r.body))
|
132
|
-
spinner.success("...done.")
|
133
|
-
@server_response.count()
|
134
|
-
else
|
135
|
-
spinner.stop("...request failed.")
|
136
|
-
print_err "Error getting data from OSS Index server. Server returned non-success code #{r.code}."
|
137
|
-
0
|
138
|
-
end
|
139
|
-
end
|
140
|
-
else
|
56
|
+
unless @quiet
|
57
|
+
spinner = _spin_msg "Parsing Versions"
|
58
|
+
end
|
59
|
+
@deps.get_dependencies_versions_as_coordinates
|
60
|
+
unless @quiet
|
141
61
|
spinner.success("...done.")
|
142
|
-
@server_response.count()
|
143
62
|
end
|
144
|
-
rescue SocketError => e
|
145
|
-
spinner.stop("...request failed.")
|
146
|
-
print_err "Socket error getting data from OSS Index server."
|
147
|
-
0
|
148
|
-
rescue RestClient::RequestFailed => e
|
149
|
-
spinner.stop("Request failed.")
|
150
|
-
print_err "Error getting data from OSS Index server:#{e.response}."
|
151
|
-
0
|
152
|
-
rescue RestClient::ResourceNotfound => e
|
153
|
-
spinner.stop("...request failed.")
|
154
|
-
print_err "Error getting data from OSS Index server. Resource not found."
|
155
|
-
0
|
156
|
-
rescue Errno::ECONNREFUSED => e
|
157
|
-
spinner.stop("...request failed.")
|
158
|
-
print_err "Error getting data from OSS Index server. Connection refused."
|
159
|
-
0
|
160
|
-
rescue StandardError => e
|
161
|
-
spinner.stop("...request failed.")
|
162
|
-
print_err "UNKNOWN Error getting data from OSS Index server."
|
163
|
-
0
|
164
|
-
end
|
165
63
|
|
166
|
-
|
167
|
-
|
168
|
-
puts "Audit Results"
|
169
|
-
puts "============="
|
170
|
-
i = 0
|
171
|
-
count = @server_response.count()
|
172
|
-
@server_response.each do |r|
|
173
|
-
i += 1
|
174
|
-
package = r["coordinates"]
|
175
|
-
vulnerable = r["vulnerabilities"].length() > 0
|
176
|
-
coord = r["coordinates"].sub("pkg:gem/", "")
|
177
|
-
name = coord.split('@')[0]
|
178
|
-
version = coord.split('@')[1]
|
179
|
-
reverse_dep_coord = "#{name}-#{version}"
|
180
|
-
if vulnerable
|
181
|
-
puts @pastel.red("[#{i}/#{count}] - #{package} ") + @pastel.red.bold("Vulnerable.")
|
182
|
-
print_reverse_deps(@reverse_deps[reverse_dep_coord], name, version)
|
183
|
-
r["vulnerabilities"].each do |k, v|
|
184
|
-
puts @pastel.red.bold(" #{k}:#{v}")
|
185
|
-
end
|
186
|
-
else
|
187
|
-
puts(@pastel.white("[#{i}/#{count}] - #{package} ") + @pastel.green.bold("No vulnerabilities found!"))
|
188
|
-
print_reverse_deps(@reverse_deps[reverse_dep_coord], name, version)
|
189
|
-
end
|
64
|
+
unless @quiet
|
65
|
+
spinner = _spin_msg "Making request to OSS Index server"
|
190
66
|
end
|
191
|
-
end
|
192
67
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
68
|
+
begin
|
69
|
+
@deps.get_vulns
|
70
|
+
rescue SocketError => e
|
71
|
+
unless @quiet
|
72
|
+
spinner.stop("...request failed.")
|
73
|
+
end
|
74
|
+
_print_err "Socket error getting data from OSS Index server."
|
75
|
+
rescue RestClient::RequestFailed => e
|
76
|
+
unless @quiet
|
77
|
+
spinner.stop("...request failed.")
|
78
|
+
end
|
79
|
+
_print_err "Error getting data from OSS Index server:#{e.response}."
|
80
|
+
rescue RestClient::ResourceNotfound => e
|
81
|
+
unless @quiet
|
82
|
+
spinner.stop("...request failed.")
|
202
83
|
end
|
84
|
+
_print_err "Error getting data from OSS Index server. Resource not found."
|
85
|
+
rescue Errno::ECONNREFUSED => e
|
86
|
+
unless @quiet
|
87
|
+
spinner.stop("...request failed.")
|
88
|
+
end
|
89
|
+
_print_err "Error getting data from OSS Index server. Connection refused."
|
90
|
+
rescue StandardError => e
|
91
|
+
unless @quiet
|
92
|
+
spinner.stop("...request failed.")
|
93
|
+
end
|
94
|
+
_print_err "UNKNOWN Error getting data from OSS Index server."
|
203
95
|
end
|
204
96
|
end
|
205
97
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
98
|
+
protected
|
99
|
+
def _spin_msg(msg)
|
100
|
+
format = "[#{@pastel.green(':spinner')}] " + @pastel.white(msg)
|
101
|
+
spinner = TTY::Spinner.new(format, success_mark: @pastel.green('+'), hide_cursor: true)
|
102
|
+
spinner.auto_spin()
|
103
|
+
spinner
|
210
104
|
end
|
211
105
|
|
212
|
-
def
|
106
|
+
def _print_err(s)
|
213
107
|
puts @pastel.red.bold(s)
|
214
108
|
end
|
215
109
|
|
216
|
-
def
|
110
|
+
def _print_success(s)
|
217
111
|
puts @pastel.green.bold(s)
|
218
112
|
end
|
219
113
|
|
220
|
-
|
221
|
-
|
222
|
-
def get_user_agent()
|
223
|
-
user_agent = "chelsea/#{Chelsea::VERSION}"
|
224
|
-
|
225
|
-
user_agent
|
226
|
-
end
|
227
|
-
|
228
|
-
# This method will take an array of values, and save them to a pstore database
|
229
|
-
# and as well set a TTL of Time.now to be checked later
|
230
|
-
def save_values_to_db(values)
|
231
|
-
values.each do |val|
|
232
|
-
if get_cached_value_from_db(val["coordinates"]).nil?
|
233
|
-
new_val = val.dup
|
234
|
-
new_val["ttl"] = Time.now
|
235
|
-
@store.transaction do
|
236
|
-
@store[new_val["coordinates"]] = new_val
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
# Checks pstore to see if a coordinate exists, and if it does also
|
243
|
-
# checks to see if it's ttl has expired. Returns nil unless a record
|
244
|
-
# is valid in the cache (ttl has not expired) and found
|
245
|
-
def get_cached_value_from_db(coordinate)
|
246
|
-
record = @store.transaction { @store[coordinate] }
|
247
|
-
if !record.nil?
|
248
|
-
diff = (Time.now - record['ttl']) / 3600
|
249
|
-
if diff > 12
|
250
|
-
return nil
|
251
|
-
else
|
252
|
-
return record
|
253
|
-
end
|
254
|
-
else
|
255
|
-
return nil
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
# Goes through the list of @coordinates and checks pstore for them, if it finds a valid coord
|
260
|
-
# it will add it to the server response. If it does not, it will append the coord to a new hash
|
261
|
-
# and eventually set @coordinates to the new hash, so we query OSS Index on only coords not in cache
|
262
|
-
def check_db_for_cached_values()
|
263
|
-
new_coords = Hash.new
|
264
|
-
new_coords["coordinates"] = Array.new
|
265
|
-
@coordinates["coordinates"].each do |coord|
|
266
|
-
record = get_cached_value_from_db(coord)
|
267
|
-
if !record.nil?
|
268
|
-
@server_response << record
|
269
|
-
else
|
270
|
-
new_coords["coordinates"].push(coord)
|
271
|
-
end
|
272
|
-
end
|
273
|
-
@coordinates = new_coords
|
114
|
+
def _gemfile_lock_file_exists?
|
115
|
+
::File.file? @file
|
274
116
|
end
|
275
117
|
end
|
276
|
-
end
|
118
|
+
end
|
data/lib/chelsea/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chelsea
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Allister Beharry
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-03
|
11
|
+
date: 2020-04-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: tty-font
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 4.8.
|
47
|
+
version: 4.8.1
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 4.8.
|
54
|
+
version: 4.8.1
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: pastel
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,6 +100,20 @@ dependencies:
|
|
100
100
|
- - "<"
|
101
101
|
- !ruby/object:Gem::Version
|
102
102
|
version: '3'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: ox
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 2.13.2
|
110
|
+
type: :runtime
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 2.13.2
|
103
117
|
- !ruby/object:Gem::Dependency
|
104
118
|
name: rake
|
105
119
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,6 +156,20 @@ dependencies:
|
|
142
156
|
- - "~>"
|
143
157
|
- !ruby/object:Gem::Version
|
144
158
|
version: 0.4.1
|
159
|
+
- !ruby/object:Gem::Dependency
|
160
|
+
name: webmock
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - "~>"
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: 3.8.3
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - "~>"
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: 3.8.3
|
145
173
|
description:
|
146
174
|
email:
|
147
175
|
- allister.beharry@gmail.com
|
@@ -171,6 +199,13 @@ files:
|
|
171
199
|
- chelsea.gemspec
|
172
200
|
- lib/chelsea.rb
|
173
201
|
- lib/chelsea/cli.rb
|
202
|
+
- lib/chelsea/dependency_exception.rb
|
203
|
+
- lib/chelsea/deps.rb
|
204
|
+
- lib/chelsea/formatters/factory.rb
|
205
|
+
- lib/chelsea/formatters/formatter.rb
|
206
|
+
- lib/chelsea/formatters/json.rb
|
207
|
+
- lib/chelsea/formatters/text.rb
|
208
|
+
- lib/chelsea/formatters/xml.rb
|
174
209
|
- lib/chelsea/gems.rb
|
175
210
|
- lib/chelsea/version.rb
|
176
211
|
homepage: https://github.com/sonatype-nexus-community/chelsea
|