# Implements a set of "convenience methods" for creating credentials and related portions of the object graph.  Creates
# {Metasploit::Credential::Core} objects and their attendant relationships as well as {Metasploit::Credential::Login}
# objects and their attendant {Mdm::Host} and {Mdm::Service} objects.
module Metasploit
  module Credential
    # Helper methods for finding or creating a tree of credentials.  The method ensure that duplicate credentials are
    # not created.
    module Creation

      # Returns true if ActiveRecord has an active database connection, false otherwise.
      # @return [Boolean]
      def active_db?
        ActiveRecord::Base.connected?
      end

      # This method takes a few simple parameters and creates a new username/password
      # credential that was obtained by cracking a hash. It reuses the relevant
      # components form the originating {Metasploit::Credential::Core} and builds new
      # {Metasploit::Credential::Login} objects based on the ones attached to the originating
      # {Metasploit::Credential::Core}
      #
      # @option opts [String] :username the username to find or create the {Metasploit::Credential::Public} from
      # @option opts [String] :password the password to find or create the {Metasploit::Credential::Password} from
      # @option opts [Fixnum] :core_id the id for the originating {Metasploit::Credential::Core}
      def create_cracked_credential(opts={})
        return nil unless active_db?
        username = opts.fetch(:username)
        password = opts.fetch(:password)
        core_id  = opts.fetch(:core_id)

        private  = Metasploit::Credential::Password.where(data: password).first_or_create!
        public   = Metasploit::Credential::Public.where(username: username).first_or_create!
        old_core = Metasploit::Credential::Core.find(core_id)
        core     = Metasploit::Credential::Core.where(public_id: public.id, private_id: private.id, realm_id: nil, workspace_id: old_core.workspace_id).first_or_initialize

        if core.origin_id.nil?
          origin      = Metasploit::Credential::Origin::CrackedPassword.where(metasploit_credential_core_id: core_id).first_or_create!
          core.origin = origin
        end
        core.save!


        old_core.logins.each do |login|
          service_id = login.service_id
          new_login = Metasploit::Credential::Login.where(core_id: core.id, service_id: service_id).first_or_initialize
          if new_login.status.blank?
            new_login.status =  Metasploit::Model::Login::Status::UNTRIED
          end
          new_login.save!
        end
      end


      # This method is responsible for creation {Metasploit::Credential::Core} objects
      # and all sub-objects that it is dependent upon.
      #
      # @option opts [String] :jtr_format The format for John the ripper to use to try and crack this
      # @option opts [Symbol] :origin_type The Origin type we are trying to create
      # @option opts [String] :address The address of the `Mdm::Host` to link this Origin to
      # @option opts [Fixnum] :port The port number of the `Mdm::Service` to link this Origin to
      # @option opts [String] :service_name The service name to use for the `Mdm::Service`
      # @option opts [String] :protocol The protocol type of the `Mdm::Service` to link this Origin to
      # @option opts [String] :module_fullname The fullname of the Metasploit Module to link this Origin to
      # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to use for the `Mdm::Host`
      # @option opts [Fixnum] :task_id The ID of the `Mdm::Task` to link this Origin and Core to
      # @option opts [String] :filename The filename of the file that was imported
      # @option opts [Fixnum] :user_id The ID of the `Mdm::User` to link this Origin to
      # @option opts [Fixnum] :session_id The ID of the `Mdm::Session` to link this Origin to
      # @option opts [String] :post_reference_name The reference name of the Metasploit Post module to link the origin to
      # @option opts [String] :private_data The actual data for the private (e.g. password, hash, key etc)
      # @option opts [Symbol] :private_type The type of {Metasploit::Credential::Private} to create
      # @option opts [String] :username The username to use for the {Metasploit::Credential::Public}
      # @raise [KeyError] if a required option is missing
      # @raise [ArgumentError] if an invalid :private_type is specified
      # @raise [ArgumentError] if an invalid :origin_type is specified
      # @return [NilClass] if there is no active database connection
      # @return [Metasploit::Credential::Core]
      # @example Reporting a Bruteforced Credential
      #     create_credential(
      #       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'
      #     )
      def create_credential(opts={})
        return nil unless active_db?

        if opts[:origin]
          origin = opts[:origin]
        else
          origin = create_credential_origin(opts)
        end

        core_opts = {
            origin: origin,
            workspace_id: opts.fetch(:workspace_id)
        }

        if opts.has_key?(:realm_key) && opts.has_key?(:realm_value)
          core_opts[:realm] = create_credential_realm(opts)
        end

        if opts.has_key?(:private_type) && opts.has_key?(:private_data)
          core_opts[:private] = create_credential_private(opts)
        end

        if opts.has_key?(:username)
          core_opts[:public] = create_credential_public(opts)
        end

        if opts.has_key?(:task_id)
          core_opts[:task_id] = opts[:task_id]
        end

        create_credential_core(core_opts)
      end

      # This method is responsible for creating {Metasploit::Credential::Core} objects.
      #
      # @option opts [Metasploit::Credential::Origin] :origin The origin object to tie the core to
      # @option opts [Metasploit::Credential::Public] :public The {Metasploit::Credential::Public} component
      # @option opts [Metasploit::Credential::Private] :private The {Metasploit::Credential::Private} component
      # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to tie the Core to
      # @option opts [Fixnum] :task_id The ID of the `Mdm::Task` to link this Core to
      # @return [NilClass] if there is no active database connection
      # @return [Metasploit::Credential::Core]
      def create_credential_core(opts={})
        return nil unless active_db?
        origin       = opts.fetch(:origin)
        workspace_id = opts.fetch(:workspace_id)

        private_id   = opts[:private].try(:id)
        public_id    = opts[:public].try(:id)
        realm_id     = opts[:realm].try(:id)

        tries = 3
        begin
          core = Metasploit::Credential::Core.where(private_id: private_id, public_id: public_id, realm_id: realm_id, workspace_id: workspace_id).first_or_initialize
          if core.origin_id.nil?
            core.origin = origin
          end
          if opts[:task_id]
            core.tasks << Mdm::Task.find(opts[:task_id])
          end
          core.save!
        rescue ActiveRecord::RecordNotUnique
          tries -= 1
          if tries > 0
            retry
          end
        end

        core
      end

      # This method is responsible for creating a {Metasploit::Credential::Login} object
      # which ties a {Metasploit::Credential::Core} to the `Mdm::Service` it is a valid
      # credential for.
      #
      # @option opts [String] :access_level The access level to assign to this login if we know it
      # @option opts [String] :address The address of the `Mdm::Host` to link this Login to
      # @option opts [DateTime] :last_attempted_at The last time this Login was attempted
      # @option opts [Metasploit::Credential::Core] :core The {Metasploit::Credential::Core} to link this login to
      # @option opts [Fixnum] :port The port number of the `Mdm::Service` to link this Login to
      # @option opts [String] :service_name The service name to use for the `Mdm::Service`
      # @option opts [String] :status The status for the Login object
      # @option opts [String] :protocol The protocol type of the `Mdm::Service` to link this Login to
      # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to use for the `Mdm::Host`
      # @option opts [Fixnum] :task_id The ID of the `Mdm::Task` to link this Login to
      # @raise [KeyError] if a required option is missing
      # @return [NilClass] if there is no active database connection
      # @return [Metasploit::Credential::Login]
      def create_credential_login(opts={})
        return nil unless active_db?
        access_level       = opts.fetch(:access_level, nil)
        core               = opts.fetch(:core)
        last_attempted_at  = opts.fetch(:last_attempted_at, nil)
        status             = opts.fetch(:status)

        service_object = create_credential_service(opts)
        login_object = Metasploit::Credential::Login.where(core_id: core.id, service_id: service_object.id).first_or_initialize

        if opts[:task_id]
          login_object.tasks << Mdm::Task.find(opts[:task_id])
        end

        login_object.access_level      = access_level if access_level
        login_object.last_attempted_at = last_attempted_at if last_attempted_at
        login_object.status            = status
        login_object.save!
        login_object
      end

      # This method is responsible for creating the various Credential::Origin objects.
      # It takes a key for the Origin type and delegates to the correct sub-method.
      #
      # @option opts [Symbol] :origin_type The Origin type we are trying to create
      # @option opts [String] :address The address of the `Mdm::Host` to link this Origin to
      # @option opts [Fixnum] :port The port number of the `Mdm::Service` to link this Origin to
      # @option opts [String] :service_name The service name to use for the `Mdm::Service`
      # @option opts [String] :protocol The protocol type of the `Mdm::Service` to link this Origin to
      # @option opts [String] :module_fullname The fullname of the Metasploit Module to link this Origin to
      # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to use for the `Mdm::Host`
      # @option opts [Fixnum] :task_id The ID of the `Mdm::Task` to link this Origin to
      # @option opts [String] :filename The filename of the file that was imported
      # @option opts [Fixnum] :user_id The ID of the `Mdm::User` to link this Origin to
      # @option opts [Fixnum] :session_id The ID of the `Mdm::Session` to link this Origin to
      # @option opts [String] :post_reference_name The reference name of the Metasploit Post module to link the origin to
      # @raise [ArgumentError] if an invalid origin_type was provided
      # @raise [KeyError] if a required option is missing
      # @return [NilClass] if there is no connected database
      # @return [Metasploit::Credential::Origin::Manual] if :origin_type was :manual
      # @return [Metasploit::Credential::Origin::Import] if :origin_type was :import
      # @return [Metasploit::Credential::Origin::Service] if :origin_type was :service
      # @return [Metasploit::Credential::Origin::Session] if :origin_type was :session
      def create_credential_origin(opts={})
        return nil unless active_db?
        case opts[:origin_type]
          when :cracked_password
            create_credential_origin_cracked_password(opts)
          when :import
            create_credential_origin_import(opts)
          when :manual
            create_credential_origin_manual(opts)
          when :service
            create_credential_origin_service(opts)
          when :session
            create_credential_origin_session(opts)
          else
            raise ArgumentError, "Unknown Origin Type #{opts[:origin_type]}"
        end
      end

      # This method is responsible for creating {Metasploit::Credential::Origin::CrackedPassword} objects.
      # These are the oorigins that show that a password Credential was obtained by cracking a hash Credential
      # that previously existed in the database.
      #
      # @option opts [Fixnum] :originating_core_id The ID of the originating Credential core.
      # @return [NilClass] if there is no connected database
      # @return [Metasploit::Credential::Origin::CrackedPassword] The created {Metasploit::Credential::Origin::CrackedPassword} object
      def create_credential_origin_cracked_password(opts={})
        return nil unless active_db?
        originating_core_id = opts.fetch(:originating_core_id)

        Metasploit::Credential::Origin::CrackedPassword.where(metasploit_credential_core_id: originating_core_id ).first_or_create
      end

      # This method is responsible for creating {Metasploit::Credential::Origin::Import} objects.
      #
      # @option opts [String] :filename The filename of the file that was imported
      # @raise [KeyError] if a required option is missing
      # @return [NilClass] if there is no connected database
      # @return [Metasploit::Credential::Origin::Manual] The created {Metasploit::Credential::Origin::Import} object
      def create_credential_origin_import(opts={})
        return nil unless active_db?
        filename = opts.fetch(:filename)

        Metasploit::Credential::Origin::Import.where(filename: filename).first_or_create
      end

      # This method is responsible for creating {Metasploit::Credential::Origin::Manual} objects.
      #
      # @option opts [Fixnum] :user_id The ID of the `Mdm::User` to link this Origin to
      # @raise [KeyError] if a required option is missing
      # @return [NilClass] if there is no connected database
      # @return [Metasploit::Credential::Origin::Manual] The created {Metasploit::Credential::Origin::Manual} object
      def create_credential_origin_manual(opts={})
        return nil unless active_db?
        user_id = opts.fetch(:user_id)

        Metasploit::Credential::Origin::Manual.where(user_id: user_id).first_or_create
      end

      # This method is responsible for creating {Metasploit::Credential::Origin::Service} objects.
      # If there is not a matching `Mdm::Host` it will create it. If there is not a matching
      # `Mdm::Service` it will create that too.
      #
      # @option opts [String] :address The address of the `Mdm::Host` to link this Origin to
      # @option opts [Fixnum] :port The port number of the `Mdm::Service` to link this Origin to
      # @option opts [String] :service_name The service name to use for the `Mdm::Service`
      # @option opts [String] :protocol The protocol type of the `Mdm::Service` to link this Origin to
      # @option opts [String] :module_fullname The fullname of the Metasploit Module to link this Origin to
      # @raise [KeyError] if a required option is missing
      # @return [NilClass] if there is no connected database
      # @return [Metasploit::Credential::Origin::Service] The created {Metasploit::Credential::Origin::Service} object
      def create_credential_origin_service(opts={})
        return nil unless active_db?
        module_fullname  = opts.fetch(:module_fullname)

        service_object = create_credential_service(opts)

        Metasploit::Credential::Origin::Service.where(service_id: service_object.id, module_full_name: module_fullname).first_or_create
      end

      # This method is responsible for creating {Metasploit::Credential::Origin::Session} objects.
      #
      # @option opts [Fixnum] :session_id The ID of the `Mdm::Session` to link this Origin to
      # @option opts [String] :post_reference_name The reference name of the Metasploit Post module to link the origin to
      # @raise [KeyError] if a required option is missing
      # @return [NilClass] if there is no connected database
      # @return [Metasploit::Credential::Origin::Session] The created {Metasploit::Credential::Origin::Session} object
      def create_credential_origin_session(opts={})
        return nil unless active_db?
        session_id           = opts.fetch(:session_id)
        post_reference_name  = opts.fetch(:post_reference_name)

        Metasploit::Credential::Origin::Session.where(session_id: session_id, post_reference_name: post_reference_name).first_or_create
      end

      # This method is responsible for the creation of {Metasploit::Credential::Private} objects.
      # It will create the correct subclass based on the type.
      #
      # @option opts [String] :jtr_format The format for John the ripper to use to try and crack this
      # @option opts [String] :private_data The actual data for the private (e.g. password, hash, key etc)
      # @option opts [Symbol] :private_type The type of {Metasploit::Credential::Private} to create
      # @raise [ArgumentError] if a valid type is not supplied
      # @raise [KeyError] if a required option is missing
      # @return [NilClass] if there is no active database connection
      # @return [Metasploit::Credential::Password] if the private_type was :password
      # @return [Metasploit::Credential::SSHKey] if the private_type was :ssh_key
      # @return [Metasploit::Credential::NTLMHash] if the private_type was :ntlm_hash
      # @return [Metasploit::Credential::NonreplayableHash] if the private_type was :nonreplayable_hash
      def create_credential_private(opts={})
        return nil unless active_db?
        private_data = opts.fetch(:private_data)
        private_type = opts.fetch(:private_type)

        case private_type
          when :password
            private_object = Metasploit::Credential::Password.where(data: private_data).first_or_create
          when :ssh_key
            private_object = Metasploit::Credential::SSHKey.where(data: private_data).first_or_create
          when :ntlm_hash
            private_object = Metasploit::Credential::NTLMHash.where(data: private_data).first_or_create
            private_object.jtr_format = 'nt,lm'
          when :nonreplayable_hash
            private_object = Metasploit::Credential::NonreplayableHash.where(data: private_data).first_or_create
            if opts[:jtr_format].present?
              private_object.jtr_format = opts[:jtr_format]
            end
          else
            raise ArgumentError, "Invalid Private type: #{private_type}"
        end
        private_object.save!
        private_object
      end

      # This method is responsible for the creation of {Metasploit::Credential::Public} objects.
      #
      # @option opts [String] :username The username to use for the {Metasploit::Credential::Public}
      # @raise [KeyError] if a required option is missing
      # @return [NilClass] if there is no active database connection
      # @return [Metasploit::Credential::Public]
      def create_credential_public(opts={})
        return nil unless active_db?
        username = opts.fetch(:username)

        Metasploit::Credential::Public.where(username: username).first_or_create
      end

      # This method is responsible for creating the {Metasploit::Credential::Realm} objects
      # that may be required.
      #
      # @option opts [String] :realm_key The type of Realm this is (e.g. 'Active Directory Domain')
      # @option opts [String] :realm_value The actual Realm name (e.g. contosso)
      # @raise [KeyError] if a required option is missing
      # @return [NilClass] if there is no active database connection
      # @return [Metasploit::Credential::Realm] if it successfully creates or finds the object
      def create_credential_realm(opts={})
        return nil unless active_db?
        realm_key   = opts.fetch(:realm_key)
        realm_value = opts.fetch(:realm_value)

        Metasploit::Credential::Realm.where(key: realm_key, value: realm_value).first_or_create!
      end



      # This method is responsible for creating a barebones `Mdm::Service` object
      # for use by Credential object creation.
      #
      # @option opts [String] :address The address of the `Mdm::Host`
      # @option opts [Fixnum] :port The port number of the `Mdm::Service`
      # @option opts [String] :service_name The service name to use for the `Mdm::Service`
      # @option opts [String] :protocol The protocol type of the `Mdm::Service``
      # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to use for the `Mdm::Host`
      # @raise [KeyError] if a required option is missing
      # @return [NilClass] if there is no connected database
      # @return [Mdm::Service]
      def create_credential_service(opts={})
        return nil unless active_db?
        address          = opts.fetch(:address)
        port             = opts.fetch(:port)
        service_name     = opts.fetch(:service_name)
        protocol         = opts.fetch(:protocol)
        workspace_id     = opts.fetch(:workspace_id)

        host_object    = Mdm::Host.where(address: address, workspace_id: workspace_id).first_or_create
        service_object = Mdm::Service.where(host_id: host_object.id, port: port, proto: protocol).first_or_initialize

        service_object.name = service_name
        service_object.save!

        service_object
      end

      # This method checks to see if a {Metasploit::Credential::Login} exists for a given
      # set of details. If it does exists, we then appropriately set the status to one of our
      # failure statuses.
      #
      # @option opts [String] :address The address of the host we attempted
      # @option opts [Fixnum] :port the port of the service we attempted
      # @option opts [String] :protocol the transport protocol of the service we attempted
      # @option opts [String] :public A string representation of the public we tried
      # @option opts [String] :private A string representation of the private we tried
      # @option opts [Symbol] :status The status symbol from the {Metasploit::Framework::LoginScanner::Result}
      # @raise [KeyError] if any of the above options are missing
      # @return [void] Do not worry about the return value from this method
      def invalidate_login(opts = {})
        return nil unless active_db?
        address     = opts.fetch(:address)
        port        = opts.fetch(:port)
        protocol    = opts.fetch(:protocol)
        public      = opts.fetch(:public)
        private     = opts.fetch(:private)
        realm_key   = opts.fetch(:realm_key)
        realm_value = opts.fetch(:realm_value)
        status      = opts.fetch(:status)


        pub_obj = Metasploit::Credential::Public.where(username: public).first.try(:id)
        priv_obj = Metasploit::Credential::Private.where(data: private).first.try(:id)
        realm_obj = Metasploit::Credential::Realm.where(key: realm_key, value: realm_value).first.try(:id)

        core = Metasploit::Credential::Core.where(public_id: pub_obj, private_id: priv_obj, realm_id: realm_obj).first

        # Do nothing else if we have no matching core. Otherwise look for a Login.
        if core.present?
          login = core.logins.joins(service: :host).where(services: { port: port, proto: protocol } ).where( hosts: {address: address}).readonly(false).first

          if login.present?
            login.status = status
            login.last_attempted_at = DateTime.now
            login.save!
          end

        end

      end

    end
  end
end