nexpose_ticketing 1.0.2 → 1.2.1

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
  SHA1:
3
- metadata.gz: f87999035739237fdbbb5c978292fb1327b95d35
4
- data.tar.gz: 6d917864b7f2a5d9246fc2214bbea37b464323e1
3
+ metadata.gz: 03778e753df032041d1c6a46bcef5f89df66d705
4
+ data.tar.gz: d163c3488d78c12ade992381b06479ba8ad40be9
5
5
  SHA512:
6
- metadata.gz: 3f8f1a3907e9ba7c3e556b75253c8833946a65c59efd62e06816f54853d894796f1bfd733323f94459ff0e2dada0d6d17a86549cc0e53a3fe1182ef5647a236f
7
- data.tar.gz: 374e3350847992ee22d5abc9377d64867f2670da2b6f5c48f256ef02e0d6906cc3e33af87ad5cf23aaeb9f397228364d1f3bf0c945065842228cc67d784d30ad
6
+ metadata.gz: 958e7e85146d2c2010b10723b06570203effb135017b3f280d8132d60fe5903d73b713a4d3bf9356f9dc63881067b52f8dde20e72546bad1565857bedb2284ae
7
+ data.tar.gz: ebaaa9c06a62105e3cb00425f1cc7e2ed93f67d2103f8224488da3f673b226dfd73abce6a72bb389869b6aa5bb4fcafad25167356167c0ef044b7f8018c61e08
data/README.md CHANGED
@@ -72,6 +72,28 @@ We welcome contributions to this package. We ask only that pull requests and pat
72
72
 
73
73
  ##Changelog
74
74
 
75
+ ###1.2.0
76
+
77
+ ####Configuration Options
78
+ Ticketing mode must be specified using the entire title, rather than a single character. e.g. 'Vulnerability' instead of 'V'
79
+ Added the following configuration option:
80
+ - log_console - NXLogger also gets printed to the console.
81
+
82
+ ####Ticketing Modes
83
+ Ticketing modes (Default, IP, Vulnerability) are now abstracted into their own classes.
84
+ CommonHelper has been replaced with BaseMode from which other modes are extended.
85
+
86
+ ####Ticketing Helpers
87
+ Ticketing helpers extend a BaseHelper class.
88
+ Ticketing helpers now log the number of tickets that are opened/closed/updated.
89
+
90
+ ###1.1.0
91
+ 10-02-2016
92
+ Added the following configuration options:
93
+ - max_ticket_length - Specifies a maximum length for the description field of a ticket.
94
+ - max_title_length - Specifies a maximum length for the title of a ticket.
95
+ - max_num_refs - Specifies the maximum number of references included in a vulnerability description.
96
+
75
97
  ###1.0.2
76
98
  08-02-2016
77
99
  - Encoding is now enforced as UTF-8 when parsing CSV files - fixes environment-specific errors.
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+ require 'yaml'
3
+ require 'nexpose_ticketing'
4
+ require 'nexpose_ticketing/nx_logger'
5
+ require 'nexpose_ticketing/version'
6
+ require 'optparse'
7
+
8
+ options = {}
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: nexpose_ticketing [service name]"
11
+ end.parse!
12
+
13
+ if ARGV.count == 0
14
+ puts 'Ticketing system name required.'
15
+ exit -1
16
+ end
17
+
18
+ system = ARGV.first
19
+ config_path = File.join(File.dirname(__FILE__),
20
+ "../lib/nexpose_ticketing/config/#{system}.config")
21
+
22
+ unless File.exists? config_path
23
+ puts "Configuration file could not be found at #{config_path}"
24
+ exit -1
25
+ end
26
+
27
+ # Read in JIRA options from jira.config.
28
+ service_options = begin
29
+ YAML.load_file(config_path)
30
+ rescue ArgumentError => e
31
+ raise "Could not parse YAML #{config_path} : #{e.message}"
32
+ end
33
+
34
+ log = NexposeTicketing::NxLogger.instance
35
+ log.setup_statistics_collection(service_options["vendor"],
36
+ service_options["product"],
37
+ NexposeTicketing::VERSION)
38
+ log.setup_logging(true, 'info')
39
+
40
+ current_encoding = Encoding.default_external=Encoding.find("UTF-8")
41
+
42
+ log.log_message("Current Encoding set to: #{current_encoding}")
43
+
44
+ # Initialize Ticket Service using JIRA.
45
+ NexposeTicketing.start(service_options)
@@ -2,7 +2,11 @@
2
2
  # This configuration file defines all the particular options necessary to support the helper.
3
3
  # Fields marked (M) are mandatory.
4
4
  #
5
- # (M)) Helper class name.
5
+ # (M) Product name
6
+ :product: JIRA
7
+ # (M) Vendor
8
+ :vendor: Atlassian
9
+ # (M) Helper class name.
6
10
  :helper_name: JiraHelper
7
11
  # Optional parameters, these are implementation specific.
8
12
  :jira_url: https://url/rest/api/2/issue/
@@ -2,7 +2,10 @@
2
2
  # This configuration file defines all the options necessary to support the helper.
3
3
  # Fields marked (M) are mandatory.
4
4
  #
5
-
5
+ # (M) Product name
6
+ :product: Remedy
7
+ # (M) Vendor
8
+ :vendor: bmc
6
9
  # (M) Helper class name
7
10
  :helper_name: RemedyHelper
8
11
 
@@ -2,7 +2,10 @@
2
2
  # This configuration file defines all the options necessary to support the helper.
3
3
  # Fields marked (M) are mandatory.
4
4
  #
5
-
5
+ # (M) Product name
6
+ :product: ServiceDesk
7
+ # (M) Vendor
8
+ :vendor: ManageEngine
6
9
  # (M) Helper class name
7
10
  :helper_name: ServiceDeskHelper
8
11
 
@@ -2,7 +2,10 @@
2
2
  # This configuration file defines all the options necessary to support the helper.
3
3
  # Fields marked (M) are mandatory.
4
4
  #
5
-
5
+ # (M) Product name
6
+ :product: ServiceNow
7
+ # (M) Vendor
8
+ :vendor: ServiceNow
6
9
  # (M) Helper class name
7
10
  :helper_name: ServiceNowHelper
8
11
 
@@ -8,20 +8,28 @@
8
8
  :logging_enabled: true
9
9
  # (M) Sets the log level threshold for output.
10
10
  :log_level: info
11
+ # Enables logger output to console.
12
+ :log_console: false
11
13
  # Filters the reports to specific sites one per line, leave empty for no site.
12
14
  :sites:
13
15
  - '1'
14
16
  # Minimum floor severity to report on. Number between 0 and 10.
15
17
  :severity: 8
16
18
  # (M) Name of the report historical file for sites saved in disk.
17
- :file_name: last_scan_data.csv
19
+ :site_file_name: last_scan_data.csv
18
20
  # (M) Name of the report historical file for tags saved in disk.
19
21
  :tag_file_name: tag_last_scan_data.csv
20
22
  # (M) Defines the ticket creation mode:
21
- # 'D' Default IP *-* Vulnerability
22
- # 'I' IP address -* Vulnerability
23
- # 'V' Vulnerability -* IP Address (Remedy helper only)
24
- :ticket_mode: I
23
+ # 'Default' Default IP *-* Vulnerability
24
+ # 'IP' IP address -* Vulnerability
25
+ # 'Vulnerability' Vulnerability -* IP Address
26
+ :ticket_mode: IP
27
+ # The maximum character length of a ticket description (-1 is unbounded)
28
+ :max_ticket_length: -1
29
+ # The maximum character length of a ticket title, including elipsis (...)
30
+ :max_title_length: 100
31
+ # The number of references to include in the ticket description
32
+ :max_num_refs: 3
25
33
  # Timeout in seconds. The number of seconds the GEM waits for a response from Nexpose before exiting.
26
34
  :timeout: 10800
27
35
  # Ticket batching. Breaks ticket processing into groups of value size controlling resource utilisation of both systems.
@@ -0,0 +1,43 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'uri'
5
+ require 'csv'
6
+ require 'nexpose_ticketing/nx_logger'
7
+ require 'nexpose_ticketing/version'
8
+ require_relative '../ticket_metrics'
9
+
10
+ class BaseHelper
11
+ attr_accessor :service_data, :options
12
+
13
+ def initialize(service_data, options, mode)
14
+ @service_data = service_data
15
+ @options = options
16
+ @log = NexposeTicketing::NxLogger.instance
17
+ @metrics = NexposeTicketing::TicketMetrics.new
18
+
19
+ load_dependencies
20
+ @mode_helper = mode
21
+ end
22
+
23
+ # Load the mode helper specified in the config
24
+ def load_dependencies
25
+ file = "#{@options[:ticket_mode]}_mode.rb".downcase
26
+ path = File.join(File.dirname(__FILE__), "../modes/#{file}")
27
+
28
+ @log.log_message("Loading #{@options[:ticket_mode]} mode dependencies.")
29
+ begin
30
+ require_relative path
31
+ rescue => e
32
+ error = "Ticket mode dependency '#{file}' could not be loaded."
33
+ @log.log_error_message e.to_s
34
+ @log.log_error_message error
35
+ fail error
36
+ end
37
+ end
38
+
39
+ # Performs any necessary clean-up
40
+ def finish
41
+ @metrics.finish
42
+ end
43
+ end
@@ -5,20 +5,15 @@ require 'uri'
5
5
  require 'csv'
6
6
  require 'nexpose_ticketing/nx_logger'
7
7
  require 'nexpose_ticketing/version'
8
- require 'nexpose_ticketing/common_helper'
8
+ require_relative './base_helper'
9
9
 
10
10
  # This class serves as the JIRA interface
11
11
  # that creates issues within JIRA from vulnerabilities
12
12
  # found in Nexpose.
13
13
  # Copyright:: Copyright (c) 2014 Rapid7, LLC.
14
- class JiraHelper
15
- attr_accessor :jira_data, :options
16
- def initialize(jira_data, options)
17
- @jira_data = jira_data
18
- @options = options
19
- @log = NexposeTicketing::NxLogger.instance
20
-
21
- @common_helper = NexposeTicketing::CommonHelper.new(@options)
14
+ class JiraHelper < BaseHelper
15
+ def initialize(service_data, options, mode)
16
+ super(service_data, options, mode)
22
17
  end
23
18
 
24
19
  # Fetches the Jira ticket key e.g INT-1. This is required to post updates to the Jira.
@@ -29,24 +24,34 @@ class JiraHelper
29
24
  # * *Returns* :
30
25
  # - Jira ticket key if found, nil otherwise.
31
26
  #
32
- def get_jira_key(jql_query)
27
+ def get_jira_key(jql_query, nxid = nil)
33
28
  fail 'JQL query string cannot be empty.' if jql_query.empty?
34
29
  headers = { 'Content-Type' => 'application/json',
35
30
  'Accept' => 'application/json' }
36
31
 
37
- uri = URI.parse(("#{@jira_data[:jira_url]}".split("/")[0..-2].join('/') + '/search'))
32
+ uri = URI.parse(("#{@service_data[:jira_url]}".split("/")[0..-2].join('/') + '/search'))
38
33
  uri.query = [uri.query, URI.escape(jql_query)].compact.join('&')
39
34
  req = Net::HTTP::Get.new(uri.to_s, headers)
40
35
  response = send_jira_request(uri, req)
41
36
 
42
37
  issues = JSON.parse(response.body)['issues']
43
- if issues.nil? || !issues.any? || issues.size > 1
44
- # If Jira returns more than one key for a "unique" NXID query result then something has gone wrong...
45
- # Safest response is to return no key and let logic elsewhere dictate the action to take.
46
- @log.log_message("Jira returned no key or too many keys for query result! Response was <#{issues}>")
47
- return nil
48
- end
49
- return issues[0]['key']
38
+
39
+ if issues.nil? || !issues.any?
40
+ @log.log_message "JIRA did not return any keys for query containing NXID #{nxid}"
41
+ return nil
42
+ end
43
+
44
+ if issues.size > 1
45
+ # If Jira returns more than one key for a "unique" NXID query result then something has gone wrong...
46
+ # Safest response is to return no key and let logic elsewhere dictate the action to take.
47
+ error = "Jira returned multiple keys for query containing NXID #{nxid}."
48
+ error += " Please check project within JIRA."
49
+ error += " Response was <#{issues}>"
50
+ @log.log_error_message(error)
51
+ return nil
52
+ end
53
+
54
+ issues[0]['key']
50
55
  end
51
56
 
52
57
  # Sends a HTTP request to the JIRA console.
@@ -59,7 +64,7 @@ class JiraHelper
59
64
  # - HTTPResponse containing result from the JIRA console.
60
65
  #
61
66
  def send_request(uri, request, ticket=false)
62
- request.basic_auth @jira_data[:username], @jira_data[:password]
67
+ request.basic_auth @service_data[:username], @service_data[:password]
63
68
  resp = Net::HTTP.new(uri.host, uri.port)
64
69
 
65
70
  # Enable this line for debugging the https call.
@@ -72,9 +77,39 @@ class JiraHelper
72
77
 
73
78
  resp.start do |http|
74
79
  res = http.request(request)
75
- next if res.code.to_i.between?(200,299)
76
- @log.log_error_message("Error submitting ticket data: #{res.message}, #{res.body}")
77
- res
80
+ code = res.code.to_i
81
+
82
+ next if code.between?(200,299)
83
+
84
+ unless code.between?(400, 499)
85
+ @log.log_error_message("Error submitting ticket data: #{res.message}, #{res.body}")
86
+ return res
87
+ end
88
+
89
+ @log.log_error_message("Unable to access JIRA.")
90
+ @log.log_error_message "Error code: #{code}"
91
+
92
+ #Bad project etc
93
+ case code
94
+ when 400
95
+ errors = res.body.scan(/errors":{(.+)}}/).first.first
96
+ errors = errors.gsub('"', '').gsub(':', ': ').gsub(',', "\n")
97
+ @log.log_error_message "Error messages:\n#{errors}"
98
+ #Log in failed
99
+ when 401
100
+ @log.log_error_message "Message: #{res.message.strip}"
101
+ @log.log_error_message "Reason: #{res['x-seraph-loginreason']}"
102
+ #Locked out
103
+ when 403
104
+ @log.log_error_message "Message: #{res.message.strip}"
105
+ @log.log_error_message "Reason: #{res['x-seraph-loginreason']}"
106
+ @log.log_error_message "#{res['x-authentication-denied-reason']}"
107
+ else
108
+ #e.g. 404 - bad URL
109
+ @log.log_error_message "Message: #{res.message.strip}"
110
+ end
111
+
112
+ return res
78
113
  end
79
114
  end
80
115
 
@@ -120,7 +155,7 @@ class JiraHelper
120
155
  headers = { 'Content-Type' => 'application/json',
121
156
  'Accept' => 'application/json' }
122
157
 
123
- uri = URI.parse(("#{@jira_data[:jira_url]}#{jira_key}/transitions?expand=transitions.fields."))
158
+ uri = URI.parse(("#{@service_data[:jira_url]}#{jira_key}/transitions?expand=transitions.fields."))
124
159
  req = Net::HTTP::Get.new(uri.to_s, headers)
125
160
  response = send_jira_request(uri, req)
126
161
 
@@ -133,42 +168,39 @@ class JiraHelper
133
168
  end
134
169
  end
135
170
  end
136
- error = "Response was <#{transitions}> and desired close Step ID was <#{@jira_data[:close_step_id]}>. Jira returned no valid transition to close the ticket!"
171
+ error = "Response was <#{transitions}> and desired close Step ID was <#{@service_data[:close_step_id]}>. Jira returned no valid transition to close the ticket!"
137
172
  @log.log_message(error)
138
173
  return nil
139
174
  end
140
175
 
141
176
  def create_tickets(tickets)
142
177
  fail 'Ticket(s) cannot be empty.' if tickets.nil? || tickets.empty?
178
+ created_tickets = 0
179
+
143
180
  tickets.each do |ticket|
144
181
  headers = { 'Content-Type' => 'application/json',
145
182
  'Accept' => 'application/json' }
146
183
 
147
- uri = URI.parse("#{@jira_data[:jira_url]}")
148
- req = Net::HTTP::Post.new(@jira_data[:jira_url], headers)
184
+ uri = URI.parse("#{@service_data[:jira_url]}")
185
+
186
+ req = Net::HTTP::Post.new(uri, headers)
149
187
  req.body = ticket
150
- send_ticket(uri, req)
188
+
189
+ code = send_ticket(uri, req).code.to_i
190
+ break if code.between?(400, 499)
191
+
192
+ created_tickets += 1
151
193
  end
194
+
195
+ @metrics.created created_tickets
152
196
  end
153
197
 
154
198
  # Prepares tickets from the CSV.
155
199
  def prepare_create_tickets(vulnerability_list, nexpose_identifier_id)
200
+ @metrics.start
156
201
  @log.log_message('Preparing ticket requests...')
157
- case @options[:ticket_mode]
158
- # 'D' Default IP *-* Vulnerability
159
- when 'D' then matching_fields = ['ip_address', 'vulnerability_id']
160
- # 'I' IP address -* Vulnerability
161
- when 'I' then matching_fields = ['ip_address']
162
- # 'V' Vulnerability -* Assets
163
- when 'V' then matching_fields = ['vulnerability_id']
164
- else
165
- fail 'Unsupported ticketing mode selected.'
166
- end
167
-
168
- prepare_tickets(vulnerability_list, nexpose_identifier_id, matching_fields)
169
- end
170
-
171
- def prepare_tickets(vulnerability_list, nexpose_identifier_id, matching_fields)
202
+ matching_fields = @mode_helper.get_matching_fields
203
+
172
204
  @ticket = Hash.new(-1)
173
205
 
174
206
  @log.log_message("Preparing tickets for #{@options[:ticket_mode]} mode.")
@@ -182,34 +214,35 @@ class JiraHelper
182
214
  @ticket = {
183
215
  'fields' => {
184
216
  'project' => {
185
- 'key' => "#{@jira_data[:project]}" },
186
- 'summary' => @common_helper.get_title(row),
217
+ 'key' => "#{@service_data[:project]}" },
218
+ 'summary' => @mode_helper.get_title(row),
187
219
  'description' => '',
188
220
  'issuetype' => {
189
221
  'name' => 'Task' }
190
222
  }
191
223
  }
192
- description = @common_helper.get_description(nexpose_identifier_id, row)
224
+ description = @mode_helper.get_description(nexpose_identifier_id, row)
193
225
  elsif matching_fields.any? { |x| previous_row[x].nil? || previous_row[x] != row[x] }
194
- info = @common_helper.get_field_info(matching_fields, previous_row)
226
+ info = @mode_helper.get_field_info(matching_fields, previous_row)
195
227
  @log.log_message("Generated ticket with #{info}")
196
228
 
197
- @ticket['fields']['description'] = @common_helper.print_description(description)
229
+ @ticket['fields']['description'] = @mode_helper.print_description(description)
198
230
  tickets.push(@ticket.to_json)
199
231
  previous_row = nil
200
232
  description = nil
201
233
  redo
202
234
  else
203
- description = @common_helper.update_description(description, row)
235
+ description = @mode_helper.update_description(description, row)
204
236
  end
205
237
  end
206
238
 
207
239
  unless @ticket.nil? || @ticket.empty?
208
- @ticket['fields']['description'] = @common_helper.print_description(description)
240
+ info = @mode_helper.get_field_info(matching_fields, previous_row)
241
+ @log.log_message("Generated ticket with #{info}")
242
+ @ticket['fields']['description'] = @mode_helper.print_description(description)
209
243
  tickets.push(@ticket.to_json)
210
244
  end
211
245
 
212
- @log.log_message("Generated <#{tickets.count.to_s}> tickets.")
213
246
  tickets
214
247
  end
215
248
 
@@ -222,43 +255,48 @@ class JiraHelper
222
255
  def close_tickets(tickets)
223
256
  if tickets.nil? || tickets.empty?
224
257
  @log.log_message('No tickets to close.')
225
- else
226
- headers = { 'Content-Type' => 'application/json',
227
- 'Accept' => 'application/json' }
258
+ return
259
+ end
260
+ closed_count = 0
228
261
 
229
- tickets.each do |ticket|
230
- uri = URI.parse(("#{@jira_data[:jira_url]}#{ticket}/transitions"))
231
- req = Net::HTTP::Post.new(uri.to_s, headers)
262
+ headers = { 'Content-Type' => 'application/json',
263
+ 'Accept' => 'application/json' }
232
264
 
233
- transition = get_jira_transition_details(ticket, @jira_data[:close_step_id])
234
- if transition.nil?
235
- #Valid transition could not be found. Ignore ticket since we do not know what to do with it.
236
- @log.log_message("No valid transition found for ticket <#{ticket}>. Skipping closure.")
237
- next
238
- end
265
+ tickets.each do |ticket|
266
+ uri = URI.parse(("#{@service_data[:jira_url]}#{ticket}/transitions"))
267
+ req = Net::HTTP::Post.new(uri.to_s, headers)
239
268
 
240
- #We need to find any required fields to send with the transition request
241
- required_fields = []
242
- transition['fields'].each do |field|
243
- if field[1]['required'] == true
244
- # Currently only required fields with 'allowedValues' in the JSON response are supported.
245
- if not field[1].has_key? 'allowedValues'
246
- @log.log_message("Closing ticket <#{ticket}> requires a field I know nothing about! Transition details are <#{transition}>. Ignoring this field.")
247
- next
248
- else
249
- if field[1]['schema']['type'] == 'array'
250
- required_fields << "\"#{field[0]}\" : [{\"id\" : \"#{field[1]['allowedValues'][0]['id']}\"}]"
251
- else
252
- required_fields << "\"#{field[0]}\" : {\"id\" : \"#{field[1]['allowedValues'][0]['id']}\"}"
253
- end
254
- end
255
- end
269
+ transition = get_jira_transition_details(ticket, @service_data[:close_step_id])
270
+ if transition.nil?
271
+ #Valid transition could not be found. Ignore ticket since we do not know what to do with it.
272
+ @log.log_message("No valid transition found for ticket <#{ticket}>. Skipping closure.")
273
+ next
274
+ end
275
+
276
+ #We need to find any required fields to send with the transition request
277
+ required_fields = []
278
+ transition['fields'].each do |field|
279
+ next unless field[1]['required'] == true
280
+
281
+ # Currently only required fields with 'allowedValues' in the JSON response are supported.
282
+ if not field[1].has_key? 'allowedValues'
283
+ @log.log_message("Closing ticket <#{ticket}> requires a field I know nothing about! Transition details are <#{transition}>. Ignoring this field.")
284
+ next
256
285
  end
257
286
 
258
- req.body = "{\"transition\" : {\"id\" : #{transition['id']}}, \"fields\" : { #{required_fields.join(",")}}}"
259
- send_ticket(uri, req)
287
+ field = "{\"id\" : \"#{field[1]['allowedValues'][0]['id']}\"}"
288
+ field = "[#{field}]" if field[1]['schema']['type'] == 'array'
289
+ required_fields << "\"#{field[0]}\" : #{field}"
260
290
  end
291
+
292
+ req.body = "{\"transition\" : {\"id\" : #{transition['id']}}, \"fields\" : { #{required_fields.join(",")}}}"
293
+ code = send_ticket(uri, req).code.to_i
294
+ break if code.between?(400, 499)
295
+
296
+ closed_count += 1
261
297
  end
298
+
299
+ @metrics.closed closed_count
262
300
  end
263
301
 
264
302
  # Prepare ticket closures from the CSV of vulnerabilities exported from Nexpose.
@@ -274,9 +312,10 @@ class JiraHelper
274
312
  @nxid = nil
275
313
  tickets = []
276
314
  CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
277
- @nxid = @common_helper.generate_nxid(nexpose_identifier_id, row)
315
+ @nxid = @mode_helper.get_nxid(nexpose_identifier_id, row)
278
316
  # Query Jira for the ticket by unique id (generated NXID)
279
- queried_key = get_jira_key("jql=project=#{@jira_data[:project]} AND description ~ \"NXID: #{@nxid}\" AND (status != #{@jira_data[:close_step_name]})&fields=key")
317
+ query_string = "jql=project=#{@service_data[:project]} AND description ~ \"NXID: #{@nxid}\" AND (status != #{@service_data[:close_step_name]})&fields=key"
318
+ queried_key = get_jira_key(query_string, @nxid)
280
319
  if queried_key.nil? || queried_key.empty?
281
320
  @log.log_message("Error when closing tickets - query for NXID <#{@nxid}> should have returned a Jira key!!")
282
321
  else
@@ -284,6 +323,7 @@ class JiraHelper
284
323
  tickets.push(queried_key)
285
324
  end
286
325
  end
326
+
287
327
  tickets
288
328
  end
289
329
 
@@ -300,20 +340,28 @@ class JiraHelper
300
340
  tickets.each do |ticket_details|
301
341
  headers = {'Content-Type' => 'application/json',
302
342
  'Accept' => 'application/json'}
343
+
344
+ create_new_ticket = ticket_details.first.nil?
345
+
346
+ url = "#{service_data[:jira_url]}"
347
+
348
+ if create_new_ticket
349
+ req = Net::HTTP::Post.new(url, headers)
350
+ req.body = ticket_details.last
351
+ else
352
+ url += "#{ticket_details[0]}"
353
+ req = Net::HTTP::Put.new(url, headers)
354
+ req.body = {'update' => {'description' => [{'set' => "#{JSON.parse(ticket_details[1])['fields']['description']}"}]}}.to_json
355
+ end
303
356
 
304
- (ticket_details.first.nil?) ? send_whole_ticket = true : send_whole_ticket = false
305
-
306
- url = "#{jira_data[:jira_url]}"
307
- url += "#{ticket_details.first}" unless send_whole_ticket
308
- uri = URI.parse(url)
309
-
310
- send_whole_ticket ? req = Net::HTTP::Post.new(uri.to_s, headers) : req = Net::HTTP::Put.new(uri.to_s, headers)
311
-
312
- send_whole_ticket ?
313
- req.body = ticket_details.last :
314
- req.body = {'update' => {'description' => [{'set' => "#{JSON.parse(ticket_details[1])['fields']['description']}"}]}}.to_json
315
-
316
- send_ticket(uri, req)
357
+ code = send_ticket(URI.parse(url), req).code.to_i
358
+ break if code.between?(400, 499)
359
+
360
+ if create_new_ticket
361
+ @metrics.created
362
+ else
363
+ @metrics.updated
364
+ end
317
365
  end
318
366
  end
319
367
  end
@@ -327,7 +375,9 @@ class JiraHelper
327
375
  # - List of JSON-formated tickets for updating within Jira.
328
376
  #
329
377
  def prepare_update_tickets(vulnerability_list, nexpose_identifier_id)
330
- fail 'Ticket updates are not supported in Default mode.' if @options[:ticket_mode] == 'D'
378
+ @metrics.start
379
+ return unless @mode_helper.updates_supported?
380
+
331
381
  @log.log_message('Preparing tickets to update.')
332
382
  #Jira uses the ticket key to push updates. Since new IPs won't have a Jira key, generate new tickets for all of the IPs found.
333
383
  updated_tickets = prepare_create_tickets(vulnerability_list, nexpose_identifier_id)
@@ -345,7 +395,9 @@ class JiraHelper
345
395
  @log.log_message("Failed to parse the NXID from a generated ticket update! Ignoring ticket <#{nxid}>")
346
396
  next
347
397
  end
348
- queried_key = get_jira_key("jql=project=#{@jira_data[:project]} AND description ~ \"#{nxid.strip}\" AND (status != #{@jira_data[:close_step_name]})&fields=key")
398
+
399
+ query_string = "jql=project=#{@service_data[:project]} AND description ~ \"#{nxid.strip}\" AND (status != #{@service_data[:close_step_name]})&fields=key"
400
+ queried_key = get_jira_key(query_string, nxid)
349
401
  ticket_key_pair = []
350
402
  ticket_key_pair << queried_key
351
403
  ticket_key_pair << ticket