chelsea 0.0.25 → 0.0.30

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Copyright 2019-Present Sonatype Inc.
3
5
  #
@@ -18,12 +20,14 @@ require 'json'
18
20
  require_relative 'formatter'
19
21
 
20
22
  module Chelsea
23
+ # Produce output in json format
21
24
  class JsonFormatter < Formatter
22
25
  def initialize(options)
26
+ super()
23
27
  @options = options
24
28
  end
25
29
 
26
- def get_results(server_response, reverse_deps)
30
+ def fetch_results(server_response, _reverse_deps)
27
31
  server_response.to_json
28
32
  end
29
33
 
@@ -1,3 +1,6 @@
1
+ # rubocop:disable Style/FrozenStringLiteralComment
2
+ # rubocop:enable Style/FrozenStringLiteralComment
3
+
1
4
  #
2
5
  # Copyright 2019-Present Sonatype Inc.
3
6
  #
@@ -19,13 +22,16 @@ require 'tty-table'
19
22
  require_relative 'formatter'
20
23
 
21
24
  module Chelsea
25
+ # Produce output in text format
22
26
  class TextFormatter < Formatter
23
27
  def initialize(options)
28
+ super()
24
29
  @options = options
25
30
  @pastel = Pastel.new
26
31
  end
27
32
 
28
- def get_results(server_response, reverse_dependencies)
33
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
34
+ def fetch_results(server_response, reverse_dependencies) # rubocop:disable Metrics/MethodLength
29
35
  response = ''
30
36
  if @options[:verbose]
31
37
  response += "\n"\
@@ -65,6 +71,7 @@ module Chelsea
65
71
  response += table.render(:unicode)
66
72
  response
67
73
  end
74
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
68
75
 
69
76
  def do_print(results)
70
77
  puts results
@@ -109,9 +116,7 @@ module Chelsea
109
116
  def _get_reverse_deps(coords, name)
110
117
  coords.each_with_object('') do |dep, s|
111
118
  dep.each do |gran|
112
- if gran.class == String && !gran.include?(name)
113
- s << "\tRequired by: #{gran}\n"
114
- end
119
+ s << "\tRequired by: #{gran}\n" if gran.instance_of?(String) && !gran.include?(name)
115
120
  end
116
121
  end
117
122
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Copyright 2019-Present Sonatype Inc.
3
5
  #
@@ -17,12 +19,14 @@
17
19
  require 'ox'
18
20
  require_relative 'formatter'
19
21
  module Chelsea
22
+ # Produce output in xml format
20
23
  class XMLFormatter < Formatter
21
24
  def initialize(options)
25
+ super()
22
26
  @options = options
23
27
  end
24
28
 
25
- def get_results(server_response, reverse_deps)
29
+ def fetch_results(server_response, _reverse_deps) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
26
30
  doc = Ox::Document.new
27
31
  instruct = Ox::Instruct.new(:xml)
28
32
  instruct[:version] = '1.0'
@@ -37,13 +41,13 @@ module Chelsea
37
41
 
38
42
  server_response.each do |coord|
39
43
  testcase = Ox::Element.new('testcase')
40
- testcase[:classname] = coord["coordinates"]
41
- testcase[:name] = coord["coordinates"]
44
+ testcase[:classname] = coord['coordinates']
45
+ testcase[:name] = coord['coordinates']
42
46
 
43
47
  if coord['vulnerabilities'].length.positive?
44
48
  failure = Ox::Element.new('failure')
45
- failure[:type] = "Vulnerable Dependency"
46
- failure << get_vulnerability_block(coord["vulnerabilities"])
49
+ failure[:type] = 'Vulnerable Dependency'
50
+ failure << get_vulnerability_block(coord['vulnerabilities'])
47
51
  testcase << failure
48
52
  testsuite << testcase
49
53
  elsif @options[:verbose]
@@ -58,20 +62,20 @@ module Chelsea
58
62
  puts Ox.dump(results)
59
63
  end
60
64
 
61
- def get_vulnerability_block(vulnerabilities)
62
- vulnBlock = String.new
65
+ def get_vulnerability_block(vulnerabilities) # rubocop:disable Metrics/MethodLength
66
+ vuln_block = ''
63
67
  vulnerabilities.each do |vuln|
64
- vulnBlock += "Vulnerability Title: #{vuln["title"]}\n"\
65
- "ID: #{vuln["id"]}\n"\
66
- "Description: #{vuln["description"]}\n"\
67
- "CVSS Score: #{vuln["cvssScore"]}\n"\
68
- "CVSS Vector: #{vuln["cvssVector"]}\n"\
69
- "CVE: #{vuln["cve"]}\n"\
70
- "Reference: #{vuln["reference"]}"\
68
+ vuln_block += "Vulnerability Title: #{vuln['title']}\n"\
69
+ "ID: #{vuln['id']}\n"\
70
+ "Description: #{vuln['description']}\n"\
71
+ "CVSS Score: #{vuln['cvssScore']}\n"\
72
+ "CVSS Vector: #{vuln['cvssVector']}\n"\
73
+ "CVE: #{vuln['cve']}\n"\
74
+ "Reference: #{vuln['reference']}"\
71
75
  "\n"
72
76
  end
73
-
74
- vulnBlock
77
+
78
+ vuln_block
75
79
  end
76
80
  end
77
81
  end
data/lib/chelsea/gems.rb CHANGED
@@ -15,6 +15,7 @@
15
15
  #
16
16
 
17
17
  # frozen_string_literal: true
18
+
18
19
  require 'pastel'
19
20
  require 'bundler'
20
21
  require 'bundler/lockfile_parser'
@@ -31,11 +32,10 @@ module Chelsea
31
32
  # Class to collect and audit packages from a Gemfile.lock
32
33
  class Gems
33
34
  attr_accessor :deps
34
- def initialize(file:, verbose:, options: { 'format': 'text' })
35
+
36
+ def initialize(file:, verbose:, options: { format: 'text' }) # rubocop:disable Metrics/MethodLength
35
37
  @verbose = verbose
36
- unless File.file?(file) || file.nil?
37
- raise 'Gemfile.lock not found, check --file path'
38
- end
38
+ raise 'Gemfile.lock not found, check --file path' unless File.file?(file) || file.nil?
39
39
 
40
40
  _silence_stderr unless @verbose
41
41
 
@@ -52,7 +52,7 @@ module Chelsea
52
52
  # Audits depenencies using deps library and prints results
53
53
  # using formatter library
54
54
 
55
- def execute
55
+ def execute # rubocop:disable Metrics/MethodLength
56
56
  server_response, dependencies, reverse_dependencies = audit
57
57
  if dependencies.nil?
58
58
  _print_err 'No dependencies retrieved. Exiting.'
@@ -62,20 +62,19 @@ module Chelsea
62
62
  _print_success 'No vulnerability data retrieved from server. Exiting.'
63
63
  return
64
64
  end
65
- results = @formatter.get_results(server_response, reverse_dependencies)
65
+ results = @formatter.fetch_results(server_response, reverse_dependencies)
66
66
  @formatter.do_print(results)
67
67
 
68
68
  server_response.map { |r| r['vulnerabilities'].length.positive? }.any?
69
69
  end
70
70
 
71
71
  def collect_iq
72
- dependencies = @deps.dependencies
73
- dependencies
72
+ @deps.dependencies
74
73
  end
75
74
 
76
75
  # Runs through auditing algorithm, raising exceptions
77
76
  # on REST calls made by @deps.get_vulns
78
- def audit
77
+ def audit # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
79
78
  # This spinner management is out of control
80
79
  # we should wrap a block with start and stop messages,
81
80
  # or use a stack to ensure all spinners stop.
@@ -84,7 +83,7 @@ module Chelsea
84
83
  begin
85
84
  dependencies = @deps.dependencies
86
85
  spin.success('...done.')
87
- rescue StandardError => e
86
+ rescue StandardError
88
87
  spin.stop
89
88
  _print_err "Parsing dependency line #{gem} failed."
90
89
  end
@@ -100,16 +99,16 @@ module Chelsea
100
99
  begin
101
100
  server_response = @client.get_vulns(coordinates)
102
101
  spin.success('...done.')
103
- rescue SocketError => e
102
+ rescue SocketError
104
103
  spin.stop('...request failed.')
105
104
  _print_err 'Socket error getting data from OSS Index server.'
106
105
  rescue RestClient::RequestFailed => e
107
106
  spin.stop('...request failed.')
108
107
  _print_err "Error getting data from OSS Index server:#{e.response}."
109
- rescue RestClient::ResourceNotFound => e
108
+ rescue RestClient::ResourceNotFound
110
109
  spin.stop('...request failed.')
111
110
  _print_err 'Error getting data from OSS Index server. Resource not found.'
112
- rescue Errno::ECONNREFUSED => e
111
+ rescue Errno::ECONNREFUSED
113
112
  spin.stop('...request failed.')
114
113
  _print_err 'Error getting data from OSS Index server. Connection refused.'
115
114
  end
@@ -122,12 +121,12 @@ module Chelsea
122
121
  $stderr.reopen('/dev/null', 'w')
123
122
  end
124
123
 
125
- def _print_err(s)
126
- puts @pastel.red.bold(s)
124
+ def _print_err(msg)
125
+ puts @pastel.red.bold(msg)
127
126
  end
128
127
 
129
- def _print_success(s)
130
- puts @pastel.green.bold(s)
128
+ def _print_success(msg)
129
+ puts @pastel.green.bold(msg)
131
130
  end
132
131
  end
133
132
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Copyright 2019-Present Sonatype Inc.
3
5
  #
@@ -17,12 +19,13 @@
17
19
  require 'rest-client'
18
20
  require 'json'
19
21
  require 'pastel'
22
+ require 'uri'
20
23
 
21
24
  require_relative 'spinner'
22
25
 
23
26
  module Chelsea
24
- class IQClient
25
-
27
+ # IQ audit operations
28
+ class IQClient # rubocop:disable Metrics/ClassLength
26
29
  DEFAULT_OPTIONS = {
27
30
  public_application_id: 'testapp',
28
31
  server_url: 'http://localhost:8070',
@@ -30,15 +33,16 @@ module Chelsea
30
33
  auth_token: 'admin123',
31
34
  internal_application_id: '',
32
35
  stage: 'build'
33
- }
36
+ }.freeze
37
+
34
38
  def initialize(options: DEFAULT_OPTIONS)
35
39
  @options = options
36
40
  @pastel = Pastel.new
37
41
  @spinner = Chelsea::Spinner.new
38
42
  end
39
43
 
40
- def post_sbom(sbom)
41
- spin = @spinner.spin_msg "Submitting sbom to Nexus IQ Server"
44
+ def post_sbom(sbom) # rubocop:disable Metrics/MethodLength
45
+ spin = @spinner.spin_msg 'Submitting sbom to Nexus IQ Server'
42
46
  @internal_application_id = _get_internal_application_id
43
47
  resource = RestClient::Resource.new(
44
48
  _api_url,
@@ -46,8 +50,8 @@ module Chelsea
46
50
  password: @options[:auth_token]
47
51
  )
48
52
  res = resource.post sbom.to_s, _headers.merge(content_type: 'application/xml')
49
- unless res.code != 202
50
- spin.success("...done.")
53
+ if res.code == 202
54
+ spin.success('...done.')
51
55
  status_url(res)
52
56
  else
53
57
  spin.stop('...request failed.')
@@ -61,33 +65,51 @@ module Chelsea
61
65
  end
62
66
 
63
67
  def poll_status(url)
64
- spin = @spinner.spin_msg "Polling Nexus IQ Server for results"
68
+ spin = @spinner.spin_msg 'Polling Nexus IQ Server for results'
65
69
  loop do
66
- begin
67
- res = _poll_iq_server(url)
68
- if res.code == 200
69
- spin.success("...done.")
70
- _handle_response(res)
71
- break
72
- end
73
- rescue
74
- sleep(1)
70
+ res = _poll_iq_server(url)
71
+ if res.code == 200
72
+ spin.success('...done.')
73
+ return _handle_response(res)
75
74
  end
75
+ rescue StandardError
76
+ sleep(1)
76
77
  end
77
78
  end
78
79
 
80
+ # colors to use when printing message
81
+ COLOR_FAILURE = 31
82
+ COLOR_WARNING = 33 # want yellow, but doesn't appear to print
83
+ COLOR_NONE = 32
84
+ # Known policy actions
85
+ POLICY_ACTION_FAILURE = 'Failure'
86
+ POLICY_ACTION_WARNING = 'Warning'
87
+ POLICY_ACTION_NONE = 'None'
88
+
79
89
  private
80
90
 
81
- def _handle_response(res)
91
+ def _handle_response(res) # rubocop:disable Metrics/MethodLength
82
92
  res = JSON.parse(res.body)
83
- unless res['policyAction'] == 'Failure'
84
- puts @pastel.white.bold("Hi! Chelsea here, no policy violations for this audit!")
85
- puts @pastel.white.bold("Report URL: #{res['reportHtmlUrl']}")
86
- exit 0
93
+ # get absolute report url
94
+ absolute_report_html_url = URI.join(@options[:server_url], res['reportHtmlUrl'])
95
+
96
+ case res['policyAction']
97
+ when POLICY_ACTION_FAILURE
98
+ ['Hi! Chelsea here, you have some policy violations to clean up!'\
99
+ "\nReport URL: #{absolute_report_html_url}",
100
+ COLOR_FAILURE, 1]
101
+ when POLICY_ACTION_WARNING
102
+ ['Hi! Chelsea here, you have some policy warnings to peck at!'\
103
+ "\nReport URL: #{absolute_report_html_url}",
104
+ COLOR_WARNING, 0]
105
+ when POLICY_ACTION_NONE
106
+ ['Hi! Chelsea here, no policy violations for this audit!'\
107
+ "\nReport URL: #{absolute_report_html_url}",
108
+ COLOR_NONE, 0]
87
109
  else
88
- puts @pastel.red.bold("Hi! Chelsea here, you have some policy violations to clean up!")
89
- puts @pastel.red.bold("Report URL: #{res['reportHtmlUrl']}")
90
- exit 1
110
+ ['Hi! Chelsea here, no policy violations for this audit, but unknown policy action!'\
111
+ "\nReport URL: #{absolute_report_html_url}",
112
+ COLOR_FAILURE, 1]
91
113
  end
92
114
  end
93
115
 
@@ -115,33 +137,37 @@ module Chelsea
115
137
  res['statusUrl']
116
138
  end
117
139
 
118
- private
119
-
120
- def _poll_status
140
+ def _poll_status # rubocop:disable Metrics/MethodLength
121
141
  return unless @status_url
122
142
 
123
143
  loop do
124
- begin
125
- res = check_status(@status_url)
126
- if res.code == 200
127
- puts JSON.parse(res.body)
128
- break
129
- end
130
- rescue RestClient::ResourceNotFound => _e
131
- print '.'
132
- sleep(1)
144
+ res = check_status(@status_url)
145
+ if res.code == 200
146
+ puts JSON.parse(res.body)
147
+ break
133
148
  end
149
+ rescue RestClient::ResourceNotFound => _e
150
+ print '.'
151
+ sleep(1)
134
152
  end
135
153
  end
136
154
 
137
- def _get_internal_application_id
155
+ def _get_internal_application_id # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
138
156
  resource = RestClient::Resource.new(
139
157
  _internal_application_id_api_url,
140
158
  user: @options[:username],
141
159
  password: @options[:auth_token]
142
160
  )
143
161
  res = resource.get _headers
162
+ if res.code != 200
163
+ puts "failure reading application id: #{@options[:public_application_id]}. response status: #{res.code}"
164
+ return
165
+ end
144
166
  body = JSON.parse(res)
167
+ if body['applications'].empty?
168
+ puts "failed to get internal application id for IQ application id: #{@options[:public_application_id]}"
169
+ return
170
+ end
145
171
  body['applications'][0]['id']
146
172
  end
147
173
 
@@ -150,7 +176,9 @@ module Chelsea
150
176
  end
151
177
 
152
178
  def _api_url
179
+ # rubocop:disable Layout/LineLength
153
180
  "#{@options[:server_url]}/api/v2/scan/applications/#{@internal_application_id}/sources/chelsea?stageId=#{@options[:stage]}"
181
+ # rubocop:enable Layout/LineLength
154
182
  end
155
183
 
156
184
  def _internal_application_id_api_url
@@ -21,11 +21,12 @@ require 'rest-client'
21
21
  require_relative 'db'
22
22
 
23
23
  module Chelsea
24
+ # OSS Index audit operations
24
25
  class OSSIndex
25
26
  DEFAULT_OPTIONS = {
26
27
  oss_index_username: '',
27
28
  oss_index_user_token: ''
28
- }
29
+ }.freeze
29
30
  def initialize(options: DEFAULT_OPTIONS)
30
31
  @oss_index_user_name = options[:oss_index_user_name]
31
32
  @oss_index_user_token = options[:oss_index_user_token]
@@ -37,13 +38,11 @@ module Chelsea
37
38
 
38
39
  def get_vulns(coordinates)
39
40
  remaining_coordinates, cached_server_response = _cache(coordinates)
40
- unless remaining_coordinates['coordinates'].count.positive?
41
- return cached_server_response
42
- end
41
+ return cached_server_response unless remaining_coordinates['coordinates'].count.positive?
43
42
 
44
43
  remaining_coordinates['coordinates'].each_slice(128).to_a.each do |coords|
45
44
  res_json = JSON.parse(call_oss_index({ 'coordinates' => coords }))
46
- cached_server_response = cached_server_response.concat(res_json)
45
+ cached_server_response.concat(res_json)
47
46
  @db.save_values_to_db(res_json)
48
47
  end
49
48
 
@@ -57,15 +56,15 @@ module Chelsea
57
56
 
58
57
  private
59
58
 
60
- def _cache(coordinates)
59
+ def _cache(coordinates) # rubocop:disable Metrics/MethodLength
61
60
  new_coords = { 'coordinates' => [] }
62
61
  cached_server_response = []
63
62
  coordinates['coordinates'].each do |coord|
64
63
  record = @db.get_cached_value_from_db(coord)
65
- if !record.nil?
66
- cached_server_response << record
67
- else
64
+ if record.nil?
68
65
  new_coords['coordinates'].push(coord)
66
+ else
67
+ cached_server_response << record
69
68
  end
70
69
  end
71
70
  [new_coords, cached_server_response]