nexpose_ticketing 0.8.0 → 0.8.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/lib/nexpose_ticketing/config/jira.config +10 -4
- data/lib/nexpose_ticketing/config/servicenow.config +1 -1
- data/lib/nexpose_ticketing/config/ticket_service.config +3 -3
- data/lib/nexpose_ticketing/helpers/jira_helper.rb +107 -33
- data/lib/nexpose_ticketing/nx_logger.rb +6 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 115064fd58b671883cce7cb3e2d248319fb49b77
|
4
|
+
data.tar.gz: 49eaabad5c7e5e5b8d8342c78971b2ee1bc421e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e31de51ea6dd0dbd4917221c1b14c6c20667349adfa245873c0626b5a77f965af00f9194ce9f8fde699d0c2b0db870e81ccd35ffb450b0eec5c1ae08f01f8ad3
|
7
|
+
data.tar.gz: e13adabcfafaf8e403952289a379038cb5f27d5bee56db5bc888d85885a4db848634fb41d445ba28a2aa82fe8736f4009b797afb8e78d431536955816205ee2e
|
@@ -5,7 +5,13 @@
|
|
5
5
|
# (M)) Helper class name.
|
6
6
|
:helper_name: JiraHelper
|
7
7
|
# Optional parameters, these are implementation specific.
|
8
|
-
:jira_url: https://
|
9
|
-
:username:
|
10
|
-
:password:
|
11
|
-
:project:
|
8
|
+
:jira_url: https://url/rest/api/2/issue/
|
9
|
+
:username: jirausername
|
10
|
+
:password: jirapassword
|
11
|
+
:project: projectname
|
12
|
+
# The workflow 'Step Name' and 'Step Name ID' for the 'closed' Status. This is used to close tickets automatically and can be any status
|
13
|
+
# not just 'Closed'. The Jira helper uses these details to try and find a transition to set the ticket to this status (a transition to
|
14
|
+
# this status should be available from every other status). Jira's default workflow details are used by default (shown below).
|
15
|
+
# Note the 'close_step_name' configuration parameter is also used to query Jira for tickets. Any ticket in that status will be ignored.
|
16
|
+
:close_step_id: 6
|
17
|
+
:close_step_name: Closed
|
@@ -12,7 +12,7 @@
|
|
12
12
|
# (M) Username for ServiceNow
|
13
13
|
:username: admin
|
14
14
|
# (M) Password for above username
|
15
|
-
:password:
|
15
|
+
:password: admin
|
16
16
|
# (M) If 'Y', SSL connections will output to stderr (default is 'N')
|
17
17
|
:verbose_mode: N
|
18
18
|
# (M) Amount of times the helper will follow 301/302 redirections
|
@@ -34,8 +34,8 @@
|
|
34
34
|
# Nexpose options.
|
35
35
|
:nexpose_data:
|
36
36
|
# (M) Nexpose console hostname.
|
37
|
-
:nxconsole:
|
37
|
+
:nxconsole: 127.0.0.1
|
38
38
|
# (M) Nexpose username.
|
39
|
-
:nxuser:
|
39
|
+
:nxuser: nxadmin
|
40
40
|
# (M) Nexpose password.
|
41
|
-
:nxpasswd:
|
41
|
+
:nxpasswd: nxadmin
|
@@ -3,6 +3,7 @@ require 'net/http'
|
|
3
3
|
require 'net/https'
|
4
4
|
require 'uri'
|
5
5
|
require 'csv'
|
6
|
+
require 'nexpose_ticketing/nx_logger'
|
6
7
|
|
7
8
|
# This class serves as the JIRA interface
|
8
9
|
# that creates issues within JIRA from vulnerabilities
|
@@ -15,9 +16,10 @@ class JiraHelper
|
|
15
16
|
def initialize(jira_data, options)
|
16
17
|
@jira_data = jira_data
|
17
18
|
@options = options
|
19
|
+
@log = NexposeTicketing::NXLogger.new
|
18
20
|
end
|
19
21
|
|
20
|
-
# Generates the NXID. The NXID is a unique
|
22
|
+
# Generates the NXID. The NXID is a unique identifier used to find and update and/or close tickets.
|
21
23
|
#
|
22
24
|
# * *Args* :
|
23
25
|
# - +site_id+ - Site ID the tickets are being generated for. Required for all ticketing modes
|
@@ -60,40 +62,87 @@ class JiraHelper
|
|
60
62
|
headers = { 'Content-Type' => 'application/json',
|
61
63
|
'Accept' => 'application/json' }
|
62
64
|
|
63
|
-
|
64
|
-
|
65
|
-
req = Net::HTTP::Get.new(
|
65
|
+
uri = URI.parse(("#{@jira_data[:jira_url]}".split("/")[0..-2].join('/') + '/search'))
|
66
|
+
uri.query = [uri.query, URI.escape(jql_query)].compact.join('&')
|
67
|
+
req = Net::HTTP::Get.new(uri.to_s, headers)
|
66
68
|
req.basic_auth @jira_data[:username], @jira_data[:password]
|
67
|
-
resp = Net::HTTP.new(
|
69
|
+
resp = Net::HTTP.new(uri.host, uri.port)
|
68
70
|
|
69
71
|
# Enable this line for debugging the https call.
|
70
|
-
# resp.set_debug_output
|
72
|
+
# resp.set_debug_output(@log)
|
71
73
|
|
72
|
-
resp.use_ssl = true if
|
74
|
+
resp.use_ssl = true if uri.scheme == 'https'
|
73
75
|
resp.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
74
76
|
response = resp.request(req)
|
75
77
|
|
76
78
|
issues = JSON.parse(response.body)['issues']
|
77
|
-
|
78
|
-
|
79
|
+
if issues.nil? || !issues.any? || issues.size > 1
|
80
|
+
# If Jira returns more than one key for a "unique" NXID query result then something has gone wrong...
|
81
|
+
# Safest response is to return no key and let logic elsewhere dictate the action to take.
|
82
|
+
@log.log_message("Jira returned no key or too many keys for query result! Response was <#{issues}>")
|
83
|
+
return nil
|
84
|
+
end
|
79
85
|
return issues[0]['key']
|
80
86
|
end
|
81
87
|
|
88
|
+
# Fetches the Jira ticket transition details for the given Jira ticket key. Tries to match the response to the
|
89
|
+
# the desired transition in the configuration file.
|
90
|
+
#
|
91
|
+
# * *Args* :
|
92
|
+
# - +Jira key+ - Jira ticket key e.g. INT-1.
|
93
|
+
# - +Step ID+ - Jira transition step id (Jira number assigned to a status).
|
94
|
+
#
|
95
|
+
# * *Returns* :
|
96
|
+
# - Jira transition details in JSON format if matched, nil otherwise.
|
97
|
+
#
|
98
|
+
def get_jira_transition_details(jira_key, step_id)
|
99
|
+
fail 'Jira ticket key and transition step ID required to find transition details.' if jira_key.nil? || step_id.nil?
|
100
|
+
|
101
|
+
headers = { 'Content-Type' => 'application/json',
|
102
|
+
'Accept' => 'application/json' }
|
103
|
+
|
104
|
+
uri = URI.parse(("#{@jira_data[:jira_url]}#{jira_key}/transitions?expand=transitions.fields."))
|
105
|
+
req = Net::HTTP::Get.new(uri.to_s, headers)
|
106
|
+
req.basic_auth @jira_data[:username], @jira_data[:password]
|
107
|
+
resp = Net::HTTP.new(uri.host, uri.port)
|
108
|
+
|
109
|
+
# Enable this line for debugging the https call.
|
110
|
+
# resp.set_debug_output(@log)
|
111
|
+
|
112
|
+
resp.use_ssl = true if uri.scheme == 'https'
|
113
|
+
resp.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
114
|
+
|
115
|
+
response = resp.request(req)
|
116
|
+
|
117
|
+
transitions = JSON.parse(response.body)
|
118
|
+
|
119
|
+
if transitions.has_key? 'transitions'
|
120
|
+
transitions['transitions'].each do |transition|
|
121
|
+
if transition['to']['id'] == step_id.to_s
|
122
|
+
return transition
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
@log.log_message("Jira returned no valid transition to close the ticket! Response was <#{transitions}> and desired close Step ID was <#{@jira_data[:close_step_id]}>.")
|
127
|
+
return nil
|
128
|
+
end
|
129
|
+
|
82
130
|
def create_tickets(tickets)
|
83
131
|
fail 'Ticket(s) cannot be empty.' if tickets.empty? || tickets.nil?
|
84
132
|
tickets.each do |ticket|
|
85
133
|
headers = { 'Content-Type' => 'application/json',
|
86
134
|
'Accept' => 'application/json' }
|
87
|
-
|
135
|
+
|
136
|
+
uri = URI.parse("#{@jira_data[:jira_url]}")
|
88
137
|
req = Net::HTTP::Post.new(@jira_data[:jira_url], headers)
|
89
138
|
req.basic_auth @jira_data[:username], @jira_data[:password]
|
90
139
|
req.body = ticket
|
91
|
-
resp = Net::HTTP.new(
|
140
|
+
resp = Net::HTTP.new(uri.host, uri.port)
|
92
141
|
|
93
142
|
# Enable this line for debugging the https call.
|
94
|
-
#resp.set_debug_output
|
143
|
+
#resp.set_debug_output(@log)
|
95
144
|
|
96
|
-
resp.use_ssl = true if
|
145
|
+
resp.use_ssl = true if uri.scheme == 'https'
|
97
146
|
resp.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
98
147
|
resp.start { |http| http.request(req) }
|
99
148
|
end
|
@@ -111,12 +160,13 @@ class JiraHelper
|
|
111
160
|
when 'I'
|
112
161
|
prepare_tickets_by_ip(vulnerability_list, site_id)
|
113
162
|
else
|
114
|
-
fail '
|
163
|
+
fail 'Unsupported ticketing mode selected.'
|
115
164
|
end
|
116
165
|
end
|
117
166
|
|
118
167
|
# Prepares and creates tickets in default mode.
|
119
168
|
def prepare_tickets_default(vulnerability_list, site_id)
|
169
|
+
@log.log_message('Preparing tickets for default mode.')
|
120
170
|
tickets = []
|
121
171
|
CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
|
122
172
|
# JiraHelper doesn't like new line characters in their summaries.
|
@@ -143,9 +193,10 @@ class JiraHelper
|
|
143
193
|
# - +site_id+ - Site ID the vulnerability list was generate from.
|
144
194
|
#
|
145
195
|
# * *Returns* :
|
146
|
-
# - List of JSON-
|
196
|
+
# - List of JSON-formatted tickets for updating within Jira.
|
147
197
|
#
|
148
198
|
def prepare_tickets_by_ip(vulnerability_list, site_id)
|
199
|
+
@log.log_message('Preparing tickets for IP mode.')
|
149
200
|
tickets = []
|
150
201
|
current_ip = -1
|
151
202
|
CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
|
@@ -162,7 +213,7 @@ class JiraHelper
|
|
162
213
|
}
|
163
214
|
}
|
164
215
|
end
|
165
|
-
# TODO: Better
|
216
|
+
# TODO: Better formatting this.
|
166
217
|
if current_ip == row['ip_address']
|
167
218
|
@ticket['fields']['description'] +=
|
168
219
|
"\n ==============================\n\n
|
@@ -197,26 +248,48 @@ class JiraHelper
|
|
197
248
|
#
|
198
249
|
def close_tickets(tickets)
|
199
250
|
if tickets.nil? || tickets.empty?
|
200
|
-
|
201
|
-
#TODO: Log error
|
251
|
+
@log.log_message('No tickets to close.')
|
202
252
|
else
|
203
253
|
headers = { 'Content-Type' => 'application/json',
|
204
254
|
'Accept' => 'application/json' }
|
205
255
|
|
206
256
|
tickets.each { |ticket|
|
207
|
-
|
208
|
-
req = Net::HTTP::Post.new(
|
257
|
+
uri = URI.parse(("#{@jira_data[:jira_url]}#{ticket}/transitions"))
|
258
|
+
req = Net::HTTP::Post.new(uri.to_s, headers)
|
209
259
|
req.basic_auth @jira_data[:username], @jira_data[:password]
|
210
260
|
|
211
|
-
|
212
|
-
|
261
|
+
transition = get_jira_transition_details(ticket, @jira_data[:close_step_id])
|
262
|
+
if transition.nil?
|
263
|
+
#Valid transition could not be found. Ignore ticket since we do not know what to do with it.
|
264
|
+
@log.log_message("No valid transition found for ticket <#{ticket}>. Skipping closure.")
|
265
|
+
next
|
266
|
+
end
|
267
|
+
|
268
|
+
#We need to find any required fields to send with the transition request
|
269
|
+
required_fields = []
|
270
|
+
transition['fields'].each do |field|
|
271
|
+
if field[1]['required'] == true
|
272
|
+
# Currently only required fields with 'allowedValues' in the JSON response are supported.
|
273
|
+
if not field[1].has_key? 'allowedValues'
|
274
|
+
@log.log_message("Closing ticket <#{ticket}> requires a field I know nothing about! Transition details are <#{transition}>. Ignoring this field.")
|
275
|
+
next
|
276
|
+
else
|
277
|
+
if field[1]['schema']['type'] == 'array'
|
278
|
+
required_fields << "\"#{field[0]}\" : [{\"id\" : \"#{field[1]['allowedValues'][0]['id']}\"}]"
|
279
|
+
else
|
280
|
+
required_fields << "\"#{field[0]}\" : {\"id\" : \"#{field[1]['allowedValues'][0]['id']}\"}"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
213
285
|
|
214
|
-
|
286
|
+
req.body = "{\"transition\" : {\"id\" : #{transition['id']}}, \"fields\" : { #{required_fields.join(",")}}}"
|
287
|
+
resp = Net::HTTP.new(uri.host, uri.port)
|
215
288
|
|
216
289
|
# Enable this line for debugging the https call.
|
217
|
-
#resp.set_debug_output
|
290
|
+
#resp.set_debug_output(@log)
|
218
291
|
|
219
|
-
resp.use_ssl = true
|
292
|
+
resp.use_ssl = true if uri.scheme == 'https'
|
220
293
|
resp.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
221
294
|
resp.start { |http| http.request(req) }
|
222
295
|
}
|
@@ -232,14 +305,15 @@ class JiraHelper
|
|
232
305
|
# - List of Jira ticket Keys to be closed.
|
233
306
|
#
|
234
307
|
def prepare_close_tickets(vulnerability_list, site_id)
|
308
|
+
@log.log_message('Preparing tickets to close.')
|
235
309
|
@nxid = nil
|
236
310
|
tickets = []
|
237
311
|
CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
|
238
312
|
@nxid = generate_nxid(site_id, nil, row['ip_address'])
|
239
313
|
# Query Jira for the ticket by unique id (generated NXID)
|
240
|
-
queried_key = get_jira_key("jql=project=#{@jira_data[:project]} AND description ~ \"NXID: #{@nxid}\" AND (status !=
|
314
|
+
queried_key = get_jira_key("jql=project=#{@jira_data[:project]} AND description ~ \"NXID: #{@nxid}\" AND (status != #{@jira_data[:close_step_name]})&fields=key")
|
241
315
|
if queried_key.nil? || queried_key.empty?
|
242
|
-
|
316
|
+
@log.log_message("Error when closing tickets - query for NXID <#{@nxid}> should have returned a Jira key!!")
|
243
317
|
else
|
244
318
|
#Jira uses a post call to the ticket key path to close the ticket. The "prepared batch of tickets" in this case is just a collection Jira ticket key's to close.
|
245
319
|
tickets.push(queried_key)
|
@@ -256,8 +330,7 @@ class JiraHelper
|
|
256
330
|
#
|
257
331
|
def update_tickets(tickets)
|
258
332
|
if (tickets.nil? || tickets.empty?) then
|
259
|
-
|
260
|
-
#TODO: Log error
|
333
|
+
@log.log_message('No tickets to update.')
|
261
334
|
else
|
262
335
|
tickets.each do |ticket_details|
|
263
336
|
headers = {'Content-Type' => 'application/json',
|
@@ -279,9 +352,9 @@ class JiraHelper
|
|
279
352
|
resp = Net::HTTP.new(uri.host, uri.port)
|
280
353
|
|
281
354
|
# Enable this line for debugging the https call.
|
282
|
-
#resp.set_debug_output
|
355
|
+
#resp.set_debug_output(@log)
|
283
356
|
|
284
|
-
resp.use_ssl = true if uri.
|
357
|
+
resp.use_ssl = true if uri.scheme == 'https'
|
285
358
|
resp.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
286
359
|
resp.start { |http| http.request(req) }
|
287
360
|
end
|
@@ -299,6 +372,7 @@ class JiraHelper
|
|
299
372
|
#
|
300
373
|
def prepare_update_tickets(vulnerability_list, site_id)
|
301
374
|
fail 'Ticket updates are only supported in IP-address mode.' if @options[:ticket_mode] != 'I'
|
375
|
+
@log.log_message('Preparing tickets to update.')
|
302
376
|
#Jira uses the ticket key to push updates. Since new IPs won't have a Jira key, generate new tickets for all of the IPs found.
|
303
377
|
updated_tickets = prepare_tickets_by_ip(vulnerability_list, site_id)
|
304
378
|
tickets_to_send = []
|
@@ -307,11 +381,11 @@ class JiraHelper
|
|
307
381
|
updated_tickets.each do |ticket|
|
308
382
|
nxid = JSON.parse(ticket)['fields']['description'].squeeze("\n").lines.to_a.last
|
309
383
|
if (nxid.slice! "NXID:").nil?
|
310
|
-
#TODO - log error.
|
311
384
|
#Could not get NXID from the last line in the description. Do not push the invalid description.
|
385
|
+
@log.log_message("Failed to parse the NXID from a generated ticket update! Ignoring ticket <#{nxid}>")
|
312
386
|
next
|
313
387
|
end
|
314
|
-
queried_key = get_jira_key("jql=project=#{@jira_data[:project]} AND description ~ \"NXID: #{nxid.strip}\" AND (status !=
|
388
|
+
queried_key = get_jira_key("jql=project=#{@jira_data[:project]} AND description ~ \"NXID: #{nxid.strip}\" AND (status != #{@jira_data[:close_step_name]})&fields=key")
|
315
389
|
ticket_key_pair = []
|
316
390
|
ticket_key_pair << queried_key
|
317
391
|
ticket_key_pair << ticket
|