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
@@ -1,441 +1,470 @@
|
|
1
|
-
module NexposeTicketing
|
2
|
-
#
|
3
|
-
# The Nexpose Ticketing service.
|
4
|
-
#
|
5
|
-
=begin
|
6
|
-
|
7
|
-
Copyright (C) 2014, Rapid7 LLC
|
8
|
-
All rights reserved.
|
9
|
-
|
10
|
-
Redistribution and use in source and binary forms, with or without modification,
|
11
|
-
are permitted provided that the following conditions are met:
|
12
|
-
|
13
|
-
* Redistributions of source code must retain the above copyright notice,
|
14
|
-
this list of conditions and the following disclaimer.
|
15
|
-
|
16
|
-
* Redistributions in binary form must reproduce the above copyright notice,
|
17
|
-
this list of conditions and the following disclaimer in the documentation
|
18
|
-
and/or other materials provided with the distribution.
|
19
|
-
|
20
|
-
* Neither the name of Rapid7 LLC nor the names of its contributors
|
21
|
-
may be used to endorse or promote products derived from this software
|
22
|
-
without specific prior written permission.
|
23
|
-
|
24
|
-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
25
|
-
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
26
|
-
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
27
|
-
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
28
|
-
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
29
|
-
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
30
|
-
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
31
|
-
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
32
|
-
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
33
|
-
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
34
|
-
|
35
|
-
=end
|
36
|
-
|
37
|
-
#
|
38
|
-
# WARNING! This code makes an SSL connection to the Nexpose server, but does NOT
|
39
|
-
# verify the certificate at this time. This can be a security issue if
|
40
|
-
# an attacker is able to man-in-the-middle the connection between the
|
41
|
-
# Metasploit console and the Nexpose server. In the common case of
|
42
|
-
# running Nexpose and Metasploit on the same host, this is a low risk.
|
43
|
-
#
|
44
|
-
|
45
|
-
#
|
46
|
-
# WARNING! This code is still rough and going through substantive changes. While
|
47
|
-
# you can build tools using this library today, keep in mind that
|
48
|
-
# method names and parameters may change in the future.
|
49
|
-
#
|
50
|
-
class TicketService
|
51
|
-
require 'csv'
|
52
|
-
require 'yaml'
|
53
|
-
require 'fileutils'
|
54
|
-
require 'nexpose_ticketing/ticket_repository'
|
55
|
-
require 'nexpose_ticketing'
|
56
|
-
require 'nexpose_ticketing/nx_logger'
|
57
|
-
require 'nexpose_ticketing/version'
|
58
|
-
|
59
|
-
TICKET_SERVICE_CONFIG_PATH = File.join(File.dirname(__FILE__), '/config/ticket_service.config')
|
60
|
-
LOGGER_FILE = File.join(File.dirname(__FILE__), '/logs/ticket_service.log')
|
61
|
-
|
62
|
-
attr_accessor :helper_data, :nexpose_data, :options, :ticket_repository, :first_time
|
63
|
-
|
64
|
-
def setup(helper_data)
|
65
|
-
# Gets the Ticket Service configuration.
|
66
|
-
service_data = begin
|
67
|
-
YAML.load_file(TICKET_SERVICE_CONFIG_PATH)
|
68
|
-
rescue ArgumentError => e
|
69
|
-
raise "Could not parse YAML #{TICKET_SERVICE_CONFIG_PATH} : #{e.message}"
|
70
|
-
end
|
71
|
-
|
72
|
-
@
|
73
|
-
@
|
74
|
-
@options
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
@
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
@
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
log_message(
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
end
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
end
|
1
|
+
module NexposeTicketing
|
2
|
+
#
|
3
|
+
# The Nexpose Ticketing service.
|
4
|
+
#
|
5
|
+
=begin
|
6
|
+
|
7
|
+
Copyright (C) 2014, Rapid7 LLC
|
8
|
+
All rights reserved.
|
9
|
+
|
10
|
+
Redistribution and use in source and binary forms, with or without modification,
|
11
|
+
are permitted provided that the following conditions are met:
|
12
|
+
|
13
|
+
* Redistributions of source code must retain the above copyright notice,
|
14
|
+
this list of conditions and the following disclaimer.
|
15
|
+
|
16
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
17
|
+
this list of conditions and the following disclaimer in the documentation
|
18
|
+
and/or other materials provided with the distribution.
|
19
|
+
|
20
|
+
* Neither the name of Rapid7 LLC nor the names of its contributors
|
21
|
+
may be used to endorse or promote products derived from this software
|
22
|
+
without specific prior written permission.
|
23
|
+
|
24
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
25
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
26
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
27
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
28
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
29
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
30
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
31
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
32
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
33
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
34
|
+
|
35
|
+
=end
|
36
|
+
|
37
|
+
#
|
38
|
+
# WARNING! This code makes an SSL connection to the Nexpose server, but does NOT
|
39
|
+
# verify the certificate at this time. This can be a security issue if
|
40
|
+
# an attacker is able to man-in-the-middle the connection between the
|
41
|
+
# Metasploit console and the Nexpose server. In the common case of
|
42
|
+
# running Nexpose and Metasploit on the same host, this is a low risk.
|
43
|
+
#
|
44
|
+
|
45
|
+
#
|
46
|
+
# WARNING! This code is still rough and going through substantive changes. While
|
47
|
+
# you can build tools using this library today, keep in mind that
|
48
|
+
# method names and parameters may change in the future.
|
49
|
+
#
|
50
|
+
class TicketService
|
51
|
+
require 'csv'
|
52
|
+
require 'yaml'
|
53
|
+
require 'fileutils'
|
54
|
+
require 'nexpose_ticketing/ticket_repository'
|
55
|
+
require 'nexpose_ticketing'
|
56
|
+
require 'nexpose_ticketing/nx_logger'
|
57
|
+
require 'nexpose_ticketing/version'
|
58
|
+
|
59
|
+
TICKET_SERVICE_CONFIG_PATH = File.join(File.dirname(__FILE__), '/config/ticket_service.config')
|
60
|
+
LOGGER_FILE = File.join(File.dirname(__FILE__), '/logs/ticket_service.log')
|
61
|
+
|
62
|
+
attr_accessor :helper_data, :nexpose_data, :options, :ticket_repository, :first_time
|
63
|
+
|
64
|
+
def setup(helper_data)
|
65
|
+
# Gets the Ticket Service configuration.
|
66
|
+
service_data = begin
|
67
|
+
YAML.load_file(TICKET_SERVICE_CONFIG_PATH)
|
68
|
+
rescue ArgumentError => e
|
69
|
+
raise "Could not parse YAML #{TICKET_SERVICE_CONFIG_PATH} : #{e.message}"
|
70
|
+
end
|
71
|
+
|
72
|
+
@helper_data = helper_data
|
73
|
+
@nexpose_data = service_data[:nexpose_data]
|
74
|
+
@options = service_data[:options]
|
75
|
+
@options[:file_name] = @options[:file_name].to_s
|
76
|
+
@options[:scan_mode] = get_scan_mode
|
77
|
+
|
78
|
+
#Temporary - this should be refactored out e.g. to include DAGs
|
79
|
+
@options[:tag_run] = @options[:scan_mode] == 'tag'
|
80
|
+
|
81
|
+
file_name = @options["#{@options[:scan_mode]}_file_name".to_sym]
|
82
|
+
@historical_file = File.join(File.dirname(__FILE__), file_name)
|
83
|
+
|
84
|
+
# Sets logging up, if enabled.
|
85
|
+
setup_logging(@options[:logging_enabled])
|
86
|
+
|
87
|
+
mode_class = load_class 'mode', @options[:ticket_mode]
|
88
|
+
@mode = mode_class.new(@options)
|
89
|
+
@options[:query_suffix] = @mode.get_query_suffix
|
90
|
+
|
91
|
+
helper_class = load_class 'helper', @helper_data[:helper_name]
|
92
|
+
@helper = helper_class.new(@helper_data, @options, @mode)
|
93
|
+
|
94
|
+
log_message("Creating ticketing repository with timeout value: #{@options[:timeout]}.")
|
95
|
+
@ticket_repository = NexposeTicketing::TicketRepository.new(options)
|
96
|
+
@ticket_repository.nexpose_login(@nexpose_data)
|
97
|
+
end
|
98
|
+
|
99
|
+
def load_class(type, name)
|
100
|
+
name.gsub!(type.capitalize, '')
|
101
|
+
path = "#{type}s/#{name}_#{type}.rb".downcase
|
102
|
+
|
103
|
+
log_message("Loading #{type} dependency: #{path}.")
|
104
|
+
begin
|
105
|
+
require_relative path
|
106
|
+
rescue => e
|
107
|
+
error = "#{type.capitalize} dependency '#{path}' could not be loaded."
|
108
|
+
@log.error e.to_s
|
109
|
+
@log.error error
|
110
|
+
fail error
|
111
|
+
end
|
112
|
+
|
113
|
+
eval("#{name}#{type.capitalize}")
|
114
|
+
end
|
115
|
+
|
116
|
+
def setup_logging(enabled = false)
|
117
|
+
helper_log = NexposeTicketing::NxLogger.instance
|
118
|
+
helper_log.setup_logging(@options[:logging_enabled],
|
119
|
+
@options[:log_level],
|
120
|
+
@options[:log_console])
|
121
|
+
|
122
|
+
return unless enabled
|
123
|
+
require 'logger'
|
124
|
+
directory = File.dirname(LOGGER_FILE)
|
125
|
+
FileUtils.mkdir_p(directory) unless File.directory?(directory)
|
126
|
+
@log = Logger.new(LOGGER_FILE, 'monthly')
|
127
|
+
@log.level = Logger::INFO
|
128
|
+
log_message('Logging enabled, starting service.')
|
129
|
+
end
|
130
|
+
|
131
|
+
# Logs a message if logging is enabled.
|
132
|
+
def log_message(message)
|
133
|
+
@log.info(message) if @options[:logging_enabled]
|
134
|
+
end
|
135
|
+
|
136
|
+
# Prepares all the local and nexpose historical data.
|
137
|
+
def prepare_historical_data(ticket_repository, options)
|
138
|
+
historical_scan_file = @historical_file
|
139
|
+
|
140
|
+
file_site_histories = nil
|
141
|
+
if File.exists?(historical_scan_file)
|
142
|
+
log_message("Reading historical CSV file: #{historical_scan_file}.")
|
143
|
+
file_site_histories = ticket_repository.read_last_scans(historical_scan_file)
|
144
|
+
end
|
145
|
+
|
146
|
+
file_site_histories
|
147
|
+
end
|
148
|
+
|
149
|
+
# Generates a full site(s) report ticket(s).
|
150
|
+
def all_site_report(ticket_repository, options)
|
151
|
+
group = "#{options[:scan_mode]}s"
|
152
|
+
|
153
|
+
log_message("Generating full vulnerability report on user entered #{group}.")
|
154
|
+
items_to_query = Array(options[group.to_sym])
|
155
|
+
log_message("Generating full vulnerability report on the following #{group}: #{items_to_query.join(', ')}")
|
156
|
+
|
157
|
+
items_to_query.each do |item|
|
158
|
+
log_message("Running full vulnerability report on item #{item}")
|
159
|
+
all_vulns_file = ticket_repository.all_new_vulns(options, item)
|
160
|
+
log_message('Preparing tickets.')
|
161
|
+
|
162
|
+
ticket_rate_limiter(all_vulns_file, 'create', item)
|
163
|
+
|
164
|
+
post_scan item
|
165
|
+
end
|
166
|
+
|
167
|
+
log_message('Finished processing all vulnerabilities.')
|
168
|
+
end
|
169
|
+
|
170
|
+
# There's possibly a new scan with new data.
|
171
|
+
def delta_site_report(ticket_repository, options, helper, scan_histories)
|
172
|
+
# Compares the scan information from file && Nexpose.
|
173
|
+
no_processing = true
|
174
|
+
@latest_scans.each do |item_id, last_scan_id|
|
175
|
+
# There's no entry in the file, so it's either a new item in Nexpose or a new item we have to monitor.
|
176
|
+
prev_scan_id = scan_histories[item_id]
|
177
|
+
|
178
|
+
if prev_scan_id.nil? || prev_scan_id == -1
|
179
|
+
options[:nexpose_item] = item_id
|
180
|
+
full_new_site_report(item_id, ticket_repository, options, helper)
|
181
|
+
options[:nexpose_item] = nil
|
182
|
+
post_scan item_id
|
183
|
+
no_processing = false
|
184
|
+
# Site has been scanned since last seen according to the file.
|
185
|
+
elsif prev_scan_id.to_s != @latest_scans[item_id].to_s
|
186
|
+
delta_new_scan(item_id, options, helper, scan_histories)
|
187
|
+
no_processing = false
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
log_name = @options["#{@options[:scan_mode]}_file_name".to_sym]
|
192
|
+
# Done processing, update the CSV to the latest scan info.
|
193
|
+
if no_processing
|
194
|
+
log_message("Nothing new to process, updating historical CSV file #{log_name}.")
|
195
|
+
else
|
196
|
+
log_message("Done processing, updating historical CSV file #{log_name}.")
|
197
|
+
end
|
198
|
+
no_processing
|
199
|
+
end
|
200
|
+
|
201
|
+
# There's a new site we haven't seen before.
|
202
|
+
def full_new_site_report(nexpose_item, ticket_repository, options, helper)
|
203
|
+
log_message("New nexpose id: #{nexpose_item} detected. Generating report.")
|
204
|
+
new_item_vuln_file = ticket_repository.all_new_vulns(options, nexpose_item)
|
205
|
+
log_message('Report generated, preparing tickets.')
|
206
|
+
|
207
|
+
nexpose_id = format_id(nexpose_item)
|
208
|
+
ticket_rate_limiter(new_item_vuln_file, 'create', nexpose_id)
|
209
|
+
end
|
210
|
+
|
211
|
+
def ticket_rate_limiter_processor(ticket_batch, ticket_method, nexpose_item)
|
212
|
+
#Just the header (no tickets).
|
213
|
+
if ticket_batch.size == 1
|
214
|
+
log_message('Received empty batch. Not sending tickets.')
|
215
|
+
return
|
216
|
+
end
|
217
|
+
|
218
|
+
nexpose_item = format_id(nexpose_item)
|
219
|
+
|
220
|
+
# Prep the batch of tickets
|
221
|
+
log_message("Preparing to #{ticket_method} tickets.")
|
222
|
+
tickets = @helper.send("prepare_#{ticket_method}_tickets",
|
223
|
+
ticket_batch.join(''),
|
224
|
+
nexpose_item)
|
225
|
+
log_message("Parsed rows: #{ticket_batch.size}")
|
226
|
+
|
227
|
+
# Send them off
|
228
|
+
log_message('Sending tickets.')
|
229
|
+
@helper.send("#{ticket_method}_tickets", tickets)
|
230
|
+
log_message('Returning for next batch.')
|
231
|
+
end
|
232
|
+
|
233
|
+
def ticket_rate_limiter(query_results_file, ticket_method, nexpose_item)
|
234
|
+
batch_size_max = @options[:batch_size]
|
235
|
+
log_message("Batching tickets in sizes: #{@options[:batch_size]}")
|
236
|
+
matching_fields = @mode.get_matching_fields
|
237
|
+
current_ids = Hash[*matching_fields.collect { |k| [k, nil] }.flatten]
|
238
|
+
|
239
|
+
# Start the batching
|
240
|
+
query_results_file.rewind
|
241
|
+
csv_header = query_results_file.readline
|
242
|
+
ticket_batch = []
|
243
|
+
|
244
|
+
begin
|
245
|
+
CSV.foreach(query_results_file, headers: csv_header) do |row|
|
246
|
+
ticket_batch << row
|
247
|
+
|
248
|
+
# Store the current
|
249
|
+
if ticket_batch.size < batch_size_max
|
250
|
+
matching_fields.each { |id| current_ids[id] = row[id] }
|
251
|
+
next
|
252
|
+
end
|
253
|
+
|
254
|
+
# Ensure that all rows associated with a ticket are captured.
|
255
|
+
# This potentially ignores the batch limit.
|
256
|
+
next if current_ids.all? { |id, val| val == row[id] }
|
257
|
+
|
258
|
+
#Last row is mismatch/independent
|
259
|
+
leftover_row = ticket_batch.pop
|
260
|
+
|
261
|
+
log_message('Batch size reached. Sending tickets.')
|
262
|
+
ticket_rate_limiter_processor(ticket_batch, ticket_method, nexpose_item)
|
263
|
+
|
264
|
+
ticket_batch.clear
|
265
|
+
ticket_batch << csv_header
|
266
|
+
ticket_batch << leftover_row
|
267
|
+
current_ids.each { |k,v| k = nil }
|
268
|
+
end
|
269
|
+
ensure
|
270
|
+
log_message('Finished reading report. Sending any remaining tickets and cleaning up file system.')
|
271
|
+
|
272
|
+
ticket_rate_limiter_processor(ticket_batch, ticket_method, nexpose_item)
|
273
|
+
|
274
|
+
query_results_file.close
|
275
|
+
query_results_file.unlink
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def get_scan_mode
|
280
|
+
return 'tag' unless @options[:tags].nil? || @options[:tags].empty?
|
281
|
+
return 'site'
|
282
|
+
end
|
283
|
+
|
284
|
+
# Starts the Ticketing Service.
|
285
|
+
def start
|
286
|
+
# Checks if the csv historical file already exists and reads it, otherwise create it and assume first time run.
|
287
|
+
scan_histories = prepare_historical_data(@ticket_repository, @options)
|
288
|
+
|
289
|
+
# If we didn't specify a site || first time run (no scan history), then it gets all the vulnerabilities.
|
290
|
+
@options[:initial_run] = full_scan_required?(scan_histories)
|
291
|
+
|
292
|
+
if @options[:initial_run]
|
293
|
+
full_scan
|
294
|
+
else
|
295
|
+
delta_scan(scan_histories)
|
296
|
+
end
|
297
|
+
|
298
|
+
@helper.finish
|
299
|
+
log_message('Exiting ticket service.')
|
300
|
+
end
|
301
|
+
|
302
|
+
def full_scan
|
303
|
+
log_message('Storing current scan state before obtaining all vulnerabilities.')
|
304
|
+
current_scan_state = ticket_repository.load_last_scans(@options)
|
305
|
+
|
306
|
+
all_site_report(@ticket_repository, @options)
|
307
|
+
|
308
|
+
#Generate historical CSV file after completing the fist query.
|
309
|
+
log_message('No historical CSV file found. Generating.')
|
310
|
+
@ticket_repository.save_to_file(@historical_file, current_scan_state)
|
311
|
+
log_message('Historical CSV file generated.')
|
312
|
+
end
|
313
|
+
|
314
|
+
def delta_scan(scan_histories)
|
315
|
+
log_message('Obtaining last scan information.')
|
316
|
+
@latest_scans = @ticket_repository.last_scans(@options)
|
317
|
+
|
318
|
+
# Scan states can change during our processing. Store the state we are
|
319
|
+
# about to process and move this to the historical file if we
|
320
|
+
# successfully process.
|
321
|
+
log_message('Calculated deltas, storing current scan state.')
|
322
|
+
current_scan_state = ticket_repository.load_last_scans(@options)
|
323
|
+
|
324
|
+
# Only run if a scan has been ran ever in Nexpose.
|
325
|
+
return if @latest_scans.empty?
|
326
|
+
|
327
|
+
delta_site_report(@ticket_repository, @options, @helper, scan_histories)
|
328
|
+
# Processing completed successfully. Update historical scan file.
|
329
|
+
@ticket_repository.save_to_file(@historical_file, current_scan_state)
|
330
|
+
end
|
331
|
+
|
332
|
+
# Performs a delta scan
|
333
|
+
def delta_new_scan(item_id, options, helper, scan_histories)
|
334
|
+
delta_func = "delta_#{options[:scan_mode]}_new_scan"
|
335
|
+
self.send(delta_func, item_id, options, helper, scan_histories)
|
336
|
+
end
|
337
|
+
|
338
|
+
# There's a new scan with possibly new vulnerabilities.
|
339
|
+
def delta_site_new_scan(nexpose_item, options, helper, file_site_histories, tag_id=nil)
|
340
|
+
log_message("New scan detected for nexpose id: #{nexpose_item}. Generating report.")
|
341
|
+
|
342
|
+
format_method = "format_#{options[:scan_mode]}_id"
|
343
|
+
nexpose_id = self.send(format_method, tag_id || nexpose_item)
|
344
|
+
|
345
|
+
scan_options = { scan_id: file_site_histories[nexpose_item],
|
346
|
+
nexpose_item: nexpose_item,
|
347
|
+
severity: options[:severity],
|
348
|
+
ticket_mode: options[:ticket_mode],
|
349
|
+
riskScore: options[:riskScore],
|
350
|
+
vulnerabilityCategories: options[:vulnerabilityCategories],
|
351
|
+
tag_run: options[:tag_run],
|
352
|
+
tag: tag_id }
|
353
|
+
|
354
|
+
log_message("Scan id for new scan: #{file_site_histories[nexpose_item]}.")
|
355
|
+
|
356
|
+
if @mode.updates_supported?
|
357
|
+
helper_method = 'update'
|
358
|
+
query = 'all_vulns_since_scan'
|
359
|
+
else
|
360
|
+
helper_method = 'create'
|
361
|
+
query = 'new_vulns_since_scan'
|
362
|
+
end
|
363
|
+
|
364
|
+
all_scan_vuln_file = ticket_repository.send(query, scan_options)
|
365
|
+
ticket_rate_limiter(all_scan_vuln_file, helper_method, nexpose_id)
|
366
|
+
|
367
|
+
return unless options[:close_old_tickets_on_update] == 'Y'
|
368
|
+
|
369
|
+
tickets_to_close_file = ticket_repository.old_tickets(scan_options)
|
370
|
+
ticket_rate_limiter(tickets_to_close_file, 'close', nexpose_id)
|
371
|
+
end
|
372
|
+
|
373
|
+
def delta_tag_new_scan(nexpose_item, options, helper, file_site_histories, tag_id=nil)
|
374
|
+
# It's a tag run and something has changed (new/removed asset or new scan ID for an asset). To find out what, we must compare
|
375
|
+
# All tag assets and their scan IDs. Firstly we fetch all the assets in the tags
|
376
|
+
# in the configuration file and store them temporarily
|
377
|
+
item_id = nexpose_item
|
378
|
+
tag_assets_tmp_file = File.join(File.dirname(__FILE__), "/tag_assets/#{options[:tag_file_name]}_#{item_id}.tmp")
|
379
|
+
tag_assets_historic_file = File.join(File.dirname(__FILE__), "/tag_assets/#{options[:tag_file_name]}_#{item_id}.csv")
|
380
|
+
ticket_repository.generate_tag_asset_list(tags: item_id,
|
381
|
+
csv_file: tag_assets_tmp_file)
|
382
|
+
new_tag_configuration = ticket_repository.read_tag_asset_list(tag_assets_tmp_file)
|
383
|
+
historic_tag_config = ticket_repository.read_tag_asset_list(tag_assets_historic_file)
|
384
|
+
#Compare the assets within the tags and their scan histories to find the ones we need to query
|
385
|
+
changed_assets = Hash[*(historic_tag_config.to_a - new_tag_configuration.to_a).flatten]
|
386
|
+
new_assets = Hash[*(new_tag_configuration.to_a - historic_tag_config.to_a).flatten]
|
387
|
+
new_assets.delete_if {|asset_id, scan_id| historic_tag_config.has_key?(asset_id.to_s)}
|
388
|
+
|
389
|
+
#all_assets_changed = new_assets.merge(changed_assets)
|
390
|
+
changed_assets.each do |asset_id, scan_id|
|
391
|
+
delta_site_new_scan(asset_id, options,
|
392
|
+
helper, changed_assets, item_id)
|
393
|
+
end
|
394
|
+
|
395
|
+
new_assets.each do |asset_id, scan_id|
|
396
|
+
#Since no previous scan IDs - we generate a full report.
|
397
|
+
options[:nexpose_item] = asset_id
|
398
|
+
full_new_site_report(item_id, ticket_repository, options, helper)
|
399
|
+
options.delete(:nexpose_item)
|
400
|
+
end
|
401
|
+
|
402
|
+
#Update the historic file
|
403
|
+
new_tag_asset_list = historic_tag_config.merge(new_tag_configuration)
|
404
|
+
trimmed_csv = []
|
405
|
+
trimmed_csv << 'asset_id, last_scan_id'
|
406
|
+
new_tag_asset_list.each do |asset_id, last_scan_id|
|
407
|
+
trimmed_csv << "#{asset_id},#{last_scan_id}"
|
408
|
+
end
|
409
|
+
ticket_repository.save_to_file(tag_assets_historic_file, trimmed_csv)
|
410
|
+
File.delete(tag_assets_tmp_file)
|
411
|
+
end
|
412
|
+
|
413
|
+
# Methods to run after a scan
|
414
|
+
def post_scan(item_id)
|
415
|
+
self.send("post_#{@options[:scan_mode]}_scan", item_id)
|
416
|
+
end
|
417
|
+
|
418
|
+
def post_site_scan(item_id)
|
419
|
+
end
|
420
|
+
|
421
|
+
def post_tag_scan(item_id)
|
422
|
+
tag_assets_historic_file = File.join(File.dirname(__FILE__),
|
423
|
+
'tag_assets',
|
424
|
+
"#{options[:tag_file_name]}_#{item_id}.csv")
|
425
|
+
|
426
|
+
ticket_repository.generate_tag_asset_list(tags: item_id,
|
427
|
+
csv_file: tag_assets_historic_file)
|
428
|
+
end
|
429
|
+
|
430
|
+
# Formats the Nexpose item ID according to the asset grouping mode
|
431
|
+
def format_id(item_id)
|
432
|
+
self.send("format_#{options[:scan_mode]}_id", item_id)
|
433
|
+
end
|
434
|
+
|
435
|
+
def format_site_id(item_id)
|
436
|
+
item_id
|
437
|
+
end
|
438
|
+
|
439
|
+
def format_tag_id(item_id)
|
440
|
+
"T#{item_id}"
|
441
|
+
end
|
442
|
+
|
443
|
+
# Determines whether all assets must be scanned
|
444
|
+
def full_scan_required?(histories)
|
445
|
+
self.send("full_#{@options[:scan_mode]}_scan_required?", histories)
|
446
|
+
end
|
447
|
+
|
448
|
+
def full_site_scan_required?(scan_histories)
|
449
|
+
is_full_run = false
|
450
|
+
|
451
|
+
if @options[:sites].nil? || @options[:sites].empty?
|
452
|
+
is_full_run = true
|
453
|
+
|
454
|
+
all_site_details = @ticket_repository.all_site_details
|
455
|
+
@options[:sites] = all_site_details.map { |s| s.id.to_s }
|
456
|
+
|
457
|
+
log_message("List of sites is now <#{@options[:sites]}>")
|
458
|
+
end
|
459
|
+
|
460
|
+
is_full_run || scan_histories.nil?
|
461
|
+
end
|
462
|
+
|
463
|
+
def full_tag_scan_required?(scan_histories)
|
464
|
+
if @options[:tags].nil? || @options[:tags].empty?
|
465
|
+
fail 'No tags specified within the configuration.'
|
466
|
+
end
|
467
|
+
return scan_histories.nil?
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|