# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: false cs__scoped_require 'ripper' cs__scoped_require 'contrast/core_extensions/module' cs__scoped_require 'contrast/components/interface' # This method is left purposefully at the top level namespace. Moving it # elsewhere will break functionality as it executes evaluations against the # namespace from which it is called -- ie putting it in Contrast would make all # changes it intends for Foo happen to Contrast::Foo instead # # @param class_name [String] the name of the class in which the eval will # redefine functionality # @param content [String] the String content that will function as the code in # the given class def unbound_eval class_name, content # Yuck, this is a top-level method that has to break encapsulation # in order to access scoping! Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.enter_contrast_scope eval(content) # rubocop:disable Security/Eval rescue Exception # rubocop:disable Lint/RescueException Contrast::Agent::SettingsState.log_error("Unable to perform unbound eval of new content for #{ class_name }.") ensure Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.exit_contrast_scope end module Contrast module Agent # Used for Ruby 2.4 & 2.5 to allow us to rewrite those methods which have # interpolation in them. # @deprecated Changes to this class are discouraged as this approach is # being phased out with support for those language versions. class ClassReopener END_NEW_LINE = "end\n".cs__freeze PROTECTED_WITH_NEW_LINE = "protected\n".cs__freeze PRIVATE_WITH_NEW_LINE = "private\n".cs__freeze CLASS_SELF_LINE = "class << self\n".cs__freeze attr_reader :public_singleton_methods, :class_module_path, :class_name, :files, :is_class, :name_space, :public_instance_methods, :protected_instance_methods, :private_instance_methods, :locations def initialize module_data @class_module_path = module_data.name clazz = module_data.mod @is_class = clazz.is_a?(Class) @public_instance_methods = [] @protected_instance_methods = [] @private_instance_methods = [] @public_singleton_methods = [] @files = {} @name_space = [] @class_name = nil @locations = Hash.new { |h, k| h[k] = [] } gather_modules end def gather_modules return if class_module_path.nil? segments = class_module_path.split(Contrast::Utils::ObjectShare::DOUBLE_COLON) @class_name = segments.last current = nil segments[0..-2].each do |chunk| defined = current ? current.cs__const_defined?(chunk) : Module.cs__const_defined?(chunk) next unless defined current = current ? current.cs__const_get(chunk) : Module.cs__const_get(chunk) if current.is_a?(Class) name_space << [chunk, Class] elsif current.is_a?(Module) name_space << [chunk, Module] end end end def staged_changes? public_singleton_methods.any? || public_instance_methods.any? || protected_instance_methods.any? || private_instance_methods.any? end def written_from_location? source_location return false unless source_location file = source_location[0] location = source_location[1] locs = locations[file] return true if locs.include?(location) locs << location false end def commit_patches return unless staged_changes? content = build_content valid = Ripper.sexp(content) unbound_eval(class_name, content) if !!valid && !class_name.empty? end def source_code location, method_name file_name = location[0] line_number = location[1] return unless file_name && line_number unless files.key?(file_name) return unless File.exist?(file_name) files[file_name] = File.readlines(file_name) end lines = files[file_name] old_verbose = $VERBOSE $VERBOSE = nil code = '' complete = false # location#line_number is 1 based, arrays are 0 based line_number -= 1 lines[line_number..-1].each do |line| begin code << line RubyVM::InstructionSequence.compile(code) # this will raise SyntaxError for malformed code # Assert that a line which ends with a , or \ is incomplete. complete = code !~ /[,\\]\s*\z/ break if complete rescue SyntaxError code.gsub(/\#\{.*?\}/, 'temp') end end $VERBOSE = old_verbose raise SyntaxError, "Failure: method #{ method_name } in #{ file_name } at #{ line_number }" unless complete code end private def build_content content = '' name_space.each do |arr| name = arr[0] type = arr[1] if type == Class content << "class #{ name }\n" elsif type == Module content << "module #{ name }\n" end end # Set the name to be a class or module content << if is_class "class #{ class_name }\n" else "module #{ class_name }\n" end # Recreate the updated class methods if public_singleton_methods.any? content << CLASS_SELF_LINE content << public_singleton_methods.join(Contrast::Utils::ObjectShare::NEW_LINE) content << Contrast::Utils::ObjectShare::NEW_LINE content << END_NEW_LINE end # Recreate the updated instance methods if public_instance_methods.any? content << public_instance_methods.join(Contrast::Utils::ObjectShare::NEW_LINE) content << Contrast::Utils::ObjectShare::NEW_LINE end if protected_instance_methods.any? content << PROTECTED_WITH_NEW_LINE content << protected_instance_methods.join(Contrast::Utils::ObjectShare::NEW_LINE) content << Contrast::Utils::ObjectShare::NEW_LINE end if private_instance_methods.any? content << PRIVATE_WITH_NEW_LINE content << private_instance_methods.join(Contrast::Utils::ObjectShare::NEW_LINE) content << Contrast::Utils::ObjectShare::NEW_LINE end content << END_NEW_LINE name_space.length.times do content << END_NEW_LINE end content end end end end