# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

require 'contrast/agent_lib/api/init'
require 'contrast/agent_lib/api/command_injection'
require 'contrast/agent_lib/api/input_tracing'
require 'contrast/agent_lib/api/panic'
require 'contrast/agent_lib/api/path_semantic_file_security_bypass'
require 'contrast/components/logger'
require 'contrast/utils/object_share'
require 'fileutils'
require 'ffi'
require 'contrast/agent_lib/return_types/eval_result'
require 'contrast/agent_lib/interface_base'

module Contrast
  module AgentLib
    # The interface to react with the AgentLib. This will be the one place of
    # contact with the DynamicLib, synchronized and loaded with modules to use.
    class Interface < InterfaceBase
      # Include the modules to use from the FFI wrappers of the AgentLib.
      include Contrast::AgentLib::Init
      include Contrast::AgentLib::Panic
      include Contrast::AgentLib::InputTracing
      include Contrast::AgentLib::CommandInjection
      include Contrast::AgentLib::PathSemanticFileSecurityBypass
      include Contrast::Components::Logger::InstanceMethods

      # Attached methods are always public. We need to make them private
      # so that this interface is the only mean to call native functions.
      # The dl__ (dynamic lib) wrapper methods are used to transfer and
      # convert data to required by the AgentLib types. If native method
      # is called with wrong arguments a panic is raised. Public instance
      # methods in this module are used to safe guard and set state by
      # instance variables. Add any newly attached native functions
      # included above here:
      private :init, :init_with_options, :change_log_settings, :check_cmd_injection_query,
              :free_check_query_sink_result, :get_index_of_chained_command, :last_error_message,
              :does_command_contain_dangerous_path, :last_error_message_length, :last_error_stack_length,
              :evaluate_header_input, :evaluate_input, :free_eval_result, :check_sql_injection_query,
              :free_check_query_sink_result

      # @return enable_log [Boolean] flag to enable or disable logging.
      attr_reader :enable_log
      # @return log_dir [String] path to existing log directory.
      attr_reader :log_dir
      # @return log_level [String] OFF, ERROR, WARN, INFO, DEBUG or TRACE.
      attr_reader :log_level
      # @return enable_log_change [Boolean] flag set during initialization.
      # If true the log level and enable status could be change during
      # runtime.
      attr_reader :enable_log_change
      # Initialization status
      # @return [Boolean]
      attr_reader :init_status
      # We need to synchronize all calls to the AgentLib vie mutex or monitor.
      #
      # @return mutex [Mutex] to synchronize calls, also to prevent situations
      # such as calling a re_init on not init lib:
      # This is not the thread you are looking for.
      attr_reader :mutex
      # retrieves last error message.
      # note: last_error_message is the name of the attached function and will
      # override the agent_lib native method if renamed.
      #
      # @return last_error_message [String]
      attr_reader :last_error

      # Initializes the Agent lib.
      #
      # @param enable_logging [Boolean, nil] flag to enable or disable logging.
      # @param set_log_level [Integer, nil]
      # @param set_log_dir [String, nil] dir to write log files.
      # @return [Boolean] true if success.
      # @raise [StandardError] Any Errors raised in the init process are most
      # likely to be a C segfaults and termination, probably redundant but safe.
      def initialize enable_logging = nil, set_log_level = nil, set_log_dir = nil
        super
        @mutex = Mutex.new
        @enable_log = !!enable_logging
        self.log_level = set_log_level
        @log_dir = create_log_dir(set_log_dir)
        @init_status = if enable_log && log_dir && log_level
                         # Preferred as we will be able to to set the log level at run time.
                         @enable_log_change = true
                         with_mutex { dl__init_with_options(enable_log, log_dir, log_level) }
                       else
                         # Initialize the lib without logging.
                         # The log level could not be changed during runtime.
                         @enable_log_change = false
                         with_mutex { dl__init }
                       end
      rescue StandardError => e
        logger.error('Could not start АgentLib', error: e, backtrace: e.backtrace)
      end

      # Changes the logs level in runtime.
      #
      # @param new_enable_log [Boolean] flag to enable or disable logging this sets the inner flag.
      # @param new_log_level [Integer]
      def change_log_options new_enable_log, new_log_level
        return unless @enable_log_change

        update_logging(new_log_level, new_enable_log)
        with_mutex { dl__change_log_settings(enable_log, log_level) }
      end

      # Set the log_level of the Interface.
      #
      # @param level [Integer, nil] one of:
      # [-1...4]
      def log_level= level = nil
        set_level = level.nil? ? 4 : level
        update_log_level(set_level)
      end

      # Execute calls to Native methods with mutex.
      # If Error is raised from the AgentLib it will
      # be logged. If C error occurs there will be a
      # segfault and termination.
      #
      # @raise [StandardError] Capture any unforeseen errors.
      # @return [String, Boolean, Integer, nil] Wrapper method's
      # return types, or nil if something terrible happens.
      def with_mutex &block
        return_value = mutex.synchronize(&block)
      rescue StandardError => e
        logger.error('AgentLib runtime error', error: e, backtrace: e.backtrace)
        handle_error
      ensure
        # Since every method is called with_mutex this is
        # the natural candidate for handling any errors
        # from the AgentLib.
        handle_error
        # The return value is used to set some instance
        # variable state depending on the original return
        # of the wrapper methods.
        return_value
      end

      # Method to extract last error if any and log it.
      def handle_error
        @last_error = mutex.synchronize { dl__get_error }
        logger.error('AgentLib encountered exception: ', error: @last_error) unless @last_error.empty?
      end

      # Checks that a given shell command contained a chained command.
      # This is used for the cmd-injection-semantic-chained-commands rule.
      #
      # @param cmd [String] command to check.
      # @return index[Integer, nil] Returns the index of the command chaining if found.
      # If the chaining index is >= 0, an injection is detected. Returns -1 when no
      # command chaining is found.
      def chained_cmdi_index cmd
        return unless cmd

        with_mutex { dl__index_of_chained_command(cmd) }
      end

      # Checks if a given shell command is trying to access a dangerous path.
      # This is used for the cmd-injection-semantic-dangerous-paths rule.
      #
      # @param path [String] path to check.
      # @return index[Boolean] Returns true if a dangerous path is found.
      # Returns false if no dangerous paths are found.
      def dangerous_path? path
        return unless path

        with_mutex { dl__dangerous_path?(path) }
      end

      # Evaluate a subset of executing shell command for token boundaries being crossed.
      # This is used by the cmd-injection rule during sink time. If a previously
      # worth-watching input is contained in the shell command it crosses token
      # boundaries then an injection should be raised.
      #
      # @param input_index [Integer] index in the cmdText string where user input was found.
      # @param input_length [Integer] length of the user input.
      # @param input_cmd [String] full text of the command being executed.
      # @return result [Hash, nil] output parameter which will contain the result of the evaluation.
      def check_cmdi_query input_index, input_length, input_cmd
        return unless input_index && input_length && input_cmd

        with_mutex { dl__check_cmdi_query(input_index, input_length, input_cmd) }
      end

      # Evaluates a header for input tracing rules.
      #
      # @param header_name [String] key/name of the header to be evaluated.
      # NOTE: the only header names used are "custom", "accept", and "user-agent".
      # Header-name-used is part of the output because the accept and user-agent
      # headers get special handling by agent-lib, so they are evaluated twice -
      # once exactly like all other headers and once with the specific header that
      # gets special handling.
      # @param header_value [String] header value
      # @param rule_set [Integer] One of Contrast::AgentLib::Interface::RULE_SET
      # @param eval_options [Integer] 0 or 1 from Contrast::AgentLib::Interface::EVAL_OPTIONS
      # @return [Contrast::AgentLib::EvalResult, nil]
      def eval_header header_name, header_value, rule_set, eval_options
        return unless header_name && header_value && rule_set && eval_options

        result = with_mutex { dl__eval_header_input(header_name, header_value, rule_set, eval_options) }
        return EvalResult.new(result) if result

        nil
      end

      # Evaluates an input part for input tracing rules. This should be used for all input parts except headers.
      #
      # @param input [String] input value to be evaluated.
      # @param input_type [Integer]  type of the input one of Contrast::AgentLib::Interface::INPUT_SET
      # @param rule_set [Integer] One of Contrast::AgentLib::Interface::RULE_SET
      # @param eval_options [Integer] 0 or 1 from Contrast::AgentLib::Interface::EVAL_OPTIONS
      # @return [Contrast::AgentLib::EvalResult, nil]
      def eval_input input, input_type, rule_set, eval_options
        return unless input && input_type && rule_set && eval_options

        result = with_mutex { dl__eval_input(input, input_type, rule_set, eval_options) }
        return EvalResult.new(result) if result

        nil
      end

      def check_sql_query input_index, input_length, db_type, sql_query
        return unless input_index && input_length && db_type && sql_query

        with_mutex { dl__check_sql_injection_query(input_index, input_length, db_type, sql_query) }
      end

      # Invoke Path Semantic File Security Bypass
      #
      # @param file_path[String] the absolute file path
      # @param is_custom_code[Integer] is this is being called from customer (user) code
      #     or the framework
      # @return result[Integer, nil] returns:
      #     1 => security bypass is detected.
      #     0 => no security bypass is detected.
      def check_path_semantic_security_bypass file_path, is_custom_code
        return unless file_path || is_custom_code

        with_mutex { dl__does_file_bypass_security(file_path, is_custom_code) }
      end
    end
  end
end