nexpose_ticketing 0.8.3 → 1.0.0

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: 7616fb5d627a944b54dd186638aec10369bf9f66
4
- data.tar.gz: 1e38b63f2ab01292ebcac1a50de0ccdb4eb05374
3
+ metadata.gz: 16cf7f70522d51ed7a38070041ad4396e4e56ce7
4
+ data.tar.gz: 946842a9a482cbf608fbdf1bf9853919db1cdbe5
5
5
  SHA512:
6
- metadata.gz: 9ba361ae83efbe879cbf087d796564d0cc92342663c98fcd28dded3a4330f7f2b62f45cd1cac756fb0d5f5a2331d3e93695bcf1fdcf3a18e5486e0c7a92b67bd
7
- data.tar.gz: 710760d84937ed652c4c378cbeaf9b50634de58145b140104b2aab5619b098baf566f00cf86f57369a47e5827c544505d94394a2d717d61462ec1ce9bc4efb7c
6
+ metadata.gz: aa83dfe6f2d916f880fc95c99cbebb01ae1998f066affc7585ed52de884201c0b85d2e3980bad6ac2f6d8d918975a0c208f68a28b6cc5664b9ae2562709b6aaf
7
+ data.tar.gz: 9002b4380aa921cdad95f5188011a47887865ef88d75b929817ef137bd83ec3f31094acd3cf679a9eca6d74170fb493c31d5fcc4763d2926982805246d106134
data/bin/nexpose_jira CHANGED
@@ -1,6 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'yaml'
3
3
  require 'nexpose_ticketing'
4
+ require 'nexpose_ticketing/nx_logger'
5
+ require 'nexpose_ticketing/version'
6
+
7
+ log = NexposeTicketing::NxLogger.instance
8
+ log.setup_statistics_collection('Atlassian', 'JIRA', NexposeTicketing::VERSION)
9
+ log.setup_logging(true, 'info')
10
+
4
11
  # Path to the JIRA Configuration file.
5
12
  JIRA_CONFIG_PATH = File.join(File.dirname(__FILE__),
6
13
  '../lib/nexpose_ticketing/config/jira.config')
data/bin/nexpose_remedy CHANGED
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'yaml'
3
3
  require 'nexpose_ticketing'
4
+ require 'nexpose_ticketing/nx_logger'
5
+ require 'nexpose_ticketing/version'
6
+
7
+ log = NexposeTicketing::NxLogger.instance
8
+ log.setup_statistics_collection('bmc', 'Remedy', NexposeTicketing::VERSION)
9
+ log.setup_logging(true, 'info')
4
10
 
5
11
  # Path to ServiceNow configuration file
6
12
  REMEDY_CONFIG_PATH = File.join(File.dirname(__FILE__),
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'yaml'
3
3
  require 'nexpose_ticketing'
4
+ require 'nexpose_ticketing/nx_logger'
5
+ require 'nexpose_ticketing/version'
6
+
7
+ log = NexposeTicketing::NxLogger.instance
8
+ log.setup_statistics_collection('ManageEngine', 'ServiceDesk', NexposeTicketing::VERSION)
9
+ log.setup_logging(true, 'info')
4
10
 
5
11
  # Path to ServiceNow configuration file
6
12
  SERVICEDESK_CONFIG_PATH = File.join(File.dirname(__FILE__),
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'yaml'
3
3
  require 'nexpose_ticketing'
4
+ require 'nexpose_ticketing/nx_logger'
5
+ require 'nexpose_ticketing/version'
6
+
7
+ log = NexposeTicketing::NxLogger.instance
8
+ log.setup_statistics_collection('ServiceNow', 'ServiceNow', NexposeTicketing::VERSION)
9
+ log.setup_logging(true, 'info')
4
10
 
5
11
  # Path to ServiceNow configuration file
6
12
  SERVICENOW_CONFIG_PATH = File.join(File.dirname(__FILE__),
@@ -0,0 +1,344 @@
1
+ module NexposeTicketing
2
+ class CommonHelper
3
+ MAX_NUM_REFS = 3
4
+
5
+ def initialize(options)
6
+ @ticketing_mode = options[:ticket_mode]
7
+ end
8
+
9
+ # Gets the base description hash from the relevant mode-specific method
10
+ # which can converted into a finished description.
11
+ #
12
+ # - +nexpose_id+ - The site or tag indentifier.
13
+ # - +options+ - The options read from the ticket config file.
14
+ #
15
+ # * *Returns* :
16
+ # - Hash containing ticket description information.
17
+ #
18
+ def get_description(nexpose_id, row)
19
+ description = { nxid: "NXID: #{generate_nxid(nexpose_id, row)}" }
20
+ case @ticketing_mode
21
+ when 'D' then get_default_ticket_description(description, row)
22
+ when 'I' then get_ip_ticket_description(description, row)
23
+ when 'V' then get_vuln_ticket_description(description, row)
24
+ else fail "Ticketing mode #{@ticketing_mode} not recognised."
25
+ end
26
+ end
27
+
28
+ # Updates an existing description hash containing information
29
+ # necessary to generate a ticket description.
30
+ # Note that Default mode tickets may not be updated.
31
+ #
32
+ # - +description+ - The existing ticket hash to be updated.
33
+ # - +row+ - CSV row containing vulnerability data.
34
+ #
35
+ # * *Returns* :
36
+ # - Hash containing ticket description information.
37
+ #
38
+ def update_description(description, row)
39
+ case @ticketing_mode
40
+ when 'I' then return update_ip_ticket_description(description, row)
41
+ when 'V' then return update_vuln_ticket_description(description, row)
42
+ else description
43
+ end
44
+ end
45
+
46
+ # Generates a final description string based on a description hash.
47
+ #
48
+ # - +description+ - The finished ticket hash to be converted.
49
+ #
50
+ # * *Returns* :
51
+ # - String containing ticket description text.
52
+ #
53
+ def print_description(description)
54
+ ticket = case @ticketing_mode
55
+ when 'D' then print_default_ticket_description(description)
56
+ when 'I' then print_ip_ticket_description(description)
57
+ when 'V' then print_vuln_ticket_description(description)
58
+ else fail "Ticketing mode #{@ticketing_mode} not recognised."
59
+ end
60
+ ticket << "\n\n\n#{description[:nxid]}"
61
+ ticket
62
+ end
63
+
64
+ # Generates a hash containing the information necessary
65
+ # to generate a Default-mode ticket description.
66
+ #
67
+ # - +description+ - Base ticket hash with NXID.
68
+ # - +row+ - CSV row containing vulnerability data.
69
+ #
70
+ # * *Returns* :
71
+ # - Hash containing ticket description information.
72
+ #
73
+ def get_default_ticket_description(description, row)
74
+ description[:header] = get_vuln_header(row)
75
+ description[:header] << get_discovery_info(row)
76
+ description[:references] = get_references(row)
77
+ description[:solutions] = get_solutions(row)
78
+ description
79
+ end
80
+
81
+ # Generates a hash containing the information necessary
82
+ # to generate an IP-mode ticket description.
83
+ #
84
+ # - +description+ - Base ticket hash with NXID.
85
+ # - +row+ - CSV row containing vulnerability data.
86
+ #
87
+ # * *Returns* :
88
+ # - Hash containing ticket description information.
89
+ #
90
+ def get_ip_ticket_description(description, row)
91
+ description[:vulnerabilities] = []
92
+
93
+ status = row['comparison']
94
+ vuln_info = "++ #{status} Vulnerabilities ++\n" if !status.nil?
95
+ description[:ticket_status] = status
96
+
97
+ vuln_info = vuln_info.to_s + get_vuln_info(row)
98
+ description[:vulnerabilities] << vuln_info
99
+ description
100
+ end
101
+
102
+ # Generates a hash containing the information necessary
103
+ # to generate a Vulnerability-mode ticket description.
104
+ #
105
+ # - +description+ - Base ticket hash with NXID.
106
+ # - +row+ - CSV row containing vulnerability data.
107
+ #
108
+ # * *Returns* :
109
+ # - Hash containing ticket description information.
110
+ #
111
+ def get_vuln_ticket_description(description, row)
112
+ description[:header] = get_vuln_header(row)
113
+ description[:references] = get_references(row)
114
+ description[:solutions] = get_solutions(row)
115
+ description[:assets] = get_assets(row)
116
+ description
117
+ end
118
+
119
+ # Updates an existing IP-mode description hash containing information
120
+ # necessary to generate a ticket description.
121
+ #
122
+ # - +description+ - The existing ticket hash to be updated.
123
+ # - +row+ - CSV row containing vulnerability data.
124
+ #
125
+ # * *Returns* :
126
+ # - Hash containing updated ticket description information.
127
+ #
128
+ def update_ip_ticket_description(description, row)
129
+ status = row['comparison']
130
+ header = "++ #{status} Vulnerabilities ++\n"
131
+ header = "" unless description[:ticket_status] != status
132
+
133
+ description[:vulnerabilities] << "#{header}#{get_vuln_info(row)}"
134
+ description
135
+ end
136
+
137
+ # Updates an existing Vulnerability-mode description hash containing
138
+ # information necessary to generate a ticket description.
139
+ #
140
+ # - +description+ - The existing ticket hash to be updated.
141
+ # - +row+ - CSV row containing vulnerability data.
142
+ #
143
+ # * *Returns* :
144
+ # - Hash containing updated ticket description information.
145
+ #
146
+ def update_vuln_ticket_description(description, row)
147
+ description[:assets] += "\n#{get_assets(row)}"
148
+ description
149
+ end
150
+
151
+ # Generates a final description string based on a Default-mode
152
+ # description hash.
153
+ #
154
+ # - +description+ - The finished ticket hash to be converted.
155
+ #
156
+ # * *Returns* :
157
+ # - String containing ticket description text.
158
+ #
159
+ def print_default_ticket_description(description)
160
+ ticket = "#{description[:header]}\n#{description[:references]}"
161
+ ticket << "#{description[:solutions]}"
162
+ ticket
163
+ end
164
+
165
+ # Generates a final description string based on an IP-mode
166
+ # description hash.
167
+ #
168
+ # - +description+ - The finished ticket hash to be converted.
169
+ #
170
+ # * *Returns* :
171
+ # - String containing ticket description text.
172
+ #
173
+ def print_ip_ticket_description(description)
174
+ ticket = ''
175
+ description[:vulnerabilities].each { |v| ticket << "#{v}\n" }
176
+ ticket
177
+ end
178
+
179
+ # Generates a final description string based on a Vulnerability-mode
180
+ # description hash.
181
+ #
182
+ # - +description+ - The finished ticket hash to be converted.
183
+ #
184
+ # * *Returns* :
185
+ # - String containing ticket description text.
186
+ #
187
+ def print_vuln_ticket_description(description)
188
+ ticket = "#{description[:header]}\n#{description[:assets]}"
189
+ ticket << "\n#{description[:references]}\n#{description[:solutions]}"
190
+ ticket
191
+ end
192
+
193
+ # Generates the NXID. The NXID is a unique identifier used to find and update and/or close tickets.
194
+ #
195
+ # * *Args* :
196
+ # - +nexpose_identifier_id+ - Site/TAG ID the tickets are being generated for. Required for all? { |e| } ticketing modes
197
+ # - +row+ - Row from the generated Nexpose CSV report. Required for default ('D') mode.
198
+ # - +current_ip+ - The IP address of that this ticket is for. Required for IP mode ('I') mode.
199
+ #
200
+ # * *Returns* :
201
+ # - NXID string.
202
+ #
203
+ def generate_nxid(nexpose_id, row)
204
+ fail 'Row data is nil' if row.nil?
205
+
206
+ case @ticketing_mode
207
+ when 'D' then "#{nexpose_id}d#{row['asset_id']}d#{row['vulnerability_id']}"
208
+ when 'I' then "#{nexpose_id}i#{row['ip_address']}"
209
+ when 'V' then "#{nexpose_id}v#{row['vulnerability_id']}"
210
+ else fail 'Ticketing mode not recognised.'
211
+ end
212
+ end
213
+
214
+ # Formats the row data to be inserted into a 'D' or 'I' mode ticket description.
215
+ #
216
+ # - +row+ - CSV row containing vulnerability data.
217
+ #
218
+ # * *Returns* :
219
+ # - String formatted with vulnerability data.
220
+ #
221
+ def get_vuln_info(row)
222
+ ticket = get_vuln_header(row)
223
+ ticket << get_discovery_info(row)
224
+ ticket << get_references(row)
225
+ ticket << "\n#{get_solutions(row)}"
226
+ ticket.gsub("\n", "\n ")
227
+ end
228
+
229
+ # Generates the vulnerability header from the row data.
230
+ #
231
+ # - +row+ - CSV row containing vulnerability data.
232
+ #
233
+ # * *Returns* :
234
+ # - String formatted with vulnerability data.
235
+ #
236
+ def get_vuln_header(row)
237
+ ticket = "\n=============================="
238
+ ticket << "\nVulnerability ID: #{row['vulnerability_id']}"
239
+ ticket << "\nCVSS Score: #{row['cvss_score']}"
240
+ ticket << "\n=============================="
241
+ ticket
242
+ end
243
+
244
+ # Generates the ticket's title depending on the ticketing mode.
245
+ #
246
+ # - +row+ - CSV row containing vulnerability data.
247
+ #
248
+ # * *Returns* :
249
+ # - String containing the ticket title.
250
+ #
251
+ def get_title(row, maximum=nil)
252
+ title = case @ticketing_mode
253
+ when 'D' then "#{row['ip_address']} => #{get_short_summary(row)}"
254
+ when 'I' then "#{row['ip_address']} => Vulnerabilities"
255
+ when 'V' then "Vulnerability: #{row['title']}"
256
+ else fail 'Ticketing mode not recognised.'
257
+ end
258
+ return title if maximum == nil || title.length < maximum
259
+
260
+ title = "#{title[0, 97]}..."
261
+ end
262
+
263
+ # Generates a short summary for a vulnerability.
264
+ #
265
+ # - +row+ - CSV row containing vulnerability data.
266
+ #
267
+ # * *Returns* :
268
+ # - String containing a short summary of the vulnerability.
269
+ #
270
+ def get_short_summary(row)
271
+ summary = row['solutions']
272
+ delimiter = summary.to_s.index('|')
273
+ return summary[summary.index(':')+1...delimiter].strip if delimiter
274
+ summary.length <= 100 ? summary : summary[0...100]
275
+ end
276
+
277
+ # Formats the solutions for a vulnerability in a format suitable to be inserted into a ticket.
278
+ #
279
+ # - +row+ - CSV row containing vulnerability data.
280
+ #
281
+ # * *Returns* :
282
+ # - String formatted with solution information.
283
+ #
284
+ def get_solutions(row)
285
+ row['solutions'].to_s.gsub('|', "\n").gsub('~', "\n--\n")
286
+ end
287
+
288
+ def get_discovery_info(row)
289
+ return '' if row['first_discovered'].to_s == ""
290
+ info = "\nFirst Seen: #{row['first_discovered']}\n"
291
+ info << "Last Seen: #{row['most_recently_discovered']}\n"
292
+ info
293
+ end
294
+
295
+ # Formats the references for a vulnerability in a format suitable to be inserted into a ticket.
296
+ #
297
+ # - +row+ - CSV row containing vulnerability data.
298
+ #
299
+ # * *Returns* :
300
+ # - String formatted with source and reference.
301
+ #
302
+ def get_references(row)
303
+ return '' if row['references'].nil?
304
+ references = "\nSources:\n"
305
+ refs = row['references'].split(', ')
306
+ refs[MAX_NUM_REFS] = '...' if refs.count > MAX_NUM_REFS
307
+ refs[0..MAX_NUM_REFS].each { |r| references << " - #{r}\n" }
308
+ references
309
+ end
310
+
311
+
312
+ # Returns the assets for a vulnerability in a format suitable to be inserted into a ticket.
313
+ #
314
+ # - +row+ - CSV row containing vulnerability data.
315
+ #
316
+ # * *Returns* :
317
+ # - String formatted with affected assets.
318
+ #
319
+ def get_assets(row)
320
+ status = row['comparison']
321
+ header = "\n#{status || 'Affected' } Assets\n"
322
+
323
+ assets = []
324
+ row['assets'].to_s.split('~').each do |a|
325
+ details = a.split('|')
326
+ assets << " - #{details[1]} #{"\t(#{details[2]})" if !details[2].empty?}"
327
+ end
328
+ asset_list = assets.join("\n")
329
+ "#{header}#{asset_list}"
330
+ end
331
+
332
+ # Returns the relevant row values for printing.
333
+ #
334
+ # - +fields+ - The fields which are relevant to the ticket.
335
+ # - +row+ - CSV row containing vulnerability data.
336
+ #
337
+ # * *Returns* :
338
+ # - String formatted with relevant fields.
339
+ #
340
+ def get_field_info(fields, row)
341
+ fields.map { |x| "#{x.sub("_", " ")}: #{row[x]}" }.join(", ")
342
+ end
343
+ end
344
+ end
@@ -6,14 +6,14 @@
6
6
  # (M) Helper class name
7
7
  :helper_name: ServiceDeskHelper
8
8
 
9
- # (M) REST Enpoint on the ServiceDesk instance
9
+ # (M) REST Endpoint on the ServiceDesk instance
10
10
  :rest_uri: https://uritoservicedesk:8080/sdpapi/request
11
11
  # (M) API Key to access the ServiceDesk instance
12
12
  :api_key: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
13
13
  # (M) local ticket database path
14
14
  :ticket_db_path: /var/lib/gems/1.9.1/gems/nexpose_ticketing-0.2.3.jh/lib/nexpose_ticketing/log/servicedesk_ticketdb
15
15
 
16
- # (M) ServiceDesk requestor name
16
+ # (M) ServiceDesk requester name
17
17
  :requester: nexpose
18
- # (M) SerivceDesk incident group
18
+ # (M) ServiceDesk incident group
19
19
  :group: Network
@@ -6,6 +6,8 @@
6
6
  :options:
7
7
  # (M) Enables logging to the log directory.
8
8
  :logging_enabled: true
9
+ # (M) Sets the log level threshold for output.
10
+ :log_level: info
9
11
  # Filters the reports to specific sites one per line, leave empty for no site.
10
12
  :sites:
11
13
  - '1'
@@ -4,49 +4,21 @@ require 'net/https'
4
4
  require 'uri'
5
5
  require 'csv'
6
6
  require 'nexpose_ticketing/nx_logger'
7
+ require 'nexpose_ticketing/version'
8
+ require 'nexpose_ticketing/common_helper'
7
9
 
8
10
  # This class serves as the JIRA interface
9
11
  # that creates issues within JIRA from vulnerabilities
10
12
  # found in Nexpose.
11
13
  # Copyright:: Copyright (c) 2014 Rapid7, LLC.
12
14
  class JiraHelper
13
- # TODO: Add V Mode.
14
- # TODO: Allow updates/closed loop.
15
15
  attr_accessor :jira_data, :options
16
16
  def initialize(jira_data, options)
17
17
  @jira_data = jira_data
18
18
  @options = options
19
- @log = NexposeTicketing::NXLogger.new
20
- end
19
+ @log = NexposeTicketing::NxLogger.instance
21
20
 
22
- # Generates the NXID. The NXID is a unique identifier used to find and update and/or close tickets.
23
- #
24
- # * *Args* :
25
- # - +nexpose_identifier_id+ - Site/TAG ID the tickets are being generated for. Required for all ticketing modes
26
- # - +row+ - Row from the generated Nexpose CSV report. Required for default ('D') mode.
27
- # - +current_ip+ - The IP address of that this ticket is for. Required for IP mode ('I') mode.
28
- #
29
- # * *Returns* :
30
- # - NXID string.
31
- #
32
- def generate_nxid(nexpose_identifier_id, row=nil, current_ip=nil)
33
- fail 'Nexpose Identifier ID is required to generate the NXID.' if nexpose_identifier_id.empty?
34
- case @options[:ticket_mode]
35
- # 'D' Default mode: IP *-* Vulnerability
36
- when 'D'
37
- fail 'Row is required to generate the NXID in \'D\' mode.' if row.nil? || row.empty?
38
- @nxid = "#{nexpose_identifier_id}#{row['asset_id']}#{row['vulnerability_id']}#{row['solution_id']}"
39
- # 'I' IP address mode: IP address -* Vulnerability
40
- when 'I'
41
- fail 'Current IP is required to generate the NXID in \'I\' mode.' if current_ip.nil? || current_ip.empty?
42
- @nxid = "#{nexpose_identifier_id}#{current_ip.tr('.','')}"
43
- # 'V' mode net yet implemented.
44
- # 'V' Vulnerability mode: Vulnerability -* IP address
45
- # when 'V'
46
- # @NXID = "#{nexpose_identifier_id}#{row['current_asset_id']}#{row['current_vuln_id']}"
47
- else
48
- fail 'Could not close tickets - do not understand the ticketing mode!'
49
- end
21
+ @common_helper = NexposeTicketing::CommonHelper.new(@options)
50
22
  end
51
23
 
52
24
  # Fetches the Jira ticket key e.g INT-1. This is required to post updates to the Jira.
@@ -123,12 +95,13 @@ class JiraHelper
123
95
  end
124
96
  end
125
97
  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]}>.")
98
+ error = "Response was <#{transitions}> and desired close Step ID was <#{@jira_data[:close_step_id]}>. Jira returned no valid transition to close the ticket!"
99
+ @log.log_message(error)
127
100
  return nil
128
101
  end
129
102
 
130
103
  def create_tickets(tickets)
131
- fail 'Ticket(s) cannot be empty.' if tickets.empty? || tickets.nil?
104
+ fail 'Ticket(s) cannot be empty.' if tickets.nil? || tickets.empty?
132
105
  tickets.each do |ticket|
133
106
  headers = { 'Content-Type' => 'application/json',
134
107
  'Accept' => 'application/json' }
@@ -149,94 +122,64 @@ class JiraHelper
149
122
  end
150
123
 
151
124
  # Prepares tickets from the CSV.
152
- # TODO Implement V Version.
153
125
  def prepare_create_tickets(vulnerability_list, nexpose_identifier_id)
154
- @ticket = Hash.new(-1)
126
+ @log.log_message('Preparing ticket requests...')
155
127
  case @options[:ticket_mode]
156
128
  # 'D' Default IP *-* Vulnerability
157
- when 'D'
158
- prepare_tickets_default(vulnerability_list, nexpose_identifier_id)
129
+ when 'D' then matching_fields = ['ip_address', 'vulnerability_id']
159
130
  # 'I' IP address -* Vulnerability
160
- when 'I'
161
- prepare_tickets_by_ip(vulnerability_list, nexpose_identifier_id)
131
+ when 'I' then matching_fields = ['ip_address']
132
+ # 'V' Vulnerability -* Assets
133
+ when 'V' then matching_fields = ['vulnerability_id']
162
134
  else
163
135
  fail 'Unsupported ticketing mode selected.'
164
136
  end
165
- end
166
137
 
167
- # Prepares and creates tickets in default mode.
168
- def prepare_tickets_default(vulnerability_list, nexpose_identifier_id)
169
- @log.log_message('Preparing tickets for default mode.')
170
- tickets = []
171
- CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
172
- # JiraHelper doesn't like new line characters in their summaries.
173
- summary = row['summary'].gsub(/\n/, ' ')
174
- ticket = {
175
- 'fields' => {
176
- 'project' => {
177
- 'key' => "#{@jira_data[:project]}" },
178
- 'summary' => "#{row['ip_address']} => #{summary}",
179
- 'description' => "CVSS Score: #{row['cvss_score']} \n\n #{row['fix']} \n\n #{row['url']} \n\n\n NXID: #{generate_nxid(nexpose_identifier_id, row)}",
180
- 'issuetype' => {
181
- 'name' => 'Task' }
182
- }
183
- }.to_json
184
- tickets.push(ticket)
185
- end
186
- tickets
138
+ prepare_tickets(vulnerability_list, nexpose_identifier_id, matching_fields)
187
139
  end
188
140
 
189
- # Prepare tickets from the CSV of vulnerabilities exported from Nexpose. This method batches tickets
190
- # per IP i.e. any vulnerabilities for a single IP in one ticket
191
- #
192
- # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
193
- # - +nexpose_identifier_id+ - Site/TAG ID the vulnerability list was generate from.
194
- #
195
- # * *Returns* :
196
- # - List of JSON-formatted tickets for updating within Jira.
197
- #
198
- def prepare_tickets_by_ip(vulnerability_list, nexpose_identifier_id)
199
- @log.log_message('Preparing tickets for IP mode.')
141
+ def prepare_tickets(vulnerability_list, nexpose_identifier_id, matching_fields)
142
+ @ticket = Hash.new(-1)
143
+
144
+ @log.log_message("Preparing tickets for #{@options[:ticket_mode]} mode.")
200
145
  tickets = []
201
- current_ip = -1
146
+ previous_row = nil
147
+ description = nil
202
148
  CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
203
- if current_ip == -1
204
- current_ip = row['ip_address']
149
+ if previous_row.nil?
150
+ previous_row = row
151
+
205
152
  @ticket = {
206
153
  'fields' => {
207
154
  'project' => {
208
155
  'key' => "#{@jira_data[:project]}" },
209
- 'summary' => "#{row['ip_address']} => Vulnerabilities",
156
+ 'summary' => @common_helper.get_title(row),
210
157
  'description' => '',
211
158
  'issuetype' => {
212
159
  'name' => 'Task' }
213
160
  }
214
161
  }
215
- end
216
- # TODO: Better formatting this.
217
- if current_ip == row['ip_address']
218
- @ticket['fields']['description'] +=
219
- "\n ==============================\n\n
220
- #{row['summary']} \n CVSS Score: #{row['cvss_score']}
221
- \n\n ==============================\n
222
- \n Source: #{row['source']}, Reference: #{row['reference']}
223
- \n
224
- \n First seen: #{row['first_discovered']}
225
- \n Last seen: #{row['most_recently_discovered']}
226
- \n Fix:
227
- \n #{row['fix']}\n\n #{row['url']}
228
- \n
229
- \n\n"
230
- end
231
- unless current_ip == row['ip_address']
232
- @ticket['fields']['description'] += "\n\n\n NXID: #{generate_nxid(nexpose_identifier_id, row, current_ip)}"
162
+ description = @common_helper.get_description(nexpose_identifier_id, row)
163
+ elsif matching_fields.any? { |x| previous_row[x].nil? || previous_row[x] != row[x] }
164
+ info = @common_helper.get_field_info(matching_fields, previous_row)
165
+ @log.log_message("Generated ticket with #{info}")
166
+
167
+ @ticket['fields']['description'] = @common_helper.print_description(description)
233
168
  tickets.push(@ticket.to_json)
234
- current_ip = -1
169
+ previous_row = nil
170
+ description = nil
235
171
  redo
172
+ else
173
+ description = @common_helper.update_description(description, row)
236
174
  end
237
175
  end
238
- @ticket['fields']['description'] += "\n\n\n NXID: #{generate_nxid(nexpose_identifier_id, nil, current_ip)}"
239
- tickets.push(@ticket.to_json) unless @ticket.nil?
176
+
177
+ unless @ticket.nil? || @ticket.empty?
178
+ @ticket['fields']['description'] = @common_helper.print_description(description)
179
+ tickets.push(@ticket.to_json)
180
+ end
181
+
182
+ @log.log_message("Generated <#{tickets.count.to_s}> tickets.")
240
183
  tickets
241
184
  end
242
185
 
@@ -309,13 +252,13 @@ class JiraHelper
309
252
  @nxid = nil
310
253
  tickets = []
311
254
  CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
312
- @nxid = generate_nxid(nexpose_identifier_id, nil, row['ip_address'])
255
+ @nxid = @common_helper.generate_nxid(nexpose_identifier_id, row)
313
256
  # Query Jira for the ticket by unique id (generated NXID)
314
257
  queried_key = get_jira_key("jql=project=#{@jira_data[:project]} AND description ~ \"NXID: #{@nxid}\" AND (status != #{@jira_data[:close_step_name]})&fields=key")
315
258
  if queried_key.nil? || queried_key.empty?
316
259
  @log.log_message("Error when closing tickets - query for NXID <#{@nxid}> should have returned a Jira key!!")
317
260
  else
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.
261
+ #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 keys to close.
319
262
  tickets.push(queried_key)
320
263
  end
321
264
  end
@@ -361,8 +304,7 @@ class JiraHelper
361
304
  end
362
305
  end
363
306
 
364
- # Prepare ticket updates from the CSV of vulnerabilities exported from Nexpose. This method batches tickets
365
- # per IP i.e. any vulnerabilities for a single IP in one ticket
307
+ # Prepare ticket updates from the CSV of vulnerabilities exported from Nexpose.
366
308
  #
367
309
  # - +vulnerability_list+ - CSV of vulnerabilities within Nexpose.
368
310
  # - +nexpose_identifier_id+ - Site/TAG ID the vulnerability list was generate from.
@@ -371,21 +313,25 @@ class JiraHelper
371
313
  # - List of JSON-formated tickets for updating within Jira.
372
314
  #
373
315
  def prepare_update_tickets(vulnerability_list, nexpose_identifier_id)
374
- fail 'Ticket updates are only supported in IP-address mode.' if @options[:ticket_mode] != 'I'
316
+ fail 'Ticket updates are not supported in Default mode.' if @options[:ticket_mode] == 'D'
375
317
  @log.log_message('Preparing tickets to update.')
376
318
  #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.
377
- updated_tickets = prepare_tickets_by_ip(vulnerability_list, nexpose_identifier_id)
319
+ updated_tickets = prepare_create_tickets(vulnerability_list, nexpose_identifier_id)
320
+
378
321
  tickets_to_send = []
379
322
 
380
323
  #Find the keys that exist (IPs that have tickets already)
381
324
  updated_tickets.each do |ticket|
382
- nxid = JSON.parse(ticket)['fields']['description'].squeeze("\n").lines.to_a.last
383
- if (nxid.slice! "NXID:").nil?
325
+ description = JSON.parse(ticket)['fields']['description']
326
+ nxid_index = description.rindex("NXID")
327
+ nxid = nxid_index.nil? ? nil : description[nxid_index..-1]
328
+
329
+ if (nxid).nil?
384
330
  #Could not get NXID from the last line in the description. Do not push the invalid description.
385
331
  @log.log_message("Failed to parse the NXID from a generated ticket update! Ignoring ticket <#{nxid}>")
386
332
  next
387
333
  end
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")
334
+ queried_key = get_jira_key("jql=project=#{@jira_data[:project]} AND description ~ \"#{nxid.strip}\" AND (status != #{@jira_data[:close_step_name]})&fields=key")
389
335
  ticket_key_pair = []
390
336
  ticket_key_pair << queried_key
391
337
  ticket_key_pair << ticket
@@ -393,4 +339,4 @@ class JiraHelper
393
339
  end
394
340
  tickets_to_send
395
341
  end
396
- end
342
+ end