# -*- coding: binary -*-

require 'metasploit/framework/require'

module Msf

###
#
# This module provides methods for reporting data to the DB
#
###

module Auxiliary::Report
  extend Metasploit::Framework::Require

  optionally_include_metasploit_credential_creation

  def db_warning_given?
    if @warning_issued
      true
    else
      @warning_issued = true
      false
    end
  end

  def create_cracked_credential(opts={})
    if active_db?
      opts = { :task_id => mytask.id }.merge(opts) if mytask
      framework.db.create_cracked_credential(opts)
    elsif !db_warning_given?
      vprint_warning('No active DB -- Credential data will not be saved!')
      nil
    end
  end

  def create_credential(opts={})
    if active_db?
      opts = { :task_id => mytask.id }.merge(opts) if mytask
      framework.db.create_credential(opts)
    elsif !db_warning_given?
      vprint_warning('No active DB -- Credential data will not be saved!')
      nil
    end
  end

  def create_credential_login(opts={})
    if active_db?
      opts = { :task_id => mytask.id }.merge(opts) if mytask
      framework.db.create_credential_login(opts)
    elsif !db_warning_given?
      vprint_warning('No active DB -- Credential data will not be saved!')
      nil
    end
  end

  def create_credential_and_login(opts={})
    if active_db?
      opts = { :task_id => mytask.id }.merge(opts) if mytask
      framework.db.create_credential_and_login(opts)
    elsif !db_warning_given?
      vprint_warning('No active DB -- Credential data will not be saved!')
      nil
    end
  end

  def invalidate_login(opts={})
    if active_db?
      opts = { :task_id => mytask.id }.merge(opts) if mytask
      framework.db.invalidate_login(opts)
    elsif !db_warning_given?
      vprint_warning('No active DB -- Credential data will not be saved!')
      nil
    end
  end

  # This method overrides the method from Metasploit::Credential to check for an active db
  def active_db?
    framework.db.active
  end

  # Shortcut method for detecting when the DB is active
  def db
    framework.db.active
  end

  def myworkspace
    @myworkspace = framework.db.find_workspace(self.workspace)
  end

  # This method safely get the workspace ID. It handles if the db is not active
  #
  # @return [NilClass] if there is no DB connection
  # @return [Integer] the ID of the current Mdm::Workspace
  def myworkspace_id
    if framework.db.active
      myworkspace.id
    else
      nil
    end
  end

  def mytask
    if self.respond_to?(:[]) && self[:task]
      return self[:task].record
    elsif @task && @task.class == Mdm::Task
      return @task
    else
      return nil
    end
  end

  def inside_workspace_boundary?(ip)
    return true if not framework.db.active
    allowed = myworkspace.allow_actions_on?(ip)
    return allowed
  end



  #
  # Report a host's liveness and attributes such as operating system and service pack
  #
  # opts must contain :host, which is an IP address identifying the host
  # you're reporting about
  #
  # See data/sql/*.sql and lib/msf/core/db.rb for more info
  #
  def report_host(opts)
    return if not db
    opts = {
      :workspace => myworkspace,
      :task => mytask
    }.merge(opts)
    framework.db.report_host(opts)
  end

  def get_host(opts)
    return if not db
    opts = {:workspace => myworkspace}.merge(opts)
    framework.db.get_host(opts)
  end

  #
  # Report a client connection
  # @param opts [Hash] report client information based on user-agent
  # @option opts [String] :host the address of the client connecting
  # @option opts [String] :ua_string a string that uniquely identifies this client
  # @option opts [String] :ua_name a brief identifier for the client, e.g. "Firefox"
  # @option opts [String] :ua_ver  the version number of the client, e.g. "3.0.11"
  #
  def report_client(opts={})
    return if not db
    opts = {
        :workspace => myworkspace,
        :task => mytask
    }.merge(opts)
    framework.db.report_client(opts)
  end

  def get_client(opts={})
    return if not db
    opts = {:workspace => myworkspace}.merge(opts)
    framework.db.get_client(opts)
  end

  #
  # Report detection of a service
  #
  def report_service(opts={})
    return if not db
    opts = {
        :workspace => myworkspace,
        :task => mytask
    }.merge(opts)
    framework.db.report_service(opts)
  end

  def report_note(opts={})
    return if not db
    opts = {
        :workspace => myworkspace,
        :task => mytask
    }.merge(opts)
    framework.db.report_note(opts)
  end

  # This Legacy method is responsible for creating credentials from data supplied
  # by a module. This method is deprecated and the new Metasploit::Credential methods
  # should be used directly instead.
  #
  # @param opts [Hash] the option hash
  # @option opts [String] :host the address of the host (also takes a Mdm::Host)
  # @option opts [Integer] :port the port of the connected service
  # @option opts [Mdm::Service] :service an optional Service object to build the cred for
  # @option opts [String] :type What type of private credential this is (e.g. "password", "hash", "ssh_key")
  # @option opts [String] :proto Which transport protocol the service uses
  # @option opts [String] :sname The 'name' of the service
  # @option opts [String] :user The username for the cred
  # @option opts [String] :pass The private part of the credential (e.g. password)
  def report_auth_info(opts={})
    print_warning("*** #{self.fullname} is still calling the deprecated report_auth_info method! This needs to be updated!")
    print_warning('*** For detailed information about LoginScanners and the Credentials objects see:')
    print_warning('     https://docs.metasploit.com/docs/development/developing-modules/guides/scanners/creating-metasploit-framework-loginscanners.html')
    print_warning('     https://docs.metasploit.com/docs/development/developing-modules/guides/scanners/how-to-write-a-http-loginscanner-module.html')
    print_warning('*** For examples of modules converted to just report credentials without report_auth_info, see:')
    print_warning('     https://github.com/rapid7/metasploit-framework/pull/5376')
    print_warning('     https://github.com/rapid7/metasploit-framework/pull/5377')
    return unless db
    raise ArgumentError.new("Missing required option :host") if opts[:host].nil?
    raise ArgumentError.new("Missing required option :port") if (opts[:port].nil? and opts[:service].nil?)

    if opts[:host].is_a?(::Mdm::Host)
      host = opts[:host].address
    else
      host = opts[:host]
    end

    type = :password
    case opts[:type]
      when "password"
        type = :password
      when "hash"
        type = :nonreplayable_hash
      when "ssh_key"
        type = :ssh_key
    end

    case opts[:proto]
      when "tcp"
        proto = "tcp"
      when "udp"
        proto = "udp"
      else
        proto = "tcp"
    end

    if opts[:service] && opts[:service].is_a?(Mdm::Service)
      port         = opts[:service].port
      proto        = opts[:service].proto
      service_name = opts[:service].name
      host         = opts[:service].host.address
    else
      port         = opts.fetch(:port)
      service_name = opts.fetch(:sname, nil)
    end

    username = opts.fetch(:user, nil)
    private  = opts.fetch(:pass, nil)

    service_data = {
      address: host,
      port: port,
      service_name: service_name,
      protocol: proto,
      workspace_id: myworkspace_id
    }

    if self.type == "post"
      credential_data = {
        origin_type: :session,
        session_id: session_db_id,
        post_reference_name: self.refname
      }
    else
      credential_data = {
        origin_type: :service,
        module_fullname: self.fullname
      }
      credential_data.merge!(service_data)
    end

    unless private.nil?
      credential_data[:private_type] = type
      credential_data[:private_data] = private
    end

    unless username.nil?
      credential_data[:username] = username
    end

    credential_core = create_credential(credential_data)

    login_data ={
      core: credential_core,
      status: Metasploit::Model::Login::Status::UNTRIED
    }
    login_data.merge!(service_data)
    create_credential_login(login_data)
  end

  def report_vuln(opts={})
    return if not db
    opts = {
        :workspace => myworkspace,
        :task => mytask
    }.merge(opts)
    vuln = framework.db.report_vuln(opts)

    raise Msf::ValidationError, "Failed to report vuln for #{opts[:host]}:#{opts[:port]} to the database" if vuln.nil?

    # add vuln attempt audit details here during report

    timestamp  = opts[:timestamp]
    username   = opts[:username]
    mname      = opts[:module_fullname] || self.fullname # use module name when reporting attempt for correlation

    # report_vuln is only called in an identified case, consider setting value reported here
    attempt_info = {
        :workspace    => opts[:workspace],
        :vuln_id      => vuln.id,
        :attempted_at => timestamp || Time.now.utc,
        :exploited    => false,
        :fail_detail  => 'vulnerability identified',
        :fail_reason  => 'Untried', # Mdm::VulnAttempt::Status::UNTRIED, avoiding direct dependency on Mdm, used elsewhere in this module
        :module       => mname,
        :username     => username  || "unknown"
    }

    # TODO: figure out what opts are required and why the above logic doesn't match that of the db_manager method
    framework.db.report_vuln_attempt(vuln, attempt_info)

    vuln
  end

  # This will simply log a deprecation warning, since report_exploit()
  # is no longer implemented.
  def report_exploit(opts={})
    return if not db
    opts = {
        :workspace => myworkspace,
        :task => mytask
    }.merge(opts)
    framework.db.report_exploit(opts)
  end

  def report_loot(opts={})
    return if not db
    opts = {
        :workspace => myworkspace,
        :task => mytask
    }.merge(opts)
    framework.db.report_loot(opts)
  end

  def report_web_site(opts={})
    return if not db
    opts = {
        :workspace => myworkspace,
        :task => mytask
    }.merge(opts)
    framework.db.report_web_site(opts)
  end

  def report_web_page(opts={})
    return if not db
    opts = {
        :workspace => myworkspace,
        :task => mytask
    }.merge(opts)
    framework.db.report_web_page(opts)
  end

  def report_web_form(opts={})
    return if not db
    opts = {
        :workspace => myworkspace,
        :task => mytask
    }.merge(opts)
    framework.db.report_web_form(opts)
  end

  def report_web_vuln(opts={})
    return if not db
    opts = {
        :workspace => myworkspace,
        :task => mytask
    }.merge(opts)
    framework.db.report_web_vuln(opts)
  end

  #
  # Store some data stolen from a session as a file
  #
  # Also stores metadata about the file in the database when available
  # +ltype+ is an OID-style loot type, e.g. "cisco.ios.config".  Ignored when
  # no database is connected.
  #
  # +ctype+ is the Content-Type, e.g. "text/plain".  Affects the extension
  # the file will be saved with.
  #
  # +host+ can be an String address or a Session object
  #
  # +data+ is the actual contents of the file
  #
  # +filename+ and +info+ are only stored as metadata, and therefore both are
  # ignored if there is no database
  #
  def store_loot(ltype, ctype, host, data, filename=nil, info=nil, service=nil, &block)
    if ! ::File.directory?(Msf::Config.loot_directory)
      FileUtils.mkdir_p(Msf::Config.loot_directory)
    end

    ext = 'bin'
    if filename
      parts = filename.to_s.split('.')
      if parts.length > 1 and parts[-1].length <= 6
        ext = parts[-1]
      end
    end

    case ctype
    when /^text\/[\w\.]+$/
      ext = "txt"
    end
    # This method is available even if there is no database, don't bother checking
    host = Msf::Util::Host.normalize_host(host)

    ws = (db ? myworkspace.name[0,16] : 'default')
    name =
      Time.now.strftime("%Y%m%d%H%M%S") + "_" + ws + "_" +
      (host || 'unknown') + '_' + ltype[0,16] + '_' +
      Rex::Text.rand_text_numeric(6) + '.' + ext

    name.gsub!(/[^a-z0-9\.\_]+/i, '')

    path = File.join(Msf::Config.loot_directory, name)
    full_path = ::File.expand_path(path)
    File.open(full_path, "wb") do |fd|
      fd.write(data)
    end

    if (db)
      # If we have a database we need to store it with all the available
      # metadata.
      conf = {}
      conf[:host] = host if host
      conf[:type] = ltype
      conf[:content_type] = ctype
      conf[:path] = full_path
      conf[:workspace] = myworkspace
      conf[:name] = filename if filename
      conf[:info] = info if info
      conf[:data] = data unless data.nil?

      if service and service.kind_of?(::Mdm::Service)
        conf[:service] = service if service
      end

      loot = framework.db.report_loot(conf)
      yield loot if block_given?
    end

    return full_path.dup
  end

  #
  # Store some locally-generated data as a file, similar to store_loot.
  # Sometimes useful for keeping artifacts of an exploit or auxiliary
  # module, such as files from fileformat exploits. (TODO: actually
  # implement this on file format modules.)
  #
  # +filename+ is the local file name.
  #
  # +data+ is the actual contents of the file
  #
  # Also stores metadata about the file in the database when available.
  # +ltype+ is an OID-style loot type, e.g. "cisco.ios.config".  Ignored when
  # no database is connected.
  #
  # +ctype+ is the Content-Type, e.g. "text/plain". Ignored when no database
  # is connected.
  #
  def store_local(ltype=nil, ctype=nil, data=nil, filename=nil)
    if ! ::File.directory?(Msf::Config.local_directory)
      FileUtils.mkdir_p(Msf::Config.local_directory)
    end

    # Split by fname an extension
    if filename and not filename.empty?
      if filename =~ /(.*)\.(.*)/
        ext = $2
        fname = $1
      else
        fname = filename
      end
    else
      fname = ctype || "local_#{Time.now.utc.to_i}"
    end

    # Split by path separator
    fname = ::File.split(fname).last

    case ctype # Probably could use more cases
    when "text/plain"
      ext ||= "txt"
    when "text/xml"
      ext ||= "xml"
    when "text/html"
      ext ||= "html"
    when "application/pdf"
      ext ||= "pdf"
    else
      ext ||= "bin"
    end

    fname.gsub!(/[^a-z0-9\.\_\-]+/i, '')
    fname << ".#{ext}"

    ltype.gsub!(/[^a-z0-9\.\_\-]+/i, '')

    path = File.join(Msf::Config.local_directory, fname)
    full_path = ::File.expand_path(path)
    File.open(full_path, "wb") { |fd| fd.write(data) }

    # This will probably evolve into a new database table
    report_note(
      :data => { :full_path => full_path.dup },
      :type => "#{ltype}.localpath"
    )

    return full_path.dup
  end

  # Takes a credential from a script (shell or meterpreter), and
  # sources it correctly to the originating user account or
  # session. Note that the passed-in session ID should be the
  # Session.local_id, which will be correlated with the Session.id
  def store_cred(opts={})
    if [opts[:port],opts[:sname]].compact.empty?
      raise ArgumentError, "Missing option: :sname or :port"
    end
    cred_opts = opts
    cred_opts = opts.merge(:workspace => myworkspace)
    cred_opts = { :task_id => mytask.id }.merge(cred_opts) if mytask
    cred_host = myworkspace.hosts.find_by_address(cred_opts[:host])
    unless opts[:port]
      possible_services = myworkspace.services.where(host_id: cred_host[:id], name: cred_opts[:sname])
      case possible_services.size
      when 0
        case cred_opts[:sname].downcase
        when "smb"
          cred_opts[:port] = 445
        when "ssh"
          cred_opts[:port] = 22
        when "telnet"
          cred_opts[:port] = 23
        when "snmp"
          cred_opts[:port] = 161
          cred_opts[:proto] = "udp"
        else
          raise ArgumentError, "No matching :sname found to store this cred."
        end
      when 1
        cred_opts[:port] = possible_services.first[:port]
      else # SMB should prefer 445. Everyone else, just take the first hit.
        if (cred_opts[:sname].downcase == "smb") && possible_services.map {|x| x[:port]}.include?(445)
          cred_opts[:port] = 445
        elsif (cred_opts[:sname].downcase == "ssh") && possible_services.map {|x| x[:port]}.include?(22)
          cred_opts[:port] = 22
        else
          cred_opts[:port] = possible_services.first[:port]
        end
      end
    end
    if opts[:collect_user]
      cred_service = cred_host.services.find_by_host_id(cred_host[:id])
      myworkspace.creds.sort {|a,b| a.created_at.to_f}.each do |cred|
        if(cred.user.downcase == opts[:collect_user].downcase &&
           cred.pass == opts[:collect_pass]
          )
          cred_opts[:source_id] ||= cred.id
          cred_opts[:source_type] ||= cred_opts[:collect_type]
          break
        end
      end
    end
    if opts[:collect_session]
      session = myworkspace.sessions.where(local_id: opts[:collect_session]).last
      if !session.nil?
        cred_opts[:source_id] = session.id
        cred_opts[:source_type] = "exploit"
      end
    end
    print_status "Collecting #{cred_opts[:user]}:#{cred_opts[:pass]}"
    framework.db.report_auth_info(cred_opts)
  end



end
end
