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 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