module Msf::DBManager::ExploitAttempt
  # report_exploit() used to be used to track sessions and which modules
  # opened them. That information is now available with the session table
  # directly. TODO: kill this completely some day -- for now just warn if
  # some other UI is actually using it.
  def report_exploit(opts={})
    wlog("Deprecated method call: report_exploit()\n" +
      "report_exploit() options: #{opts.inspect}\n" +
      "report_exploit() call stack:\n\t#{caller.join("\n\t")}"
    )
  end

  def report_exploit_attempt(host, opts)
  ::ApplicationRecord.connection_pool.with_connection {
    return if not host
    info = {}

    # Opts can be keyed by strings or symbols
    ::Mdm::VulnAttempt.column_names.each do |kn|
      k = kn.to_sym
      next if ['id', 'host_id'].include?(kn)
      info[k] = opts[kn] if opts[kn]
      info[k] = opts[k]  if opts[k]
    end

    host.exploit_attempts.create(info)
  }
  end

  # Create an `Mdm::ExploitAttempt` (and possibly an `Mdm::VulnAttempt`, if
  # the `vuln` option is passed).
  #
  # @option (see #do_report_failure_or_success)
  # @return (see #do_report_failure_or_success)
  def report_exploit_failure(opts)
    return unless opts.has_key?(:refs) && !opts[:refs].blank?
    host   = opts[:host] || return

    wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
    port   = opts[:port]
    proto   = opts[:proto] || Msf::DBManager::DEFAULT_SERVICE_PROTO
    svc    = opts[:service]

    # Look up the service as appropriate
    if port and svc.nil?
      # only one result can be returned, as the +port+ field restricts potential results to a single service
      svc = services(:workspace => wspace,
                     :hosts => {address: host},
                     :proto => proto,
                     :port => port).first
    end

    # Look up the host as appropriate
    if !host || !host.kind_of?(::Mdm::Host)
      if svc.kind_of? ::Mdm::Service
        host = svc.host
      else
        host = get_host(workspace: wspace, address: host)
      end
    end

    # Bail if we dont have a host object
    return if not host

    opts = opts.clone()
    opts[:service] = svc
    opts[:host] = host

    do_report_failure_or_success(opts)
  end

  # Create an `Mdm::ExploitAttempt` (and possibly an `Mdm::VulnAttempt`, if
  # the `vuln` option is passed).
  #
  # @return (see #do_report_failure_or_success)
  def report_exploit_success(opts)
    return unless opts[:refs]
    host   = opts[:host] || return

    wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
    # it is rude to modify arguments in place
    opts = opts.clone()
    port   = opts[:port]
    prot   = opts[:proto] || Msf::DBManager::DEFAULT_SERVICE_PROTO
    svc    = opts[:service]

    # Look up or generate the service as appropriate
    if port and svc.nil?
      opts[:proto] ||= Msf::DBManager::DEFAULT_SERVICE_PROTO
      opts[:service] = report_service(
        workspace: wspace, host: host, port: port, proto: prot
      )
    end

    do_report_failure_or_success(opts)
  end

  private

  # @option opts [Array<String>, Array<Msf::Module::Reference>] :refs
  # @option opts [Mdm::Host] :host
  # @option opts [Mdm::Service] :service
  # @option opts [Integer] :port (nil)
  # @option opts ["tcp","udp"] :proto (Msf::DBManager::DEFAULT_SERVICE_PROTO) See `Mdm::Service::PROTOS`
  # @option opts [Mdm::Vuln] :vuln (nil)
  # @option opts [Time] :timestamp (nil)
  # @option opts [Mdm::Vuln] :timestamp (nil)
  # @option opts [String] :module (nil)
  # @return [void]
  def do_report_failure_or_success(opts)
    return unless opts[:refs]
    ::ApplicationRecord.connection_pool.with_connection {
      mrefs  = opts[:refs]
      host   = opts[:host]
      port   = opts[:port]
      prot   = opts[:proto]
      svc    = opts[:service]
      vuln   = opts[:vuln]

      timestamp  = opts[:timestamp]
      freason    = opts[:fail_reason]
      fdetail    = opts[:fail_detail]
      username   = opts[:username]
      mname      = opts[:module]


      if vuln.nil?
        ref_names = mrefs.map { |ref|
          if ref.respond_to?(:ctx_id) and ref.respond_to?(:ctx_val)
            "#{ref.ctx_id}-#{ref.ctx_val}"
          else
            ref.to_s
          end
        }

        # Create a references map from the module list
        ref_objs = ::Mdm::Ref.where(name: ref_names)

        # Try find a matching vulnerability
        vuln = find_vuln_by_refs(ref_objs, host, svc)
      end

      attempt_info = {
        :attempted_at => timestamp || Time.now.utc,
        :exploited    => (freason.nil? ? true : false),
        :fail_detail  => fdetail,
        :fail_reason  => freason,
        :module       => mname,
        :username     => username  || "unknown",
      }

      attempt_info[:session_id] = opts[:session_id] if opts[:session_id]
      attempt_info[:loot_id]    = opts[:loot_id]    if opts[:loot_id]

      # We have match, lets create a vuln_attempt record
      if vuln
        attempt_info[:vuln_id] = vuln.id
        vuln.vuln_attempts.create(attempt_info)

        create_match_result_for_vuln(vuln,opts)

        # Correct the vuln's associated service if necessary
        if svc and vuln.service_id.nil?
          vuln.service = svc
          vuln.save
        end
      end

      # Report an exploit attempt all the same

      if svc
        attempt_info[:port]  = svc.port
        attempt_info[:proto] = svc.proto
      end

      if port and svc.nil?
        attempt_info[:port]  = port
        attempt_info[:proto] = prot || Msf::DBManager::DEFAULT_SERVICE_PROTO
      end

      host.exploit_attempts.create(attempt_info)
    }

  end

  # Create a MetasploitDataModels::AutomaticExploitation::Match result for the given vuln
  # @option opts [Integer] :run_id
  # @return [void]
  def create_match_result_for_vuln(vuln, opts)
    run = MetasploitDataModels::AutomaticExploitation::Run.where(id:opts[:run_id]).last

    if run.present?
      match = MetasploitDataModels::AutomaticExploitation::Match.by_run_and_vuln(run,vuln).last

      # If no match found in the current run
      unless match.present?
        # Create match if the vuln has the data we need to create a match
        match = create_match_for_vuln(vuln,opts.merge(run:run))
      end

      create_match_result(opts.merge(match:match,run:run)) if match.present?
    end
  end

  # Create a MetasploitDataModels::AutomaticExploitation::Match result with a success or failure state
  # @option opts [MetasploitDataModels::AutomaticExploitation::Match] :match
  # @option opts [MetasploitDataModels::AutomaticExploitation::Run] :run
  # @return [void]
  def create_match_result(opts)
    if opts[:session_id]
      state =  MetasploitDataModels::AutomaticExploitation::MatchResult::SUCCEEDED
    else
      state =  MetasploitDataModels::AutomaticExploitation::MatchResult::FAILED
    end

    MetasploitDataModels::AutomaticExploitation::MatchResult.create!(
      match: opts[:match],
      run: opts[:run],
      state: state
    )
  end

  # Create a MetasploitDataModels::AutomaticExploitation::Match for the given vuln
  # @option vuln [Mdm::Vuln] :vuln
  # @option opts [Mdm::Workspace] :workspace
  # @option opts [String] :username
  # @return [ MetasploitDataModels::AutomaticExploitation::Match, MetasploitDataModels::AutomaticExploitation::Run]
  def create_match_for_vuln(vuln,opts)
    wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
    run     = opts[:run]
    module_fullname  = opts[:module]

    run.match_set.create_match_for_vuln(
      vuln,
      workspace: wspace,
      module_fullname: module_fullname
    )
  end

end
