chelsea 0.0.7 → 0.0.8

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