nexpose_ticketing 1.0.2 → 1.2.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 +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
|