# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

require 'contrast/agent/patching/policy/method_policy'

module Contrast
  module Agent
    module Assess
      module Policy
        # This class is used to create dynamic source nodes & source nodes from a db model that receives untrusted data
        class DynamicSourceFactory
          DB_SOURCE_TYPE = 'TAINTED_DATABASE'
          WRITE_QUERY_TIME = 'writeDateTimeUtc'
          WRITE_QUERY_URL = 'writeRequestUrl'
          READ_TABLE = 'readTable'
          READ_COLUMN = 'readColumn'
          class << self
            # Given a Class representing a table in a Database and a map of
            # methods representing columns, generate sources for each method
            # such that calls to that method will result in a Source Event.
            #
            # @param klass [Class] the Class to taint
            # @param tainted_columns [Hash{String => Contrast::Agent::Assess::Properties}]
            #   the name of the method to taint, mapped to the properties it
            #   should apply
            def create_sources klass, tainted_columns
              class_name = klass.cs__name
              instance_methods = klass.instance_methods
              instance_methods.concat(klass.private_instance_methods)
              current_context = Contrast::Agent::REQUEST_TRACKER.current
              current_request = current_context&.request

              tainted_columns.each_pair do |field, properties|
                next unless properties

                method_name = field.to_sym
                # Move on if we already know about this Dynamic Source
                next if Contrast::Agent::Assess::Policy::Policy.instance.find_source_node(class_name, method_name, true)

                dynamic_source_node = create_source_node(class_name, method_name, Set.new(properties.tag_keys),
                                                         current_request)
                Contrast::Agent::Assess::Policy::Policy.instance.add_node(dynamic_source_node, :dynamic_source)
                method_policy = build_source_policy(method_name, dynamic_source_node)
                Contrast::Agent::Patching::Policy::Patcher.patch_method(klass, instance_methods, method_policy)
                next unless current_context

                dynamic_source = create_dynamic_source(current_request, dynamic_source_node, field, properties)
                node_id = Contrast::Utils::StringUtils.force_utf8(dynamic_source_node.id)
                current_context.activity.dynamic_sources[node_id] = dynamic_source
              end
            end

            private

            # Given a class and method, create a SourceNode for them that will
            # add the given tags as well as properties required to construct a
            # finding with a TAINTED_DATABASE source
            #
            # @param class_name [String] the name of the Class to patch
            # @param method_name [Symbol] the name of the Method to patch
            # @param tags [Set<String>] the tags this event should apply
            # @param request [Contrast::Agent::Request] the request during
            #  which this source is to be created.
            # @return [Contrast::Agent::Assess::Policy::SourceNode]
            def create_source_node class_name, method_name, tags, request
              node = Contrast::Agent::Assess::Policy::SourceNode.new
              node.class_name = class_name
              node.instance_method = true
              node.method_name = method_name
              node.method_visibility = :public
              node.target_string = Contrast::Utils::ObjectShare::RETURN_KEY
              node.type = DB_SOURCE_TYPE
              node.tags = tags
              node.tags << 'DATABASE_WRITE'
              node.add_property('dynamic_source_id', node.id)
              node.add_property('dynamic_source_name', "#{ class_name }.#{ method_name }")
              node.add_property(READ_TABLE, class_name)
              node.add_property(READ_COLUMN, method_name)
              node.add_property(WRITE_QUERY_TIME, Contrast::Utils::Timer.now_ms)
              node.add_property(WRITE_QUERY_URL, request&.normalized_uri)
              node
            end

            # @param method_name [Symbol] the name of the Method to which this
            #   method applies
            # @param dynamic_source_node [Contrast::Api::Dtm::DynamicSource]
            #   the SourceNode that applies to this method
            # @return [Contrast::Agent::Patching::Policy::MethodPolicy] the
            #   policy to apply to the given method
            def build_source_policy method_name, dynamic_source_node
              method_policy = Contrast::Agent::Patching::Policy::MethodPolicy.new
              method_policy.method_visibility = :public
              method_policy.instance_method = true
              method_policy.method_name = method_name
              method_policy.source_node = dynamic_source_node
              method_policy
            end

            # @param request [Contrast::Agent::Request] the request during
            #   which this source is to be created.
            # @param source_node [Contrast::Agent::Assess::Policy::SourceNode]
            #   the SourceNode that applies to this method
            # @param field [String] the name of the method to which this source
            #   applies
            # @param properties [Contrast::Agent::Assess::Properties] the
            #   properties which this source should relate to the data tracked
            #   as a result of this Source Method
            # @return [Contrast::Api::Dtm::DynamicSource] the message to send
            #   to the Service to allow it to report the events leading up to
            #   the creation of the Dynamic Source
            def create_dynamic_source request, source_node, field, properties
              dynamic_source = Contrast::Api::Dtm::DynamicSource.new
              dynamic_source.class_name = Contrast::Utils::StringUtils.force_utf8(source_node.class_name)
              dynamic_source.method_name = Contrast::Utils::StringUtils.force_utf8(field)
              dynamic_source.instance_method = source_node.instance_method?
              dynamic_source.target = Contrast::Utils::StringUtils.force_utf8(source_node.target_string)
              append_properties!(dynamic_source, request, source_node, field)
              append_events!(dynamic_source, properties.event)
              dynamic_source
            end

            # Append the properties needed to reconstruct the given DynamicSource in other dataflows and for rendering
            # in TeamServer
            #
            # @param dynamic_source [Contrast::Api::Dtm::DynamicSource] the message to send to the Service to allow it
            #   to report the events leading up to the creation of the Dynamic Source.
            # @param request [Contrast::Agent::Request] the request during which this source is to be created.
            # @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the SourceNode that applies to this
            #   method.
            # @param field [String] the name of the method to which this source applies.
            def append_properties! dynamic_source, request, source_node, field
              dynamic_source.properties[READ_TABLE] = Contrast::Utils::StringUtils.force_utf8(source_node.class_name)
              dynamic_source.properties[READ_COLUMN] = Contrast::Utils::StringUtils.force_utf8(field)
              dynamic_source.properties[WRITE_QUERY_TIME] =
                Contrast::Utils::StringUtils.force_utf8(Contrast::Utils::Timer.now_ms)
              dynamic_source.properties[WRITE_QUERY_URL] =
                Contrast::Utils::StringUtils.force_utf8(request&.normalized_uri)
            end

            def append_events! dynamic_source, event
              return unless event

              event.parent_events&.each do |parent_event|
                append_events(dynamic_source, parent_event)
              end
              dynamic_source.events << event.to_dtm_event
            end
          end
        end
      end
    end
  end
end