nexpose_ticketing 1.3.0 → 1.4.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/Gemfile.lock +1 -1
- data/README.md +44 -12
- data/lib/nexpose_ticketing/config/servicenow.config +1 -1
- data/lib/nexpose_ticketing/config/servicenow_updateset/Rapid7_Nexpose_Ticketing_ServiceNow_Update_Set_v2.xml +1060 -0
- data/lib/nexpose_ticketing/config/ticket_service.config +3 -1
- data/lib/nexpose_ticketing/helpers/servicenow_helper.rb +276 -192
- data/lib/nexpose_ticketing/modes/base_mode.rb +29 -6
- data/lib/nexpose_ticketing/modes/default_mode.rb +2 -2
- data/lib/nexpose_ticketing/modes/vulnerability_mode.rb +7 -2
- data/lib/nexpose_ticketing/queries.rb +101 -42
- data/lib/nexpose_ticketing/store.rb +45 -0
- data/lib/nexpose_ticketing/ticket_metrics.rb +1 -1
- data/lib/nexpose_ticketing/ticket_repository.rb +627 -61
- data/lib/nexpose_ticketing/ticket_service.rb +100 -62
- data/lib/nexpose_ticketing/version.rb +1 -1
- metadata +28 -7
- data/lib/nexpose_ticketing/config/servicenow_updateset/Rapid7_Nexpose_Ticketing_ServiceNow_update_set.xml +0 -4488
@@ -8,6 +8,10 @@ class BaseMode
|
|
8
8
|
@log = NexposeTicketing::NxLogger.instance
|
9
9
|
end
|
10
10
|
|
11
|
+
def set_solution_store(solution_store)
|
12
|
+
@solution_store = solution_store
|
13
|
+
end
|
14
|
+
|
11
15
|
# True if this mode supports ticket updates
|
12
16
|
def updates_supported?
|
13
17
|
true
|
@@ -122,9 +126,12 @@ class BaseMode
|
|
122
126
|
# - String containing a short summary of the vulnerability.
|
123
127
|
#
|
124
128
|
def get_short_summary(row)
|
125
|
-
|
126
|
-
|
127
|
-
|
129
|
+
solution_ids = row['solution_ids'][1..-2].split(',')
|
130
|
+
return '' if solution_ids.first == 'NULL'
|
131
|
+
|
132
|
+
sol = @solution_store.get_solution(solution_ids.first)
|
133
|
+
summary = sol[:summary] || ''
|
134
|
+
|
128
135
|
summary.length <= 100 ? summary : summary[0...100]
|
129
136
|
end
|
130
137
|
|
@@ -136,11 +143,25 @@ class BaseMode
|
|
136
143
|
# - String formatted with solution information.
|
137
144
|
#
|
138
145
|
def get_solutions(row)
|
139
|
-
row['
|
146
|
+
solution_ids = row['solution_ids'][1..-2].split(',')
|
147
|
+
return '' if solution_ids.first == 'NULL'
|
148
|
+
|
149
|
+
solutions = @solution_store.get_solutions solution_ids
|
150
|
+
|
151
|
+
solutions.map! do |sol|
|
152
|
+
format = "Summary: #{sol[:summary] || 'None'}\n" \
|
153
|
+
"Nexpose ID: #{sol[:nexpose_id]}\n\n" \
|
154
|
+
"Fix: #{sol[:fix]}\n"
|
155
|
+
|
156
|
+
format = format + "\nURL: #{sol[:url]}" unless sol[:url].nil?
|
157
|
+
format + "\n"
|
158
|
+
end
|
159
|
+
|
160
|
+
solutions.join("\n--\n")
|
140
161
|
end
|
141
162
|
|
142
163
|
def get_discovery_info(row)
|
143
|
-
return '' if row['first_discovered'].to_s ==
|
164
|
+
return '' if row['first_discovered'].to_s == ''
|
144
165
|
info = "\nFirst Seen: #{row['first_discovered']}\n"
|
145
166
|
info << "Last Seen: #{row['most_recently_discovered']}\n"
|
146
167
|
info
|
@@ -175,7 +196,9 @@ class BaseMode
|
|
175
196
|
|
176
197
|
row['assets'].to_s.split('~').each do |a|
|
177
198
|
asset = a.split('|')
|
178
|
-
|
199
|
+
asset_entry = " - #{asset[1]} "
|
200
|
+
asset_entry << "\t(#{asset[2]})" unless (asset[2].nil? || asset[2].empty?)
|
201
|
+
assets << "#{asset_entry}\n"
|
179
202
|
end
|
180
203
|
assets
|
181
204
|
end
|
@@ -14,7 +14,7 @@ class DefaultMode < BaseMode
|
|
14
14
|
|
15
15
|
# Returns the fields used to identify individual tickets
|
16
16
|
def get_matching_fields
|
17
|
-
|
17
|
+
%w(ip_address vulnerability_id)
|
18
18
|
end
|
19
19
|
|
20
20
|
# Returns the ticket's title
|
@@ -30,7 +30,7 @@ class DefaultMode < BaseMode
|
|
30
30
|
# Returns the base ticket description object
|
31
31
|
def get_description(nexpose_id, row)
|
32
32
|
description = { nxid: "NXID: #{get_nxid(nexpose_id, row)}" }
|
33
|
-
fields =
|
33
|
+
fields = %w(header references solutions)
|
34
34
|
fields.each { |f| description[f.intern] = self.send("get_#{f}", row) }
|
35
35
|
description[:header] << get_discovery_info(row)
|
36
36
|
description
|
@@ -27,10 +27,15 @@ class VulnerabilityMode < BaseMode
|
|
27
27
|
'_by_vuln_id'
|
28
28
|
end
|
29
29
|
|
30
|
+
def get_vuln_description(row)
|
31
|
+
return '' if row['description'].to_s == ''
|
32
|
+
"\nVulnerability Description: #{row['description']}"
|
33
|
+
end
|
34
|
+
|
30
35
|
# Returns the base ticket description object
|
31
36
|
def get_description(nexpose_id, row)
|
32
37
|
description = { nxid: "NXID: #{get_nxid(nexpose_id, row)}" }
|
33
|
-
fields = ['header', 'references', 'solutions', 'assets']
|
38
|
+
fields = ['header', 'references', 'solutions', 'assets', 'vuln_description']
|
34
39
|
fields.each { |f| description[f.intern] = self.send("get_#{f}", row) }
|
35
40
|
description
|
36
41
|
end
|
@@ -43,7 +48,7 @@ class VulnerabilityMode < BaseMode
|
|
43
48
|
|
44
49
|
# Converts the ticket description object into a formatted string
|
45
50
|
def print_description(description)
|
46
|
-
fields = [:header, :assets, :references, :solutions]
|
51
|
+
fields = [:header, :assets, :references, :solutions, :vuln_description]
|
47
52
|
finalize_description(fields.map { |f| description[f] }.join("\n"),
|
48
53
|
description[:nxid])
|
49
54
|
end
|
@@ -13,12 +13,9 @@ module NexposeTicketing
|
|
13
13
|
# - Returns String - Formatted SQL string for inserting into queries.
|
14
14
|
#
|
15
15
|
def self.createRiskString(riskScore)
|
16
|
-
if riskScore.nil?
|
17
|
-
|
18
|
-
|
19
|
-
riskString = "WHERE fa.riskscore >= #{riskScore}"
|
20
|
-
end
|
21
|
-
return riskString
|
16
|
+
return '' if riskScore.nil?
|
17
|
+
|
18
|
+
"WHERE fa.riskscore >= #{riskScore}"
|
22
19
|
end
|
23
20
|
|
24
21
|
# Formats SQL query for filtering per asset based on user config options.
|
@@ -32,11 +29,10 @@ module NexposeTicketing
|
|
32
29
|
|
33
30
|
def self.createAssetString(options)
|
34
31
|
if options[:tag_run] && options[:nexpose_item]
|
35
|
-
|
32
|
+
"WHERE asset_id = #{options[:nexpose_item]}"
|
36
33
|
else
|
37
|
-
|
34
|
+
''
|
38
35
|
end
|
39
|
-
return assetString
|
40
36
|
end
|
41
37
|
|
42
38
|
# Gets all the latest scans for sites.
|
@@ -47,6 +43,16 @@ module NexposeTicketing
|
|
47
43
|
JOIN dim_scan dsc ON ds.last_scan_id = dsc.scan_id'
|
48
44
|
end
|
49
45
|
|
46
|
+
# Returns the solutions for every vulnerability
|
47
|
+
# stored within Nexpose
|
48
|
+
def self.all_solutions
|
49
|
+
"SELECT solution_id, nexpose_id,
|
50
|
+
summary,
|
51
|
+
proofAsText(fix) as fix,
|
52
|
+
url
|
53
|
+
FROM dim_solution"
|
54
|
+
end
|
55
|
+
|
50
56
|
# Gets all the latest scans for tags.
|
51
57
|
# Returns |tag.id| |asset.id| |last_scan_id| |finished|
|
52
58
|
def self.last_tag_scans
|
@@ -56,43 +62,96 @@ module NexposeTicketing
|
|
56
62
|
order by dta.tag_id, dta.asset_id, fa.last_scan_id, fa.scan_finished'
|
57
63
|
end
|
58
64
|
|
65
|
+
# Returns the current necessary information on vulnerable_instances for a
|
66
|
+
# site / tag to create a ticket, for IP mode
|
67
|
+
# * *Returns* :
|
68
|
+
# - Returns |asset_id| |vulnerability_id| |first_discovered|
|
69
|
+
# |most_recently_discovered| |solution_ids|
|
70
|
+
def self.all_new_vulns_by_ip(options={})
|
71
|
+
'SELECT asset_id, vulnerability_id,
|
72
|
+
first_discovered, most_recently_discovered,
|
73
|
+
array_agg(solution_id) as solution_ids
|
74
|
+
FROM (
|
75
|
+
SELECT asset_id, vulnerability_id
|
76
|
+
FROM fact_asset_vulnerability_finding) favf
|
77
|
+
JOIN (
|
78
|
+
SELECT asset_id, vulnerability_id, first_discovered,
|
79
|
+
most_recently_discovered
|
80
|
+
FROM fact_asset_vulnerability_age) fava USING (asset_id, vulnerability_id)
|
81
|
+
LEFT JOIN dim_asset_vulnerability_solution USING (asset_id, vulnerability_id)
|
82
|
+
GROUP BY asset_id, vulnerability_id, first_discovered, most_recently_discovered
|
83
|
+
ORDER BY asset_id, vulnerability_id'
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the current necessary information on vulnerable_instances for a
|
87
|
+
# site to creating a ticket, for Vulnerability mode
|
88
|
+
# * *Returns* :
|
89
|
+
# - Returns |vulnerability_id| |solution_ids| |references|
|
90
|
+
def self.all_new_vulns_by_vuln_id(options={})
|
91
|
+
"SELECT DISTINCT(vulnerability_id) vulnerability_id,
|
92
|
+
array_agg(DISTINCT solution_id) as solution_ids,
|
93
|
+
string_agg(DISTINCT dvr.source || ': ' || dvr.reference, ', ') as references
|
94
|
+
FROM (
|
95
|
+
SELECT asset_id, vulnerability_id
|
96
|
+
FROM fact_asset_vulnerability_finding) favf
|
97
|
+
LEFT JOIN dim_asset_vulnerability_solution USING (asset_id, vulnerability_id)
|
98
|
+
LEFT JOIN dim_vulnerability_reference dvr USING (vulnerability_id)
|
99
|
+
GROUP BY vulnerability_id
|
100
|
+
ORDER BY vulnerability_id"
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns information on the previous state of the site / tag, for IP mode
|
104
|
+
# * *Returns* :
|
105
|
+
# - Returns |asset_id| |vulnerability_id| |scan_id|
|
106
|
+
def self.last_scan_state_by_ip(options={})
|
107
|
+
"SELECT asset_id, vulnerability_id, scan_id
|
108
|
+
FROM fact_asset_scan_vulnerability_finding
|
109
|
+
WHERE scan_id = #{options[:scan_id]}
|
110
|
+
ORDER BY asset_id, vulnerability_id"
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns information on the previous state of the site, for Vulnerability
|
114
|
+
# mode
|
115
|
+
# * *Returns* :
|
116
|
+
# - Returns |vulnerability_id| |asset_ids| |scan_id|
|
117
|
+
def self.last_scan_state_by_vuln_id(options={})
|
118
|
+
"SELECT DISTINCT(vulnerability_id) vulnerability_id,
|
119
|
+
array_agg(DISTINCT asset_id) as asset_ids, scan_id
|
120
|
+
FROM fact_asset_scan_vulnerability_finding
|
121
|
+
WHERE scan_id = #{options[:scan_id]}
|
122
|
+
GROUP BY vulnerability_id, scan_id
|
123
|
+
ORDER BY vulnerability_id"
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.all_new_vulns_by_ip_solutions(options={})
|
127
|
+
'SELECT asset_id, vulnerability_id, summary, fix, url
|
128
|
+
FROM (select asset_id, vulnerability_id FROM fact_asset_scan_vulnerability_finding) fasv
|
129
|
+
JOIN dim_asset_vulnerability_solution dvs USING (asset_id, vulnerability_id)
|
130
|
+
JOIN (select solution_id, summary, fix, url FROM dim_solution) ds USING (solution_id)
|
131
|
+
ORDER BY asset_id, vulnerability_id'
|
132
|
+
end
|
133
|
+
|
59
134
|
# Gets all delta vulns for all sites sorted by IP.
|
60
135
|
#
|
61
136
|
# * *Returns* :
|
62
|
-
# -Returns |asset_id| |ip_address| |current_scan| |vulnerability_id||solution_id| |nexpose_id|
|
137
|
+
# -Returns |asset_id| |ip_address| |current_scan| |vulnerability_id||solution_id| |nexpose_id|
|
63
138
|
# |url| |summary| |fix|
|
64
139
|
#
|
65
|
-
def self.
|
66
|
-
"SELECT
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
FROM dim_asset #{createAssetString(options)}) s
|
81
|
-
ON s.asset_id = fasv.asset_id AND (fasv.scan_id = s.baseline_scan OR fasv.scan_id = s.current_scan)
|
82
|
-
GROUP BY fasv.asset_id, fasv.vulnerability_id, s.current_scan, fasv.scan_id
|
83
|
-
HAVING NOT baselineComparison(fasv.scan_id, current_scan) = 'Old'
|
84
|
-
) subs
|
85
|
-
JOIN dim_vulnerability dv USING (vulnerability_id)
|
86
|
-
LEFT JOIN dim_vulnerability_reference dvr USING (vulnerability_id)
|
87
|
-
JOIN dim_asset_vulnerability_solution davs USING (vulnerability_id)
|
88
|
-
JOIN fact_asset_vulnerability_age fasva ON subs.vulnerability_id = fasva.vulnerability_id AND subs.asset_id = fasva.asset_id
|
89
|
-
JOIN dim_solution ds USING (solution_id)
|
90
|
-
JOIN dim_asset da ON subs.asset_id = da.asset_id
|
91
|
-
JOIN fact_asset fa ON fa.asset_id = da.asset_id
|
92
|
-
#{createRiskString( options[:riskScore])}
|
93
|
-
GROUP BY subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id, dv.nexpose_id,
|
94
|
-
fa.riskscore, dv.cvss_score, fasva.first_discovered, fasva.most_recently_discovered
|
95
|
-
ORDER BY da.ip_address, subs.vulnerability_id"
|
140
|
+
def self.all_new_vulns_by_ip_old(options = {})
|
141
|
+
"SELECT asset_id, vulnerability_id, ip_address, riskscore, nexpose_id, cvss_score,
|
142
|
+
first_discovered, most_recently_discovered,
|
143
|
+
array_agg(solution_id) as solution_ids
|
144
|
+
FROM (SELECT asset_id, vulnerability_id FROM fact_asset_vulnerability_finding) favf
|
145
|
+
JOIN (SELECT vulnerability_id, nexpose_id, cvss_score FROM dim_vulnerability) dv USING (vulnerability_id)
|
146
|
+
JOIN (SELECT asset_id, vulnerability_id, first_discovered, most_recently_discovered FROM fact_asset_vulnerability_age) fava USING (asset_id, vulnerability_id)
|
147
|
+
JOIN (SELECT asset_id, ip_address FROM dim_asset) da USING (asset_id)
|
148
|
+
JOIN (SELECT asset_id, riskscore FROM fact_asset) fa USING (asset_id)
|
149
|
+
JOIN dim_asset_vulnerability_solution USING (asset_id, vulnerability_id)
|
150
|
+
#{createAssetString(options)}
|
151
|
+
#{createRiskString( options[:riskScore])}
|
152
|
+
GROUP BY asset_id, vulnerability_id, ip_address, riskscore, nexpose_id, cvss_score,
|
153
|
+
first_discovered, most_recently_discovered
|
154
|
+
ORDER BY asset_id, vulnerability_id"
|
96
155
|
end
|
97
156
|
|
98
157
|
# Gets all delta vulns for all sites sorted by vuln ID.
|
@@ -101,7 +160,7 @@ module NexposeTicketing
|
|
101
160
|
# -Returns |asset_id| |ip_address| |current_scan| |vulnerability_id||solution_id| |nexpose_id|
|
102
161
|
# |url| |summary| |fix|
|
103
162
|
#
|
104
|
-
def self.
|
163
|
+
def self.all_new_vulns_by_vuln_id_old(options = {})
|
105
164
|
"SELECT DISTINCT on (subs.vulnerability_id) subs.vulnerability_id, dv.nexpose_id as vuln_nexpose_id, dv.title, MAX(dv.cvss_score) as cvss_score,
|
106
165
|
string_agg(DISTINCT subs.asset_id ||
|
107
166
|
'|' || da.ip_address ||
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module NexposeTicketing
|
4
|
+
|
5
|
+
class Store
|
6
|
+
def initialize()
|
7
|
+
@solutions = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# For now we don't cache anything
|
11
|
+
def self.store_exists?
|
12
|
+
return false
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_path(csv_path)
|
16
|
+
@csv_path = csv_path
|
17
|
+
end
|
18
|
+
|
19
|
+
def fill_store
|
20
|
+
# Should this be a single transaction?
|
21
|
+
CSV.foreach(@csv_path, headers: true) do |row|
|
22
|
+
@solutions[row['solution_id']] = { nexpose_id: row['nexpose_id'],
|
23
|
+
summary: row['summary'],
|
24
|
+
fix: row['fix'],
|
25
|
+
url: row['url'] }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_solution(solution_id)
|
30
|
+
@solutions.fetch(solution_id, {})
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_solutions(solution_ids)
|
34
|
+
sols = []
|
35
|
+
|
36
|
+
solution_ids.each do |s|
|
37
|
+
sol = @solutions[s]
|
38
|
+
next if sol.nil?
|
39
|
+
sols << sol
|
40
|
+
end
|
41
|
+
|
42
|
+
sols
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -27,7 +27,7 @@ module NexposeTicketing
|
|
27
27
|
|
28
28
|
def finish
|
29
29
|
return if @start_time == nil
|
30
|
-
@time_taken = Time.at(Time.now - @start_time).utc.strftime(
|
30
|
+
@time_taken = Time.at(Time.now - @start_time).utc.strftime('%H:%M:%S')
|
31
31
|
@start_time = nil
|
32
32
|
|
33
33
|
@log.log_message("Ticket processing took #{@time_taken} to complete.")
|
@@ -8,6 +8,7 @@ module NexposeTicketing
|
|
8
8
|
require 'nexpose_ticketing/nx_logger'
|
9
9
|
require 'nexpose_ticketing/version'
|
10
10
|
|
11
|
+
API_VERSION = '1.2.0'
|
11
12
|
@timeout = 10800
|
12
13
|
|
13
14
|
def initialize(options = nil)
|
@@ -21,7 +22,7 @@ module NexposeTicketing
|
|
21
22
|
|
22
23
|
def define_query_methods
|
23
24
|
methods = Queries.methods.grep Regexp.new (@method_suffix+'$')
|
24
|
-
|
25
|
+
|
25
26
|
methods.each do |m|
|
26
27
|
define_singleton_method m do |options, override=nil|
|
27
28
|
request_query(m, options, override)
|
@@ -29,16 +30,582 @@ module NexposeTicketing
|
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
32
|
-
def nexpose_login(nexpose_data)
|
33
|
-
@nsc = Nexpose::Connection.new(nexpose_data[:nxconsole],
|
33
|
+
def nexpose_login(nexpose_data)
|
34
|
+
@nsc = Nexpose::Connection.new(nexpose_data[:nxconsole],
|
35
|
+
nexpose_data[:nxuser],
|
36
|
+
nexpose_data[:nxpasswd])
|
34
37
|
@nsc.login
|
35
38
|
@log = NexposeTicketing::NxLogger.instance
|
36
|
-
@log.on_connect(nexpose_data[:nxconsole], 3780, @nsc.session_id,
|
39
|
+
@log.on_connect(nexpose_data[:nxconsole], 3780, @nsc.session_id, '{}')
|
37
40
|
|
38
41
|
#After login, create the report helper
|
39
42
|
@report_helper = NexposeReportHelper::ReportOps.new(@nsc, @timeout)
|
40
43
|
end
|
41
44
|
|
45
|
+
# Logs a message if logging is enabled.
|
46
|
+
def log_message(message)
|
47
|
+
@log.log_message(message)
|
48
|
+
end
|
49
|
+
|
50
|
+
def log_debug_message(message)
|
51
|
+
@log.log_debug_message(message)
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_solution_data
|
55
|
+
report_config = @report_helper.generate_sql_report_config()
|
56
|
+
report_config.add_filter('version', API_VERSION)
|
57
|
+
report_config.add_filter('query', Queries.all_solutions)
|
58
|
+
@report_helper.save_generate_cleanup_report_config(report_config)
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_asset_list(item, mode)
|
62
|
+
self.send("get_#{mode}_asset_list", item)
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_site_asset_list(site_id)
|
66
|
+
@nsc.assets(site_id)
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_tag_asset_list(tag_id)
|
70
|
+
Nexpose::Tag.load(@nsc, tag_id).asset_ids
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_vuln_instances(asset_id, severity = 0)
|
74
|
+
@nsc.list_device_vulns(asset_id).select {|vuln| vuln.severity >= severity}
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_asset_ip(asset_id)
|
78
|
+
Nexpose::Asset.load(@nsc, asset_id).ip
|
79
|
+
end
|
80
|
+
|
81
|
+
def create_solution_hash(options, nexpose_item)
|
82
|
+
self.send("create_solution_hash#{@method_suffix}", options, nexpose_item)
|
83
|
+
end
|
84
|
+
|
85
|
+
def create_solution_hash_by_ip(options, nexpose_item)
|
86
|
+
report = all_new_vulns(options, nexpose_item)
|
87
|
+
|
88
|
+
info = {}
|
89
|
+
CSV.foreach(report) do |row|
|
90
|
+
asset_id, vulnerability_id, first_discov,
|
91
|
+
most_recently_discov, solution_ids = row
|
92
|
+
|
93
|
+
next if asset_id == 'asset_id'
|
94
|
+
unless info.key? asset_id.to_s
|
95
|
+
info[asset_id.to_s] = {}
|
96
|
+
end
|
97
|
+
|
98
|
+
info[asset_id.to_s][vulnerability_id.to_s] = {
|
99
|
+
first_discovered: first_discov,
|
100
|
+
most_recently: most_recently_discov,
|
101
|
+
solution_ids: solution_ids }
|
102
|
+
end
|
103
|
+
info
|
104
|
+
end
|
105
|
+
|
106
|
+
def create_solution_hash_by_vuln_id(options, nexpose_item)
|
107
|
+
report = all_new_vulns(options, nexpose_item)
|
108
|
+
|
109
|
+
info = {}
|
110
|
+
CSV.foreach(report) do |row|
|
111
|
+
vulnerability_id, solution_ids, references = row
|
112
|
+
|
113
|
+
next if vulnerability_id == 'vulnerability_id'
|
114
|
+
unless info.key? vulnerability_id.to_i
|
115
|
+
info[vulnerability_id.to_i] = {}
|
116
|
+
end
|
117
|
+
|
118
|
+
info[vulnerability_id.to_i] = { solution_ids: solution_ids,
|
119
|
+
refs: references }
|
120
|
+
end
|
121
|
+
info
|
122
|
+
end
|
123
|
+
|
124
|
+
# Method retrieves current state of a site / tag from Nexpose for use in a
|
125
|
+
# Ticketing Integration
|
126
|
+
#
|
127
|
+
# Params:
|
128
|
+
# - Options: The options to use to generate information
|
129
|
+
# - Nexpose_Item: The ID of the Site / Tag to generate data for
|
130
|
+
#
|
131
|
+
# Returns: CSV containing vulnerability information
|
132
|
+
def generate_initial_scan_data(options, nexpose_item)
|
133
|
+
self.send("initial_scan#{@method_suffix}", options, nexpose_item)
|
134
|
+
end
|
135
|
+
|
136
|
+
def initial_scan_by_ip(options, nexpose_item)
|
137
|
+
log_message "Getting vuln info for #{options[:scan_mode]}: " \
|
138
|
+
"#{nexpose_item}"
|
139
|
+
|
140
|
+
query_options = options.dup
|
141
|
+
query_options[:report_type] = 'initial' if options[:tag_run]
|
142
|
+
info_hash = create_solution_hash_by_ip(query_options, nexpose_item)
|
143
|
+
|
144
|
+
log_debug_message "Getting assets for #{nexpose_item}"
|
145
|
+
assets = get_asset_list(nexpose_item, options[:scan_mode])
|
146
|
+
|
147
|
+
initial_scan_file = Tempfile.new("vulnerable_items_#{nexpose_item}")
|
148
|
+
initial_scan_file.binmode
|
149
|
+
|
150
|
+
log_debug_message 'Creating CSV for helper'
|
151
|
+
CSV.open(initial_scan_file, 'wb') do |csv|
|
152
|
+
csv << ['asset_id', 'vulnerability_id', 'first_discovered',
|
153
|
+
'most_recently_discovered', 'ip_address', 'riskscore',
|
154
|
+
'vuln_nexpose_id', 'cvss_score', 'solution_ids']
|
155
|
+
|
156
|
+
assets.each do |asset|
|
157
|
+
if options[:tag_run]
|
158
|
+
asset_id = asset.to_s
|
159
|
+
asset_deets = Nexpose::Asset.load(@nsc, asset)
|
160
|
+
address = asset_deets.ip
|
161
|
+
risk_score = asset_deets.assessment.risk_score
|
162
|
+
else
|
163
|
+
asset_id = asset.id.to_s
|
164
|
+
address = asset.address
|
165
|
+
risk_score = asset.risk_score
|
166
|
+
end
|
167
|
+
|
168
|
+
next if risk_score < (options[:riskScore] || 0)
|
169
|
+
|
170
|
+
log_debug_message "Getting vulns for asset #{address}"
|
171
|
+
vulns = get_vuln_instances(asset_id, options[:severity])
|
172
|
+
vulns.each do |vuln|
|
173
|
+
info = info_hash[asset_id][vuln.console_id.to_s]
|
174
|
+
csv << [asset_id, vuln.console_id, info[:first_discovered],
|
175
|
+
info[:most_recently], address, risk_score,
|
176
|
+
vuln.id, vuln.cvss_score, info[:solution_ids]]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
initial_scan_file.flush
|
181
|
+
|
182
|
+
initial_scan_file
|
183
|
+
end
|
184
|
+
|
185
|
+
def initial_scan_by_vuln_id(options, nexpose_item)
|
186
|
+
log_message "Getting vuln info for #{options[:scan_mode]}: " \
|
187
|
+
"#{nexpose_item}"
|
188
|
+
soln_ref_hash = create_solution_hash_by_vuln_id(options, nexpose_item)
|
189
|
+
|
190
|
+
log_debug_message "Getting assets for #{nexpose_item}"
|
191
|
+
assets = get_asset_list(nexpose_item, options[:scan_mode])
|
192
|
+
current_vuln_instances = {}
|
193
|
+
assets.each do |asset|
|
194
|
+
next if asset.risk_score < (options[:riskScore] || 0)
|
195
|
+
|
196
|
+
vulns = get_vuln_instances(asset.id.to_s, options[:severity])
|
197
|
+
|
198
|
+
vulns.each do |vuln|
|
199
|
+
unless current_vuln_instances.key? vuln.console_id
|
200
|
+
current_vuln_instances[vuln.console_id] = { vuln: vuln.id,
|
201
|
+
assets: [] }
|
202
|
+
end
|
203
|
+
|
204
|
+
current_vuln_instances[vuln.console_id][:assets] << asset
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
initial_scan_file = Tempfile.new("vulnerable_items_#{nexpose_item}")
|
209
|
+
initial_scan_file.binmode
|
210
|
+
log_debug_message 'Creating CSV for helper'
|
211
|
+
|
212
|
+
CSV.open(initial_scan_file, 'wb') do |csv|
|
213
|
+
csv << ['vulnerability_id', 'vuln_nexpose_id', 'title', 'cvss_score',
|
214
|
+
'assets', 'description', 'solution_ids', 'references']
|
215
|
+
current_vuln_instances.each do |vuln_id, data|
|
216
|
+
vuln_def = Nexpose::VulnerabilityDefinition.load(@nsc, data[:vuln])
|
217
|
+
assets = []
|
218
|
+
data[:assets].each do |asset|
|
219
|
+
assets << "#{asset.id}|#{asset.address}|#{asset.risk_score}"
|
220
|
+
end
|
221
|
+
|
222
|
+
csv << [vuln_id, data[:vuln], vuln_def.title, vuln_def.cvss_score,
|
223
|
+
assets.join('~'), vuln_def.description,
|
224
|
+
soln_ref_hash[vuln_id][:solution_ids],
|
225
|
+
soln_ref_hash[vuln_id][:refs]]
|
226
|
+
end
|
227
|
+
end
|
228
|
+
initial_scan_file.flush
|
229
|
+
|
230
|
+
initial_scan_file
|
231
|
+
end
|
232
|
+
|
233
|
+
# Method retrieves current state of a site from Nexpose, before creating a
|
234
|
+
# diff against the state the last time the integration was run.
|
235
|
+
#
|
236
|
+
# Params:
|
237
|
+
# - Options: The options to use to generate information
|
238
|
+
#
|
239
|
+
# Returns: The current state of vulnerabilities for assets in a site.
|
240
|
+
#
|
241
|
+
def generate_delta_scan_data(options)
|
242
|
+
self.send("delta_scan#{@method_suffix}", options)
|
243
|
+
end
|
244
|
+
|
245
|
+
def delta_scan_by_ip(options)
|
246
|
+
current_vuln_instances = {}
|
247
|
+
current_vuln_ids = {}
|
248
|
+
ignored_assets = []
|
249
|
+
|
250
|
+
assets = if options[:tag_run]
|
251
|
+
[Nexpose::Asset.load(@nsc, options[:nexpose_item])]
|
252
|
+
else
|
253
|
+
get_site_asset_list(options[:nexpose_item])
|
254
|
+
end
|
255
|
+
|
256
|
+
assets.each do |asset|
|
257
|
+
risk_score = if options[:tag_run]
|
258
|
+
asset.assessment.risk_score
|
259
|
+
else
|
260
|
+
asset.risk_score
|
261
|
+
end
|
262
|
+
if risk_score < (options[:riskScore] || 0)
|
263
|
+
ignored_assets << asset.id
|
264
|
+
next
|
265
|
+
end
|
266
|
+
|
267
|
+
vulns = get_vuln_instances(asset.id, options[:severity])
|
268
|
+
current_vuln_instances[asset.id.to_s] = { asset: asset, vulns: vulns }
|
269
|
+
current_vuln_ids[asset.id.to_s] = vulns.map { |v| v.console_id }
|
270
|
+
end
|
271
|
+
|
272
|
+
state = {}
|
273
|
+
previous_state_file = get_previous_state(options,
|
274
|
+
'last_scan_state_by_ip')
|
275
|
+
|
276
|
+
CSV.foreach(previous_state_file, headers: true) do |row|
|
277
|
+
asset_id = row['asset_id'].to_s
|
278
|
+
vuln_id = row['vulnerability_id'].to_i
|
279
|
+
|
280
|
+
next if ignored_assets.include? asset_id
|
281
|
+
|
282
|
+
if current_vuln_ids.key? asset_id
|
283
|
+
unless state.key? asset_id.to_s
|
284
|
+
state[asset_id] =
|
285
|
+
{ asset: current_vuln_instances[asset_id][:asset], new: [],
|
286
|
+
same: [], old: [] }
|
287
|
+
end
|
288
|
+
|
289
|
+
success = current_vuln_ids[asset_id].delete(vuln_id)
|
290
|
+
if success.nil?
|
291
|
+
state[asset_id][:old] << vuln_id
|
292
|
+
else
|
293
|
+
vulns = current_vuln_instances[asset_id][:vulns]
|
294
|
+
state[asset_id][:same] << vulns.find { |v| v.console_id == vuln_id }
|
295
|
+
end
|
296
|
+
else
|
297
|
+
unless state.key? asset_id
|
298
|
+
state[asset_id] = { old_ticket: '', new: [], same: [], old: [] }
|
299
|
+
end
|
300
|
+
|
301
|
+
state[asset_id][:old] << vuln_id
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
current_vuln_ids.each_key do |asset_id|
|
306
|
+
asset_id = asset_id.to_s
|
307
|
+
unless state.key? asset_id
|
308
|
+
state[asset_id] =
|
309
|
+
{ asset: current_vuln_instances[asset_id][:asset], new: [],
|
310
|
+
same: [], old: [] }
|
311
|
+
end
|
312
|
+
|
313
|
+
current_vuln_ids[asset_id].each do |vuln_id|
|
314
|
+
vulns = current_vuln_instances[asset_id][:vulns]
|
315
|
+
state[asset_id][:new] << vulns.find { |v| v.console_id == vuln_id }
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
state.each_key do |asset_id|
|
320
|
+
if state[asset_id][:new].size == 0 && state[asset_id][:same].size == 0
|
321
|
+
state[asset_id][:old_ticket] = ''
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
state
|
326
|
+
end
|
327
|
+
|
328
|
+
def delta_scan_by_vuln_id(options)
|
329
|
+
current_vuln_instances = {}
|
330
|
+
current_vuln_ids = {}
|
331
|
+
ignored_assets = []
|
332
|
+
assets = get_site_asset_list(options[:nexpose_item])
|
333
|
+
|
334
|
+
assets.each do |asset|
|
335
|
+
if asset.risk_score < (options[:riskScore] || 0)
|
336
|
+
ignored_assets << asset.id
|
337
|
+
next
|
338
|
+
end
|
339
|
+
vulns = get_vuln_instances(asset.id.to_s, options[:severity])
|
340
|
+
|
341
|
+
vulns.each do |vuln|
|
342
|
+
console_id = vuln.console_id
|
343
|
+
unless current_vuln_instances.key? console_id
|
344
|
+
current_vuln_instances[console_id] = { vuln: vuln.id,
|
345
|
+
assets: {} }
|
346
|
+
current_vuln_ids[console_id] = []
|
347
|
+
end
|
348
|
+
|
349
|
+
current_vuln_instances[console_id][:assets][asset.id] = asset
|
350
|
+
current_vuln_ids[console_id] << asset.id
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
state = {}
|
355
|
+
previous_state_file = get_previous_state(options,
|
356
|
+
'last_scan_state_by_vuln_id')
|
357
|
+
|
358
|
+
CSV.foreach(previous_state_file, headers: true) do |row|
|
359
|
+
vuln_id = row['vulnerability_id'].to_i
|
360
|
+
asset_ids = row['asset_ids'][1..-2].split(',').map(&:to_i)
|
361
|
+
|
362
|
+
if current_vuln_ids.key? vuln_id
|
363
|
+
unless state.key? vuln_id
|
364
|
+
state[vuln_id] = { vuln: current_vuln_instances[vuln_id][:vuln],
|
365
|
+
new: [], same: [], old: [] }
|
366
|
+
end
|
367
|
+
|
368
|
+
asset_ids.each do |asset_id|
|
369
|
+
next if ignored_assets.include? asset_id
|
370
|
+
|
371
|
+
success = current_vuln_ids[vuln_id].delete(asset_id)
|
372
|
+
|
373
|
+
if success.nil?
|
374
|
+
asset_info = { id: asset_id, ip: get_asset_ip(asset_id) }
|
375
|
+
state[vuln_id][:old] << asset_info
|
376
|
+
else
|
377
|
+
asset = current_vuln_instances[vuln_id][:assets][asset_id]
|
378
|
+
state[vuln_id][:same] << asset
|
379
|
+
end
|
380
|
+
end
|
381
|
+
else
|
382
|
+
unless state.key? vuln_id
|
383
|
+
state[vuln_id] = { vuln: vuln_id, old_ticket: 1, new: [],
|
384
|
+
same: [], old: [] }
|
385
|
+
end
|
386
|
+
|
387
|
+
state[vuln_id][:old] << asset_ids
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
current_vuln_ids.each_key do |vuln_id|
|
392
|
+
unless state.key? vuln_id
|
393
|
+
state[vuln_id] =
|
394
|
+
{ vuln: current_vuln_instances[vuln_id][:vuln], new: [],
|
395
|
+
same: [], old: [] }
|
396
|
+
end
|
397
|
+
|
398
|
+
current_vuln_ids[vuln_id].each do |asset_id|
|
399
|
+
asset = current_vuln_instances[vuln_id][:assets][asset_id]
|
400
|
+
state[vuln_id][:new] << asset
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
state.each_key do |vuln_id|
|
405
|
+
if state[vuln_id][:new].size == 0 && state[vuln_id][:same].size == 0
|
406
|
+
state[vuln_id][:old_ticket] = 1
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
state
|
411
|
+
end
|
412
|
+
|
413
|
+
# Method converts retrieved information from Nexpose, sorted by the current
|
414
|
+
# state, into a CSV to be parsed into tickets for sending to a third party
|
415
|
+
# service
|
416
|
+
#
|
417
|
+
# Params:
|
418
|
+
# - options: The options to use to generate information
|
419
|
+
# - close_tickets: Whether the user has ticket closures enabled
|
420
|
+
#
|
421
|
+
# Returns: A CSV containing all necessary information to create tickets
|
422
|
+
#
|
423
|
+
def generate_delta_csv(options, close_tickets)
|
424
|
+
self.send("generate_delta_csv#{@method_suffix}", options, close_tickets)
|
425
|
+
end
|
426
|
+
|
427
|
+
def generate_delta_csv_by_ip(options, close_tickets)
|
428
|
+
nexpose_item = options[:nexpose_item]
|
429
|
+
|
430
|
+
state = generate_delta_scan_data(options)
|
431
|
+
info_hash = create_solution_hash(options, nexpose_item)
|
432
|
+
|
433
|
+
delta_scan_file = Tempfile.new("delta_vulnerable_items_#{nexpose_item}")
|
434
|
+
delta_scan_file.binmode
|
435
|
+
|
436
|
+
CSV.open(delta_scan_file, 'wb') do |csv|
|
437
|
+
csv << ['asset_id', 'vulnerability_id', 'first_discovered',
|
438
|
+
'most_recently_discovered', 'ip_address', 'riskscore',
|
439
|
+
'vuln_nexpose_id', 'cvss_score', 'solution_ids', 'comparison']
|
440
|
+
|
441
|
+
state.each do |asset_id, vulns|
|
442
|
+
asset = vulns[:asset]
|
443
|
+
asset_id = asset.id.to_s
|
444
|
+
|
445
|
+
if options[:tag_run]
|
446
|
+
address = asset.ip
|
447
|
+
risk_score = asset.assessment.risk_score
|
448
|
+
else
|
449
|
+
address = asset.address
|
450
|
+
risk_score = asset.risk_score
|
451
|
+
end
|
452
|
+
|
453
|
+
if vulns.key? :new
|
454
|
+
vulns[:new].each do |vuln|
|
455
|
+
info = info_hash[asset_id][vuln.console_id.to_s]
|
456
|
+
csv << [asset_id, vuln.console_id, info[:first_discovered],
|
457
|
+
info[:most_recently], address, risk_score,
|
458
|
+
vuln.id, vuln.cvss_score, info[:solution_ids], 'New']
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
if vulns.key? :same
|
463
|
+
vulns[:same].each do |vuln|
|
464
|
+
info = info_hash[asset_id][vuln.console_id.to_s]
|
465
|
+
csv << [asset_id, vuln.console_id, info[:first_discovered],
|
466
|
+
info[:most_recently], address, risk_score,
|
467
|
+
vuln.id, vuln.cvss_score, info[:solution_ids], 'Same']
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
delta_scan_file.flush
|
474
|
+
unless close_tickets
|
475
|
+
return { new_csv: delta_scan_file }
|
476
|
+
end
|
477
|
+
|
478
|
+
old_vulns_file = Tempfile.new("delta_vuln_old_items_#{nexpose_item}")
|
479
|
+
old_vulns_file.binmode
|
480
|
+
|
481
|
+
old_vulns_mode = options[:old_vulns_mode]
|
482
|
+
CSV.open(old_vulns_file, 'wb') do |csv|
|
483
|
+
csv << %w(asset_id vulnerability_id ip_address)
|
484
|
+
|
485
|
+
if old_vulns_mode == 'old'
|
486
|
+
key = old_vulns_mode.intern
|
487
|
+
old_vulns = state.select { |asset_id, vulns| vulns[key].size > 0 }
|
488
|
+
old_vulns.each do |asset_id, vulns|
|
489
|
+
asset = old_vulns[asset_id][:asset]
|
490
|
+
vulns[:old].each do |vuln|
|
491
|
+
csv << [asset_id, vuln, asset.address]
|
492
|
+
end
|
493
|
+
end
|
494
|
+
else
|
495
|
+
key = old_vulns_mode.intern
|
496
|
+
old_vulns = state.select{ |asset_id, vulns| vulns.key? key }
|
497
|
+
old_vulns.each do |asset_id, vulns|
|
498
|
+
ip = if vulns.key? :asset
|
499
|
+
vulns[:asset].address
|
500
|
+
else
|
501
|
+
get_asset_ip(asset_id)
|
502
|
+
end
|
503
|
+
csv << [asset_id, '', ip]
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
old_vulns_file.flush
|
509
|
+
{ new_csv: delta_scan_file, old_csv: old_vulns_file }
|
510
|
+
end
|
511
|
+
|
512
|
+
def generate_delta_csv_by_vuln_id(options, close_tickets)
|
513
|
+
nexpose_item = options[:nexpose_item]
|
514
|
+
|
515
|
+
state = generate_delta_scan_data(options)
|
516
|
+
info = create_solution_hash(options, nexpose_item)
|
517
|
+
|
518
|
+
delta_scan_file = Tempfile.new("delta_vulnerable_items_#{nexpose_item}")
|
519
|
+
delta_scan_file.binmode
|
520
|
+
|
521
|
+
old_tickets = []
|
522
|
+
CSV.open(delta_scan_file, 'wb') do |csv|
|
523
|
+
csv << ['vulnerability_id', 'vuln_nexpose_id', 'title', 'cvss_score',
|
524
|
+
'assets', 'description', 'solution_ids', 'references',
|
525
|
+
'comparison']
|
526
|
+
|
527
|
+
state.each do |vuln_id, data|
|
528
|
+
if data.has_key? :old_ticket
|
529
|
+
old_tickets << vuln_id
|
530
|
+
next
|
531
|
+
end
|
532
|
+
|
533
|
+
vuln = data[:vuln]
|
534
|
+
vuln_def = Nexpose::VulnerabilityDefinition.load(@nsc, vuln)
|
535
|
+
|
536
|
+
if data[:new].count > 0
|
537
|
+
assets = []
|
538
|
+
data[:new].each do |asset|
|
539
|
+
assets << "#{asset.id}|#{asset.address}|#{asset.risk_score}"
|
540
|
+
end
|
541
|
+
csv << [vuln_id, vuln, vuln_def.title, vuln_def.cvss_score,
|
542
|
+
assets.join('~'), vuln_def.description,
|
543
|
+
info[vuln_id][:solution_ids], info[vuln_id][:refs], 'New']
|
544
|
+
|
545
|
+
if data[:same].count > 0
|
546
|
+
assets = []
|
547
|
+
data[:same].each do |asset|
|
548
|
+
assets << "#{asset.id}|#{asset.address}|#{asset.risk_score}"
|
549
|
+
end
|
550
|
+
|
551
|
+
csv << [vuln_id, vuln, vuln_def.title, vuln_def.cvss_score,
|
552
|
+
assets.join('~'), '', '', '', 'Same']
|
553
|
+
end
|
554
|
+
elsif data[:same].count > 0
|
555
|
+
assets = []
|
556
|
+
data[:same].each do |asset|
|
557
|
+
assets << "#{asset.id}|#{asset.address}|#{asset.risk_score}"
|
558
|
+
end
|
559
|
+
csv << [vuln_id, vuln, vuln_def.title, vuln_def.cvss_score,
|
560
|
+
assets.join('~'), vuln_def.description,
|
561
|
+
info[vuln_id][:solution_ids], info[vuln_id][:refs], 'Same']
|
562
|
+
end
|
563
|
+
|
564
|
+
if data[:old].count > 0
|
565
|
+
assets = []
|
566
|
+
data[:old].each do |asset|
|
567
|
+
assets << "#{asset[:id]}|#{asset[:ip]}|"
|
568
|
+
end
|
569
|
+
|
570
|
+
csv << [vuln_id, vuln, vuln_def.title, vuln_def.cvss_score,
|
571
|
+
assets.join('~'), '', '', '', 'Old']
|
572
|
+
end
|
573
|
+
end
|
574
|
+
end
|
575
|
+
delta_scan_file.flush
|
576
|
+
|
577
|
+
unless close_tickets
|
578
|
+
return { new_csv: delta_scan_file }
|
579
|
+
end
|
580
|
+
|
581
|
+
old_vulns_file = Tempfile.new("delta_vuln_old_items_#{nexpose_item}")
|
582
|
+
old_vulns_file.binmode
|
583
|
+
|
584
|
+
CSV.open(old_vulns_file, 'wb') do |csv|
|
585
|
+
csv << %w(vulnerability_id)
|
586
|
+
old_tickets.each { |id| csv << [id] }
|
587
|
+
end
|
588
|
+
|
589
|
+
old_vulns_file.flush
|
590
|
+
{ new_csv: delta_scan_file, old_csv: old_vulns_file }
|
591
|
+
end
|
592
|
+
|
593
|
+
# Returns the previous state of a site
|
594
|
+
def get_previous_state(options, query_name)
|
595
|
+
report_config = @report_helper.generate_sql_report_config()
|
596
|
+
report_config.add_filter('version', API_VERSION)
|
597
|
+
report_config.add_filter('query', Queries.send(query_name, options))
|
598
|
+
if options[:tag_run]
|
599
|
+
report_config.add_filter('device', options[:nexpose_item].to_s)
|
600
|
+
else
|
601
|
+
report_config.add_filter('site', options[:nexpose_item])
|
602
|
+
end
|
603
|
+
|
604
|
+
report_config.add_filter('vuln-severity', options[:severity] || 0)
|
605
|
+
|
606
|
+
@report_helper.save_generate_cleanup_report_config(report_config)
|
607
|
+
end
|
608
|
+
|
42
609
|
# Returns an array of all sites in the users environment.
|
43
610
|
#
|
44
611
|
# * *Returns* :
|
@@ -59,7 +626,7 @@ module NexposeTicketing
|
|
59
626
|
def read_last_scans(csv_file_name)
|
60
627
|
file_identifier_histories = Hash.new(-1)
|
61
628
|
CSV.foreach(csv_file_name, headers: true) do |row|
|
62
|
-
|
629
|
+
file_identifier_histories[row[0]] = row[1]
|
63
630
|
end
|
64
631
|
file_identifier_histories
|
65
632
|
end
|
@@ -79,22 +646,35 @@ module NexposeTicketing
|
|
79
646
|
# * *Args* :
|
80
647
|
# - +csv_file_name+ - CSV File name.
|
81
648
|
#
|
82
|
-
def load_last_scans(options = {}
|
83
|
-
|
84
|
-
sites = Array(options[:sites]).map!(&:to_s)
|
85
|
-
tags = Array(options[:tags]).map!(&:to_s)
|
649
|
+
def load_last_scans(options = {})
|
650
|
+
@nsc.login
|
86
651
|
|
87
|
-
if
|
88
|
-
|
89
|
-
tags
|
90
|
-
|
652
|
+
if options[:tag_run]
|
653
|
+
# Need a scan ID for every asset associated with that tag
|
654
|
+
tags = Array(options[:tags]).map!(&:to_s)
|
655
|
+
scan_details = CSV.generate do |csv|
|
656
|
+
csv << %w(tag_id asset_id last_scan_id scan_finished)
|
657
|
+
|
658
|
+
tags.each do |t|
|
659
|
+
assets = Nexpose::Tag.load(@nsc, t).asset_ids
|
660
|
+
assets.each do |a|
|
661
|
+
latest = @nsc.asset_scan_history(a).max_by { |s| s.scan_id }
|
662
|
+
csv << [t, a, latest.scan_id, latest.end_time]
|
663
|
+
end
|
664
|
+
end
|
91
665
|
end
|
92
666
|
else
|
93
|
-
|
667
|
+
sites = Array(options[:sites]).map!(&:to_s)
|
668
|
+
scan_details = CSV.generate do |csv|
|
669
|
+
csv << %w(site_id last_scan_id finished)
|
670
|
+
sites.each do |s|
|
671
|
+
scan = @nsc.last_scan(s)
|
672
|
+
csv << [s, scan.scan_id, scan.end_time]
|
673
|
+
end
|
674
|
+
end
|
94
675
|
end
|
95
676
|
|
96
|
-
|
97
|
-
csv_output = CSV.parse(report_output.chomp, headers: :first_row)
|
677
|
+
csv_output = CSV.parse(scan_details.chomp, headers: :first_row)
|
98
678
|
|
99
679
|
#We only care about sites we are monitoring.
|
100
680
|
trimmed_csv = []
|
@@ -125,11 +705,9 @@ module NexposeTicketing
|
|
125
705
|
trimmed_csv << CSV::Row.new('tag_id,last_scan_fingerprint'.split(','), "#{current_tag_id},#{Digest::MD5::hexdigest(tag_finger_print)}".split(','))
|
126
706
|
end
|
127
707
|
else
|
128
|
-
trimmed_csv <<
|
129
|
-
csv_output.each
|
130
|
-
|
131
|
-
trimmed_csv << row
|
132
|
-
end
|
708
|
+
trimmed_csv << scan_details.lines.first
|
709
|
+
csv_output.each do |row|
|
710
|
+
trimmed_csv << row
|
133
711
|
end
|
134
712
|
end
|
135
713
|
|
@@ -149,7 +727,7 @@ module NexposeTicketing
|
|
149
727
|
if options[:vulnerabilityCategories].nil? || options[:vulnerabilityCategories].empty?
|
150
728
|
return nil
|
151
729
|
end
|
152
|
-
|
730
|
+
|
153
731
|
filter = options[:vulnerabilityCategories].strip.split(',')
|
154
732
|
filter.map { |category| "include:#{category}" }.join(',')
|
155
733
|
end
|
@@ -162,38 +740,19 @@ module NexposeTicketing
|
|
162
740
|
file_identifier_histories
|
163
741
|
end
|
164
742
|
|
165
|
-
def generate_tag_asset_list(options = {}
|
166
|
-
|
167
|
-
tags = Array(options[:tags])
|
168
|
-
report_config.add_filter('query', Queries.last_tag_scans)
|
169
|
-
tags.each { |tag| report_config.add_filter('tag', tag) }
|
170
|
-
|
171
|
-
report_output = report_config.generate(@nsc, @timeout)
|
172
|
-
csv_output = CSV.parse(report_output.chomp, headers: :first_row)
|
173
|
-
trimmed_csv = []
|
174
|
-
trimmed_csv << 'asset_id, last_scan_id'
|
175
|
-
current_tag_id = nil
|
176
|
-
csv_output.each do |row|
|
177
|
-
if (tags.include? row[0].to_s) && (row[0].to_i != current_tag_id)
|
178
|
-
if(current_tag_id.nil?)
|
179
|
-
#Initial run
|
180
|
-
current_tag_id = row[0].to_i
|
181
|
-
else
|
182
|
-
#New tag ID, finish off the previous tag asset list and start on the new one
|
183
|
-
save_to_file(options[:csv_file], trimmed_csv)
|
184
|
-
current_tag_id = row[0].to_i
|
185
|
-
trimmed_csv = []
|
743
|
+
def generate_tag_asset_list(options = {})
|
744
|
+
tags = Array(options[:tags]).map!(&:to_s)
|
186
745
|
|
187
|
-
|
188
|
-
|
189
|
-
end
|
190
|
-
end
|
746
|
+
tags.each do |t|
|
747
|
+
trimmed_csv = ['asset_id, last_scan_id']
|
191
748
|
|
192
|
-
|
193
|
-
|
749
|
+
assets = Nexpose::Tag.load(@nsc, t).asset_ids
|
750
|
+
assets.each do |a|
|
751
|
+
scan = @nsc.asset_scan_history(a).max_by { |s| s.scan_id }
|
752
|
+
trimmed_csv << "#{a},#{scan.scan_id}"
|
194
753
|
end
|
754
|
+
save_to_file(options[:csv_file], trimmed_csv)
|
195
755
|
end
|
196
|
-
save_to_file(options[:csv_file], trimmed_csv) if trimmed_csv.any?
|
197
756
|
end
|
198
757
|
|
199
758
|
# Saves CSV scan information to disk
|
@@ -203,7 +762,7 @@ module NexposeTicketing
|
|
203
762
|
#
|
204
763
|
def save_to_file(csv_file_name, trimmed_csv, saved_file = nil)
|
205
764
|
unless saved_file.nil?
|
206
|
-
saved_file.open(csv_file_name, 'w') { |file| file.puts(trimmed_csv) }
|
765
|
+
saved_file.open(csv_file_name, 'w') { |file| file.puts(trimmed_csv) }
|
207
766
|
return
|
208
767
|
end
|
209
768
|
|
@@ -227,12 +786,13 @@ module NexposeTicketing
|
|
227
786
|
end
|
228
787
|
|
229
788
|
def request_query(query_name, options = {}, nexpose_items = nil)
|
230
|
-
items =
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
789
|
+
items = if nexpose_items
|
790
|
+
Array(nexpose_items)
|
791
|
+
elsif options[:nexpose_item]
|
792
|
+
nil
|
793
|
+
else
|
794
|
+
options["#{options[:scan_mode]}s".intern]
|
795
|
+
end
|
236
796
|
|
237
797
|
report_config = generate_config(query_name, options, items)
|
238
798
|
end
|
@@ -250,14 +810,20 @@ module NexposeTicketing
|
|
250
810
|
report_config.add_filter('version', '1.2.0')
|
251
811
|
report_config.add_filter('query', Queries.send(query_name, options))
|
252
812
|
|
253
|
-
id_type = options[:
|
813
|
+
id_type = if options[:report_type].to_s == 'initial'
|
814
|
+
'tag'
|
815
|
+
elsif options[:tag_run]
|
816
|
+
'device'
|
817
|
+
else
|
818
|
+
'site'
|
819
|
+
end
|
254
820
|
|
255
821
|
if nexpose_items != nil && !nexpose_items.empty?
|
256
822
|
nexpose_items.each { |id| report_config.add_filter(id_type, id) }
|
257
823
|
else
|
258
824
|
item = options[:tag_run] ? options[:tag] : nexpose_item
|
259
825
|
report_config.add_filter(id_type, item)
|
260
|
-
end
|
826
|
+
end
|
261
827
|
|
262
828
|
report_config.add_filter('vuln-severity', options[:severity] || 0)
|
263
829
|
|
@@ -272,12 +838,12 @@ module NexposeTicketing
|
|
272
838
|
|
273
839
|
def method_missing(name, *args, &block)
|
274
840
|
full_method_name = "#{name}#{@method_suffix}"
|
275
|
-
|
841
|
+
|
276
842
|
unless Queries.respond_to? full_method_name
|
277
843
|
fail %Q{Query request "#{full_method_name}" not understood}
|
278
844
|
end
|
279
845
|
|
280
|
-
|
846
|
+
log_message %Q{Creating query request "#{full_method_name}".}
|
281
847
|
request_query(full_method_name, args[0], args[1])
|
282
848
|
end
|
283
849
|
|