nexpose_ticketing 1.0.2 → 1.2.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.
@@ -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