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.
@@ -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
- summary = row['solutions'].to_s
126
- delimiter = summary.index('|')
127
- return summary[summary.index(':')+1...delimiter].strip if delimiter
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['solutions'].to_s.gsub('|', "\n").gsub('~', "\n--\n")
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
- assets << " - #{asset[1]} #{"\t(#{asset[2]})" if !asset[2].empty?}\n"
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
- ['ip_address', 'vulnerability_id']
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 = ['header', 'references', 'solutions']
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
- riskString = ""
18
- else
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
- assetString = "WHERE asset_id = #{options[:nexpose_item]}"
32
+ "WHERE asset_id = #{options[:nexpose_item]}"
36
33
  else
37
- assetString = ""
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.all_new_vulns_by_ip(options = {})
66
- "SELECT DISTINCT on (da.ip_address, subs.vulnerability_id) subs.asset_id, da.ip_address, da.host_name, subs.current_scan,
67
- subs.vulnerability_id, dv.nexpose_id as vuln_nexpose_id,
68
- string_agg(DISTINCT 'Summary: ' || coalesce(ds.summary, 'None') ||
69
- '|Nexpose ID: ' || ds.nexpose_id ||
70
- '|Fix: ' || coalesce(proofAsText(ds.fix), 'None') ||
71
- '|URL: ' || coalesce(ds.url, 'None'), '~') as solutions,
72
- fa.riskscore, dv.cvss_score,
73
- string_agg(DISTINCT dvr.source || ': ' || dvr.reference, ', ') as references,
74
- fasva.first_discovered, fasva.most_recently_discovered
75
- FROM (SELECT fasv.asset_id, fasv.vulnerability_id, s.current_scan
76
- FROM fact_asset_scan_vulnerability_finding fasv
77
- JOIN
78
- (
79
- SELECT asset_id, previousScan(asset_id) AS baseline_scan, lastScan(asset_id) AS current_scan
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.all_new_vulns_by_vuln_id(options = {})
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("%H:%M:%S")
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], nexpose_data[:nxuser], nexpose_data[:nxpasswd])
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
- file_identifier_histories[row[0]] = row[1]
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 = {}, report_config = Nexpose::AdhocReportConfig.new(nil, 'sql'))
83
- report_config.add_filter('version', '1.2.0')
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(options[:tag_run])
88
- report_config.add_filter('query', Queries.last_tag_scans)
89
- tags.each do |tag|
90
- report_config.add_filter('tag', tag)
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
- report_config.add_filter('query', Queries.last_scans)
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
- report_output = report_config.generate(@nsc, @timeout)
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 << report_output.lines.first
129
- csv_output.each do |row|
130
- if sites.include? row[0].to_s
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 = {}, report_config = Nexpose::AdhocReportConfig.new(nil, 'sql'))
166
- report_config.add_filter('version', '1.2.0')
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
- # TODO: test this change
188
- trimmed_csv << 'asset_id, last_scan_id'
189
- end
190
- end
746
+ tags.each do |t|
747
+ trimmed_csv = ['asset_id, last_scan_id']
191
748
 
192
- if(current_tag_id == row[0].to_i)
193
- trimmed_csv << "#{row[1].to_s},#{row[2].to_s}"
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
- if nexpose_items
232
- Array(nexpose_items)
233
- else
234
- options[:nexpose_item] ? nil : options["#{options[:scan_mode]}s".intern]
235
- end
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[:tag_run] ? 'tag' : 'site'
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
- @log.log_message %Q{Creating query request "#{full_method_name}".}
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