# 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