nexpose_servicenow 0.4.24 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3df03f3e1e1368fc16c60c61da775e397035309f
4
- data.tar.gz: 5997f1479b0084bb174329a43ac92373240ec704
3
+ metadata.gz: 7f3da5fa2194d025ab5192815f682a09a94583be
4
+ data.tar.gz: 94e1f6a2c0976fba3215185d62a2f0a03edcb5c5
5
5
  SHA512:
6
- metadata.gz: ad9e10d478b454d5b474c20f6cfdddb8018f979f71562f0948a569f76ef6e1ee0c9dfa3ad871013d6b4f3cf5d72bc3c9aa33bb016b0226f219976137a06cf16c
7
- data.tar.gz: 7c07adc6765b58543b9172b3bb0b00ab01f9fa6e6fc21698a6b07c02681cca728fba16843e200c4d7ef1ae2ad7130778d38ddd7f98a631d5228aabc55f7c9962
6
+ metadata.gz: 6c6223fa14d6e6b0221e6176f5a4b79c4e9816dc2350592b68262cc94a98cf2cdf26d3bc647f699102c20954f9120fa12fdb1d0d4aa041a9c8cda75748036322
7
+ data.tar.gz: b5f07c1718b8cf959b932d6d76192c8ccd2a71b30ec70805334ab117f2e4f3b0e373ac2c3e876dced59f2e2d7f1dabc427568dd547c854cb636f6700c2b1fb2b
data/README.md CHANGED
@@ -1,14 +1,22 @@
1
1
  # NexposeServicenow
2
2
 
3
3
  ## Installation
4
+ The gem may installed via the following command from the RubyGems repository:
4
5
 
5
- gem install nexpose_servicenow
6
+ `gem install nexpose_servicenow`
6
7
 
7
8
  ## Usage
9
+ The gem is called by the ServiceNow console when a vulnerability integration executes.
8
10
 
9
- ## Development
11
+ Alternatively, it is also possible to call the following to see a list of parameters:
12
+ `nexpose_servicenow -h`
10
13
 
11
- ## Contributing
14
+
15
+ ## Support
16
+ Please contact the following address for support queries:
17
+ [support@rapid7.com](support@rapid7.com)
18
+
19
+ Please attach both the gem logs and relevant snippets from the agent logs.
12
20
 
13
21
  ## License
14
22
 
@@ -8,31 +8,22 @@ require 'nexpose_servicenow/arg_parser'
8
8
  require 'nexpose_servicenow/chunker'
9
9
  require 'nexpose_servicenow/nx_logger'
10
10
  require 'nexpose_servicenow/historical_data'
11
- require "nexpose_servicenow/version"
11
+ require 'nexpose_servicenow/version'
12
12
 
13
13
  module NexposeServiceNow
14
14
  class Main
15
15
  def self.start(args)
16
16
  options = ArgParser.parse(args)
17
17
 
18
- log = setup_logging(options)
19
-
20
- censored_options = options.dup
21
- censored_options[:nexpose_username] = "*****"
22
- censored_options[:nexpose_password] = "*****"
23
- log.log_message("Options: #{censored_options}")
18
+ @log = setup_logging(options)
24
19
 
25
- if options[:mode].to_s == ""
26
- log.log_message("Script was called without mode.")
27
- puts "No mode selected. Use -h to see command line options."
28
- exit -1
29
- end
20
+ censored_options = options.dup
21
+ censored_options[:nexpose_username] = '*****'
22
+ censored_options[:nexpose_password] = '*****'
23
+ @log.log_message("Options: #{censored_options}")
30
24
 
31
- if options[:nexpose_ids].first.to_s == "0"
32
- log.log_error_message('Retrieving array of all site IDs')
33
- options[:nexpose_ids] = get_nexpose_helper(options).all_sites.sort
34
- options[:nexpose_ids].map! { |i| i.to_s }
35
- end
25
+ options[:nexpose_ids] = get_collection_ids(options)
26
+ options[:start_time] = Time.new().strftime('%Y-%m-%m %H:%M:%S')
36
27
 
37
28
  report_details = NexposeHelper.get_report_names(options[:query],
38
29
  options[:nexpose_ids])
@@ -41,13 +32,21 @@ module NexposeServiceNow
41
32
  options[:output_dir])
42
33
  end
43
34
 
44
- update_last_scan_data(options)
45
- create_report(report_details, options)
35
+ update_delta_files(options) if options[:mode] == 'latest_scans'
46
36
 
47
- log.log_message("Initialising #{options[:mode]} mode")
37
+ create_report(report_details, options)
38
+
39
+ @log.log_message("Initialising #{options[:mode]} mode")
48
40
  self.send("#{options[:mode]}_mode", report_details, options)
49
41
  end
50
42
 
43
+ def self.get_historical_data(options)
44
+ HistoricalData.new(options[:output_dir],
45
+ options[:nexpose_ids],
46
+ options[:id_type],
47
+ options[:start_time])
48
+ end
49
+
51
50
  def self.get_nexpose_helper(options)
52
51
  NexposeHelper.new(options[:nexpose_url],
53
52
  options[:nexpose_port],
@@ -55,6 +54,15 @@ module NexposeServiceNow
55
54
  options[:nexpose_password])
56
55
  end
57
56
 
57
+ # Retrieves list of all IDs if the user has chosen to import each group
58
+ def self.get_collection_ids(options)
59
+ return options[:nexpose_ids] if options[:nexpose_ids].first.to_s != '0'
60
+
61
+ @log.log_error_message("Retrieving array of all #{options[:id_type]} IDs")
62
+ helper = get_nexpose_helper(options)
63
+ helper.collection_ids(options[:id_type]).map(&:to_s)
64
+ end
65
+
58
66
  def self.setup_logging(options)
59
67
  log = NexposeServiceNow::NxLogger.instance
60
68
  log.setup_statistics_collection(NexposeServiceNow::VENDOR,
@@ -66,43 +74,42 @@ module NexposeServiceNow
66
74
  log
67
75
  end
68
76
 
69
- #Merges in the details from the last time the
70
- #integration ran reports.
71
- def self.update_last_scan_data(options)
72
- return unless options[:mode] == "latest_scans"
73
- historical_data = HistoricalData.new(options)
74
- historical_data.update_last_scan_data
75
-
77
+ # Merges in the details from the last time the integration ran reports.
78
+ def self.update_delta_files(options)
79
+ historical_data = get_historical_data(options)
80
+ historical_data.update_delta_file
76
81
  historical_data.save_vuln_timestamp(filter_sites(options))
77
82
  end
78
83
 
79
- #Create a report if explicitly required or else an existing
80
- #report file isn't found
84
+ # Create a report if explicitly required or else an existing
85
+ # report file isn't found
81
86
  def self.create_report(report_details, options)
82
- return if options[:mode].start_with? 'update_'
87
+ if options[:mode].start_with? 'update_' or
88
+ (options[:mode] == 'latest_scans' && options[:id_type] != :site)
89
+ return
90
+ end
83
91
 
84
92
  unless options[:gen_report]
85
- #If any file is missing, perform all queries
86
- return if report_details.all? { |f| File.exists?(f[:report_name]) }
93
+ # If any file is missing, perform all queries
94
+ return if report_details.all? { |f| File.exists? f[:report_name] }
87
95
  end
88
96
 
89
97
  credentials = %i{nexpose_username nexpose_password}
90
- if credentials.any? { |cred| options[cred].to_s == "" }
91
- log = NexposeServiceNow::NxLogger.instance
92
- log.log_error_message "Nexpose credentials necessary but not supplied."
98
+ if credentials.any? { |cred| options[cred].to_s == '' }
99
+ @log.log_error_message 'Nexpose credentials necessary but not supplied.'
93
100
  exit -1
94
101
  end
95
102
 
96
103
  #Filter it down to sites which actively need queried
97
104
  sites_to_scan = filter_sites(options)
98
105
  nexpose_helper = get_nexpose_helper(options)
99
- hist_data = HistoricalData.new(options)
106
+ hist_data = get_historical_data(options)
100
107
  vuln_query = options[:query].to_s.start_with? 'vulnerabili'
101
- last_run = nil
102
-
103
- start_time = Time.new
104
- query_options = { last_scans: hist_data.last_scan_ids(sites_to_scan) }
108
+
109
+ delta_values = hist_data.stored_delta_values(sites_to_scan)
110
+ query_options = { delta_values: delta_values }
105
111
  query_options[:vuln_query_date] = hist_data.last_vuln_run if vuln_query
112
+ query_options[:filters] = options[:filters]
106
113
 
107
114
  filename = nexpose_helper.create_report(options[:query],
108
115
  sites_to_scan,
@@ -110,8 +117,8 @@ module NexposeServiceNow
110
117
  options[:output_dir],
111
118
  query_options)
112
119
 
113
- #A single String may be returned or an Array of Strings
114
- if filename.class.to_s == "Array"
120
+ # A single String may be returned or an Array of Strings
121
+ if filename.class.to_s == 'Array'
115
122
  filename.map! { |f| File.expand_path(options[:output_dir], f) }
116
123
  return filename.join("\n")
117
124
  end
@@ -120,8 +127,8 @@ module NexposeServiceNow
120
127
  end
121
128
 
122
129
  def self.filter_sites(options)
123
- #These queries always run to make sure certain data is up to date
124
- exceptions = ['vulnerabili', 'asset_groups', 'sites', 'tags']
130
+ # These queries always run to make sure certain data is up to date
131
+ exceptions = %w(vulnerabili asset_groups sites tags)
125
132
  if exceptions.any? { |e| options[:query].to_s.start_with? e }
126
133
  return options[:nexpose_ids]
127
134
  end
@@ -132,28 +139,30 @@ module NexposeServiceNow
132
139
  return options[:nexpose_ids]
133
140
  end
134
141
 
135
- historical_data = HistoricalData.new(options)
142
+ historical_data = get_historical_data(options)
136
143
  imported_sites_only = options[:query].to_s.eql? 'vulnerable_old_items'
137
- sites_to_scan = historical_data.sites_to_scan(imported_sites_only)
144
+ sites_to_scan = historical_data.collections_to_import(imported_sites_only)
138
145
 
139
146
  return sites_to_scan unless (sites_to_scan.nil? || sites_to_scan.empty?)
140
147
 
141
- log = NexposeServiceNow::NxLogger.instance
142
- log.log_message "Sites #{options[:nexpose_ids]} are up to date."
143
- log.log_message "Query requested was: #{options[:query]}."
148
+ @log.log_message "Sites #{options[:nexpose_ids]} are up to date."
149
+ @log.log_message "Query requested was: #{options[:query]}."
144
150
  exit 0
145
151
  end
146
152
 
147
- #Print the chunk info
153
+ # Print the chunk info
148
154
  def self.chunk_info_mode(report_details, options)
149
155
  filtered_sites = filter_sites(options)
150
- report_details = report_details.select { |d| d[:id] == -1 or filtered_sites.include? d[:id] }
156
+ report_details = report_details.select do |d|
157
+ d[:id] == -1 or filtered_sites.include? d[:id]
158
+ end
151
159
  chunker = Chunker.new(report_details, options[:row_limit])
152
160
 
153
- puts chunker.preprocess(filtered_sites)
161
+ # TODO: Check why filtered_sites are passed in here
162
+ puts chunker.preprocess
154
163
  end
155
164
 
156
- #Prints a chunk of CSV to the console
165
+ # Prints a chunk of CSV to the console
157
166
  def self.get_chunk_mode(report_details, options)
158
167
  #Get the byte offset and length
159
168
  chunker = Chunker.new(report_details, options[:row_limit])
@@ -165,28 +174,28 @@ module NexposeServiceNow
165
174
  end
166
175
 
167
176
  def self.latest_scans_mode(report_details, options)
168
- historical_data = HistoricalData.new(options)
177
+ historical_data = get_historical_data(options)
169
178
  puts historical_data.filter_report
170
179
  end
171
180
 
172
181
  def self.remove_last_scan_mode(report_details, options)
173
- historical_data = HistoricalData.new(options)
182
+ historical_data = get_historical_data(options)
174
183
  historical_data.remove_last_scan_data
175
184
  end
176
185
 
177
186
  def self.update_last_scan_mode(report_details, options)
178
- historical_data = HistoricalData.new(options)
187
+ historical_data = get_historical_data(options)
179
188
  historical_data.set_last_scan(options[:nexpose_ids].first,
180
189
  options[:last_scan_data])
181
190
  end
182
191
 
183
192
  def self.remove_last_vuln_mode(report_details, options)
184
- historical_data = HistoricalData.new(options)
193
+ historical_data = get_historical_data(options)
185
194
  historical_data.remove_last_vuln_data
186
195
  end
187
196
 
188
197
  def self.update_last_vuln_mode(report_details, options)
189
- historical_data = HistoricalData.new(options)
198
+ historical_data = get_historical_data(options)
190
199
  historical_data.set_last_vuln(options[:last_scan_data],
191
200
  options[:nexpose_ids])
192
201
  end
@@ -1,119 +1,211 @@
1
1
  require 'optparse'
2
2
  require 'json'
3
+ require 'time'
3
4
  require_relative './queries'
4
5
  require_relative './nx_logger'
5
6
 
6
7
  module NexposeServiceNow
7
8
  class ArgParser
8
- NX_ID_TYPES = %i[site tag]
9
+ NX_ID_TYPES = %i[site asset_group]
9
10
  MODES = %i[chunk_info get_chunk latest_scans
10
11
  remove_last_scan update_last_scan
11
12
  remove_last_vuln update_last_vuln]
12
13
 
13
14
  QUERY_NAMES = Queries.methods(false)
14
-
15
- QUERY_ALIASES = { 'devices' => 'cmdb_ci_outofband_device',
16
- 'vuln_items' => 'sn_vul_vulnerable_item',
17
- 'vuln_entries' => 'sn_vul_third_party_entry' }
18
-
15
+
19
16
  def self.parse(args)
20
17
  options = Hash.new
21
18
 
19
+ log = NexposeServiceNow::NxLogger.instance
20
+ log.log_message 'Parsing options.'
21
+
22
22
  opt_parser = OptionParser.new do |opts|
23
- opts.banner = "Usage: example.rb [options]"
23
+ opts.banner = 'Usage: example.rb [options]'
24
24
 
25
- opts.on("-o", "--output-dir DIRECTORY",
26
- "Directory in which to save reports") do |output_dir|
25
+ opts.on('-o', '--output-dir DIRECTORY',
26
+ 'Directory in which to save reports') do |output_dir|
27
27
  options[:output_dir] = output_dir
28
28
  end
29
29
 
30
- opts.on("-m", "--mode MODE",
30
+ opts.on('-m', '--mode MODE',
31
31
  "Mode for program output. (#{MODES.join(', ')})") do |mode|
32
32
  options[:mode] = mode
33
33
  end
34
34
 
35
- opts.on("-g", "--generate-report BOOLEAN",
36
- "True to generate and download new report.") do |gen|
35
+ opts.on('-g', '--generate-report BOOLEAN',
36
+ 'True to generate and download new report') do |gen|
37
37
  char = gen.downcase[0]
38
- options[:gen_report] = char == 'y' || char == 't'
38
+ options[:gen_report] = %w(y t).any? { |c| c == char }
39
39
  end
40
40
 
41
- opts.separator ""
42
- opts.separator "Query options:"
41
+ opts.separator ''
42
+ opts.separator 'Query options:'
43
43
 
44
- opts.on("-q", "--query QUERY", QUERY_NAMES,
44
+ opts.on('-q', '--query QUERY', QUERY_NAMES,
45
45
  "Select query (#{QUERY_NAMES.join(', ')})") do |query|
46
46
  options[:query] = query
47
47
  end
48
48
 
49
- opts.on("-t", "--type TYPE", NX_ID_TYPES,
49
+ opts.on('-t', '--type TYPE', NX_ID_TYPES,
50
50
  "Select type (#{NX_ID_TYPES.join(', ')})") do |type|
51
51
  options[:id_type] = type
52
52
  end
53
53
 
54
- opts.on("-i", "--items x,y,z", Array,
55
- "IDs of the nexpose items to scan") do |items|
54
+ opts.on('-i', '--items x,y,z', Array,
55
+ 'IDs of the nexpose items to scan') do |items|
56
56
  options[:nexpose_ids] = items
57
57
  end
58
58
 
59
- opts.separator ""
60
- opts.separator "Nexpose options:"
59
+ opts.separator ''
60
+ opts.separator 'Nexpose options:'
61
61
 
62
- opts.on("-n", "--nexpose-address URL",
63
- "URL of the Nexpose server") do |url|
62
+ opts.on('-n', '--nexpose-address URL',
63
+ 'URL of the Nexpose server') do |url|
64
64
  port = url.slice!(/:(\d+)$/)
65
65
  port.slice! ':' unless port.nil?
66
66
 
67
- url.slice! /https:\/\//
67
+ url.slice! 'https://'
68
68
  options[:nexpose_url] = url
69
69
  options[:nexpose_port] = port || '3780'
70
- @full_url = options[:nexpose_url] + ':' + options[:nexpose_port]
71
70
  end
72
71
 
73
- opts.on("-u", "--user USER",
74
- "Username for Nexpose console") do |username|
72
+ opts.on('-u', '--user USER',
73
+ 'Username for Nexpose console') do |username|
75
74
  options[:nexpose_username] = username
76
75
  end
77
76
 
78
- opts.on("-p", "--password PASSWORD",
79
- "Password for the Nexpose user") do |password|
77
+ opts.on('-p', '--password PASSWORD',
78
+ 'Password for the Nexpose user') do |password|
80
79
  options[:nexpose_password] = password
81
80
  end
82
81
 
83
- opts.separator ""
84
- opts.separator "Chunk info mode options:"
82
+ opts.separator ''
83
+ opts.separator 'Chunk info mode options:'
85
84
 
86
- opts.on("-r", "--row-limit LIMIT",
87
- "Maximum number of rows per chunk (with header).") do |limit|
85
+ opts.on('-r', '--row-limit LIMIT',
86
+ 'Maximum number of rows per chunk (including header).') do |limit|
88
87
  options[:row_limit] = limit.to_i
89
88
  options[:row_limit] = 9_999_999 if options[:row_limit] <= 0
90
89
  end
91
90
 
92
- opts.separator ""
93
- opts.separator "Get chunk mode options:"
91
+ opts.separator ''
92
+ opts.separator 'Get chunk mode options:'
94
93
 
95
- opts.on("-s", "--start START",
96
- "The chunk starting offset.") do |start|
94
+ opts.on('-s', '--start START',
95
+ 'The chunk starting offset.') do |start|
97
96
  options[:chunk_start] = start.to_i
98
97
  end
99
98
 
100
- opts.on("-l", "--length LENGTH",
101
- "The chunk length.") do |length|
99
+ opts.on('-l', '--length LENGTH',
100
+ 'The chunk length.') do |length|
102
101
  options[:chunk_length] = length.to_i
103
102
  end
104
103
 
105
- opts.separator ""
106
- opts.separator "Last scan file modification options:"
104
+ opts.separator ''
105
+ opts.separator 'Filter options:'
106
+
107
+ =begin
108
+ # CVE filter is not currently supported.
109
+ opts.on('-v', '--vuln-identifier CVE',
110
+ 'The CVE values for which to filter.') do |data|
111
+ # A value of 'none' means the user has left field blank
112
+ if data.to_s.downcase != 'none'
113
+ data = data.to_s.sub(' ', '').split(',')
114
+
115
+ invalid_cve = data.select { |f| (f =~ /(CVE-)?\d{4}-\d+/) == nil }
116
+ unless invalid_cve.empty?
117
+ error = "Invalid CVEs applied: #{invalid_cve}"
118
+ puts error
119
+ log.log_message error
120
+ exit -1
121
+ end
122
+
123
+ data = data.map do |c|
124
+ if c.start_with? 'CVE-'
125
+ c
126
+ else
127
+ "CVE-#{c}"
128
+ end
129
+ end
130
+ else
131
+ data = nil
132
+ end
133
+
134
+ options[:filters] ||= {}
135
+ options[:filters][:cve] = data
136
+ end
137
+ =end
138
+ opts.on('-c', '--cvss-score CVSS',
139
+ 'The minimum CVSS score to import') do |data|
140
+
141
+ cvss_range = data.split('~')
142
+
143
+ if cvss_range.count != 2
144
+ error = "Expected two CVSS scores. Received #{cvss_range.count}"
145
+ puts error
146
+ log.log_message error
147
+ exit -1
148
+ end
149
+
150
+ cvss_range.each do |cvss|
151
+ next if cvss.to_s =~ /^0*(10(\.0+)?|\d(\.\d+)?)?$/
152
+ error = "Invalid CVSS score supplied: #{cvss}. Exiting"
153
+ puts error
154
+ log.log_message error
155
+ exit -1
156
+ end
157
+
158
+ options[:filters] ||= {}
159
+ options[:filters][:cvss] = cvss_range
160
+ end
107
161
 
108
- opts.on("-d", "--data DATA",
109
- "Date or scan ID to be inserted in last scan file.") do |data|
162
+ opts.on('-d', '--date DATE',
163
+ 'The minimum date for each vulnerability instance.') do |date|
164
+ # Date should be in format 'YYYY-MM-DD~YYYY-MM-DD'
165
+ dates = date.to_s.split('~')
166
+
167
+ if dates.count != 2
168
+ error = "Expected two dates. Received #{dates.count}"
169
+ puts error
170
+ log.log_message error
171
+ exit -1
172
+ end
173
+
174
+ # Add the dates
175
+ dates[0] = dates[0] + ' 00:00:00'
176
+ dates[1] = dates[1] + ' 23:59:59'
177
+
178
+ # Check for valid dates and placeholders
179
+ dates.map! do |d|
180
+ if d =~ /Y{4}-M{1,2}-D{1,2}/i
181
+ nil
182
+ elsif d =~ /\d{4}-\d{1,2}-\d{1,2}/
183
+ d
184
+ else
185
+ error = "Invalid date supplied: #{d}. Exiting."
186
+ puts error
187
+ log.log_message error
188
+ exit -1
189
+ end
190
+ end
191
+
192
+ options[:filters] ||= {}
193
+ options[:filters][:date] = dates
194
+ end
195
+
196
+
197
+ opts.separator ''
198
+ opts.separator 'Last scan file modification options:'
199
+
200
+ opts.on('-e', '--errata DATA',
201
+ 'Date or scan ID to be inserted in last scan file.') do |data|
110
202
  options[:last_scan_data] = data
111
203
  end
112
204
 
113
- opts.separator ""
114
- opts.separator "Common options:"
205
+ opts.separator ''
206
+ opts.separator 'Common options:'
115
207
 
116
- opts.on_tail("-h", "--help", "Show this message") do
208
+ opts.on_tail('-h', '--help', 'Show this message') do
117
209
  puts opts
118
210
  exit
119
211
  end
@@ -125,21 +217,27 @@ module NexposeServiceNow
125
217
  options
126
218
  end
127
219
 
128
- #TODO: Validate input depending on mode AND whether generating a report
129
- # is required.
130
220
  def self.validate_input(options)
131
221
  #Insert defaults. Some are mode-specific.
132
222
  options[:output_dir] ||= '.'
133
223
  options[:row_limit] ||= 9_999_999
134
224
  options[:id_type] ||= 'site'
135
225
  options[:nexpose_ids] ||= []
226
+ options[:filters] ||= {}
136
227
 
137
228
  options[:query] = 'latest_scans' if options[:mode] == 'latest_scans'
138
229
 
139
230
  #By default, a report won't be generated if a chunk's being retrieved
140
231
  if options[:gen_report].nil?
141
- options[:gen_report] = options[:mode] == "chunk_info" ||
142
- options[:mode] == "latest_scans"
232
+ options[:gen_report] = options[:mode] == 'chunk_info' ||
233
+ options[:mode] == 'latest_scans'
234
+ end
235
+
236
+ if options[:mode].to_s == ''
237
+ log = NexposeServiceNow::NxLogger.instance
238
+ log.log_message('Script was called without mode.')
239
+ puts 'No mode selected. Use -h to see command line options.'
240
+ exit -1
143
241
  end
144
242
 
145
243
  options
@@ -150,7 +248,7 @@ module NexposeServiceNow
150
248
  return options if options[:gen_report] == false
151
249
 
152
250
  log = NexposeServiceNow::NxLogger.instance
153
- log.log_message "Retrieving environment variables."
251
+ log.log_message 'Retrieving environment variables.'
154
252
 
155
253
  # Retrieve environment variable settings
156
254
  %i[url port username password].each do |setting|