nexpose_ticketing 0.8.0 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|