nexpose_ticketing 1.3.0 → 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +44 -12
- data/lib/nexpose_ticketing/config/servicenow.config +1 -1
- data/lib/nexpose_ticketing/config/servicenow_updateset/Rapid7_Nexpose_Ticketing_ServiceNow_Update_Set_v2.xml +1060 -0
- data/lib/nexpose_ticketing/config/ticket_service.config +3 -1
- data/lib/nexpose_ticketing/helpers/servicenow_helper.rb +276 -192
- data/lib/nexpose_ticketing/modes/base_mode.rb +29 -6
- data/lib/nexpose_ticketing/modes/default_mode.rb +2 -2
- data/lib/nexpose_ticketing/modes/vulnerability_mode.rb +7 -2
- data/lib/nexpose_ticketing/queries.rb +101 -42
- data/lib/nexpose_ticketing/store.rb +45 -0
- data/lib/nexpose_ticketing/ticket_metrics.rb +1 -1
- data/lib/nexpose_ticketing/ticket_repository.rb +627 -61
- data/lib/nexpose_ticketing/ticket_service.rb +100 -62
- data/lib/nexpose_ticketing/version.rb +1 -1
- metadata +28 -7
- data/lib/nexpose_ticketing/config/servicenow_updateset/Rapid7_Nexpose_Ticketing_ServiceNow_update_set.xml +0 -4488
@@ -33,7 +33,9 @@
|
|
33
33
|
# Timeout in seconds. The number of seconds the GEM waits for a response from Nexpose before exiting.
|
34
34
|
:timeout: 10800
|
35
35
|
# Ticket batching. Breaks ticket processing into groups of value size controlling resource utilisation of both systems.
|
36
|
-
:batch_size:
|
36
|
+
:batch_size: 5000
|
37
|
+
# Ticket batching. Imposes a limit on the number of tickets created per batch.
|
38
|
+
:batch_ticket_limit: 50
|
37
39
|
# (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
|
38
40
|
# open for a user to manually close or change the status.
|
39
41
|
:close_old_tickets_on_update: Y
|
@@ -7,167 +7,22 @@ require 'nexpose_ticketing/nx_logger'
|
|
7
7
|
require 'nexpose_ticketing/version'
|
8
8
|
require_relative './base_helper'
|
9
9
|
require 'securerandom'
|
10
|
+
require 'typhoeus'
|
10
11
|
|
11
12
|
# Serves as the ServiceNow interface for creating/updating issues from
|
12
|
-
#
|
13
|
+
# vulnerabilities found in Nexpose.
|
13
14
|
class ServiceNowHelper < BaseHelper
|
15
|
+
|
16
|
+
NEW_STATE = 1
|
17
|
+
RESOLVED_STATE = 6
|
18
|
+
CLOSED_STATE = 7
|
19
|
+
|
14
20
|
attr_accessor :log, :transform
|
15
21
|
def initialize(service_data, options, mode)
|
16
22
|
super(service_data, options, mode)
|
17
23
|
end
|
18
24
|
|
19
|
-
#
|
20
|
-
# as a separate HTTP post).
|
21
|
-
#
|
22
|
-
# * *Args* :
|
23
|
-
# - +tickets+ - List of JSON-formatted ticket creates (new tickets).
|
24
|
-
#
|
25
|
-
def create_tickets(tickets)
|
26
|
-
fail 'Ticket(s) cannot be empty' if tickets.nil? || tickets.empty?
|
27
|
-
|
28
|
-
tickets.each do |ticket|
|
29
|
-
send_ticket(ticket, @service_data[:servicenow_url], @service_data[:redirect_limit])
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
# Sends ticket updates (in JSON format) to ServiceNow individually (each ticket in the list as a
|
34
|
-
# separate HTTP post).
|
35
|
-
#
|
36
|
-
# * *Args* :
|
37
|
-
# - +tickets+ - List of JSON-formatted ticket updates.
|
38
|
-
#
|
39
|
-
def update_tickets(tickets)
|
40
|
-
if tickets.nil? || tickets.empty?
|
41
|
-
@log.log_message('No tickets to update.')
|
42
|
-
return
|
43
|
-
end
|
44
|
-
tickets.each do |ticket|
|
45
|
-
send_ticket(ticket, @service_data[:servicenow_url], @service_data[:redirect_limit])
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# Sends ticket closure (in JSON format) to ServiceNow individually (each ticket in the list as a
|
50
|
-
# separate HTTP post).
|
51
|
-
#
|
52
|
-
# * *Args* :
|
53
|
-
# - +tickets+ - List of JSON-formatted ticket closures.
|
54
|
-
#
|
55
|
-
def close_tickets(tickets)
|
56
|
-
if tickets.nil? || tickets.empty?
|
57
|
-
@log.log_message('No tickets to close.')
|
58
|
-
return
|
59
|
-
end
|
60
|
-
@metrics.closed tickets.count
|
61
|
-
|
62
|
-
tickets.each do |ticket|
|
63
|
-
send_ticket(ticket, @service_data[:servicenow_url], @service_data[:redirect_limit])
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Retrieves the unique ticket identifier for a particular NXID if one exists.
|
68
|
-
#
|
69
|
-
# * *Args* :
|
70
|
-
# - +nxid+ - NXID of the ticket to be updated.
|
71
|
-
#
|
72
|
-
def get_ticket_identifier(nxid)
|
73
|
-
headers = { 'Content-Type' => 'application/json',
|
74
|
-
'Accept' => 'application/json' }
|
75
|
-
|
76
|
-
#Get the address
|
77
|
-
query = "incident.do?JSONv2&sysparm_query=active=true^u_nxid=#{nxid}"
|
78
|
-
uri = URI.join(@service_data[:servicenow_url], '/')
|
79
|
-
full_url = URI.join(uri, "/").to_s + query
|
80
|
-
req = Net::HTTP::Get.new(full_url, headers)
|
81
|
-
req.basic_auth @service_data[:username], @service_data[:password]
|
82
|
-
resp = Net::HTTP.new(uri.host, uri.port)
|
83
|
-
|
84
|
-
# Enable this line for debugging the https call.
|
85
|
-
# resp.set_debug_output(@log)
|
86
|
-
|
87
|
-
if uri.scheme == 'https'
|
88
|
-
resp.use_ssl = true
|
89
|
-
resp.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
90
|
-
end
|
91
|
-
|
92
|
-
begin
|
93
|
-
retries ||= 0
|
94
|
-
response = resp.request(req)
|
95
|
-
rescue Exception => e
|
96
|
-
@log.log_error_message("Request failed for NXID #{nxid}.\n#{e}. Retry #{retries}")
|
97
|
-
retry if (retries += 1) < 3
|
98
|
-
return
|
99
|
-
end
|
100
|
-
|
101
|
-
tickets = JSON.parse(response.body)
|
102
|
-
records = tickets['records']
|
103
|
-
if records.count > 1
|
104
|
-
@log.log_error_message("Found more than one result for NXID #{nxid}. Updating first result.")
|
105
|
-
records.each { |r| @log.log_error_message("NXID #{nxid} found with Rapid7 Identifier #{r['u_rpd_id']}") }
|
106
|
-
elsif records.count == 0
|
107
|
-
@log.log_error_message("No results found for NXID #{nxid}.")
|
108
|
-
return nil
|
109
|
-
end
|
110
|
-
|
111
|
-
ticket_id = records.first['u_rpd_id']
|
112
|
-
@log.log_message("Found ticket for NXID #{nxid} ID is: #{ticket_id}")
|
113
|
-
if ticket_id.nil?
|
114
|
-
@log.log_error_message("ID is nil for ticket with NXID #{nxid}.")
|
115
|
-
end
|
116
|
-
|
117
|
-
ticket_id
|
118
|
-
end
|
119
|
-
|
120
|
-
# Post an individual JSON-formatted ticket to ServiceNow. If the response from the post is a 301/
|
121
|
-
# 302 redirect, the method will attempt to resend the ticket to the response's location for up to
|
122
|
-
# [limit] times (which starts at the redirect_limit config value and is decremented with each
|
123
|
-
# redirect response.
|
124
|
-
#
|
125
|
-
# * *Args* :
|
126
|
-
# - +ticket+ - JSON-formatted ticket.
|
127
|
-
# - +url+ - URL to post the ticket to.
|
128
|
-
# - +limit+ - The amount of times to retry the send ticket request before failing.l
|
129
|
-
#
|
130
|
-
def send_ticket(ticket, url, limit)
|
131
|
-
raise ArgumentError, 'HTTP Redirect too deep' if limit == 0
|
132
|
-
|
133
|
-
uri = URI.parse(url)
|
134
|
-
headers = { 'Content-Type' => 'application/json',
|
135
|
-
'Accept' => 'application/json' }
|
136
|
-
req = Net::HTTP::Post.new(url, headers)
|
137
|
-
req.basic_auth @service_data[:username], @service_data[:password]
|
138
|
-
req.body = ticket
|
139
|
-
|
140
|
-
resp = Net::HTTP.new(uri.host, uri.port)
|
141
|
-
# Setting verbose_mode to 'Y' will debug the https call(s).
|
142
|
-
resp.set_debug_output $stderr if @service_data[:verbose_mode] == 'Y'
|
143
|
-
resp.use_ssl = true if uri.scheme == 'https'
|
144
|
-
# Currently, we do not verify SSL certificates (in case the local servicenow instance uses
|
145
|
-
# and unsigned or expired certificate)
|
146
|
-
resp.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
147
|
-
res = resp.start { |http| http.request(req) }
|
148
|
-
case res
|
149
|
-
when Net::HTTPSuccess then res
|
150
|
-
when Net::HTTPRedirection then send_ticket(ticket, res['location'], limit - 1)
|
151
|
-
else
|
152
|
-
@log.log_message("Error in response: #{res['error']}")
|
153
|
-
raise ArgumentError, res['error']
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
# Prepare tickets from the CSV of vulnerabilities exported from Nexpose. This method determines
|
158
|
-
# how to prepare the tickets (either by default or by IP address) based on config options.
|
159
|
-
#
|
160
|
-
# * *Args* :
|
161
|
-
# - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
|
162
|
-
#
|
163
|
-
# * *Returns* :
|
164
|
-
# - List of JSON-formated tickets for creating within ServiceNow.
|
165
|
-
#
|
166
|
-
def prepare_create_tickets(vulnerability_list, nexpose_identifier_id)
|
167
|
-
prepare_tickets(vulnerability_list, nexpose_identifier_id)
|
168
|
-
end
|
169
|
-
|
170
|
-
# Prepares a list of vulnerabilities into a list of JSON-formatted tickets (incidents) for
|
25
|
+
# Prepares a list of vulnerabilities into a list of JSON-formatted tickets (incidents) for
|
171
26
|
# ServiceNow.
|
172
27
|
#
|
173
28
|
# * *Args* :
|
@@ -181,12 +36,12 @@ class ServiceNowHelper < BaseHelper
|
|
181
36
|
matching_fields = @mode_helper.get_matching_fields
|
182
37
|
@ticket = Hash.new(-1)
|
183
38
|
|
184
|
-
@log.log_message("Preparing tickets in #{options[:ticket_mode]} mode.")
|
39
|
+
@log.log_message("Preparing tickets in #{options[:ticket_mode]} mode format.")
|
185
40
|
tickets = []
|
186
41
|
previous_row = nil
|
187
42
|
description = nil
|
188
|
-
action = 'insert'
|
189
|
-
|
43
|
+
action = 'insert'
|
44
|
+
|
190
45
|
CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
|
191
46
|
if previous_row.nil?
|
192
47
|
previous_row = row.dup
|
@@ -200,12 +55,12 @@ class ServiceNowHelper < BaseHelper
|
|
200
55
|
|
201
56
|
@ticket = {
|
202
57
|
'sysparm_action' => action,
|
203
|
-
'
|
204
|
-
'
|
205
|
-
'
|
206
|
-
'
|
207
|
-
'
|
208
|
-
'
|
58
|
+
'u_caller_id' => "#{@service_data[:username]}",
|
59
|
+
'u_category' => 'Software',
|
60
|
+
'u_impact' => '1',
|
61
|
+
'u_urgency' => '1',
|
62
|
+
'u_short_description' => @mode_helper.get_title(row),
|
63
|
+
'u_work_notes' => "",
|
209
64
|
'u_nxid' => nxid,
|
210
65
|
'u_rpd_id' => nil
|
211
66
|
}
|
@@ -214,44 +69,74 @@ class ServiceNowHelper < BaseHelper
|
|
214
69
|
info = @mode_helper.get_field_info(matching_fields, previous_row)
|
215
70
|
@log.log_message("Generated ticket with #{info}")
|
216
71
|
|
217
|
-
@ticket['
|
72
|
+
@ticket['u_work_notes'] = @mode_helper.print_description(description)
|
218
73
|
tickets.push(@ticket)
|
219
|
-
|
74
|
+
|
220
75
|
previous_row = nil
|
221
76
|
description = nil
|
222
77
|
redo
|
223
78
|
else
|
224
79
|
unless row['comparison'].nil? || row['comparison'] == 'New'
|
225
80
|
@ticket['sysparm_action'] = 'update'
|
226
|
-
end
|
227
|
-
description = @mode_helper.update_description(description, row)
|
81
|
+
end
|
82
|
+
description = @mode_helper.update_description(description, row)
|
228
83
|
end
|
229
84
|
end
|
230
85
|
|
231
86
|
unless @ticket.nil? || @ticket.empty?
|
232
87
|
info = @mode_helper.get_field_info(matching_fields, previous_row)
|
233
88
|
@log.log_message("Generated ticket with #{info}")
|
234
|
-
@ticket['
|
89
|
+
@ticket['u_work_notes'] = @mode_helper.print_description(description) unless (@ticket.size == 0)
|
235
90
|
tickets.push(@ticket)
|
236
91
|
end
|
237
92
|
@log.log_message("Generated <#{tickets.count.to_s}> tickets.")
|
238
93
|
|
239
|
-
tickets
|
240
|
-
|
241
|
-
t['sysparm_action'] = 'insert'
|
242
|
-
t['u_rpd_id'] = get_ticket_identifier(t['u_nxid'])
|
243
|
-
@metrics.updated
|
244
|
-
else
|
245
|
-
@metrics.created
|
246
|
-
end
|
94
|
+
tickets
|
95
|
+
end
|
247
96
|
|
248
|
-
|
249
|
-
|
97
|
+
# Prepare tickets from the CSV of vulnerabilities exported from Nexpose. This method determines
|
98
|
+
# how to prepare the tickets (either by default or by IP address) based on config options.
|
99
|
+
#
|
100
|
+
# * *Args* :
|
101
|
+
# - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
|
102
|
+
#
|
103
|
+
# * *Returns* :
|
104
|
+
# - List of JSON-formated tickets for creating within ServiceNow.
|
105
|
+
#
|
106
|
+
def prepare_create_tickets(vulnerability_list, nexpose_identifier_id)
|
107
|
+
prepare_tickets(vulnerability_list, nexpose_identifier_id)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Sends a list of tickets (in JSON format) to ServiceNow individually (each ticket in the list
|
111
|
+
# as a separate HTTP post).
|
112
|
+
#
|
113
|
+
# * *Args* :
|
114
|
+
# - +tickets+ - List of JSON-formatted ticket creates (new tickets).
|
115
|
+
#
|
116
|
+
def create_tickets(tickets)
|
117
|
+
fail 'Ticket(s) cannot be empty' if tickets.nil? || tickets.empty?
|
118
|
+
final_ticket = tickets.count - 1
|
119
|
+
ticket_index = 0
|
120
|
+
|
121
|
+
hydra = Typhoeus::Hydra.new
|
122
|
+
requests = tickets.map do |ticket|
|
123
|
+
ticket['u_rpd_id'] = SecureRandom.uuid
|
124
|
+
request = generate_post_request(ticket.to_json,
|
125
|
+
ticket_index == final_ticket)
|
126
|
+
hydra.queue request
|
127
|
+
ticket_index += 1
|
128
|
+
request
|
250
129
|
end
|
130
|
+
|
131
|
+
hydra.run
|
132
|
+
|
133
|
+
@metrics.created tickets.count
|
134
|
+
@log.log_message('Created ticket batch.')
|
135
|
+
requests.map(&:response)
|
251
136
|
end
|
252
137
|
|
253
|
-
# Prepare ticket updates from the CSV of vulnerabilities exported from Nexpose. The list of vulnerabilities
|
254
|
-
# are ordered depending on the ticketing mode and then by ticket_status, allowing the method to loop through and
|
138
|
+
# Prepare ticket updates from the CSV of vulnerabilities exported from Nexpose. The list of vulnerabilities
|
139
|
+
# are ordered depending on the ticketing mode and then by ticket_status, allowing the method to loop through and
|
255
140
|
# display new, old, and same vulnerabilities in that order.
|
256
141
|
#
|
257
142
|
# - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
|
@@ -263,6 +148,170 @@ class ServiceNowHelper < BaseHelper
|
|
263
148
|
prepare_tickets(vulnerability_list, nexpose_identifier_id)
|
264
149
|
end
|
265
150
|
|
151
|
+
# Sends ticket updates (in JSON format) to ServiceNow by placing each request
|
152
|
+
# on a Typhoeus queue (each ticket in the list as a separate HTTP post).
|
153
|
+
#
|
154
|
+
# * *Args* :
|
155
|
+
# - +tickets+ - List of Hash-formatted ticket updates.
|
156
|
+
#
|
157
|
+
def update_tickets(tickets)
|
158
|
+
if tickets.nil? || tickets.empty?
|
159
|
+
@log.log_message('No tickets to update.')
|
160
|
+
return
|
161
|
+
end
|
162
|
+
|
163
|
+
requests = []
|
164
|
+
final_ticket = tickets.count - 1
|
165
|
+
|
166
|
+
hydra = Typhoeus::Hydra.new
|
167
|
+
tickets.each_with_index do |ticket, i|
|
168
|
+
if ticket['sysparm_action'] == 'update'
|
169
|
+
id_request = generate_identifier_request(ticket['u_nxid'])
|
170
|
+
id_request.on_complete do |response|
|
171
|
+
ticket['sysparm_action'] = 'insert'
|
172
|
+
|
173
|
+
current_data = parse_identifier_response(response, ticket['u_nxid'])
|
174
|
+
u_rpd_id = current_data[:id]
|
175
|
+
ticket['u_rpd_id'] = u_rpd_id || SecureRandom.uuid
|
176
|
+
|
177
|
+
if current_data[:state] == RESOLVED_STATE
|
178
|
+
ticket['u_state'] = NEW_STATE
|
179
|
+
title = "(Reopened) #{ticket['u_short_description']}"
|
180
|
+
ticket['u_short_description'] = title
|
181
|
+
current_notes = ticket['u_work_notes'].rpartition("\n\n\nNXID: ")
|
182
|
+
new_notes = '++ Reopened by Nexpose Ticketing Gem ' \
|
183
|
+
"++\n#{current_notes.first}"
|
184
|
+
nxid = current_notes[1,2].join('').lstrip
|
185
|
+
description = @mode_helper.finalize_description(new_notes, nxid)
|
186
|
+
ticket['u_work_notes'] = description
|
187
|
+
end
|
188
|
+
|
189
|
+
ticket_request = generate_post_request(ticket.to_json,
|
190
|
+
i == final_ticket)
|
191
|
+
hydra.queue ticket_request
|
192
|
+
ticket['u_rpd_id'] == u_rpd_id ? @metrics.updated : @metrics.created
|
193
|
+
requests << ticket_request
|
194
|
+
end
|
195
|
+
|
196
|
+
hydra.queue id_request
|
197
|
+
elsif ticket['sysparm_action'] == 'insert'
|
198
|
+
ticket['u_rpd_id'] ||= SecureRandom.uuid
|
199
|
+
ticket_request = generate_post_request(ticket.to_json,
|
200
|
+
i == final_ticket)
|
201
|
+
hydra.queue ticket_request
|
202
|
+
@metrics.created
|
203
|
+
requests << ticket_request
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
hydra.run
|
208
|
+
@log.log_message('Updated ticket batch.')
|
209
|
+
requests.map(&:response)
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
|
214
|
+
# Method generates a HTTP POST request containing the provided ticket to
|
215
|
+
# send to ServiceNow. Provides error handling via on_complete functionality
|
216
|
+
#
|
217
|
+
# * *Args* :
|
218
|
+
# - +ticket+ - The ticket to be sent to ServiceNow
|
219
|
+
# - +forbid_connection_reuse+ - Whether the current HTTP connection can be
|
220
|
+
# reused to send tickets to ServiceNow.
|
221
|
+
#
|
222
|
+
# * *Returns* :
|
223
|
+
# - A HTTP post request object to be placed on the queue for sending
|
224
|
+
#
|
225
|
+
def generate_post_request(ticket, forbid_connection_reuse)
|
226
|
+
request = generate_ticket_request(ticket, forbid_connection_reuse)
|
227
|
+
request.on_complete do |response|
|
228
|
+
unless response.success?
|
229
|
+
msg = if response.timed_out?
|
230
|
+
'Time out has occurred.'
|
231
|
+
elsif response.code == 0
|
232
|
+
response.return_message
|
233
|
+
else
|
234
|
+
"HTTP request failed: #{response.code}"
|
235
|
+
end
|
236
|
+
|
237
|
+
@log.log_error_message msg
|
238
|
+
raise msg
|
239
|
+
end
|
240
|
+
end
|
241
|
+
request
|
242
|
+
end
|
243
|
+
|
244
|
+
# Method generates a HTTP POST request containing the provided ticket to
|
245
|
+
# send to ServiceNow. Provides error handling via on_complete functionality
|
246
|
+
#
|
247
|
+
# * *Args* :
|
248
|
+
# - +ticket+ - The ticket to be sent to ServiceNow
|
249
|
+
# - +forbid_connection_reuse+ - Whether the current HTTP connection can be
|
250
|
+
# reused to send tickets to ServiceNow.
|
251
|
+
#
|
252
|
+
# * *Returns* :
|
253
|
+
# - A HTTP request object to be placed on the queue for sending
|
254
|
+
#
|
255
|
+
def generate_ticket_request(ticket, forbid_connection_reuse)
|
256
|
+
address = @service_data[:servicenow_url]
|
257
|
+
userpwd = "#{@service_data[:username]}:#{@service_data[:password]}"
|
258
|
+
headers = { 'Content-Type' => 'application/json' }
|
259
|
+
|
260
|
+
options = {
|
261
|
+
method: :post,
|
262
|
+
userpwd: userpwd,
|
263
|
+
headers: headers,
|
264
|
+
accept_encoding: 'application/json',
|
265
|
+
maxredirs: @service_data[:redirect_limit],
|
266
|
+
ssl_verifyhost: 0,
|
267
|
+
forbid_reuse: forbid_connection_reuse,
|
268
|
+
body: ticket
|
269
|
+
}
|
270
|
+
|
271
|
+
Typhoeus::Request.new(address, options)
|
272
|
+
end
|
273
|
+
|
274
|
+
def generate_identifier_request(nxid)
|
275
|
+
query = "incident.do?JSONv2&sysparm_query=active=true^u_nxid=#{nxid}"
|
276
|
+
url = URI.join(@service_data[:servicenow_url], "/").to_s + query
|
277
|
+
userpwd = "#{@service_data[:username]}:#{@service_data[:password]}"
|
278
|
+
headers = { 'Content-Type' => 'application/json' }
|
279
|
+
options = {
|
280
|
+
method: :get,
|
281
|
+
userpwd: userpwd,
|
282
|
+
headers: headers,
|
283
|
+
accept_encoding: 'application/json',
|
284
|
+
maxredirs: @service_data[:redirect_limit],
|
285
|
+
ssl_verifyhost: 0
|
286
|
+
}
|
287
|
+
|
288
|
+
Typhoeus::Request.new(url, options)
|
289
|
+
end
|
290
|
+
|
291
|
+
def parse_identifier_response(response, nxid)
|
292
|
+
tickets = JSON.parse(response.body)
|
293
|
+
records = tickets['records']
|
294
|
+
|
295
|
+
if records.count > 1
|
296
|
+
@log.log_error_message("Found more than one result for NXID #{nxid}. " \
|
297
|
+
'Updating first result.')
|
298
|
+
records.each { |r| @log.log_error_message("NXID #{nxid} found with " \
|
299
|
+
"Rapid7 Identifier #{r['u_rpd_id']}") }
|
300
|
+
elsif records.count == 0
|
301
|
+
@log.log_error_message("No results found for NXID #{nxid}.")
|
302
|
+
return { id: nil, state: nil }
|
303
|
+
end
|
304
|
+
|
305
|
+
ticket_id = records.first['u_rpd_id']
|
306
|
+
state = records.first['state']
|
307
|
+
@log.log_message("Found ticket for NXID #{nxid} ID is: #{ticket_id}")
|
308
|
+
if ticket_id.nil?
|
309
|
+
@log.log_error_message("ID is nil for ticket with NXID #{nxid}.")
|
310
|
+
state = nil
|
311
|
+
end
|
312
|
+
|
313
|
+
{ id: ticket_id, state: state }
|
314
|
+
end
|
266
315
|
|
267
316
|
# Prepare ticket closures from the CSV of vulnerabilities exported from Nexpose. This method
|
268
317
|
# currently only supports updating default mode tickets in ServiceNow.
|
@@ -275,20 +324,55 @@ class ServiceNowHelper < BaseHelper
|
|
275
324
|
#
|
276
325
|
def prepare_close_tickets(vulnerability_list, nexpose_identifier_id)
|
277
326
|
@log.log_message("Preparing ticket closures for mode #{@options[:ticket_mode]}.")
|
278
|
-
|
279
|
-
|
327
|
+
requests = {}
|
328
|
+
|
280
329
|
CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
ticket = {
|
286
|
-
'sysparm_action' => 'insert',
|
287
|
-
'u_rpd_id' => ticket_id,
|
288
|
-
'state' => '7'
|
289
|
-
}.to_json
|
290
|
-
tickets.push(ticket)
|
330
|
+
nxid = @mode_helper.get_nxid(nexpose_identifier_id, row)
|
331
|
+
@log.log_message("Closing ticket with NXID: #{nxid}.")
|
332
|
+
request = generate_identifier_request(nxid)
|
333
|
+
requests[nxid] = request
|
291
334
|
end
|
292
|
-
|
335
|
+
|
336
|
+
requests
|
337
|
+
end
|
338
|
+
|
339
|
+
# Sends ticket closure (in JSON format) to ServiceNow individually
|
340
|
+
# (each ticket in the list as a separate HTTP post).
|
341
|
+
#
|
342
|
+
# * *Args* :
|
343
|
+
# - +requests+ - Hash containing NXIDs and associated Typheous requests.
|
344
|
+
#
|
345
|
+
def close_tickets(nxid_requests)
|
346
|
+
if nxid_requests.nil? || nxid_requests.empty?
|
347
|
+
@log.log_message('No tickets to close.')
|
348
|
+
return
|
349
|
+
end
|
350
|
+
|
351
|
+
ticket = {
|
352
|
+
'sysparm_action' => 'insert',
|
353
|
+
'u_rpd_id' => nil,
|
354
|
+
'u_state' => CLOSED_STATE
|
355
|
+
}
|
356
|
+
|
357
|
+
requests = []
|
358
|
+
final_ticket = nxid_requests.count - 1
|
359
|
+
|
360
|
+
hydra = Typhoeus::Hydra.new
|
361
|
+
nxid_requests.each_with_index do |(nxid, request), i|
|
362
|
+
request.on_complete do |response|
|
363
|
+
u_rpd_id = parse_identifier_response(response, nxid)[:id]
|
364
|
+
ticket['u_rpd_id'] = u_rpd_id
|
365
|
+
ticket_request = generate_post_request(ticket.to_json,
|
366
|
+
i == final_ticket)
|
367
|
+
hydra.queue ticket_request
|
368
|
+
requests << ticket_request
|
369
|
+
end
|
370
|
+
hydra.queue request
|
371
|
+
end
|
372
|
+
|
373
|
+
hydra.run
|
374
|
+
@metrics.closed requests.count
|
375
|
+
@log.log_message('Closed ticket batch.')
|
376
|
+
requests.map(&:response)
|
293
377
|
end
|
294
378
|
end
|