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