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 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