# Copyright (c) 2023 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