nexpose_ticketing 0.8.3 → 1.0.0

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