# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true module Contrast module Agent module Assess module Policy module Propagator # This class is specifically for String#tr(_s) propagation # # Disclaimer: there may be a better way, but we're in a 'get it work' state. hopefully, we'll be in a 'get it # right' state soon. module Trim class << self # @param policy_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this # propagation event. # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to # the invocation of the patched method. # @param ret [nil, String] the target to which to propagate. # @return [nil, String] ret def tr_tagger policy_node, preshift, ret, _block return ret unless ret && !ret.empty? return ret unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret)) properties.copy_from(preshift.object, ret) handle_tr(policy_node, preshift, ret, properties) event_data = Contrast::Agent::Assess::Events::EventData.new(policy_node, ret, preshift.object, ret, preshift.args) properties.build_event(event_data, 1) ret end def tr_s_tagger patcher, preshift, ret, _block return unless ret && !ret.empty? return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret)) source = preshift.object args = preshift.args properties.splat_from(source, ret) event_data = Contrast::Agent::Assess::Events::EventData.new patcher, ret, source, ret, args properties.build_event(event_data) ret end private # @param policy_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this # propagation event. # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to # the invocation of the patched method. # @param ret [String] the target to which to propagate. # @param properties [Contrast::Agent::Assess::Properties] the properties of the ret def handle_tr policy_node, preshift, ret, properties source = preshift.object replace_string = preshift.args[1] # if the replace string is empty, then there's a bunch of deletes. this functions the same as the # Removal propagation. if replace_string == Contrast::Utils::ObjectShare::EMPTY_STRING Contrast::Agent::Assess::Policy::Propagator::Remove.handle_removal(policy_node, source, ret) return end # Otherwise, we need to target each insertion point. Based on the spec for #tr & #tr_s, the find is # treated as a regexp range, excepting the `\` character, which we'll need to escape. This converts to # that form, wrapping the input in `[]`. find_string = preshift.args[0] find_string += '\\' if find_string.end_with?('\\') find_regexp = Regexp.new("[#{ find_string }]") # Find the first instance to be replaced. If there isn't one, than nothing changed here. idx = source.index(find_regexp) return unless idx # Iterate over each change and record where it happened. B/c this is a one to one replace, the index of # the replacement is always one; however, there may be adjacent replacements which become a single # range. start = idx stop = idx + 1 remove_ranges = [] while (idx = source.index(find_regexp, idx + 1)) # If the previous range ends at this index, we can expand that range to include this index. # Otherwise, we need to record the held range and start a new one. if stop != idx remove_ranges << (start...stop) start = idx end stop = idx + 1 end # Be sure to capture the last range in the holder. remove_ranges << (start...stop) properties.delete_tags_at_ranges(remove_ranges, shift: false) end end end end end end end end