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

require 'contrast/utils/assess/event_limit_utils'

module Contrast
  module Utils
    module Assess
      # This module will include all methods for some internal validations/appliers in the TriggerMethod module
      # and some other module methods from the same place, so we can ease the main module
      module TriggerMethodUtils
        extend Contrast::Utils::Assess::EventLimitUtils
        # A request is reportable if it is not from ActionController::Live
        #
        # @param env [Hash] the env of the Request
        # @return [Boolean]
        def reportable? env
          !(defined?(ActionController::Live) &&
            env &&
            env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live))
        end

        # Find the request for this finding. This assumes, for now, that if there is an active request, then that
        # is the request to report. Otherwise, we'll use the first request found in the events of the
        # source_object if the non request tracking flag is set.
        #
        # @param source [Object,nil] some Object used as the source of a trigger event
        # @return [Contrast::Agent::Request,nil] the request from which the dataflow on the request originated.
        def find_request source
          return Contrast::Agent::REQUEST_TRACKER.current.request if Contrast::Agent::REQUEST_TRACKER.current
          return unless ::Contrast::ASSESS.non_request_tracking?
          return unless (properties = Contrast::Agent::Assess::Tracker.properties(source))

          find_event_request(properties.event)
        end

        # Finds the first request along the left most tree of parent events
        #
        # @param event [Contrast::Agent::Reporting::FindingEvent]
        # @return [Contrast::Agent::Request, nil]
        def find_event_request event
          return event.request if event&.source_type

          idx = 0
          while idx <= event.parent_events.length
            found = find_event_request(event.parent_events[idx])
            return found if found

            idx += 1
          end

          event.request
        end

        # ===== APPLIERS =====
        # This is our method that actually checks the taint on the object our trigger_node targets.
        #
        # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
        #   trigger event
        # @param source [Object] the source of the Trigger 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 apply_trigger trigger_node, source, object, ret, *args
          return unless trigger_node
          return if trigger_node.rule_disabled?
          return if trigger_node.dataflow? && source.nil?

          if trigger_node.regexp_rule?
            apply_regexp_rule(trigger_node, source, object, ret, *args)
          elsif trigger_node.custom_trigger?
            trigger_node.apply_custom_trigger(trigger_node, source, object, ret, *args)
          elsif trigger_node.dataflow?
            apply_dataflow_rule(trigger_node, source, object, ret, *args)
          else # trigger rule - just calling the method is dangerous
            finding = build_finding(trigger_node, source, object, ret, *args)
            Contrast::Agent::Assess::Policy::TriggerMethod.report_finding(finding) if finding
          end
        rescue StandardError => e
          logger.warn('Unable to apply trigger', e, node_id: trigger_node.id)
        end

        # This is our method that actually checks the taint on the object our trigger_node targets for our Regexp
        # based rules.
        #
        # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this trigger
        #   event
        # @param source [Object] the source of the Trigger 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 apply_regexp_rule trigger_node, source, object, ret, *args
          return unless source.is_a?(String)
          return if trigger_node.good_value && source.match?(trigger_node.good_value)
          return if trigger_node.bad_value && source !~ trigger_node.bad_value

          finding = build_finding(trigger_node, source, object, ret, *args)
          Contrast::Agent::Assess::Policy::TriggerMethod.report_finding(finding) if finding
        end

        # This is our method that actually checks the taint on the object our trigger_node targets for our Dataflow
        # based rules.
        #
        # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
        #   trigger event
        # @param source [Object] the source of the Trigger 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 apply_dataflow_rule trigger_node, source, object, ret, *args
          return unless source && Contrast::Agent::Assess::Tracker.tracked?(source)

          if Contrast::Agent::Assess::Tracker.trackable?(source)
            return unless trigger_node.violated?(source)

            finding = build_finding(trigger_node, source, object, ret, *args)
            report_finding(finding) if finding
          elsif Contrast::Utils::DuckUtils.iterable_hash?(source)
            source.each_pair do |key, value|
              apply_dataflow_rule(trigger_node, key, object, ret, *args)
              apply_dataflow_rule(trigger_node, value, object, ret, *args)
            end
          elsif Contrast::Utils::DuckUtils.iterable_enumerable?(source)
            source.each do |value|
              apply_dataflow_rule(trigger_node, value, object, ret, *args)
            end
          end
        end
      end
    end
  end
end