nexpose_ticketing 0.8.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,6 +5,8 @@ require 'uri'
5
5
  require 'csv'
6
6
  require 'savon'
7
7
  require 'nexpose_ticketing/nx_logger'
8
+ require 'nexpose_ticketing/version'
9
+ require 'nexpose_ticketing/common_helper'
8
10
 
9
11
  # Serves as the Remedy interface for creating/updating issues from
10
12
  # vulnelrabilities found in Nexpose.
@@ -13,42 +15,90 @@ class RemedyHelper
13
15
  def initialize(remedy_data, options)
14
16
  @remedy_data = remedy_data
15
17
  @options = options
16
- @log = NexposeTicketing::NXLogger.new
18
+ @log = NexposeTicketing::NxLogger.instance
19
+ @common_helper = NexposeTicketing::CommonHelper.new(@options)
20
+ end
21
+
22
+ # Generates a savon-based ticket object.
23
+ #
24
+ # * *Args* :
25
+ # - +extra_fields+ - List of mode-specific fields (hash) to be added to the ticket.
26
+ #
27
+ def generate_new_ticket(extra_fields=nil)
28
+ base_ticket = {
29
+ 'First_Name' => "#{@remedy_data[:first_name]}",
30
+ 'Impact' => '1-Extensive/Widespread',
31
+ 'Last_Name' => "#{@remedy_data[:last_name]}",
32
+ 'Reported_Source' => 'Other',
33
+ 'Service_Type' => 'Infrastructure Event',
34
+ 'Status' => 'New',
35
+ 'Action' => 'CREATE',
36
+ "Summary"=>"",
37
+ "Notes"=>"",
38
+ 'Urgency' => '1-Critical',
39
+ }
40
+ extra_fields.each { |k, v| base_ticket[k.to_s] = v } if extra_fields
41
+ base_ticket
42
+ end
43
+
44
+ # Sends a list of tickets (in SOAP format) to Remedy individually (each ticket in the list
45
+ # as a separate web service call).
46
+ #
47
+ # * *Args* :
48
+ # - +wdsl+ - XML file which describes the network service.
49
+ # - +endpoint+ - Endpoint to which the data will be submitted.
50
+ #
51
+ def get_client(wdsl, endpoint)
52
+ Savon.client(wsdl: File.join(File.dirname(__FILE__), "../config/remedy_wsdl/#{wdsl}"),
53
+ adapter: :net_http,
54
+ ssl_verify_mode: :none,
55
+ open_timeout: @remedy_data[:open_timeout],
56
+ read_timeout: @remedy_data[:read_timeout],
57
+ endpoint: @remedy_data[endpoint.intern],
58
+ soap_header: { 'AuthenticationInfo' =>
59
+ { 'userName' => "#{@remedy_data[:username]}",
60
+ 'password' => "#{@remedy_data[:password]}",
61
+ 'authentication' => "#{@remedy_data[:authentication]}"
62
+ }
63
+ })
17
64
  end
18
65
 
19
66
  # Sends a list of tickets (in SOAP format) to Remedy individually (each ticket in the list
20
67
  # as a separate web service call).
21
68
  #
22
69
  # * *Args* :
70
+ # - +service+ - The helpdesk service to which the tickets should be submitted.
23
71
  # - +tickets+ - List of savon-formatted (hash) ticket creates (new tickets).
24
72
  #
25
- def create_tickets(tickets)
26
- fail 'Ticket(s) cannot be empty' if tickets.nil? || tickets.empty?
27
- client = Savon.client(wsdl: File.join(File.dirname(__FILE__), '../config/remedy_wsdl/HPD_IncidentInterface_Create_WS.xml'),
28
- ssl_verify_mode: :none,
29
- open_timeout: @remedy_data[:open_timeout],
30
- read_timeout: @remedy_data[:read_timeout],
31
- endpoint: @remedy_data[:create_soap_endpoint],
32
- soap_header: { 'AuthenticationInfo' =>
33
- { 'userName' => "#{@remedy_data[:username]}",
34
- 'password' => "#{@remedy_data[:password]}",
35
- 'authentication' => "#{@remedy_data[:authentication]}"
36
- }
37
- })
73
+ def send_tickets(client, service, tickets)
74
+ service_name = service.to_s.match(/desk_([a-z]*)_/)
75
+ service_name = service_name.captures.first unless service_name.nil?
38
76
  tickets.each do |ticket|
39
77
  begin
40
78
  @log.log_message(ticket)
41
- response = client.call(:help_desk_submit_service, message: ticket)
79
+ response = client.call(service, message: ticket)
42
80
  rescue Savon::SOAPFault => e
43
- @log.log_message("SOAP exception in create ticket: #{e.message}")
81
+ @log.log_message("SOAP exception in #{service_name} ticket: #{e.message}")
44
82
  raise
45
83
  rescue Savon::HTTPError => e
46
- @log.log_message("HTTP error in create ticket: #{e.message}")
84
+ @log.log_message("HTTP error in #{service_name} ticket: #{e.message}")
47
85
  raise
48
86
  end
49
87
  end
50
88
  end
51
-
89
+
90
+ # Sends a list of tickets (in SOAP format) to Remedy individually (each ticket in the list
91
+ # as a separate web service call).
92
+ #
93
+ # * *Args* :
94
+ # - +tickets+ - List of savon-formatted (hash) ticket creates (new tickets).
95
+ #
96
+ def create_tickets(tickets)
97
+ fail 'Ticket(s) cannot be empty' if tickets.nil? || tickets.empty?
98
+ client = get_client('HPD_IncidentInterface_Create_WS.xml', :create_soap_endpoint)
99
+ send_tickets(client, :help_desk_submit_service, tickets)
100
+ end
101
+
52
102
  # Sends ticket updates (in SOAP format) to Remedy individually (each ticket in the list
53
103
  # as a separate web service call).
54
104
  #
@@ -58,30 +108,10 @@ class RemedyHelper
58
108
  def update_tickets(tickets)
59
109
  if tickets.nil? || tickets.empty?
60
110
  @log.log_message("No tickets to update.")
61
- else
62
- client = Savon.client(wsdl: File.join(File.dirname(__FILE__), '../config/remedy_wsdl/HPD_IncidentInterface_WS.xml'),
63
- ssl_verify_mode: :none,
64
- open_timeout: @remedy_data[:open_timeout],
65
- read_timeout: @remedy_data[:read_timeout],
66
- endpoint: @remedy_data[:query_modify_soap_endpoint],
67
- soap_header: { 'AuthenticationInfo' =>
68
- { 'userName' => "#{@remedy_data[:username]}",
69
- 'password' => "#{@remedy_data[:password]}",
70
- 'authentication' => "#{@remedy_data[:authentication]}"
71
- }
72
- })
73
- tickets.each do |ticket|
74
- begin
75
- response = client.call(:help_desk_modify_service, message: ticket)
76
- rescue Savon::SOAPFault => e
77
- @log.log_message("SOAP exception in create ticket: #{e.message}")
78
- raise
79
- rescue Savon::HTTPError => e
80
- @log.log_message("HTTP error in create ticket: #{e.message}")
81
- raise
82
- end
83
- end
111
+ return
84
112
  end
113
+ client = get_client('HPD_IncidentInterface_WS.xml', :query_modify_soap_endpoint)
114
+ send_tickets(client, :help_desk_modify_service, tickets)
85
115
  end
86
116
 
87
117
  # Sends ticket closure (in SOAP format) to Remedy individually (each ticket in the list
@@ -93,30 +123,10 @@ class RemedyHelper
93
123
  def close_tickets(tickets)
94
124
  if tickets.nil? || tickets.empty?
95
125
  @log.log_message("No tickets to close.")
96
- else
97
- client = Savon.client(wsdl: File.join(File.dirname(__FILE__), '../config/remedy_wsdl/HPD_IncidentInterface_WS.xml'),
98
- ssl_verify_mode: :none,
99
- open_timeout: @remedy_data[:open_timeout],
100
- read_timeout: @remedy_data[:read_timeout],
101
- endpoint: @remedy_data[:query_modify_soap_endpoint],
102
- soap_header: { 'AuthenticationInfo' =>
103
- { 'userName' => "#{@remedy_data[:username]}",
104
- 'password' => "#{@remedy_data[:password]}",
105
- 'authentication' => "#{@remedy_data[:authentication]}"
106
- }
107
- })
108
- tickets.each do |ticket|
109
- begin
110
- response = client.call(:help_desk_modify_service, message: ticket)
111
- rescue Savon::SOAPFault => e
112
- @log.log_message("SOAP exception in create ticket: #{e.message}")
113
- raise
114
- rescue Savon::HTTPError => e
115
- @log.log_message("HTTP error in create ticket: #{e.message}")
116
- raise
117
- end
118
- end
126
+ return
119
127
  end
128
+ client = get_client('HPD_IncidentInterface_WS.xml', :query_modify_soap_endpoint)
129
+ send_tickets(client, :help_desk_modify_service, tickets)
120
130
  end
121
131
 
122
132
  # Sends a query (in SOAP format) to Remedy to return back a single ticket based on the criteria.
@@ -128,19 +138,10 @@ class RemedyHelper
128
138
  # - Remedy incident information in hash format or nil if no results are found.
129
139
  #
130
140
  def query_for_ticket(unique_id)
131
- client = Savon.client(wsdl: File.join(File.dirname(__FILE__), '../config/remedy_wsdl/HPD_IncidentInterface_WS.xml'),
132
- ssl_verify_mode: :none,
133
- open_timeout: @remedy_data[:open_timeout],
134
- read_timeout: @remedy_data[:read_timeout],
135
- endpoint: @remedy_data[:query_modify_soap_endpoint],
136
- soap_header: { 'AuthenticationInfo' =>
137
- { 'userName' => "#{@remedy_data[:username]}",
138
- 'password' => "#{@remedy_data[:password]}",
139
- 'authentication' => "#{@remedy_data[:authentication]}"
140
- }
141
- })
141
+ client = get_client('HPD_IncidentInterface_WS.xml', :query_modify_soap_endpoint)
142
+
142
143
  begin
143
- response = client.call(:help_desk_query_list_service, message: {'Qualification' => "'Detailed Decription' LIKE \"%#{unique_id}%\""})
144
+ response = client.call(:help_desk_query_list_service, message: {'Qualification' => "'Status' < \"Closed\" AND 'Detailed Decription' LIKE \"%#{unique_id}%\""})
144
145
  rescue Savon::SOAPFault => e
145
146
  @log.log_message("SOAP exception in query ticket: #{e.message}")
146
147
  return if e.to_hash[:fault][:faultstring].index("ERROR (302)") == 0
@@ -163,20 +164,19 @@ class RemedyHelper
163
164
  # - List of savon-formated (hash) tickets for creating within Remedy.
164
165
  #
165
166
  def prepare_create_tickets(vulnerability_list, nexpose_identifier_id)
166
- @ticket = Hash.new(-1)
167
+ @log.log_message('Preparing ticket requests...')
167
168
  case @options[:ticket_mode]
168
- # 'D' Default mode: IP *-* Vulnerability
169
- when 'D'
170
- prepare_create_tickets_default(vulnerability_list, nexpose_identifier_id)
171
- # 'I' IP address mode: IP address -* Vulnerability
172
- when 'I'
173
- prepare_create_tickets_by_ip(vulnerability_list, nexpose_identifier_id)
174
- # 'V' Vulnerability mode: Vulnerability -* IP address
175
- when 'V'
176
- prepare_create_tickets_by_vulnerability(vulnerability_list, nexpose_identifier_id)
169
+ # 'D' Default IP *-* Vulnerability
170
+ when 'D' then matching_fields = ['ip_address', 'vulnerability_id']
171
+ # 'I' IP address -* Vulnerability
172
+ when 'I' then matching_fields = ['ip_address']
173
+ # 'V' Vulnerability -* Assets
174
+ when 'V' then matching_fields = ['vulnerability_id']
177
175
  else
178
- fail 'No ticketing mode selected.'
176
+ fail 'Unsupported ticketing mode selected.'
179
177
  end
178
+
179
+ prepare_tickets(vulnerability_list, nexpose_identifier_id, matching_fields)
180
180
  end
181
181
 
182
182
  # Prepare to update tickets from the CSV of vulnerabilities exported from Nexpose. This method determines
@@ -189,113 +189,22 @@ class RemedyHelper
189
189
  # - List of savon-formated (hash) tickets for creating within Remedy.
190
190
  #
191
191
  def prepare_update_tickets(vulnerability_list, nexpose_identifier_id)
192
- @ticket = Hash.new(-1)
193
192
  case @options[:ticket_mode]
194
- # 'I' IP address mode: IP address -* Vulnerability
195
- when 'I'
196
- prepare_update_tickets_by_ip(vulnerability_list, nexpose_identifier_id)
197
- # 'V' Vulnerability mode: Vulnerability -* IP address
198
- when 'V'
199
- prepare_update_tickets_by_vulnerability(vulnerability_list, nexpose_identifier_id)
200
- else
201
- fail 'No ticketing mode selected.'
202
- end
203
- end
204
-
205
- # Prepares a list of vulnerabilities into a list of savon-formatted tickets (incidents) for
206
- # Remedy. The preparation by default means that each vulnerability within Nexpose is a
207
- # separate incident within Remedy. This makes for smaller, more actionalble incidents but
208
- # could lead to a very large total number of incidents.
209
- #
210
- # * *Args* :
211
- # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
212
- #
213
- # * *Returns* :
214
- # - List of savon-formated (hash) tickets for creating within Remedy.
215
- #
216
- def prepare_create_tickets_default(vulnerability_list, nexpose_identifier_id)
217
- @log.log_message("Preparing tickets by default method.")
218
- tickets = []
219
- CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
220
- # NXID in the notes is a unique identifier used to query incidents to update/resolve
221
- # incidents as they are resolved in Nexpose.
222
- ticket = {
223
- 'First_Name' => "#{@remedy_data[:first_name]}",
224
- 'Impact' => '1-Extensive/Widespread',
225
- 'Last_Name' => "#{@remedy_data[:last_name]}",
226
- 'Reported_Source' => 'Other',
227
- 'Service_Type' => 'Infrastructure Event',
228
- 'Status' => 'New',
229
- 'Action' => 'CREATE',
230
- 'Summary' => "#{row['ip_address']} => #{row['summary']}",
231
- 'Notes' => "Summary: #{row['summary']} \n\nFix: #{row['fix']} \n\nURL: #{row['url']}
232
- \n\nNXID: #{nexpose_identifier_id}#{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}",
233
- 'Urgency' => '1-Critical'
234
- }
235
- tickets.push(ticket)
193
+ # 'D' Default IP *-* Vulnerability
194
+ when 'D' then fail 'Ticket updates are not supported in Default mode.'
195
+ # 'I' IP address -* Vulnerability
196
+ when 'I' then matching_fields = ['ip_address']
197
+ # 'V' Vulnerability -* Assets
198
+ when 'V' then matching_fields = ['vulnerability_id']
199
+ else
200
+ fail 'Unsupported ticketing mode selected.'
236
201
  end
237
- tickets
202
+
203
+ prepare_tickets(vulnerability_list, nexpose_identifier_id, matching_fields)
238
204
  end
239
205
 
240
206
  # Prepares a list of vulnerabilities into a list of savon-formatted tickets (incidents) for
241
- # Remedy. The preparation by IP means that all vulnerabilities within Nexpose for one IP
242
- # address are consolidated into a single Remedy incident. This reduces the number of incidents
243
- # within ServiceNow but greatly increases the size of the work notes.
244
- #
245
- # * *Args* :
246
- # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
247
- #
248
- # * *Returns* :
249
- # - List of savon-formated (hash) tickets for creating within Remedy.
250
- #
251
- def prepare_create_tickets_by_ip(vulnerability_list, nexpose_identifier_id)
252
- @log.log_message('Preparing tickets by IP address.')
253
- tickets = []
254
- current_ip = -1
255
- CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
256
- if current_ip == -1
257
- current_ip = row['ip_address']
258
- @log.log_message("Creating ticket with IP address: #{row['ip_address']}, Asset ID: #{row['asset_id']} and Nexpose Identifier ID: #{nexpose_identifier_id}")
259
- @ticket = {
260
- 'First_Name' => "#{@remedy_data[:first_name]}",
261
- 'Impact' => '1-Extensive/Widespread',
262
- 'Last_Name' => "#{@remedy_data[:last_name]}",
263
- 'Reported_Source' => 'Other',
264
- 'Service_Type' => 'Infrastructure Event',
265
- 'Status' => 'New',
266
- 'Action' => 'CREATE',
267
- 'Summary' => "#{row['ip_address']} => Vulnerabilities",
268
- 'Notes' => "++ New Vulnerabilities +++++++++++++++++++++++++++++++++++++\n",
269
- 'Urgency' => '1-Critical'
270
- }
271
- end
272
- if current_ip == row['ip_address']
273
- @ticket['Notes'] +=
274
- "\n\n========================================== \nSummary: #{row['summary']} \nFix: #{row['fix']}"
275
- unless row['url'].nil?
276
- @ticket['Notes'] +=
277
- "\nURL: #{row['url']}"
278
- end
279
- end
280
- unless current_ip == row['ip_address']
281
- # NXID in the work_notes is the unique identifier used to query incidents to update them.
282
- @ticket['Notes'] += "\n\nNXID: #{nexpose_identifier_id}#{current_ip}"
283
- tickets.push(@ticket)
284
- current_ip = -1
285
- redo
286
- end
287
- end
288
- # NXID in the work_notes is the unique identifier used to query incidents to update them.
289
- @ticket['Notes'] += "\n\nNXID: #{nexpose_identifier_id}#{current_ip}"
290
- tickets.push(@ticket) unless @ticket.nil?
291
- tickets
292
- end
293
-
294
-
295
- # Prepares a list of vulnerabilities into a list of savon-formatted tickets (incidents) for
296
- # Remedy. The preparation by vulnerability means that all IP addresses within Nexpose for one vulnerability
297
- # are consolidated into a single Remedy incident. This reduces the number of incidents
298
- # within ServiceNow but greatly increases the size of the work notes.
207
+ # Remedy.
299
208
  #
300
209
  # * *Args* :
301
210
  # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
@@ -303,266 +212,131 @@ class RemedyHelper
303
212
  # * *Returns* :
304
213
  # - List of savon-formated (hash) tickets for creating within Remedy.
305
214
  #
306
- def prepare_create_tickets_by_vulnerability(vulnerability_list, nexpose_identifier_id)
307
- @log.log_message("Preparing tickets by vulnerability.")
308
- tickets = []
309
- current_vuln_id = -1
310
- current_solution_id = -1
311
- current_asset_id = -1
312
- full_summary = nil
313
- vulnerability_list_header = vulnerability_list[0]
314
- CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
315
- #New vulnerability ID (the query sorting criteria).
316
- if current_vuln_id == -1
317
- current_vuln_id = row['vulnerability_id']
318
- current_solution_id = row['solution_id']
319
- current_asset_id = row['asset_id']
320
- @log.log_message("Creating ticket with vulnerability id: #{row['vulnerability_id']}, Asset ID: #{row['asset_id']} and Nexpose Identifier ID: #{nexpose_identifier_id}")
321
- summary = "Vulnerability: #{row['title']}"
322
-
323
- #Remedy has a summary field max size of 100 so truncate any summaries that are larger than that and place the full summary in the notes.
324
- if summary.length > 100
325
- full_summary = summary += "\n\n\n"
326
- summary = summary[0..96]
327
- summary += '...'
328
- else
329
- full_summary = nil
330
- end
331
-
332
- @ticket = {
333
- 'First_Name' => "#{@remedy_data[:first_name]}",
334
- 'Impact' => '1-Extensive/Widespread',
335
- 'Last_Name' => "#{@remedy_data[:last_name]}",
336
- 'Reported_Source' => 'Other',
337
- 'Service_Type' => 'Infrastructure Event',
338
- 'Status' => 'New',
339
- 'Action' => 'CREATE',
340
- 'Summary' => summary,
341
- 'Full_Summary' => summary,
342
- 'Assets' => "++ Assets affected +++++++++++++++++++++++\n",
343
- 'Solutions' => "++ Details ++++++++++++++++++++++++++\n",
344
- 'Notes' => "++ Additional information ++++++++++++++++\n",
345
- 'Urgency' => '1-Critical'
346
- }
347
- @ticket['Solutions'] +=
348
- "\n\n========================================== \nSummary: #{row['summary']} \nFix: #{row['fix']}"
349
- @ticket['Assets'] +=
350
- "#{row['ip_address']}"
351
- end
352
- if current_vuln_id == row['vulnerability_id']
353
- #Add solutions for the now affected assets.
354
- if current_solution_id != row['solution_id']
355
- new_solution_text = "\n========================================== \nSummary: #{row['summary']} \nFix: #{row['fix']}\n"
356
- if @ticket['Solutions'].include? new_solution_text
357
- @log.log_message('Ignoring duplicate solution in ticket creation.')
358
- else
359
- @ticket['Solutions'] += new_solution_text
360
- #Add any references.
361
- unless row['url'].nil?
362
- @ticket['Solutions'] += "\nURL: #{row['url']}"
363
- end
364
- end
365
- current_solution_id = row['solution_id']
366
- end
367
-
368
- #Added the new asset to the list of affected systems if it is different (could have been the same asset with a different solution ID).
369
- if current_asset_id != row['asset_id']
370
- @ticket['Assets'] += ", #{row['ip_address']}"
371
- current_asset_id = row['asset_id']
372
- end
373
- end
374
- unless current_vuln_id == row['vulnerability_id']
375
- # NXID in the work_notes is the unique identifier used to query incidents to update them.
376
- @ticket['Notes'] += "\n\nNXID: #{nexpose_identifier_id}#{current_asset_id}#{current_vuln_id}"
377
- current_vuln_id = -1
378
- current_solution_id = -1
379
- current_asset_id = -1
380
- @ticket = format_notes_by_vulnerability(@ticket, full_summary)
381
- tickets.push(@ticket)
382
- redo
383
- end
384
- end
385
- # NXID in the work_notes is the unique identifier used to query incidents to update them.
386
- @ticket['Notes'] += "\n\nNXID: #{nexpose_identifier_id}#{current_asset_id}#{current_vuln_id}"
387
- @ticket = format_notes_by_vulnerability(@ticket, full_summary)
388
- tickets.push(@ticket) unless @ticket.nil?
389
- tickets
390
- end
391
-
392
- def format_notes_by_vulnerability(ticket, prepend)
393
- nxid_holder = ticket['Notes']
394
- ticket['Notes'] = ''
395
- ticket['Notes'] += prepend unless prepend.nil?
396
- ticket['Notes'] += ticket['Assets'] += "\n\n"
397
- ticket['Notes'] += ticket['Solutions'] += "\n\n"
398
- ticket['Notes'] += nxid_holder
399
- ticket.delete("Assets")
400
- ticket.delete("Solutions")
401
- ticket.delete("Full_Summary")
402
- ticket
403
- end
404
-
405
-
406
- # Prepare ticket updates from the CSV of vulnerabilities exported from Nexpose. This method
407
- # currently only supports updating IP-address mode tickets in Remedy. The list of vulnerabilities
408
- # are ordered by IP address and then by ticket_status, allowing the method to loop through and
409
- # display new, old, and same vulnerabilities in that order.
410
- #
411
- # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
412
- #
413
- # * *Returns* :
414
- # - List of savon-formated (hash) tickets for updating within Remedy.
415
- #
416
- def prepare_update_tickets_by_ip(vulnerability_list, nexpose_identifier_id)
417
- fail 'Ticket updates are only supported in IP-address mode.' if @options[:ticket_mode] != 'I'
215
+ def prepare_tickets(vulnerability_list, nexpose_identifier_id, matching_fields)
418
216
  @ticket = Hash.new(-1)
419
217
 
420
- @log.log_message('Preparing ticket updates by IP address.')
218
+ @log.log_message("Preparing tickets for #{@options[:ticket_mode]} mode.")
421
219
  tickets = []
422
- current_ip = -1
423
- ticket_status = 'New'
220
+ previous_row = nil
221
+ description = nil
424
222
  CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
425
- if current_ip == -1
426
- current_ip = row['ip_address']
427
- ticket_status = row['comparison']
223
+ if previous_row.nil?
224
+ previous_row = row.dup
225
+ description = @common_helper.get_description(nexpose_identifier_id, row)
226
+ @ticket = generate_new_ticket({'Summary' => "#{@common_helper.get_title(row, 100)}",
227
+ 'Notes' => ""})
228
+ #Skip querying for ticket if it's the initial scan
229
+ next if row['comparison'].nil?
428
230
 
429
231
  # Query Remedy for the incident by unique id (generated NXID)
430
- queried_incident = query_for_ticket("NXID: #{nexpose_identifier_id}#{row['ip_address']}")
431
- if queried_incident.nil? || queried_incident.empty?
432
- @log.log_message("No incident found for NXID: #{nexpose_identifier_id}#{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}")
433
- else
434
- @log.log_message("Creating ticket update with IP address: #{row['ip_address']} for Nexpose Identifier with ID: #{nexpose_identifier_id}")
435
- @log.log_message("Ticket status #{ticket_status}")
436
- # Remedy incident updates require populating all fields.
437
- @ticket = extract_queried_incident(queried_incident, "++ #{row['comparison']} Vulnerabilities ++++++++++++++++++++++++++\n")
438
- end
439
- end
440
- if current_ip == row['ip_address']
441
- # If the ticket_status is different, add a a new 'header' to signify a new block of tickets.
442
- unless ticket_status == row['comparison']
443
- @ticket['Notes'] +=
444
- "\n\n\n++ #{row['comparison']} Vulnerabilities ++++++++++++++++++++++++++\n"
445
- ticket_status = row['comparison']
446
- end
447
-
448
- @ticket['Notes'] +=
449
- "\n\n========================================== \nSummary: #{row['summary']} \nFix: #{row['fix']}"
450
- # Only add the URL block if data exists in the row.
451
- unless row['url'].nil?
452
- @ticket['Notes'] +=
453
- "\nURL: #{row['url']}"
232
+ queried_incident = query_for_ticket("NXID: #{@common_helper.generate_nxid(nexpose_identifier_id, row)}")
233
+ if !queried_incident.nil? && queried_incident.first.is_a?(Hash)
234
+ queried_incident.select! { |t| !['Closed', 'Resolved', 'Cancelled'].include?(t[:status]) }
454
235
  end
455
- end
456
- unless current_ip == row['ip_address']
457
- # NXID in the work_notes is the unique identifier used to query incidents to update them.
458
- @ticket['Notes'] += "\n\nNXID: #{nexpose_identifier_id}#{current_ip}"
459
- tickets.push(@ticket)
460
- current_ip = -1
461
- redo
462
- end
463
- end
464
- # NXID in the work_notes is the unique identifier used to query incidents to update them.
465
- @ticket['Notes'] += "\n\nNXID: #{nexpose_identifier_id}#{current_ip}" unless @ticket.empty?
466
- tickets.push(@ticket) unless @ticket.nil? || @ticket.empty?
467
- tickets
468
- end
469
-
470
- # Prepare ticket updates from the CSV of vulnerabilities exported from Nexpose. This method
471
- # currently only supports updating vulnerability mode tickets in Remedy. The list of vulnerabilities
472
- # are ordered by vulnerability ID and then by ticket_status, allowing the method to loop through and
473
- # display new, old, and same vulnerabilities in that order.
474
- #
475
- # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
476
- #
477
- # * *Returns* :
478
- # - List of savon-formated (hash) tickets for updating within Remedy.
479
- #
480
- def prepare_update_tickets_by_vulnerability(vulnerability_list, nexpose_identifier_id)
481
- fail 'Ticket updates are only supported in IP-address mode.' if @options[:ticket_mode] != 'V'
482
- @ticket = Hash.new(-1)
483
-
484
- @log.log_message('Preparing ticket updates by IP address.')
485
- tickets = []
486
- current_vuln_id = -1
487
- current_solution_id = -1
488
- current_asset_id = -1
489
- current_solutions_text = "\n++ Details ++++++++++++++\n"
490
- ticket_status = 'New'
491
- CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
492
- if current_vuln_id == -1
493
- current_solutions_text = "\n++ Details +++++++++++++++\n"
494
- current_vuln_id = row['vulnerability_id']
495
- ticket_status = row['comparison']
496
- current_asset_id = -1
497
- current_solution_id = -1
498
-
499
- # Query Remedy for the incident by unique id (generated NXID)
500
- queried_incident = query_for_ticket("NXID: #{nexpose_identifier_id}#{row['asset_id']}#{row['vulnerability_id']}")
501
236
  if queried_incident.nil? || queried_incident.empty?
502
- @log.log_message("No incident found for NXID: #{nexpose_identifier_id}#{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}. Creating...")
237
+ @log.log_message("No incident found for NXID: #{@common_helper.generate_nxid(nexpose_identifier_id, row)}. Creating...")
503
238
  new_ticket_csv = vulnerability_list.split("\n").first
504
239
  new_ticket_csv += "\n#{row.to_s}"
505
- new_ticket = prepare_create_tickets_by_vulnerability(new_ticket_csv, nexpose_identifier_id)
240
+
241
+ #delete the comparison row
242
+ data = CSV::Table.new(CSV.parse(new_ticket_csv, headers: true))
243
+ data.delete("comparison")
244
+
245
+ new_ticket = prepare_create_tickets(data.to_s, nexpose_identifier_id)
506
246
  @log.log_message('Created ticket. Sending to Remedy...')
507
247
  create_tickets(new_ticket)
508
248
  @log.log_message('Ticket sent. Performing update for ticket...')
509
249
  #Now that there is a ticket for this NXID update it as if it existed this whole time...
510
- current_vuln_id = -1
250
+ previous_row = nil
511
251
  redo
512
252
  else
513
- @log.log_message("Creating ticket update for vulnerability with ID: #{row['vulnerability_id']}, Asset ID: #{row['asset_id']} and Nexpose Identifier ID: #{nexpose_identifier_id}. Ticket status #{ticket_status}.")
253
+ info = @common_helper.get_field_info(matching_fields, previous_row)
254
+ @log.log_message("Creating ticket update with #{info} for Nexpose Identifier with ID: #{nexpose_identifier_id}")
255
+ @log.log_message("Ticket status #{row['comparison']}")
514
256
  # Remedy incident updates require populating all fields.
515
- @ticket = extract_queried_incident(queried_incident, "++ #{row['comparison']} Assets ++++++++++++++++++++++\n")
516
- end
517
- end
518
- if current_vuln_id == row['vulnerability_id']
519
- # If the ticket_status is different, add a a new 'header' to signify a new block of tickets.
520
- unless ticket_status == row['comparison']
521
- @ticket['Notes'] +=
522
- "\n\n\n++ #{row['comparison']} Assets +++++++++++++++++++++++\n"
523
- ticket_status = row['comparison']
524
- end
257
+ @ticket = extract_queried_incident(queried_incident, "")
258
+ end
259
+ elsif matching_fields.any? { |x| previous_row[x].nil? || previous_row[x] != row[x] }
260
+ info = @common_helper.get_field_info(matching_fields, previous_row)
261
+ @log.log_message("Generated ticket with #{info}")
525
262
 
526
- #Added the new asset to the list of affected systems if it is different (could have been the same asset with a different solution ID).
527
- if current_asset_id != row['asset_id']
528
- @ticket['Notes'] += "#{row['ip_address']}, "
529
- current_asset_id = row['asset_id']
530
- end
531
-
532
- #Add solutions for the now affected assets.
533
- if current_solution_id != row['solution_id']
534
- new_solution_text = "\n========================================== \nSummary: #{row['summary']} \nFix: #{row['fix']}\n"
535
- if current_solutions_text.include? new_solution_text
536
- @log.log_message('Ignoring duplicate solution for ticket update.')
537
- else
538
- current_solutions_text += new_solution_text
539
- #Add any references.
540
- unless row['url'].nil?
541
- current_solutions_text +=
542
- "\nURL: #{row['url']}"
543
- end
544
- end
545
- current_solution_id = row['solution_id']
546
- end
547
- end
548
- unless current_vuln_id == row['vulnerability_id']
549
- # NXID in the work_notes is the unique identifier used to query incidents to update them.
550
- @ticket['Notes'] += "\n\n" + current_solutions_text
551
- @ticket['Notes'] += "\n\nNXID: #{nexpose_identifier_id}#{current_asset_id}#{current_vuln_id}"
263
+ @ticket['Notes'] = @common_helper.print_description(description)
552
264
  tickets.push(@ticket)
553
- current_vuln_id = -1
554
- current_solution_id = -1
555
- current_asset_id = -1
265
+ previous_row = nil
266
+ description = nil
556
267
  redo
268
+ else
269
+ description = @common_helper.update_description(description, row)
557
270
  end
558
271
  end
559
- # NXID in the work_notes is the unique identifier used to query incidents to update them.
560
- @ticket['Notes'] += current_solutions_text
561
- @ticket['Notes'] += "\n\nNXID: #{nexpose_identifier_id}#{current_asset_id}#{current_vuln_id}" unless @ticket.empty?
562
- tickets.push(@ticket) unless @ticket.nil? || @ticket.empty?
272
+
273
+ unless @ticket.nil? || @ticket.empty?
274
+ @ticket['Notes'] = @common_helper.print_description(description)
275
+ tickets.push(@ticket)
276
+ end
277
+
278
+ @log.log_message("Generated <#{tickets.count.to_s}> tickets.")
563
279
  tickets
564
280
  end
565
281
 
282
+ # Creates a ticket with the extracted data from a queried Remedy incident.
283
+ #
284
+ # - +queried_incident+ - The queried incident from Remedy
285
+ # - +notes_header+ - The texted to be placed at the top of the Remedy 'Notes' field.
286
+ # - +status+ - The status to which the ticket will be set.
287
+ #
288
+ # * *Returns* :
289
+ # - A single savon-formated (hash) ticket for updating within Remedy.
290
+ #
291
+ def ticket_from_queried_incident(queried_incident, notes_header, status)
292
+ {
293
+ 'Categorization_Tier_1' => queried_incident[:categorization_tier_1],
294
+ 'Categorization_Tier_2' => queried_incident[:categorization_tier_2],
295
+ 'Categorization_Tier_3' => queried_incident[:categorization_tier_3],
296
+ 'Closure_Manufacturer' => queried_incident[:closure_manufacturer],
297
+ 'Closure_Product_Category_Tier1' => queried_incident[:closure_product_category_tier1],
298
+ 'Closure_Product_Category_Tier2' => queried_incident[:closure_product_category_tier2],
299
+ 'Closure_Product_Category_Tier3' => queried_incident[:closure_product_category_tier3],
300
+ 'Closure_Product_Model_Version' => queried_incident[:closure_product_model_version],
301
+ 'Closure_Product_Name' => queried_incident[:closure_product_name],
302
+ 'Company' => queried_incident[:company],
303
+ 'Summary' => queried_incident[:summary],
304
+ 'Notes' => notes_header || queried_incident[:notes],
305
+ 'Impact' => queried_incident[:impact],
306
+ 'Manufacturer' => queried_incident[:manufacturer],
307
+ 'Product_Categorization_Tier_1' => queried_incident[:product_categorization_tier_1],
308
+ 'Product_Categorization_Tier_2' => queried_incident[:product_categorization_tier_2],
309
+ 'Product_Categorization_Tier_3' => queried_incident[:product_categorization_tier_3],
310
+ 'Product_Model_Version' => queried_incident[:product_model_version],
311
+ 'Product_Name' => queried_incident[:product_name],
312
+ 'Reported_Source' => queried_incident[:reported_source],
313
+ 'Resolution' => queried_incident[:resolution],
314
+ 'Resolution_Category' => queried_incident[:resolution_category],
315
+ 'Resolution_Category_Tier_2' => queried_incident[:resolution_category_tier_2],
316
+ 'Resolution_Category_Tier_3' => queried_incident[:resolution_category_tier_3],
317
+ 'Resolution_Method' => queried_incident[:resolution_method],
318
+ 'Service_Type' => queried_incident[:service_type],
319
+ 'Status' => status || queried_incident[:status],
320
+ 'Urgency' => queried_incident[:urgency],
321
+ 'Action' => 'MODIFY',
322
+ 'Work_Info_Summary' => queried_incident[:work_info_summary],
323
+ 'Work_Info_Notes' => queried_incident[:work_info_notes],
324
+ 'Work_Info_Type' => queried_incident[:work_info_type],
325
+ 'Work_Info_Date' => queried_incident[:work_info_date],
326
+ 'Work_Info_Source' => queried_incident[:work_info_source],
327
+ 'Work_Info_Locked' => queried_incident[:work_info_locked],
328
+ 'Work_Info_View_Access' => queried_incident[:work_info_view_access],
329
+ 'Incident_Number' => queried_incident[:incident_number],
330
+ 'Status_Reason' => queried_incident[:status_reason],
331
+ 'ServiceCI' => queried_incident[:service_ci],
332
+ 'ServiceCI_ReconID' => queried_incident[:service_ci_recon_id],
333
+ 'HPD_CI' => queried_incident[:hpd_ci],
334
+ 'HPD_CI_ReconID' => queried_incident[:hpd_ci_recon_id],
335
+ 'HPD_CI_FormName' => queried_incident[:hpd_ci_form_name],
336
+ 'z1D_CI_FormName' => queried_incident[:z1d_ci_form_name]
337
+ }
338
+ end
339
+
566
340
  # Extracts from a queried Remedy incident the relevant data required for an update to be made to said incident.
567
341
  # Creates a ticket with the extracted data.
568
342
  #
@@ -573,116 +347,13 @@ class RemedyHelper
573
347
  # - A single savon-formated (hash) ticket for updating within Remedy.
574
348
  #
575
349
  def extract_queried_incident(queried_incident, notes_header)
576
-
577
- if queried_incident[0].is_a?(Hash)
578
- #Hash of hashes
579
- @log.log_message("More than one ticket returned from Remedy. Number of tickets returned: #{queried_incident.count}. Parsing return to check returned ticket status...")
580
- @ticket = nil
581
- queried_incident.count.times do |x|
582
- #TODO: This should be moved to the config for the Remedy helper.
583
- if ['Closed', 'Resolved', 'Cancelled'].include? queried_incident[x][:status]
584
- @log.log_message("Returned ticket number <#{x}> of <#{(queried_incident.count - 1)}> is of status <#{queried_incident[x][:status]}>. Ignoring.")
585
- else
586
- fail 'When trying to update tickets Remedy returned multiple tickets with the same NXID and in progress status!' unless @ticket.nil?
587
- @ticket = {
588
- 'Categorization_Tier_1' => queried_incident[x][:categorization_tier_1],
589
- 'Categorization_Tier_2' => queried_incident[x][:categorization_tier_2],
590
- 'Categorization_Tier_3' => queried_incident[x][:categorization_tier_3],
591
- 'Closure_Manufacturer' => queried_incident[x][:closure_manufacturer],
592
- 'Closure_Product_Category_Tier1' => queried_incident[x][:closure_product_category_tier1],
593
- 'Closure_Product_Category_Tier2' => queried_incident[x][:closure_product_category_tier2],
594
- 'Closure_Product_Category_Tier3' => queried_incident[x][:closure_product_category_tier3],
595
- 'Closure_Product_Model_Version' => queried_incident[x][:closure_product_model_version],
596
- 'Closure_Product_Name' => queried_incident[x][:closure_product_name],
597
- 'Company' => queried_incident[x][:company],
598
- 'Summary' => queried_incident[x][:summary],
599
- 'Notes' => notes_header,
600
- 'Impact' => queried_incident[x][:impact],
601
- 'Manufacturer' => queried_incident[x][:manufacturer],
602
- 'Product_Categorization_Tier_1' => queried_incident[x][:product_categorization_tier_1],
603
- 'Product_Categorization_Tier_2' => queried_incident[x][:product_categorization_tier_2],
604
- 'Product_Categorization_Tier_3' => queried_incident[x][:product_categorization_tier_3],
605
- 'Product_Model_Version' => queried_incident[x][:product_model_version],
606
- 'Product_Name' => queried_incident[x][:product_name],
607
- 'Reported_Source' => queried_incident[x][:reported_source],
608
- 'Resolution' => queried_incident[x][:resolution],
609
- 'Resolution_Category' => queried_incident[x][:resolution_category],
610
- 'Resolution_Category_Tier_2' => queried_incident[x][:resolution_category_tier_2],
611
- 'Resolution_Category_Tier_3' => queried_incident[x][:resolution_category_tier_3],
612
- 'Resolution_Method' => queried_incident[x][:resolution_method],
613
- 'Service_Type' => queried_incident[x][:service_type],
614
- 'Status' => queried_incident[x][:status],
615
- 'Urgency' => queried_incident[x][:urgency],
616
- 'Action' => 'MODIFY',
617
- 'Work_Info_Summary' => queried_incident[x][:work_info_summary],
618
- 'Work_Info_Notes' => queried_incident[x][:work_info_notes],
619
- 'Work_Info_Type' => queried_incident[x][:work_info_type],
620
- 'Work_Info_Date' => queried_incident[x][:work_info_date],
621
- 'Work_Info_Source' => queried_incident[x][:work_info_source],
622
- 'Work_Info_Locked' => queried_incident[x][:work_info_locked],
623
- 'Work_Info_View_Access' => queried_incident[x][:work_info_view_access],
624
- 'Incident_Number' => queried_incident[x][:incident_number],
625
- 'Status_Reason' => queried_incident[x][:status_reason],
626
- 'ServiceCI' => queried_incident[x][:service_ci],
627
- 'ServiceCI_ReconID' => queried_incident[x][:service_ci_recon_id],
628
- 'HPD_CI' => queried_incident[x][:hpd_ci],
629
- 'HPD_CI_ReconID' => queried_incident[x][:hpd_ci_recon_id],
630
- 'HPD_CI_FormName' => queried_incident[x][:hpd_ci_form_name],
631
- 'z1D_CI_FormName' => queried_incident[x][:z1d_ci_form_name]
632
- }
633
- end
634
- end
635
- else
636
- @ticket = {
637
- 'Categorization_Tier_1' => queried_incident[:categorization_tier_1],
638
- 'Categorization_Tier_2' => queried_incident[:categorization_tier_2],
639
- 'Categorization_Tier_3' => queried_incident[:categorization_tier_3],
640
- 'Closure_Manufacturer' => queried_incident[:closure_manufacturer],
641
- 'Closure_Product_Category_Tier1' => queried_incident[:closure_product_category_tier1],
642
- 'Closure_Product_Category_Tier2' => queried_incident[:closure_product_category_tier2],
643
- 'Closure_Product_Category_Tier3' => queried_incident[:closure_product_category_tier3],
644
- 'Closure_Product_Model_Version' => queried_incident[:closure_product_model_version],
645
- 'Closure_Product_Name' => queried_incident[:closure_product_name],
646
- 'Company' => queried_incident[:company],
647
- 'Summary' => queried_incident[:summary],
648
- 'Notes' => notes_header,
649
- 'Impact' => queried_incident[:impact],
650
- 'Manufacturer' => queried_incident[:manufacturer],
651
- 'Product_Categorization_Tier_1' => queried_incident[:product_categorization_tier_1],
652
- 'Product_Categorization_Tier_2' => queried_incident[:product_categorization_tier_2],
653
- 'Product_Categorization_Tier_3' => queried_incident[:product_categorization_tier_3],
654
- 'Product_Model_Version' => queried_incident[:product_model_version],
655
- 'Product_Name' => queried_incident[:product_name],
656
- 'Reported_Source' => queried_incident[:reported_source],
657
- 'Resolution' => queried_incident[:resolution],
658
- 'Resolution_Category' => queried_incident[:resolution_category],
659
- 'Resolution_Category_Tier_2' => queried_incident[:resolution_category_tier_2],
660
- 'Resolution_Category_Tier_3' => queried_incident[:resolution_category_tier_3],
661
- 'Resolution_Method' => queried_incident[:resolution_method],
662
- 'Service_Type' => queried_incident[:service_type],
663
- 'Status' => queried_incident[:status],
664
- 'Urgency' => queried_incident[:urgency],
665
- 'Action' => 'MODIFY',
666
- 'Work_Info_Summary' => queried_incident[:work_info_summary],
667
- 'Work_Info_Notes' => queried_incident[:work_info_notes],
668
- 'Work_Info_Type' => queried_incident[:work_info_type],
669
- 'Work_Info_Date' => queried_incident[:work_info_date],
670
- 'Work_Info_Source' => queried_incident[:work_info_source],
671
- 'Work_Info_Locked' => queried_incident[:work_info_locked],
672
- 'Work_Info_View_Access' => queried_incident[:work_info_view_access],
673
- 'Incident_Number' => queried_incident[:incident_number],
674
- 'Status_Reason' => queried_incident[:status_reason],
675
- 'ServiceCI' => queried_incident[:service_ci],
676
- 'ServiceCI_ReconID' => queried_incident[:service_ci_recon_id],
677
- 'HPD_CI' => queried_incident[:hpd_ci],
678
- 'HPD_CI_ReconID' => queried_incident[:hpd_ci_recon_id],
679
- 'HPD_CI_FormName' => queried_incident[:hpd_ci_form_name],
680
- 'z1D_CI_FormName' => queried_incident[:z1d_ci_form_name]
681
- }
350
+ unless queried_incident.first.is_a?(Hash)
351
+ return ticket_from_queried_incident(queried_incident, notes_header, nil)
682
352
  end
683
- @ticket
684
- end
685
353
 
354
+ fail "Multiple tickets returned for same NXID" if queried_incidents.count > 1
355
+ ticket_from_queried_incident(queried_incident.first, notes_header, nil)
356
+ end
686
357
 
687
358
  # Prepare ticket closures from the CSV of vulnerabilities exported from Nexpose.
688
359
  #
@@ -693,79 +364,22 @@ class RemedyHelper
693
364
  # - List of savon-formated (hash) tickets for closing within Remedy.
694
365
  #
695
366
  def prepare_close_tickets(vulnerability_list, nexpose_identifier_id)
696
- fail 'Ticket closures are only supported in default mode.' if @options[:ticket_mode] == 'I'
697
- @log.log_message('Preparing ticket closures by default method.')
367
+ @log.log_message("Preparing ticket closures for mode #{@options[:ticket_mode]}.")
698
368
  @nxid = nil
699
369
  tickets = []
700
370
  CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
701
- case @options[:ticket_mode]
702
- # 'D' Default mode: IP *-* Vulnerability
703
- when 'D'
704
- @nxid = "#{nexpose_identifier_id}#{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}"
705
- # 'I' IP address mode: IP address -* Vulnerability
706
- when 'I'
707
- @nxid = "#{nexpose_identifier_id}#{row['current_ip']}"
708
- # 'V' Vulnerability mode: Vulnerability -* IP address
709
- when 'V'
710
- @nxid = "#{nexpose_identifier_id}#{row['current_asset_id']}#{row['current_vuln_id']}"
711
- else
712
- fail 'Could not close tickets - do not understand the ticketing mode!'
713
- end
371
+ @nxid = @common_helper.generate_nxid(nexpose_identifier_id, row)
372
+
714
373
  # Query Remedy for the incident by unique id (generated NXID)
715
374
  queried_incident = query_for_ticket("NXID: #{@nxid}")
716
375
  if queried_incident.nil? || queried_incident.empty?
717
376
  @log.log_message("No incident found for NXID: #{@nxid}")
718
377
  else
719
378
  # Remedy incident updates require populating all fields.
720
- ticket = {
721
- 'Categorization_Tier_1' => queried_incident[:categorization_tier_1],
722
- 'Categorization_Tier_2' => queried_incident[:categorization_tier_2],
723
- 'Categorization_Tier_3' => queried_incident[:categorization_tier_3],
724
- 'Closure_Manufacturer' => queried_incident[:closure_manufacturer],
725
- 'Closure_Product_Category_Tier1' => queried_incident[:closure_product_category_tier1],
726
- 'Closure_Product_Category_Tier2' => queried_incident[:closure_product_category_tier2],
727
- 'Closure_Product_Category_Tier3' => queried_incident[:closure_product_category_tier3],
728
- 'Closure_Product_Model_Version' => queried_incident[:closure_product_model_version],
729
- 'Closure_Product_Name' => queried_incident[:closure_product_name],
730
- 'Company' => queried_incident[:company],
731
- 'Summary' => queried_incident[:summary],
732
- 'Notes' => queried_incident[:notes],
733
- 'Impact' => queried_incident[:impact],
734
- 'Manufacturer' => queried_incident[:manufacturer],
735
- 'Product_Categorization_Tier_1' => queried_incident[:product_categorization_tier_1],
736
- 'Product_Categorization_Tier_2' => queried_incident[:product_categorization_tier_2],
737
- 'Product_Categorization_Tier_3' => queried_incident[:product_categorization_tier_3],
738
- 'Product_Model_Version' => queried_incident[:product_model_version],
739
- 'Product_Name' => queried_incident[:product_name],
740
- 'Reported_Source' => queried_incident[:reported_source],
741
- 'Resolution' => queried_incident[:resolution],
742
- 'Resolution_Category' => queried_incident[:resolution_category],
743
- 'Resolution_Category_Tier_2' => queried_incident[:resolution_category_tier_2],
744
- 'Resolution_Category_Tier_3' => queried_incident[:resolution_category_tier_3],
745
- 'Resolution_Method' => queried_incident[:resolution_method],
746
- 'Service_Type' => queried_incident[:service_type],
747
- 'Status' => 'Closed',
748
- 'Urgency' => queried_incident[:urgency],
749
- 'Action' => 'MODIFY',
750
- 'Work_Info_Summary' => queried_incident[:work_info_summary],
751
- 'Work_Info_Notes' => queried_incident[:work_info_notes],
752
- 'Work_Info_Type' => queried_incident[:work_info_type],
753
- 'Work_Info_Date' => queried_incident[:work_info_date],
754
- 'Work_Info_Source' => queried_incident[:work_info_source],
755
- 'Work_Info_Locked' => queried_incident[:work_info_locked],
756
- 'Work_Info_View_Access' => queried_incident[:work_info_view_access],
757
- 'Incident_Number' => queried_incident[:incident_number],
758
- 'Status_Reason' => queried_incident[:status_reason],
759
- 'ServiceCI' => queried_incident[:service_ci],
760
- 'ServiceCI_ReconID' => queried_incident[:service_ci_recon_id],
761
- 'HPD_CI' => queried_incident[:hpd_ci],
762
- 'HPD_CI_ReconID' => queried_incident[:hpd_ci_recon_id],
763
- 'HPD_CI_FormName' => queried_incident[:hpd_ci_form_name],
764
- 'z1D_CI_FormName' => queried_incident[:z1d_ci_form_name]
765
- }
379
+ ticket = ticket_from_queried_incident(queried_incident, nil, 'Closed')
766
380
  tickets.push(ticket)
767
381
  end
768
382
  end
769
383
  tickets
770
384
  end
771
- end
385
+ end