chelsea 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1633315a7f5da46a2c43414058160959b0be5202a3c7a6aa333e99f91f48a755
4
- data.tar.gz: ac7a5f1f3c77061ccf54edf458758b621038f74ca9e272261cb0dc417cb94298
3
+ metadata.gz: e8ddb15d0a891dbfa7ce2c666d01ef1cda63727631496efc44fb71202931c485
4
+ data.tar.gz: 982bdf90ff3ed1851c6eea78519f7dbab3b40c60c23e5b21949035437bc9680c
5
5
  SHA512:
6
- metadata.gz: 1b7eea08843bed093b27054b29da075edfc63730836421e8151e0dfd661d06f030bf7be242c8a08e11d5c4aae49434d3976f4b8a628962f44fa8e8b787979eea
7
- data.tar.gz: c946fd43b812750e8d3ebe7b4c13914ca54e257c6b86a63d3abb1224bec4d94af027a1e4dcc7cd65940af23091b5e82d10d99e5397c1406e6d4199f2fb2f49b8
6
+ metadata.gz: 77205e26a996f43726400fa0ab2ac96299829c75546a3b2d8b153df476d9065defbae40007e1095046fa1f917655fe57c0a919e2986ff28d66fb7b0fcbf3b196
7
+ data.tar.gz: 45499656d17b88a75584c687488af409770dffcdd331bc55292f52cad2aa572e3b9ff757ba880006928b8da386ba8900e65d72d54a3fb03121b745c777b17027
@@ -1,30 +1,38 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- chelsea (0.0.2)
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.0)
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
- slop (4.8.0)
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.6)
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.0.2
82
+ 2.1.2
data/README.md CHANGED
@@ -30,13 +30,16 @@ $ chelsea
30
30
  \____/|_| |_| \___||_||___/ \___| \__,_|
31
31
 
32
32
 
33
- Version: 0.0.1
33
+ Version: 0.0.3
34
34
 
35
35
  usage: chelsea [options] ...
36
36
 
37
37
  Options:
38
- -f, --file do the dang thing
39
- --version print the version
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:
@@ -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.main
29
+ Chelsea::CLI.new(opts).process!
@@ -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.0"
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
@@ -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
- if arguments.fetch(:file)
14
- gems(arguments[:file])
15
- elsif set?(arguments, :help)
16
- puts cli_flags
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
- def set?(arguments, flag)
21
- !arguments.fetch(flag).nil?
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
- def cli_flags()
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.string '-f', '--file', 'do the dang thing'
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 validate_arguments(arguments)
40
- if number_of_required_flags_set(arguments) < 1 && !arguments.fetch(:file)
41
- flags_error
42
- end
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 gems(file)
63
- require_relative 'gems'
64
- Chelsea::Gems.new(file, nil).execute
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 parse_arguments(command_line_options, parser)
68
- begin
69
- result = parser.parse command_line_options
70
- result.to_hash
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 version()
80
- Chelsea::VERSION
76
+ def _flags
77
+ # Seems wrong, should all be handled by bin
78
+ [:file, :help]
81
79
  end
82
-
83
- def show_logo()
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
@@ -0,0 +1,8 @@
1
+ module Chelsea
2
+ class DependencyException < StandardError
3
+ def initialize(msg="This is a custom exception", exception_type="custom")
4
+ @exception_type = exception_type
5
+ super(msg)
6
+ end
7
+ end
8
+ end
@@ -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,12 @@
1
+ class Formatter
2
+ def initialize
3
+ @pastel = Pastel.new
4
+ end
5
+ def get_results
6
+ raise 'must implement get_results method in subclass'
7
+ end
8
+
9
+ def do_print
10
+ raise 'must implement do_print method in subclass'
11
+ end
12
+ 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
@@ -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
- require 'pstore'
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 gemfile_lock_file_exists()
27
- return
19
+ if not _gemfile_lock_file_exists? or file.nil?
20
+ raise "Gemfile.lock not found, check --file path"
28
21
  end
29
-
30
- path = Pathname.new(@file)
31
- @lockfile = Bundler::LockfileParser.new(
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
- n = get_dependencies()
44
- if n == 0
45
- print_err "No dependencies retrieved. Exiting."
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
- get_dependencies_versions()
49
- get_coordinates()
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
- print_results()
37
+ @formatter.do_print(@formatter.get_results(@deps))
56
38
  end
57
39
 
58
- def gemfile_lock_file_exists()
59
- if not ::File.file? @file
60
- return false
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
- @lockfile.specs.each do |gem|
77
- @dependencies[gem.name] = [gem.name, gem.version]
78
- rescue StandardError => e
79
- spinner.stop("...failed.")
80
- print_err "Parsing dependency line #{gem} failed."
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
- @dependencies_versions[p] = v
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
- check_db_for_cached_values()
54
+ @deps.get_reverse_dependencies
121
55
 
122
- if @coordinates["coordinates"].count() > 0
123
- chunked = Hash.new()
124
- @coordinates["coordinates"].each_slice(128).to_a.each do |coords|
125
- chunked["coordinates"] = coords
126
- r = RestClient.post "https://ossindex.sonatype.org/api/v3/component-report", chunked.to_json,
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
- def print_results()
167
- puts ""
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
- def print_reverse_deps(reverse_deps, name, version)
194
- reverse_deps.each do |dep|
195
- dep.each do |gran|
196
- if gran.class == String && !gran.include?(name)
197
- # There is likely a fun and clever way to check @server-results, etc... and see if a dep is in there
198
- # Right now this looks at all Ruby deps, so it might find some in your Library, but that don't belong to your project
199
- puts "\tRequired by: " + gran
200
- else
201
- end
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
- def to_purl(name, version)
207
- purl = "pkg:gem/#{name}@#{version}"
208
-
209
- purl
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 print_err(s)
106
+ def _print_err(s)
213
107
  puts @pastel.red.bold(s)
214
108
  end
215
109
 
216
- def print_success(s)
110
+ def _print_success(s)
217
111
  puts @pastel.green.bold(s)
218
112
  end
219
113
 
220
- private
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
@@ -1,3 +1,3 @@
1
1
  module Chelsea
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
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.3
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-23 00:00:00.000000000 Z
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.0
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.0
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