lib/contrast/utils/class_util.rb in contrast-agent-3.8.5 vs lib/contrast/utils/class_util.rb in contrast-agent-3.9.0

- old
+ new

@@ -1,58 +1,113 @@ # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true -cs__scoped_require 'contrast/core_extensions/object' +cs__scoped_require 'contrast/extensions/ruby_core/module' module Contrast module Utils - # Utility methods for exploring the complete space of objects + # Utility methods for exploring the complete space of Objects class ClassUtil - def self.ancestors_of clazz - arr = [] - ObjectSpace.each_object(Module) do |obj| - next unless obj <= clazz + class << self + # Given a module, return all of its descendants + # + # @param mod [Module] the module whose descendants you want to find. + # @return [Array<Module>] those Modules that inherit from the given. + def descendants mod + ObjectSpace.each_object(mod).to_a + end - arr << obj + # some classes have had things prepended to them, like Marshal in Rails + # 5 and higher. Their ActiveSupport::MarshalWithAutoloading will break + # our alias patching approach, as will any other prepend on something + # that we touch. Prepend and Alias are inherently incompatible monkey + # patching approaches. As such, we need to know if something has been + # prepended to. + # + # @param mod [Module] the Module to check to see if it has had something + # prepended + # @param ancestors [Array<Module>] the array of ancestors for the mod + # @return [Boolean] if the mod has been prepended or not + def prepended? mod, ancestors = nil + ancestors ||= mod.ancestors + ancestors[0] != mod end - arr - end - # some classes have had things prepended to them, like Marshal in Rails - # 5 and higher. Their ActiveSupport::MarshalWithAutoloading will break - # our alias patching approach, as will any other prepend on something - # that we touch. Prepend and Alias are inherently incompatible monkey - # patching approaches. As such, we need to know if something has been - # prepended to. - # - # return true if the given module is not its first ancestor - def self.prepended? mod, ancestors = nil - ancestors ||= mod.ancestors - ancestors[0] != mod - end + # return true if the given method is overwritten by one of the ancestors + # in the ancestor change that comes before the given module + def prepended_method? mod, method_policy + target_module = determine_target_class mod, method_policy.instance_method + ancestors = target_module.ancestors + return false unless prepended?(target_module, ancestors) - def self.determine_target_class mod, is_instance - return mod if mod.singleton_class? + ancestors.each do |ancestor| + break if ancestor == target_module - return mod.cs__singleton_class unless is_instance + methods = ancestor.instance_methods(false) + return true if methods.include?(method_policy.method_name) + end + false + end - mod - end + # Return a String representing the object invoking this method in the + # form expected by our dataflow events. + # + # @param object [Object] the entity to convert to a String + # @return [String] the human readable form of the String, as defined by + # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/capture-snapshot.md + def to_contrast_string object + if object.cs__is_a?(String) + return Contrast::Utils::ObjectShare::EMPTY_STRING if object.empty? - # return true if the given method is overwritten by one of the ancestors - # in the ancestor change that comes before the given module - def self.prepended_method? mod, method_policy - target_module = determine_target_class mod, method_policy.instance_method - ancestors = target_module.ancestors - return false unless prepended?(target_module, ancestors) + object.dup + elsif object.cs__is_a?(Symbol) + ":#{ object }" + elsif object.cs__is_a?(Numeric) + object.to_s + elsif object.cs__is_a?(Module) || object.cs__is_a?(Class) + "#{ object.cs__name }@#{ object.__id__ }" + else + "#{ object.cs__class.cs__name }@#{ object.__id__ }" + end + end - ancestors.each do |ancestor| - break if ancestor == target_module + # The method const_defined? can cause autoload, which is bad for us. + # The method autoload? doesn't traverse namespaces. This method lets us + # provide a constant, as a String, and parse it to determine if it has + # been truly truly defined, meaning it existed before this method was + # invoked, not as a result of it. + # + # This is required to handle a bug in Ruby prior to 2.7.0. When we drop + # support for 2.6.X, we should remove this code. + # https://bugs.ruby-lang.org/issues/10741 + # @param name [String] the name of the constant to look up + # @return [Boolean] + def truly_defined? name + return false unless name - methods = ancestor.instance_methods(false) - return true if methods.include?(method_policy.method_name) + segments = name.split(Contrast::Utils::ObjectShare::DOUBLE_COLON) + previous_module = Module + segments.each do |segment| + return false if previous_module.cs__autoload?(segment) + return false unless previous_module.cs__const_defined?(segment) + + previous_module = previous_module.cs__const_get(segment) + end + + true + rescue NameError # account for nonsense / poorly formatted constants + false end - false + + private + + def determine_target_class mod, is_instance + return mod if mod.singleton_class? + + return mod.cs__singleton_class unless is_instance + + mod + end end end end end