Module: Contrast::Agent::Patching::Policy::Patch

Overview

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.

Constant Summary collapse

POLICIES =
[
  Contrast::Agent::Assess::Policy::Policy,
  Contrast::Agent::Inventory::Policy::Policy,
  Contrast::Agent::Protect::Policy::Policy
].cs__freeze

Constants included from Assess::Policy::TriggerMethod

Assess::Policy::TriggerMethod::NON_REQUEST_RULES

Constants included from Utils::Assess::PropagationMethodUtils

Utils::Assess::PropagationMethodUtils::APPEND_ACTION, Utils::Assess::PropagationMethodUtils::BUFFER_ACTION, Utils::Assess::PropagationMethodUtils::CENTER_ACTION, Utils::Assess::PropagationMethodUtils::CUSTOM_ACTION, Utils::Assess::PropagationMethodUtils::DB_WRITE_ACTION, Utils::Assess::PropagationMethodUtils::INSERT_ACTION, Utils::Assess::PropagationMethodUtils::KEEP_ACTION, Utils::Assess::PropagationMethodUtils::NEXT_ACTION, Utils::Assess::PropagationMethodUtils::NOOP_ACTION, Utils::Assess::PropagationMethodUtils::PREPEND_ACTION, Utils::Assess::PropagationMethodUtils::PROPAGATION_ACTIONS, Utils::Assess::PropagationMethodUtils::REMOVE_ACTION, Utils::Assess::PropagationMethodUtils::REPLACE_ACTION, Utils::Assess::PropagationMethodUtils::RESPONSE_ACTION, Utils::Assess::PropagationMethodUtils::REVERSE_ACTION, Utils::Assess::PropagationMethodUtils::SPLAT_ACTION, Utils::Assess::PropagationMethodUtils::SPLIT_ACTION, Utils::Assess::PropagationMethodUtils::ZERO_LENGTH_ACTIONS

Constants included from Assess::Policy::SourceMethod

Assess::Policy::SourceMethod::COOKIE_KEY_TYPE, Assess::Policy::SourceMethod::COOKIE_TYPE, Assess::Policy::SourceMethod::HEADER_KEY_TYPE, Assess::Policy::SourceMethod::HEADER_TYPE, Assess::Policy::SourceMethod::PARAMETER_KEY_TYPE, Assess::Policy::SourceMethod::PARAMETER_TYPE

Class Method Summary collapse

Methods included from Components::Scope::InstanceMethods

contrast_enter_method_scopes!, contrast_exit_method_scopes!, with_app_scope, with_contrast_scope, with_deserialization_scope, with_split_scope

Methods included from Components::Logger::InstanceMethods

cef_logger, logger

Methods included from Assess::Policy::TriggerMethod

apply_eval_trigger, apply_trigger_rule, build_finding, report_finding

Methods included from Utils::Assess::TriggerMethodUtils

#apply_dataflow_rule, #apply_regexp_rule, #apply_trigger, #find_event_request, #find_request, #reportable?

Methods included from Utils::Assess::EventLimitUtils

#event_limit?, #event_limit_for_rule?, #increment_event_count

Methods included from Assess::Policy::PropagationMethod

apply_propagation, apply_propagator, apply_tags, apply_untags, context_available?

Methods included from Utils::Assess::PropagationMethodUtils

#appropriate_source?, #appropriate_target?, #can_propagate?, #determine_target, #valid_length?, #valid_target?

Methods included from Assess::Policy::SourceMethod

apply_source

Methods included from Utils::Assess::SourceMethodUtils

#analyze?, #determine_source_name, #safe_invocation?

Methods included from Utils::Patching::PatchUtils

apply_assess, apply_inventory, apply_post_patch, apply_pre_patch, apply_protect, apply_trigger_only, build_method_name, build_unbound_method_name

Class Method Details

.enter_method_scope!(method_policy) ⇒ Object



51
52
53
# File 'lib/contrast/agent/patching/policy/patch.rb', line 51

def enter_method_scope! method_policy
  contrast_enter_method_scopes!(method_policy.scopes_to_enter)
end

.exit_method_scope!(method_policy) ⇒ Object



55
56
57
# File 'lib/contrast/agent/patching/policy/patch.rb', line 55

def exit_method_scope! method_policy
  contrast_exit_method_scopes!(method_policy.scopes_to_exit)
end

.instrument_with_alias(mod, methods, method_policy) ⇒ Boolean

Returns if patched, either by this invocation or a previous, or not.

Parameters:

  • mod (Module)

    the module in which the patch should be placed.

  • methods (Array(Symbol))

    all the instance or singleton methods in this clazz.

  • method_policy (Contrast::Agent::Patching::Policy::MethodPolicy)

    the policy that applies to the method to be patched.

Returns:

  • (Boolean)

    if patched, either by this invocation or a previous, or not



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/contrast/agent/patching/policy/patch.rb', line 67

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

.instrument_with_prepend(mod, method_policy) ⇒ Boolean

Returns if patched, either by this invocation or a previous, or not.

Parameters:

Returns:

  • (Boolean)

    if patched, either by this invocation or a previous, or not



98
99
100
# File 'lib/contrast/agent/patching/policy/patch.rb', line 98

def instrument_with_prepend mod, method_policy
  contrast_prepend_method(mod, method_policy)
end

.reflect_implementation(impl, target_module, unbound_method, visibility) ⇒ Object

Parameters:

  • impl (Symbol)

    Strategy for applying the patch: { :alias_instance, :alias_singleton, or :prepend }:

  • target_module (Module)

    The targeted module

  • unbound_method (UnboundMethod)

    An unbound method, to be patched into target_module.

  • visibility (Symbol)

    method visibility



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/contrast/agent/patching/policy/patch.rb', line 161

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

.register_c_hook(unbound_method) ⇒ Object

Parameters:

  • unbound_method (UnboundMethod)

    An unbound method, that doesn’t reference its binding. This method executes C hooking code.



104
105
106
107
# File 'lib/contrast/agent/patching/policy/patch.rb', line 104

def register_c_hook unbound_method
  # current binding is as meaningless as any other.  but we need something
  unbound_method.bind_call(self)
end

.register_c_patch(target_module_name, unbound_method, impl = :alias_instance) ⇒ Symbol

Returns new alias for the underlying method (presumably, so the patched method can call it).

Parameters:

  • target_module_name (String)

    Fully-qualified module name, as string, to which the C patch applies.

  • unbound_method (UnboundMethod)

    An unbound method, to be patched into target_module.

  • impl (Symbol) (defaults to: :alias_instance)

    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

Returns:

  • (Symbol)

    new alias for the underlying method (presumably, so the patched method can call it)



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/contrast/agent/patching/policy/patch.rb', line 119

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

.skip_assess_analysis?Boolean

Skip if we should skip_contrast_analysis?, sampling says to ignore this request, or assess has been disabled.

Returns:

  • (Boolean)


200
201
202
203
204
# File 'lib/contrast/agent/patching/policy/patch.rb', line 200

def skip_assess_analysis?
  return true if skip_contrast_analysis?

  !ASSESS&.enabled?
end

.skip_contrast_analysis?Boolean

Returns:

  • (Boolean)


187
188
189
190
191
192
193
194
# File 'lib/contrast/agent/patching/policy/patch.rb', line 187

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

.underlying_method_name(method_name, impl) ⇒ Object



206
207
208
209
210
# File 'lib/contrast/agent/patching/policy/patch.rb', line 206

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