nexpose_servicenow 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/nexpose_servicenow.rb +7 -2
- data/lib/nexpose_servicenow/csv_compare.rb +140 -0
- data/lib/nexpose_servicenow/historical_data.rb +27 -7
- data/lib/nexpose_servicenow/nexpose_helper.rb +9 -1
- data/lib/nexpose_servicenow/queries.rb +153 -56
- data/lib/nexpose_servicenow/version.rb +1 -1
- data/nexpose_servicenow.gemspec +3 -2
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 293f877592c7698b0730bed8471ac8d9557009b3
|
4
|
+
data.tar.gz: 56922788a7b2b7a399b9146c8075f2cea9d013ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2d0cf6ebf473a19eb15497b70344c0f332cfcffda10c09cbb0d07c119e057cf4633aac4dc6ba77849c400d31129e3ca325157e66bf207a427309c2a8fd28b4f3
|
7
|
+
data.tar.gz: 55a7cd2740faf850e6534e0aa4f240ef2dff326ec968cafd458b0920d1b336701e5d4fdf150ed0d89cfe7467a3bb23aa7f5f63c254cd5c98501b6c2db3a56858
|
data/lib/nexpose_servicenow.rb
CHANGED
@@ -84,8 +84,8 @@ module NexposeServiceNow
|
|
84
84
|
# Create a report if explicitly required or else an existing
|
85
85
|
# report file isn't found
|
86
86
|
def self.create_report(report_details, options)
|
87
|
-
if options[:mode].start_with?
|
88
|
-
|
87
|
+
if %w(update_ remove_).any? { |m| options[:mode].start_with? m} or
|
88
|
+
(options[:mode] == 'latest_scans' && options[:id_type] != :site)
|
89
89
|
return
|
90
90
|
end
|
91
91
|
|
@@ -199,5 +199,10 @@ module NexposeServiceNow
|
|
199
199
|
historical_data.set_last_vuln(options[:last_scan_data],
|
200
200
|
options[:nexpose_ids])
|
201
201
|
end
|
202
|
+
|
203
|
+
def self.remove_diff_comparison_mode(report_details, options)
|
204
|
+
historical_data = get_historical_data(options)
|
205
|
+
historical_data.remove_last_diff_comparison_data options[:output_dir]
|
206
|
+
end
|
202
207
|
end
|
203
208
|
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'csv-diff'
|
4
|
+
require 'csv'
|
5
|
+
|
6
|
+
module NexposeServiceNow
|
7
|
+
class CsvCompare
|
8
|
+
def self.get_columns(csv_file)
|
9
|
+
columns = ''
|
10
|
+
File::open(csv_file,'r') do |f|
|
11
|
+
columns = f.readline.rstrip
|
12
|
+
end
|
13
|
+
|
14
|
+
columns.split(',')
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.get_row(value, columns, status)
|
18
|
+
columns.map { |c| value.fields[c] }.push(status)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.get_delete(value, columns)
|
22
|
+
self.get_row(value, columns, 'old')
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.get_add(value, columns)
|
26
|
+
self.get_row(value, columns, 'new')
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.update_to_old(value, columns)
|
30
|
+
self.update_to_row(value, columns, 0, 'old')
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.update_to_new(value, columns)
|
34
|
+
self.update_to_row(value, columns, 1, 'new')
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.update_to_row(value, columns, index, status)
|
38
|
+
row = []
|
39
|
+
columns.each do |c|
|
40
|
+
val = value.fields[c]
|
41
|
+
row << (val.kind_of?(Array) ? val[index] : val)
|
42
|
+
end
|
43
|
+
row.push(status)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.append_to_filename(current_filename, string_to_append)
|
47
|
+
extension = File.extname current_filename
|
48
|
+
name = File.basename current_filename, extension
|
49
|
+
path = File.dirname current_filename
|
50
|
+
|
51
|
+
"#{path}/#{name}-#{string_to_append}#{extension}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.update_report_with_diff(report_file, key_fields=[0])
|
55
|
+
old_filename = self.append_to_filename(report_file, 'old')
|
56
|
+
new_filename = self.append_to_filename(report_file, 'new')
|
57
|
+
|
58
|
+
# Report is 'new' file for purpose of diff
|
59
|
+
FileUtils.mv(report_file, new_filename)
|
60
|
+
|
61
|
+
# If the old file doesn't exist, we can just add the status column
|
62
|
+
if File.exists?(old_filename)
|
63
|
+
self.create_csv_diff(old_filename,
|
64
|
+
new_filename,
|
65
|
+
report_file,
|
66
|
+
key_fields)
|
67
|
+
else
|
68
|
+
self.overwrite_existing_report(new_filename, report_file)
|
69
|
+
end
|
70
|
+
|
71
|
+
# 'new' file becomes the basis of comparison next time
|
72
|
+
FileUtils.mv(new_filename, old_filename, :force => true)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Instead of diffing, append 'status' column to an existing file
|
76
|
+
def self.overwrite_existing_report(source_file, target_file)
|
77
|
+
temp = Tempfile.new("#{File.basename source_file}tmp")
|
78
|
+
|
79
|
+
#TODO: Don't do the column_written check every time
|
80
|
+
|
81
|
+
CSV.open(temp, 'w') do |temp_csv|
|
82
|
+
column_written = false
|
83
|
+
new_column_value = ['status']
|
84
|
+
CSV.foreach(source_file) do |orig|
|
85
|
+
temp_csv << (orig + new_column_value)
|
86
|
+
|
87
|
+
unless column_written
|
88
|
+
new_column_value = ['new']
|
89
|
+
column_written = true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
FileUtils.mv(temp, target_file, :force => true)
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.create_csv_diff(old_file, new_file, target_file, key_fields=[0])
|
98
|
+
diff = CSVDiff.new(old_file,
|
99
|
+
new_file,
|
100
|
+
ignore_moves: true,
|
101
|
+
key_fields: key_fields
|
102
|
+
)
|
103
|
+
|
104
|
+
columns = self.get_columns(new_file)
|
105
|
+
|
106
|
+
CSV.open(target_file, 'wb') do |csv|
|
107
|
+
csv << (columns+['status'])
|
108
|
+
|
109
|
+
diff.deletes.each_value { |v| csv << get_delete(v, columns) }
|
110
|
+
diff.adds.each_value { |v| csv << get_add(v, columns) }
|
111
|
+
|
112
|
+
if(key_fields.count == 1)
|
113
|
+
# If only a single key field, we don't need the old values
|
114
|
+
# Just grab the row and let ServiceNow coalesce and update the row
|
115
|
+
update_rows = diff.updates.each_value.map { |u| u.row }
|
116
|
+
update_rows = update_rows.map(&:to_i).sort
|
117
|
+
update_row_num = update_rows.shift
|
118
|
+
current_row_num = 0
|
119
|
+
|
120
|
+
CSV.foreach(new_file, headers: true) do |new_csv|
|
121
|
+
current_row_num = current_row_num + 1
|
122
|
+
next unless current_row_num == update_row_num
|
123
|
+
|
124
|
+
csv << new_csv.push('new')
|
125
|
+
update_row_num = update_rows.shift
|
126
|
+
break if update_row_num == nil
|
127
|
+
end
|
128
|
+
elsif(key_fields.count == 2)
|
129
|
+
# Multiple key fields result in row "updates"
|
130
|
+
diff.updates.each_value do |v|
|
131
|
+
csv << self.update_to_old(v, columns)
|
132
|
+
csv << self.update_to_new(v, columns)
|
133
|
+
end
|
134
|
+
else
|
135
|
+
raise "Received #{key_fields.count} key fields. Only 1/2 supported."
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -12,6 +12,9 @@ module NexposeServiceNow
|
|
12
12
|
DAG_TIMESTAMP_FILE = 'last_scan_data_dag.csv'
|
13
13
|
NEW_DAG_TIMESTAMP_FILE = 'new_dag_timestamp.csv'
|
14
14
|
|
15
|
+
ASSET_GROUP_FILE = 'Nexpose-ServiceNow-asset_groups-old.csv'
|
16
|
+
DIFFERENTIAL_FILE_REGEX = 'Nexpose-ServiceNow-*-old.csv'
|
17
|
+
|
15
18
|
SITE_IDENTIFIER = 'site_id'
|
16
19
|
SITE_DELTA_VALUE = 'last_scan_id'
|
17
20
|
SITE_BASE_VALUE = 0
|
@@ -358,18 +361,15 @@ module NexposeServiceNow
|
|
358
361
|
log_and_print 'Last vuln data updated.'
|
359
362
|
end
|
360
363
|
|
361
|
-
def remove_local_file(filename)
|
364
|
+
def remove_local_file(filename, action)
|
362
365
|
unless File.exist? filename
|
363
366
|
log_and_error 'Can\'t remove file.'
|
364
367
|
log_and_error "File #{filename} cannot be located."
|
365
368
|
exit -1
|
366
369
|
end
|
367
370
|
|
368
|
-
new_name = "#{filename}.#{Time.new.strftime('%Y-%m-%d.%H:%M:%S')}"
|
369
371
|
begin
|
370
|
-
#
|
371
|
-
File.delete new_name if File.exist? new_name
|
372
|
-
File.rename(filename, new_name)
|
372
|
+
self.send("by_#{action}", filename)
|
373
373
|
rescue Exception => e
|
374
374
|
log_and_error "Error removing file:\n#{e}"
|
375
375
|
exit -1
|
@@ -378,13 +378,33 @@ module NexposeServiceNow
|
|
378
378
|
log_and_print "File #{filename} removed"
|
379
379
|
end
|
380
380
|
|
381
|
+
def self.by_rename(filename)
|
382
|
+
new_name = "#{filename}.#{Time.new.strftime('%Y-%m-%d.%H:%M:%S')}"
|
383
|
+
File.delete new_name if File.exist? new_name
|
384
|
+
File.rename(filename, new_name)
|
385
|
+
end
|
386
|
+
|
387
|
+
def self.by_delete(filename)
|
388
|
+
File.delete filename
|
389
|
+
end
|
390
|
+
|
381
391
|
def remove_last_scan_data
|
382
|
-
remove_local_file @local_file
|
392
|
+
remove_local_file @local_file, 'rename'
|
393
|
+
remove_local_file @remote_file, 'delete'
|
383
394
|
end
|
384
395
|
|
385
396
|
def remove_last_vuln_data
|
386
|
-
remove_local_file @timestamp_file
|
397
|
+
remove_local_file @timestamp_file, 'rename'
|
398
|
+
remove_local_file @prev_timestamp_file, 'delete'
|
387
399
|
end
|
388
400
|
|
401
|
+
def remove_last_diff_comparison_data(output_dir)
|
402
|
+
local_path = File.expand_path(output_dir)
|
403
|
+
remove_local_file (local_path + ASSET_GROUP_FILE), 'rename'
|
404
|
+
|
405
|
+
Dir[local_path + DIFFERENTIAL_FILE_REGEX].each do |file|
|
406
|
+
remove_local_file file, 'delete'
|
407
|
+
end
|
408
|
+
end
|
389
409
|
end
|
390
410
|
end
|
@@ -2,6 +2,7 @@ require 'nexpose'
|
|
2
2
|
require 'fileutils'
|
3
3
|
require_relative './queries'
|
4
4
|
require_relative './nx_logger'
|
5
|
+
require_relative './csv_compare'
|
5
6
|
|
6
7
|
module NexposeServiceNow
|
7
8
|
class NexposeHelper
|
@@ -77,7 +78,14 @@ module NexposeServiceNow
|
|
77
78
|
report_id = generate_config(query, report_name, [id], id_type, min_cvss)
|
78
79
|
|
79
80
|
run_report(report_id)
|
80
|
-
|
81
|
+
local_report_name = save_report(report_name, report_id, output_dir)
|
82
|
+
reports << local_report_name
|
83
|
+
|
84
|
+
if Queries.csv_diff_required?(query_name)
|
85
|
+
@log.log_message "Calculating diff for #{local_report_name}..."
|
86
|
+
CsvCompare.update_report_with_diff(local_report_name,
|
87
|
+
Queries.query_keys(query_name))
|
88
|
+
end
|
81
89
|
end
|
82
90
|
|
83
91
|
reports
|
@@ -14,7 +14,7 @@ module NexposeServiceNow
|
|
14
14
|
pci_status,
|
15
15
|
pci_adjusted_cvss_score as PCI_Severity,
|
16
16
|
title as Summary,
|
17
|
-
description as Threat,
|
17
|
+
proofAsText(description) as Threat,
|
18
18
|
ROUND(riskscore::numeric, 2) as Riskscore,
|
19
19
|
cvss_vector,
|
20
20
|
ROUND(cvss_impact_score::numeric, 2) as Impact_Score,
|
@@ -36,7 +36,7 @@ module NexposeServiceNow
|
|
36
36
|
THEN 1
|
37
37
|
ELSE 0
|
38
38
|
END AS bit) as Malware_Kits,
|
39
|
-
sol.solutions as Solution
|
39
|
+
array_to_string(sol.solutions, ',', '') as Solution
|
40
40
|
|
41
41
|
FROM
|
42
42
|
dim_vulnerability
|
@@ -68,14 +68,8 @@ module NexposeServiceNow
|
|
68
68
|
GROUP BY dvr.vulnerability_id) ref USING (vulnerability_id)
|
69
69
|
|
70
70
|
LEFT OUTER JOIN(SELECT vulnerability_id,
|
71
|
-
|
72
|
-
|
73
|
-
'URL: ' || url,
|
74
|
-
'Estimate: ' || estimate,
|
75
|
-
'Applies To: ' || applies_to,
|
76
|
-
'Additional Data: ' || additional_data), '\n') as solutions
|
77
|
-
FROM dim_solution
|
78
|
-
JOIN dim_vulnerability_solution USING (solution_id)
|
71
|
+
array_agg(solution_id) as solutions
|
72
|
+
FROM dim_vulnerability_solution
|
79
73
|
GROUP BY vulnerability_id) sol USING (vulnerability_id)
|
80
74
|
WHERE date_modified >= '#{options[:vuln_query_date]}'"
|
81
75
|
end
|
@@ -113,10 +107,12 @@ module NexposeServiceNow
|
|
113
107
|
dim_operating_system.description as Operating_System,
|
114
108
|
fact_asset.scan_finished as Most_Recent_Discovery,
|
115
109
|
dim_asset.asset_id as Nexpose_ID,
|
116
|
-
fact_asset.pci_status
|
110
|
+
fact_asset.pci_status,
|
111
|
+
concat(fact_asset.aggregated_credential_status_id, ' - ', aggregated_credential_status_description) as Credential_Status
|
117
112
|
|
118
113
|
FROM dim_asset
|
119
114
|
JOIN fact_asset USING (asset_id)
|
115
|
+
JOIN dim_aggregated_credential_status USING (aggregated_credential_status_id)
|
120
116
|
LEFT OUTER JOIN dim_operating_system on dim_asset.operating_system_id = dim_operating_system.operating_system_id
|
121
117
|
LEFT OUTER JOIN dim_host_type USING (host_type_id)"
|
122
118
|
end
|
@@ -129,16 +125,44 @@ module NexposeServiceNow
|
|
129
125
|
WHERE scan_id = lastScan(asset_id)"
|
130
126
|
end
|
131
127
|
|
128
|
+
def self.service_definition(options={})
|
129
|
+
"SELECT DISTINCT on(dsf.name, ds.name, dp.name, port)
|
130
|
+
dsf.name, ds.name as service_name, dp.name as protocol, port
|
131
|
+
|
132
|
+
FROM (SELECT service_id, protocol_id, port, service_fingerprint_id
|
133
|
+
FROM fact_asset_scan_service
|
134
|
+
GROUP BY asset_id, service_id, protocol_id, port, service_fingerprint_id
|
135
|
+
HAVING min(scan_id) > #{options[:delta]} and max(scan_id) = lastScan(asset_id)) fass
|
136
|
+
JOIN dim_service ds USING (service_id)
|
137
|
+
JOIN dim_protocol dp USING (protocol_id)
|
138
|
+
JOIN dim_service_fingerprint dsf USING (service_fingerprint_id)"
|
139
|
+
end
|
140
|
+
|
141
|
+
# When a service only appears in either the old or latest import,
|
142
|
+
# then it needs deleted or inserted, respectively.
|
132
143
|
def self.service_instance(options={})
|
133
|
-
|
144
|
+
"SELECT asset_id, dsf.name as name,
|
145
|
+
ds.name as service_name,
|
146
|
+
dp.name as protocol, port,
|
147
|
+
CASE
|
148
|
+
WHEN scan_id = #{options[:delta]}
|
149
|
+
THEN 'old'
|
150
|
+
WHEN scan_id = lastScan(asset_id)
|
151
|
+
THEN 'new'
|
152
|
+
ELSE 'current'
|
153
|
+
END as status
|
154
|
+
|
155
|
+
FROM (SELECT asset_id, service_id, protocol_id, port, min(scan_id) as scan_id, service_fingerprint_id
|
134
156
|
FROM fact_asset_scan_service
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
157
|
+
WHERE scan_id = lastScan(asset_id) OR scan_id = #{options[:delta]}
|
158
|
+
GROUP BY asset_id, service_id, protocol_id, port, service_fingerprint_id
|
159
|
+
HAVING min(scan_id) = max(scan_id)) fass
|
160
|
+
JOIN dim_service ds USING (service_id)
|
161
|
+
JOIN dim_protocol dp USING (protocol_id)
|
162
|
+
JOIN dim_service_fingerprint dsf USING (service_fingerprint_id)
|
163
|
+
GROUP BY asset_id, dsf.name, ds.name, dp.name, port, scan_id"
|
139
164
|
end
|
140
165
|
|
141
|
-
|
142
166
|
# Need to wipe table each time
|
143
167
|
def self.group_accounts(options={})
|
144
168
|
'SELECT asset_id as Nexpose_ID, daga.name as Group_Account_Name
|
@@ -155,12 +179,13 @@ module NexposeServiceNow
|
|
155
179
|
JOIN dim_asset_user_account daua USING (asset_id)'
|
156
180
|
end
|
157
181
|
|
158
|
-
# Need to wipe table each time
|
159
182
|
def self.asset_groups(options={})
|
160
|
-
'SELECT
|
161
|
-
|
162
|
-
|
163
|
-
|
183
|
+
'SELECT asset_group_id, name as asset_group_name, description, dynamic_membership
|
184
|
+
FROM dim_asset_group'
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.asset_group_memberships(options={})
|
188
|
+
'select * from dim_asset_group_asset'
|
164
189
|
end
|
165
190
|
|
166
191
|
# Need to wipe table each time
|
@@ -243,8 +268,6 @@ module NexposeServiceNow
|
|
243
268
|
end
|
244
269
|
|
245
270
|
def self.vulnerable_new_items(options={})
|
246
|
-
# self.send("vulnerable_new_items_per_#{options[:id_type]}", options)
|
247
|
-
|
248
271
|
standard_filter = if options[:id_type] == 'site'
|
249
272
|
"MIN(fasv.scan_id) > #{options[:delta]}"
|
250
273
|
else
|
@@ -256,40 +279,51 @@ module NexposeServiceNow
|
|
256
279
|
cvss_filter = self.generate_cvss_filter(options[:filters][:cvss])
|
257
280
|
|
258
281
|
"SELECT
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
282
|
+
CAST(subq.asset_id as text) Configuration_Item,
|
283
|
+
TRUE as Active,
|
284
|
+
concat('R7_', subq.vulnerability_id) as Vulnerability,
|
285
|
+
fasva.first_discovered as First_Found,
|
286
|
+
fasva.most_recently_discovered as Last_Found,
|
287
|
+
subq.vulnerability_instances as Times_Found,
|
288
|
+
subq.ip_address as IP_Address,
|
289
|
+
coalesce(NULLIF(CONCAT('\"', favi.proof ,'\"'), '\"\"'), 'None') as Proof,
|
290
|
+
favi.stat_desc as Status,
|
291
|
+
array_to_string(favi.ports, ' ', '') as Ports,
|
292
|
+
coalesce(NULLIF(concat(favi.pro_desc, ' (', favi.name, ')') ,'N/A ()'), 'None') as Protocol,
|
293
|
+
array_to_string(favi.solution_ids, ',', '') as Solutions
|
294
|
+
FROM (
|
270
295
|
SELECT fasv.asset_id, fasv.vulnerability_id, vulnerability_instances,
|
271
296
|
MIN(fasv.scan_id) as first_found, MAX(fasv.scan_id) as latest_found,
|
272
297
|
s.current_scan, s.host_name, s.ip_address
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
JOIN (
|
291
|
-
|
292
|
-
|
298
|
+
FROM fact_asset_scan_vulnerability_finding fasv
|
299
|
+
|
300
|
+
#{cve_filter}
|
301
|
+
#{cvss_filter}
|
302
|
+
|
303
|
+
JOIN (SELECT asset_id, host_name, ip_address, lastScan(asset_id) AS current_scan
|
304
|
+
FROM dim_asset
|
305
|
+
) s ON s.asset_id = fasv.asset_id
|
306
|
+
GROUP BY fasv.asset_id, fasv.vulnerability_id, s.current_scan, s.host_name, s.ip_address, vulnerability_instances
|
307
|
+
HAVING MAX(fasv.scan_id)=current_scan AND #{standard_filter}
|
308
|
+
) subq
|
309
|
+
|
310
|
+
JOIN (SELECT asset_id, vulnerability_id,
|
311
|
+
first_discovered, most_recently_discovered
|
312
|
+
FROM fact_asset_vulnerability_age
|
313
|
+
#{date_filter}) fasva USING (asset_id, vulnerability_id)
|
314
|
+
|
315
|
+
JOIN (SELECT DISTINCT on(asset_id, vulnerability_id, scan_id)
|
316
|
+
asset_id, scan_id, vulnerability_id,
|
317
|
+
proofAsText(proof) as proof, vs.description as stat_desc,
|
318
|
+
array_agg(DISTINCT port) as ports, dp.name,
|
319
|
+
dp.description as pro_desc,
|
320
|
+
array_agg(DISTINCT dvs.solution_id) as solution_ids
|
321
|
+
FROM fact_asset_vulnerability_instance
|
322
|
+
INNER JOIN dim_protocol dp USING (protocol_id)
|
323
|
+
INNER JOIN dim_vulnerability_status vs using (status_id)
|
324
|
+
LEFT JOIN dim_asset_vulnerability_solution dvs USING (asset_id, vulnerability_id)
|
325
|
+
GROUP BY asset_id, scan_id, vulnerability_id, proof, stat_desc, port, dp.name, pro_desc
|
326
|
+
) favi ON favi.asset_id = subq.asset_id AND favi.scan_id = subq.current_scan AND favi.vulnerability_id = subq.vulnerability_id
|
293
327
|
ORDER BY fasva.asset_id, vulnerability"
|
294
328
|
end
|
295
329
|
|
@@ -342,6 +376,53 @@ module NexposeServiceNow
|
|
342
376
|
|
343
377
|
end
|
344
378
|
|
379
|
+
def self.vulnerability_solutions(options={})
|
380
|
+
"SELECT DISTINCT (solution_id)
|
381
|
+
solution_id,
|
382
|
+
nexpose_id,
|
383
|
+
coalesce(NULLIF(CONCAT('\"', proofAsText(fix),'\"'), '\"\"'), 'None') as fix,
|
384
|
+
estimate,
|
385
|
+
summary,
|
386
|
+
solution_type,
|
387
|
+
applies_to,
|
388
|
+
url,
|
389
|
+
coalesce(NULLIF(CONCAT('\"',proofAsText(additional_data),'\"'), '\"\"'), 'None') as additional_data,
|
390
|
+
array_to_string(req_solutions, ',', '') as required_solutions,
|
391
|
+
array_to_string(super_solutions, ',', '') as superceding_solutions
|
392
|
+
FROM dim_solution
|
393
|
+
RIGHT OUTER JOIN (
|
394
|
+
SELECT DISTINCT (solution_id) solution_id
|
395
|
+
FROM (
|
396
|
+
SELECT solution_id, vulnerability_id, date_modified
|
397
|
+
FROM dim_vulnerability
|
398
|
+
LEFT JOIN dim_vulnerability_solution idvs USING (vulnerability_id)
|
399
|
+
) dvs
|
400
|
+
WHERE date_modified >= '#{options[:vuln_query_date]}'
|
401
|
+
UNION
|
402
|
+
SELECT DISTINCT (solution_id) solution_id
|
403
|
+
FROM dim_solution
|
404
|
+
LEFT JOIN (
|
405
|
+
SELECT solution_id, vulnerability_id, date_modified
|
406
|
+
FROM dim_vulnerability
|
407
|
+
LEFT JOIN dim_vulnerability_solution idvs USING (vulnerability_id)
|
408
|
+
) ndvs USING (solution_id)
|
409
|
+
WHERE vulnerability_id IS NULL
|
410
|
+
) dvs USING (solution_id)
|
411
|
+
LEFT JOIN (
|
412
|
+
SELECT DISTINCT (solution_id) solution_id,
|
413
|
+
array_agg(required_solution_id) as req_solutions
|
414
|
+
FROM dim_solution_prerequisite
|
415
|
+
GROUP BY solution_id
|
416
|
+
) dsp USING (solution_id)
|
417
|
+
JOIN (
|
418
|
+
SELECT DISTINCT (solution_id) solution_id,
|
419
|
+
array_agg(superceding_solution_id) as super_solutions
|
420
|
+
FROM dim_solution_highest_supercedence
|
421
|
+
GROUP BY solution_id
|
422
|
+
) dshs USING (solution_id)
|
423
|
+
ORDER BY solution_id"
|
424
|
+
end
|
425
|
+
|
345
426
|
def self.latest_scans(options={})
|
346
427
|
'SELECT ds.site_id, ds.last_scan_id, dsc.finished
|
347
428
|
FROM dim_site ds
|
@@ -350,8 +431,24 @@ module NexposeServiceNow
|
|
350
431
|
|
351
432
|
def self.multiple_reports?(query_name)
|
352
433
|
single_queries = %w(vulnerabilities vulnerability_category
|
353
|
-
|
434
|
+
asset_groups latest_scans
|
435
|
+
vulnerability_solutions vulnerability_references
|
436
|
+
)
|
354
437
|
return !(single_queries.include? query_name.to_s)
|
355
438
|
end
|
439
|
+
|
440
|
+
def self.csv_diff_required?(query_name)
|
441
|
+
diff_required = %w(asset_groups asset_group_memberships)
|
442
|
+
return (diff_required.include? query_name.to_s)
|
443
|
+
end
|
444
|
+
|
445
|
+
# TODO: Refactor this.
|
446
|
+
def self.query_keys(query_name)
|
447
|
+
if query_name.to_s == 'asset_groups'
|
448
|
+
[0]
|
449
|
+
else
|
450
|
+
[0,1]
|
451
|
+
end
|
452
|
+
end
|
356
453
|
end
|
357
454
|
end
|
data/nexpose_servicenow.gemspec
CHANGED
@@ -28,7 +28,8 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.bindir = "bin"
|
29
29
|
spec.require_paths = ["lib"]
|
30
30
|
|
31
|
-
spec.add_development_dependency
|
32
|
-
spec.add_development_dependency
|
31
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
32
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
33
33
|
spec.add_dependency 'nexpose', '~> 3.2'
|
34
|
+
spec.add_dependency 'csv-diff', '~> 0.3.3'
|
34
35
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nexpose_servicenow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Valente
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: csv-diff
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.3.3
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.3.3
|
55
69
|
description: Provides an interface to Nexpose for the Rapid7 ServiceNow MarketPlace
|
56
70
|
application.
|
57
71
|
email:
|
@@ -70,6 +84,7 @@ files:
|
|
70
84
|
- lib/nexpose_servicenow.rb
|
71
85
|
- lib/nexpose_servicenow/arg_parser.rb
|
72
86
|
- lib/nexpose_servicenow/chunker.rb
|
87
|
+
- lib/nexpose_servicenow/csv_compare.rb
|
73
88
|
- lib/nexpose_servicenow/historical_data.rb
|
74
89
|
- lib/nexpose_servicenow/nexpose_helper.rb
|
75
90
|
- lib/nexpose_servicenow/nx_logger.rb
|