nexpose_ticketing 1.3.0 → 1.4.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 +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
|