nexpose_ticketing 0.0.1 → 0.2.1
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 +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
|