nexpose_ticketing 1.3.0 → 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|