lib/contrast/utils/class_util.rb in contrast-agent-4.11.0 vs lib/contrast/utils/class_util.rb in contrast-agent-4.12.0

- old
+ new

@@ -7,30 +7,33 @@ module Contrast module Utils # Utility methods for exploring the complete space of Objects class ClassUtil - @lru_cache = LRUCache.new + @lru_cache = LRUCache.new(300) + @string_cache = LRUCache.new(300) class << self - # 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. + # 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 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 - # return true if the given method is overwritten by one of the ancestors - # in the ancestor change that comes before the given module + # return true if the given method is overwritten by one of the ancestors in the ancestor change that comes + # before the given module + # + # @param mod [Module] the Module to check to see if it has had something prepended + # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] the policy that holds the method we + # need to check + # @return [Boolean] if this method specifically was prepended 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) @@ -41,53 +44,53 @@ return true if methods.include?(method_policy.method_name) end false end - # Return a String representing the object invoking this method in the - # form expected by our dataflow events. + # Return a String representing the object invoking this method in the form expected by our dataflow events. + # After implementing the LRU Cache, we firstly need to check if already had that object cached and if we have + # it - we can return it directly; otherwise we'll calculate and store the result before returning. # + # TODO: RUBY-1327 + # Once we move to 2.7+, we can combine the caches using ID b/c the memory location stops being the id + # # @param object [Object, nil] 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 - # After implementing the LRU Cache, we firstly need to check if already had that object cached - # and if we have it - we can return it directly - return @lru_cache[object.__id__] if @lru_cache.key? object.__id__ + # Only treat object like a string if it actually is a string+ some subclasses of String override string + # methods we depend on + if object.cs__class == String + return @string_cache[object] if @string_cache.key? object - # Only treat object like a string if it actually is a string+ - # some subclasses of String override string methods we depend on - @lru_cache[object.__id__] = if object.cs__class == String - cached = to_cached_string(object) - return cached if cached + @string_cache[object] = to_cached_string(object) || object.dup + else + return @lru_cache[object.__id__] if @lru_cache.key? object.__id__ - object.dup - elsif object.nil? - Contrast::Utils::ObjectShare::NIL_STRING - elsif object.cs__is_a?(Symbol) - ":#{ object }" - elsif object.cs__is_a?(Module) || object.cs__is_a?(Class) - "#{ object.cs__name }@#{ object.__id__ }" - elsif object.cs__is_a?(Regexp) - object.source - elsif use_to_s?(object) - object.to_s - else - "#{ object.cs__class.cs__name }@#{ object.__id__ }" - end + @lru_cache[object.__id__] = if object.nil? + Contrast::Utils::ObjectShare::NIL_STRING + elsif object.cs__is_a?(Symbol) + ":#{ object }" + elsif object.cs__is_a?(Module) || object.cs__is_a?(Class) + "#{ object.cs__name }@#{ object.__id__ }" + elsif object.cs__is_a?(Regexp) + object.source + elsif use_to_s?(object) + object.to_s + else + "#{ object.cs__class.cs__name }@#{ object.__id__ }" + end + end end - # 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. + # 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 + # TODO: RUBY-1326 + # 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 @@ -106,35 +109,39 @@ end private # Some objects have nice to_s that we can use to make them human readable. If they do, we should leverage them. - # We used to do this by default, but this opened us up to danger, so we're instead using an allow list approach. + # We used to do this by default, but this opened us up to danger, so we're instead using an allow list + # approach. # # @param object [Object] something that may have a safe to_s method # @return [Boolean] if we should invoke to_s to represent the object def use_to_s? object return true if object.cs__is_a?(Numeric) return true if defined?(Arel::Nodes::SqlLiteral) && object.cs__is_a?(Arel::Nodes::SqlLiteral) false end + # Find the target class based on the instance, or module, provided. If a module, return it. + # + # @param mod [Module] the Module, or instance of a Module, that we need to check + # @param is_instance [Boolean] is the object provided an instance of a class, requiring lookup by class + # @return [Module] def determine_target_class mod, is_instance return mod if mod.singleton_class? return mod.cs__singleton_class unless is_instance mod end - # If the String matches a common String in our ObjectShare, return that - # rather that for use as the representation of the String rather than - # forcing a duplication of the String. + # If the String matches a common String in our ObjectShare, return that rather that for use as the + # representation of the String rather than forcing a duplication of the String. # - # @param string [String] some string of which we want a Contrast - # representation. - # @return [String,nil] the ObjectShare version of the String or nil + # @param string [String] some string of which we want a Contrast representation. + # @return [String, nil] the ObjectShare version of the String or nil def to_cached_string string return Contrast::Utils::ObjectShare::EMPTY_STRING if string.empty? return Contrast::Utils::ObjectShare::SLASH if string == Contrast::Utils::ObjectShare::SLASH return Contrast::Utils::ObjectShare::EQUALS if string == Contrast::Utils::ObjectShare::EQUALS