# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'monitor' require 'contrast/components/logger' require 'contrast/components/scope' require 'contrast/utils/patching/policy/patch_utils' require 'contrast/agent' require 'contrast/logger/log' require 'contrast/agent/patching/policy/method_policy' require 'contrast/agent/patching/policy/patch_status' require 'contrast/agent/patching/policy/trigger_node' # assess require 'contrast/agent/assess/policy/policy' require 'contrast/agent/assess/policy/propagation_method' require 'contrast/agent/assess/policy/source_method' require 'contrast/agent/assess/policy/trigger_method' # protect require 'contrast/agent/protect/policy/policy' # inventory require 'contrast/agent/inventory/policy/policy' module Contrast module Agent module Patching module Policy # This is how we patch into our customer's code. It provides a way to # track which classes we need to patch into and, once we've woven, # provides a map for which methods our renamed functions need to call # and how. module Patch extend Contrast::Utils::Patching::PatchUtils class << self include Contrast::Agent::Assess::Policy::SourceMethod include Contrast::Agent::Assess::Policy::PropagationMethod include Contrast::Agent::Assess::Policy::TriggerMethod include Contrast::Components::Logger::InstanceMethods include Contrast::Components::Scope::InstanceMethods POLICIES = [ Contrast::Agent::Assess::Policy::Policy, Contrast::Agent::Inventory::Policy::Policy, Contrast::Agent::Protect::Policy::Policy ].cs__freeze def enter_method_scope! method_policy contrast_enter_method_scopes!(method_policy.scopes_to_enter) end def exit_method_scope! method_policy contrast_exit_method_scopes!(method_policy.scopes_to_exit) end # @param mod [Module] the module in which the patch should be # placed. # @param methods [Array(Symbol)] all the instance or singleton # methods in this clazz. # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] # the policy that applies to the method to be patched. # @return [Boolean] if patched, either by this invocation or a # previous, or not def instrument_with_alias mod, methods, method_policy cs_method_name = build_method_name(mod, method_policy.method_name) # we've already patched this class, don't do it again return true if methods.include?(cs_method_name) # that method is within Contrast definition so it should be skipped method = mod.instance_method(method_policy.method_name) if method_policy.instance_method method = mod.singleton_method(method_policy.method_name) unless method_policy.instance_method return true if method.owner <= Contrast begin contrast_define_method(mod, method_policy, cs_method_name) rescue NameError => e # This shouldn't happen anymore, but just in case calling alias # results in a NameError, we'll be safe here. logger.error( 'Attempted to alias a method on a Module that doesn\'t respond to it.', e, module: mod.cs__name, method: method_policy.method_name) return false end true end # @param mod [Module] the module in which the patch should be # placed. # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] # the policy that applies to the method to be patched. # @return [Boolean] if patched, either by this invocation or a # previous, or not def instrument_with_prepend mod, method_policy contrast_prepend_method(mod, method_policy) end # @param unbound_method [UnboundMethod] An unbound method, that doesn't reference its binding. # This method executes C hooking code. def register_c_hook unbound_method # current binding is as meaningless as any other. but we need something unbound_method.bind_call(self) end # @param target_module_name [String] Fully-qualified module name, as string, to which the C patch applies. # @param unbound_method [UnboundMethod] An unbound method, to be patched into target_module. # @param impl [Symbol] Strategy for applying the patch: { :alias_instance, :alias_singleton, or :prepend }: # :alias_instance -> alias instance method of module # :alias_singleton -> alias instance method of singleton class of module # (equivalent to :alias, where `module = module.singleton class`) # (this is a.k.a. "class-method patch") # :prepend -> prepend instance method of module # :prepending singleton -> prepend singleton method of module # @return [Symbol] new alias for the underlying method (presumably, so the patched method can call it) def register_c_patch target_module_name, unbound_method, impl = :alias_instance # These could be set as AfterLoadPatches. method_name = unbound_method.name.to_sym # rubocop:disable Security/Module/Name -- ruby built in attribute. underlying_method_name = underlying_method_name(method_name, impl) target_module = Module.cs__const_get(target_module_name) target_module = target_module.cs__singleton_class if %i[prepend_singleton alias_singleton].include?(impl) visibility = if target_module.private_instance_methods(false).include?(method_name) :private elsif target_module.protected_instance_methods(false).include?(method_name) :protected elsif target_module.public_instance_methods(false).include?(method_name) :public else raise(NoMethodError, <<~ERR) Tried to register a C-defined #{ impl } patch for \ #{ target_module_name }##{ method_name }, but can't find :#{ method_name }. ERR end reflect_implementation(impl, target_module, unbound_method, visibility) # Ougai::Logger.create_item_with_2args calls Hash#[]=, so we # can't invoke this logging method or we'll seg fault as we'd # change the method definition mid-call # if method_name != :[]= && # Contrast::Agent::Logger.defined! # logger.trace( # 'Registered C-defined patch', # implementation: impl, # module: target_mod, # method: method_name, # visibility: visibility) # end underlying_method_name end # @param impl [Symbol] Strategy for applying the patch: { :alias_instance, :alias_singleton, or :prepend }: # @param target_module [Module] The targeted module # @param unbound_method [UnboundMethod] An unbound method, to be patched into target_module. # @param visibility [Symbol] method visibility def reflect_implementation impl, target_module, unbound_method, visibility method_name = unbound_method.name.to_sym # rubocop:disable Security/Module/Name -- ruby built in attribute. underlying_method_name = underlying_method_name(method_name, impl) case impl when :alias_instance, :alias_singleton # Core to patching. Ignore define method usage cop. # rubocop:disable Performance/Kernel/DefineMethod unless target_module.instance_methods(false).include?(underlying_method_name) # alias_method may be private target_module.send(:alias_method, underlying_method_name, method_name) target_module.send(:define_method, method_name, unbound_method.bind(target_module)) end target_module.send(visibility, method_name) # e.g., module.private(:my_method) when :prepend_instance, :prepend_singleton prepending_module = Module.new prepending_module.send(:define_method, method_name, unbound_method.bind(target_module)) prepending_module.send(visibility, method_name) # This prepends to the singleton class (it patches a class method) target_module.prepend(prepending_module) # rubocop:enable Performance/Kernel/DefineMethod end end # @return [Boolean] def skip_contrast_analysis? return true if in_contrast_scope? return false unless defined?(Contrast::Agent::REQUEST_TRACKER) return false unless Contrast::Agent::REQUEST_TRACKER.current return true unless Contrast::Agent::REQUEST_TRACKER.current.analyze_request? false end # Skip if we should skip_contrast_analysis?, sampling says to ignore this # request, or assess has been disabled. # # @return [Boolean] def skip_assess_analysis? return true if skip_contrast_analysis? !ASSESS&.enabled? end def underlying_method_name method_name, impl return method_name.to_sym if %i[prepend_instance prepend_singleton].include?(impl) build_unbound_method_name(method_name).to_sym end end end end end end end require 'cs__contrast_patch/cs__contrast_patch'