nexpose_ticketing 0.3.1 → 0.5.0

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: 5d263b9bebd36736baf337638d239978dfeccaa0
4
- data.tar.gz: 696d970f7f45d6ab028eea7220d845f664accfb8
3
+ metadata.gz: d994e0ee3fa0a54776a378818816102c883e7e91
4
+ data.tar.gz: 89ee1701ef6ecbeaab92bcecc45ac3b8c06f2831
5
5
  SHA512:
6
- metadata.gz: a4759ef9ceb9ab0ad2c8a98f4c3ab42aa138f26514f9ef30fd7bff14c66a8c58715858ae4979b256f96099ed0b997adfc1dfa0d88e0c160ece6c1c653a2ef805
7
- data.tar.gz: faf6ca1aa7155c2edfd9477ef0ac9b9496758e743c1d85e22862fd6cedd703743587e61da38fecd35b981e9ff82e870fa2aa147016cdb6b22bf228cee668f752
6
+ metadata.gz: ebc7223e2c01f5b55ddc5e4c163439fdd1225ba9f90a25c45801dbc3e765c0f84cfde9ca4ae8cbe869056df07a864311a70cb9dc5d18c07cb4d9430add4d093c
7
+ data.tar.gz: 09aae916d5a73cf2fcf11cc9db9551773d28f3eeac6fe2bc563bae3e8f17184aa0321d9d2b0e45c9f7b7657cf56b09b1896b59c740d4e6d1fcc30ef96e557750
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ require 'yaml'
3
+ require 'nexpose_ticketing'
4
+
5
+ # Path to ServiceNow configuration file
6
+ SERVICEDESK_CONFIG_PATH = File.join(File.dirname(__FILE__),
7
+ '../lib/nexpose_ticketing/config/servicedesk.config')
8
+
9
+ # Read in ServiceDesk options from servicenow.config
10
+ servicedesk_options = begin
11
+ YAML.load_file(SERVICEDESK_CONFIG_PATH)
12
+ rescue ArgumentError => e
13
+ raise "Could not parse YAML #{SERVICEDESK_CONFIG_PATH} : #{e.message}"
14
+ end
15
+
16
+ # Initialize Nexpose Ticket Service using ServiceDesk
17
+ NexposeTicketing.start(servicedesk_options)
@@ -0,0 +1,19 @@
1
+ ---
2
+ # This configuration file defines all the options necessary to support the helper.
3
+ # Fields marked (M) are mandatory.
4
+ #
5
+
6
+ # (M) Helper class name
7
+ :helper_name: ServiceDeskHelper
8
+
9
+ # (M) REST Enpoint on the ServiceDesk instance
10
+ :rest_uri: https://uritoservicedesk:8080/sdpapi/request
11
+ # (M) API Key to access the ServiceDesk instance
12
+ :api_key: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
13
+ # (M) local ticket database path
14
+ :ticket_db_path: /var/lib/gems/1.9.1/gems/nexpose_ticketing-0.2.3.jh/lib/nexpose_ticketing/log/servicedesk_ticketdb
15
+
16
+ # (M) ServiceDesk requestor name
17
+ :requester: nexpose
18
+ # (M) SerivceDesk incident group
19
+ :group: Network
@@ -7,7 +7,7 @@
7
7
  :helper_name: ServiceNowHelper
8
8
 
9
9
  # (M) ServiceNow url (currently requires using JSON)
10
- :servicenow_url: https://your.service-now.com/incident.do?JSON
10
+ :servicenow_url: https://your.service-now.com/u_rpd_vulnerabilities.do?JSON
11
11
  # (M) Username for ServiceNow
12
12
  :username: admin
13
13
  # (M) Password for above username
@@ -8,23 +8,33 @@
8
8
  :logging_enabled: true
9
9
  # Filters the reports to specific sites one per line, leave empty for no site.
10
10
  :sites:
11
- - '7'
11
+ - '1'
12
12
  # Minimum floor severity to report on. Number between 0 and 10.
13
13
  :severity: 8
14
- # (M) Name of the report historial file saved in disk.
14
+ # (M) Name of the report historical file saved in disk.
15
15
  :file_name: last_scan_data.csv
16
16
  # (M) Defines the ticket creation mode:
17
- # 'D' Default IP *-* Vulnerability
18
- # 'I' IP address -* Vulnerability
17
+ # 'D' Default IP *-* Vulnerability
18
+ # 'I' IP address -* Vulnerability
19
+ # 'V' Vulnerability -* IP Address (Remedy helper only)
19
20
  :ticket_mode: I
20
21
  # Timeout in seconds. The number of seconds the GEM waits for a response from Nexpose before exiting.
21
22
  :timeout: 10800
22
23
  # Ticket batching. Breaks ticket processing into groups of value size controlling resource utilisation of both systems.
23
24
  :batch_size: 300
25
+ # (M) For 'I' & 'V' mode. Set to 'Y' for tickets that have been fixed to be closed after update, set to 'N' for ticket to be left
26
+ # open for a user to manually close or change the status.
27
+ :close_old_tickets_on_update: Y
28
+ # A comma separated list of tags to scope what gets ticketed
29
+ :tags:
30
+ # A comma separated list of vulnerability categories to include
31
+ :vulnerabilityCategories:
32
+ # The asset minimum risk score to open tickets for
33
+ :riskScore:
24
34
  # Nexpose options.
25
35
  :nexpose_data:
26
36
  # (M) Nexpose console hostname.
27
- :nxconsole: your_nexpose_install
37
+ :nxconsole: 127.0.0.1
28
38
  # (M) Nexpose username.
29
39
  :nxuser: nxadmin
30
40
  # (M) Nexpose password.
@@ -33,22 +33,22 @@ class JiraHelper
33
33
  end
34
34
 
35
35
  # Prepares tickets from the CSV.
36
- def prepare_create_tickets(vulnerability_list)
36
+ def prepare_create_tickets(vulnerability_list, site_id)
37
37
  @ticket = Hash.new(-1)
38
38
  case @options[:ticket_mode]
39
39
  # 'D' Default IP *-* Vulnerability
40
40
  when 'D'
41
- prepare_tickets_default(vulnerability_list)
41
+ prepare_tickets_default(vulnerability_list, site_id)
42
42
  # 'I' IP address -* Vulnerability
43
43
  when 'I'
44
- prepare_tickets_by_ip(vulnerability_list)
44
+ prepare_tickets_by_ip(vulnerability_list, site_id)
45
45
  else
46
46
  fail 'No ticketing mode selected.'
47
47
  end
48
48
  end
49
49
 
50
50
  # Prepares and creates tickets in default mode.
51
- def prepare_tickets_default(vulnerability_list)
51
+ def prepare_tickets_default(vulnerability_list, site_id)
52
52
  tickets = []
53
53
  CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
54
54
  # JiraHelper doesn't like new line characters in their summaries.
@@ -69,7 +69,7 @@ class JiraHelper
69
69
  end
70
70
 
71
71
  # Prepares and creates tickets in IP mode.
72
- def prepare_tickets_by_ip(vulnerability_list)
72
+ def prepare_tickets_by_ip(vulnerability_list, site_id)
73
73
  tickets = []
74
74
  current_ip = -1
75
75
  CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
@@ -154,7 +154,7 @@ class RemedyHelper
154
154
  end
155
155
 
156
156
  # Prepare tickets from the CSV of vulnerabilities exported from Nexpose. This method determines
157
- # how to prepare the tickets (either by default or by IP address) based on config options.
157
+ # how to prepare the tickets (by default, by IP address or by vulnerability) based on config options.
158
158
  #
159
159
  # * *Args* :
160
160
  # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
@@ -162,19 +162,45 @@ class RemedyHelper
162
162
  # * *Returns* :
163
163
  # - List of savon-formated (hash) tickets for creating within Remedy.
164
164
  #
165
- def prepare_create_tickets(vulnerability_list)
165
+ def prepare_create_tickets(vulnerability_list, site_id)
166
166
  @ticket = Hash.new(-1)
167
167
  case @options[:ticket_mode]
168
168
  # 'D' Default mode: IP *-* Vulnerability
169
169
  when 'D'
170
- prepare_create_tickets_default(vulnerability_list)
170
+ prepare_create_tickets_default(vulnerability_list, site_id)
171
171
  # 'I' IP address mode: IP address -* Vulnerability
172
172
  when 'I'
173
- prepare_create_tickets_by_ip(vulnerability_list)
173
+ prepare_create_tickets_by_ip(vulnerability_list, site_id)
174
+ # 'V' Vulnerability mode: Vulnerability -* IP address
175
+ when 'V'
176
+ prepare_create_tickets_by_vulnerability(vulnerability_list, site_id)
174
177
  else
175
178
  fail 'No ticketing mode selected.'
176
179
  end
177
180
  end
181
+
182
+ # Prepare to update tickets from the CSV of vulnerabilities exported from Nexpose. This method determines
183
+ # how to prepare the tickets for update (by IP address or by vulnerability) based on config options.
184
+ #
185
+ # * *Args* :
186
+ # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
187
+ #
188
+ # * *Returns* :
189
+ # - List of savon-formated (hash) tickets for creating within Remedy.
190
+ #
191
+ def prepare_update_tickets(vulnerability_list, site_id)
192
+ @ticket = Hash.new(-1)
193
+ case @options[:ticket_mode]
194
+ # 'I' IP address mode: IP address -* Vulnerability
195
+ when 'I'
196
+ prepare_update_tickets_by_ip(vulnerability_list, site_id)
197
+ # 'V' Vulnerability mode: Vulnerability -* IP address
198
+ when 'V'
199
+ prepare_update_tickets_by_vulnerability(vulnerability_list, site_id)
200
+ else
201
+ fail 'No ticketing mode selected.'
202
+ end
203
+ end
178
204
 
179
205
  # Prepares a list of vulnerabilities into a list of savon-formatted tickets (incidents) for
180
206
  # Remedy. The preparation by default means that each vulnerability within Nexpose is a
@@ -187,7 +213,7 @@ class RemedyHelper
187
213
  # * *Returns* :
188
214
  # - List of savon-formated (hash) tickets for creating within Remedy.
189
215
  #
190
- def prepare_create_tickets_default(vulnerability_list)
216
+ def prepare_create_tickets_default(vulnerability_list, site_id)
191
217
  @log.log_message("Preparing tickets by default method.")
192
218
  tickets = []
193
219
  CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
@@ -203,7 +229,7 @@ class RemedyHelper
203
229
  'Action' => 'CREATE',
204
230
  'Summary' => "#{row['ip_address']} => #{row['summary']}",
205
231
  'Notes' => "Summary: #{row['summary']} \n\nFix: #{row['fix']} \n\nURL: #{row['url']}
206
- \n\nNXID: #{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}",
232
+ \n\nNXID: #{site_id}#{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}",
207
233
  'Urgency' => '1-Critical'
208
234
  }
209
235
  tickets.push(ticket)
@@ -222,14 +248,14 @@ class RemedyHelper
222
248
  # * *Returns* :
223
249
  # - List of savon-formated (hash) tickets for creating within Remedy.
224
250
  #
225
- def prepare_create_tickets_by_ip(vulnerability_list)
226
- @log.log_message("Preparing tickets by IP address.")
251
+ def prepare_create_tickets_by_ip(vulnerability_list, site_id)
252
+ @log.log_message('Preparing tickets by IP address.')
227
253
  tickets = []
228
254
  current_ip = -1
229
255
  CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
230
256
  if current_ip == -1
231
257
  current_ip = row['ip_address']
232
- @log.log_message("Creating ticket with IP address: #{row['ip_address']}")
258
+ @log.log_message("Creating ticket with IP address: #{row['ip_address']}, Asset ID: #{row['asset_id']} and Site ID: #{site_id}")
233
259
  @ticket = {
234
260
  'First_Name' => "#{@remedy_data[:first_name]}",
235
261
  'Impact' => '1-Extensive/Widespread',
@@ -253,18 +279,130 @@ class RemedyHelper
253
279
  end
254
280
  unless current_ip == row['ip_address']
255
281
  # NXID in the work_notes is the unique identifier used to query incidents to update them.
256
- @ticket['Notes'] += "\n\nNXID: #{current_ip}"
282
+ @ticket['Notes'] += "\n\nNXID: #{site_id}#{current_ip}"
257
283
  tickets.push(@ticket)
258
284
  current_ip = -1
259
285
  redo
260
286
  end
261
287
  end
262
288
  # NXID in the work_notes is the unique identifier used to query incidents to update them.
263
- @ticket['Notes'] += "\n\nNXID: #{current_ip}"
289
+ @ticket['Notes'] += "\n\nNXID: #{site_id}#{current_ip}"
264
290
  tickets.push(@ticket) unless @ticket.nil?
265
291
  tickets
266
292
  end
267
-
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.
299
+ #
300
+ # * *Args* :
301
+ # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
302
+ #
303
+ # * *Returns* :
304
+ # - List of savon-formated (hash) tickets for creating within Remedy.
305
+ #
306
+ def prepare_create_tickets_by_vulnerability(vulnerability_list, site_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 Site ID: #{site_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: #{site_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: #{site_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
+
268
406
  # Prepare ticket updates from the CSV of vulnerabilities exported from Nexpose. This method
269
407
  # currently only supports updating IP-address mode tickets in Remedy. The list of vulnerabilities
270
408
  # are ordered by IP address and then by ticket_status, allowing the method to loop through and
@@ -275,11 +413,11 @@ class RemedyHelper
275
413
  # * *Returns* :
276
414
  # - List of savon-formated (hash) tickets for updating within Remedy.
277
415
  #
278
- def prepare_update_tickets(vulnerability_list)
279
- fail 'Ticket updates are only supported in IP-address mode.' if @options[:ticket_mode] == 'D'
416
+ def prepare_update_tickets_by_ip(vulnerability_list, site_id)
417
+ fail 'Ticket updates are only supported in IP-address mode.' if @options[:ticket_mode] != 'I'
280
418
  @ticket = Hash.new(-1)
281
419
 
282
- @log.log_message("Preparing ticket updates by IP address.")
420
+ @log.log_message('Preparing ticket updates by IP address.')
283
421
  tickets = []
284
422
  current_ip = -1
285
423
  ticket_status = 'New'
@@ -289,70 +427,25 @@ class RemedyHelper
289
427
  ticket_status = row['comparison']
290
428
 
291
429
  # Query Remedy for the incident by unique id (generated NXID)
292
- queried_incident = query_for_ticket("NXID: #{row['ip_address']}")
430
+ queried_incident = query_for_ticket("NXID: #{site_id}#{row['ip_address']}")
293
431
  if queried_incident.nil? || queried_incident.empty?
294
- @log.log_message("No incident found for NXID: #{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}")
432
+ @log.log_message("No incident found for NXID: #{site_id}#{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}")
295
433
  else
296
- @log.log_message("Creating ticket update with IP address: #{row['ip_address']}")
434
+ @log.log_message("Creating ticket update with IP address: #{row['ip_address']} for site with ID: #{site_id}")
297
435
  @log.log_message("Ticket status #{ticket_status}")
298
436
  # Remedy incident updates require populating all fields.
299
- @ticket = {
300
- 'Categorization_Tier_1' => queried_incident[:categorization_tier_1],
301
- 'Categorization_Tier_2' => queried_incident[:categorization_tier_2],
302
- 'Categorization_Tier_3' => queried_incident[:categorization_tier_3],
303
- 'Closure_Manufacturer' => queried_incident[:closure_manufacturer],
304
- 'Closure_Product_Category_Tier1' => queried_incident[:closure_product_category_tier1],
305
- 'Closure_Product_Category_Tier2' => queried_incident[:closure_product_category_tier2],
306
- 'Closure_Product_Category_Tier3' => queried_incident[:closure_product_category_tier3],
307
- 'Closure_Product_Model_Version' => queried_incident[:closure_product_model_version],
308
- 'Closure_Product_Name' => queried_incident[:closure_product_name],
309
- 'Company' => queried_incident[:company],
310
- 'Summary' => queried_incident[:summary],
311
- 'Notes' => "++ #{row['comparison']} Vulnerabilities +++++++++++++++++++++++++++++++++++++\n",
312
- 'Impact' => queried_incident[:impact],
313
- 'Manufacturer' => queried_incident[:manufacturer],
314
- 'Product_Categorization_Tier_1' => queried_incident[:product_categorization_tier_1],
315
- 'Product_Categorization_Tier_2' => queried_incident[:product_categorization_tier_2],
316
- 'Product_Categorization_Tier_3' => queried_incident[:product_categorization_tier_3],
317
- 'Product_Model_Version' => queried_incident[:product_model_version],
318
- 'Product_Name' => queried_incident[:product_name],
319
- 'Reported_Source' => queried_incident[:reported_source],
320
- 'Resolution' => queried_incident[:resolution],
321
- 'Resolution_Category' => queried_incident[:resolution_category],
322
- 'Resolution_Category_Tier_2' => queried_incident[:resolution_category_tier_2],
323
- 'Resolution_Category_Tier_3' => queried_incident[:resolution_category_tier_3],
324
- 'Resolution_Method' => queried_incident[:resolution_method],
325
- 'Service_Type' => queried_incident[:service_type],
326
- 'Status' => queried_incident[:status],
327
- 'Urgency' => queried_incident[:urgency],
328
- 'Action' => 'MODIFY',
329
- 'Work_Info_Summary' => queried_incident[:work_info_summary],
330
- 'Work_Info_Notes' => queried_incident[:work_info_notes],
331
- 'Work_Info_Type' => queried_incident[:work_info_type],
332
- 'Work_Info_Date' => queried_incident[:work_info_date],
333
- 'Work_Info_Source' => queried_incident[:work_info_source],
334
- 'Work_Info_Locked' => queried_incident[:work_info_locked],
335
- 'Work_Info_View_Access' => queried_incident[:work_info_view_access],
336
- 'Incident_Number' => queried_incident[:incident_number],
337
- 'Status_Reason' => queried_incident[:status_reason],
338
- 'ServiceCI' => queried_incident[:service_ci],
339
- 'ServiceCI_ReconID' => queried_incident[:service_ci_recon_id],
340
- 'HPD_CI' => queried_incident[:hpd_ci],
341
- 'HPD_CI_ReconID' => queried_incident[:hpd_ci_recon_id],
342
- 'HPD_CI_FormName' => queried_incident[:hpd_ci_form_name],
343
- 'z1D_CI_FormName' => queried_incident[:z1d_ci_form_name]
344
- }
345
- end
437
+ @ticket = extract_queried_incident(queried_incident, "++ #{row['comparison']} Vulnerabilities ++++++++++++++++++++++++++\n")
438
+ end
346
439
  end
347
440
  if current_ip == row['ip_address']
348
441
  # If the ticket_status is different, add a a new 'header' to signify a new block of tickets.
349
442
  unless ticket_status == row['comparison']
350
443
  @ticket['Notes'] +=
351
- "\n\n\n++ #{row['comparison']} Vulnerabilities +++++++++++++++++++++++++++++++++++++\n"
444
+ "\n\n\n++ #{row['comparison']} Vulnerabilities ++++++++++++++++++++++++++\n"
352
445
  ticket_status = row['comparison']
353
446
  end
354
-
355
- @ticket['Notes'] +=
447
+
448
+ @ticket['Notes'] +=
356
449
  "\n\n========================================== \nSummary: #{row['summary']} \nFix: #{row['fix']}"
357
450
  # Only add the URL block if data exists in the row.
358
451
  unless row['url'].nil?
@@ -362,20 +455,236 @@ class RemedyHelper
362
455
  end
363
456
  unless current_ip == row['ip_address']
364
457
  # NXID in the work_notes is the unique identifier used to query incidents to update them.
365
- @ticket['Notes'] += "\n\nNXID: #{current_ip}"
458
+ @ticket['Notes'] += "\n\nNXID: #{site_id}#{current_ip}"
366
459
  tickets.push(@ticket)
367
460
  current_ip = -1
368
461
  redo
369
462
  end
370
463
  end
371
464
  # NXID in the work_notes is the unique identifier used to query incidents to update them.
372
- @ticket['Notes'] += "\n\nNXID: #{current_ip}"
373
- tickets.push(@ticket) unless @ticket.nil?
465
+ @ticket['Notes'] += "\n\nNXID: #{site_id}#{current_ip}" unless @ticket.empty?
466
+ tickets.push(@ticket) unless @ticket.nil? || @ticket.empty?
374
467
  tickets
375
468
  end
376
-
377
- # Prepare ticket closures from the CSV of vulnerabilities exported from Nexpose. This method
378
- # currently only supports updating default mode tickets in ServiceNow.
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, site_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: #{site_id}#{row['asset_id']}#{row['vulnerability_id']}")
501
+ if queried_incident.nil? || queried_incident.empty?
502
+ @log.log_message("No incident found for NXID: #{site_id}#{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}. Creating...")
503
+ new_ticket_csv = vulnerability_list.split("\n").first
504
+ new_ticket_csv += "\n#{row.to_s}"
505
+ new_ticket = prepare_create_tickets_by_vulnerability(new_ticket_csv, site_id)
506
+ @log.log_message('Created ticket. Sending to Remedy...')
507
+ create_tickets(new_ticket)
508
+ @log.log_message('Ticket sent. Performing update for ticket...')
509
+ #Now that there is a ticket for this NXID update it as if it existed this whole time...
510
+ current_vuln_id = -1
511
+ redo
512
+ else
513
+ @log.log_message("Creating ticket update for vulnerability with ID: #{row['vulnerability_id']}, Asset ID: #{row['asset_id']} and Site ID: #{site_id}. Ticket status #{ticket_status}.")
514
+ # 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
525
+
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: #{site_id}#{current_asset_id}#{current_vuln_id}"
552
+ tickets.push(@ticket)
553
+ current_vuln_id = -1
554
+ current_solution_id = -1
555
+ current_asset_id = -1
556
+ redo
557
+ end
558
+ 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: #{site_id}#{current_asset_id}#{current_vuln_id}" unless @ticket.empty?
562
+ tickets.push(@ticket) unless @ticket.nil? || @ticket.empty?
563
+ tickets
564
+ end
565
+
566
+ # Extracts from a queried Remedy incident the relevant data required for an update to be made to said incident.
567
+ # Creates a ticket with the extracted data.
568
+ #
569
+ # - +queried_incident+ - The queried incident from Remedy
570
+ # - +notes_header+ - The texted to be placed at the top of the Remedy 'Notes' field.
571
+ #
572
+ # * *Returns* :
573
+ # - A single savon-formated (hash) ticket for updating within Remedy.
574
+ #
575
+ 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
+ }
682
+ end
683
+ @ticket
684
+ end
685
+
686
+
687
+ # Prepare ticket closures from the CSV of vulnerabilities exported from Nexpose.
379
688
  #
380
689
  # * *Args* :
381
690
  # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
@@ -383,15 +692,29 @@ class RemedyHelper
383
692
  # * *Returns* :
384
693
  # - List of savon-formated (hash) tickets for closing within Remedy.
385
694
  #
386
- def prepare_close_tickets(vulnerability_list)
695
+ def prepare_close_tickets(vulnerability_list, site_id)
387
696
  fail 'Ticket closures are only supported in default mode.' if @options[:ticket_mode] == 'I'
388
- @log.log_message("Preparing ticket closures by default method.")
697
+ @log.log_message('Preparing ticket closures by default method.')
698
+ @nxid = nil
389
699
  tickets = []
390
700
  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 = "#{site_id}#{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}"
705
+ # 'I' IP address mode: IP address -* Vulnerability
706
+ when 'I'
707
+ @nxid = "#{site_id}#{row['current_ip']}"
708
+ # 'V' Vulnerability mode: Vulnerability -* IP address
709
+ when 'V'
710
+ @nxid = "#{site_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
391
714
  # Query Remedy for the incident by unique id (generated NXID)
392
- queried_incident = query_for_ticket("NXID: #{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}")
715
+ queried_incident = query_for_ticket("NXID: #{@nxid}")
393
716
  if queried_incident.nil? || queried_incident.empty?
394
- @log.log_message("No incident found for NXID: #{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}")
717
+ @log.log_message("No incident found for NXID: #{@nxid}")
395
718
  else
396
719
  # Remedy incident updates require populating all fields.
397
720
  ticket = {