nexpose_ticketing 0.3.1 → 0.5.0

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: 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 = {