# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true module Contrast module Agent module Assess # A Tag represents a range in a given piece of data. It is used by the # Agent to determine if a vulnerable dataflow has occurred. class Tag attr_reader :label, # the label of this tag :length, # length of tagged text within string :start_idx, # start of range :end_idx # end of range (calculated from start + length), exclusive # Initialize a new tag # # @param label [String] the label of the tag # @param length [Integer] the length of the string described with this # tag # @param start_idx [Integer] (0) the starting position in the string for # this tag def initialize label, length, start_idx = 0 @label = label update_range(start_idx, start_idx + length) end # Return true if the tag covers the given position in the string # # @param idx [Integer] the index to check # @return [Boolean] def covers? idx idx >= start_idx && idx < end_idx end # Return true if the tag is above the given position in the string # @param idx [Integer] the index to check # @return [Boolean] def above? idx idx < start_idx end # Return the range that this tag covers, from start (inclusive) to # end (exclusive). # # @return [Range] def range start_idx...end_idx end def extends_beyond_string_size? string_length @end_idx > string_length end # Return if a given tag overlaps this one def overlaps? start_idx, end_idx return true if @start_idx < start_idx && @end_idx >= start_idx # we start below range & end in it return true if @start_idx >= start_idx && @end_idx <= end_idx # we start and end in range @start_idx <= end_idx && @end_idx > end_idx # we start in range & end above it end def shift idx update_range(@start_idx + idx, @end_idx + idx) end def shift_end idx update_range(@start_idx, @end_idx + idx) end def update_start start_idx update_range(start_idx, @end_idx) end def update_end end_idx update_range(@start_idx, end_idx) end def repurpose start_idx, end_idx update_range(start_idx, end_idx) end # Given a tag, merge its ranges with this one # such that the lowest start and highest end # become the values of this tag # # Returns true if the other tag was merged into # this tag def merge other return unless overlaps?(other.start_idx, other.end_idx) start = other.start_idx < @start_idx ? other.start_idx : @start_idx finish = other.end_idx > @end_idx ? other.end_idx : @end_idx update_range(start, finish) end # Modification to tracked String can change the position and length of the tracked tag # shift : negative value moves left def copy_modified shift start = start_idx + shift # Tags cannot start below 0 new_start_idx = start >= 0 ? start : 0 # If a tag were to go negative, cut off the negative portion from length new_length = start >= 0 ? length : (length + start) Contrast::Agent::Assess::Tag.new(label, new_length, new_start_idx) end def str_val @_str_val ||= "[#{ start_idx },#{ end_idx }]" end alias_method :to_s, :str_val BELOW = 'BELOW' LOW_SPAN = 'LOW_SPAN' WITHIN = 'WITHIN' WITHOUT = 'WITHOUT' HIGH_SPAN = 'HIGH_SPAN' ABOVE = 'ABOVE' # The tag is ______ the range # rrrrrrr == self.range, the range of the tag def compare_range start, stop # the range starts below the given values if @start_idx < start # r starts and stops below # rrrrrrrrrrrrr # start stop return BELOW if @end_idx <= start # r starts below and finishes within # rrrrrrrrrrrrr # start stop return LOW_SPAN if @end_idx > start && @end_idx <= stop # r starts below and finishes above stop # rrrrrrrrrrrrrrrrrrrrrrrr # start stop return WITHOUT if @end_idx > stop end # the range starts at or above the given values # r is between start and stop # rrrrrrrrrrrrrrr # start stop return WITHIN if @start_idx < stop && @end_idx <= stop # r starts within and finishes above stop # rrrrrrrrrrrrr # start stop return HIGH_SPAN if @start_idx < stop && @end_idx > stop # the range is above the given values # starts and stops above # rrrrrrrrrrrrr # start stop ABOVE end private # Update range should be how start and end index of tags are changed, # as it includes validation # # Note that we allow start_idx == end_idx b/c this is how we determine # if a tag range is 'covered' in trigger detection ERROR_NEGATIVE_START = 'Unable to set start idx negative' ERROR_END_BEFORE_START = 'Unable to set start idx after end idx' def update_range start_idx, end_idx raise(ArgumentError, ERROR_NEGATIVE_START) if start_idx.negative? raise(ArgumentError, ERROR_END_BEFORE_START) if end_idx < start_idx @start_idx = start_idx @end_idx = end_idx @length = @end_idx - @start_idx @_str_val = nil end end end end end