nexpose_ticketing 1.0.2 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,199 @@
1
+ require 'nexpose_ticketing/nx_logger'
2
+
3
+ class BaseMode
4
+
5
+ # Initializes the mode
6
+ def initialize(options)
7
+ @options = options
8
+ @log = NexposeTicketing::NxLogger.instance
9
+ end
10
+
11
+ # True if this mode supports ticket updates
12
+ def updates_supported?
13
+ true
14
+ end
15
+
16
+ # Returns the fields used to identify individual tickets
17
+ def get_matching_fields
18
+ ['']
19
+ end
20
+
21
+ # Returns the ticket's title
22
+ def get_title(row)
23
+ "#{nil} => #{nil}"
24
+ end
25
+
26
+ # Generates a unique identifier for a ticket
27
+ def get_nxid(nexpose_id, row)
28
+ "#{nil}c#{nil}"
29
+ end
30
+
31
+ # Returns the base ticket description object
32
+ def get_description(nexpose_id, row)
33
+ description
34
+ end
35
+
36
+ # Updates the ticket description based on row data
37
+ def update_description(description, row)
38
+ description
39
+ end
40
+
41
+ # Converts the ticket description object into a formatted string
42
+ def print_description(description)
43
+ ''
44
+ end
45
+
46
+ # Cuts the title down to size specified in config, if necessary
47
+ def truncate_title(title)
48
+ return title if title.length <= @options[:max_title_length]
49
+ "#{title[0, @options[:max_title_length]-3]}..."
50
+ end
51
+
52
+ # Returns the suffix used for query method names
53
+ def get_query_suffix
54
+ '_by_ip'
55
+ end
56
+
57
+ def load_queries
58
+ file_name = "#{self.class.to_s.downcase}_queries.rb"
59
+ file_path = File.join(File.dirname(__FILE__), "../queries/#{file_name}")
60
+ @queries = []
61
+
62
+ @queries << YAML.load_file(file_path)
63
+ end
64
+
65
+ # Generates a final description string based on a description hash.
66
+ #
67
+ # - +ticket_desc+ - The ticket description to be formatted.
68
+ # - +nxid+ - The NXID to be appended to the ticket.
69
+ #
70
+ # * *Returns* :
71
+ # - String containing ticket description text.
72
+ #
73
+ def finalize_description(ticket_desc, nxid)
74
+ nxid_line = "\n\n\n#{nxid}"
75
+
76
+ #If the ticket is too long, truncate it to fit the NXID
77
+ max_len = @options[:max_ticket_length]
78
+ if max_len > 0 and (ticket_desc + nxid_line).length > max_len
79
+ #Leave space for newline characters, nxid and ellipsis (...)
80
+ ticket_desc = ticket_desc[0...max_len - (nxid_line.length+5)]
81
+ ticket_desc << "\n...\n"
82
+ end
83
+
84
+ "#{ticket_desc}#{nxid_line}"
85
+ end
86
+
87
+ # Formats the row data to be inserted into a 'D' or 'I' mode ticket description.
88
+ #
89
+ # - +row+ - CSV row containing vulnerability data.
90
+ #
91
+ # * *Returns* :
92
+ # - String formatted with vulnerability data.
93
+ #
94
+ def get_vuln_info(row)
95
+ ticket = get_header(row)
96
+ ticket << get_discovery_info(row)
97
+ ticket << get_references(row)
98
+ ticket << "\n#{get_solutions(row)}"
99
+ ticket.gsub("\n", "\n ")
100
+ end
101
+
102
+ # Generates the vulnerability header from the row data.
103
+ #
104
+ # - +row+ - CSV row containing vulnerability data.
105
+ #
106
+ # * *Returns* :
107
+ # - String formatted with vulnerability data.
108
+ #
109
+ def get_header(row)
110
+ ticket = "\n=============================="
111
+ ticket << "\nVulnerability ID: #{row['vulnerability_id']}"
112
+ ticket << "\nNexpose ID: #{row['vuln_nexpose_id']}"
113
+ ticket << "\nCVSS Score: #{row['cvss_score']}"
114
+ ticket << "\n=============================="
115
+ end
116
+
117
+ # Generates a short summary for a vulnerability.
118
+ #
119
+ # - +row+ - CSV row containing vulnerability data.
120
+ #
121
+ # * *Returns* :
122
+ # - String containing a short summary of the vulnerability.
123
+ #
124
+ 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
128
+ summary.length <= 100 ? summary : summary[0...100]
129
+ end
130
+
131
+ # Formats the solutions for a vulnerability in a format suitable to be inserted into a ticket.
132
+ #
133
+ # - +row+ - CSV row containing vulnerability data.
134
+ #
135
+ # * *Returns* :
136
+ # - String formatted with solution information.
137
+ #
138
+ def get_solutions(row)
139
+ row['solutions'].to_s.gsub('|', "\n").gsub('~', "\n--\n")
140
+ end
141
+
142
+ def get_discovery_info(row)
143
+ return '' if row['first_discovered'].to_s == ""
144
+ info = "\nFirst Seen: #{row['first_discovered']}\n"
145
+ info << "Last Seen: #{row['most_recently_discovered']}\n"
146
+ info
147
+ end
148
+
149
+ # Formats the references for a vulnerability in a format suitable to be inserted into a ticket.
150
+ #
151
+ # - +row+ - CSV row containing vulnerability data.
152
+ #
153
+ # * *Returns* :
154
+ # - String formatted with source and reference.
155
+ #
156
+ def get_references(row)
157
+ num_refs = @options[:max_num_refs]
158
+ return '' if row['references'].nil? || num_refs == 0
159
+
160
+ refs = row['references'].split(', ')[0..num_refs]
161
+ refs[num_refs] = '...' if refs.count > num_refs
162
+ "\nSources:\n#{refs.map { |r| " - #{r}" }.join("\n")}\n"
163
+ end
164
+
165
+
166
+ # Returns the assets for a vulnerability in a format suitable to be inserted into a ticket.
167
+ #
168
+ # - +row+ - CSV row containing vulnerability data.
169
+ #
170
+ # * *Returns* :
171
+ # - String formatted with affected assets.
172
+ #
173
+ def get_assets(row)
174
+ assets = "\n#{row['comparison'] || 'Affected' } Assets\n"
175
+
176
+ row['assets'].to_s.split('~').each do |a|
177
+ asset = a.split('|')
178
+ assets << " - #{asset[1]} #{"\t(#{asset[2]})" if !asset[2].empty?}\n"
179
+ end
180
+ assets
181
+ end
182
+
183
+ # Returns the relevant row values for printing.
184
+ #
185
+ # - +fields+ - The fields which are relevant to the ticket.
186
+ # - +row+ - CSV row containing vulnerability data.
187
+ #
188
+ # * *Returns* :
189
+ # - String formatted with relevant fields.
190
+ #
191
+ def get_field_info(fields, row)
192
+ fields.map { |x| "#{x.gsub("_", " ")}: #{row[x]}" }.join(", ")
193
+ end
194
+
195
+ # Catch-all method when a unknown method is called
196
+ def method_missing(name, *args)
197
+ @log.log_message("Method #{name} not implemented for #{@options[:ticket_mode]} mode.")
198
+ end
199
+ end
@@ -0,0 +1,50 @@
1
+ require_relative './base_mode.rb'
2
+
3
+ class DefaultMode < BaseMode
4
+
5
+ # Initializes the mode
6
+ def initialize(options)
7
+ super(options)
8
+ end
9
+
10
+ # True if this mode supports ticket updates
11
+ def updates_supported?
12
+ false
13
+ end
14
+
15
+ # Returns the fields used to identify individual tickets
16
+ def get_matching_fields
17
+ ['ip_address', 'vulnerability_id']
18
+ end
19
+
20
+ # Returns the ticket's title
21
+ def get_title(row)
22
+ truncate_title "#{row['ip_address']} => #{get_short_summary(row)}"
23
+ end
24
+
25
+ # Generates a unique identifier for a ticket
26
+ def get_nxid(nexpose_id, row)
27
+ "#{nexpose_id}d#{row['asset_id']}d#{row['vulnerability_id']}"
28
+ end
29
+
30
+ # Returns the base ticket description object
31
+ def get_description(nexpose_id, row)
32
+ description = { nxid: "NXID: #{get_nxid(nexpose_id, row)}" }
33
+ fields = ['header', 'references', 'solutions']
34
+ fields.each { |f| description[f.intern] = self.send("get_#{f}", row) }
35
+ description[:header] << get_discovery_info(row)
36
+ description
37
+ end
38
+
39
+ # Updates the ticket description based on row data
40
+ def update_description(description, row)
41
+ description
42
+ end
43
+
44
+ # Converts the ticket description object into a formatted string
45
+ def print_description(description)
46
+ fields = [:header, :references, :solutions].map { |f| description[f] }
47
+ finalize_description(fields.join("\n"),
48
+ description[:nxid])
49
+ end
50
+ end
@@ -0,0 +1,52 @@
1
+ require_relative './base_mode.rb'
2
+
3
+ class IPMode < BaseMode
4
+
5
+ # Initializes the mode
6
+ def initialize(options)
7
+ super(options)
8
+ end
9
+
10
+ # Returns the fields used to identify individual tickets
11
+ def get_matching_fields
12
+ ['ip_address']
13
+ end
14
+
15
+ # Returns the ticket's title
16
+ def get_title(row)
17
+ truncate_title "#{row['ip_address']} => Vulnerabilities"
18
+ end
19
+
20
+ # Generates a unique identifier for a ticket
21
+ def get_nxid(nexpose_id, row)
22
+ "#{nexpose_id}i#{row['ip_address']}"
23
+ end
24
+
25
+ # Returns the base ticket description object
26
+ def get_description(nexpose_id, row)
27
+ description = { nxid: "NXID: #{get_nxid(nexpose_id, row)}" }
28
+ status = row['comparison']
29
+ description[:ticket_status] = status
30
+ header = "++ #{status} Vulnerabilities ++\n" if !status.nil?
31
+ description[:vulnerabilities] = [ header.to_s + get_vuln_info(row) ]
32
+ description
33
+ end
34
+
35
+ # Updates the ticket description based on row data
36
+ def update_description(description, row)
37
+ header = ""
38
+ if description[:ticket_status] != row['comparison']
39
+ header = "++ #{row['comparison']} Vulnerabilities ++\n"
40
+ description[:ticket_status] = row['comparison']
41
+ end
42
+
43
+ description[:vulnerabilities] << "#{header}#{get_vuln_info(row)}"
44
+ description
45
+ end
46
+
47
+ # Converts the ticket description object into a formatted string
48
+ def print_description(description)
49
+ finalize_description(description[:vulnerabilities].join("\n"),
50
+ description[:nxid])
51
+ end
52
+ end
@@ -0,0 +1,50 @@
1
+ require_relative './base_mode.rb'
2
+
3
+ class VulnerabilityMode < BaseMode
4
+
5
+ # Initializes the mode
6
+ def initialize(options)
7
+ super(options)
8
+ end
9
+
10
+ # Returns the fields used to identify individual tickets
11
+ def get_matching_fields
12
+ ['vulnerability_id']
13
+ end
14
+
15
+ # Returns the ticket's title
16
+ def get_title(row)
17
+ truncate_title "Vulnerability: #{row['title']}"
18
+ end
19
+
20
+ # Generates a unique identifier for a ticket
21
+ def get_nxid(nexpose_id, row)
22
+ "#{nexpose_id}v#{row['vulnerability_id']}"
23
+ end
24
+
25
+ # Returns the suffix used for query method names
26
+ def get_query_suffix
27
+ '_by_vuln_id'
28
+ end
29
+
30
+ # Returns the base ticket description object
31
+ def get_description(nexpose_id, row)
32
+ description = { nxid: "NXID: #{get_nxid(nexpose_id, row)}" }
33
+ fields = ['header', 'references', 'solutions', 'assets']
34
+ fields.each { |f| description[f.intern] = self.send("get_#{f}", row) }
35
+ description
36
+ end
37
+
38
+ # Updates the ticket description based on row data
39
+ def update_description(description, row)
40
+ description[:assets] += "\n#{get_assets(row)}"
41
+ description
42
+ end
43
+
44
+ # Converts the ticket description object into a formatted string
45
+ def print_description(description)
46
+ fields = [:header, :assets, :references, :solutions]
47
+ finalize_description(fields.map { |f| description[f] }.join("\n"),
48
+ description[:nxid])
49
+ end
50
+ end
@@ -6,27 +6,23 @@ require 'singleton'
6
6
  module NexposeTicketing
7
7
  class NxLogger
8
8
  include Singleton
9
- attr_accessor :options, :statistic_key, :product, :logger_file
10
9
  LOG_PATH = "./logs/rapid7_%s.log"
11
10
  KEY_FORMAT = "external.integration.%s"
12
11
  PRODUCT_FORMAT = "%s_%s"
13
12
 
14
13
  DEFAULT_LOG = 'integration'
15
- PRODUCT_RANGE = 3..30
14
+ PRODUCT_RANGE = 4..30
16
15
  KEY_RANGE = 3..15
17
16
 
18
17
  ENDPOINT = '/data/external/statistic/'
19
18
 
20
19
  def initialize()
21
- @logger_file = get_log_path product
20
+ create_calls
21
+ @logger_file = get_log_path @product
22
22
  setup_logging(true, 'info')
23
23
  end
24
24
 
25
25
  def setup_statistics_collection(vendor, product_name, gem_version)
26
- #Remove illegal characters
27
- vendor.to_s.gsub!('-', '_')
28
- product_name.to_s.gsub!('-', '_')
29
-
30
26
  begin
31
27
  @statistic_key = get_statistic_key vendor
32
28
  @product = get_product product_name, gem_version
@@ -35,13 +31,14 @@ module NexposeTicketing
35
31
  end
36
32
  end
37
33
 
38
- def setup_logging(enabled, log_level = 'info')
39
- unless enabled || @log.nil?
40
- log_message('Logging disabled.')
41
- return
42
- end
34
+ def setup_logging(enabled, log_level = 'info', stdout=false)
35
+ @stdout = stdout
43
36
 
44
- @logger_file = get_log_path product
37
+ log_message('Logging disabled.') unless enabled || @log.nil?
38
+ @enabled = enabled
39
+ return unless @enabled
40
+
41
+ @logger_file = get_log_path @product
45
42
 
46
43
  require 'logger'
47
44
  directory = File.dirname(@logger_file)
@@ -58,24 +55,19 @@ module NexposeTicketing
58
55
  log_message("Logging enabled at level <#{log_level}>")
59
56
  end
60
57
 
61
- # Logs an info message
62
- def log_message(message)
63
- @log.info(message) unless @log.nil?
64
- end
65
-
66
- # Logs a debug message
67
- def log_debug_message(message)
68
- @log.debug(message) unless @log.nil?
69
- end
70
-
71
- # Logs an error message
72
- def log_error_message(message)
73
- @log.error(message) unless @log.nil?
58
+ def create_calls
59
+ levels = [:info, :debug, :error, :warn]
60
+ levels.each do |level|
61
+ method_name =
62
+ define_singleton_method("log_#{level.to_s}_message") do |message|
63
+ puts message if @stdout
64
+ @log.send(level, message) unless !@enabled || @log.nil?
65
+ end
66
+ end
74
67
  end
75
68
 
76
- # Logs a warn message
77
- def log_warn_message(message)
78
- @log.warn(message) unless @log.nil?
69
+ def log_message(message)
70
+ log_info_message message
79
71
  end
80
72
 
81
73
  def log_stat_message(message)
@@ -92,13 +84,28 @@ module NexposeTicketing
92
84
  return nil
93
85
  end
94
86
 
87
+ vendor.gsub!('-', '_')
88
+ vendor.slice! vendor.rindex('_') until vendor.count('_') <= 1
89
+
90
+ vendor.delete! "^A-Za-z0-9\_"
91
+
95
92
  KEY_FORMAT % vendor[0...KEY_RANGE.max].downcase
96
93
  end
97
94
 
98
95
  def get_product(product, version)
99
- return nil if (product.nil? || version.nil?)
96
+ return nil if ((product.nil? || product.empty?) ||
97
+ (version.nil? || version.empty?))
98
+
99
+ product.gsub!('-', '_')
100
+ product.slice! product.rindex('_') until product.count('_') <= 1
101
+
102
+ product.delete! "^A-Za-z0-9\_"
103
+ version.delete! "^A-Za-z0-9\.\-"
104
+
100
105
  product = (PRODUCT_FORMAT % [product, version])[0...PRODUCT_RANGE.max]
101
106
 
107
+ product.slice! product.rindex(/[A-Z0-9]/i)+1..-1
108
+
102
109
  if product.length < PRODUCT_RANGE.min
103
110
  log_stat_message("Product length below minimum <#{PRODUCT_RANGE.min}>.")
104
111
  return nil
@@ -107,9 +114,12 @@ module NexposeTicketing
107
114
  end
108
115
 
109
116
  def generate_payload(statistic_value='')
110
- payload = {'statistic-key' => @statistic_key,
111
- 'statistic-value' => statistic_value,
112
- 'product' => @product}
117
+ product_name, separator, version = @product.to_s.rpartition('_')
118
+ payload_value = {'version' => version}.to_json
119
+
120
+ payload = {'statistic-key' => @statistic_key.to_s,
121
+ 'statistic-value' => payload_value,
122
+ 'product' => product_name}
113
123
  JSON.generate(payload)
114
124
  end
115
125
 
@@ -126,6 +136,7 @@ module NexposeTicketing
126
136
  log_stat_message "Received code #{response.code} from Nexpose console."
127
137
  log_stat_message "Received message #{response.msg} from Nexpose console."
128
138
  log_stat_message 'Finished sending statistics data to Nexpose.'
139
+
129
140
  response.code
130
141
  end
131
142
 
@@ -4,9 +4,8 @@ module NexposeTicketing
4
4
  # for Nexpose.
5
5
  # Copyright:: Copyright (c) 2014 Rapid7, LLC.
6
6
  module Queries
7
-
8
7
  # Formats SQL query for riskscore based on user config options.
9
- #
8
+ #
10
9
  # * *Args* :
11
10
  # - +riskScore+ - riskscore for assets to match in results of query.
12
11
  #
@@ -63,8 +62,9 @@ module NexposeTicketing
63
62
  # -Returns |asset_id| |ip_address| |current_scan| |vulnerability_id||solution_id| |nexpose_id|
64
63
  # |url| |summary| |fix|
65
64
  #
66
- def self.all_new_vulns(options = {})
67
- "SELECT DISTINCT on (da.ip_address, subs.vulnerability_id) subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id,
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
68
  string_agg(DISTINCT 'Summary: ' || coalesce(ds.summary, 'None') ||
69
69
  '|Nexpose ID: ' || ds.nexpose_id ||
70
70
  '|Fix: ' || coalesce(proofAsText(ds.fix), 'None') ||
@@ -90,7 +90,7 @@ module NexposeTicketing
90
90
  JOIN dim_asset da ON subs.asset_id = da.asset_id
91
91
  JOIN fact_asset fa ON fa.asset_id = da.asset_id
92
92
  #{createRiskString( options[:riskScore])}
93
- GROUP BY subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id,
93
+ GROUP BY subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id, dv.nexpose_id,
94
94
  fa.riskscore, dv.cvss_score, fasva.first_discovered, fasva.most_recently_discovered
95
95
  ORDER BY da.ip_address, subs.vulnerability_id"
96
96
  end
@@ -102,7 +102,7 @@ module NexposeTicketing
102
102
  # |url| |summary| |fix|
103
103
  #
104
104
  def self.all_new_vulns_by_vuln_id(options = {})
105
- "SELECT DISTINCT on (subs.vulnerability_id) subs.vulnerability_id, dv.title, MAX(dv.cvss_score) as cvss_score,
105
+ "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
106
  string_agg(DISTINCT subs.asset_id ||
107
107
  '|' || da.ip_address ||
108
108
  '|' || coalesce(da.host_name, '') ||
@@ -132,7 +132,7 @@ module NexposeTicketing
132
132
  JOIN fact_asset_vulnerability_age fasva ON subs.vulnerability_id = fasva.vulnerability_id AND subs.asset_id = fasva.asset_id
133
133
  #{createRiskString(options[:riskScore])}
134
134
 
135
- GROUP BY subs.vulnerability_id, dv.title
135
+ GROUP BY subs.vulnerability_id, dv.title, dv.nexpose_id
136
136
  ORDER BY subs.vulnerability_id"
137
137
  end
138
138
 
@@ -145,8 +145,9 @@ module NexposeTicketing
145
145
  # - Returns |asset_id| |ip_address| |current_scan| |vulnerability_id| |solution_id| |nexpose_id|
146
146
  # |url| |summary| |fix|
147
147
  #
148
- def self.new_vulns_since_scan(options = {})
149
- "SELECT DISTINCT on (da.ip_address, subs.vulnerability_id) subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id,
148
+ def self.new_vulns_since_scan_by_ip(options = {})
149
+ "SELECT DISTINCT on (da.ip_address, subs.vulnerability_id) subs.asset_id, da.ip_address, da.host_name, subs.current_scan,
150
+ subs.vulnerability_id, dv.nexpose_id as vuln_nexpose_id,
150
151
  string_agg(DISTINCT 'Summary: ' || coalesce(ds.summary, 'None') ||
151
152
  '|Nexpose ID: ' || ds.nexpose_id ||
152
153
  '|Fix: ' || coalesce(proofAsText(ds.fix), 'None') ||
@@ -173,7 +174,7 @@ module NexposeTicketing
173
174
  AND subs.current_scan > #{options[:scan_id]}
174
175
  JOIN fact_asset fa ON fa.asset_id = da.asset_id
175
176
  #{createRiskString(options[:riskScore])}
176
- GROUP BY subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id,
177
+ GROUP BY subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id, dv.nexpose_id,
177
178
  fa.riskscore, dv.cvss_score, fasva.first_discovered, fasva.most_recently_discovered
178
179
  ORDER BY da.ip_address, subs.vulnerability_id"
179
180
  end
@@ -188,8 +189,9 @@ module NexposeTicketing
188
189
  # - Returns |asset_id| |ip_address| |current_scan| |vulnerability_id| |solution_id| |nexpose_id|
189
190
  # |url| |summary| |fix|
190
191
  #
191
- def self.new_vulns_by_vuln_id_since_scan(options = {})
192
- "SELECT DISTINCT on (subs.vulnerability_id) subs.vulnerability_id, dv.title, MAX(dv.cvss_score) as cvss_score,
192
+ def self.new_vulns_since_scan_by_vuln_id(options = {})
193
+ "SELECT DISTINCT on (subs.vulnerability_id) subs.vulnerability_id, dv.nexpose_id as vuln_nexpose_id,
194
+ dv.title, MAX(dv.cvss_score) as cvss_score,
193
195
  string_agg(DISTINCT subs.asset_id ||
194
196
  '|' || da.ip_address ||
195
197
  '|' || coalesce(da.host_name, '') ||
@@ -219,7 +221,7 @@ module NexposeTicketing
219
221
  AND subs.current_scan > #{options[:scan_id]}
220
222
  JOIN fact_asset fa ON fa.asset_id = da.asset_id
221
223
  #{createRiskString(options[:riskScore])}
222
- GROUP BY subs.vulnerability_id, dv.title
224
+ GROUP BY subs.vulnerability_id, dv.title, dv.nexpose_id
223
225
  ORDER BY vulnerability_id"
224
226
  end
225
227
 
@@ -266,8 +268,9 @@ module NexposeTicketing
266
268
  # - Returns |asset_id| |ip_address| |current_scan| |vulnerability_id| |solution_id| |nexpose_id|
267
269
  # |url| |summary| |fix| |comparison|
268
270
  #
269
- def self.all_vulns_since_scan(options = {})
270
- "SELECT DISTINCT on (da.ip_address, subs.vulnerability_id) subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id,
271
+ def self.all_vulns_since_scan_by_ip(options = {})
272
+ "SELECT DISTINCT on (da.ip_address, subs.vulnerability_id) subs.asset_id, da.ip_address, da.host_name, subs.current_scan,
273
+ subs.vulnerability_id, dv.nexpose_id as vuln_nexpose_id,
271
274
  string_agg(DISTINCT 'Summary: ' || coalesce(ds.summary, 'None') ||
272
275
  '|Nexpose ID: ' || ds.nexpose_id ||
273
276
  '|Fix: ' || coalesce(proofAsText(ds.fix), 'None') ||
@@ -293,12 +296,11 @@ module NexposeTicketing
293
296
  JOIN fact_asset fa ON fa.asset_id = subs.asset_id
294
297
  #{createRiskString(options[:riskScore])}
295
298
  AND subs.current_scan > #{options[:scan_id]}
296
- GROUP BY subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id,
299
+ GROUP BY subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id, dv.nexpose_id,
297
300
  fa.riskscore, dv.cvss_score, subs.comparison
298
-
299
301
  UNION
300
-
301
- SELECT DISTINCT on (da.ip_address, subs.vulnerability_id) subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id,
302
+ SELECT DISTINCT on (da.ip_address, subs.vulnerability_id) subs.asset_id, da.ip_address, da.host_name, subs.current_scan,
303
+ subs.vulnerability_id, dv.nexpose_id as vuln_nexpose_id,
302
304
  string_agg(DISTINCT 'Summary: ' || coalesce(ds.summary, 'None') ||
303
305
  '|Nexpose ID: ' || ds.nexpose_id ||
304
306
  '|Fix: ' || coalesce(proofAsText(ds.fix), 'None') ||
@@ -327,9 +329,8 @@ module NexposeTicketing
327
329
  JOIN fact_asset fa ON fa.asset_id = subs.asset_id
328
330
  #{createRiskString(options[:riskScore])}
329
331
  AND subs.current_scan > #{options[:scan_id]}
330
- GROUP BY subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id,
332
+ GROUP BY subs.asset_id, da.ip_address, da.host_name, subs.current_scan, subs.vulnerability_id, dv.nexpose_id,
331
333
  fa.riskscore, dv.cvss_score, fasva.first_discovered, fasva.most_recently_discovered, subs.comparison
332
-
333
334
  ORDER BY ip_address, comparison"
334
335
  end
335
336
 
@@ -343,8 +344,9 @@ module NexposeTicketing
343
344
  # - Returns |asset_id| |ip_address| |current_scan| |vulnerability_id| |solution_id| |nexpose_id|
344
345
  # |url| |summary| |fix| |comparison|
345
346
  #
346
- def self.all_vulns_by_vuln_id_since_scan(options = {})
347
- "SELECT DISTINCT on (subs.vulnerability_id, subs.comparison) subs.vulnerability_id, dv.title, MAX(dv.cvss_score) as cvss_score,
347
+ def self.all_vulns_since_scan_by_vuln_id(options = {})
348
+ "SELECT DISTINCT on (subs.vulnerability_id, subs.comparison) subs.vulnerability_id, dv.nexpose_id as vuln_nexpose_id,
349
+ dv.title, MAX(dv.cvss_score) as cvss_score,
348
350
  string_agg(DISTINCT subs.asset_id ||
349
351
  '|' || da.ip_address ||
350
352
  '|' || coalesce(da.host_name, '') ||
@@ -374,11 +376,12 @@ module NexposeTicketing
374
376
  JOIN fact_asset fa ON fa.asset_id = subs.asset_id
375
377
  #{createRiskString(options[:riskScore])}
376
378
  AND subs.current_scan > #{options[:scan_id]}
377
- GROUP BY subs.vulnerability_id, dv.title, subs.comparison
379
+ GROUP BY subs.vulnerability_id, dv.title, dv.nexpose_id, subs.comparison
378
380
 
379
381
  UNION
380
382
 
381
- SELECT DISTINCT on (subs.vulnerability_id, subs.comparison) subs.vulnerability_id, dv.title, MAX(dv.cvss_score) as cvss_score,
383
+ SELECT DISTINCT on (subs.vulnerability_id, subs.comparison) subs.vulnerability_id, dv.nexpose_id as vuln_nexpose_id,
384
+ dv.title, MAX(dv.cvss_score) as cvss_score,
382
385
  string_agg(DISTINCT subs.asset_id ||
383
386
  '|' || da.ip_address ||
384
387
  '|' || coalesce(da.host_name, '') ||
@@ -410,7 +413,7 @@ module NexposeTicketing
410
413
  JOIN fact_asset fa ON fa.asset_id = subs.asset_id
411
414
  #{createRiskString(options[:riskScore])}
412
415
  AND subs.current_scan > #{options[:scan_id]}
413
- GROUP BY subs.vulnerability_id, dv.title, subs.comparison
416
+ GROUP BY subs.vulnerability_id, dv.title, dv.nexpose_id, subs.comparison
414
417
 
415
418
  ORDER BY vulnerability_id, comparison"
416
419
  end
@@ -462,7 +465,7 @@ module NexposeTicketing
462
465
  # - Returns |asset_id| |ip_address| |current_scan| |vulnerability_id| |comparison|
463
466
  #
464
467
  def self.old_tickets_by_vuln_id(options = {})
465
- "SELECT subs.vulnerability_id, subs.asset_id, subs.ip_address, subs.current_scan, subs.comparison
468
+ "SELECT DISTINCT on(subs.vulnerability_id) subs.vulnerability_id, subs.asset_id, subs.ip_address, subs.current_scan, subs.comparison
466
469
  FROM (
467
470
  SELECT fasv.asset_id, s.ip_address, fasv.vulnerability_id, s.current_scan, baselineComparison(fasv.scan_id, s.current_scan) as comparison, fa.riskscore
468
471
  FROM fact_asset_scan_vulnerability_finding fasv
@@ -1,3 +1,4 @@
1
+ require 'tempfile'
1
2
  require 'nexpose'
2
3
  require 'securerandom'
3
4
  include Nexpose