Module: Contrast::Agent::Patching::Policy::Patch
- Extended by:
- Assess::Policy::PropagationMethod, Assess::Policy::SourceMethod, Assess::Policy::TriggerMethod, Components::Interface
- Defined in:
- lib/contrast/agent/patching/policy/patch.rb,
ext/cs__common/cs__common.c
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::CURRENT_FINDING_VERSION, Assess::Policy::TriggerMethod::MINIMUM_FINDING_VERSION
Constants included from Assess::Policy::PropagationMethod
Assess::Policy::PropagationMethod::APPEND_ACTION, Assess::Policy::PropagationMethod::CENTER_ACTION, Assess::Policy::PropagationMethod::CUSTOM_ACTION, Assess::Policy::PropagationMethod::DB_WRITE_ACTION, Assess::Policy::PropagationMethod::INSERT_ACTION, Assess::Policy::PropagationMethod::KEEP_ACTION, Assess::Policy::PropagationMethod::NEXT_ACTION, Assess::Policy::PropagationMethod::NOOP_ACTION, Assess::Policy::PropagationMethod::PREPEND_ACTION, Assess::Policy::PropagationMethod::PROPAGATION_ACTIONS, Assess::Policy::PropagationMethod::REMOVE_ACTION, Assess::Policy::PropagationMethod::REPLACE_ACTION, Assess::Policy::PropagationMethod::REVERSE_ACTION, Assess::Policy::PropagationMethod::SPLAT_ACTION, Assess::Policy::PropagationMethod::SPLIT_ACTION, Assess::Policy::PropagationMethod::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
-
.apply_assess(method_policy, preshift, object, ret, args, block) ⇒ Object
Apply the Assess patches which apply to the given method.
-
.apply_inventory(method_policy, method, exception, object, args) ⇒ Object
Apply the Inventory patch which applies to the given method.
-
.apply_post_patch(method_policy, preshift, object, ret, args, block) ⇒ Object
THIS IS CALLED FROM C.
-
.apply_pre_patch(method_policy, method, exception, object, args) ⇒ Object
THIS IS CALLED FROM C.
-
.apply_protect(method_policy, method, exception, object, args) ⇒ Object
Apply the Protect patch which applies to the given method.
-
.apply_trigger_only(trigger_node, method, exception, object, args) ⇒ Object
Generic invocation of the Inventory or Protect patch which apply to the given method.
-
.build_method_name(patched_class, patched_method) ⇒ Symbol
Given a module and method, construct an expected name for the alias by which Contrast will reference the original.
-
.build_unbound_method_name(patcher_method) ⇒ Object
Given a method, return a symbol in the format <method_start>unbound<method_name>.
- .enter_method_scope!(method_policy) ⇒ Object
- .exit_method_scope!(method_policy) ⇒ Object
-
.handle_return(propagated_ret, source_ret, ret) ⇒ Object?
Method to choose which replaced return from the post_patch to actually return.
-
.instrument_with_alias(mod, methods, method_policy) ⇒ Boolean
If patched, either by this invocation or a previous, or not.
-
.instrument_with_prepend(mod, method_policy) ⇒ Boolean
If patched, either by this invocation or a previous, or not.
- .register_c_hook(unbound_method) ⇒ Object
-
.register_c_patch(target_module_name, unbound_method, impl = :alias_instance) ⇒ Symbol
New alias for the underlying method (presumably, so the patched method can call it).
-
.skip_assess_analysis? ⇒ Boolean
Skip if we should skip_contrast_analysis?, sampling says to ignore this request, or assess has been disabled.
- .skip_contrast_analysis? ⇒ Boolean
Methods included from Components::Interface
Methods included from Assess::Policy::TriggerMethod
apply_eval_trigger, apply_trigger_rule, build_finding, report_finding
Methods included from Assess::Policy::PropagationMethod
apply_propagation, apply_propagator, apply_tags, apply_untags, appropriate_target?, can_propagate?, context_available?, determine_target, valid_length?, valid_target?
Methods included from Assess::Policy::SourceMethod
Class Method Details
.apply_assess(method_policy, preshift, object, ret, args, block) ⇒ Object
Apply the Assess patches which apply to the given method.
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 175 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('Unable to assess method call.', e) handle_return(propagated_ret, source_ret, ret) rescue Exception => e # rubocop:disable Lint/RescueException logger.error('Unable to assess method call.', e) handle_return(propagated_ret, source_ret, ret) raise e end |
.apply_inventory(method_policy, method, exception, object, args) ⇒ Object
Apply the Inventory patch which applies to the given method.
151 152 153 154 155 156 157 158 159 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 151 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_post_patch(method_policy, preshift, object, ret, args, block) ⇒ Object
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
112 113 114 115 116 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 112 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('Unable to apply post patch to method.', e) end |
.apply_pre_patch(method_policy, method, exception, object, args) ⇒ Object
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.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 75 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) rescue Contrast::SecurityException => e # 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 raise e rescue StandardError => e # Anything else was our bad and we gotta catch that to allow for # normal application flow logger.error('Unable to apply pre patch to method.', e) rescue Exception => e # rubocop:disable Lint/RescueException # This is something like NoMemoryError that we can't # hope to handle. Nonetheless, shouldn't leak scope. exit_contrast_scope! raise e end |
.apply_protect(method_policy, method, exception, object, args) ⇒ Object
Apply the Protect patch which applies to the given method.
129 130 131 132 133 134 135 136 137 138 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 129 def apply_protect method_policy, method, exception, object, args return unless AGENT.enabled? return unless PROTECT.enabled? apply_trigger_only(method_policy&.protect_node, method, exception, object, args) end |
.apply_trigger_only(trigger_node, method, exception, object, args) ⇒ Object
Generic invocation of the Inventory or Protect patch which apply to the given method.
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 222 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. # 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 trigger_node.applicator.send(applicator_method, method, exception, trigger_node.properties, object, args) end |
.build_method_name(patched_class, patched_method) ⇒ Symbol
Given a module and method, construct an expected name for the alias by which Contrast will reference the original.
269 270 271 272 273 274 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 269 def build_method_name patched_class, patched_method (Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START + patched_class.cs__name.gsub('::', '_').downcase + Contrast::Utils::ObjectShare::UNDERSCORE + patched_method.to_s).to_sym end |
.build_unbound_method_name(patcher_method) ⇒ Object
Given a method, return a symbol in the format <method_start>unbound<method_name>
278 279 280 281 282 283 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 278 def build_unbound_method_name patcher_method (Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START + 'unbound' + Contrast::Utils::ObjectShare::UNDERSCORE + patcher_method.to_s).to_sym end |
.enter_method_scope!(method_policy) ⇒ Object
48 49 50 51 52 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 48 def enter_method_scope! method_policy method_policy.scopes_to_enter.each do |scope| enter_scope!(scope) end end |
.exit_method_scope!(method_policy) ⇒ Object
54 55 56 57 58 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 54 def exit_method_scope! method_policy method_policy.scopes_to_exit.each do |scope| exit_scope!(scope) end end |
.handle_return(propagated_ret, source_ret, ret) ⇒ Object?
Method to choose which replaced return from the post_patch to actually return
257 258 259 260 261 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 257 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 |
.instrument_with_alias(mod, methods, method_policy) ⇒ Boolean
Returns if patched, either by this invocation or a previous, or not.
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 293 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.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.
319 320 321 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 319 def instrument_with_prepend mod, method_policy contrast_prepend_method(mod, method_policy) end |
.register_c_hook(unbound_method) ⇒ Object
325 326 327 328 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 325 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).
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 340 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 = build_unbound_method_name(method_name).to_sym target_module = Module.cs__const_get(target_module_name) target_module = target_module.cs__singleton_class if %i[alias_singleton prepend].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 case impl when :alias_instance, :alias_singleton unless target_module.instance_methods(false).include? # alias_method may be private target_module.send(:alias_method, , method_name) # TODO: RUBY-1052 # rubocop:disable Kernel/DefineMethod target_module.send(:define_method, method_name, unbound_method.bind(target_module)) # rubocop:enable Kernel/DefineMethod end target_module.send(visibility, method_name) # e.g., module.private(:my_method) when :prepend prepending_module = Module.new # TODO: RUBY-1052 # rubocop:disable Kernel/DefineMethod prepending_module.send(:define_method, method_name, unbound_method.bind(target_module)) # rubocop:enable Kernel/DefineMethod prepending_module.send(visibility, method_name) # This prepends to the singleton class (it patches a class method) target_module.prepend prepending_module end # 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 != :[]= # logger.trace( # 'Registered C-defined patch', # implementation: impl, # module: target_module_name, # method: method_name, # visibility: visibility) # end .to_sym end |
.skip_assess_analysis? ⇒ Boolean
Skip if we should skip_contrast_analysis?, sampling says to ignore this request, or assess has been disabled.
411 412 413 414 415 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 411 def skip_assess_analysis? return true if skip_contrast_analysis? !ASSESS.enabled? end |
.skip_contrast_analysis? ⇒ Boolean
399 400 401 402 403 404 405 |
# File 'lib/contrast/agent/patching/policy/patch.rb', line 399 def skip_contrast_analysis? return true if in_contrast_scope? return true unless defined?(Contrast::Agent::REQUEST_TRACKER) return true unless Contrast::Agent::REQUEST_TRACKER.current&.analyze_request? false end |