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

require 'contrast/agent/protect/rule/base'
require 'contrast/agent/protect/policy/applies_sqli_rule'
require 'contrast/agent/protect/rule/sqli/sql_sample_builder'
require 'contrast/agent/reporting/input_analysis/input_type'
require 'contrast/agent/protect/rule/sqli/sqli_base_rule'
require 'contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions'
require 'contrast/agent/protect/rule/sqli/sqli_input_classification'

module Contrast
  module Agent
    module Protect
      module Rule
        # The Ruby implementation of the Protect SQL Injection rule.
        class Sqli < Contrast::Agent::Protect::Rule::SqliBaseRule
          # Generate a sample for the SQLI injection detection rule, allowing for reporting to and rendering
          # by TeamServer
          include SqlSampleBuilder::SqliSample
          # Defining build_attack_with_match method
          include SqlSampleBuilder::AttackBuilder
          include Contrast::Agent::Reporting::InputType
          class << self
            include Contrast::Agent::Reporting::InputType
          end

          NAME = 'sql-injection'

          def rule_name
            NAME
          end

          def block_message
            BLOCK_MESSAGE
          end

          # Array of sub_rules
          #
          # @return [Array]
          def sub_rules
            @_sub_rules ||= [Contrast::Agent::Protect::Rule::SqliDangerousFunctions.new].cs__freeze
          end

          def applicable_user_inputs
            APPLICABLE_USER_INPUTS
          end

          # SQLI input classification
          #
          # @return [module<Contrast::Agent::Protect::Rule::SqliInputClassification>]
          def classification
            @_classification ||= Contrast::Agent::Protect::Rule::SqliInputClassification.cs__freeze
          end

          # Allows for the InputAnalysis from Agent Library to be extracted early
          # @param context [Contrast::Agent::RequestContext]
          # @param potential_attack_string [String, nil]
          # @param ia_results [Array<Contrast::Agent::Reporting::InputAnalysis>]
          # @param **kwargs
          # @return [Contrast::Agent::Reporting, nil]
          def find_attacker_with_results context, potential_attack_string, ia_results, **kwargs
            logger.trace('Checking vectors for attacks', rule: rule_name, input: potential_attack_string)

            result = nil
            ia_results.each do |ia_result|
              if potential_attack_string
                idx = potential_attack_string.index(ia_result.value)
                next unless idx

                database_type = kwargs[:database].to_sym
                input_length = ia_result.value.length
                lib_result = check_sql_input_with_agent(potential_attack_string, database_type, idx, input_length)

                kwargs[:result_struct] = lib_result
                result = build_attack_with_match(context, ia_result, result, potential_attack_string, **kwargs)
              else
                result = build_attack_without_match(context, ia_result, result, **kwargs)
              end
            end
            result
          end

          # We'll need a second place, where we need to check the token boundaries if are being crossed and
          # worth-watching.
          #
          # @param sql[String] SQL coming from parameter
          # @param database[String] Type of database
          # @param input_index[String] index in the sqlQuery string where user input was found
          # @param input_length[Number] length of the input value
          # @return [Hash, Boolean]
          def check_sql_input_with_agent sql, database, input_index, input_length
            return false unless (agent_lib = Contrast::AGENT_LIB) && sql && database

            agent_lib.check_sql_query(input_index, input_length, database, sql)
          end
        end
      end
    end
  end
end