nexpose_ticketing 0.0.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/{README.markdown → README.md} +43 -38
- data/bin/nexpose_jira +2 -3
- data/bin/nexpose_remedy +17 -0
- data/bin/nexpose_servicenow +17 -0
- data/lib/nexpose_ticketing.rb +3 -1
- data/lib/nexpose_ticketing/config/remedy.config +26 -0
- data/lib/nexpose_ticketing/config/remedy_wsdl/HPD_IncidentInterface_Create_WS.xml +296 -0
- data/lib/nexpose_ticketing/config/remedy_wsdl/HPD_IncidentInterface_WS.xml +675 -0
- data/lib/nexpose_ticketing/config/servicenow.config +18 -0
- data/lib/nexpose_ticketing/config/ticket_service.config +2 -2
- data/lib/nexpose_ticketing/helpers/jira_helper.rb +6 -6
- data/lib/nexpose_ticketing/helpers/remedy_helper.rb +448 -0
- data/lib/nexpose_ticketing/helpers/servicenow_helper.rb +315 -0
- data/lib/nexpose_ticketing/nx_logger.rb +35 -0
- data/lib/nexpose_ticketing/queries.rb +105 -14
- data/lib/nexpose_ticketing/ticket_repository.rb +57 -12
- data/lib/nexpose_ticketing/ticket_service.rb +56 -19
- data/nexpose_ticketing.gemspec +5 -4
- metadata +29 -4
@@ -0,0 +1,18 @@
|
|
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: ServiceNowHelper
|
8
|
+
|
9
|
+
# (M) ServiceNow url (currently requires using JSON)
|
10
|
+
:servicenow_url: https://myinstance.service-now.com/incident.do?JSON
|
11
|
+
# (M) Username for ServiceNow
|
12
|
+
:username: admin
|
13
|
+
# (M) Password for above username
|
14
|
+
:password: password
|
15
|
+
# (M) If 'Y', SSL connections will output to stderr (default is 'N')
|
16
|
+
:verbose_mode: N
|
17
|
+
# (M) Amount of times the helper will follow 301/302 redirections
|
18
|
+
:redirect_limit: 10
|
@@ -1,12 +1,12 @@
|
|
1
|
-
# This class serves as the JIRA interface
|
2
|
-
# that creates issues within JIRA from vulnerabilities
|
3
|
-
# found in Nexpose.
|
4
|
-
# Copyright:: Copyright (c) 2014 Rapid7, LLC.
|
5
1
|
require 'json'
|
6
2
|
require 'net/http'
|
7
3
|
require 'net/https'
|
8
4
|
require 'uri'
|
9
5
|
require 'csv'
|
6
|
+
# This class serves as the JIRA interface
|
7
|
+
# that creates issues within JIRA from vulnerabilities
|
8
|
+
# found in Nexpose.
|
9
|
+
# Copyright:: Copyright (c) 2014 Rapid7, LLC.
|
10
10
|
class JiraHelper
|
11
11
|
attr_accessor :jira_data, :options
|
12
12
|
def initialize(jira_data, options)
|
@@ -14,7 +14,7 @@ class JiraHelper
|
|
14
14
|
@options = options
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
17
|
+
def create_tickets(tickets)
|
18
18
|
fail 'Ticket(s) cannot be empty.' if tickets.empty? || tickets.nil?
|
19
19
|
tickets.each do |ticket|
|
20
20
|
headers = { 'Content-Type' => 'application/json',
|
@@ -33,7 +33,7 @@ class JiraHelper
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# Prepares tickets from the CSV.
|
36
|
-
def
|
36
|
+
def prepare_create_tickets(vulnerability_list)
|
37
37
|
@ticket = Hash.new(-1)
|
38
38
|
case @options[:ticket_mode]
|
39
39
|
# 'D' Default IP *-* Vulnerability
|
@@ -0,0 +1,448 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
require 'net/https'
|
4
|
+
require 'uri'
|
5
|
+
require 'csv'
|
6
|
+
require 'savon'
|
7
|
+
require 'nexpose_ticketing/nx_logger'
|
8
|
+
|
9
|
+
# Serves as the Remedy interface for creating/updating issues from
|
10
|
+
# vulnelrabilities found in Nexpose.
|
11
|
+
class RemedyHelper
|
12
|
+
attr_accessor :remedy_data, :options, :log, :client
|
13
|
+
def initialize(remedy_data, options)
|
14
|
+
@remedy_data = remedy_data
|
15
|
+
@options = options
|
16
|
+
@log = NexposeTicketing::NXLogger.new
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sends a list of tickets (in SOAP format) to Remedy individually (each ticket in the list
|
20
|
+
# as a separate web service call).
|
21
|
+
#
|
22
|
+
# * *Args* :
|
23
|
+
# - +tickets+ - List of savon-formatted (hash) ticket creates (new tickets).
|
24
|
+
#
|
25
|
+
def create_tickets(tickets)
|
26
|
+
fail 'Ticket(s) cannot be empty' if tickets.nil? || tickets.empty?
|
27
|
+
client = Savon.client(wsdl: File.join(File.dirname(__FILE__), '../config/remedy_wsdl/HPD_IncidentInterface_Create_WS.xml'),
|
28
|
+
ssl_verify_mode: :none,
|
29
|
+
open_timeout: @remedy_data[:open_timeout],
|
30
|
+
read_timeout: @remedy_data[:read_timeout],
|
31
|
+
endpoint: @remedy_data[:create_soap_endpoint],
|
32
|
+
soap_header: { 'AuthenticationInfo' =>
|
33
|
+
{ 'userName' => "#{@remedy_data[:username]}",
|
34
|
+
'password' => "#{@remedy_data[:password]}",
|
35
|
+
'authentication' => "#{@remedy_data[:authentication]}"
|
36
|
+
}
|
37
|
+
})
|
38
|
+
tickets.each do |ticket|
|
39
|
+
begin
|
40
|
+
@log.log_message(ticket)
|
41
|
+
response = client.call(:help_desk_submit_service, message: ticket)
|
42
|
+
rescue Savon::SOAPFault => e
|
43
|
+
@log.log_message("SOAP exception in create ticket: #{e.message}")
|
44
|
+
raise
|
45
|
+
rescue Savon::HTTPError => e
|
46
|
+
@log.log_message("HTTP error in create ticket: #{e.message}")
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Sends ticket updates (in SOAP format) to Remedy individually (each ticket in the list
|
53
|
+
# as a separate web service call).
|
54
|
+
#
|
55
|
+
# * *Args* :
|
56
|
+
# - +tickets+ - List of savon-formatted (hash) ticket updates.
|
57
|
+
#
|
58
|
+
def update_tickets(tickets)
|
59
|
+
if tickets.nil? || tickets.empty?
|
60
|
+
@log.log_message("No tickets to update.")
|
61
|
+
else
|
62
|
+
client = Savon.client(wsdl: File.join(File.dirname(__FILE__), '../config/remedy_wsdl/HPD_IncidentInterface_WS.xml'),
|
63
|
+
ssl_verify_mode: :none,
|
64
|
+
open_timeout: @remedy_data[:open_timeout],
|
65
|
+
read_timeout: @remedy_data[:read_timeout],
|
66
|
+
endpoint: @remedy_data[:query_modify_soap_endpoint],
|
67
|
+
soap_header: { 'AuthenticationInfo' =>
|
68
|
+
{ 'userName' => "#{@remedy_data[:username]}",
|
69
|
+
'password' => "#{@remedy_data[:password]}",
|
70
|
+
'authentication' => "#{@remedy_data[:authentication]}"
|
71
|
+
}
|
72
|
+
})
|
73
|
+
tickets.each do |ticket|
|
74
|
+
begin
|
75
|
+
response = client.call(:help_desk_modify_service, message: ticket)
|
76
|
+
rescue Savon::SOAPFault => e
|
77
|
+
@log.log_message("SOAP exception in create ticket: #{e.message}")
|
78
|
+
raise
|
79
|
+
rescue Savon::HTTPError => e
|
80
|
+
@log.log_message("HTTP error in create ticket: #{e.message}")
|
81
|
+
raise
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Sends ticket closure (in SOAP format) to Remedy individually (each ticket in the list
|
88
|
+
# as a separate web service call).
|
89
|
+
#
|
90
|
+
# * *Args* :
|
91
|
+
# - +tickets+ - List of savon-formatted (hash) ticket closures.
|
92
|
+
#
|
93
|
+
def close_tickets(tickets)
|
94
|
+
if tickets.nil? || tickets.empty?
|
95
|
+
@log.log_message("No tickets to close.")
|
96
|
+
else
|
97
|
+
client = Savon.client(wsdl: File.join(File.dirname(__FILE__), '../config/remedy_wsdl/HPD_IncidentInterface_WS.xml'),
|
98
|
+
ssl_verify_mode: :none,
|
99
|
+
open_timeout: @remedy_data[:open_timeout],
|
100
|
+
read_timeout: @remedy_data[:read_timeout],
|
101
|
+
endpoint: @remedy_data[:query_modify_soap_endpoint],
|
102
|
+
soap_header: { 'AuthenticationInfo' =>
|
103
|
+
{ 'userName' => "#{@remedy_data[:username]}",
|
104
|
+
'password' => "#{@remedy_data[:password]}",
|
105
|
+
'authentication' => "#{@remedy_data[:authentication]}"
|
106
|
+
}
|
107
|
+
})
|
108
|
+
tickets.each do |ticket|
|
109
|
+
begin
|
110
|
+
response = client.call(:help_desk_modify_service, message: ticket)
|
111
|
+
rescue Savon::SOAPFault => e
|
112
|
+
@log.log_message("SOAP exception in create ticket: #{e.message}")
|
113
|
+
raise
|
114
|
+
rescue Savon::HTTPError => e
|
115
|
+
@log.log_message("HTTP error in create ticket: #{e.message}")
|
116
|
+
raise
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Sends a query (in SOAP format) to Remedy to return back a single ticket based on the criteria.
|
123
|
+
#
|
124
|
+
# * *Args* :
|
125
|
+
# - +unique_id+ - Unique identifier generated by the helper.
|
126
|
+
#
|
127
|
+
# * *Returns* :
|
128
|
+
# - Remedy incident information in hash format or nil if no results are found.
|
129
|
+
#
|
130
|
+
def query_for_ticket(unique_id)
|
131
|
+
client = Savon.client(wsdl: File.join(File.dirname(__FILE__), '../config/remedy_wsdl/HPD_IncidentInterface_WS.xml'),
|
132
|
+
ssl_verify_mode: :none,
|
133
|
+
open_timeout: @remedy_data[:open_timeout],
|
134
|
+
read_timeout: @remedy_data[:read_timeout],
|
135
|
+
endpoint: @remedy_data[:query_modify_soap_endpoint],
|
136
|
+
soap_header: { 'AuthenticationInfo' =>
|
137
|
+
{ 'userName' => "#{@remedy_data[:username]}",
|
138
|
+
'password' => "#{@remedy_data[:password]}",
|
139
|
+
'authentication' => "#{@remedy_data[:authentication]}"
|
140
|
+
}
|
141
|
+
})
|
142
|
+
begin
|
143
|
+
response = client.call(:help_desk_query_list_service, message: {'Qualification' => "'Detailed Decription' LIKE \"%#{unique_id}%\""})
|
144
|
+
rescue Savon::SOAPFault => e
|
145
|
+
@log.log_message("SOAP exception in query ticket: #{e.message}")
|
146
|
+
return if e.to_hash[:fault][:faultstring].index("ERROR (302)") == 0
|
147
|
+
raise
|
148
|
+
rescue Savon::HTTPError => e
|
149
|
+
@log.log_message("HTTP error in query ticket: #{e.message}")
|
150
|
+
raise
|
151
|
+
end
|
152
|
+
|
153
|
+
response.body[:help_desk_query_list_service_response][:get_list_values]
|
154
|
+
end
|
155
|
+
|
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.
|
158
|
+
#
|
159
|
+
# * *Args* :
|
160
|
+
# - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
|
161
|
+
#
|
162
|
+
# * *Returns* :
|
163
|
+
# - List of savon-formated (hash) tickets for creating within Remedy.
|
164
|
+
#
|
165
|
+
def prepare_create_tickets(vulnerability_list)
|
166
|
+
@ticket = Hash.new(-1)
|
167
|
+
case @options[:ticket_mode]
|
168
|
+
# 'D' Default mode: IP *-* Vulnerability
|
169
|
+
when 'D'
|
170
|
+
prepare_create_tickets_default(vulnerability_list)
|
171
|
+
# 'I' IP address mode: IP address -* Vulnerability
|
172
|
+
when 'I'
|
173
|
+
prepare_create_tickets_by_ip(vulnerability_list)
|
174
|
+
else
|
175
|
+
fail 'No ticketing mode selected.'
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Prepares a list of vulnerabilities into a list of savon-formatted tickets (incidents) for
|
180
|
+
# Remedy. The preparation by default means that each vulnerability within Nexpose is a
|
181
|
+
# separate incident within Remedy. This makes for smaller, more actionalble incidents but
|
182
|
+
# could lead to a very large total number of incidents.
|
183
|
+
#
|
184
|
+
# * *Args* :
|
185
|
+
# - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
|
186
|
+
#
|
187
|
+
# * *Returns* :
|
188
|
+
# - List of savon-formated (hash) tickets for creating within Remedy.
|
189
|
+
#
|
190
|
+
def prepare_create_tickets_default(vulnerability_list)
|
191
|
+
@log.log_message("Preparing tickets by default method.")
|
192
|
+
tickets = []
|
193
|
+
CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
|
194
|
+
# NXID in the notes is a unique identifier used to query incidents to update/resolve
|
195
|
+
# incidents as they are resolved in Nexpose.
|
196
|
+
ticket = {
|
197
|
+
'First_Name' => "#{@remedy_data[:first_name]}",
|
198
|
+
'Impact' => '1-Extensive/Widespread',
|
199
|
+
'Last_Name' => "#{@remedy_data[:last_name]}",
|
200
|
+
'Reported_Source' => 'Other',
|
201
|
+
'Service_Type' => 'Infrastructure Event',
|
202
|
+
'Status' => 'New',
|
203
|
+
'Action' => 'CREATE',
|
204
|
+
'Summary' => "#{row['ip_address']} => #{row['summary']}",
|
205
|
+
'Notes' => "Summary: #{row['summary']} \n\nFix: #{row['fix']} \n\nURL: #{row['url']}
|
206
|
+
\n\nNXID: #{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}",
|
207
|
+
'Urgency' => '1-Critical'
|
208
|
+
}
|
209
|
+
tickets.push(ticket)
|
210
|
+
end
|
211
|
+
tickets
|
212
|
+
end
|
213
|
+
|
214
|
+
# Prepares a list of vulnerabilities into a list of savon-formatted tickets (incidents) for
|
215
|
+
# Remedy. The preparation by IP means that all vulnerabilities within Nexpose for one IP
|
216
|
+
# address are consolidated into a single Remedy incident. This reduces the number of incidents
|
217
|
+
# within ServiceNow but greatly increases the size of the work notes.
|
218
|
+
#
|
219
|
+
# * *Args* :
|
220
|
+
# - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
|
221
|
+
#
|
222
|
+
# * *Returns* :
|
223
|
+
# - List of savon-formated (hash) tickets for creating within Remedy.
|
224
|
+
#
|
225
|
+
def prepare_create_tickets_by_ip(vulnerability_list)
|
226
|
+
@log.log_message("Preparing tickets by IP address.")
|
227
|
+
tickets = []
|
228
|
+
current_ip = -1
|
229
|
+
CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
|
230
|
+
if current_ip == -1
|
231
|
+
current_ip = row['ip_address']
|
232
|
+
@log.log_message("Creating ticket with IP address: #{row['ip_address']}")
|
233
|
+
@ticket = {
|
234
|
+
'First_Name' => "#{@remedy_data[:first_name]}",
|
235
|
+
'Impact' => '1-Extensive/Widespread',
|
236
|
+
'Last_Name' => "#{@remedy_data[:last_name]}",
|
237
|
+
'Reported_Source' => 'Other',
|
238
|
+
'Service_Type' => 'Infrastructure Event',
|
239
|
+
'Status' => 'New',
|
240
|
+
'Action' => 'CREATE',
|
241
|
+
'Summary' => "#{row['ip_address']} => Vulnerabilities",
|
242
|
+
'Notes' => "++ New Vulnerabilities +++++++++++++++++++++++++++++++++++++\n",
|
243
|
+
'Urgency' => '1-Critical'
|
244
|
+
}
|
245
|
+
end
|
246
|
+
if current_ip == row['ip_address']
|
247
|
+
@ticket['Notes'] +=
|
248
|
+
"\n\n========================================== \nSummary: #{row['summary']} \nFix: #{row['fix']}"
|
249
|
+
unless row['url'].nil?
|
250
|
+
@ticket['Notes'] +=
|
251
|
+
"\nURL: #{row['url']}"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
unless current_ip == row['ip_address']
|
255
|
+
# NXID in the work_notes is the unique identifier used to query incidents to update them.
|
256
|
+
@ticket['Notes'] += "\n\nNXID: #{current_ip}"
|
257
|
+
tickets.push(@ticket)
|
258
|
+
current_ip = -1
|
259
|
+
redo
|
260
|
+
end
|
261
|
+
end
|
262
|
+
# NXID in the work_notes is the unique identifier used to query incidents to update them.
|
263
|
+
@ticket['Notes'] += "\n\nNXID: #{current_ip}"
|
264
|
+
tickets.push(@ticket) unless @ticket.nil?
|
265
|
+
tickets
|
266
|
+
end
|
267
|
+
|
268
|
+
# Prepare ticket updates from the CSV of vulnerabilities exported from Nexpose. This method
|
269
|
+
# currently only supports updating IP-address mode tickets in Remedy. The list of vulnerabilities
|
270
|
+
# are ordered by IP address and then by ticket_status, allowing the method to loop through and
|
271
|
+
# display new, old, and same vulnerabilities in that order.
|
272
|
+
#
|
273
|
+
# - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
|
274
|
+
#
|
275
|
+
# * *Returns* :
|
276
|
+
# - List of savon-formated (hash) tickets for updating within Remedy.
|
277
|
+
#
|
278
|
+
def prepare_update_tickets(vulnerability_list)
|
279
|
+
fail 'Ticket updates are only supported in IP-address mode.' if @options[:ticket_mode] == 'D'
|
280
|
+
@ticket = Hash.new(-1)
|
281
|
+
|
282
|
+
@log.log_message("Preparing ticket updates by IP address.")
|
283
|
+
tickets = []
|
284
|
+
current_ip = -1
|
285
|
+
ticket_status = 'New'
|
286
|
+
CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
|
287
|
+
if current_ip == -1
|
288
|
+
current_ip = row['ip_address']
|
289
|
+
ticket_status = row['comparison']
|
290
|
+
|
291
|
+
# Query Remedy for the incident by unique id (generated NXID)
|
292
|
+
queried_incident = query_for_ticket("NXID: #{row['ip_address']}")
|
293
|
+
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']}")
|
295
|
+
else
|
296
|
+
@log.log_message("Creating ticket update with IP address: #{row['ip_address']}")
|
297
|
+
@log.log_message("Ticket status #{ticket_status}")
|
298
|
+
# 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
|
346
|
+
end
|
347
|
+
if current_ip == row['ip_address']
|
348
|
+
# If the ticket_status is different, add a a new 'header' to signify a new block of tickets.
|
349
|
+
unless ticket_status == row['comparison']
|
350
|
+
@ticket['Notes'] +=
|
351
|
+
"\n\n\n++ #{row['comparison']} Vulnerabilities +++++++++++++++++++++++++++++++++++++\n"
|
352
|
+
ticket_status = row['comparison']
|
353
|
+
end
|
354
|
+
|
355
|
+
@ticket['Notes'] +=
|
356
|
+
"\n\n========================================== \nSummary: #{row['summary']} \nFix: #{row['fix']}"
|
357
|
+
# Only add the URL block if data exists in the row.
|
358
|
+
unless row['url'].nil?
|
359
|
+
@ticket['Notes'] +=
|
360
|
+
"\nURL: #{row['url']}"
|
361
|
+
end
|
362
|
+
end
|
363
|
+
unless current_ip == row['ip_address']
|
364
|
+
# NXID in the work_notes is the unique identifier used to query incidents to update them.
|
365
|
+
@ticket['Notes'] += "\n\nNXID: #{current_ip}"
|
366
|
+
tickets.push(@ticket)
|
367
|
+
current_ip = -1
|
368
|
+
redo
|
369
|
+
end
|
370
|
+
end
|
371
|
+
# 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?
|
374
|
+
tickets
|
375
|
+
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.
|
379
|
+
#
|
380
|
+
# * *Args* :
|
381
|
+
# - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
|
382
|
+
#
|
383
|
+
# * *Returns* :
|
384
|
+
# - List of savon-formated (hash) tickets for closing within Remedy.
|
385
|
+
#
|
386
|
+
def prepare_close_tickets(vulnerability_list)
|
387
|
+
fail 'Ticket closures are only supported in default mode.' if @options[:ticket_mode] == 'I'
|
388
|
+
@log.log_message("Preparing ticket closures by default method.")
|
389
|
+
tickets = []
|
390
|
+
CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
|
391
|
+
# 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']}")
|
393
|
+
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']}")
|
395
|
+
else
|
396
|
+
# Remedy incident updates require populating all fields.
|
397
|
+
ticket = {
|
398
|
+
'Categorization_Tier_1' => queried_incident[:categorization_tier_1],
|
399
|
+
'Categorization_Tier_2' => queried_incident[:categorization_tier_2],
|
400
|
+
'Categorization_Tier_3' => queried_incident[:categorization_tier_3],
|
401
|
+
'Closure_Manufacturer' => queried_incident[:closure_manufacturer],
|
402
|
+
'Closure_Product_Category_Tier1' => queried_incident[:closure_product_category_tier1],
|
403
|
+
'Closure_Product_Category_Tier2' => queried_incident[:closure_product_category_tier2],
|
404
|
+
'Closure_Product_Category_Tier3' => queried_incident[:closure_product_category_tier3],
|
405
|
+
'Closure_Product_Model_Version' => queried_incident[:closure_product_model_version],
|
406
|
+
'Closure_Product_Name' => queried_incident[:closure_product_name],
|
407
|
+
'Company' => queried_incident[:company],
|
408
|
+
'Summary' => queried_incident[:summary],
|
409
|
+
'Notes' => queried_incident[:notes],
|
410
|
+
'Impact' => queried_incident[:impact],
|
411
|
+
'Manufacturer' => queried_incident[:manufacturer],
|
412
|
+
'Product_Categorization_Tier_1' => queried_incident[:product_categorization_tier_1],
|
413
|
+
'Product_Categorization_Tier_2' => queried_incident[:product_categorization_tier_2],
|
414
|
+
'Product_Categorization_Tier_3' => queried_incident[:product_categorization_tier_3],
|
415
|
+
'Product_Model_Version' => queried_incident[:product_model_version],
|
416
|
+
'Product_Name' => queried_incident[:product_name],
|
417
|
+
'Reported_Source' => queried_incident[:reported_source],
|
418
|
+
'Resolution' => queried_incident[:resolution],
|
419
|
+
'Resolution_Category' => queried_incident[:resolution_category],
|
420
|
+
'Resolution_Category_Tier_2' => queried_incident[:resolution_category_tier_2],
|
421
|
+
'Resolution_Category_Tier_3' => queried_incident[:resolution_category_tier_3],
|
422
|
+
'Resolution_Method' => queried_incident[:resolution_method],
|
423
|
+
'Service_Type' => queried_incident[:service_type],
|
424
|
+
'Status' => 'Closed',
|
425
|
+
'Urgency' => queried_incident[:urgency],
|
426
|
+
'Action' => 'MODIFY',
|
427
|
+
'Work_Info_Summary' => queried_incident[:work_info_summary],
|
428
|
+
'Work_Info_Notes' => queried_incident[:work_info_notes],
|
429
|
+
'Work_Info_Type' => queried_incident[:work_info_type],
|
430
|
+
'Work_Info_Date' => queried_incident[:work_info_date],
|
431
|
+
'Work_Info_Source' => queried_incident[:work_info_source],
|
432
|
+
'Work_Info_Locked' => queried_incident[:work_info_locked],
|
433
|
+
'Work_Info_View_Access' => queried_incident[:work_info_view_access],
|
434
|
+
'Incident_Number' => queried_incident[:incident_number],
|
435
|
+
'Status_Reason' => queried_incident[:status_reason],
|
436
|
+
'ServiceCI' => queried_incident[:service_ci],
|
437
|
+
'ServiceCI_ReconID' => queried_incident[:service_ci_recon_id],
|
438
|
+
'HPD_CI' => queried_incident[:hpd_ci],
|
439
|
+
'HPD_CI_ReconID' => queried_incident[:hpd_ci_recon_id],
|
440
|
+
'HPD_CI_FormName' => queried_incident[:hpd_ci_form_name],
|
441
|
+
'z1D_CI_FormName' => queried_incident[:z1d_ci_form_name]
|
442
|
+
}
|
443
|
+
tickets.push(ticket)
|
444
|
+
end
|
445
|
+
end
|
446
|
+
tickets
|
447
|
+
end
|
448
|
+
end
|