nexpose_ticketing 1.0.2 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +22 -0
- data/bin/nexpose_ticketing +45 -0
- data/lib/nexpose_ticketing/config/jira.config +5 -1
- data/lib/nexpose_ticketing/config/remedy.config +4 -1
- data/lib/nexpose_ticketing/config/servicedesk.config +4 -1
- data/lib/nexpose_ticketing/config/servicenow.config +4 -1
- data/lib/nexpose_ticketing/config/ticket_service.config +13 -5
- data/lib/nexpose_ticketing/helpers/base_helper.rb +43 -0
- data/lib/nexpose_ticketing/helpers/jira_helper.rb +149 -97
- data/lib/nexpose_ticketing/helpers/remedy_helper.rb +37 -52
- data/lib/nexpose_ticketing/helpers/servicedesk_helper.rb +30 -49
- data/lib/nexpose_ticketing/helpers/servicenow_helper.rb +39 -56
- data/lib/nexpose_ticketing/modes/base_mode.rb +199 -0
- data/lib/nexpose_ticketing/modes/default_mode.rb +50 -0
- data/lib/nexpose_ticketing/modes/ip_mode.rb +52 -0
- data/lib/nexpose_ticketing/modes/vulnerability_mode.rb +50 -0
- data/lib/nexpose_ticketing/nx_logger.rb +44 -33
- data/lib/nexpose_ticketing/queries.rb +30 -27
- data/lib/nexpose_ticketing/report_helper.rb +1 -0
- data/lib/nexpose_ticketing/ticket_metrics.rb +39 -0
- data/lib/nexpose_ticketing/ticket_repository.rb +65 -206
- data/lib/nexpose_ticketing/ticket_service.rb +470 -441
- data/lib/nexpose_ticketing/version.rb +1 -1
- metadata +15 -16
- data/bin/nexpose_jira +0 -27
- data/bin/nexpose_remedy +0 -27
- data/bin/nexpose_servicedesk +0 -27
- data/bin/nexpose_servicenow +0 -27
- data/lib/nexpose_ticketing/common_helper.rb +0 -344
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 03778e753df032041d1c6a46bcef5f89df66d705
|
4
|
+
data.tar.gz: d163c3488d78c12ade992381b06479ba8ad40be9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 958e7e85146d2c2010b10723b06570203effb135017b3f280d8132d60fe5903d73b713a4d3bf9356f9dc63881067b52f8dde20e72546bad1565857bedb2284ae
|
7
|
+
data.tar.gz: ebaaa9c06a62105e3cb00425f1cc7e2ed93f67d2103f8224488da3f673b226dfd73abce6a72bb389869b6aa5bb4fcafad25167356167c0ef044b7f8018c61e08
|
data/README.md
CHANGED
@@ -72,6 +72,28 @@ We welcome contributions to this package. We ask only that pull requests and pat
|
|
72
72
|
|
73
73
|
##Changelog
|
74
74
|
|
75
|
+
###1.2.0
|
76
|
+
|
77
|
+
####Configuration Options
|
78
|
+
Ticketing mode must be specified using the entire title, rather than a single character. e.g. 'Vulnerability' instead of 'V'
|
79
|
+
Added the following configuration option:
|
80
|
+
- log_console - NXLogger also gets printed to the console.
|
81
|
+
|
82
|
+
####Ticketing Modes
|
83
|
+
Ticketing modes (Default, IP, Vulnerability) are now abstracted into their own classes.
|
84
|
+
CommonHelper has been replaced with BaseMode from which other modes are extended.
|
85
|
+
|
86
|
+
####Ticketing Helpers
|
87
|
+
Ticketing helpers extend a BaseHelper class.
|
88
|
+
Ticketing helpers now log the number of tickets that are opened/closed/updated.
|
89
|
+
|
90
|
+
###1.1.0
|
91
|
+
10-02-2016
|
92
|
+
Added the following configuration options:
|
93
|
+
- max_ticket_length - Specifies a maximum length for the description field of a ticket.
|
94
|
+
- max_title_length - Specifies a maximum length for the title of a ticket.
|
95
|
+
- max_num_refs - Specifies the maximum number of references included in a vulnerability description.
|
96
|
+
|
75
97
|
###1.0.2
|
76
98
|
08-02-2016
|
77
99
|
- Encoding is now enforced as UTF-8 when parsing CSV files - fixes environment-specific errors.
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'yaml'
|
3
|
+
require 'nexpose_ticketing'
|
4
|
+
require 'nexpose_ticketing/nx_logger'
|
5
|
+
require 'nexpose_ticketing/version'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
OptionParser.new do |opts|
|
10
|
+
opts.banner = "Usage: nexpose_ticketing [service name]"
|
11
|
+
end.parse!
|
12
|
+
|
13
|
+
if ARGV.count == 0
|
14
|
+
puts 'Ticketing system name required.'
|
15
|
+
exit -1
|
16
|
+
end
|
17
|
+
|
18
|
+
system = ARGV.first
|
19
|
+
config_path = File.join(File.dirname(__FILE__),
|
20
|
+
"../lib/nexpose_ticketing/config/#{system}.config")
|
21
|
+
|
22
|
+
unless File.exists? config_path
|
23
|
+
puts "Configuration file could not be found at #{config_path}"
|
24
|
+
exit -1
|
25
|
+
end
|
26
|
+
|
27
|
+
# Read in JIRA options from jira.config.
|
28
|
+
service_options = begin
|
29
|
+
YAML.load_file(config_path)
|
30
|
+
rescue ArgumentError => e
|
31
|
+
raise "Could not parse YAML #{config_path} : #{e.message}"
|
32
|
+
end
|
33
|
+
|
34
|
+
log = NexposeTicketing::NxLogger.instance
|
35
|
+
log.setup_statistics_collection(service_options["vendor"],
|
36
|
+
service_options["product"],
|
37
|
+
NexposeTicketing::VERSION)
|
38
|
+
log.setup_logging(true, 'info')
|
39
|
+
|
40
|
+
current_encoding = Encoding.default_external=Encoding.find("UTF-8")
|
41
|
+
|
42
|
+
log.log_message("Current Encoding set to: #{current_encoding}")
|
43
|
+
|
44
|
+
# Initialize Ticket Service using JIRA.
|
45
|
+
NexposeTicketing.start(service_options)
|
@@ -2,7 +2,11 @@
|
|
2
2
|
# This configuration file defines all the particular options necessary to support the helper.
|
3
3
|
# Fields marked (M) are mandatory.
|
4
4
|
#
|
5
|
-
# (M)
|
5
|
+
# (M) Product name
|
6
|
+
:product: JIRA
|
7
|
+
# (M) Vendor
|
8
|
+
:vendor: Atlassian
|
9
|
+
# (M) Helper class name.
|
6
10
|
:helper_name: JiraHelper
|
7
11
|
# Optional parameters, these are implementation specific.
|
8
12
|
:jira_url: https://url/rest/api/2/issue/
|
@@ -2,7 +2,10 @@
|
|
2
2
|
# This configuration file defines all the options necessary to support the helper.
|
3
3
|
# Fields marked (M) are mandatory.
|
4
4
|
#
|
5
|
-
|
5
|
+
# (M) Product name
|
6
|
+
:product: ServiceDesk
|
7
|
+
# (M) Vendor
|
8
|
+
:vendor: ManageEngine
|
6
9
|
# (M) Helper class name
|
7
10
|
:helper_name: ServiceDeskHelper
|
8
11
|
|
@@ -2,7 +2,10 @@
|
|
2
2
|
# This configuration file defines all the options necessary to support the helper.
|
3
3
|
# Fields marked (M) are mandatory.
|
4
4
|
#
|
5
|
-
|
5
|
+
# (M) Product name
|
6
|
+
:product: ServiceNow
|
7
|
+
# (M) Vendor
|
8
|
+
:vendor: ServiceNow
|
6
9
|
# (M) Helper class name
|
7
10
|
:helper_name: ServiceNowHelper
|
8
11
|
|
@@ -8,20 +8,28 @@
|
|
8
8
|
:logging_enabled: true
|
9
9
|
# (M) Sets the log level threshold for output.
|
10
10
|
:log_level: info
|
11
|
+
# Enables logger output to console.
|
12
|
+
:log_console: false
|
11
13
|
# Filters the reports to specific sites one per line, leave empty for no site.
|
12
14
|
:sites:
|
13
15
|
- '1'
|
14
16
|
# Minimum floor severity to report on. Number between 0 and 10.
|
15
17
|
:severity: 8
|
16
18
|
# (M) Name of the report historical file for sites saved in disk.
|
17
|
-
:
|
19
|
+
:site_file_name: last_scan_data.csv
|
18
20
|
# (M) Name of the report historical file for tags saved in disk.
|
19
21
|
:tag_file_name: tag_last_scan_data.csv
|
20
22
|
# (M) Defines the ticket creation mode:
|
21
|
-
# '
|
22
|
-
# '
|
23
|
-
# '
|
24
|
-
:ticket_mode:
|
23
|
+
# 'Default' Default IP *-* Vulnerability
|
24
|
+
# 'IP' IP address -* Vulnerability
|
25
|
+
# 'Vulnerability' Vulnerability -* IP Address
|
26
|
+
:ticket_mode: IP
|
27
|
+
# The maximum character length of a ticket description (-1 is unbounded)
|
28
|
+
:max_ticket_length: -1
|
29
|
+
# The maximum character length of a ticket title, including elipsis (...)
|
30
|
+
:max_title_length: 100
|
31
|
+
# The number of references to include in the ticket description
|
32
|
+
:max_num_refs: 3
|
25
33
|
# Timeout in seconds. The number of seconds the GEM waits for a response from Nexpose before exiting.
|
26
34
|
:timeout: 10800
|
27
35
|
# Ticket batching. Breaks ticket processing into groups of value size controlling resource utilisation of both systems.
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
require 'net/https'
|
4
|
+
require 'uri'
|
5
|
+
require 'csv'
|
6
|
+
require 'nexpose_ticketing/nx_logger'
|
7
|
+
require 'nexpose_ticketing/version'
|
8
|
+
require_relative '../ticket_metrics'
|
9
|
+
|
10
|
+
class BaseHelper
|
11
|
+
attr_accessor :service_data, :options
|
12
|
+
|
13
|
+
def initialize(service_data, options, mode)
|
14
|
+
@service_data = service_data
|
15
|
+
@options = options
|
16
|
+
@log = NexposeTicketing::NxLogger.instance
|
17
|
+
@metrics = NexposeTicketing::TicketMetrics.new
|
18
|
+
|
19
|
+
load_dependencies
|
20
|
+
@mode_helper = mode
|
21
|
+
end
|
22
|
+
|
23
|
+
# Load the mode helper specified in the config
|
24
|
+
def load_dependencies
|
25
|
+
file = "#{@options[:ticket_mode]}_mode.rb".downcase
|
26
|
+
path = File.join(File.dirname(__FILE__), "../modes/#{file}")
|
27
|
+
|
28
|
+
@log.log_message("Loading #{@options[:ticket_mode]} mode dependencies.")
|
29
|
+
begin
|
30
|
+
require_relative path
|
31
|
+
rescue => e
|
32
|
+
error = "Ticket mode dependency '#{file}' could not be loaded."
|
33
|
+
@log.log_error_message e.to_s
|
34
|
+
@log.log_error_message error
|
35
|
+
fail error
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Performs any necessary clean-up
|
40
|
+
def finish
|
41
|
+
@metrics.finish
|
42
|
+
end
|
43
|
+
end
|
@@ -5,20 +5,15 @@ require 'uri'
|
|
5
5
|
require 'csv'
|
6
6
|
require 'nexpose_ticketing/nx_logger'
|
7
7
|
require 'nexpose_ticketing/version'
|
8
|
-
|
8
|
+
require_relative './base_helper'
|
9
9
|
|
10
10
|
# This class serves as the JIRA interface
|
11
11
|
# that creates issues within JIRA from vulnerabilities
|
12
12
|
# found in Nexpose.
|
13
13
|
# Copyright:: Copyright (c) 2014 Rapid7, LLC.
|
14
|
-
class JiraHelper
|
15
|
-
|
16
|
-
|
17
|
-
@jira_data = jira_data
|
18
|
-
@options = options
|
19
|
-
@log = NexposeTicketing::NxLogger.instance
|
20
|
-
|
21
|
-
@common_helper = NexposeTicketing::CommonHelper.new(@options)
|
14
|
+
class JiraHelper < BaseHelper
|
15
|
+
def initialize(service_data, options, mode)
|
16
|
+
super(service_data, options, mode)
|
22
17
|
end
|
23
18
|
|
24
19
|
# Fetches the Jira ticket key e.g INT-1. This is required to post updates to the Jira.
|
@@ -29,24 +24,34 @@ class JiraHelper
|
|
29
24
|
# * *Returns* :
|
30
25
|
# - Jira ticket key if found, nil otherwise.
|
31
26
|
#
|
32
|
-
def get_jira_key(jql_query)
|
27
|
+
def get_jira_key(jql_query, nxid = nil)
|
33
28
|
fail 'JQL query string cannot be empty.' if jql_query.empty?
|
34
29
|
headers = { 'Content-Type' => 'application/json',
|
35
30
|
'Accept' => 'application/json' }
|
36
31
|
|
37
|
-
uri = URI.parse(("#{@
|
32
|
+
uri = URI.parse(("#{@service_data[:jira_url]}".split("/")[0..-2].join('/') + '/search'))
|
38
33
|
uri.query = [uri.query, URI.escape(jql_query)].compact.join('&')
|
39
34
|
req = Net::HTTP::Get.new(uri.to_s, headers)
|
40
35
|
response = send_jira_request(uri, req)
|
41
36
|
|
42
37
|
issues = JSON.parse(response.body)['issues']
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
38
|
+
|
39
|
+
if issues.nil? || !issues.any?
|
40
|
+
@log.log_message "JIRA did not return any keys for query containing NXID #{nxid}"
|
41
|
+
return nil
|
42
|
+
end
|
43
|
+
|
44
|
+
if issues.size > 1
|
45
|
+
# If Jira returns more than one key for a "unique" NXID query result then something has gone wrong...
|
46
|
+
# Safest response is to return no key and let logic elsewhere dictate the action to take.
|
47
|
+
error = "Jira returned multiple keys for query containing NXID #{nxid}."
|
48
|
+
error += " Please check project within JIRA."
|
49
|
+
error += " Response was <#{issues}>"
|
50
|
+
@log.log_error_message(error)
|
51
|
+
return nil
|
52
|
+
end
|
53
|
+
|
54
|
+
issues[0]['key']
|
50
55
|
end
|
51
56
|
|
52
57
|
# Sends a HTTP request to the JIRA console.
|
@@ -59,7 +64,7 @@ class JiraHelper
|
|
59
64
|
# - HTTPResponse containing result from the JIRA console.
|
60
65
|
#
|
61
66
|
def send_request(uri, request, ticket=false)
|
62
|
-
request.basic_auth @
|
67
|
+
request.basic_auth @service_data[:username], @service_data[:password]
|
63
68
|
resp = Net::HTTP.new(uri.host, uri.port)
|
64
69
|
|
65
70
|
# Enable this line for debugging the https call.
|
@@ -72,9 +77,39 @@ class JiraHelper
|
|
72
77
|
|
73
78
|
resp.start do |http|
|
74
79
|
res = http.request(request)
|
75
|
-
|
76
|
-
|
77
|
-
|
80
|
+
code = res.code.to_i
|
81
|
+
|
82
|
+
next if code.between?(200,299)
|
83
|
+
|
84
|
+
unless code.between?(400, 499)
|
85
|
+
@log.log_error_message("Error submitting ticket data: #{res.message}, #{res.body}")
|
86
|
+
return res
|
87
|
+
end
|
88
|
+
|
89
|
+
@log.log_error_message("Unable to access JIRA.")
|
90
|
+
@log.log_error_message "Error code: #{code}"
|
91
|
+
|
92
|
+
#Bad project etc
|
93
|
+
case code
|
94
|
+
when 400
|
95
|
+
errors = res.body.scan(/errors":{(.+)}}/).first.first
|
96
|
+
errors = errors.gsub('"', '').gsub(':', ': ').gsub(',', "\n")
|
97
|
+
@log.log_error_message "Error messages:\n#{errors}"
|
98
|
+
#Log in failed
|
99
|
+
when 401
|
100
|
+
@log.log_error_message "Message: #{res.message.strip}"
|
101
|
+
@log.log_error_message "Reason: #{res['x-seraph-loginreason']}"
|
102
|
+
#Locked out
|
103
|
+
when 403
|
104
|
+
@log.log_error_message "Message: #{res.message.strip}"
|
105
|
+
@log.log_error_message "Reason: #{res['x-seraph-loginreason']}"
|
106
|
+
@log.log_error_message "#{res['x-authentication-denied-reason']}"
|
107
|
+
else
|
108
|
+
#e.g. 404 - bad URL
|
109
|
+
@log.log_error_message "Message: #{res.message.strip}"
|
110
|
+
end
|
111
|
+
|
112
|
+
return res
|
78
113
|
end
|
79
114
|
end
|
80
115
|
|
@@ -120,7 +155,7 @@ class JiraHelper
|
|
120
155
|
headers = { 'Content-Type' => 'application/json',
|
121
156
|
'Accept' => 'application/json' }
|
122
157
|
|
123
|
-
uri = URI.parse(("#{@
|
158
|
+
uri = URI.parse(("#{@service_data[:jira_url]}#{jira_key}/transitions?expand=transitions.fields."))
|
124
159
|
req = Net::HTTP::Get.new(uri.to_s, headers)
|
125
160
|
response = send_jira_request(uri, req)
|
126
161
|
|
@@ -133,42 +168,39 @@ class JiraHelper
|
|
133
168
|
end
|
134
169
|
end
|
135
170
|
end
|
136
|
-
error = "Response was <#{transitions}> and desired close Step ID was <#{@
|
171
|
+
error = "Response was <#{transitions}> and desired close Step ID was <#{@service_data[:close_step_id]}>. Jira returned no valid transition to close the ticket!"
|
137
172
|
@log.log_message(error)
|
138
173
|
return nil
|
139
174
|
end
|
140
175
|
|
141
176
|
def create_tickets(tickets)
|
142
177
|
fail 'Ticket(s) cannot be empty.' if tickets.nil? || tickets.empty?
|
178
|
+
created_tickets = 0
|
179
|
+
|
143
180
|
tickets.each do |ticket|
|
144
181
|
headers = { 'Content-Type' => 'application/json',
|
145
182
|
'Accept' => 'application/json' }
|
146
183
|
|
147
|
-
uri = URI.parse("#{@
|
148
|
-
|
184
|
+
uri = URI.parse("#{@service_data[:jira_url]}")
|
185
|
+
|
186
|
+
req = Net::HTTP::Post.new(uri, headers)
|
149
187
|
req.body = ticket
|
150
|
-
|
188
|
+
|
189
|
+
code = send_ticket(uri, req).code.to_i
|
190
|
+
break if code.between?(400, 499)
|
191
|
+
|
192
|
+
created_tickets += 1
|
151
193
|
end
|
194
|
+
|
195
|
+
@metrics.created created_tickets
|
152
196
|
end
|
153
197
|
|
154
198
|
# Prepares tickets from the CSV.
|
155
199
|
def prepare_create_tickets(vulnerability_list, nexpose_identifier_id)
|
200
|
+
@metrics.start
|
156
201
|
@log.log_message('Preparing ticket requests...')
|
157
|
-
|
158
|
-
|
159
|
-
when 'D' then matching_fields = ['ip_address', 'vulnerability_id']
|
160
|
-
# 'I' IP address -* Vulnerability
|
161
|
-
when 'I' then matching_fields = ['ip_address']
|
162
|
-
# 'V' Vulnerability -* Assets
|
163
|
-
when 'V' then matching_fields = ['vulnerability_id']
|
164
|
-
else
|
165
|
-
fail 'Unsupported ticketing mode selected.'
|
166
|
-
end
|
167
|
-
|
168
|
-
prepare_tickets(vulnerability_list, nexpose_identifier_id, matching_fields)
|
169
|
-
end
|
170
|
-
|
171
|
-
def prepare_tickets(vulnerability_list, nexpose_identifier_id, matching_fields)
|
202
|
+
matching_fields = @mode_helper.get_matching_fields
|
203
|
+
|
172
204
|
@ticket = Hash.new(-1)
|
173
205
|
|
174
206
|
@log.log_message("Preparing tickets for #{@options[:ticket_mode]} mode.")
|
@@ -182,34 +214,35 @@ class JiraHelper
|
|
182
214
|
@ticket = {
|
183
215
|
'fields' => {
|
184
216
|
'project' => {
|
185
|
-
'key' => "#{@
|
186
|
-
'summary' => @
|
217
|
+
'key' => "#{@service_data[:project]}" },
|
218
|
+
'summary' => @mode_helper.get_title(row),
|
187
219
|
'description' => '',
|
188
220
|
'issuetype' => {
|
189
221
|
'name' => 'Task' }
|
190
222
|
}
|
191
223
|
}
|
192
|
-
description = @
|
224
|
+
description = @mode_helper.get_description(nexpose_identifier_id, row)
|
193
225
|
elsif matching_fields.any? { |x| previous_row[x].nil? || previous_row[x] != row[x] }
|
194
|
-
info = @
|
226
|
+
info = @mode_helper.get_field_info(matching_fields, previous_row)
|
195
227
|
@log.log_message("Generated ticket with #{info}")
|
196
228
|
|
197
|
-
@ticket['fields']['description'] = @
|
229
|
+
@ticket['fields']['description'] = @mode_helper.print_description(description)
|
198
230
|
tickets.push(@ticket.to_json)
|
199
231
|
previous_row = nil
|
200
232
|
description = nil
|
201
233
|
redo
|
202
234
|
else
|
203
|
-
description = @
|
235
|
+
description = @mode_helper.update_description(description, row)
|
204
236
|
end
|
205
237
|
end
|
206
238
|
|
207
239
|
unless @ticket.nil? || @ticket.empty?
|
208
|
-
|
240
|
+
info = @mode_helper.get_field_info(matching_fields, previous_row)
|
241
|
+
@log.log_message("Generated ticket with #{info}")
|
242
|
+
@ticket['fields']['description'] = @mode_helper.print_description(description)
|
209
243
|
tickets.push(@ticket.to_json)
|
210
244
|
end
|
211
245
|
|
212
|
-
@log.log_message("Generated <#{tickets.count.to_s}> tickets.")
|
213
246
|
tickets
|
214
247
|
end
|
215
248
|
|
@@ -222,43 +255,48 @@ class JiraHelper
|
|
222
255
|
def close_tickets(tickets)
|
223
256
|
if tickets.nil? || tickets.empty?
|
224
257
|
@log.log_message('No tickets to close.')
|
225
|
-
|
226
|
-
|
227
|
-
|
258
|
+
return
|
259
|
+
end
|
260
|
+
closed_count = 0
|
228
261
|
|
229
|
-
|
230
|
-
|
231
|
-
req = Net::HTTP::Post.new(uri.to_s, headers)
|
262
|
+
headers = { 'Content-Type' => 'application/json',
|
263
|
+
'Accept' => 'application/json' }
|
232
264
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
@log.log_message("No valid transition found for ticket <#{ticket}>. Skipping closure.")
|
237
|
-
next
|
238
|
-
end
|
265
|
+
tickets.each do |ticket|
|
266
|
+
uri = URI.parse(("#{@service_data[:jira_url]}#{ticket}/transitions"))
|
267
|
+
req = Net::HTTP::Post.new(uri.to_s, headers)
|
239
268
|
|
240
|
-
|
241
|
-
|
242
|
-
transition
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
269
|
+
transition = get_jira_transition_details(ticket, @service_data[:close_step_id])
|
270
|
+
if transition.nil?
|
271
|
+
#Valid transition could not be found. Ignore ticket since we do not know what to do with it.
|
272
|
+
@log.log_message("No valid transition found for ticket <#{ticket}>. Skipping closure.")
|
273
|
+
next
|
274
|
+
end
|
275
|
+
|
276
|
+
#We need to find any required fields to send with the transition request
|
277
|
+
required_fields = []
|
278
|
+
transition['fields'].each do |field|
|
279
|
+
next unless field[1]['required'] == true
|
280
|
+
|
281
|
+
# Currently only required fields with 'allowedValues' in the JSON response are supported.
|
282
|
+
if not field[1].has_key? 'allowedValues'
|
283
|
+
@log.log_message("Closing ticket <#{ticket}> requires a field I know nothing about! Transition details are <#{transition}>. Ignoring this field.")
|
284
|
+
next
|
256
285
|
end
|
257
286
|
|
258
|
-
|
259
|
-
|
287
|
+
field = "{\"id\" : \"#{field[1]['allowedValues'][0]['id']}\"}"
|
288
|
+
field = "[#{field}]" if field[1]['schema']['type'] == 'array'
|
289
|
+
required_fields << "\"#{field[0]}\" : #{field}"
|
260
290
|
end
|
291
|
+
|
292
|
+
req.body = "{\"transition\" : {\"id\" : #{transition['id']}}, \"fields\" : { #{required_fields.join(",")}}}"
|
293
|
+
code = send_ticket(uri, req).code.to_i
|
294
|
+
break if code.between?(400, 499)
|
295
|
+
|
296
|
+
closed_count += 1
|
261
297
|
end
|
298
|
+
|
299
|
+
@metrics.closed closed_count
|
262
300
|
end
|
263
301
|
|
264
302
|
# Prepare ticket closures from the CSV of vulnerabilities exported from Nexpose.
|
@@ -274,9 +312,10 @@ class JiraHelper
|
|
274
312
|
@nxid = nil
|
275
313
|
tickets = []
|
276
314
|
CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
|
277
|
-
@nxid = @
|
315
|
+
@nxid = @mode_helper.get_nxid(nexpose_identifier_id, row)
|
278
316
|
# Query Jira for the ticket by unique id (generated NXID)
|
279
|
-
|
317
|
+
query_string = "jql=project=#{@service_data[:project]} AND description ~ \"NXID: #{@nxid}\" AND (status != #{@service_data[:close_step_name]})&fields=key"
|
318
|
+
queried_key = get_jira_key(query_string, @nxid)
|
280
319
|
if queried_key.nil? || queried_key.empty?
|
281
320
|
@log.log_message("Error when closing tickets - query for NXID <#{@nxid}> should have returned a Jira key!!")
|
282
321
|
else
|
@@ -284,6 +323,7 @@ class JiraHelper
|
|
284
323
|
tickets.push(queried_key)
|
285
324
|
end
|
286
325
|
end
|
326
|
+
|
287
327
|
tickets
|
288
328
|
end
|
289
329
|
|
@@ -300,20 +340,28 @@ class JiraHelper
|
|
300
340
|
tickets.each do |ticket_details|
|
301
341
|
headers = {'Content-Type' => 'application/json',
|
302
342
|
'Accept' => 'application/json'}
|
343
|
+
|
344
|
+
create_new_ticket = ticket_details.first.nil?
|
345
|
+
|
346
|
+
url = "#{service_data[:jira_url]}"
|
347
|
+
|
348
|
+
if create_new_ticket
|
349
|
+
req = Net::HTTP::Post.new(url, headers)
|
350
|
+
req.body = ticket_details.last
|
351
|
+
else
|
352
|
+
url += "#{ticket_details[0]}"
|
353
|
+
req = Net::HTTP::Put.new(url, headers)
|
354
|
+
req.body = {'update' => {'description' => [{'set' => "#{JSON.parse(ticket_details[1])['fields']['description']}"}]}}.to_json
|
355
|
+
end
|
303
356
|
|
304
|
-
(
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
send_whole_ticket ?
|
313
|
-
req.body = ticket_details.last :
|
314
|
-
req.body = {'update' => {'description' => [{'set' => "#{JSON.parse(ticket_details[1])['fields']['description']}"}]}}.to_json
|
315
|
-
|
316
|
-
send_ticket(uri, req)
|
357
|
+
code = send_ticket(URI.parse(url), req).code.to_i
|
358
|
+
break if code.between?(400, 499)
|
359
|
+
|
360
|
+
if create_new_ticket
|
361
|
+
@metrics.created
|
362
|
+
else
|
363
|
+
@metrics.updated
|
364
|
+
end
|
317
365
|
end
|
318
366
|
end
|
319
367
|
end
|
@@ -327,7 +375,9 @@ class JiraHelper
|
|
327
375
|
# - List of JSON-formated tickets for updating within Jira.
|
328
376
|
#
|
329
377
|
def prepare_update_tickets(vulnerability_list, nexpose_identifier_id)
|
330
|
-
|
378
|
+
@metrics.start
|
379
|
+
return unless @mode_helper.updates_supported?
|
380
|
+
|
331
381
|
@log.log_message('Preparing tickets to update.')
|
332
382
|
#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.
|
333
383
|
updated_tickets = prepare_create_tickets(vulnerability_list, nexpose_identifier_id)
|
@@ -345,7 +395,9 @@ class JiraHelper
|
|
345
395
|
@log.log_message("Failed to parse the NXID from a generated ticket update! Ignoring ticket <#{nxid}>")
|
346
396
|
next
|
347
397
|
end
|
348
|
-
|
398
|
+
|
399
|
+
query_string = "jql=project=#{@service_data[:project]} AND description ~ \"#{nxid.strip}\" AND (status != #{@service_data[:close_step_name]})&fields=key"
|
400
|
+
queried_key = get_jira_key(query_string, nxid)
|
349
401
|
ticket_key_pair = []
|
350
402
|
ticket_key_pair << queried_key
|
351
403
|
ticket_key_pair << ticket
|