nexpose_ticketing 1.0.2 → 1.2.1

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