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

require 'contrast/utils/object_share'
require 'contrast/agent/reporting/input_analysis/input_type'
require 'contrast/agent/reporting/input_analysis/score_level'
require 'contrast/agent/protect/input_analyzer/input_analyzer'
require 'contrast/components/logger'

module Contrast
  module Agent
    module Protect
      module Rule
        # This module will include all the similar information for all input classifications
        # between different rules
        module InputClassificationBase
          UNKNOWN_KEY = 'unknown'
          THRESHOLD = 90.cs__freeze
          WORTHWATCHING_THRESHOLD = 10.cs__freeze
          include Contrast::Agent::Reporting::InputType
          include Contrast::Agent::Reporting::ScoreLevel
          include Contrast::Components::Logger::InstanceMethods

          KEYS_NEEDED = [COOKIE_VALUE, PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE].cs__freeze

          # Input Classification stage is done to determine if an user input is
          # DEFINITEATTACK or to be ignored.
          #
          # @param rule_id [String] Name of the protect rule.
          # @param input_type [Symbol, Contrast::Agent::Reporting::InputType] The type of the user input.
          # @param value [String, Array<String>] the value of the input.
          # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
          #                                                       agent analysis from the current
          #                                                       Request.
          # @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results.
          def classify rule_id, input_type, value, input_analysis
            return unless (rule = Contrast::PROTECT.rule(rule_id))
            return unless rule.applicable_user_inputs.include?(input_type)
            return unless input_analysis.request

            Array(value).each do |val|
              Array(val).each do |v|
                next unless v

                result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, v)
                append_result(input_analysis, result)
              end
            end

            input_analysis
          rescue StandardError => e
            logger.debug("An Error was recorded in the input classification of the #{ rule_id }")
            logger.debug(e)
            nil
          end

          # Creates new isntance of InputAnalysisResult with basic info.
          #
          # @param rule_id [String] The name of the Protect Rule.
          # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
          # @param value [String, Array<String>] the value of the input.
          # @param path [String] the path of the current request context.
          #
          # @return res [Contrast::Agent::Reporting::InputAnalysisResult]
          def new_ia_result rule_id, input_type, path, value = nil
            res = Contrast::Agent::Reporting::InputAnalysisResult.new
            res.rule_id = rule_id
            res.input_type = input_type
            res.path = path
            res.value = value
            res
          end

          # This methods checks if input is value that matches a key in the input.
          #
          # @param request [Contrast::Agent::Request] the current request context.
          # @param result [Contrast::Agent::Reporting::InputAnalysisResult] result to be updated.
          # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
          # @param value [String, Array<String>] the value of the input.
          #
          # @return result [Contrast::Agent::Reporting::InputAnalysisResult] updated with key result.
          def add_needed_key request, result, input_type, value
            case input_type
            when COOKIE_VALUE
              result.key = request.cookies.key(value)
            when PARAMETER_VALUE, URL_PARAMETER
              result.key = request.parameters.key(value)
            when HEADER
              result.key = request.headers.key(value)
            when UNKNOWN
              result.key = UNKNOWN_KEY
            else
              result.key
            end
          rescue StandardError => e
            logger.warn('Could not find proper key for input traced value', message: e)
          end

          # Some input types are not yet supported from the AgentLib.
          # This will convert the type to the closet possible if viable,
          # so that the input tracing could be done.
          #
          # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
          # @return [Integer<Contrast::AgentLib::Interface::INPUT_SET>]
          def convert_input_type input_type
            case input_type
            when URI, URL_PARAMETER
              Contrast::AGENT_LIB.input_set[:URI_PATH]
            when BODY, DWR_VALUE, SOCKET, UNDEFINED_TYPE, UNKNOWN, REQUEST, QUERYSTRING
              Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
            when HEADER
              Contrast::AGENT_LIB.input_set[:HEADER_VALUE]
            when MULTIPART_VALUE, MULTIPART_FIELD_NAME
              Contrast::AGENT_LIB.input_set[:MULTIPART_NAME]
            when JSON_ARRAYED_VALUE
              Contrast::AGENT_LIB.input_set[:JSON_KEY]
            when PARAMETER_NAME
              Contrast::AGENT_LIB.input_set[:PARAMETER_KEY]
            else
              Contrast::AGENT_LIB.input_set[input_type]
            end
          rescue StandardError => e
            logger.debug('Protect Input classification could not determine input type, falling back to default',
                         error: e)
            Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
          end

          private

          # This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
          # key if needed and Creates new instance of InputAnalysisResult.
          #
          # @param request [Contrast::Agent::Request] the current request context.
          # @param rule_id [String] The name of the Protect Rule.
          # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
          # @param value [String, Array<String>] the value of the input.
          #
          # @return res [Contrast::Agent::Reporting::InputAnalysisResult, nil]
          def create_new_input_result request, rule_id, input_type, value
            return unless Contrast::AGENT_LIB

            input_eval = Contrast::AGENT_LIB.eval_input(value,
                                                        convert_input_type(input_type),
                                                        Contrast::AGENT_LIB.rule_set[rule_id],
                                                        Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING])

            ia_result = new_ia_result(rule_id, input_type, request.path, value)
            score = input_eval&.score || 0
            if score >= WORTHWATCHING_THRESHOLD
              ia_result.score_level = WORTHWATCHING
              ia_result.ids << self::WORTHWATCHING_MATCH
            else
              ia_result.score_level = IGNORE
              return
            end

            add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type)
            ia_result
          end

          def append_result ia_analysis, result
            ia_analysis.results << result if result
            ia_analysis
          end
        end
      end
    end
  end
end