chelsea 0.0.7 → 0.0.8

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: 52f656dfb244ce9739b9dff4ae38959a211bd38d5849a3c97bea14b43f2f5a3a
4
- data.tar.gz: 06443dcea17e77700763020b47d899179c062dcfb8003c944628b1bf8bb8491b
3
+ metadata.gz: eee37ec30bb7ddc5a3817c2d966bce9b7cd3ab35cd711c5e2ab79a82bee3f8bd
4
+ data.tar.gz: 31b9f377cf841c8c4e07580e6bc3a5a957baeac1ef6ddf0e019910c27c3213ee
5
5
  SHA512:
6
- metadata.gz: 90bba2fec2be99b5168ba80366fffdff8774f3120bdfa50aa87720c6348b16628a1784ef1cb40cc20bfd31a7c1e50aa20eca9d5fb426e100421d568cc33eeae4
7
- data.tar.gz: 4dfbbf3db3529e7ef257c71c4f30f9ecfb00adae8c7cd59dea98fe27380dc33b0724b5d34d078d6b740892b51eebc640bad1500045487fb77963baa66e6dd256
6
+ metadata.gz: aa2e1cde68d722b3d4adbaff36e8b25fe0175b208b20ca18a9a592259dd6445e3481401f2de7c5708c74c2bbf80b5c945f4f9f046c7f6b688e424404428cb246
7
+ data.tar.gz: '019a53c49865e117967e9cbed7711e5b0b2d339bf5b3457e99440b118f7775a07505f2311fae4bb3ce9fa8c00e3c4187cc01cf1dd1a5718a3c74acda9b9a3fda'
data/bin/chelsea CHANGED
@@ -7,11 +7,16 @@ opts =
7
7
  Slop.parse do |o|
8
8
  o.string '-f', '--file', 'path to your Gemfile.lock'
9
9
  o.bool '-c', '--config', 'Set persistent config for OSS Index'
10
- o.string '-u', '--user', 'Specify OSS Index Username', default: ""
11
- o.string '-p', '--token', 'Specify OSS Index API Token', default: ""
10
+ o.string '-u', '--user', 'Specify OSS Index Username'
11
+ o.string '-p', '--token', 'Specify OSS Index API Token'
12
+ o.string '-a', '--application', 'Specify the IQ application id', default: 'testapp'
13
+ o.string '-i', '--server', 'Specific the IQ server url', default: 'http://localhost:8070'
14
+ o.string '-iu', '--iquser', 'Specify the IQ username', default: 'admin'
15
+ o.string '-it', '--iqpass', 'Specify the IQ auth token', default: 'admin123'
12
16
  o.string '-w', '--whitelist', 'Set path to vulnerability whitelist file'
13
17
  o.bool '-q', '--quiet', 'make chelsea only output vulnerable third party dependencies for text output (default: false)', default: false
14
18
  o.string '-t', '--format', 'choose what type of format you want your report in (default: text) (options: text, json, xml)', default: 'text'
19
+ o.bool '-b', '--sbom', 'generate an sbom'
15
20
  o.on '--version', 'print the version' do
16
21
  puts Chelsea::VERSION
17
22
  exit
data/lib/chelsea.rb CHANGED
@@ -1 +1,19 @@
1
- require_relative "./chelsea/cli"
1
+ # frozen_string_literal: true
2
+
3
+ # Lazy loading
4
+ require_relative 'chelsea/cli'
5
+ require_relative 'chelsea/deps'
6
+ require_relative 'chelsea/bom'
7
+ require_relative 'chelsea/iq_client'
8
+ require_relative 'chelsea/oss_index'
9
+ require_relative 'chelsea/config'
10
+ require_relative 'chelsea/version'
11
+ # module Chelsea
12
+ # autoload :CLI, 'chelsea/cli'
13
+ # autoload :Deps, 'chelsea/deps'
14
+ # autoload :Bom, 'chelsea/bom'
15
+ # autoload :IQClient, 'chelsea/iq_client'
16
+ # autoload :OSSIndex, 'chelsea/oss_index'
17
+ # autoload :Config, 'chelsea/config'
18
+ # autoload :Version, 'chelsea/version'
19
+ # end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'ox'
5
+
6
+ module Chelsea
7
+ # Class to convext dependencies to BOM xml
8
+ class Bom
9
+ attr_accessor :xml
10
+ def initialize(dependencies)
11
+ @dependencies = dependencies
12
+ @xml = _get_xml
13
+ end
14
+
15
+ def to_s
16
+ Ox.dump(@xml).to_s
17
+ end
18
+
19
+ def random_urn_uuid
20
+ 'urn:uuid:' + SecureRandom.uuid
21
+ end
22
+
23
+ private
24
+
25
+ def _get_xml
26
+ doc = Ox::Document.new
27
+ doc << _root_xml
28
+ bom = _bom_xml
29
+ doc << bom
30
+ components = Ox::Element.new('components')
31
+ @dependencies.each do |_, (name, version)|
32
+ components << _component_xml(name, version)
33
+ end
34
+ bom << components
35
+ doc
36
+ end
37
+
38
+ def _bom_xml
39
+ bom = Ox::Element.new('bom')
40
+ bom[:xmlns] = 'http://cyclonedx.org/schema/bom/1.1'
41
+ bom[:version] = '1'
42
+ bom[:serialNumber] = random_urn_uuid
43
+ bom
44
+ end
45
+
46
+ def _root_xml
47
+ instruct = Ox::Instruct.new(:xml)
48
+ instruct[:version] = '1.0'
49
+ instruct[:encoding] = 'UTF-8'
50
+ instruct[:standalone] = 'yes'
51
+ instruct
52
+ end
53
+
54
+ def _component_xml(name, version)
55
+ component = Ox::Element.new('component')
56
+ component[:type] = 'library'
57
+ n = Ox::Element.new('name')
58
+ n << name
59
+ v = Ox::Element.new('version')
60
+ v << version.version
61
+ purl = Ox::Element.new('purl')
62
+ purl << Chelsea.to_purl(name, version.version)
63
+ component << n << v << purl
64
+ component
65
+ end
66
+
67
+ def _show_logo
68
+ logo = %Q(
69
+ -o/
70
+ -+hNmNN-
71
+ .:+osyhddddyso/-``ody+- .NN.
72
+ /mMMdhssooooooosyhdmhs/. /Mm-
73
+ oMs` `.-:. oMNs. .
74
+ `N. `. .+hNh+` +N.
75
+ yo -m` -d` `dm. `:smd+. `yMM.
76
+ -m`mM/ -mN/`ddMs -sNh/ .dy-M-
77
+ dmdsd/m--dmo Nh `o: /o` `+md- :m/ N:
78
+ /y `Nd` do my .dMy. .hMy` `oN+ om- m:
79
+ + . `No +NN+oNm: .d+ `hd` d:
80
+ `Mo .dMMy SBOM `d+ms` d:
81
+ `. -M+ `yMhmNo` BABY `hN/- d/
82
+ /: yd /o -M/ /NN/ +Nm: +Nd.-mo m+
83
+ dm`/mmo-NMo /M- .dMs` `o/ /mN+ `hh. N+
84
+ -MMdN//NNhhMysm /- `+mMs` +mo N+
85
+ sN/Ny hd``yMMh :yNNs. `sm+M+
86
+ dd.`` `` /d- oy. `/yNNh/` .yM+
87
+ `yNy/` oMm` `/sdMdo- ..
88
+ `/ymmys+///++shN+/Nm. /NMNo.
89
+ `-/+ooo+/:.` :NN- /MMo`
90
+ -NNoNM+
91
+ :MMM+
92
+ :d/
93
+ )
94
+ puts logo
95
+ end
96
+ end
97
+ end
data/lib/chelsea/cli.rb CHANGED
@@ -4,50 +4,66 @@ require 'tty-font'
4
4
 
5
5
  require_relative 'version'
6
6
  require_relative 'gems'
7
+ require_relative 'iq_client'
7
8
  require_relative 'config'
8
9
 
9
10
  module Chelsea
10
11
  ##
11
12
  # This class provides an interface to the oss index, gems and deps
12
13
  class CLI
13
-
14
14
  def initialize(opts)
15
15
  @opts = opts
16
16
  @pastel = Pastel.new
17
17
  _validate_arguments
18
- _show_logo
18
+ _show_logo # Move to formatter
19
19
  end
20
20
 
21
21
  def process!
22
22
  if @opts.config?
23
- _try_set_config()
24
- end
25
- if @opts.file?
26
- @gems = Chelsea::Gems.new(file: @opts[:file], quiet: false, options: @opts)
27
- @gems.execute
23
+ _set_config # move to init
24
+ elsif @opts.file? # don't process unless there was a file
25
+ _process_file
26
+ _submit_sbom if @opts.sbom?
27
+ elsif @opts.help? # quit on opts.help earlier
28
+ puts _cli_flags # this doesn't exist
28
29
  end
29
30
  end
30
31
 
31
- # this is how you do static methods in ruby, because in a test we want to
32
- # check for version without opts, and heck, we don't even want a dang object!
33
32
  def self.version
34
33
  Chelsea::VERSION
35
34
  end
36
35
 
37
- protected
36
+ private
38
37
 
39
- def _flags_error
40
- # should be custom exception!
41
- switches = _flags.collect {|f| "--#{f}"}
38
+ def _submit_sbom
39
+ iq = Chelsea::IQClient.new(
40
+ @opts[:application],
41
+ @opts[:server],
42
+ @opts[:iquser],
43
+ @opts[:iqpass]
44
+ )
45
+ bom = Chelsea::Bom.new(@gems.deps)
46
+ iq.submit_sbom(bom)
47
+ end
48
+
49
+ def _process_file
50
+ gems = Chelsea::Gems.new(
51
+ file: @opts[:file],
52
+ quiet: @opts[:quiet],
53
+ options: @opts
54
+ )
55
+ gems.execute # should be more like collect
56
+ end
42
57
 
58
+ def _flags_error
59
+ switches = _flags.collect { |f| "--#{f}" }
43
60
  abort "please set one of #{switches}"
44
61
  end
45
62
 
46
63
  def _validate_arguments
47
- if !_flags_set? && !@opts.file?
48
- ## require at least one argument
49
- _flags_error
50
- end
64
+ return unless !_flags_set? && !@opts.file?
65
+
66
+ _flags_error
51
67
  end
52
68
 
53
69
  def _flags_set?
@@ -58,23 +74,22 @@ module Chelsea
58
74
 
59
75
  def _flags
60
76
  # Seems wrong, should all be handled by bin
61
- [:file, :help, :config]
77
+ %i[file help config]
62
78
  end
63
79
 
64
- def _show_logo()
80
+ def _show_logo
65
81
  font = TTY::Font.new(:doom)
66
- puts @pastel.green(font.write("Chelsea"))
67
- puts @pastel.green("Version: " + CLI::version)
82
+ puts @pastel.green(font.write('Chelsea'))
83
+ puts @pastel.green('Version: ' + CLI.version)
68
84
  end
69
85
 
70
- def _try_load_config()
86
+ def _load_config
71
87
  config = Chelsea::Config.new
72
- oss_index_config = config.get_oss_index_config()
88
+ config.oss_index_config
73
89
  end
74
90
 
75
- def _try_set_config()
76
- config = Chelsea::Config.new
77
- config.get_oss_index_config_from_command_line()
91
+ def _set_config
92
+ Chelsea.oss_index_config_from_command_line
78
93
  end
79
94
  end
80
95
  end
@@ -1,53 +1,72 @@
1
1
  require 'yaml'
2
+ require_relative 'oss_index'
2
3
 
3
4
  module Chelsea
4
- class Config
5
- def initialize(opts = {})
6
- @oss_index_config_location = File.join("#{Dir.home}", ".ossindex")
7
- @oss_index_config_filename = ".oss-index-config"
8
- end
5
+ @oss_index_config_location = File.join(Dir.home.to_s, '.ossindex')
6
+ @oss_index_config_filename = '.oss-index-config'
9
7
 
10
- def get_oss_index_config()
11
- if !File.exist? File.join(@oss_index_config_location, @oss_index_config_filename)
12
- return {}
13
- else
14
- oss_index_config = YAML.load(File.read(File.join(@oss_index_config_location, @oss_index_config_filename)))
8
+ def self.to_purl(name, version)
9
+ "pkg:gem/#{name}@#{version}"
10
+ end
15
11
 
16
- oss_index_config
17
- end
12
+ def self.config(options = {})
13
+ if !options[:user].nil? && !options[:token].nil?
14
+ Chelsea::OSSIndex.new(
15
+ oss_index_user_name: options[:user],
16
+ oss_index_user_token: options[:token]
17
+ )
18
+ else
19
+ Chelsea::OSSIndex.new(oss_index_config)
18
20
  end
21
+ end
19
22
 
20
- def get_white_list_vuln_config(white_list_config_path)
21
- if white_list_config_path.nil?
22
- white_list_vuln_config = YAML.load(File.read(File.join(Dir.pwd, "chelsea-ignore.yaml")))
23
- else
24
- white_list_vuln_config = YAML.load(File.read(white_list_config_path))
25
- end
23
+ def self.client(options = {})
24
+ @client ||= config(options)
25
+ @client
26
+ end
26
27
 
27
- white_list_vuln_config
28
+ def self.oss_index_config
29
+ if !File.exist? File.join(@oss_index_config_location, @oss_index_config_filename)
30
+ { oss_index_user_name: '', oss_index_user_token: '' }
31
+ else
32
+ conf_hash = YAML.safe_load(
33
+ File.read(
34
+ File.join(@@oss_index_config_location, @@oss_index_config_filename)
35
+ )
36
+ )
37
+ {
38
+ oss_index_user_name: conf_hash['Username'],
39
+ oss_index_user_token: conf_hash['Token']
40
+ }
28
41
  end
42
+ end
29
43
 
30
- def get_oss_index_config_from_command_line()
31
- config = {}
32
-
33
- puts "What username do you want to authenticate as (ex: your email address)? "
34
- config["Username"] = STDIN.gets.chomp
35
-
36
- puts "What token do you want to use? "
37
- config["Token"] = STDIN.gets.chomp
38
-
39
- _set_oss_index_config(config)
44
+ def get_white_list_vuln_config(white_list_config_path)
45
+ if white_list_config_path.nil?
46
+ YAML.safe_load(File.read(File.join(Dir.pwd, 'chelsea-ignore.yaml')))
47
+ else
48
+ YAML.safe_load(File.read(white_list_config_path))
40
49
  end
50
+ end
51
+
52
+ def self.read_oss_index_config_from_command_line
53
+ config = {}
41
54
 
42
- private
55
+ puts 'What username do you want to authenticate as (ex: your email address)? '
56
+ config['Username'] = STDIN.gets.chomp
43
57
 
44
- def _set_oss_index_config(config)
45
- Dir.mkdir(@oss_index_config_location) unless File.exists? @oss_index_config_location
58
+ puts 'What token do you want to use? '
59
+ config['Token'] = STDIN.gets.chomp
46
60
 
47
- File.open(File.join(@oss_index_config_location, @oss_index_config_filename), "w") do |file|
48
- file.write config.to_yaml
49
- end
50
- end
61
+ _write_oss_index_config_file(config)
62
+ end
51
63
 
64
+ def self._write_oss_index_config_file(config)
65
+ unless File.exist? @oss_index_config_location
66
+ Dir.mkdir(@oss_index_config_location)
67
+ end
68
+ File.open(File.join(@oss_index_config_location, @oss_index_config_filename), "w") do |file|
69
+ file.write config.to_yaml
70
+ end
52
71
  end
53
- end
72
+ end
data/lib/chelsea/db.rb ADDED
@@ -0,0 +1,39 @@
1
+ require 'pstore'
2
+
3
+ module Chelsea
4
+ class DB
5
+ def initialize
6
+ @store = PStore.new(_get_db_store_location)
7
+ end
8
+
9
+ # This method will take an array of values, and save them to a pstore database
10
+ # and as well set a TTL of Time.now to be checked later
11
+ def save_values_to_db(values)
12
+ values.each do |val|
13
+ next unless get_cached_value_from_db(val['coordinates']).nil?
14
+
15
+ new_val = val.dup
16
+ new_val['ttl'] = Time.now
17
+ @store.transaction do
18
+ @store[new_val['coordinates']] = new_val
19
+ end
20
+ end
21
+ end
22
+
23
+ def _get_db_store_location()
24
+ initial_path = File.join(Dir.home.to_s, '.ossindex')
25
+ Dir.mkdir(initial_path) unless File.exist? initial_path
26
+ File.join(initial_path, 'chelsea.pstore')
27
+ end
28
+
29
+ # Checks pstore to see if a coordinate exists, and if it does also
30
+ # checks to see if it's ttl has expired. Returns nil unless a record
31
+ # is valid in the cache (ttl has not expired) and found
32
+ def get_cached_value_from_db(val)
33
+ record = @store.transaction { @store[val] }
34
+ return if record.nil?
35
+
36
+ (Time.now - record['ttl']) / 3600 > 12 ? nil : record
37
+ end
38
+ end
39
+ end
data/lib/chelsea/deps.rb CHANGED
@@ -4,34 +4,16 @@ require 'rubygems'
4
4
  require 'rubygems/commands/dependency_command'
5
5
  require 'json'
6
6
  require 'rest-client'
7
- require 'pstore'
8
7
 
9
8
  require_relative 'dependency_exception'
10
9
  require_relative 'oss_index'
11
10
 
12
11
  module Chelsea
13
12
  class Deps
14
- attr_reader :server_response, :reverse_dependencies, :coordinates, :dependencies
15
-
16
- def initialize(path: , oss_index_client: , quiet: false)
17
- @oss_index_client = oss_index_client
18
- @path, @quiet = path, quiet
19
- ENV['BUNDLE_GEMFILE'] = File.expand_path(path).chomp(".lock")
20
-
21
- begin
22
- @lockfile = Bundler::LockfileParser.new(
23
- File.read(@path)
24
- )
25
- rescue
26
- raise "Gemfile.lock not parseable, please check file or that it's path is valid"
27
- end
28
-
29
- @dependencies = {}
30
- @reverse_dependencies = {}
31
- @dependencies_versions = {}
32
- @coordinates = { 'coordinates' => [] }
33
- @server_response = []
34
- @store = PStore.new(_get_db_store_location())
13
+ def initialize(path:, quiet: false)
14
+ @quiet = quiet
15
+ ENV['BUNDLE_GEMFILE'] = File.expand_path(path).chomp('.lock')
16
+ @lockfile = Bundler::LockfileParser.new(File.read(path))
35
17
  end
36
18
 
37
19
  def nil?
@@ -39,119 +21,36 @@ module Chelsea
39
21
  end
40
22
 
41
23
  def self.to_purl(name, version)
42
- return "pkg:gem/#{name}@#{version}"
24
+ "pkg:gem/#{name}@#{version}"
43
25
  end
44
26
 
45
- # Parses specs from lockfile instanct var and inserts into dependenices instance var
46
- def get_dependencies
47
- @lockfile.specs.each do |gem|\
48
- begin
49
- @dependencies[gem.name] = [gem.name, gem.version]
50
- rescue StandardError => e
51
- raise Chelsea::DependencyException e, "Parsing dependency line #{gem} failed."
52
- end
27
+ # Parses specs from lockfile instanct var and
28
+ # inserts into dependenices instance var
29
+ def dependencies
30
+ @lockfile.specs.each_with_object({}) do |gem, h|
31
+ h[gem.name] = [gem.name, gem.version]
53
32
  end
54
33
  end
55
34
 
56
35
  # Collects all reverse dependencies in reverse_dependencies instance var
57
- def get_reverse_dependencies
58
- begin
59
- reverse = Gem::Commands::DependencyCommand.new
60
- reverse.options[:reverse_dependencies] = true
61
- @reverse_dependencies = reverse.reverse_dependencies(@lockfile.specs).to_h
62
- rescue => e
63
- raise Chelsea::DependencyException e, "ReverseDependencyException"
36
+ # this rescue block honks
37
+ def reverse_dependencies
38
+ reverse = Gem::Commands::DependencyCommand.new
39
+ reverse.options[:reverse_dependencies] = true
40
+ # We want to filter the reverses dependencies by specs in lockfile
41
+ spec_names = @lockfile.specs.map { |i| i.to_s.split }.map { |n, _| "#{n}" }
42
+ reverse.reverse_dependencies(@lockfile.specs).to_h.transform_values do |reverse_dep|
43
+ # Add filtering if version meets range
44
+ reverse_dep.select { |name, dep, req, _| spec_names.include?(name.split("-")[0]) }
64
45
  end
65
46
  end
66
47
 
67
48
  # Iterates over all dependencies and stores them
68
49
  # in dependencies_versions and coordinates instance vars
69
- def get_dependencies_versions_as_coordinates
70
- @dependencies.each do |p, r|
71
- o = r[0]
72
- v = r[1].to_s
73
- if v.split('.').length == 1 then
74
- v = v + ".0.0"
75
- elsif v.split('.').length == 2 then
76
- v = v + ".0"
77
- end
78
- @dependencies_versions[p] = v
79
- end
80
-
81
- @dependencies_versions.each do |p, v|
82
- @coordinates["coordinates"] << self.class.to_purl(p,v);
83
- end
84
- end
85
-
86
- # Makes REST calls to OSS for vulnerabilities 128 coordinates at a time
87
- # Checks cache and stores results in cache
88
- def get_vulns()
89
- _check_db_for_cached_values()
90
-
91
- if @coordinates["coordinates"].count() > 0
92
- chunked = Hash.new()
93
- @coordinates["coordinates"].each_slice(128).to_a.each do |coords|
94
- chunked["coordinates"] = coords
95
- res_json = @oss_index_client.call_oss_index(chunked)
96
- @server_response = @server_response.concat(res_json)
97
- _save_values_to_db(res_json)
98
- end
99
- end
100
- end
101
-
102
- protected
103
- # This method will take an array of values, and save them to a pstore database
104
- # and as well set a TTL of Time.now to be checked later
105
- def _save_values_to_db(values)
106
- values.each do |val|
107
- if _get_cached_value_from_db(val["coordinates"]).nil?
108
- new_val = val.dup
109
- new_val["ttl"] = Time.now
110
- @store.transaction do
111
- @store[new_val["coordinates"]] = new_val
112
- end
113
- end
114
- end
115
- end
116
-
117
- def _get_db_store_location()
118
- initial_path = File.join("#{Dir.home}", ".ossindex")
119
- Dir.mkdir(initial_path) unless File.exists? initial_path
120
- path = File.join(initial_path, "chelsea.pstore")
121
- end
122
-
123
- # Checks pstore to see if a coordinate exists, and if it does also
124
- # checks to see if it's ttl has expired. Returns nil unless a record
125
- # is valid in the cache (ttl has not expired) and found
126
- def _get_cached_value_from_db(coordinate)
127
- record = @store.transaction { @store[coordinate] }
128
- if !record.nil?
129
- diff = (Time.now - record['ttl']) / 3600
130
- if diff > 12
131
- return nil
132
- else
133
- return record
134
- end
135
- else
136
- return nil
137
- end
138
- end
139
-
140
- # Goes through the list of @coordinates and checks pstore for them, if it finds a valid coord
141
- # it will add it to the server response. If it does not, it will append the coord to a new hash
142
- # and eventually set @coordinates to the new hash, so we query OSS Index on only coords not in cache
143
- def _check_db_for_cached_values()
144
- new_coords = Hash.new
145
- new_coords["coordinates"] = Array.new
146
- @coordinates["coordinates"].each do |coord|
147
- record = _get_cached_value_from_db(coord)
148
- if !record.nil?
149
- @server_response << record
150
- else
151
- new_coords["coordinates"].push(coord)
152
- end
50
+ def coordinates
51
+ dependencies.each_with_object({ 'coordinates' => [] }) do |(name, v), coords|
52
+ coords['coordinates'] << self.class.to_purl(name, v[1]);
153
53
  end
154
- @coordinates = new_coords
155
54
  end
156
55
  end
157
56
  end
@@ -2,15 +2,18 @@ require_relative 'json'
2
2
  require_relative 'xml'
3
3
  require_relative 'text'
4
4
 
5
+ # Factory for formatting dependencies
5
6
  class FormatterFactory
6
- def get_formatter(format: 'text', options: {})
7
- case format
8
- when 'text'
9
- Chelsea::TextFormatter.new()
10
- when 'json'
11
- Chelsea::JsonFormatter.new()
12
- when 'xml'
13
- Chelsea::XMLFormatter.new()
14
- end
7
+ def get_formatter(format: 'text', quiet: false)
8
+ case format
9
+ when 'text'
10
+ Chelsea::TextFormatter.new quiet: quiet
11
+ when 'json'
12
+ Chelsea::JsonFormatter.new quiet: quiet
13
+ when 'xml'
14
+ Chelsea::XMLFormatter.new quiet: quiet
15
+ else
16
+ Chelsea::TextFormatter.new quiet: quiet
17
+ end
15
18
  end
16
- end
19
+ end
@@ -8,7 +8,7 @@ module Chelsea
8
8
  @pastel = Pastel.new
9
9
  end
10
10
 
11
- def get_results(dependencies)
11
+ def get_results(server_response, reverse_dependencies)
12
12
  response = String.new
13
13
  if !@quiet
14
14
  response += "\n"\
@@ -17,25 +17,25 @@ module Chelsea
17
17
  end
18
18
 
19
19
  i = 0
20
- count = dependencies.server_response.count()
21
- dependencies.server_response.each do |r|
20
+ count = server_response.count()
21
+ server_response.each do |r|
22
22
  i += 1
23
- package = r["coordinates"]
24
- vulnerable = r["vulnerabilities"].length() > 0
25
- coord = r["coordinates"].sub("pkg:gem/", "")
23
+ package = r['coordinates']
24
+ vulnerable = r['vulnerabilities'].length.positive?
25
+ coord = r['coordinates'].sub('pkg:gem/', '')
26
26
  name = coord.split('@')[0]
27
27
  version = coord.split('@')[1]
28
- reverse_deps = dependencies.reverse_dependencies["#{name}-#{version}"]
28
+ reverse_deps = reverse_dependencies["#{name}-#{version}"]
29
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")
30
+ response += @pastel.red("[#{i}/#{count}] - #{package} ") + @pastel.red.bold("Vulnerable.\n")
31
+ response += _get_reverse_deps(reverse_deps, name) if reverse_deps
32
+ r['vulnerabilities'].each do |k, v|
33
+ response += _format_vuln(v)
34
34
  end
35
35
  else
36
36
  if !@quiet
37
37
  response += @pastel.white("[#{i}/#{count}] - #{package} ") + @pastel.green.bold("No vulnerabilities found!\n")
38
- response += _get_reverse_deps(reverse_deps, name)
38
+ response += _get_reverse_deps(reverse_deps, name) if reverse_deps
39
39
  end
40
40
  end
41
41
  end
@@ -47,17 +47,18 @@ module Chelsea
47
47
  puts results
48
48
  end
49
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|
50
+ def _format_vuln(vuln)
51
+ @pastel.red.bold("\n#{vuln}\n")
52
+ end
53
+
54
+ def _get_reverse_deps(coords, name)
55
+ coords.each_with_object(String.new) do |dep, s|
54
56
  dep.each do |gran|
55
57
  if gran.class == String && !gran.include?(name)
56
- response += "\tRequired by: #{gran}\n"
58
+ s << "\tRequired by: #{gran}\n"
57
59
  end
58
60
  end
59
61
  end
60
- response
61
62
  end
62
63
  end
63
64
  end
data/lib/chelsea/gems.rb CHANGED
@@ -1,118 +1,107 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require 'pastel'
4
3
  require 'tty-spinner'
5
4
  require 'bundler'
6
5
  require 'bundler/lockfile_parser'
7
6
  require 'rubygems'
8
7
  require 'rubygems/commands/dependency_command'
8
+
9
9
  require_relative 'version'
10
10
  require_relative 'formatters/factory'
11
11
  require_relative 'deps'
12
-
12
+ require_relative 'bom'
13
13
 
14
14
  module Chelsea
15
15
  class Gems
16
16
  def initialize(file:, quiet: false, options: {})
17
- @file, @quiet, @options = file, quiet, options
18
-
19
- if not _gemfile_lock_file_exists? or file.nil?
20
- raise "Gemfile.lock not found, check --file path"
17
+ @quiet = quiet
18
+ unless File.file?(file) || file.nil?
19
+ raise 'Gemfile.lock not found, check --file path'
21
20
  end
21
+ _silence_stderr if @quiet
22
+
22
23
  @pastel = Pastel.new
23
- @formatter = FormatterFactory.new.get_formatter(format: @options[:format], options: @options)
24
- @deps = Chelsea::Deps.new({path: Pathname.new(@file), oss_index_client: Chelsea::OSSIndex.new(oss_index_user_name: @options[:user], oss_index_user_token: @options[:token])})
24
+ @formatter = FormatterFactory.new.get_formatter(format: options[:format], quiet: @quiet)
25
+ @client = Chelsea.client(options)
26
+ @deps = Chelsea::Deps.new(path: Pathname.new(file))
25
27
  end
26
28
 
27
29
  # Audits depenencies using deps library and prints results
28
30
  # using formatter library
29
- def execute(input: $stdin, output: $stdout)
30
- audit
31
- if @deps.nil?
32
- _print_err "No dependencies retrieved. Exiting."
31
+
32
+ def execute
33
+ server_response, dependencies, reverse_dependencies = audit
34
+ if dependencies.nil?
35
+ _print_err 'No dependencies retrieved. Exiting.'
33
36
  return
34
37
  end
35
- if !@deps.server_response.count
36
- _print_err "No vulnerability data retrieved from server. Exiting."
38
+ if server_response.nil?
39
+ _print_err 'No vulnerability data retrieved from server. Exiting.'
37
40
  return
38
41
  end
39
- # if !@options[:whitelist]
40
-
41
- # end
42
- @formatter.do_print(@formatter.get_results(@deps))
42
+ results = @formatter.get_results(server_response, reverse_dependencies)
43
+ @formatter.do_print(results)
43
44
  end
44
45
 
45
46
  # Runs through auditing algorithm, raising exceptions
46
47
  # on REST calls made by @deps.get_vulns
47
48
  def audit
48
- unless @quiet
49
- spinner = _spin_msg "Parsing dependencies"
50
- end
49
+ # This spinner management is out of control
50
+ # we should wrap a block with start and stop messages,
51
+ # or use a stack to ensure all spinners stop.
52
+ spinner = _spin_msg 'Parsing dependencies'
51
53
 
52
54
  begin
53
- @deps.get_dependencies
54
- unless @quiet
55
- spinner.success("...done.")
56
- end
55
+ dependencies = @deps.dependencies
56
+ spinner.success('...done.')
57
57
  rescue StandardError => e
58
- unless @quiet
59
- spinner.stop
60
- end
58
+ spinner.stop
61
59
  _print_err "Parsing dependency line #{gem} failed."
62
60
  end
63
61
 
64
- @deps.get_reverse_dependencies
62
+ reverse_dependencies = @deps.reverse_dependencies
65
63
 
66
- unless @quiet
67
- spinner = _spin_msg "Parsing Versions"
68
- end
69
- @deps.get_dependencies_versions_as_coordinates
70
- unless @quiet
71
- spinner.success("...done.")
72
- end
73
-
74
- unless @quiet
75
- spinner = _spin_msg "Making request to OSS Index server"
76
- end
64
+ spinner = _spin_msg 'Parsing Versions'
65
+ coordinates = @deps.coordinates
66
+ spinner.success('...done.')
67
+ spinner = _spin_msg 'Making request to OSS Index server'
77
68
 
78
69
  begin
79
- @deps.get_vulns
80
- unless @quiet
81
- spinner.success("...done.")
82
- end
70
+ server_response = @client.get_vulns(coordinates)
71
+ spinner.success('...done.')
83
72
  rescue SocketError => e
84
- unless @quiet
85
- spinner.stop("...request failed.")
86
- end
87
- _print_err "Socket error getting data from OSS Index server."
73
+ spinner.stop('...request failed.')
74
+ _print_err 'Socket error getting data from OSS Index server.'
88
75
  rescue RestClient::RequestFailed => e
89
- unless @quiet
90
- spinner.stop("...request failed.")
91
- end
76
+ spinner.stop('...request failed.')
92
77
  _print_err "Error getting data from OSS Index server:#{e.response}."
93
- rescue RestClient::ResourceNotfound => e
94
- unless @quiet
95
- spinner.stop("...request failed.")
96
- end
97
- _print_err "Error getting data from OSS Index server. Resource not found."
78
+ rescue RestClient::ResourceNotFound => e
79
+ spinner.stop('...request failed.')
80
+ _print_err 'Error getting data from OSS Index server. Resource not found.'
98
81
  rescue Errno::ECONNREFUSED => e
99
- unless @quiet
100
- spinner.stop("...request failed.")
101
- end
102
- _print_err "Error getting data from OSS Index server. Connection refused."
82
+ spinner.stop('...request failed.')
83
+ _print_err 'Error getting data from OSS Index server. Connection refused.'
103
84
  rescue StandardError => e
104
- unless @quiet
105
- spinner.stop("...request failed.")
106
- end
107
- _print_err "UNKNOWN Error getting data from OSS Index server."
85
+ spinner.stop('...request failed.')
86
+ _print_err 'UNKNOWN Error getting data from OSS Index server.'
108
87
  end
88
+ [server_response, dependencies, reverse_dependencies]
109
89
  end
110
90
 
111
91
  protected
92
+
93
+ def _silence_stderr
94
+ $stderr.reopen('/dev/null', 'w')
95
+ end
96
+
112
97
  def _spin_msg(msg)
113
98
  format = "[#{@pastel.green(':spinner')}] " + @pastel.white(msg)
114
- spinner = TTY::Spinner.new(format, success_mark: @pastel.green('+'), hide_cursor: true)
115
- spinner.auto_spin()
99
+ spinner = TTY::Spinner.new(
100
+ format,
101
+ success_mark: @pastel.green('+'),
102
+ hide_cursor: true
103
+ )
104
+ spinner.auto_spin
116
105
  spinner
117
106
  end
118
107
 
@@ -123,9 +112,5 @@ module Chelsea
123
112
  def _print_success(s)
124
113
  puts @pastel.green.bold(s)
125
114
  end
126
-
127
- def _gemfile_lock_file_exists?
128
- ::File.file? @file
129
- end
130
115
  end
131
- end
116
+ end
@@ -0,0 +1,61 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+
4
+ module Chelsea
5
+ class IQClient
6
+ DEFAULT_OPTIONS = {
7
+ public_application_id: 'testapp',
8
+ server_url: 'http://localhost:8070',
9
+ username: 'admin',
10
+ auth_token: 'admin123',
11
+ internal_application_id: ''
12
+ }
13
+ def initialize(options: DEFAULT_OPTIONS)
14
+ @options = options
15
+ end
16
+
17
+ def submit_sbom(sbom)
18
+ @internal_application_id = _get_internal_application_id()
19
+ resource = RestClient::Resource.new(
20
+ _api_url,
21
+ user: @options[:username],
22
+ password: @options[:auth_token]
23
+ )
24
+ _headers['Content-Type'] = 'application/xml'
25
+ resource.post sbom.to_s, _headers
26
+ end
27
+
28
+ def status_url(res)
29
+ res = JSON.parse(res.body)
30
+ res['statusUrl']
31
+ end
32
+
33
+ private
34
+
35
+ def _get_internal_application_id
36
+ resource = RestClient::Resource.new(
37
+ _internal_application_id_api_url,
38
+ user: @username,
39
+ password: @auth_token
40
+ )
41
+ res = JSON.parse(resource.get(headers))
42
+ res['applications'][0]['id']
43
+ end
44
+
45
+ def _headers
46
+ { 'User-Agent' => _user_agent }
47
+ end
48
+
49
+ def _api_url
50
+ "#{@options[:server_url]}/api/v2/scan/applications/#{@@internal_application_id}/sources/chelsea"
51
+ end
52
+
53
+ def _internal_application_id_api_url
54
+ "#{@options[:server_url]}/api/v2/applications?publicId=#{@options[:public_application_id]}"
55
+ end
56
+
57
+ def _user_agent
58
+ "chelsea/#{Chelsea::VERSION}"
59
+ end
60
+ end
61
+ end
@@ -1,50 +1,77 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'config'
2
4
  require 'rest-client'
5
+ require_relative 'db'
3
6
 
4
7
  module Chelsea
5
8
  class OSSIndex
9
+ def initialize(oss_index_user_name: '', oss_index_user_token: '')
10
+ @oss_index_user_name = oss_index_user_name
11
+ @oss_index_user_token = oss_index_user_token
12
+ @db = DB.new
13
+ end
6
14
 
7
- def initialize(oss_index_user_name: "", oss_index_user_token: "")
8
- if oss_index_user_name.empty? || oss_index_user_token.empty?
9
- config = Chelsea::Config.new().get_oss_index_config()
10
- if config != {}
11
- @oss_index_user_name, @oss_index_user_token = config["Username"], config["Token"]
12
- else
13
- @oss_index_user_name, @oss_index_user_token = oss_index_user_name, oss_index_user_token
14
- end
15
- else
16
- @oss_index_user_name, @oss_index_user_token = oss_index_user_name, oss_index_user_token
15
+ # Makes REST calls to OSS for vulnerabilities 128 coordinates at a time
16
+ # Checks cache and stores results in cache
17
+
18
+ def get_vulns(coordinates)
19
+ remaining_coordinates, cached_server_response = _cache(coordinates)
20
+ unless remaining_coordinates['coordinates'].count.positive?
21
+ return cached_server_response
22
+ end
23
+
24
+ remaining_coordinates['coordinates'].each_slice(128).to_a.each do |coords|
25
+ res_json = call_oss_index({ 'coordinates' => coords })
26
+ cached_server_response = cached_server_response.concat(res_json)
27
+ @db.save_values_to_db(res_json)
17
28
  end
29
+ cached_server_response
18
30
  end
19
31
 
20
32
  def call_oss_index(coords)
21
33
  r = _resource.post coords.to_json, _headers
22
- if r.code == 200
23
- JSON.parse(r.body)
24
- end
34
+ r.code == 200 ? JSON.parse(r.body) : {}
25
35
  end
26
36
 
27
37
  private
28
38
 
39
+ def _cache(coordinates)
40
+ new_coords = { 'coordinates' => [] }
41
+ cached_server_response = []
42
+ coordinates['coordinates'].each do |coord|
43
+ record = @db.get_cached_value_from_db(coord)
44
+ if !record.nil?
45
+ cached_server_response << record
46
+ else
47
+ new_coords['coordinates'].push(coord)
48
+ end
49
+ end
50
+ [new_coords, cached_server_response]
51
+ end
52
+
29
53
  def _headers
30
54
  { :content_type => :json, :accept => :json, 'User-Agent' => _user_agent }
31
55
  end
32
56
 
33
57
  def _resource
34
58
  if !@oss_index_user_name.empty? && !@oss_index_user_token.empty?
35
- RestClient::Resource.new _api_url, :user => @oss_index_user_name, :password => @oss_index_user_token
59
+ RestClient::Resource.new(
60
+ _api_url,
61
+ user: @oss_index_user_name,
62
+ password: @oss_index_user_token
63
+ )
36
64
  else
37
65
  RestClient::Resource.new _api_url
38
66
  end
39
67
  end
40
68
 
41
69
  def _api_url
42
- "https://ossindex.sonatype.org/api/v3/component-report"
70
+ 'https://ossindex.sonatype.org/api/v3/component-report'
43
71
  end
44
72
 
45
73
  def _user_agent
46
74
  "chelsea/#{Chelsea::VERSION}"
47
75
  end
48
-
49
76
  end
50
- end
77
+ end
@@ -1,3 +1,3 @@
1
1
  module Chelsea
2
- VERSION = "0.0.7"
2
+ VERSION = '0.0.8'.freeze
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.7
4
+ version: 0.0.8
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-04-07 00:00:00.000000000 Z
11
+ date: 2020-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tty-font
@@ -198,8 +198,10 @@ files:
198
198
  - chelsea
199
199
  - chelsea.gemspec
200
200
  - lib/chelsea.rb
201
+ - lib/chelsea/bom.rb
201
202
  - lib/chelsea/cli.rb
202
203
  - lib/chelsea/config.rb
204
+ - lib/chelsea/db.rb
203
205
  - lib/chelsea/dependency_exception.rb
204
206
  - lib/chelsea/deps.rb
205
207
  - lib/chelsea/formatters/factory.rb
@@ -208,6 +210,7 @@ files:
208
210
  - lib/chelsea/formatters/text.rb
209
211
  - lib/chelsea/formatters/xml.rb
210
212
  - lib/chelsea/gems.rb
213
+ - lib/chelsea/iq_client.rb
211
214
  - lib/chelsea/oss_index.rb
212
215
  - lib/chelsea/version.rb
213
216
  homepage: https://github.com/sonatype-nexus-community/chelsea