# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/assess/tracker' require 'contrast/components/interface' require 'contrast/utils/duck_utils' module Contrast module Utils module Assess # TrackingUtil has methods for determining if a object is being tracked class TrackingUtil include Contrast::Components::Interface access_component :logging class << self # Public interface to our tracking check, isolating the internals # required for recursion. # # @param obj [Object] the thing to check if tracked # @return [Boolean] if the obj, or something in it if a collection, is # tracked. def tracked? obj _tracked?(obj, 0) end # Public interface to our tracking check, isolating the internals # required for recursion. # # @param obj [Object] the thing to check if tracked # @return [Boolean] if the obj, or something in it if a collection, is # tracked. def trackable? obj _trackable?(obj, 0) end private # Sometimes things are nested inside of each other, such as an Array # holding a Hash, holding that Array. In those cases, rather than # entering an infinite loop, we'll break out. # Right now, that level of nesting has been arbitrarily set to 10. # # @param obj [Object] the thing to check if tracked # @param idx [Integer] the number of levels nested we've gone # @return [Boolean] if the obj, or something in it if a collection, is # tracked. def _tracked? obj, idx return false if obj.nil? return false if idx > 10 idx += 1 if Contrast::Utils::DuckUtils.iterable_hash?(obj) obj.each_pair do |k, v| return true if _tracked?(k, idx) return true if _tracked?(v, idx) end false elsif Contrast::Utils::DuckUtils.iterable_enumerable?(obj) obj.any? do |ele| _tracked?(ele, idx) unless obj == ele end else Contrast::Agent::Assess::Tracker.tracked?(obj) end rescue StandardError => e # This is used to ask if a ton of objects are tracked. They may not # all be iterable. Bad things could happen in some cases, like when # checking a closed statement for SQL injection trigger events logger.warn('Failed to determine tracking', e, module: obj.cs__class) false end # Sometimes things are nested inside of each other, such as an Array # holding a Hash, holding that Array. In those cases, rather than # entering an infinite loop, we'll break out. # Right now, that level of nesting has been arbitrarily set to 10. # # @param obj [Object] the thing to check if trackable # @param idx [Integer] the number of levels nested we've gone # @return [Boolean] if the obj, or something in it if a collection, is # trackable. def _trackable? obj, idx return false if obj.nil? return false if idx > 10 idx += 1 if Contrast::Utils::DuckUtils.iterable_hash?(obj) obj.each_pair do |k, v| return true if _trackable?(k, idx) return true if _trackable?(v, idx) end false elsif Contrast::Utils::DuckUtils.iterable_enumerable?(obj) obj.any? do |ele| _trackable?(ele, idx) unless obj == ele end else Contrast::Agent::Assess::Tracker.trackable?(obj) end rescue StandardError => e # This is used to ask if a ton of objects are tracked. They may not # all be iterable. Bad things could happen in some cases, like when # checking a closed statement for SQL injection trigger events logger.warn('Failed to determine trackable', e, module: obj.cs__class) false end end end end end end