# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/logger' require 'contrast/utils/duck_utils' require 'contrast/agent/assess/events/event_data' module Contrast module Agent module Assess module Policy module Propagator # This module is specifically for String#(g)sub 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 SubstitutionUtils CAPTURE_GROUP_REGEXP = /\\[[:digit:]]/.cs__freeze CAPTURE_NAME_REGEXP = /\\k<[[:alpha:]]/.cs__freeze private # Update our properties of the object dependent on the substitution pattern used. # @param patcher [Contrast::Agent::Assess::Policy::PolicyNode] # the node that governs this event. # @param preshift [Contrast::Agent::Assess::PreShift] instance of PreShift # @param ret [String] the result of the substitution # @param block, the given block # @param global [Boolean] if this was a global or single substitution, optional def substitution_tagger patcher, preshift, ret, block, global: true return ret unless ret begin # define here what is used across private methods @_preshift = preshift args = @_preshift.args[1] @_self_tracked = Contrast::Agent::Assess::Tracker.tracked?(source) @_incoming_tracked = args && determine_tracked(args) @_ret = ret return @_ret unless @_self_tracked || @_incoming_tracked @_parent_events = [] if block block_sub elsif args.is_a?(String) string_sub(args, global) elsif args.is_a?(Hash) hash_sub else # Enumerator, only for gsub pattern_gsub end self_tracked_handle(@_parent_events) string_build_event(patcher) rescue StandardError => e logger.error('Unable to apply gsub propagator', e) end ret end def self_tracked_handle parent_events return unless @_self_tracked source_properties = Contrast::Agent::Assess::Tracker.properties(source) parent_event = source_properties&.event parent_events.prepend(parent_event) if parent_event end def source @_preshift.object end # @param args [String] the new String going into the substitution def determine_tracked args # if there's no arg, it can't be tracked return false unless args # if it's a string, just ask if it's tracked case args when String Contrast::Agent::Assess::Tracker.tracked?(args) # if it's a hash, ask if it has a tracked string when Hash args.values.any? { |value| value.is_a?(String) && Contrast::Agent::Assess::Tracker.tracked?(value) } # this should never happen else false end end # @param incoming [String] the new String going into the substitution # @param global [Boolean] if this was a global or single substitution def string_sub incoming, global return unless (properties = Contrast::Agent::Assess::Tracker.properties!(@_ret)) incoming_properties = Contrast::Agent::Assess::Tracker.properties(incoming) @_parent_events << incoming_properties&.event if incoming_properties&.event # We can't efficiently find the places that things were # copied from regexp / captures. Trading accuracy for # performance if incoming.match?(CAPTURE_GROUP_REGEXP) || incoming.match?(CAPTURE_NAME_REGEXP) properties.splat_from(source, @_ret) if @_self_tracked return end # if it's just a straight insert, that we can do # Copy the tags from us to the return ranges = find_string_sub_insert(properties, incoming, global) properties.delete_tags_at_ranges(ranges) properties.shift_tags(ranges) return unless @_incoming_tracked return unless incoming_properties tags = incoming_properties.tag_keys ranges.each do |range| tags.each do |tag| properties.add_tag(tag, range) end end end # Find the points at which the new String was placed into the original # # @param properties [Contrast::Agent::Assess::Properties] the Properties of the ret # @param incoming [String] the new String going into the substitution # @param global [Boolean] if this was a global or single substitution # @return [Array] the Ranges where substitution occurred def find_string_sub_insert properties, incoming, global pattern = @_preshift.args[0] properties.copy_from(source, @_ret) # Figure out where inserts occurred last_idx = 0 ranges = [] # For each insert, move the tag ranges while last_idx idx = source.index(pattern, last_idx) break unless idx last_idx = idx ? idx + 1 : nil start_index = idx end_index = idx + incoming.length ranges << (start_index...end_index) break unless global end ranges end def block_sub hash_sub end def hash_sub return unless @_self_tracked properties = Contrast::Agent::Assess::Tracker.properties!(@_ret) properties&.splat_from(source, @_ret) end def pattern_gsub return unless (source_properties = Contrast::Agent::Assess::Tracker.properties(source)) return unless (properties = Contrast::Agent::Assess::Tracker.properties!(@_ret)) source_properties.tag_keys.each do |key| properties.add_tag(key, 0...1) end parent_event = source_properties.event @_parent_events << parent_event if parent_event end # @param patcher [Contrast::Agent::Assess::Policy::PolicyNode] # the node that governs this event. def string_build_event patcher return unless Contrast::Agent::Assess::Tracker.tracked?(@_ret) properties = Contrast::Agent::Assess::Tracker.properties(@_ret) args = @_preshift.args event_data = Contrast::Agent::Assess::Events::EventData.new(patcher, @_ret, @_preshift, @_ret, args) properties.build_event(event_data, 2) properties.event.instance_variable_set(:@_parent_events, @_parent_events) end end end end end end end