Module: PWN::Plugins::NexposeVulnScan

Defined in:
lib/pwn/plugins/nexpose_vuln_scan.rb

Overview

This plugin is used for interacting w/ Nexpose using the Nexpose API.

Constant Summary collapse

@@logger =
PWN::Plugins::PWNLogger.create

Class Method Summary collapse

Class Method Details

.authorsObject

Author(s)

0day Inc. <[email protected]>



303
304
305
306
307
# File 'lib/pwn/plugins/nexpose_vuln_scan.rb', line 303

public_class_method def self.authors
  "AUTHOR(S):
    0day Inc. <[email protected]>
  "
end

.delete_site_assets_older_than(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::NexposeVulnScan.delete_site_assets_older_than(

nsc_obj: 'required nsc_obj returned from login method',
site_name: 'required Nexpose site name to update (case-sensitive),
days: 'required assets to remove older than number of days in this parameter'

)



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/pwn/plugins/nexpose_vuln_scan.rb', line 131

public_class_method def self.delete_site_assets_older_than(opts = {})
  nsc_obj = opts[:nsc_obj]
  site_name = opts[:site_name]
  days = opts[:days].to_i

  site_id = -1
  nsc_obj.list_sites.each do |site|
    site_id = site.id if site.name == site_name
  end

  if site_id > -1
    @@logger.info("Removing the Following Assets from #{site.name} (site id: #{site_id}) Older than #{days} Days:")
    nsc_obj.filter(Nexpose::Search::Field::SCAN_DATE, Nexpose::Search::Operator::EARLIER_THAN, days).each do |asset|
      if asset.site_id == site_id
        @@logger.info("#{asset.id}|#{asset.ip}|#{asset.last_scan}")
        nsc_obj.delete_asset(asset.id)
      end
    end
  else
    @@logger.error("Site: #{site_name} Not Found.  Please check the spelling and try again.")
  end

  nsc_obj
rescue StandardError => e
  raise e
end

.download_recurring_report(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::NexposeVulnScan.download_recurring_report(

nsc_obj: 'required nsc_obj returned from login method',
report_names: 'required array of report name/types to generate e.g. ["report.html", "report.pdf", "report.xml"]',
poll_interval: 'optional poll interval to check the completion status of report generation (defaults to 60 seconds)

)



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/pwn/plugins/nexpose_vuln_scan.rb', line 231

public_class_method def self.download_recurring_report(opts = {})
  nsc_obj = opts[:nsc_obj]
  report_names = opts[:report_names].to_s.scrub.split(',')
  @@logger.info("Generating #{report_names.count} Report(s): #{report_names.inspect}...")

  poll_interval = if opts[:poll_interval].nil?
                    60
                  else
                    opts[:poll_interval].to_i
                  end

  report_arr = []
  report_status_arr = []
  until report_status_arr.count == report_names.count
    nsc_obj.reports.each do |report|
      report_names.each do |requested_report|
        this_report_name = requested_report.to_s.strip.chomp.delete('"')
        next unless report.name == this_report_name

        @@logger.info("Generating Recurring Report: #{report.name} @ #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}..Current Report Status: #{report.status}")
        if report.status == 'Failed'
          @@logger.info("Report Generation for #{report.name} failed...re-generating now...")
          # Re-generate report from pre-existing config.
          nsc_obj = generate_report_via_existing_config(nsc_obj: nsc_obj, config_id: report.config_id)
        end

        report_hash = {}
        report_hash[:report_name] = report.name
        report_hash[:report_status] = report.status
        report_hash[:report_uri] = report.uri
        report_arr.push(report_hash)
        report_status_arr = report_arr.uniq.select { |this_report| this_report[:report_status] == 'Generated' }
      end
    end
    # TODO: Ensure report_names are available within nsc_obj.reports (thus making it worthwhile to loop vs saying report !found).
    @@logger.info("Total Reports Generated So Far: #{report_status_arr.count}...")
    sleep poll_interval # Sleep and check the status again...
  end

  # @@logger.info(report_arr.inspect)
  # @@logger.info(report_status_arr.inspect)
  report_status_arr.each do |report_hash|
    this_file_extention = File.extname(report_hash[:report_uri])
    @@logger.info("\nDownloading #{report_hash[:report_name]}#{this_file_extention} from #{report_hash[:report_uri]}...")
    nsc_obj.download(report_hash[:report_uri], "#{report_hash[:report_name]}#{this_file_extention}")
  end
  @@logger.info('complete.')

  nsc_obj
rescue StandardError => e
  raise e
end

.generate_report_via_existing_config(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::NexposeVulnScan.generate_report_via_existing_config(

nsc_obj: 'required nsc_obj returned from login method',
config_id: 'relevant r.config_id returned when invoking the block nsc_obj.reports.each {|r| puts "#{r.name} => #{r.config_id}"}',

)



212
213
214
215
216
217
218
219
220
221
222
# File 'lib/pwn/plugins/nexpose_vuln_scan.rb', line 212

public_class_method def self.generate_report_via_existing_config(opts = {})
  nsc_obj = opts[:nsc_obj]
  config_id = opts[:config_id].to_i

  existing_report_config = Nexpose::ReportConfig.load(nsc_obj, config_id)
  existing_report_config.generate(nsc_obj)

  nsc_obj
rescue StandardError => e
  raise e
end

.helpObject

Display Usage for this Module



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/pwn/plugins/nexpose_vuln_scan.rb', line 311

public_class_method def self.help
  puts "USAGE:
    nsc_obj = #{self}.login(
      console_ip: 'required host/ip of Nexpose Console (server)',
      username: 'required username',
      password: 'optional password (will prompt if nil)'
    )
    puts nsc_obj.public_methods

    all_individual_site_assets_arr = #{self}.list_all_individual_site_assets(
      nsc_obj: 'required nsc_obj returned from login method',
      site_name: 'required Nexpose site name to update (case-sensitive)'
    )

    nsc_obj = #{self}.update_site_assets(
      nsc_obj: 'required nsc_obj returned from login method',
      site_name: 'required Nexpose site name to update (case-sensitive),
      assets: 'required array of hashes containing called :ip => values being IP address (All IPs not included in the :assets parameter will be removed from Nexpose)'
    )

    nsc_obj = #{self}.delete_site_assets_older_than(
      nsc_obj: 'required nsc_obj returned from login method',
      site_name: 'required Nexpose site name to update (case-sensitive),
      days: 'required assets to remove older than number of days in this parameter'
    )

    nsc_obj = #{self}.scan_site_by_name(
      nsc_obj: 'required nsc_obj returned from login method',
      site_name: 'required Nexpose site name to scan (case-sensitive),
      poll_interval: 'optional poll interval to check the completion status of the scan (defaults to 60 seconds)
    )

    #{self}.download_recurring_report(
      nsc_obj: 'required nsc_obj returned from login method',
      report_names: 'required array of report name/types to generate e.g. ['report.html', 'report.pdf', 'report.xml']',
      poll_interval: 'optional poll interval to check the completion status of report generation (defaults to 60 seconds)
    )

    #{self}.logout(nsc_obj: 'required nsc_obj returned from login method')

    #{self}.authors
  "
end

.list_all_individual_site_assets(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::NexposeVulnScan.list_all_individual_site_assets(

nsc_obj: 'required nsc_obj returned from login method',
site_name: 'required Nexpose site name to update (case-sensitive)'

)



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/pwn/plugins/nexpose_vuln_scan.rb', line 45

public_class_method def self.list_all_individual_site_assets(opts = {})
  nsc_obj = opts[:nsc_obj]
  site_name = opts[:site_name]

  site_id = -1
  nsc_obj.list_sites.each do |site|
    site_id = site.id if site.name == site_name
  end

  all_individual_site_assets_arr = []
  if site_id > -1
    @@logger.info("Listing All Assets from #{site_name} (site id: #{site_id}):")
    nsc_obj.filter(Nexpose::Search::Field::SITE_ID, Nexpose::Search::Operator::IN, site_id).each do |asset|
      @@logger.info("#{asset.id}|#{asset.ip}|#{asset.last_scan}")
      all_individual_site_assets_arr.push(asset.ip)
    end
  else
    @@logger.error("Site: #{site_name} Not Found.  Please check the spelling and try again.")
  end

  all_individual_site_assets_arr
rescue StandardError => e
  raise e
end

.login(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::NexposeVulnScan.login(

console_ip: 'required host/ip of Nexpose Console (server)',
username: 'required username',
password: 'optional password (will prompt if nil)'

)



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/pwn/plugins/nexpose_vuln_scan.rb', line 19

public_class_method def self.(opts = {})
  console_ip = opts[:console_ip]
  username = opts[:username].to_s

  password = if opts[:password].nil?
               PWN::Plugins::AuthenticationHelper.mask_password
             else
               opts[:password].to_s
             end

  nsc_obj = Nexpose::Connection.new(console_ip, username, password)
  nsc_obj.
  config = Nexpose::Console.load(nsc_obj)
  config.session_timeout = 21_600
  # config.save(nsc_obj) # This will change the global sesion timeout config in the console
  nsc_obj
rescue StandardError => e
  raise e
end

.logout(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::NexposeVulnScan.logout(

nsc_obj: 'required nsc_obj returned from login method'

)



289
290
291
292
293
294
295
296
297
298
299
# File 'lib/pwn/plugins/nexpose_vuln_scan.rb', line 289

public_class_method def self.logout(opts = {})
  nsc_obj = opts[:nsc_obj]

  # config = Nexpose::Console.load(nsc_obj)
  # config.session_timeout = 600 # This is the default session timeout in the console
  # config.save(nsc_obj) # This will change the global sesion timeout config in the console
  nsc_obj.logout
  'logged out'
rescue StandardError => e
  raise e
end

.scan_site_by_name(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::NexposeVulnScan.scan_site_by_name(

nsc_obj: 'required nsc_obj returned from login method',
site_name: 'required Nexpose site name to scan (case-sensitive),
poll_interval: 'optional poll interval to check the completion status of the scan (defaults to 3 minutes)

)



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/pwn/plugins/nexpose_vuln_scan.rb', line 165

public_class_method def self.scan_site_by_name(opts = {})
  nsc_obj = opts[:nsc_obj]
  site_name = opts[:site_name].to_s
  site_id = nil

  poll_interval = if opts[:poll_interval].nil?
                    60
                  else
                    opts[:poll_interval].to_i
                  end

  # Find the site and kick off the scan
  nsc_obj.list_sites.each do |site|
    next unless site.name == site_name

    nsc_obj.scan_site(site.id)
    @@logger.info("Scan Started for #{site_name} @ #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}")
    site_id = site.id
  end

  # Periodically check the status of the scan
  raise @logger.error("Site name: #{site_name} does not exist as a site in Nexpose.  Please check your spelling and try again.") if site_id == ''

  @@logger.info("Info: Checking status for an interval of #{poll_interval} seconds until completion.")
  loop do
    scan_status = nil
    nsc_obj.scan_activity.each { |scan| scan_status = scan.status if scan.site_id == site_id }
    if scan_status == 'running'
      print '~' # Seeing progress is good :)
      sleep poll_interval # Sleep and check the status again...
    else
      @@logger.info("Scan Completed for #{site_name} @ #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}")
      break
    end
  end

  nsc_obj
rescue StandardError => e
  raise e
end

.update_site_assets(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::NexposeVulnScan.update_site_assets(

nsc_obj: 'required nsc_obj returned from login method',
site_name: 'required Nexpose site name to update (case-sensitive),
assets: 'required array of hashes containing called :ip => values being IP address (All IPs not included in the :assets parameter will be removed from Nexpose)'

)



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/pwn/plugins/nexpose_vuln_scan.rb', line 77

public_class_method def self.update_site_assets(opts = {})
  nsc_obj = opts[:nsc_obj]
  site_name = opts[:site_name]
  assets = opts[:assets]

  nsc_obj.list_sites.each do |site|
    next unless site.name == site_name

    # Load Site
    site_id = site.id
    refresh_site = Nexpose::Site.load(nsc_obj, site_id)

    # Obtain Current List of Assets for Given Site & Remove Old List of Assets from Given Site (if applicable)
    @@logger.info('Removing the Following Assets:')
    current_site_assets = nsc_obj.filter(Nexpose::Search::Field::SITE_ID, Nexpose::Search::Operator::IN, site_id)
    new_site_assets = []
    assets.each { |ip_host_hash| new_site_assets.push(ip_host_hash[:ip].to_s.scrub.strip.chomp) }
    current_site_assets.each do |current_site_asset|
      next if new_site_assets.include?(current_site_asset.ip)

      @@logger.info("Removing #{current_site_asset.ip}")
      # refresh_site.remove_included_asset(current_asset) # This should work and be less invasive but no worky :(
      nsc_obj.delete_asset(current_site_asset.id) # So we completely remove the asset from Nexpose altogether :/
    end
    @@logger.info('Complete.')

    # Add New List of Assets to Given Site
    @@logger.info("Adding the Following Assets to #{site.name} (site id: #{site_id}):")
    assets.each do |ip_host_hash|
      current_ip = IPAddr.new(ip_host_hash[:ip].to_s.scrub.strip.chomp)
      unless current_ip.to_s.match?('255') # || current_ip =~ /^[224-239]/ # Multicast?
        # TODO: try to reverse DNS word to see if an IP s available as well
        @@logger.info("Adding #{current_ip}")
        refresh_site.include_asset(current_ip)
      end
    rescue StandardError
      next
    end
    refresh_site.save(nsc_obj)
    @@logger.info('Complete.')
  end

  nsc_obj
rescue StandardError => e
  raise e
end