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

require 'contrast/components/logger'

module Contrast
  module Agent
    module Protect
      module Policy
        # This Module is the base of our Protect Applicators. It lays out the
        # form of the Applicator, which will override specific implementations
        # in order to properly invoke its Rule.
        module RuleApplicator
          include Contrast::Components::Logger::InstanceMethods

          # Calls the actual invocation for this applicator, if required. Will
          # attempt to transform the data as required prior to invocation and
          # provides a common interface for those rules that have the same
          # implementation regardless of the method patched.
          #
          # For those methods with different transformations depending on the
          # method instrumented, variations of this method, including an
          # indication of for which instrumented method they apply, will exist.
          #
          # @param method [Symbol] the name of the method for which this rule
          #   is invoked
          # @param exception [Exception] any exception raised; used for rules
          #   like Padding Oracle Attack (now defunct), which determine if the
          #   number and type of exceptions are an attack
          # @param properties [Hash] set of extra information provided by the
          #   applicator in an attempt to build a better story for the user
          # @param object [Object] the thing on which the triggering method was
          #   invoked
          # @param args [Array<Object>] the arguments passed to the triggering
          #   method at invocation
          # @raise [Contrast::SecurityException] on block, will pass the
          #   exception from the rule
          def apply_rule method, exception, properties, object, args
            invoke(method, exception, properties, object, args)
          rescue Contrast::SecurityException => e
            raise(e)
          rescue StandardError => e
            logger.error('Error applying protect rule', e, module: object.cs__class.cs__name, method: method,
                                                           rule: rule_name)
          end

          protected

          # Calls the actual rule for this applicator, if required. Most rules
          # invoke this from within their apply_rule method after doing
          # whatever transformations they need to get into this common format.
          #
          # @param _method [Symbol] the name of the method for which this rule
          #   is invoked
          # @param _exception [Exception] any exception raised; used for rules
          #   like Padding Oracle Attack (now defunct), which determine if the
          #   number and type of exceptions are an attack
          # @param _properties [Hash] set of extra information provided by the
          #   applicator in an attempt to build a better story for the user
          # @param _object [Object] the thing on which the triggering method
          #   was invoked
          # @param _args [Array<Object>] the arguments passed to the triggering
          #   method at invocation
          # @raise [Contrast::SecurityException] on block, will pass the
          #   exception from the rule
          def invoke _method, _exception, _properties, _object, _args
            raise(NoMethodError, 'This is abstract, override it.')
          end

          # The name of the rule, as expected by the Contrast Service and Contrast UI.
          #
          # @return [String]
          def rule_name
            raise(NoMethodError, 'This is abstract, override it.')
          end

          # The rule for which this applicator applies. It'll be a concrete
          # sub-class of Contrast::Agent::Protect::Rule::Base, found based on
          # the value of Contrast::Agent::Protect::Policy::RuleApplicator#name.
          #
          # @return [Contrast::Agent::Protect::Rule::Base]
          def rule
            ::Contrast::PROTECT.rule(rule_name)
          end

          # Should we skip analysis for this rule for this method invocation?
          # This allows us to short circuit in those cases for which the rule
          # will not apply.
          #
          # @return [Boolean]
          def skip_analysis?
            context = Contrast::Agent::REQUEST_TRACKER.current
            return true unless context&.app_loaded?
            return true unless rule&.enabled?

            false
          end
        end
      end
    end
  end
end