rapid7/metasploit-framework

View on GitHub
lib/msf/core/rpc/v10/rpc_db.rb

Summary

Maintainability
F
2 wks
Test Coverage
# -*- coding: binary -*-
module Msf
module RPC
class RPC_Db < RPC_Base

private

  include Metasploit::Credential::Creation

  def db
    self.framework.db.active
  end

  def find_workspace(wspace = nil)
    if wspace and wspace != ""
      return self.framework.db.find_workspace(wspace) || error(500, "Invalid workspace")
    end
    self.framework.db.workspace
  end

  def fix_cred_options(opts)
    new_opts = fix_options(opts)

    # Convert some of the raw data back to symbols
    if new_opts[:origin_type]
      new_opts[:origin_type] = new_opts[:origin_type].to_sym
    end

    if new_opts[:private_type]
      new_opts[:private_type] = new_opts[:private_type].to_sym
    end

    new_opts
  end

  def fix_options(opts)
    newopts = {}
    opts.each do |k,v|
      newopts[k.to_sym] = v
    end
    newopts
  end

  def opts_to_hosts(opts)
  ::ApplicationRecord.connection_pool.with_connection {
    wspace = find_workspace(opts[:workspace])
    hosts  = []
    if opts[:host] or opts[:address]
      host = opts[:host] || opts[:address]
      hent = wspace.hosts.find_by_address(host)
      return hosts if hent == nil
      hosts << hent if hent.class == ::Mdm::Host
      hosts |= hent if hent.class == Array
    elsif opts[:addresses]
      return hosts if opts[:addresses].class != Array
      conditions = {}
      conditions[:address] = opts[:addresses]
      hent = wspace.hosts.where(conditions)
      hosts |= hent if hent.class == Array
    end
    return hosts
  }
  end

  def opts_to_services(hosts,opts)
  ::ApplicationRecord.connection_pool.with_connection {
    wspace = find_workspace(opts[:workspace])
    services = []
    if opts[:host] or opts[:address] or opts[:addresses]
      return services if hosts.count < 1
      hosts.each do |h|
        if opts[:port] or opts[:proto]
          conditions = {}
          conditions[:port] = opts[:port] if opts[:port]
          conditions[:proto] = opts[:proto] if opts[:proto]
          sret = h.services.where(conditions)
          next if sret == nil
          services |= sret if sret.class == Array
          services << sret if sret.class == ::Mdm::Service
        else
          services |= h.services
        end
      end
    elsif opts[:port] or opts[:proto]
      conditions = {}
      conditions[:port] = opts[:port] if opts[:port]
      conditions[:proto] = opts[:proto] if opts[:proto]
      sret = wspace.services.where(conditions)
      services |= sret if sret.class == Array
      services << sret if sret.class == ::Mdm::Service
    end
    return services
  }
  end

  def db_check
    error(500, "Database Not Loaded") if not db
  end

  def init_db_opts_workspace(xopts)
    db_check
    opts = fix_options(xopts)
    opts[:workspace] = find_workspace(opts[:workspace])
    return opts, opts[:workspace]
  end

  def get_notes(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    notes = []

    host = self.framework.db.get_host(opts)
    return notes if not host

    if opts[:proto] && opts[:port]
      services = []
      nret = host.services.find_by_proto_and_port(opts[:proto], opts[:port])
      return ret if nret == nil
      services << nret if nret.class == ::Mdm::Service
      services |= nret if nret.class == Array

      services.each do |s|
        nret = nil
        if opts[:ntype]
          nret = s.notes.find_by_ntype(opts[:ntype])
        else
          nret = s.notes
        end
        next if nret == nil
        notes << nret if nret.class == ::Mdm::Note
        notes |= nret if nret.class == Array
      end
    else
      notes = host.notes
    end
    notes
  }
  end

public


  # Creates a cracked credential.
  #
  # @param [Hash] xopts Credential options. (See #create_credential Documentation)
  # @return [Metasploit::Credential::Core]
  # @see https://github.com/rapid7/metasploit-credential/blob/master/lib/metasploit/credential/creation.rb#L107 #create_credential Documentation.
  # @see #rpc_create_credential
  # @example Here's how you would use this from the client:
  #  opts = {
  #    username: username,
  #    password: password,
  #    core_id: core_id
  #  }
  #  rpc.call('db.create_cracked_credential', opts)
  def rpc_create_cracked_credential(xopts)
    opts = fix_cred_options(xopts)
    create_cracked_credential(opts)
  end


  # Creates a credential.
  #
  # @param [Hash] xopts Credential options. (See #create_credential Documentation)
  # @return [Hash] Credential data. It contains the following keys:
  #  * 'username' [String] Username saved.
  #  * 'private' [String] Password saved.
  #  * 'private_type' [String] Password type.
  #  * 'realm_value' [String] Realm.
  #  * 'realm_key' [String] Realm key.
  #  * 'host' [String] Host (Only avilable if there's a :last_attempted_at and :status)
  #  * 'sname' [String] Service name (only available if there's a :last_attempted_at and :status)
  #  * 'status' [Status] Login status (only available if there's a :last_attempted_at and :status)
  # @see https://github.com/rapid7/metasploit-credential/blob/master/lib/metasploit/credential/creation.rb#L107 #create_credential Documentation.
  # @example Here's how you would use this from the client:
  #  opts = {
  #   origin_type: :service,
  #   address: '192.168.1.100',
  #   port: 445,
  #   service_name: 'smb',
  #   protocol: 'tcp',
  #   module_fullname: 'auxiliary/scanner/smb/smb_login',
  #   workspace_id: myworkspace_id,
  #   private_data: 'password1',
  #   private_type: :password,
  #   username: 'Administrator'
  #  }
  #  rpc.call('db.create_cracked_credential', opts)
  def rpc_create_credential(xopts)
    opts = fix_cred_options(xopts)
    core = create_credential(opts)

    ret = {
        username: core.public.try(:username),
        private: core.private.try(:data),
        private_type: core.private.try(:type),
        realm_value: core.realm.try(:value),
        realm_key: core.realm.try(:key)
    }

    if opts[:last_attempted_at] && opts[:status]
      opts[:core] = core
      opts[:last_attempted_at] = opts[:last_attempted_at].to_datetime
      login = create_credential_login(opts)

      ret[:host]   = login.service.host.address,
      ret[:sname]  = login.service.name
      ret[:status] = login.status
    end
    ret
  end


  # Sets the status of a login credential to a failure.
  #
  # @param [Hash] xopts Credential data (See #invalidate_login Documentation)
  # @raise [Msf::RPC::Exception] If there's an option missing.
  # @return [void]
  # @see https://github.com/rapid7/metasploit-credential/blob/master/lib/metasploit/credential/creation.rb#L492 #invalidate_login Documentation
  # @see https://github.com/rapid7/metasploit-model/blob/master/lib/metasploit/model/login/status.rb Status symbols.
  # @example Here's how you would use this from the client:
  #  opts = {
  #    address: '192.168.1.100',
  #    port: 445,
  #    protocol: 'tcp',
  #    public: 'admin',
  #    private: 'password1',
  #    status: 'Incorrect'
  #  }
  #  rpc.call('db.invalidate_login', opts)
  def rpc_invalidate_login(xopts)
    opts = fix_cred_options(xopts)
    invalidate_login(opts)
  end


  # Returns login credentials from a specific workspace.
  #
  # @param [Hash] xopts Options:
  # @option xopts [String] :workspace Name of the workspace.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] Credentials with the following hash key:
  #  * 'creds' [Array<Hash>] An array of credentials. Each hash in the array will have the following:
  #    * 'user' [String] Username.
  #    * 'pass' [String] Password.
  #    * 'updated_at' [Integer] Last updated at.
  #    * 'type' [String] Password type.
  #    * 'host' [String] Host.
  #    * 'port' [Integer] Port.
  #    * 'proto' [String] Protocol.
  #    * 'sname' [String] Service name.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.creds', {})
  def rpc_creds(xopts)
    ::ApplicationRecord.connection_pool.with_connection {
      ret = {}
      ret[:creds] = []
      opts, wspace = init_db_opts_workspace(xopts)
      limit = opts.delete(:limit) || 100
      offset = opts.delete(:offset) || 0
      query = Metasploit::Credential::Core.where(
        workspace_id: wspace
      ).offset(offset).limit(limit)
      query.each do |cred|
        host = ''
        port = 0
        proto = ''
        sname = ''

        unless cred.logins.empty?
          login = cred.logins.first
          host = login.service.host.address.to_s
          sname = login.service.name.to_s if login.service.name.present?
          port = login.service.port.to_i
          proto = login.service.proto.to_s
        end

        updated_at = nil
        pass = nil
        type = nil

        unless cred.private.nil?
          updated_at = cred.private.updated_at.to_i
          pass = cred.private.data.to_s
          type = cred.private.type.to_s
        else
          updated_at = cred.public.updated_at.to_i
        end

        ret[:creds] << {
          :user => cred.public.username.to_s,
          :pass => pass,
          :updated_at => updated_at,
          :type => type,
          :host => host,
          :port => port,
          :proto => proto,
          :sname => sname
        }
      end
      ret
    }
  end


  # Delete credentials from a specific workspace.
  #
  # @param [Hash] xopts Options:
  # @option xopts [String] :workspace Name of the workspace.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] Credentials with the following hash key:
  #  * 'result' [String] A message that says 'success'.
  #  * 'deleted' [Array<Hash>] An array of credentials. Each hash in the array will have the following:
  #    * 'user' [String] Username.
  #    * 'pass' [String] Password.
  #    * 'updated_at' [Integer] Last updated at.
  #    * 'type' [String] Password type.
  #    * 'host' [String] Host.
  #    * 'port' [Integer] Port.
  #    * 'proto' [String] Protocol.
  #    * 'sname' [String] Service name.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.del_creds', {})
  def rpc_del_creds(xopts)
    ::ApplicationRecord.connection_pool.with_connection {
      deleted = []
      ret = {}
      ret[:creds] = []
      opts, wspace = init_db_opts_workspace(xopts)
      limit = opts.delete(:limit) || 100
      offset = opts.delete(:offset) || 0
      query = Metasploit::Credential::Core.where(
        workspace_id: wspace
      ).offset(offset).limit(limit)
      query.each do |cred|
        host = ''
        port = 0
        proto = ''
        sname = ''
        unless cred.logins.empty?
          login = cred.logins.first
          host = login.service.host.address.to_s
          sname = login.service.name.to_s if login.service.name.present?
          port = login.service.port.to_i
          proto = login.service.proto.to_s
        end
        ret[:creds] << {
                :user => cred.public.username.to_s,
                :pass => cred.private.data.to_s,
                :updated_at => cred.private.updated_at.to_i,
                :type => cred.private.type.to_s,
                :host => host,
                :port => port,
                :proto => proto,
                :sname => sname}
        deleted << ret
        cred.destroy
      end
      return { :result => 'success', :deleted => deleted }
    }
  end


  # Returns information about hosts.
  #
  # @param [Hash] xopts Options:
  # @option xopts [String] :workspace Name of the workspace.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] Host information that starts with the following hash key:
  #  * 'hosts' [Array<Hash>] An array of hosts. Each hash in the array will have the following:
  #    * 'created_at' [Integer] Creation date.
  #    * 'address' [String] IP address.
  #    * 'mac' [String] MAC address.
  #    * 'name' [String] Computer name.
  #    * 'state' [String] Host's state.
  #    * 'os_name' [String] Name of the operating system.
  #    * 'os_flavor' [String] OS flavor.
  #    * 'os_sp' [String] Service pack.
  #    * 'os_lang' [String] OS language.
  #    * 'updated_at' [Integer] Last updated at.
  #    * 'purpose' [String] Host purpose (example: server)
  #    * 'info' [String] Additional information about the host.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.hosts', {})
  def rpc_hosts(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)

    conditions = {}
    conditions[:state] = [Msf::HostState::Alive, Msf::HostState::Unknown] if opts[:only_up]
    conditions[:address] = opts[:addresses] if opts[:addresses]

    limit = opts.delete(:limit) || 100
    offset = opts.delete(:offset) || 0

    ret = {}
    ret[:hosts] = []
    wspace.hosts.where(conditions).offset(offset).order(:address).limit(limit).each do |h|
      host = {}
      host[:created_at] = h.created_at.to_i
      host[:address] = h.address.to_s
      host[:mac] = h.mac.to_s
      host[:name] = h.name.to_s
      host[:state] = h.state.to_s
      host[:os_name] = h.os_name.to_s
      host[:os_flavor] = h.os_flavor.to_s
      host[:os_sp] = h.os_sp.to_s
      host[:os_lang] = h.os_lang.to_s
      host[:updated_at] = h.updated_at.to_i
      host[:purpose] = h.purpose.to_s
      host[:info] = h.info.to_s
      ret[:hosts]  << host
    end
    ret
  }
  end


  # Returns information about services.
  #
  # @param [Hash] xopts Options:
  # @option xopts [String] :workspace Name of workspace.
  # @option xopts [Integer] :limit Limit.
  # @option xopts [Integer] :offset Offset.
  # @option xopts [String] :proto Protocol.
  # @option xopts [String] :address Address.
  # @option xopts [String] :ports Port range.
  # @option xopts [String] :names Names (Use ',' as the separator).
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash with the following keys:
  #  * 'services' [Array<Hash>] In each hash of the array, you will get these keys:
  #    * 'host' [String] Host.
  #    * 'created_at' [Integer] Last created at.
  #    * 'updated_at' [Integer] Last updated at.
  #    * 'port' [Integer] Port.
  #    * 'proto' [String] Protocol.
  #    * 'state' [String] Service state.
  #    * 'name' [String] Service name.
  #    * 'info' [String] Additional information about the service.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.services', {})
  def rpc_services( xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    limit = opts.delete(:limit) || 100
    offset = opts.delete(:offset) || 0

    conditions = {}
    conditions[:state] = [Msf::ServiceState::Open] if opts[:only_up]
    conditions[:proto] = opts[:proto] if opts[:proto]
    conditions["hosts.address"] = opts[:addresses] if opts[:addresses]
    conditions[:port] = Rex::Socket.portspec_to_portlist(opts[:ports]) if opts[:ports]
    conditions[:name] = opts[:names].strip().split(",") if opts[:names]

    ret = {}
    ret[:services] = []

    wspace.services.includes(:host).where(conditions).offset(offset).limit(limit).each do |s|
      service = {}
      host = s.host
      service[:host] = host.address || "unknown"
      service[:created_at] = s[:created_at].to_i
      service[:updated_at] = s[:updated_at].to_i
      service[:port] = s[:port]
      service[:proto] = s[:proto].to_s
      service[:state] = s[:state].to_s
      service[:name] = s[:name].to_s
      service[:info] = s[:info].to_s
      ret[:services] << service
    end
    ret
  }
  end


  # Returns information about reported vulnerabilities.
  #
  # @param [Hash] xopts Options:
  # @option xopts [String] :workspace Name of workspace.
  # @option xopts [Integer] :limit Limit.
  # @option xopts [Integer] :offset Offset.
  # @option xopts [String] :proto Protocol.
  # @option xopts [String] :address Address.
  # @option xopts [String] :ports Port range.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash with the following key:
  #  * 'vulns' [Array<Hash>] In each hash of the array, you will get these keys:
  #    * 'port' [Integer] Port.
  #    * 'proto' [String] Protocol.
  #    * 'time' [Integer] Time reported.
  #    * 'host' [String] Vulnerable host.
  #    * 'name' [String] Exploit that was used.
  #    * 'refs' [String] Vulnerability references.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.vulns', {})
  def rpc_vulns(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    limit = opts.delete(:limit) || 100
    offset = opts.delete(:offset) || 0

    conditions = {}
    conditions["hosts.address"] = opts[:address] if opts[:address]
    conditions[:name] = opts[:names].strip().split(",") if opts[:names]
    conditions["services.port"] = Rex::Socket.portspec_to_portlist(opts[:ports]) if opts[:port]
    conditions["services.proto"] = opts[:proto] if opts[:proto]

    ret = {}
    ret[:vulns] = []
    wspace.vulns.includes(:service).where(conditions).offset(offset).limit(limit).each do |v|
      vuln = {}
      reflist = v.refs.map { |r| r.name }
      if v.service
        vuln[:port] = v.service.port
        vuln[:proto] = v.service.proto
      else
        vuln[:port] = nil
        vuln[:proto] = nil
      end
      vuln[:time] = v.created_at.to_i
      vuln[:host] = v.host.address || nil
      vuln[:name] = v.name
      vuln[:refs] = reflist.join(',')
      ret[:vulns] << vuln
    end
    ret
  }
  end


  # Returns information about workspaces.
  #
  # @raise [Msf::RPC::Exception] Database not loaded.
  # @return [Hash] A hash with the following key:
  #  * 'workspaces' [Array<Hash>] In each hash of the array, you will get these keys:
  #    * 'id' [Integer] Workspace ID.
  #    * 'name' [String] Workspace name.
  #    * 'created_at' [Integer] Last created at.
  #    * 'updated_at' [Integer] Last updated at.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.workspaces')
  def rpc_workspaces
    db_check

    res = {}
    res[:workspaces] = []
    self.framework.db.workspaces.each do |j|
      ws = {}
      ws[:id] = j.id
      ws[:name] = j.name
      ws[:created_at] = j.created_at.to_i
      ws[:updated_at] = j.updated_at.to_i
      res[:workspaces] << ws
    end
    res
  end


  # Returns the current workspace.
  #
  # @raise [Msf::RPC::Exception] Database not loaded. Try: rpc.call('console.create')
  # @return [Hash] A hash with the following keys:
  #  * 'workspace' [String] Workspace name.
  #  * 'workspace_id' [Integer] Workspace ID.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.current_workspace')
  def rpc_current_workspace
    db_check
    { "workspace" => self.framework.db.workspace.name, "workspace_id" => self.framework.db.workspace.id }
  end


  # Returns the current workspace.
  #
  # @param [String] wspace workspace name.
  # @raise [Msf::RPC::Exception] You might get one of the following errors:
  #  * 500 Database not loaded.
  #  * 500 Invalid workspace.
  # @return [Hash] A hash with the following key:
  #  * 'workspace' [Array<Hash>] In each hash of the array, you will get these keys:
  #    * 'name' [String] Workspace name.
  #    * 'id' [Integer] Workspace ID.
  #    * 'created_at' [Integer] Last created at.
  #    * 'updated_at' [Integer] Last updated at.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.get_workspace', 'default')
  def rpc_get_workspace(wspace)
    db_check
    wspace = find_workspace(wspace)
    ret = {}
    ret[:workspace] = []
    if wspace
      w = {}
      w[:name] = wspace.name
      w[:id] = wspace.id
      w[:created_at] = wspace.created_at.to_i
      w[:updated_at] = wspace.updated_at.to_i
      ret[:workspace] << w
    end
    ret
  end


  # Sets a workspace.
  #
  # @param [String] wspace Workspace name.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace
  #  * 404 Workspace not found.
  # @return [Hash] A hash indicating whether the action was successful or not. You will get:
  #  * 'result' [String] A message that says either 'success' or 'failed'
  # @example Here's how you would use this from the client:
  #  # This will set the current workspace to 'default'
  #  rpc.call('db.set_workspace', 'default')
  def rpc_set_workspace(wspace)
  ::ApplicationRecord.connection_pool.with_connection {
    db_check
    workspace = find_workspace(wspace)
    if workspace
      self.framework.db.workspace = workspace
      return { 'result' => "success" }
    end
    { 'result' => 'failed' }
  }
  end


  # Deletes a workspace.
  #
  # @param [String] wspace Workspace name.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 404 Workspace not found.
  # @return [Hash] A hash indicating the action was successful. It contains the following:
  #  * 'result' [String] A message that says 'success'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.del_workspace', 'temp_workspace')
  def rpc_del_workspace(wspace)
  ::ApplicationRecord.connection_pool.with_connection {
    db_check
    # Delete workspace
    workspace = find_workspace(wspace)
    if workspace.nil?
      error(404, "Workspace not found: #{wspace}")
    elsif workspace.default?
      workspace.destroy
      workspace = self.framework.db.add_workspace(workspace.name)
    else
      # switch to the default workspace if we're about to delete the current one
      self.framework.db.workspace = self.framework.db.default_workspace if self.framework.db.workspace.name == workspace.name
      # now destroy the named workspace
      workspace.destroy
    end
    { 'result' => "success" }
  }
  end


  # Adds a new workspace.
  #
  # @param [String] wspace Workspace name.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash indicating whether the action was successful or not. You get:
  #  * 'result' [String] A message that says either 'success' or 'failed'.
  # @example Here's how you would use this from the client:
  #  * rpc.call('db.add_workspace', 'my_new_workspace')
  def rpc_add_workspace(wspace)
  ::ApplicationRecord.connection_pool.with_connection {
    db_check
    wspace = self.framework.db.add_workspace(wspace)
    return { 'result' => 'success' } if wspace
    { 'result' => 'failed' }
  }
  end


  # Returns information about a host.
  #
  # @param [Hash] xopts Options (:addr, :address, :host are the same thing, and you only need one):
  # @option xopts [String] :workspace Name of the workspace.
  # @option xopts [String] :addr Host address.
  # @option xopts [String] :address Same as :addr.
  # @option xopts [String] :host Same as :address.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'host' [Array<Hash>] Each hash in the array contains the following:
  #    * 'created_at' [Integer] Last created at.
  #    * 'address' [String] Address.
  #    * 'mac' [String] Mac address.
  #    * 'name' [String] Host name.
  #    * 'state' [String] Host state.
  #    * 'os_name' [String] OS name.
  #    * 'os_flavor' [String] OS flavor.
  #    * 'os_sp' [String] OS service pack.
  #    * 'os_lang' [String] OS language.
  #    * 'updated_at' [Integer] Last updated at.
  #    * 'purpose' [String] Purpose. Example: 'server'.
  #    * 'info' [String] Additional information.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.get_host', {:workspace => 'default', :host => ip})
  def rpc_get_host(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)

    ret = {}
    ret[:host] = []
    opts = fix_options(xopts)
    h = self.framework.db.get_host(opts)
    if h
      host = {}
      host[:created_at] = h.created_at.to_i
      host[:address] = h.address.to_s
      host[:mac] = h.mac.to_s
      host[:name] = h.name.to_s
      host[:state] = h.state.to_s
      host[:os_name] = h.os_name.to_s
      host[:os_flavor] = h.os_flavor.to_s
      host[:os_sp] = h.os_sp.to_s
      host[:os_lang] = h.os_lang.to_s
      host[:updated_at] = h.updated_at.to_i
      host[:purpose] = h.purpose.to_s
      host[:info] = h.info.to_s
      ret[:host] << host
    end
    ret
  }
  end

  # Returns analysis of module suggestions for known data about a host.
  #
  # @param [Hash] xopts Options (:addr, :address, :host are the same thing, and you only need one):
  # @option xopts [String] :workspace Name of the workspace.
  # @option xopts [String] :addr Host address.
  # @option xopts [String] :address Same as :addr.
  # @option xopts [String] :host Same as :address.
  # @option xopts [Map<String, Object>]  :analyze_options  All returned modules will support these options
  # * [Array<Sting>] :payloads Modules returned will be compatible with at least one payload
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'host' [Array<Hash>] Each hash in the array contains the following:
  #    * 'address' [String] Address.
  #    * 'modules' [Array<Hash>] Each hash in the array modules contains the following:
  #      * 'mtype' [String] Module type.
  #      * 'mname' [String] Module name. For example: 'windows/wlan/wlan_profile'
  # @example Here's how you would use this from the client:
  #  rpc.call('db.analyze_host', {:workspace => 'default', :host => ip})
def rpc_analyze_host(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    _opts, _wspace = init_db_opts_workspace(xopts)
    ret = {
      host: {}
    }
    opts = fix_options(xopts)
    opts.deep_symbolize_keys!
    host = self.framework.db.get_host(opts)
    # consider expanding to support a list of hosts?
    return ret unless host

    # as a consumer options should be a filter object not a top level param
    analyze_options = {}
    analyze_options = opts[:analyze_options] if opts[:analyze_options]
    analyze_result = self.framework.analyze.host(host, **analyze_options)
    # this should just serialize the result as hosts details why did it end up limited?
    module_suggestions = analyze_result[:results].sort_by { |result| result.mod.fullname }
    host_detail = {
      address: host.address,
      modules: module_suggestions.map do |result|
        mod = result.mod
        {
          mtype: mod.type,
          mname: mod.fullname,
          state: result.state,
          description: result.description,
          options: {
            invalid: result.invalid,
            missing: result.missing
          }
        }
      end
    }
    ret[:host] = host_detail
    ret
  }
end


  # Reports a new host to the database.
  #
  # @param [Hash] xopts Information to report about the host. See below:
  # @option xopts [String] :workspace Name of the workspace.
  # @option xopts [String] :host IP address. You must supply this.
  # @option xopts [String] :state One of the Msf::HostState constants. (See Most::HostState Documentation)
  # @option xopts [String] :os_name Something like "Windows", "Linux", or "Mac OS X".
  # @option xopts [String] :os_flavor Something like "Enterprise", "Pro", or "Home".
  # @option xopts [String] :os_sp Something like "SP2".
  # @option xopts [String] :os_lang Something like "English", "French", or "en-US".
  # @option xopts [String] :arch one of the ARCH_* constants. (see ARCH Documentation)
  # @option xopts [String] :mac Mac address.
  # @option xopts [String] :scope Interface identifier for link-local IPv6.
  # @option xopts [String] :virtual_host The name of the VM host software, eg "VMWare", "QEMU", "Xen", etc.
  # @see https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/host_state.rb Most::HostState Documentation.
  # @see https://github.com/rapid7/metasploit-framework/blob/master/lib/rex/constants.rb#L66 ARCH Documentation.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash indicating whether the action was successful or not. It contains the following:
  #  * 'result' [String] A message that says either 'success' or 'failed'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.report_host', {:host => ip})
  def rpc_report_host(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)

    res = self.framework.db.report_host(opts)
    return { :result => 'success' } if res
    { :result => 'failed' }
  }
  end


  # Reports a service to the database.
  #
  # @param [Hash] xopts Information to report about the service. See below:
  # @option xopts [String] :workspace Name of the workspace.
  # @option xopts [String] :host Required. The host where this service is running.
  # @option xopts [String] :port Required. The port where this service listens.
  # @option xopts [String] :proto Required. The transport layer protocol (e.g. tcp, udp).
  # @option xopts [String] :name The application layer protocol (e.g. ssh, mssql, smb).
  # @option xopts [String] :sname An alias for the above
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash indicating whether the action was successful or not. It contains:
  #  * 'result' [String] A message that says either 'success' or 'failed'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.report_service', {:host=>ip, :port=>8181, :proto=>'tcp', :name=>'http'})
  def rpc_report_service(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    res = self.framework.db.report_service(opts)
    return { :result => 'success' } if res
    { :result => 'failed' }
  }
  end


  # Returns information about a service.
  #
  # @param [Hash] xopts Filters for the search, see below:
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :proto Protocol.
  # @option xopts [Integer] :port Port.
  # @option xopts [String] :names Service names.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following key:
  #  * 'service' [Array<Hash>] Each hash in the array contains the following:
  #    * 'host' [String] Host address.
  #    * 'created_at' [Integer] Creation date.
  #    * 'port' [Integer] Port.
  #    * 'proto' [String] Protocol.
  #    * 'state' [String] Service state.
  #    * 'name' [String] Service name.
  #    * 'info' [String] Additional information.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.get_service', {:workspace=>'default', :proto=>'tcp', :port=>443})
  def rpc_get_service(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)

    ret = {}
    ret[:service] = []

    host = self.framework.db.get_host(opts)

    services = []
    sret = nil

    if host && opts[:proto] && opts[:port]
      sret = host.services.find_by_proto_and_port(opts[:proto], opts[:port])
    elsif opts[:proto] && opts[:port]
      conditions = {}
      conditions[:state] = [Msf::ServiceState::Open] if opts[:up]
      conditions[:proto] = opts[:proto] if opts[:proto]
      conditions[:port] = opts[:port] if opts[:port]
      conditions[:name] = opts[:names] if opts[:names]
      sret = wspace.services.where(conditions).order("hosts.address, port").to_a()
    elsif host
      sret = host.services.to_a()
    end
    return ret if sret == nil
    services << sret if sret.class == ::Mdm::Service
    services |= sret if sret.class == Array


    services.each do |s|
      service = {}
      host = s.host
      service[:host] = host.address || "unknown"
      service[:created_at] = s[:created_at].to_i
      service[:updated_at] = s[:updated_at].to_i
      service[:port] = s[:port]
      service[:proto] = s[:proto].to_s
      service[:state] = s[:state].to_s
      service[:name] = s[:name].to_s
      service[:info] = s[:info].to_s
      ret[:service] << service
    end
    ret
  }
  end

  # Returns a note.
  #
  # @param [Hash] xopts Options.
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :addr Host address.
  # @option xopts [String] :address Same as :addr.
  # @option xopts [String] :host Same as :address.
  # @option xopts [String] :proto Protocol.
  # @option xopts [Integer] :port Port.
  # @option xopts [String] :ntype Note type.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'note' [Array<Hash>] Each hash in the array contains the following:
  #    * 'host' [String] Host.
  #    * 'port' [Integer] Port.
  #    * 'proto' [String] Protocol.
  #    * 'created_at' [Integer] Last created at.
  #    * 'updated_at' [Integer] Last updated at.
  #    * 'ntype' [String] Note type.
  #    * 'data' [String] Note data.
  #    * 'critical' [String] A boolean indicating criticality.
  #    * 'seen' [String] A boolean indicating whether the note has been seen before.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.get_note', {:proto => 'tcp', :port => 80})
  def rpc_get_note(xopts)
    ret = {}
    ret[:note] = []

    notes = get_notes(xopts)

    notes.each do |n|
      note = {}
      host = n.host
      note[:host] = host.address || "unknown"
      if n.service
        note[:port] = n.service.port
        note[:proto] = n.service.proto
      end
      note[:created_at] = n[:created_at].to_i
      note[:updated_at] = n[:updated_at].to_i
      note[:ntype] = n[:ntype].to_s
      note[:data] = n[:data]
      note[:critical] = n[:critical].to_s
      note[:seen] = n[:seen].to_s
      ret[:note] << note
    end
    ret
  end


  # Returns information about a client connection.
  #
  # @param [Hash] xopts Options:
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :ua_string User agent string.
  # @option xopts [String] :host Host IP.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the client connection:
  #  * 'client' [Array<Hash>] Each hash of the array contains the following:
  #    * 'host' [String] Host IP.
  #    * 'created_at' [Integer] Created date.
  #    * 'updated_at' [Integer] Last updated at.
  #    * 'ua_string' [String] User-Agent string.
  #    * 'ua_name' [String] User-Agent name.
  #    * 'ua_ver' [String] User-Agent version.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.get_client', {:workspace=>'default', :ua_string=>user_agent, :host=>ip})
  def rpc_get_client(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    ret = {}
    ret[:client] = []
    c = self.framework.db.get_client(opts)
    if c
      client = {}
      host = c.host
      client[:host] = host.address
      client[:created_at] = c.created_at.to_i
      client[:updated_at] = c.updated_at.to_i
      client[:ua_string] = c.ua_string.to_s
      client[:ua_name] = c.ua_name.to_s
      client[:ua_ver] = c.ua_ver.to_s
      ret[:client] << client
    end
    ret
  }
  end


  # Reports a client connection.
  #
  # @param [Hash] xopts Information about the client.
  # @option xopts [String] :workspace Name of the workspace.
  # @option xopts [String] :ua_string Required. User-Agent string.
  # @option xopts [String] :host Required. Host IP.
  # @option xopts [String] :ua_name One of the Msf::HttpClients constants. (See Msf::HttpClient Documentation.)
  # @option xopts [String] :ua_ver Detected version of the given client.
  # @option xopts [String] :campaign An id or Campaign object.
  # @see https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/constants.rb#L52 Msf::HttpClient Documentation.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash indicating whether the action was successful or not. It contains:
  #  * 'result' [String] A message that says either 'success' or 'failed'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.report_client', {:workspace=>'default', :ua_string=>user_agent, :host=>ip})
  def rpc_report_client(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    res = self.framework.db.report_client(opts)
    return { :result => 'success' } if res
    { :result => 'failed' }
  }
  end


  # Reports a note.
  #
  # @param [Hash] xopts Information about the note.
  # @option xopts [String] :type Required. The type of note, e.g. smb_peer_os.
  # @option xopts [String] :workspace The workspace to associate with this note.
  # @option xopts [String] :host An IP address or a Host object to associate with this note.
  # @option xopts [String] :service A Service object to associate with this note.
  # @option xopts [String] :data Whatever it is you're making a note of.
  # @option xopts [Integer] :port Along with +:host+ and +:proto+, a service to associate with this note.
  # @option xopts [String] :proto Along with +:host+ and +:port+, a service to associate with this note.
  # @option xopts [Hash] A hash that contains the following information.
  #  * :unique [Boolean] Allow only a single Note per +:host+/+:type+ pair.
  #  * :unique_data [Boolean] Like +:uniqe+, but also compare +:data+.
  #  * :insert [Boolean] Always insert a new Note even if one with identical values exists.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash indicating whether the action was successful or not. It contains:
  #  * 'result' [String] A message that says either 'success' or 'failed'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.report_note', {:type=>'http_data', :host=>'192.168.1.123', :data=>'data'})
  def rpc_report_note(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    if (opts[:host] or opts[:address]) and opts[:port] and opts[:proto]
      addr = opts[:host] || opts[:address]
      wspace = opts[:workspace] || self.framework.db.workspace
      host = wspace.hosts.find_by_address(addr)
      if host && host.services.count > 0
        service = host.services.find_by_proto_and_port(opts[:proto],opts[:port])
        if service
          opts[:service] = service
        end
      end
    end

    res = self.framework.db.report_note(opts)
    return { :result => 'success' } if res
    { :result => 'failed' }
  }
  end


  # Returns notes from the database.
  #
  # @param [Hash] xopts Filters for the search. See below:
  # @option xopts [String] :workspace Name of the workspace.
  # @option xopts [String] :address Host address.
  # @option xopts [String] :names Names (separated by ',').
  # @option xopts [String] :ntype Note type.
  # @option xopts [String] :proto Protocol.
  # @option xopts [String] :ports Port change.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'notes' [Array<Hash>] Each hash in the array contains the following:
  #    * 'time' [Integer] Creation date.
  #    * 'host' [String] Host address.
  #    * 'service' [String] Service name or port.
  #    * 'type' [String] Host type.
  #    * 'data' [String] Host data.
  # @example Here's how you would use this from the client:
  #  # This gives you all the notes.
  #  rpc.call('db.notes', {})
  def rpc_notes(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    limit = opts.delete(:limit) || 100
    offset = opts.delete(:offset) || 0

    conditions = {}
    conditions["hosts.address"] = opts[:address] if opts[:address]
    conditions[:name] = opts[:names].strip().split(",") if opts[:names]
    conditions[:ntype] = opts[:ntype] if opts[:ntype]
    conditions["services.port"] = Rex::Socket.portspec_to_portlist(opts[:ports]) if opts[:ports]
    conditions["services.proto"] = opts[:proto] if opts[:proto]

    ret = {}
    ret[:notes] = []
    wspace.notes.includes(:host, :service).where(conditions).offset(offset).limit(limit).each do |n|
      note = {}
      note[:time] = n.created_at.to_i
      note[:host] = ""
      note[:service] = ""
      note[:host] = n.host.address if n.host
      note[:service] = n.service.name || n.service.port  if n.service
      note[:type ] = n.ntype.to_s
      note[:data] = n.data.inspect
      ret[:notes] << note
    end
    ret
  }
  end


  # Returns an external vulnerability reference.
  #
  # @param [String] name Reference name.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [String] Reference.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.get_ref', ref_name)
  def rpc_get_ref(name)
  ::ApplicationRecord.connection_pool.with_connection {
    db_check
    self.framework.db.get_ref(name)
  }
  end


  # Deletes vulnerabilities.
  #
  # @param [Hash] xopts Filters that narrow down which vulnerabilities to delete. See below:
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :host Host address.
  # @option xopts [String] :address Same as :host.
  # @option xopts [Array] :addresses Same as :address.
  # @option xopts [Integer] :port Port.
  # @option xopts [String] :proto Protocol.
  # @option xopts [String] :name Name of the vulnerability.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'result' [String] A message that says 'success'.
  #  * 'deleted' [Array<Hash>] Each hash in the array contains the following:
  #    * 'address' [String] Host address.
  #    * 'port' [Integer] Port.
  #    * 'proto' [String] Protocol.
  #    * 'name' [String] Vulnerability name.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.del_vuln', {:host=>ip, :port=>445, :proto=>'tcp'})
  def rpc_del_vuln(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    opts[:workspace] = opts[:workspace].name
    hosts  = []
    services = []
    vulns = []

    if opts[:host] or opts[:address] or opts[:addresses]
      hosts = opts_to_hosts(opts)
    end

    if opts[:port] or opts[:proto]
      if opts[:host] or opts[:address] or opts[:addresses]
        services = opts_to_services(hosts, opts)
      else
        services = opts_to_services([], opts)
      end
    end

    if opts[:port] or opts[:proto]
      services.each do |s|
        vret = nil
        if opts[:name]
          vret = s.vulns.find_by_name(opts[:name])
        else
          vret = s.vulns.to_a()
        end
        next if vret == nil
        vulns << vret if vret.class == ::Mdm::Vuln
        vulns |= vret if vret.class == Array
      end
    elsif opts[:address] or opts[:host] or opts[:addresses]
      hosts.each do |h|
        vret = nil
        if opts[:name]
          vret = h.vulns.find_by_name(opts[:name])
        else
          vret = h.vulns.to_a()
        end
        next if vret == nil
        vulns << vret if vret.class == ::Mdm::Vuln
        vulns |= vret if vret.class == Array
      end
    else
      vret = nil
      if opts[:name]
        vret = wspace.vulns.find_by_name(opts[:name])
      else
        vret = wspace.vulns.to_a()
      end
      vulns << vret if vret.class == ::Mdm::Vuln
      vulns |= vret if vret.class == Array
    end

    deleted = []
    vulns.each do |v|
      dent = {}
      dent[:address] = v.host.address.to_s if v.host
      dent[:port] = v.service.port if v.service
      dent[:proto] = v.service.proto if v.service
      dent[:name] = v.name
      deleted << dent
      v.destroy
    end

    return { :result => 'success', :deleted => deleted }
  }
  end


  # Deletes notes.
  #
  # @param [Hash] xopts Filters to narrow down which notes to delete.
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :host Host address.
  # @option xopts [String] :address Same as :host.
  # @option xopts [Array] :addresses Same as :address.
  # @option xopts [Integer] :port Port.
  # @option xopts [String] :proto Protocol.
  # @option xopts [String] :ntype Note type.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'result' [String] A message that says 'success'.
  #  * 'deleted' [Array<Hash>] Each hash in the array contains the following:
  #    * 'address' [String] Host address.
  #    * 'port' [Integer] Port.
  #    * 'proto' [String] Protocol.
  #    * 'ntype' [String] Note type.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.del_note', {:workspace=>'default', :host=>ip, :port=>443, :proto=>'tcp'})
  def rpc_del_note(xopts)
    notes = get_notes(xopts)

    deleted = []
    notes.each do |n|
      dent = {}
      dent[:address] = n.host.address.to_s if n.host
      dent[:port] = n.service.port if n.service
      dent[:proto] = n.service.proto if n.service
      dent[:ntype] = n.ntype
      deleted << dent
      n.destroy
    end

    return { :result => 'success', :deleted => deleted }
  end


  # Deletes services.
  #
  # @param [Hash] xopts Filters to narrow down which services to delete.
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :host Host address.
  # @option xopts [String] :address Same as :host.
  # @option xopts [Array] :addresses Host addresses.
  # @option xopts [Integer] :port Port.
  # @option xopts [String] :proto Protocol.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'result' [String] A message that says 'success' or 'failed'.
  #  * 'deleted' [Array<Hash>] If result says success, then you will get this key.
  #    Each hash in the array contains:
  #    * 'address' [String] Host address.
  #    * 'port' [Integer] Port.
  #    * 'proto' [String] Protocol.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.del_service', {:host=>ip})
  def rpc_del_service(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    hosts  = []
    services = []
    if opts[:host] or opts[:address]
      host = opts[:host] || opts[:address]
      hent = wspace.hosts.find_by_address(host)
      return { :result => 'failed' } if hent == nil or hent.class != ::Mdm::Host
      hosts << hent
    elsif opts[:addresses]
      return { :result => 'failed' } if opts[:addresses].class != Array
      conditions = { :address => opts[:addresses] }
      hent = wspace.hosts.where(conditions)
      return { :result => 'failed' } if hent == nil
      hosts |= hent if hent.class == Array
      hosts << hent if hent.class == ::Mdm::Host
    end
    if opts[:addresses] or opts[:address] or opts[:host]
      hosts.each do |h|
        sret = nil
        if opts[:port] or opts[:proto]
          conditions = {}
          conditions[:port] = opts[:port] if opts[:port]
          conditions[:proto] = opts[:proto] if opts[:proto]
          sret = h.services.where(conditions)
          next if sret == nil
          services << sret if sret.class == ::Mdm::Service
          services |= sret if sret.class == Array
        else
          services |= h.services
        end
      end
    elsif opts[:port] or opts[:proto]
      conditions = {}
      conditions[:port] = opts[:port] if opts[:port]
      conditions[:proto] = opts[:proto] if opts[:proto]
      sret = wspace.services.where(conditions)
      services << sret if sret and sret.class == ::Mdm::Service
      services |= sret if sret and sret.class == Array
    end

    deleted = []
    services.each do |s|
      dent = {}
      dent[:address] = s.host.address.to_s
      dent[:port] = s.port
      dent[:proto] = s.proto
      deleted << dent
      s.destroy
    end

    return { :result => 'success', :deleted => deleted }
  }
  end


  # Deletes hosts.
  #
  # @param [Hash] xopts Filters to narrow down which hosts to delete.
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :host Host address.
  # @option xopts [String] :address Same as :host.
  # @option xopts [Array] :addresses Host addresses.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'result' [String] A message that says 'success'.
  #  * 'deleted' [Array<String>] All the deleted hosts.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.del_host', {:host=>ip})
  def rpc_del_host(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    db_check
    opts = fix_options(xopts)
    wspace = find_workspace(opts[:workspace])
    hosts  = []
    if opts[:host] or opts[:address]
      host = opts[:host] || opts[:address]
      hent = wspace.hosts.find_by_address(host)
      return { :result => 'failed' } if hent == nil or hent.class != ::Mdm::Host
      hosts << hent
    elsif opts[:addresses]
      return { :result => 'failed' } if opts[:addresses].class != Array
      conditions = { :address => opts[:addresses] }
      hent = wspace.hosts.where(conditions)
      return { :result => 'failed' } if hent == nil
      hosts |= hent if hent.class == Array
      hosts << hent if hent.class == ::Mdm::Host
    end
    deleted = []
    hosts.each do |h|
      deleted << h.address.to_s
      h.destroy
    end

    return { :result => 'success', :deleted => deleted }
  }
  end


  # Reports a vulnerability.
  #
  # @param [Hash] xopts Information about the vulnerability:
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :host The host where this vulnerability resides
  # @option xopts [String] :name The friendly name for this vulnerability (title).
  # @option xopts [String] :info A human readable description of the vuln, free-form text.
  # @option xopts [Array] :refs An array of Ref objects or string names of references.
  # @option xopts [Hash] :details A hash with :key pointed to a find criteria hash and the rest containing VulnDetail fields.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash indicating whether the action was successful or not. It contains:
  #  * 'result' [String] A message that says either 'success' or 'failed'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.report_vuln', {:host=>ip, :name=>'file upload'})
  def rpc_report_vuln(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    db_check
    opts = fix_options(xopts)
    opts[:workspace] = find_workspace(opts[:workspace]) if opts[:workspace]
    res = self.framework.db.report_vuln(opts)
    return { :result => 'success' } if res
    { :result => 'failed' }
  }
  end


  # Returns framework events.
  #
  # @param [Hash] xopts Options:
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [Integer] :limit Limit.
  # @option xopts [Integer] :offset Offset.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'events' [Array<Hash>] Each hash in the array contains the following:
  #    * 'host' [String] Host address.
  #    * 'created_at' [Integer] Creation date.
  #    * 'updated_at' [Integer] Last updated at.
  #    * 'name' [String] Event name.
  #    * 'critical' [Boolean] Criticality.
  #    * 'username' [String] Username.
  #    * 'info' [String] Additional information.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.events', {})
  def rpc_events(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    limit = opts.delete(:limit) || 100
    offset = opts.delete(:offset) || 0

    ret = {}
    ret[:events] = []

    wspace.events.offset(offset).limit(limit).each do |e|
      event = {}
      event[:host] = e.host.address if e.host
      event[:created_at] = e.created_at.to_i
      event[:updated_at] = e.updated_at.to_i
      event[:name] = e.name
      event[:critical] = e.critical if e.critical
      event[:username] = e.username if e.username
      event[:info] = e.info
      ret[:events] << event
    end
    ret
  }
  end


  # Reports a framework event.
  #
  # @param [Hash] xopts Information about the event.
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :username Username.
  # @option xopts [String] :host Host address.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash indicating the action was successful. It contains:
  #  * 'result' [String] A message that says 'success'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.report_event', {:username => username, :host=>ip})
  def rpc_report_event(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    res = self.framework.db.report_event(opts)
    { :result => 'success' } if res
  }
  end


  # Reports a looted item.
  #
  # @param [Hash] xopts Information about the looted item.
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :host Host address.
  # @option xopts [Integer] :port Port. Should match :service.
  # @option xopts [String] :proto Protocol. Should match :service.
  # @option xopts [String] :path Required. Path where the item was looted.
  # @option xopts [String] :type Loot type.
  # @option xopts [String] :ctype Content type.
  # @option xopts [String] :name Name.
  # @option xopts [String] :info Additional information.
  # @option xopts [String] :data Looted data.
  # @option xopts [Mdm::Service] :service Service where the data was found.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'result' [String] A message that says 'success'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.report_loot', {})
  def rpc_report_loot(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    if opts[:host] && opts[:port] && opts[:proto]
      opts[:service] = self.framework.db.find_or_create_service(opts)
    end

    res = self.framework.db.report_loot(opts)
    { :result => 'success' } if res
  }
  end


  # Returns all the looted items.
  #
  # @param [Hash] xopts Filters that narrow down the search:
  # @option xopts [Hash] :workspace Workspace name.
  # @option xopts [Integer] :limit Limit.
  # @option xopts [Integer] :offset Offset.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'loots' [Array<Hash>] Each hash in the array contains the following:
  #    * 'host' [String] Host address.
  #    * 'service' [String] Service name or port.
  #    * 'ltype' [String] Loot type.
  #    * 'ctype' [String] Content type.
  #    * 'data' [String] Looted data.
  #    * 'created_at' [Integer] Creation date.
  #    * 'updated_at' [Integer] Last updated at.
  #    * 'name' [String] Name.
  #    * 'info' [String] Additional information.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.loots', {})
  def rpc_loots(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    limit = opts.delete(:limit) || 100
    offset = opts.delete(:offset) || 0

    ret = {}
    ret[:loots] = []
    wspace.loots.offset(offset).limit(limit).each do |l|
      loot = {}
      loot[:host] = l.host.address if l.host
      loot[:service] = l.service.name || l.service.port  if l.service
      loot[:ltype] = l.ltype
      loot[:ctype] = l.content_type
      loot[:data] = l.data
      loot[:created_at] = l.created_at.to_i
      loot[:updated_at] = l.updated_at.to_i
      loot[:name] = l.name
      loot[:info] = l.info
      ret[:loots] << loot
    end
    ret
  }
  end


  # Imports file to the database.
  #
  # @param [Hash] xopts A hash that contains:
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] 'data' Data to import. The method will automatically detect the file type:
  #  * :acunetix_xml
  #  * :amap_log
  #  * :amap_mlog
  #  * :appscan_xml
  #  * :burp_session_xml
  #  * :ci_xml
  #  * :foundstone_xml
  #  * :fusionvm_xml
  #  * :gpp_xml
  #  * :ip360_aspl_xml
  #  * :ip360_xml_v3
  #  * :ip_list
  #  * :libpcap
  #  * :mbsa_xml
  #  * :msf_cred_dump_zip
  #  * :msf_pwdump
  #  * :msf_xml
  #  * :msf_zip
  #  * :nessus_nbe
  #  * :nessus_xml
  #  * :nessus_xml_v2
  #  * :netsparker_xml
  #  * :nexpose_rawxml
  #  * :nexpose_simplexml
  #  * :nikto_xml
  #  * :nmap_xml
  #  * :openvas_new_xml
  #  * :openvas_xml
  #  * :outpost24_xml
  #  * :qualys_asset_xml
  #  * :qualys_scan_xml
  #  * :retina_xml
  #  * :spiceworks_csv
  #  * :wapiti_xml
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that indicates the action was successful. It contains the following:
  #  * 'result' <String> A message that says 'success'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.import_data', {'data'=>nexpose_scan_results})
  def rpc_import_data(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    self.framework.db.import(opts)
    return { :result => 'success' }
  }
  end


  # Returns vulnerabilities from services or from a host.
  #
  # @param [Hash] xopts Filters to narrow down which vulnerabilities to find.
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :host Host address.
  # @option xopts [String] :address Same as :host.
  # @option xopts [String] :proto Protocol.
  # @option xopts [Integer] :port Port.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'vuln' [Array<Hash>] Each hash in the array contains the following:
  #    * 'host' [String] Host address.
  #    * 'port' [Integer] Port.
  #    * 'proto' [String] Protocol.
  #    * 'created_at' [Integer] Creation date.
  #    * 'updated_at' [Integer] Last updated at.
  #    * 'name' [String] Vulnerability name.
  #    * 'info' [String] Additional information.
  #    * 'refs' [Array<String>] Reference names.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.get_vuln', {:host => ip, :proto => 'tcp'})
  def rpc_get_vuln(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)

    ret = {}
    ret[:vuln] = []

    host = self.framework.db.get_host(opts)

    return ret if not host
    vulns = []

    if opts[:proto] && opts[:port]
      services = []
      sret = host.services.find_by_proto_and_port(opts[:proto], opts[:port])
      return ret if sret == nil
      services << sret if sret.class == ::Mdm::Service
      services |= sret if sret.class == Array

      services.each do |s|
        vulns |= s.vulns
      end
    else
      vulns = host.vulns
    end

    return ret if (not vulns)

    vulns.each do |v|
      vuln= {}
      host= v.host
      vuln[:host] = host.address || "unknown"
      if v.service
        vuln[:port] = v.service.port
        vuln[:proto] = v.service.proto
      end
      vuln[:created_at] = v[:created_at].to_i
      vuln[:updated_at] = v[:updated_at].to_i
      vuln[:name] = v[:name].to_s
      vuln[:info] = v[:info].to_s
      vuln[:refs] = []
      v.refs.each do |r|
        vuln[:refs] << r.name
      end
      ret[:vuln] << vuln
    end
    ret
  }
  end


  # Returns browser clients information.
  #
  # @param [Hash] xopts Filters that narrow down the search.
  # @option xopts [String] :workspace Name of the workspace.
  # @option xopts [String] :ua_name User-Agent name.
  # @option xopts [String] :ua_ver Browser version.
  # @option xopts [Array] :addresses Addresses.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'clients' [Array<Hash>] Each hash in the array that contains the following:
  #   * 'host' [String] Host address.
  #   * 'ua_string' [String] User-agent string.
  #   * 'ua_name' [String] Browser name.
  #   * 'ua_ver' [String] Browser version.
  #   * 'created_at' [Integer] Creation date.
  #   * 'updated_at' [Integer] Last updated at.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.clients', {})
  def rpc_clients(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    opts, wspace = init_db_opts_workspace(xopts)
    limit = opts.delete(:limit) || 100
    offset = opts.delete(:offset) || 0

    conditions = {}
    conditions[:ua_name] = opts[:ua_name] if opts[:ua_name]
    conditions[:ua_ver] = opts[:ua_ver] if opts[:ua_ver]
    conditions["hosts.address"] = opts[:addresses] if opts[:addresses]

    ret = {}
    ret[:clients] = []

    wspace.clients.includes(:host).where(conditions).offset(offset).limit(limit).each do |c|
      client = {}
      client[:host] = c.host.address.to_s if c.host
      client[:ua_string] = c.ua_string
      client[:ua_name] = c.ua_name
      client[:ua_ver] = c.ua_ver
      client[:created_at] = c.created_at.to_i
      client[:updated_at] = c.updated_at.to_i
      ret[:clients] << client
    end
    ret
  }
  end


  # Deletes browser information from a client.
  #
  # @param [Hash] xopts Filters that narrow down what to delete.
  # @option xopts [String] :workspace Workspace name.
  # @option xopts [String] :host Host address.
  # @option xopts [String] :address Same as :host.
  # @option xopts [Array] :addresses Same as :address.
  # @option xopts [String] :ua_name Browser name.
  # @option xopts [String] :ua_ver Browser version.
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  #  * 500 Invalid workspace.
  # @return [Hash] A hash that contains the following:
  #  * 'result' [String] A message that says 'success'.
  #  * 'deleted' [Array<Hash>] Each hash in the array contains the following:
  #    * 'address' [String] Host address.
  #    * 'ua_string' [String] User-Agent string.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.del_client', {})
  def rpc_del_client(xopts)
  ::ApplicationRecord.connection_pool.with_connection {
    db_check
    opts = fix_options(xopts)
    wspace = find_workspace(opts[:workspace])
    hosts = []
    clients = []

    if opts[:host] or opts[:address] or opts[:addresses]
      hosts = opts_to_hosts(xopts)
    else
      hosts = wspace.hosts
    end

    hosts.each do |h|
      cret = nil
      if opts[:ua_name] or opts[:ua_ver]
        conditions = {}
        conditions[:ua_name] = opts[:ua_name] if opts[:ua_name]
        conditions[:ua_ver] = opts[:ua_ver] if opts[:ua_ver]
        cret = h.clients.where(conditions)
      else
        cret = h.clients
      end
      next if cret == nil
      clients << cret if cret.class == ::Mdm::Client
      clients |= cret if cret.class == Array
    end

    deleted = []
    clients.each do |c|
      dent = {}
      dent[:address] = c.host.address.to_s
      dent[:ua_string] = c.ua_string
      deleted << dent
      c.destroy
    end

    { :result => 'success', :deleted => deleted }
  }
  end


  # Sets the driver for the database or returns the current one.
  #
  # @param [Hash] xopts Options:
  # @option [String] :workspace Workspace name.
  # @option [String] :driver Driver name. For example: 'postgresql'. If this option is not set,
  #                  then the method returns the current one.
  # @return [Hash] A hash that contains:
  #  * 'result' [String] Indiciating whether we've successfully set the driver or not.
  #  * 'driver' [String] If the :driver option isn't set, then this returns the current one.
  # @example Here's how you would use this from the client:
  #  # Sets a driver
  #  rpc.call('db.driver', {:driver=>new_driver})
  #  # Returns the current driver
  #  rpc.call('db.driver', {})
  def rpc_driver(xopts)
    opts = fix_options(xopts)
    if opts[:driver]
      if self.framework.db.drivers.include?(opts[:driver])
        self.framework.db.driver = opts[:driver]
        return { :result => 'success' }
      else
        return { :result => 'failed' }

      end
    else
      return { :driver => self.framework.db.driver.to_s }
    end
    return { :result => 'failed' }
  end


  # Connects to the database.
  #
  # @param [Hash] xopts Options:
  # This must contain :driver and driver specific options.
  # @option xopts [String] :driver Driver name. For example: 'postgresql'.
  # @return [Hash] A hash that indicates whether the action was successful or not.
  #  * 'result' [String] A message that says either 'success' or 'failed'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.connect', {:driver=>'postgresql', :host => db_host, :port => db_port, :database => db_name, :username => db_username, :password=> db_password})
  def rpc_connect(xopts)
    opts = fix_options(xopts)
    if not self.framework.db.driver and not opts[:driver]
      return { :result => 'failed' }
    end

    if opts[:driver]
      if self.framework.db.drivers.include?(opts[:driver])
        self.framework.db.driver = opts[:driver]
      else
        return { :result => 'failed' }
      end
    end

    driver = self.framework.db.driver

    case driver
    when 'postgresql'
      opts['adapter'] = 'postgresql'
    else
      return { :result => 'failed' }
    end

    if (not self.framework.db.connect(opts))
      return { :result => 'failed' }
    end
    return { :result => 'success' }

  end


  # Returns the database status.
  #
  # @raise [Msf::RPC::ServerException] You might get one of these errors:
  #  * 500 ActiveRecord::ConnectionNotEstablished. Try: rpc.call('console.create').
  #  * 500 Database not loaded. Try: rpc.call('console.create')
  # @return [Hash] A hash that contains the following keys:
  #  * 'driver' [String] Name of the database driver.
  #  * 'db' [String] Name of the database.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.status')
  def rpc_status
    if (not self.framework.db.driver)
      return {:driver => 'None' }
    end

    cdb = ""
    if framework.db.connection_established?
      ::ApplicationRecord.connection_pool.with_connection { |conn|
        if conn.respond_to? :current_database
          cdb = conn.current_database
        else
          cdb = conn.instance_variable_get(:@config)[:database]
        end
      }
      return {:driver => self.framework.db.driver.to_s , :db => cdb }
    else
      return {:driver => self.framework.db.driver.to_s}
    end
    {:driver => 'None' }
  end


  # Disconnects the database.
  #
  # @return [Hash] A hash that indicates whether the action was successful or not. It contains:
  #  * 'result' [String] A message that says either 'success' or 'failed'.
  # @example Here's how you would use this from the client:
  #  rpc.call('db.disconnect')
  def rpc_disconnect
    if (self.framework.db)
      self.framework.db.disconnect()
      return { :result => 'success' }
    else
      return { :result => 'failed' }
    end
  end


end
end
end