# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/utils/assess/tracking_util' require 'contrast/utils/class_util' require 'contrast/utils/duck_utils' require 'contrast/utils/object_share' require 'contrast/utils/prevent_serialization' require 'contrast/utils/stack_trace_utils' require 'contrast/utils/string_utils' require 'contrast/utils/timer' module Contrast module Agent module Assess # This class holds the data about an event in the application # We'll use it to build an event that TeamServer can consume if # the object to which this event belongs ends in a trigger. # # @attr_reader event_id [Integer] the atomic id of this event # @attr_reader policy_node [Contrast::Agent::Assess::Policy::PolicyNode] # the node that governs this event. # @attr_reader stack_trace [Array<String>] the execution stack at the # time the method for this event was invoked # @attr_reader time [Integer] the time, in epoch ms, when this event was # created # @attr_reader thread [Integer] the object id of the thread on which this # event was generated # @attr_reader object [String] the safe representation of the Object on # which the method was invoked # @attr_reader ret [String] the safe representation of the Return of the # invoked method # @attr_reader args [Array<Object>] the safe representation of the # Arguments with which the method was invoked class ContrastEvent include Contrast::Utils::PreventSerialization class << self # Given an array of arguments, copy them into a safe, meaning String, # format that we can use to send to SR and TS for rendering. # # @param args [Array<Object>] the arguments to translate # @return [Array<String>] the String forms of those Objects, as # determined by Contrast::Utils::ClassUtil.to_contrast_string def safe_args_representation args return nil unless args return Contrast::Utils::ObjectShare::EMPTY_ARRAY if args.empty? rep = [] args.each do |arg| # We have to handle named args rep << if arg.is_a?(Hash) safe_arg_hash_representation(arg) else Contrast::Utils::ClassUtil.to_contrast_string(arg) end end rep end # if given an object that can be duped, duplicate it. otherwise just # return the original object. swallow all exceptions from # non-duplicable things. # # we can't just check respond_to? though b/c dup exists on the # base Object class # # @param original [Object, nil] the thing to duplicate # @return [Object, nil] a copy of that thing def safe_dup original return nil unless original Contrast::Agent::Assess::Tracker.duplicate(original) end private def safe_arg_hash_representation hash # since this is the named hash for arguments, only the value is # suspect here hash.transform_values { |v| Contrast::Utils::ClassUtil.to_contrast_string(v) } end end attr_reader :event_id, :policy_node, :stack_trace, :time, :thread, :object, :ret, :args # We need this to track the parent id's of events to build up a flow # chart of the finding @atomic_id = 0 @atomic_mutex = Mutex.new def self.next_atomic_id @atomic_mutex.synchronize do @atomic_id += 1 # Rollover things rescue StandardError @atomic_id = 1 end end # @param policy_node [Contrast::Agent::Assess::Policy::PolicyNode] # the node that governs this event. # @param tagged [Object] the Target to which this event pertains. # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param args [Array<Object>] the Arguments with which the method # was invoked def initialize policy_node, tagged, object, ret, args @policy_node = policy_node # so long as this event is built in a factory, we know Contrast Code # will be the first three events @stack_trace = caller(3, 20) @time = Contrast::Utils::Timer.now_ms @thread = Thread.current.object_id # These methods rely on the above being set. Don't move them! @event_id = Contrast::Agent::Assess::ContrastEvent.next_atomic_id find_parent_events!(policy_node, object, ret, args) snapshot!(tagged, object, ret, args) end def parent_events @_parent_events ||= [] end # We have to do a little work to figure out what our TS appropriate # target is. To break this down, the logic is as follows: # 1) If my policy_node has a target, work on targets. Else, work on sources. # Per TS law, each policy_node must have at least a source or a target. # The only type of policy_node w/o targets is a Trigger, but that may # change. # 2) If I have a highlight, it means that I have a P target that is # not in integer form (it was a named / keyword type for which I had # to find the index). I need to address this so that TS can process # it. # 3) I'll set the event's source and target to TS values. # 4) Return the highlight or the first source/target as the taint # target. def determine_taint_target event_dtm if @policy_node&.targets&.any? event_dtm.source = @policy_node.source_string if @policy_node.source_string event_dtm.target = @highlight ? "P#{ @highlight }" : @policy_node.target_string @highlight || @policy_node.targets[0] elsif policy_node&.sources&.any? event_dtm.source = @highlight ? "P#{ @highlight }" : @policy_node.source_string event_dtm.target = @policy_node.target_string if @policy_node.target_string @highlight || @policy_node.sources[0] end end # Convert this event into a DTM that TeamServer can consume def to_dtm_event Contrast::Api::Dtm::TraceEvent.build(self) end private # Parent events are the events of all the sources of this event which # were tracked prior to this event occurring. Depending on which, if # any of the sources were tracked, there may be more than one parent. # # All events except for [Contrast::Agent::Assess::Events::SourceEvent] # will have at least one parent. # # We set those events to this event's instance variables. # # @param policy_node [Contrast::Agent::Assess::Policy::PolicyNode] # the node that governs this event. # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param args [Array<Object>] the Arguments with which the method # was invoked def find_parent_events! policy_node, object, ret, args policy_node.sources.each do |source_marker| source = value_of_source(source_marker, object, ret, args) next unless source event = Contrast::Agent::Assess::Tracker.properties(source)&.event parent_events << event if event end end # @param source [String] the marker for the source type # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param args [Array<Object>] the Arguments with which the method # was invoked # @return [Object,nil] the literal value of the source indicated by the # given marker def value_of_source source, object, ret, args case source when Contrast::Utils::ObjectShare::OBJECT_KEY object when Contrast::Utils::ObjectShare::RETURN_KEY ret else if source.is_a?(Integer) args[source] else args.each do |search| next unless search.is_a?(Hash) s = search[source] return s if s end end end end # Everything* is mutable in Ruby. As such, to ensure we can accurately # report the application state at the time of this method's invocation, # we have to snapshot the given values, making safe representations of # them for our later use. We set those safe values to this event's # instance variables. # # @param tagged [Object] the Target to which this event pertains. # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param args [Array<Object>] the Arguments with which the method # was invoked def snapshot! tagged, object, ret, args target = @policy_node.target case target # If the target is nil, this rule was violated simply by a method # being called. We'll save all the information, but nothing will be # marked up, as nothing need be tracked when nil @object = Contrast::Utils::ClassUtil.to_contrast_string(object) @args = cs__class.safe_args_representation(args) @ret = Contrast::Utils::ClassUtil.to_contrast_string(ret) # If the target is O, then we dup the O and safely represent the rest when Contrast::Utils::ObjectShare::OBJECT_KEY @object = cs__class.safe_dup(tagged) @args = cs__class.safe_args_representation(args) @ret = Contrast::Utils::ClassUtil.to_contrast_string(ret) # If the target is R, then we dup the R and safely represent the rest when Contrast::Utils::ObjectShare::RETURN_KEY @object = Contrast::Utils::ClassUtil.to_contrast_string(object) @args = cs__class.safe_args_representation(args) @ret = cs__class.safe_dup(tagged) # If the target is P*, then we need to dup things a differently. We # need to find the true target inside so that we can mark it up # later, but the other args should be represented as their safe form. else @object = Contrast::Utils::ClassUtil.to_contrast_string(object) @args = cs__class.safe_args_representation(args) @ret = Contrast::Utils::ClassUtil.to_contrast_string(ret) save_target_arg(target, tagged) end end # I know we're creating an extra string here since we replace the safe # one w/ a dup, but good enough for now. Trying not to make this too # complicated. - HM 8/8/19 # # @param target [String,Integer] the marker for the target index # @param tagged [Object] the actual Object that we're acting on which # has tags def save_target_arg target, tagged return if @args.cs__frozen? if target.is_a?(Integer) @args[target] = cs__class.safe_dup(tagged) return end @args.each_with_index do |search, index| next unless search.is_a?(Hash) next unless search[target] search[target] = cs__class.safe_dup(tagged) @highlight = index break end end end end end end