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

cs__scoped_require 'contrast/agent/protect/rule/base_service'
cs__scoped_require 'contrast/agent/protect/policy/applies_sqli_rule'

module Contrast
  module Agent
    module Protect
      module Rule
        # The Ruby implementation of the Protect SQL Injection rule.
        class Sqli < Contrast::Agent::Protect::Rule::BaseService
          NAME = 'sql-injection'
          BLOCK_MESSAGE = 'SQLi rule triggered. Response blocked.'

          def name
            NAME
          end

          def block_message
            BLOCK_MESSAGE
          end

          def infilter context, database, query_string
            return nil unless infilter?(context)

            result = find_attacker(context, query_string, database: database)
            return nil unless result

            append_to_activity(context, result)

            raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked?
          end

          def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
            attack_string = input_analysis_result.value
            regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)

            return nil unless query_string.match?(regexp)

            database = kwargs[:database]
            scanner = select_scanner(database)

            ss = StringScanner.new(query_string)
            length = attack_string.length
            while ss.scan_until(regexp)
              # the pos of StringScanner is at the end of the regexp (input string),
              # we need the beginning
              idx = ss.pos - attack_string.length
              last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
              next unless last_boundary && boundary

              input_analysis_result.attack_count = input_analysis_result.attack_count + 1

              kwargs[:start_idx] = idx
              kwargs[:end_idx] = idx + length
              kwargs[:boundary_overrun_idx] = boundary
              kwargs[:input_boundary_idx] = last_boundary

              result ||= build_attack_result(context)
              update_successful_attack_response(context, input_analysis_result, result, query_string)
              append_sample(context, input_analysis_result, result, query_string, **kwargs)
            end

            result
          end

          protected

          def build_sample context, input_analysis_result, candidate_string, **kwargs
            input = input_analysis_result.value

            sample = build_base_sample(context, input_analysis_result)
            sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new
            sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
            sample.sqli.start_idx = sample.sqli.query.index(input).to_i
            sample.sqli.end_idx = sample.sqli.start_idx + input.length
            sample.sqli.boundary_overrun_idx = kwargs[:boundary].to_i
            sample.sqli.input_boundary_idx = kwargs[:last_boundary].to_i
            sample
          end

          private

          def select_scanner database
            @sql_scanners ||= {
                Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
                    Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new,
                Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_PG =>
                    Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new,
                Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_SQLITE =>
                    Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new
            }.cs__freeze

            @default_sql_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new
            @sql_scanners[database.to_s] || @default_sql_scanner
          end
        end
      end
    end
  end
end