#!/usr/bin/env ruby # # This plugin provides integration with Rapid7 NeXpose # module Msf class Plugin::Nexpose < Msf::Plugin class NexposeCommandDispatcher include Msf::Ui::Console::CommandDispatcher def name "NeXpose" end def commands { 'nexpose_connect' => "Connect to a running NeXpose instance ( user:pass@host[:port] )", 'nexpose_activity' => "Display any active scan jobs on the NeXpose instance", 'nexpose_scan' => "Launch a NeXpose scan against a specific IP range and import the results", 'nexpose_discover' => "Launch a scan but only perform host and minimal service discovery", 'nexpose_exhaustive' => "Launch a scan covering all TCP ports and all authorized safe checks", 'nexpose_dos' => "Launch a scan that includes checks that can crash services and devices (caution)", 'nexpose_import_raw' => "Import a Raw XML file from NeXpose (premium editions)", 'nexpose_import_simple' => "Import a Simple XML file from any version of NeXpose", 'nexpose_disconnect' => "Disconnect from an active NeXpose instance", # TODO: # nexpose_stop_scan } end def nexpose_verify if ! @nsc print_error("No active NeXpose instance has been configured, please use 'nexpose_connect'") return false end if ! (framework.db and framework.db.usable) print_error("No database has been configured, please use db_create/db_connect first") return false end true end # # Determine if an IP address is inside a given range # def range_include?(ranges, addr) ranges.each do |sets| sets.split(',').each do |set| rng = set.split('-').map{ |c| Rex::Socket::addr_atoi(c) } tst = Rex::Socket::addr_atoi(addr) if (not rng[1]) return tst == rng[0] elsif (tst >= rng[0] and tst <= rng[1]) return true end end end false end def cmd_nexpose_connect(*args) if(args.length == 0 or args[0].empty? or args[0] == "-h") print_status("Usage: ") print_status(" nexpose_connect username:password@host[:port] ") print_status(" -OR- ") print_status(" nexpose_connect username password host port ") return end user = pass = host = port = sslv = nil case args.length when 1,2 cred,targ = args[0].split('@', 2) user,pass = cred.split(':', 2) targ ||= '127.0.0.1:3780' host,port = targ.split(':', 2) port ||= '3780' sslv = args[1] when 4,5 user,pass,host,port,sslv = args else print_status("Usage: ") print_status(" nexpose_connect username:password@host[:port] ") print_status(" -OR- ") print_status(" nexpose_connect username password host port ") return end if ! ((user and user.length > 0) and (host and host.length > 0) and (port and port.length > 0 and port.to_i > 0) and (pass and pass.length > 0)) print_status("Usage: ") print_status(" nexpose_connect username:password@host[:port] ") print_status(" -OR- ") print_status(" nexpose_connect username password host port ") return end if(host != "localhost" and host != "127.0.0.1" and sslv != "ok") print_error("Warning: SSL connections are not verified in this release, it is possible for an attacker") print_error(" with the ability to man-in-the-middle the NeXpose traffic to capture the NeXpose") print_error(" credentials. If you are running this on a trusted network, please pass in 'ok'") print_error(" as an additional parameter to this command.") return end # Wrap this so a duplicate session doesnt prevent a new login begin cmd_nexpose_disconnect rescue ::Interrupt raise $! rescue ::Exception end begin print_status("Connecting to NeXpose instance at #{host}:#{port} with username #{user}...") nsc = ::Nexpose::Connection.new(host, user, pass, port) nsc.login rescue ::Nexpose::APIError => e print_error("Connection failed: #{e.reason}") return end @nsc = nsc end def cmd_nexpose_activity(*args) return if not nexpose_verify scans = @nsc.scan_activity || [] case scans.length when 0 print_status("There are currently no active scan jobs on this NeXpose instance") when 1 print_status("There is 1 active scan job on this NeXpose instance") else print_status("There are currently #{scans.length} active scan jobs on this NeXpose instance") end scans.each do |scan| print_status(" Scan ##{scan[:scan_id]} is running on Engine ##{scan[:engine_id]} against site ##{scan[:site_id]} since #{scan[:start_time].to_s}") end end def cmd_nexpose_discover(*args) args << "-h" if args.length == 0 args << "-t" args << "aggressive-discovery" cmd_nexpose_scan(*args) end def cmd_nexpose_exhaustive(*args) args << "-h" if args.length == 0 args << "-t" args << "exhaustive-audit" cmd_nexpose_scan(*args) end def cmd_nexpose_dos(*args) args << "-h" if args.length == 0 args << "-t" args << "dos-audit" cmd_nexpose_scan(*args) end def cmd_nexpose_scan(*args) opts = Rex::Parser::Arguments.new( "-h" => [ false, "This help menu"], "-t" => [ true, "The scan template to use (default:pentest-audit options:full-audit,exhaustive-audit,discovery,aggressive-discovery,dos-audit)"], "-n" => [ true, "The maximum number of IPs to scan at a time (default is 32)"], "-s" => [ true, "The directory to store the raw XML files from the NeXpose instance (optional)"], "-P" => [ false, "Leave the scan data on the server when it completes (this counts against the maximum licensed IPs)"], "-v" => [ false, "Display diagnostic information about the scanning process"], "-x" => [ false, "Automatically launch all exploits by matching reference after the scan completes (unsafe)"], "-X" => [ false, "Automatically launch all exploits by matching reference and port after the scan completes (unsafe)"], "-d" => [ false, "Scan hosts based on the contents of the existing database"], "-I" => [ true, "Only scan systems with an address within the specified range"], "-E" => [ true, "Exclude hosts in the specified range from the scan"] ) opt_template = "pentest-audit" opt_maxaddrs = 32 opt_monitor = false opt_verbose = false opt_savexml = nil opt_preserve = false opt_autopwn = false opt_rescandb = false opt_addrinc = nil opt_addrexc = nil opt_scanned = [] opt_ranges = [] report_format = "ns-xml" opts.parse(args) do |opt, idx, val| case opt when "-h" print_line("Usage: nexpose_scan [options] ") print_line(opts.usage) return when "-t" opt_template = val when "-n" opt_maxaddrs = val.to_i when "-s" opt_savexml = val when "-v" opt_verbose = true when "-P" opt_preserve = true when "-X" opt_autopwn = "-p -x" when "-x" opt_autopwn = "-x" unless opt_autopwn when "-d" opt_rescandb = true when '-I' opt_addrinc = OptAddressRange.new('TEMPRANGE', [ true, '' ]).normalize(val) when '-E' opt_addrexc = OptAddressRange.new('TEMPRANGE', [ true, '' ]).normalize(val) else opt_ranges << val end end return if not nexpose_verify # Include all database hosts as scan targets if specified if(opt_rescandb) print_status("Loading scan targets from the active database...") if opt_verbose framework.db.hosts.each do |host| next if host.state != ::Msf::HostState::Alive opt_ranges << host.address end end opt_ranges = opt_ranges.join(',') if(opt_ranges.strip.empty?) print_line("Usage: nexpose_scan [options] ") print_line(opts.usage) return end if(opt_verbose) print_status("Creating a new scan using template #{opt_template} and #{opt_maxaddrs} concurrent IPs against #{opt_ranges}") end range_inp = ::Msf::OptAddressRange.new('TEMPRANGE', [ true, '' ]).normalize(opt_ranges) range = ::Rex::Socket::RangeWalker.new(range_inp) completed = 0 total = range.num_ips count = 0 print_status("Scanning #{total} addresses with template #{opt_template} in sets of #{opt_maxaddrs}") while(completed < total) count += 1 queue = [] while(ip = range.next_ip and queue.length < opt_maxaddrs) if(opt_addrexc and range_include?([opt_addrexc], ip)) print_status(" >> Skipping host #{ip} due to exclusion") if opt_verbose next end if(opt_addrinc and ! range_include?([opt_addrinc], ip)) print_status(" >> Skipping host #{ip} due to inclusion filter") if opt_verbose next end opt_scanned << ip queue << ip end break if queue.empty? print_status("Scanning #{queue[0]}-#{queue[-1]}...") if opt_verbose msfid = Time.now.to_i # Create a temporary site site = Nexpose::Site.new(@nsc) site.setSiteConfig("Metasploit-#{msfid}", "Autocreated by the Metasploit Framework") queue.each do |ip| site.site_config.addHost(Nexpose::IPRange.new(ip)) end site.site_config._set_scanConfig(Nexpose::ScanConfig.new(-1, "tmp", opt_template)) site.saveSite() print_status(" >> Created temporary site ##{site.site_id}") if opt_verbose # Configure reporting report = Nexpose::ReportConfig.new(@nsc) report.set_name("Metasploit Export #{msfid}") report.set_template_id(opt_template) report.set_format(report_format) report.addFilter("SiteFilter",site.site_id) report.set_generate_after_scan(1) report.set_storeOnServer(1) report.saveReport() print_status(" >> Created temporary report configuration ##{report.config_id}") if opt_verbose # Run the scan res = site.scanSite() sid = res[:scan_id] print_status(" >> Scan has been launched with ID ##{sid}") if opt_verbose rep = true begin prev = nil while(true) info = @nsc.scan_statistics(sid) break if info[:summary]['status'] != "running" stat = "Found #{info[:nodes]['live']} devices and #{info[:nodes]['dead']} unresponsive" if(stat != prev) print_status(" >> #{stat}") if opt_verbose end prev = stat select(nil, nil, nil, 5.0) end print_status(" >> Scan has been completed with ID ##{sid}") if opt_verbose rescue ::Interrupt rep = false print_status(" >> Terminating scan ID ##{sid} due to console interupt") if opt_verbose @nsc.scan_stop(sid) break end # Wait for the automatic report generation to complete if(rep) print_status(" >> Waiting on the report to generate...") if opt_verbose url = nil while(! url) url = @nsc.report_last(report.config_id) select(nil, nil, nil, 1.0) end print_status(" >> Downloading the report data from NeXpose...") if opt_verbose data = @nsc.download(url) if(opt_savexml) ::FileUtils.mkdir_p(opt_savexml) path = File.join(opt_savexml, "nexpose-#{msfid}-#{count}.xml") print_status(" >> Saving scan data into #{path}") if opt_verbose ::File.open(path, "wb") do |fd| fd.write(data) end end process_nexpose_data(report_format, data) end if ! opt_preserve print_status(" >> Deleting the temporary site and report...") if opt_verbose @nsc.site_delete(site.site_id) end end print_status("Completed the scan of #{total} addresses") if(opt_autopwn) print_status("Launching an automated exploitation session") driver.run_single("db_autopwn -q -r -e -t #{opt_autopwn} -I #{opt_scanned.join(",")}") end end def cmd_nexpose_disconnect(*args) @nsc.logout if @nsc @nsc = nil end def cmd_nexpose_import_raw(*args) if ! (args[0] and args[0] != "-h") print_status("Usage: nexpose_import_raw ") return end process_nexpose_data_rxml(::File.read(args[0], ::File.size(args[0]))) end def cmd_nexpose_import_simple(*args) if ! (args[0] and args[0] != "-h") print_status("Usage: nexpose_import_simple ") return end process_nexpose_data_sxml(::File.read(args[0], ::File.size(args[0]))) end def process_nexpose_data(fmt, data) case fmt when 'raw-xml' process_nexpose_data_rxml(data) when 'ns-xml' process_nexpose_data_sxml(data) else print_error("Unsupported NeXpose data format: #{fmt}") end end def process_nexpose_data_rxml(data) doc = REXML::Document.new(data) doc.elements.each('/NexposeReport/nodes/node') do |host| addr = host.attributes['address'] xhost = addr refs = {} # os based vuln host.elements['tests'].elements.each('test') do |vuln| if vuln.attributes['status'] == 'vulnerable-exploited' or vuln.attributes['status'] == 'vulnerable-version' dhost = framework.db.get_host(nil, addr) next if not dhost vid = vuln.attributes['id'].to_s nexpose_vuln_lookup(doc,vid,refs,dhost) nexpose_vuln_lookup(doc,vid.upcase,refs,dhost) end end # skip if no endpoints next unless host.elements['endpoints'] # parse the ports and add the vulns host.elements['endpoints'].elements.each('endpoint') do |port| prot = port.attributes['protocol'] pnum = port.attributes['port'] stat = port.attributes['status'] next if not port.elements['services'] name = port.elements['services'].elements['service'].attributes['name'].downcase next if not port.elements['services'].elements['service'].elements['fingerprints'] prod = port.elements['services'].elements['service'].elements['fingerprints'].elements['fingerprint'].attributes['product'] vers = port.elements['services'].elements['service'].elements['fingerprints'].elements['fingerprint'].attributes['version'] vndr = port.elements['services'].elements['service'].elements['fingerprints'].elements['fingerprint'].attributes['vendor'] next if stat != 'open' dhost = framework.db.get_host(nil, addr) next if not dhost if dhost.state != Msf::HostState::Alive framework.db.report_host_state(self, addr, Msf::HostState::Alive) end service = framework.db.get_service(nil, dhost, prot.downcase, pnum.to_i) if name != "unknown" service.name = name service.save end port.elements['services'].elements['service'].elements['tests'].elements.each('test') do |vuln| if vuln.attributes['status'] == 'vulnerable-exploited' or vuln.attributes['status'] == 'vulnerable-version' vid = vuln.attributes['id'].to_s # TODO, improve the vuln_lookup check so case of the vuln_id doesnt matter nexpose_vuln_lookup(doc,vid,refs,dhost,service) nexpose_vuln_lookup(doc,vid.upcase,refs,dhost,service) end end end end end # # NeXpose vuln lookup # def nexpose_vuln_lookup(doc, vid, refs, host, serv=nil) doc.elements.each("/NexposeReport/VulnerabilityDefinitions/vulnerability[@id = '#{vid}']]") do |vulndef| title = vulndef.attributes['title'] pciSeverity = vulndef.attributes['pciSeverity'] cvss_score = vulndef.attributes['cvssScore'] cvss_vector = vulndef.attributes['cvssVector'] vulndef.elements['references'].elements.each('reference') do |ref| if ref.attributes['source'] == 'BID' refs[ 'BID-' + ref.text ] = true elsif ref.attributes['source'] == 'CVE' # ref.text is CVE-$ID refs[ ref.text ] = true elsif ref.attributes['source'] == 'MS' refs[ 'MSB-MS-' + ref.text ] = true end end refs[ 'NEXPOSE-' + vid.downcase ] = true vuln = framework.db.get_vuln(nil, host, serv, 'NEXPOSE-' + vid.downcase, title) rids = [] refs.keys.each do |r| rids << framework.db.get_ref(nil, r) end vuln.refs << (rids - vuln.refs) end end def process_nexpose_data_sxml(data) doc = REXML::Document.new(data) doc.elements.each('/NeXposeSimpleXML/devices/device') do |dev| addr = dev.attributes['address'].to_s desc = '' dev.elements.each('fingerprint/description') do |fdesc| desc = fdesc.text.to_s.strip end host = framework.db.get_host(nil, addr) next if not host if host.state != Msf::HostState::Alive framework.db.report_host_state(self, addr, Msf::HostState::Alive) end # Load vulnerabilities not associated with a service dev.elements.each('vulnerabilities/vulnerability') do |vuln| vid = vuln.attributes['id'].to_s.downcase rids = [] refs = process_nexpose_data_sxml_refs(vuln) next if not refs vuln = framework.db.get_vuln(nil, host, nil, 'NEXPOSE-' + vid, vid) refs.each { |r| rids << framework.db.get_ref(nil, r) } vuln.refs << (rids - vuln.refs) end # Load the services dev.elements.each('services/service') do |svc| sname = svc.attributes['name'].to_s sprot = svc.attributes['protocol'].to_s.downcase sport = svc.attributes['port'].to_s.to_i serv = framework.db.get_service(nil, host, sprot, sport) if(sname.downcase != '') serv.name = sname.split('(')[0].strip serv.save end # Load vulnerabilities associated with this service svc.elements.each('vulnerabilities/vulnerability') do |vuln| vid = vuln.attributes['id'].to_s.downcase rids = [] refs = process_nexpose_data_sxml_refs(vuln) next if not refs vuln = framework.db.get_vuln(nil, host, serv, 'NEXPOSE-' + vid, vid) refs.each { |r| rids << framework.db.get_ref(nil, r) } vuln.refs << (rids - vuln.refs) end end end end def process_nexpose_data_sxml_refs(vuln) refs = [] vid = vuln.attributes['id'].to_s.downcase vry = vuln.attributes['resultCode'].to_s.upcase # Only process vuln-exploitable and vuln-version statuses return if vry !~ /^V[VE]$/ refs = [] vuln.elements.each('id') do |ref| rtyp = ref.attributes['type'].to_s.upcase rval = ref.text.to_s.strip case rtyp when 'CVE' refs << rval.gsub('CAN', 'CVE') when 'MS' # obsolete? refs << "MSB-MS-#{rval}" else refs << "#{rtyp}-#{rval}" end end refs << "NEXPOSE-#{vid}" refs end end # # Plugin initialization # def initialize(framework, opts) super add_console_dispatcher(NexposeCommandDispatcher) banner = ["0a205f5f5f5f202020202020202020202020205f20202020205f205f5f5f5f5f2020205f2020205f20202020205f5f20205f5f2020202020202020202020202020202020202020200a7c20205f205c205f5f205f205f205f5f20285f29205f5f7c207c5f5f5f20207c207c205c207c207c205f5f5f5c205c2f202f5f205f5f2020205f5f5f20205f5f5f20205f5f5f200a7c207c5f29202f205f60207c20275f205c7c207c2f205f60207c20202f202f20207c20205c7c207c2f205f205c5c20202f7c20275f205c202f205f205c2f205f5f7c2f205f205c0a7c20205f203c20285f7c207c207c5f29207c207c20285f7c207c202f202f2020207c207c5c20207c20205f5f2f2f20205c7c207c5f29207c20285f29205c5f5f205c20205f5f2f0a7c5f7c205c5f5c5f5f2c5f7c202e5f5f2f7c5f7c5c5f5f2c5f7c2f5f2f202020207c5f7c205c5f7c5c5f5f5f2f5f2f5c5f5c202e5f5f2f205c5f5f5f2f7c5f5f5f2f5c5f5f5f7c0a20202020202020202020207c5f7c20202020202020202020202020202020202020202020202020202020202020202020207c5f7c202020202020202020202020202020202020200a0a0a"].pack("H*") if ! (Rex::Compat.is_windows or Rex::Compat.is_cygwin) banner = ["202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200a20e29684e29684e29684202020e29684e29684202020202020202020202020e29684e29684e296842020e29684e29684e2968420202020202020202020202020202020202020202020202020202020202020202020202020202020200a20e29688e29688e29688202020e29688e2968820202020202020202020202020e29688e2968820e29684e29688e296882020202020202020202020202020202020202020202020202020202020202020202020202020202020200a20e29688e29688e29680e296882020e29688e29688202020e29684e29688e29688e29688e29688e296842020202020e29688e29688e29688e2968820202020e29688e29688e29684e29688e29688e29688e2968420202020e29684e29688e29688e29688e29688e29684202020e29684e29684e29688e29688e29688e29688e29688e29684202020e29684e29688e29688e29688e29688e2968420200a20e29688e2968820e29688e2968820e29688e296882020e29688e29688e29684e29684e29684e29684e29688e296882020202020e29688e296882020202020e29688e29688e296802020e29680e29688e296882020e29688e29688e296802020e29680e29688e296882020e29688e29688e29684e29684e29684e2968420e296802020e29688e29688e29684e29684e29684e29684e29688e29688200a20e29688e296882020e29688e29684e29688e296882020e29688e29688e29680e29680e29680e29680e29680e2968020202020e29688e29688e29688e2968820202020e29688e2968820202020e29688e296882020e29688e2968820202020e29688e29688202020e29680e29680e29680e29680e29688e29688e296842020e29688e29688e29680e29680e29680e29680e29680e29680200a20e29688e29688202020e29688e29688e296882020e29680e29688e29688e29684e29684e29684e29684e29688202020e29688e296882020e29688e29688202020e29688e29688e29688e29684e29684e29688e29688e296802020e29680e29688e29688e29684e29684e29688e29688e296802020e29688e29684e29684e29684e29684e29684e29688e296882020e29680e29688e29688e29684e29684e29684e29684e29688200a20e29680e29680202020e29680e29680e2968020202020e29680e29680e29680e29680e29680202020e29680e29680e296802020e29680e29680e296802020e29688e2968820e29680e29680e29680202020202020e29680e29680e29680e296802020202020e29680e29680e29680e29680e29680e296802020202020e29680e29680e29680e29680e2968020200a20202020202020202020202020202020202020202020202020202020202020e29688e29688202020202020202020202020202020202020202020202020202020202020202020202020200a202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200a"].pack("H*") end print(banner) print_status("NeXpose integration has been activated") end def cleanup remove_console_dispatcher('NeXpose') end def name "nexpose" end def desc "Integrates with the Rapid7 NeXpose vulnerability management product" end end end # # The NeXpose API # # # WARNING! This code makes an SSL connection to the NeXpose server, but does NOT # verify the certificate at this time. This can be a security issue if # an attacker is able to man-in-the-middle the connection between the # Metasploit console and the NeXpose server. In the common case of # running NeXpose and Metasploit on the same host, this is a low risk. # # # WARNING! This code is still rough and will go through a number of major changes # before the final release. Please do not build any tools using this API # unless you are prepared to rewrite them in the near future. # require 'date' require 'rexml/document' require 'net/https' require 'net/http' require 'uri' module Nexpose class APIError < ::RuntimeError attr_accessor :req, :reason def initialize(req, reason = '') self.req = req self.reason = reason end def to_s "NexposeAPI: #{self.reason}" end end module XMLUtils def parse_xml(xml) ::REXML::Document.new(xml.to_s) end end class APIRequest include XMLUtils attr_reader :http attr_reader :uri attr_reader :headers attr_reader :retry_count attr_reader :time_out attr_reader :pause attr_reader :req attr_reader :res attr_reader :sid attr_reader :success attr_reader :error attr_reader :trace def initialize(req, url) @retry_count = 0 @retry_count_max = 10 @time_out = 30 @pause = 2 @req = req @uri = URI.parse(url) @http = Net::HTTP.new(@uri.host, @uri.port) @http.use_ssl = true @http.verify_mode = OpenSSL::SSL::VERIFY_NONE # XXX: security issue @headers = {'Content-Type' => 'text/xml'} @success = false end def execute begin resp, data = @http.post(@uri.path, @req, @headers) @res = parse_xml(data) if(not @res.root) @error = "NeXpose service returned invalid XML" return @sid end @sid = attributes['session-id'] if(attributes['success'] and attributes['success'].to_i == 1) @success = true else @success = false @res.elements.each('//Failure/Exception') do |s| s.elements.each('message') do |m| @error = m.text end s.elements.each('stacktrace') do |m| @trace = m.text end end end rescue ::Interrupt @error = "received a user interrupt" rescue ::Timeout::Error, ::Errno::EHOSTUNREACH,::Errno::ENETDOWN,::Errno::ENETUNREACH,::Errno::ENETRESET,::Errno::EHOSTDOWN,::Errno::EACCES,::Errno::EINVAL,::Errno::EADDRNOTAVAIL @error = "NeXpose host is unreachable" rescue ::Errno::ECONNRESET,::Errno::ECONNREFUSED,::Errno::ENOTCONN,::Errno::ECONNABORTED @error = "NeXpose service is not available" end if ! (@success or @error) @error = "NeXpose service returned an unrecognized response" end @sid end def attributes(*args) return if not @res.root @res.root.attributes(*args) end def self.execute(url,req) obj = self.new(req,url) obj.execute if(not obj.success) raise APIError.new(obj, "Action failed: #{obj.error}") end obj end end module NexposeAPI def make_xml(name, opts={}) xml = REXML::Element.new(name) if(@session_id) xml.attributes['session-id'] = @session_id end opts.keys.each do |k| xml.attributes[k] = opts[k] end xml end def scan_stop(param) r = execute(make_xml('ScanStopRequest', { 'scan-id' => param })) r.success end def scan_status(param) r = execute(make_xml('ScanStatusRequest', { 'scan-id' => param })) r.success ? r.attributes['status'] : nil end def scan_activity r = execute(make_xml('ScanActivityRequest', { })) if(r.success) res = [] r.res.elements.each("//ScanSummary") do |scan| res << { :scan_id => scan.attributes['scan-id'].to_i, :site_id => scan.attributes['site-id'].to_i, :engine_id => scan.attributes['engine-id'].to_i, :status => scan.attributes['status'].to_s, :start_time => Date.parse(scan.attributes['startTime'].to_s).to_time } end return res else return false end end def scan_statistics(param) r = execute(make_xml('ScanStatisticsRequest', {'scan-id' => param })) if(r.success) res = {} r.res.elements.each("//ScanSummary/nodes") do |node| res[:nodes] = {} node.attributes.keys.each do |k| res[:nodes][k] = node.attributes[k].to_i end end r.res.elements.each("//ScanSummary/tasks") do |task| res[:task] = {} task.attributes.keys.each do |k| res[:task][k] = task.attributes[k].to_i end end r.res.elements.each("//ScanSummary/vulnerabilities") do |vuln| res[:vulns] ||= {} k = vuln.attributes['status'] + (vuln.attributes['severity'] ? ("-" + vuln.attributes['severity']) : '') res[:vulns][k] = vuln.attributes['count'].to_i end r.res.elements.each("//ScanSummary") do |summ| res[:summary] = {} summ.attributes.keys.each do |k| res[:summary][k] = summ.attributes[k] if (res[:summary][k] =~ /^\d+$/) res[:summary][k] = res[:summary][k].to_i end end end return res else return false end end def report_generate(param) r = execute(make_xml('ReportGenerateRequest', { 'report-id' => param })) r.success end def report_last(param) r = execute(make_xml('ReportHistoryRequest', { 'reportcfg-id' => param })) res = nil if(r.success) stk = [] r.res.elements.each("//ReportSummary") do |rep| stk << [ rep.attributes['id'].to_i, rep.attributes['report-URI'] ] end if (stk.length > 0) stk.sort!{|a,b| b[0] <=> a[0]} res = stk[0][1] end end res end def report_history(param) execute(make_xml('ReportHistoryRequest', { 'reportcfg-id' => param })) end def report_config_delete(param) r = execute(make_xml('ReportDeleteRequest', { 'reportcfg-id' => param })) r.success end def report_delete(param) r = execute(make_xml('ReportDeleteRequest', { 'report-id' => param })) r.success end def site_delete(param) r = execute(make_xml('SiteDeleteRequest', { 'site-id' => param })) r.success end def device_delete(param) r = execute(make_xml('DeviceDeleteRequest', { 'site-id' => param })) r.success end def asset_group_delete(connection, id, debug = false) r = execute(make_xml('AssetGroupDeleteRequest', { 'group-id' => param })) r.success end end # === Description # Object that represents a connection to a NeXpose Security Console. # # === Examples # # Create a new Nexpose Connection on the default port # nsc = Connection.new("10.1.40.10","nxadmin","password") # # # Login to NSC and Establish a Session ID # nsc.login() # # # Check Session ID # if (nsc.session_id) # puts "Login Successful" # else # puts "Login Failure" # end # # # //Logout # logout_success = nsc.logout() # if (! logout_success) # puts "Logout Failure" + "

" + nsc.error_msg.to_s # end # class Connection include XMLUtils include NexposeAPI # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # Session ID of this connection attr_reader :session_id # The hostname or IP Address of the NSC attr_reader :host # The port of the NSC (default is 3780) attr_reader :port # The username used to login to the NSC attr_reader :username # The password used to login to the NSC attr_reader :password # The URL for communication attr_reader :url # Constructor for Connection def initialize(ip, user, pass, port = 3780) @host = ip @port = port @username = user @password = pass @session_id = nil @error = false @url = "https://#{@host}:#{@port}/api/1.1/xml" end # Establish a new connection and Session ID def login r = execute(make_xml('LoginRequest', { 'sync-id' => 0, 'password' => @password, 'user-id' => @username })) if(r.success) @session_id = r.sid return true end raise APIError.new(r, 'Login failed') end # Logout of the current connection def logout r = execute(make_xml('LogoutRequest', {'sync-id' => 0})) if(r.success) return true end raise APIError.new(r, 'Logout failed') end # Execute an API request def execute(xml) APIRequest.execute(url,xml.to_s) end # Download a specific URL def download(url) uri = URI.parse(url) http = Net::HTTP.new(@host, @port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE # XXX: security issue headers = {'Cookie' => "nexposeCCSessionID=#{@session_id}"} resp, data = http.get(uri.path, headers) data end end # === Description # Object that represents a listing of all of the sites available on an NSC. # # === Example # # Create a new Nexpose Connection on the default port and Login # nsc = Connection.new("10.1.40.10","nxadmin","password") # nsc->login(); # # # Get Site Listing # sitelisting = SiteListing.new(nsc) # # # Enumerate through all of the SiteSummaries # sitelisting.sites.each do |sitesummary| # # Do some operation on each site # end # class SiteListing # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # Array containing SiteSummary objects for each site in the connection attr_reader :sites # The number of sites attr_reader :site_count # Constructor # SiteListing (connection) def initialize(connection) @sites = [] @connection = connection r = @connection.execute('') if (r.success) parse(r.res) else raise APIError.new(r, "Failed to get site listing") end end def parse(r) r.elements.each('SiteListingResponse/SiteSummary') do |s| site_summary = SiteSummary.new( s.attributes['id'].to_s, s.attributes['name'].to_s, s.attributes['description'].to_s, s.attributes['riskfactor'].to_s ) @sites.push(site_summary) end @site_count = @sites.length end end # === Description # Object that represents the summary of a NeXpose Site. # class SiteSummary # The Site ID attr_reader :id # The Site Name attr_reader :site_name # A Description of the Site attr_reader :description # User assigned risk multiplier attr_reader :riskfactor # Constructor # SiteSummary(id, site_name, description, riskfactor = 1) def initialize(id, site_name, description, riskfactor = 1) @id = id @site_name = site_name @description = description @riskfactor = riskfactor end def _set_id(id) @id = id end end # === Description # Object that represents a single IP address or an inclusive range of IP addresses. If to is nil then the from field will be used to specify a single IP Address only. # class IPRange # Start of Range *Required attr_reader :from; # End of Range *Optional (If Null then IPRange is a single IP Address) attr_reader :to; def initialize(from, to = nil) @from = from @to = to end end # === Description # Object that represents a hostname to be added to a site. class HostName # The hostname attr_reader :hostname def initialize(hostname) @hostname = hostname end end # === Description # Object that represents the configuration of a Site. This object is automatically created when a new Site object is instantiated. # class SiteConfig # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # The Site ID attr_reader :site_id # The Site Name attr_reader :site_name # A Description of the Site attr_reader :description # User assigned risk multiplier attr_reader :riskfactor # Array containing ((IPRange|HostName)*) attr_reader :hosts # Array containing (AdminCredentials*) attr_reader :credentials # Array containing ((SmtpAlera|SnmpAlert|SyslogAlert)*) attr_reader :alerts # ScanConfig object which holds Schedule and ScanTrigger Objects attr_reader :scanConfig def initialize() @xml_tag_stack = Array.new() @hosts = Array.new() @credentials = Array.new() @alerts = Array.new() @error = false end # Adds a new host to the hosts array def addHost(host) @hosts.push(host) end # Adds a new alert to the alerts array def addAlert(alert) @alerts.push(alert) end # Adds a new set of credentials to the credentials array def addCredentials(credential) @credentials.push(credential) end # TODO def getSiteConfig(connection,site_id) @connection = connection @site_id = site_id r = APIRequest.execute(@connection.url,'') parse(r.res) end def _set_site_id(site_id) @site_id = site_id end def _set_site_name(site_name) @site_name = site_name end def _set_description(description) @description = description end def _set_riskfactor(riskfactor) @riskfactor = riskfactor end def _set_scanConfig(scanConfig) @scanConfig = scanConfig end def _set_connection(connection) @connection = connection end =begin =end def parse(response) response.elements.each('SiteConfigResponse/Site') do |s| @site_id = s.attributes['id'] @site_name = s.attributes['name'] @description = s.attributes['description'] @riskfactor = s.attributes['riskfactor'] s.elements.each('Hosts/range') do |r| @hosts.push(IPRange.new(r.attributes['from'],r.attributes['to'])) end s.elements.each('ScanConfig') do |c| @scanConfig = ScanConfig.new(c.attributes['configID'], c.attributes['name'], c.attributes['configVersion'], c.attributes['templateID']) s.elements.each('Schedule') do |schedule| schedule = new Schedule(schedule.attributes["type"], schedule.attributes["interval"], schedule.attributes["start"], schedule.attributes["enabled"]) @scanConfig.addSchedule(schedule) end end s.elements.each('Alerting/Alert') do |a| a.elements.each('smtpAlert') do |smtp| smtp_alert = SmtpAlert.new(a.attributes["name"], smtp.attributes["sender"], smtp.attributes["limitText"], a.attributes["enabled"]) smtp.elements.each('recipient') do |recipient| smtp_alert.addRecipient(recipient.text) end @alerts.push(smtp_alert) end a.elements.each('snmpAlert') do |snmp| snmp_alert = SnmpAlert.new(a.attributes["name"], snmp.attributes["community"], snmp.attributes["server"], a.attributes["enabled"]) @alerts.push(snmp_alert) end a.elements.each('syslogAlert') do |syslog| syslog_alert = SyslogAlert.new(a.attributes["name"], syslog.attributes["server"], a.attributes["enabled"]) @alerts.push(syslog_alert) end a.elements.each('vulnFilter') do |vulnFilter| #vulnfilter = new VulnFilter.new(a.attributes["typemask"], a.attributes["severityThreshold"], $attrs["MAXALERTS"]) # Pop off the top alert on the stack #$alert = @alerts.pop() # Add the new recipient string to the Alert Object #$alert.setVulnFilter($vulnfilter) # Push the alert back on to the alert stack #array_push($this->alerts, $alert) end a.elements.each('scanFilter') do |scanFilter| # #scanfilter = ScanFilter.new(scanFilter.attributes['scanStop'],scanFilter.attributes['scanFailed'],scanFilter.attributes['scanStart']) #alert = @alerts.pop() #alert.setScanFilter(scanfilter) #@alerts.push(alert) end end end end end # === Description # Object that represents the scan history of a site. # class SiteScanHistory # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # The Site ID attr_reader :site_id # //Array containing (ScanSummary*) attr_reader :scan_summaries def initialize(connection, id) @site_id = id @error = false @connection = connection @scan_summaries = Array.new() r = @connection.execute('') status = r.success end end # === Description # Object that represents a listing of devices for a site or the entire NSC. Note that only devices which are accessible to the account used to create the connection object will be returned. This object is created and populated automatically with the instantiation of a new Site object. # class SiteDeviceListing # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # The Site ID. 0 if all sites are specified. attr_reader :site_id # //Array of (Device)* attr_reader :devices def initialize(connection, site_id = 0) @site_id = site_id @error = false @connection = connection @devices = Array.new() r = nil if (@site_id) r = @connection.execute('') else r = @connection.execute('') end if(r.success) response.elements.each('SiteDeviceListingResponse/SiteDevices/device') do |d| @devices.push(Device.new(d.attributes['id'],@site_id,d.attributes["address"],d.attributes["riskfactor"],d.attributes['riskscore'])) end end end end # === Description # Object that represents a site, including the site configuration, scan history, and device listing. # # === Example # # Create a new Nexpose Connection on the default port and Login # nsc = Connection.new("10.1.40.10","nxadmin","password") # nsc.login() # # # Get an Existing Site # site_existing = Site.new(nsc,184) # # # Create a New Site, add some hosts, and save it to the NSC # site = Site.new(nsc) # site.setSiteConfig("New Site", "New Site Created in the API") # # # Add the hosts # site.site_config.addHost(HostName.new("localhost")) # site.site_config.addHost(IPRange.new("192.168.7.1","192.168.7.255")) # site.site_config.addHost(IPRange.new("10.1.20.30")) # # status = site.saveSite() # class Site # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # The Site ID # site_id = -1 means create a new site. The NSC will assign a new site_id on SiteSave. attr_reader :site_id # A summary overview of this site # SiteSummary Object attr_reader :site_summary # The configuration of this site # SiteConfig Object attr_reader :site_config # The device listing for this site # SiteDeviceListing Object attr_reader :site_device_listing # The scan history of this site # SiteScanHistory Object attr_reader :site_scan_history def initialize(connection, site_id = -1) @error = false @connection = connection @site_id = site_id # If site_id > 0 then get SiteConfig if (@site_id.to_i > 0) # Create new SiteConfig object @site_config = SiteConfig.new() # Populate SiteConfig Obect with Data from the NSC @site_config.getSiteConfig(@connection,@site_id) @site_summary = SiteSummary.new(@site_id, @site_config.site_name, @site_config.description, @site_config.riskfactor) @site_scan_history = SiteScanHistory.new(@connection,@site_id) @site_device_listing = SiteDeviceListing.new(@connection,@site_id) else # Just in case user enters a number > -1 or = 0 @site_id = -1 @site_config = SiteConfig.new() setSiteConfig("New Site " + rand(999999999999).to_s,"") @site_summary = nil end end # Creates a new site summary def setSiteSummary(site_name, description, riskfactor = 1) @site_summary = SiteSummary.new(-1,site_name,description,riskfactor) end # Creates a new site configuration def setSiteConfig(site_name, description, riskfactor = 1) setSiteSummary(site_name,description,riskfactor) @site_config = SiteConfig.new() @site_config._set_site_id(-1) @site_config._set_site_name(site_name) @site_config._set_description(description) @site_config._set_riskfactor(riskfactor) @site_config._set_scanConfig(ScanConfig.new(-1,"tmp","full-audit")) @site_config._set_connection(@connection) end # Initiates a scan of this site. If successful returns scan_id and engine_id in an associative array. Returns false if scan is unsuccessful. def scanSite() r = @connection.execute('') if(r.success) res = {} r.res.elements.each('//Scan/') do |s| res[:scan_id] = s.attributes['scan-id'] res[:engine_id] = s.attributes['engine-id'] end return res else return false end end # Saves this site in the NSC def saveSite() r = @connection.execute('' + getSiteXML() + ' ') if (r.success) @site_id = r.attributes['site-id'] @site_config._set_site_id(@site_id) @site_config.scanConfig._set_configID(@site_id) @site_config.scanConfig._set_name(@site_id) return true else return false end end def deleteSite() r = @connection.execute('') r.success end def printSite() puts "Site ID: " + @site_summary.id puts "Site Name: " + @site_summary.site_name puts "Site Description: " + @site_summary.description puts "Site Risk Factor: " + @site_summary.riskfactor end def getSiteXML() xml = '' xml += ' ' @site_config.hosts.each do |h| if (h.class.to_s == "Nexpose::IPRange") if (h.to and not h.to.empty?) xml += ' ' else xml += ' ' end elsif (h.class.to_s == "Nexpose::HostName") xml += ' ' + h.hostname + '' end end xml +=' ' xml += ' ' @site_config.credentials.each do |c| xml += ' ' a.recipients.each do |r| xml += ' ' + r + '' end xml += ' ' xml += ' ' when :snmp xml += ' ' xml += ' ' xml += ' ' when :syslog xml += ' ' xml += ' ' xml += ' ' end end xml += ' ' xml += ' ' xml += ' ' @site_config.scanConfig.schedules.each do |s| xml += ' ' end xml += ' ' xml += ' ' @site_config.scanConfig.scanTriggers.each do |s| if (s.class.to_s == "Nexpose::AutoUpdate") xml += ' ' end end xml += ' ' xml += ' ' xml += ' ' return xml end end # === Description # Object that represents administrative credentials to be used during a scan. When retrived from an existing site configuration the credentials will be returned as a security blob and can only be passed back as is during a Site Save operation. This object can only be used to create a new set of credentials. # class AdminCredentials # Security blob for an existing set of credentials attr_reader :securityblob # Designates if this object contains user defined credentials or a security blob attr_reader :isblob # The service for these credentials. Can be All. attr_reader :service # The host for these credentials. Can be Any. attr_reader :host # The port on which to use these credentials. attr_reader :port # The user id or username attr_reader :userid # The password attr_reader :password # The realm for these credentials attr_reader :realm def initialize(isblob = false) @isblob = isblob end # Sets the credentials information for this object. def setCredentials(service, host, port, userid, password, realm) @isblob = false @securityblob = nil @service = service @host = host @port = port @userid = userid @password = password @realm = realm end # TODO: add description def setService(service) @service = service end def setHost(host) @host = host end # TODO: add description def setBlob(securityblob) @isblob = true @securityblob = securityblob end end # === Description # Object that represents an SMTP (Email) Alert. # class SmtpAlert # A unique name for this alert attr_reader :name # If this alert is enabled or not attr_reader :enabled # The email address of the sender attr_reader :sender # Limit the text for mobile devices attr_reader :limitText # Array containing Strings of email addresses # Array of strings with the email addresses of the intended recipients attr_reader :recipients # The vulnerability filter to trigger the alert attr_reader :vulnFilter # The alert type attr_reader :type def initialize(name, sender, limitText, enabled = 1) @type = :smtp @name = name @sender = sender @enabled = enabled @limitText = limitText @recipients = Array.new() # Sets default vuln filter - All Events setVulnFilter(VulnFilter.new("50790400",1)) end # Adds a new Recipient to the recipients array def addRecipient(recipient) @recipients.push(recipient) end # Sets the Vulnerability Filter for this alert. def setVulnFilter(vulnFilter) @vulnFilter = vulnFilter end end # === Description # Object that represents an SNMP Alert. # class SnmpAlert # A unique name for this alert attr_reader :name # If this alert is enabled or not attr_reader :enabled # The community string attr_reader :community # The SNMP server to sent this alert attr_reader :server # The vulnerability filter to trigger the alert attr_reader :vulnFilter # The alert type attr_reader :type def initialize(name, community, server, enabled = 1) @type = :snmp @name = name @community = community @server = server @enabled = enabled # Sets default vuln filter - All Events setVulnFilter(VulnFilter.new("50790400",1)) end # Sets the Vulnerability Filter for this alert. def setVulnFilter(vulnFilter) @vulnFilter = vulnFilter end end # === Description # Object that represents a Syslog Alert. # class SyslogAlert # A unique name for this alert attr_reader :name # If this alert is enabled or not attr_reader :enabled # The Syslog server to sent this alert attr_reader :server # The vulnerability filter to trigger the alert attr_reader :vulnFilter # The alert type attr_reader :type def initialize(name, server, enabled = 1) @type = :syslog @name = name @server = server @enabled = enabled # Sets default vuln filter - All Events setVulnFilter(VulnFilter.new("50790400",1)) end # Sets the Vulnerability Filter for this alert. def setVulnFilter(vulnFilter) @vulnFilter = vulnFilter end end # TODO: review # # === Description # class ScanFilter attr_reader :scanStop attr_reader :scanFailed attr_reader :scanStart def initialize(scanstop, scanFailed, scanStart) @scanStop = scanStop @scanFailed = scanFailed @scanStart = scanStart end end # TODO: review # === Description # class VulnFilter attr_reader :typeMask attr_reader :maxAlerts attr_reader :severityThreshold def initialize(typeMask, severityThreshold, maxAlerts = -1) @typeMask = typeMask @maxAlerts = maxAlerts @severityThreshold = severityThreshold end end # TODO add engineID # === Description # Object that represents the scanning configuration for a Site. # class ScanConfig # A unique ID for this scan configuration attr_reader :configID # The name of the scan template attr_reader :name # The ID of the scan template used full-audit, exhaustive-audit, web-audit, dos-audit, internet-audit, network-audit attr_reader :templateID # The configuration version (default is 2) attr_reader :configVersion # Array of (Schedule)* attr_reader :schedules # Array of (ScanTrigger)* attr_reader :scanTriggers def initialize(configID, name, templateID, configVersion = 2) @configID = configID @name = name @templateID = templateID @configVersion = configVersion @schedules = Array.new() @scanTriggers = Array.new() end # Adds a new Schedule for this ScanConfig def addSchedule(schedule) @schedules.push(schedule) end # Adds a new ScanTrigger to the scanTriggers array def addScanTrigger(scanTrigger) @scanTriggers.push(scanTrigger) end def _set_configID(configID) @configID = configID end def _set_name(name) @name = name end end # === Description # Object that holds a scan schedule # class Schedule # Type of Schedule (daily|hourly|monthly|weekly) attr_reader :type # The schedule interval attr_reader :interval # The date and time to start the first scan attr_reader :start # Enable or disable this schedule attr_reader :enabled # The date and time to disable to schedule. If null then the schedule will run forever. attr_reader :notValidAfter # Scan on the same date each time attr_reader :byDate def initialize(type, interval, start, enabled = 1) @type = type @interval = interval @start = start @enabled = enabled end end # === Description # Object that holds an event that triggers the start of a scan. # class ScanTrigger # Type of Trigger (AutoUpdate) attr_reader :type # Enable or disable this scan trigger attr_reader :enabled # Sets the trigger to start an incremental scan or a full scan attr_reader :incremental def initialize(type, incremental, enabled = 1) @type = type @incremental = incremental @enabled = enabled end end # === Description # Object that represents a single device in an NSC. # class Device # A unique device ID (assigned by the NSC) attr_reader :id # The site ID of this devices site attr_reader :site_id # IP Address or Hostname of this device attr_reader :address # User assigned risk multiplier attr_reader :riskfactor # NeXpose risk score attr_reader :riskscore def initialize(id, site_id, address, riskfactor=1, riskscore=0) @id = id @site_id = site_id @address = address @riskfactor = riskfactor @riskscore = riskscore end end # === Description # Object that represents a summary of a scan. # class ScanSummary # The Scan ID of the Scan attr_reader :scan_id # The Engine ID used to perform the scan attr_reader :engine_id # TODO: add description attr_reader :name # The scan start time attr_reader :startTime # The scan finish time attr_reader :endTime # The scan status (running|finished|stopped|error| dispatched|paused|aborted|uknown) attr_reader :status # The number of pending tasks attr_reader :tasks_pending # The number of active tasks attr_reader :tasks_active # The number of completed tasks attr_reader :tasks_completed # The number of "live" nodes attr_reader :nodes_live # The number of "dead" nodes attr_reader :nodes_dead # The number of filtered nodes attr_reader :nodes_filtered # The number of unresolved nodes attr_reader :nodes_unresolved # The number of "other" nodes attr_reader :nodes_other # Confirmed vulnerabilities found (indexed by severity) # Associative array, indexed by severity attr_reader :vuln_exploit # Unconfirmed vulnerabilities found (indexed by severity) # Associative array, indexed by severity attr_reader :vuln_version # Not vulnerable checks run (confirmed) attr_reader :not_vuln_exploit # Not vulnerable checks run (unconfirmed) attr_reader :not_vuln_version # Vulnerability check errors attr_reader :vuln_error # Vulnerability checks disabled attr_reader :vuln_disabled # Vulnerability checks other attr_reader :vuln_other # Constructor # ScanSummary(can_id, $engine_id, $name, tartTime, $endTime, tatus) def initialize(scan_id, engine_id, name, startTime, endTime, status) @scan_id = scan_id @engine_id = engine_id @name = name @startTime = startTime @endTime = endTime @status = status end end # TODO # === Description # Object that represents the overview statistics for a particular scan. # # === Examples # # # Create a new Nexpose Connection on the default port and Login # nsc = Connection.new("10.1.40.10","nxadmin","password") # nsc.login() # # # Get a Site (Site ID = 12) from the NSC # site = new Site(nsc,12) # # # Start a Scan of this site and pause for 1 minute # scan1 = site.scanSite() # sleep(60) # # # Get the Scan Statistics for this scan # scanStatistics = new ScanStatistics(nsc,scan1["scan_id"]) # # # Print out number of confirmed vulnerabilities with a 10 severity # puts scanStatistics.scansummary.vuln_exploit[10] # # # Print out the number of pending tasks left in the scan # puts scanStatistics.scan_summary.tasks_pending # class ScanStatistics # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :reseponse_xml # The Scan ID attr_reader :scan_id # The ScanSummary of the scan attr_reader :scan_summary # The NSC Connection associated with this object attr_reader :connection # Vulnerability checks other attr_reader :vuln_other def initialize(connection, scan_id) @error = false @connection = connection @scan_id = scan_id end end # ==== Description # Object that represents a listing of all of the scan engines available on to an NSC. # class EngineListing # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # Array containing (EngineSummary*) attr_reader :engines # The number of scan engines attr_reader :engine_count # Constructor # EngineListing (connection) def initialize(connection) @connection = connection end end # ==== Description # Object that represents the summary of a scan engine. # # ==== Examples # # # Create a new Nexpose Connection on the default port and Login # nsc = Connection.new("10.1.40.10","nxadmin","password") # nsc.login() # # # Get the engine listing for the connection # enginelisting = EngineListing.new(nsc) # # # Print out the status of the first scan engine # puts enginelisting.engines[0].status # class EngineSummary # A unique ID that identifies this scan engine attr_reader :id # The name of this scan engine attr_reader :name # The hostname or IP address of the engine attr_reader :address # The port there the engine is listening attr_reader :port # The engine status (active|pending-auth| incompatible|not-responding|unknown) attr_reader :status # Constructor # EngineSummary(id, name, address, port, status) def initialize(id, name, address, port, status) @id = id @name = name @address = address @port = port @status = status end end # TODO class EngineActivity # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # The Engine ID attr_reader :engine_id # Array containing (ScanSummary*) attr_reader :scan_summaries end # === Description # Object that represents a listing of all of the vulnerabilities in the vulnerability database # class VulnerabilityListing # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # Array containing (VulnerabilitySummary*) attr_reader :vulnerability_summaries # The number of vulnerability definitions attr_reader :vulnerability_count # Constructor # VulnerabilityListing(connection) def initialize(connection) @error = false @vulnerability_summaries = [] @connection = connection r = @connection.execute('') if (r.success) response.elements.each('VulnerabilityListingResponse/VulnerabilitySummary') do |v| @vulnerability_summaries.push(VulnerabilitySummary.new(v.attributes['id'],v.attributes["title"],v.attributes["severity"])) end else @error = true @error_msg = 'VulnerabilitySummaryRequest Parse Error' end @vulnerability_count = @vulnerability_summaries.length end end # === Description # Object that represents the summary of an entry in the vulnerability database # class VulnerabilitySummary # The unique ID string for this vulnerability attr_reader :id # The title of this vulnerability attr_reader :title # The severity of this vulnerability (1 – 10) attr_reader :severity # Constructor # VulnerabilitySummary(id, title, severity) def initialize(id, title, severity) @id = id @title = title @severity = severity end end # === Description # class Reference attr_reader :source attr_reader :reference def initialize(source, reference) @source = source @reference = reference end end # === Description # Object that represents the details for an entry in the vulnerability database # class VulnerabilityDetail # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # The unique ID string for this vulnerability attr_reader :id # The title of this vulnerability attr_reader :title # The severity of this vulnerability (1 – 10) attr_reader :severity # The pciSeverity of this vulnerability attr_reader :pciSeverity # The CVSS score of this vulnerability attr_reader :cvssScore # The CVSS vector of this vulnerability attr_reader :cvssVector # The date this vulnerability was published attr_reader :published # The date this vulnerability was added to NeXpose attr_reader :added # The last date this vulnerability was modified attr_reader :modified # The HTML Description of this vulnerability attr_reader :description # External References for this vulnerability # Array containing (Reference) attr_reader :references # The HTML Solution for this vulnerability attr_reader :solution # Constructor # VulnerabilityListing(connection,id) def initialize(connection, id) @error = false @connection = connection @id = id @references = [] r = @connection.execute('') if (r.success) r.res.elements.each('VulnerabilityDetailsResponse/Vulnerability') do |v| @id = v.attributes['id'] @title = v.attributes["title"] @severity = v.attributes["severity"] @pciSeverity = v.attributes['pciSeverity'] @cvssScore = v.attributes['cvssScore'] @cvssVector = v.attributes['cvssVector'] @published = v.attributes['published'] @added = v.attributes['added'] @modified = v.attributes['modified'] v.elements.each('description') do |d| @description = d.to_s.gsub(/\<\/?description\>/i, '') end v.elements.each('solution') do |s| @solution = s.to_s.gsub(/\<\/?solution\>/i, '') end v.elements.each('references/reference') do |r| @references.push(Reference.new(r.attributes['source'],r.text)) end end else @error = true @error_msg = 'VulnerabilitySummaryRequest Parse Error' end end end # === Description # Object that represents the summary of a Report Configuration. # class ReportConfigSummary # The Report Configuration ID attr_reader :id # A unique name for the Report attr_reader :name # The report format attr_reader :format # The date of the last report generation attr_reader :last_generated_on # Relative URI of the last generated report attr_reader :last_generated_uri # Constructor # ReportConfigSummary(id, name, format, last_generated_on, last_generated_uri) def initialize(id, name, format, last_generated_on, last_generated_uri) @id = id @name = name @format = format @last_generated_on = last_generated_on @last_generated_uri = last_generated_uri end end # === Description # Object that represents the schedule on which to automatically generate new reports. class ReportHistory # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # The report definition (report config) ID # Report definition ID attr_reader :config_id # Array (ReportSummary*) attr_reader :report_summaries def initialize(connection, config_id) @error = false @connection = connection @config_id = config_id @report_summaries = [] reportHistory_request = APIRequest.new('',@connection.geturl()) reportHistory_request.execute() @response_xml = reportHistory_request.response_xml @request_xml = reportHistory_request.request_xml end def xml_parse(response) response = REXML::Document.new(response.to_s) status = response.root.attributes['success'] if (status == '1') response.elements.each('ReportHistoryResponse/ReportSummary') do |r| @report_summaries.push(ReportSummary.new(r.attributes["id"], r.attributes["cfg-id"], r.attributes["status"], r.attributes["generated-on"],r.attributes['report-uri'])) end else @error = true @error_msg = 'Error ReportHistoryReponse' end end end # === Description # Object that represents the summary of a single report. class ReportSummary # The Report ID attr_reader :id # The Report Configuration ID attr_reader :cfg_id # The status of this report # available | generating | failed attr_reader :status # The date on which this report was generated attr_reader :generated_on # The relative URI of the report attr_reader :report_uri def initialize(id, cfg_id, status, generated_on, report_uri) @id = id @cfg_id = cfg_id @status = status @generated_on = generated_on @report_uri = report_uri end end # === Description # class ReportAdHoc attr_reader :error attr_reader :error_msg attr_reader :connection # Report Template ID strong e.g. full-audit attr_reader :template_id # pdf|html|xml|text|csv|raw-xml attr_reader :format # Array of (ReportFilter)* attr_reader :filters attr_reader :request_xml attr_reader :response_xml attr_reader :report_decoded def initialize(connection, template_id = 'full-audit', format = 'raw-xml') @error = false @connection = connection @xml_tag_stack = array() @filters = Array.new() @template_id = template_id @format = format end def addFilter(filter_type, id) # filter_type can be site|group|device|scan # id is the ID number. For scan, you can use 'last' for the most recently run scan filter = new ReportFilter.new(filter_type,id) filters.push(filter) end def generate() request_xml = '' request_xml += '' request_xml += '' @filters.each do |f| request_xml += '' end request_xml += '' request_xml += '' request_xml += '' myReportAdHoc_request = APIRequest.new(request_xml, @connection.geturl()) myReportAdHoc_request.execute() myReportAdHoc_response = myReportAdHoc_request.response_xml end end # === Description # Object that represents the configuration of a report definition. # class ReportConfig # true if an error condition exists; false otherwise attr_reader :error # Error message string attr_reader :error_msg # The last XML request sent by this object attr_reader :request_xml # The last XML response received by this object attr_reader :response_xml # The NSC Connection associated with this object attr_reader :connection # The ID for this report definition attr_reader :config_id # A unique name for this report definition attr_reader :name # The template ID used for this report definition attr_reader :template_id # html, db, txt, xml, raw-xml, csv, pdf attr_reader :format # XXX new attr_reader :timezone # XXX new attr_reader :owner # Array of (ReportFilter)* - The Sites, Asset Groups, or Devices to run the report against attr_reader :filters # Automatically generate a new report at the conclusion of a scan # 1 or 0 attr_reader :generate_after_scan # Schedule to generate reports # ReportSchedule Object attr_reader :schedule # Store the reports on the server # 1 or 0 attr_reader :storeOnServer # Location to store the report on the server attr_reader :store_location # Form to send the report via email # "file", "zip", "url", or NULL (don’t send email) attr_reader :email_As # Send the Email to all Authorized Users # boolean - Send the Email to all Authorized Users attr_reader :email_to_all # Array containing the email addresses of the recipients attr_reader :email_recipients # IP Address or Hostname of SMTP Relay Server attr_reader :smtp_relay_server # Sets the FROM field of the Email attr_reader :sender # TODO attr_reader :db_export # TODO attr_reader :csv_export # TODO attr_reader :xml_export def initialize(connection, config_id = -1) @error = false @connection = connection @config_id = config_id @xml_tag_stack = Array.new() @filters = Array.new() @email_recipients = Array.new() @name = "New Report " + rand(999999999).to_s r = @connection.execute('') if (r.success) r.res.elements.each('ReportConfigResponse/ReportConfig') do |r| @name = r.attributes['name'] @format = r.attributes['format'] @timezone = r.attributes['timezone'] @id = r.attributes['id'] @template_id = r.attributes['template-id'] @owner = r.attributes['owner'] end else @error = true @error_msg = 'Error ReportHistoryReponse' end end # === Description # Generate a new report on this report definition. Returns the new report ID. def generateReport(debug = false) return generateReport(@connection, @config_id, debug) end # === Description # Save the report definition to the NSC. # Returns the config-id. def saveReport() r = @connection.execute('' + getXML().to_s + ' ') if(r.success) @config_id = r.attributes['reportcfg-id'] return true end return false end # === Description # Adds a new filter to the report config def addFilter(filter_type, id) filter = ReportFilter.new(filter_type,id) @filters.push(filter) end # === Description # Adds a new email recipient def addEmailRecipient(recipient) @email_recipients.push(recipient) end # === Description # Sets the schedule for this report config def setSchedule(schedule) @schedule = schedule end def getXML() xml = '' xml += ' ' @filters.each do |f| xml += ' <' + f.type.to_s + ' id="' + f.id.to_s + '"/>' end xml += ' ' xml += ' ' if (@schedule) xml += ' ' end xml += ' ' xml += ' ' xml += ' ' if (@store_location and @store_location.length > 0) xml += ' ' + @store_location.to_s + '' end xml += ' ' xml += ' ' xml += ' ' return xml end def set_name(name) @name = name end def set_template_id(template_id) @template_id = template_id end def set_format(format) @format = format end def set_email_As(email_As) @email_As = email_As end def set_storeOnServer(storeOnServer) @storeOnServer = storeOnServer end def set_smtp_relay_server(smtp_relay_server) @smtp_relay_server = smtp_relay_server end def set_sender(sender) @sender = sender end def set_generate_after_scan(generate_after_scan) @generate_after_scan = generate_after_scan end end # === Description # Object that represents a report filter which determines which sites, asset # groups, and/or devices that a report is run against. gtypes are # "SiteFilter", "AssetGroupFilter", "DeviceFilter", or "ScanFilter". gid is # the site-id, assetgroup-id, or devce-id. ScanFilter, if used, specifies # a specifies a specific scan to use as the data source for the report. The gid # can be a specific scan-id or "first" for the first run scan, or “last” for # the last run scan. # class ReportFilter attr_reader :type attr_reader :id def initialize(type, id) @type = type @id = id end end # === Description # Object that represents the schedule on which to automatically generate new reports. # class ReportSchedule # The type of schedule # (daily, hourly, monthly, weekly) attr_reader :type # The frequency with which to run the scan attr_reader :interval # The earliest date to generate the report attr_reader :start def initialize(type, interval, start) @type = type @interval = interval @start = start end end class ReportTemplateListing attr_reader :error_msg attr_reader :error attr_reader :request_xml attr_reader :response_xml attr_reader :connection attr_reader :xml_tag_stack attr_reader :report_template_summaries#; //Array (ReportTemplateSummary*) def ReportTemplateListing(connection) @error = nil @connection = connection @report_template_summaries = Array.new() r = @connection.execute('') if (r.success) r.res.elements.each('ReportTemplateListingResponse/ReportTemplateSummary') do |r| @report_template_summaries.push(ReportTemplateSumary.new(r.attributes['id'],r.attributes['name'])) end else @error = true @error_msg = 'ReportTemplateListingRequest Parse Error' end end end class ReportTemplateSummary attr_reader :id attr_reader :name attr_reader :description def ReportTemplateSummary(id, name, description) @id = id @name = name @description = description end end class ReportSection attr_reader :name attr_reader :properties def ReportSection(name) @properties = Array.new() @name = name end def addProperty(name, value) @properties[name.to_s] = value end end # TODO add def self.site_device_scan(connection, site_id, device_array, host_array, debug = false) request_xml = '' request_xml += '' device_array.each do |d| request_xml += '' end request_xml += '' request_xml += '' # The host array can only by single IP addresses for now. TODO: Expand to full API Spec. host_array.each do |h| request_xml += '' end request_xml += '' request_xml += '' r = connection.execute(request_xml) r.success ? { :engine_id => r.attributes['engine_id'], :scan_id => r.attributes['scan-id'] } : nil end # === Description # TODO def self.getAttribute(attribute, xml) value = '' #@value = substr(substr(strstr(strstr(@xml,@attribute),'"'),1),0,strpos(substr(strstr(strstr(@xml,@attribute),'"'),1),'"')) return value end # === Description # Returns an ISO 8601 formatted date/time stamp. All dates in NeXpose must use this format. def self.get_iso_8601_date(int_date) #@date_mod = date('Ymd\THis000', @int_date) date_mod = '' return date_mod end # ==== Description # Echos the last XML API request and response for the specified object. (Useful for debugging) def self.printXML(object) puts "request" + object.request_xml.to_s puts "response is " + object.response_xml.to_s end def self.testa(ip, port, user, passwd) nsc = Connection.new(ip, user, passwd, port) nsc.login site_listing = SiteListing.new(nsc) site_listing.sites.each do |site| puts "name is #{site.site_name}" puts "id is #{site.id}" end =begin ## Site Delete ## nsc.login status = deleteSite(nsc, '244', true) puts "status: #{status}" =end =begin nsc.login site = Site.new(nsc) site.setSiteConfig("New Site 3", "New Site Description") site.site_config.addHost(IPRange.new("10.1.90.86")) status = site.saveSite() report_config = ReportConfig.new(nsc) report_config.set_template_id("raw-xml") report_config.set_format("xml") report_config.addFilter("SiteFilter",site.site_id) report_config.set_generate_after_scan(1) report_config.set_storeOnServer(1) report_config.saveReport() puts report_config.config_id.to_s site.scanSite() nsc.logout =end =begin nsc.login site = Site.new(nsc) site.setSiteConfig("New Site 3", "New Site Description") site.site_config.addHost(IPRange.new("10.1.90.86")) status = site.saveSite() report_config = ReportConfig.new(nsc) report_config.set_template_id("audit-report") report_config.set_format("pdf") report_config.addFilter("SiteFilter",site.site_id) report_config.set_email_As("file") report_config.set_smtp_relay_server("") report_config.set_sender("nexpose@rapid7.com") report_config.addEmailRecipient("jabra@rapid7.com") report_config.set_generate_after_scan(1) report_config.saveReport() site.scanSite() =end nsc.logout =begin vuln_listing = VulnerabilityListing.new(nsc) vuln_listing.vulnerability_summaries.each do |v| puts "vuln id #{v.id}" exit end n.logout =end =begin nsc.login vuln_id = 'generic-icmp-timestamp' vuln = VulnerabilityDetail.new(n,vuln_id.to_s) puts "#{vuln.id}" puts "#{vuln.title}" puts "#{vuln.pciSeverity}" puts "#{vuln.cvssScore}" puts "#{vuln.cvssVector}" puts "#{vuln.description}" vuln.references.each do |r| puts "source: #{r.source}" puts "reference: #{r.reference}" end puts "#{vuln.solution}" =end =begin site = Site.new(n) site.setSiteConfig("New Site Name", "New Site Description") site.site_config.addHost(IPRange.new("10.1.90.86")) #site.site_config.addHost(HostName.new("localhost")) #site.site_config.addHost(IPRange.new("192.168.7.1","192.168.7.20")) #site.site_config.addHost(IPRange.new("10.1.90.130")) status = site.saveSite() puts "#{site.site_id}" site.scanSite nsc.logout =end =begin site = Site.new(nsc,'263') site.printSite() site.getSiteXML() puts "#{site.site_id}" puts "#{site.site_config.description}" puts "#{site.site_config.riskfactor}" nsc.logout =end #site.scanSite() =begin site_config = SiteConfig.new() my_site = site_config.getSiteConfig(n, '244') history = SiteScanHistory.new(n, '244') devices = SiteDeviceListing.new(n, '244') =end =begin site_listing = SiteListing.new(n) site_listing.sites.each do |site| puts "name is #{site.site_name}" end =end end =begin def self.test(url,user,pass) xml = " ]> " r = APIRequest.new(xml, url) r.execute puts r.response_xml end # Run the program # Logon, get a session-id, list the sites, then logout. test("http://x.x.x.x:3780", 'nxadmin', 'PASSWORD') =end end