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

module Contrast
  module Agent
    module Protect
      module Rule
        # Module to hold required generic filters (prefilter, infilter, postfilter)
        module Filters
          POSTFILTER_MODES = Set.new(%i[BLOCK MONITOR]).cs__freeze

          # Actions required for the rules that have to happen before the
          # application has completed its processing of the request.
          #
          # For most rules, these actions are performed within the analysis
          # engine and communicated as an input analysis result. Those that
          # require specific action need to provide that action.
          #
          # @param context [Contrast::Agent::RequestContext] the context for
          #   the current request
          def prefilter context
            return unless prefilter?(context)

            ia_results = gather_ia_results(context)

            ia_results.each do |ia_result|
              result = build_attack_result(context)
              result = build_attack_without_match(context, ia_result, result)
              next unless result

              append_to_activity(context, result)
              record_triggered(context)
              raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
            end
          end

          # Prefilter check always called before infilter to check if the rule is infilter
          # capable, not disabled or in other way excluded by url or input exclusions.
          #
          # @param context [Contrast::Agent::RequestContext]
          # @return [Boolean]
          def prefilter? context
            return false unless enabled?
            return false if protect_excluded_by_url?(rule_name)
            return false unless context
            return false unless (results = gather_ia_results(context)) && results.any?
            return false if protect_excluded_by_input?(results)

            true
          end

          # This should only ever be called directly from patched code and will
          # have a different implementation based on the rule. As such, there
          # is not parent implementation.
          #
          # @param context [Contrast::Agent::RequestContext] the context for
          #   the current request
          # @param match_string [String] the input that violated the rule and
          #   matched the attack detection logic
          # @param _kwargs [Hash] key-value pairs used by the rule to build a
          #   report.
          def infilter context, match_string, _kwargs
            return unless infilter?(context)
            return unless (result = build_violation(context, match_string))

            append_to_activity(context, result)
            record_triggered(context)
            raise(Contrast::SecurityException.new(self, block_message)) if blocked?
          end

          # Infilter check always called before infilter to check if the rule is infilter
          # capable, not disabled or in other way excluded by url or input exclusions.
          #
          # @param context [Contrast::Agent::RequestContext]
          # @return [Boolean]
          def infilter? context
            return false unless enabled?
            return false unless (results = gather_ia_results(context)) && results.any?
            return false if protect_excluded_by_url?(rule_name)
            return false if protect_excluded_by_input?(results)

            true
          end

          # Check befor commiting infilter
          #
          # @param context [Contrast::Agent::RequestContext]
          def postfilter? context
            return false unless enabled? && POSTFILTER_MODES.include?(mode)
            return false if protect_excluded_by_url?(rule_name)
            return false if protect_excluded_by_input?(gather_ia_results(context))
            return false if mode == :NO_ACTION || mode == :PERMIT

            true
          end

          # Actions required for the rules that have to happen after the
          # application has completed its processing of the request.
          #
          # Any implementation here needs to account for the fact that
          # responses may be streaming and, as such, transformations of the
          # response itself may not be permissible.
          #
          # Override for rules that need the response
          # Currently postfilter can be applied to streamed responses, if any logic within postfilter changes to modify
          # the response streamed responses will break
          #
          # @param context [Contrast::Agent::RequestContext]
          # @raise [Contrast::SecurityException]
          def postfilter context
            return unless postfilter?(context)

            result = find_postfilter_attacker(context, nil)
            return unless result&.samples&.any?

            append_to_activity(context, result)
            record_triggered(context)
            return unless blocked_violation?(result)

            raise(Contrast::SecurityException.new(self, "#{ rule_name } triggered in postfilter. Response blocked."))
          end
        end
      end
    end
  end
end