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 +4 -4
- data/bin/nexpose_servicedesk +17 -0
- data/lib/nexpose_ticketing/config/servicedesk.config +19 -0
- data/lib/nexpose_ticketing/config/servicenow.config +1 -1
- data/lib/nexpose_ticketing/config/ticket_service.config +15 -5
- data/lib/nexpose_ticketing/helpers/jira_helper.rb +5 -5
- data/lib/nexpose_ticketing/helpers/remedy_helper.rb +401 -78
- data/lib/nexpose_ticketing/helpers/servicedesk_helper.rb +337 -0
- data/lib/nexpose_ticketing/helpers/servicenow_helper.rb +68 -52
- data/lib/nexpose_ticketing/queries.rb +251 -23
- data/lib/nexpose_ticketing/ticket_repository.rb +151 -8
- data/lib/nexpose_ticketing/ticket_service.rb +80 -37
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d994e0ee3fa0a54776a378818816102c883e7e91
|
4
|
+
data.tar.gz: 89ee1701ef6ecbeaab92bcecc45ac3b8c06f2831
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
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
|
-
- '
|
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
|
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
|
-
#
|
18
|
-
#
|
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:
|
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 (
|
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(
|
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
|
279
|
-
fail 'Ticket updates are only supported in IP-address mode.' if @options[:ticket_mode]
|
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(
|
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
|
-
|
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
|
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
|
378
|
-
# currently only supports updating
|
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(
|
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: #{
|
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: #{
|
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 = {
|