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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8fcbb2154c8ceb5ebfab167803d4b8eb1e8bbd7e
4
- data.tar.gz: 01b9c52553f5bfff7068e3b1f053019eee376f07
3
+ metadata.gz: 115064fd58b671883cce7cb3e2d248319fb49b77
4
+ data.tar.gz: 49eaabad5c7e5e5b8d8342c78971b2ee1bc421e5
5
5
  SHA512:
6
- metadata.gz: e630c064eeafd32dce7ad4acee581db6d9d59782ecd56ce17f17260c9c01e5d849b6ce1c8f97f1157fc35e5fd554afdef10589787e27c6501b1de3f9d7ca3209
7
- data.tar.gz: 97d83bf0c24fd43bbe9347c4a8e88cf8d33811a17361f2e65348ef619c8b46ac4615263bb64080e11533a4a08d8cac548cfa7f6b5c6fb362c47a33e2a3584f32
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://JIRA_URL/rest/api/2/issue/
9
- :username: admin
10
- :password: admin
11
- :project: KEY
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: nxadmin
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: nexpose-console.host.com
37
+ :nxconsole: 127.0.0.1
38
38
  # (M) Nexpose username.
39
- :nxuser: admin
39
+ :nxuser: nxadmin
40
40
  # (M) Nexpose password.
41
- :nxpasswd: admin
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 indentifier used to find and update and/or close tickets.
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
- url = URI.parse(("#{@jira_data[:jira_url]}".split("/")[0..-2].join("/") + '/search'))
64
- url.query = [url.query, URI.escape(jql_query)].compact.join('&')
65
- req = Net::HTTP::Get.new(url.to_s, headers)
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(url.host, url.port)
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 $stderr
72
+ # resp.set_debug_output(@log)
71
73
 
72
- resp.use_ssl = true if @jira_data[:jira_url].to_s.start_with?('https')
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
- #TODO: fail(?) if more than one issue exits with a "unique" NXID
78
- return nil if issues.nil? || !issues.any? || issues.size > 1
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
- url = URI.parse("#{@jira_data[:jira_url]}")
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(url.host, url.port)
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 $stderr
143
+ #resp.set_debug_output(@log)
95
144
 
96
- resp.use_ssl = true if @jira_data[:jira_url].to_s.start_with?('https')
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 'No ticketing mode selected.'
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-formated tickets for updating within Jira.
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 formating this.
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
- #@log.log_message("No tickets to close.")
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
- url = URI.parse(("#{@jira_data[:jira_url]}#{ticket}/transitions"))
208
- req = Net::HTTP::Post.new(url.to_s, headers)
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
- #TODO: May need to make this customisable as Jira does not allow for some transitions depending on workflow.
212
- req.body = {"transition" => {"id" => "2"}, "fields" => {"resolution" => {"name" => "Fixed"}}}.to_json
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
- resp = Net::HTTP.new(url.host, url.port)
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 $stderr
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 != Closed AND status != Resolved)&fields=key")
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
- #TODO: Log error
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
- #@log.log_message('No tickets to update.')
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 $stderr
355
+ #resp.set_debug_output(@log)
283
356
 
284
- resp.use_ssl = true if uri.to_s.start_with?('https')
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 != Closed AND status != Resolved)&fields=key")
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
@@ -31,5 +31,11 @@ module NexposeTicketing
31
31
  def log_message(message)
32
32
  @log.info(message) if @options[:logging_enabled]
33
33
  end
34
+
35
+ # Logs a message if logging is enabled.
36
+ def << (message)
37
+ @log.info(message) if @options[:logging_enabled]
38
+ end
39
+
34
40
  end
35
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nexpose_ticketing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damian Finol