nexpose_servicenow 0.4.24 → 0.5.1

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