# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'monitor' # base cs__scoped_require 'contrast/core_extensions/module' cs__scoped_require 'contrast/agent/patching/policy/method_policy' cs__scoped_require 'contrast/agent/patching/policy/patch_status' cs__scoped_require 'contrast/agent/patching/policy/trigger_node' cs__scoped_require 'contrast/components/interface' cs__scoped_require 'contrast/utils/scope_util' # assess cs__scoped_require 'contrast/agent/assess/policy/policy' cs__scoped_require 'contrast/agent/assess/policy/propagation_method' cs__scoped_require 'contrast/agent/assess/policy/source_method' cs__scoped_require 'contrast/agent/assess/policy/trigger_method' cs__scoped_require 'contrast/core_extensions/assess' # inventory cs__scoped_require 'contrast/agent/inventory/policy/policy' cs__scoped_require 'contrast/core_extensions/inventory' # protect cs__scoped_require 'contrast/agent/protect/policy/policy' cs__scoped_require 'contrast/core_extensions/protect' cs__scoped_require 'contrast/core_extensions/protect/kernel' 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 class << self include Contrast::Agent::Assess::Policy::SourceMethod include Contrast::Agent::Assess::Policy::PropagationMethod include Contrast::Agent::Assess::Policy::TriggerMethod extend Contrast::Utils::ScopeUtil include Contrast::Components::Interface access_component :logging, :analysis, :agent, :scope POLICIES = [ Contrast::Agent::Assess::Policy::Policy, Contrast::Agent::Inventory::Policy::Policy, Contrast::Agent::Protect::Policy::Policy ].cs__freeze # THIS IS CALLED FROM C. Do not change the signature lightly. # # This method functions to call the infilter methods from our # patches, allowing for analysis and reporting at the point just # before the patched code is invoked. # # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] # Mapping of the triggers on the given method. # @param method [Symbol] The method into which we're patching # @param exception [StandardError] Any exception raised during the # call of the patched method. # @param object [Object] The object on which the method is invoked, # typically what would be returned by self. # @param args [Array] The arguments passed to the method # being invoked. def apply_pre_patch method_policy, method, exception, object, args apply_protect(method_policy, method, exception, object, args) apply_inventory(method_policy, method, exception, object, args) # We were told to block something, so we gotta. Don't catch this # one, let it get back to our Middleware or even all the way out to # the framework rescue Contrast::SecurityException => e # This can be C's responsibility, but it's a lot of code. # Rewrite this to make C do it, once we have better macroing. exit_contrast_scope! raise e # Anything else was our bad and we gotta catch that to allow for # normal application flow rescue StandardError => e logger.error(e, 'Unable to apply pre patch to method.') end # THIS IS CALLED FROM C. Do not change the signature lightly. # # This method functions to call the infilter methods from our # patches, allowing for analysis and reporting at the point just # after the patched code is invoked # # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] # Mapping of the triggers on the given method. # @param preshift [Contrast::Agent::Assess::PreShift] The capture # of the state of the code just prior to the invocation of the # patched method. # @param object [Object] The object on which the method was # invoked, typically what would be returned by self. # @param ret [Object] The return of the method that was invoked. # @param args [Array] The arguments passed to the method # being invoked. # @param block [Proc] The block passed to the method that was # invoked. def apply_post_patch method_policy, preshift, object, ret, args, block apply_assess(method_policy, preshift, object, ret, args, block) rescue StandardError => e logger.error(e, 'Unable to apply post patch to method.') end # Apply the Protect patch which applies to the given method. # # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] # Mapping of the triggers on the given method. # @param method [Symbol] The method into which we're patching # @param exception [StandardError] Any exception raised during the # call of the patched method. # @param object [Object] The object on which the method is invoked, # typically what would be returned by self. # @param args [Array] The arguments passed to the method # being invoked. def apply_protect method_policy, method, exception, object, args return unless PROTECT.enabled? apply_trigger_only(method_policy&.protect_node, method, exception, object, args) end # Apply the Inventory patch which applies to the given method. # # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] # Mapping of the triggers on the given method. # @param method [Symbol] The method into which we're patching # @param exception [StandardError] Any exception raised during the # call of the patched method. # @param object [Object] The object on which the method is invoked, # typically what would be returned by self. # @param args [Array] The arguments passed to the method # being invoked. def apply_inventory method_policy, method, exception, object, args return unless INVENTORY.enabled? apply_trigger_only(method_policy&.inventory_node, method, exception, object, args) end # Apply the Assess patches which apply to the given method. # # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] # Mapping of the triggers on the given method. # @param preshift [Contrast::Agent::Assess::PreShift] The capture # of the state of the code just prior to the invocation of the # patched method. # @param object [Object] The object on which the method was # invoked, typically what would be returned by self. # @param ret [Object] The return of the method that was invoked. # @param args [Array] The arguments passed to the method # being invoked. # @param block [Proc] The block passed to the method that was # invoked. def apply_assess method_policy, preshift, object, ret, args, block source_ret = nil propagated_ret = nil return ret unless method_policy && ASSESS.enabled? current_context = Contrast::Agent::REQUEST_TRACKER.current return ret unless current_context.analyze_request? trigger_node = method_policy.trigger_node Contrast::Agent::Assess::Policy::TriggerMethod.apply_trigger_rule(trigger_node, object, ret, args) if trigger_node if method_policy.source_node # If we were given a frozen return, and it was the target of a # source, and we have frozen sources enabled, we'll need to # replace the return. Note, this is not the default case. source_ret = Contrast::Agent::Assess::Policy::SourceMethod.source_patchers(method_policy, object, ret, args) end if method_policy.propagation_node propagated_ret = Contrast::Agent::Assess::Policy::PropagationMethod.apply_propagation( method_policy, preshift, object, source_ret || ret, args, block) end handle_return(propagated_ret, source_ret, ret) rescue StandardError => e logger.error(e, 'Unable to assess method call.') handle_return(propagated_ret, source_ret, ret) rescue Exception => e # rubocop:disable Lint/RescueException logger.error(e, 'Unable to assess method call.') handle_return(propagated_ret, source_ret, ret) # This will unwind the stack, so we handle our own scope exit. # Move this to C when it's convenient. exit_contrast_scope! raise e end # Generic invocation of the Inventory or Protect patch which apply # to the given method. # # @param trigger_node [Contrast::Agent::Inventory::Policy::TriggerNode] # Mapping of the specific trigger on the given method. # @param method [Symbol] The method into which we're patching # @param exception [StandardError] Any exception raised during the # call of the patched method. # @param object [Object] The object on which the method is invoked, # typically what would be returned by self. # @param args [Array] The arguments passed to the method # being invoked. def apply_trigger_only trigger_node, method, exception, object, args return unless trigger_node # If that rule only applies in the case of an exception being # thrown and there's no exception here, move along, or vice versa return if trigger_node.on_exception && !exception return if !trigger_node.on_exception && exception # Each patch has an applicator that handles logic for it. Think # of this as being similar to propagator actions, most closely # resembling CUSTOM - they all have a common interface but their # own logic based on what's in the method(s) they've been patched # into. applicator = trigger_node.applicator # Each patch also knows the method of its applicator. Some # things, like AppliesXxeRule, have different methods depending # on the library patched. This lets us handle the boilerplate of # patching while still allowing for custom handling of the # methods. applicator_method = trigger_node.applicator_method # By calling send like this, we can reuse all the patching. # We `send` to the given method of the given class # (applicator) since they all accept the same inputs applicator.send(applicator_method, method, exception, trigger_node.properties, object, args) end # Method to choose which replaced return from the post_patch to # actually return # # @param propagated_ret [Object, nil] The replaced return from the # propagation patch. # @param source_ret [Object, nil] The replaced return from the # source patch. # @param ret [Object, nil] The original return of the patched # method. # @return [Object, nil] The thing to return from the post patch. def handle_return propagated_ret, source_ret, ret safe_return = propagated_ret || source_ret || ret safe_return.rewind if Contrast::Utils::IOUtil.should_rewind?(safe_return) safe_return end # Given a class, method, and type, return a symbol in the format # __ def build_method_name patcher_class, patcher_method (Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START + patcher_class.cs__name.gsub('::', '_').downcase + Contrast::Utils::ObjectShare::UNDERSCORE + patcher_method.to_s).to_sym 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) 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.debug( e, "The class #{ mod.cs__name } does not respond to the "\ "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 end end end end end end cs__scoped_require 'cs__contrast_patch/cs__contrast_patch'