# 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/components/interface'

module Contrast
  module CoreExtensions
    module Protect
      # This Module is how we apply the NoSQL Injection rule. It is called from
      # our patches of the targeted methods in which the execution of String
      # based NoSQL queries occur. It is responsible for deciding if the
      # infilter methods of the rule should be invoked.
      module AppliesNoSqliRule
        DATABASE_MONGO = 'MongoDB'
        ACTION_DISPATCH = 'dispatch'
        ACTION_READ = 'read'
        ACTION_WRITE = 'write'
        ACTION_PROCESS = 'process'

        class << self
          include Contrast::Components::Interface
          access_component :logging, :analysis

          def cs__patched_apply_nosql_rule method, _exception, properties, _object, args
            return unless valid_input?(args)
            return if cs__skip_analysis?

            database = properties['database']
            operations = args[0]
            context = Contrast::Agent::REQUEST_TRACKER.current
            if operations.is_a?(Array)
              operations.each do |m|
                handle_operation(context, database, method, m)
              end
            else
              handle_operation(context, database, method, operations)
            end
          rescue Contrast::SecurityException => e
            raise e
          rescue StandardError => e
            logger.error(e, "Error running NoSQLi rule in #{ properties['database'] }")
          end

          def rule
            PROTECT.rule Contrast::Agent::Protect::Rule::NoSqli::NAME
          end

          def valid_input? args
            return false unless args&.any?

            args[0]
          end

          def cs__skip_analysis?
            context = Contrast::Agent::REQUEST_TRACKER.current
            return true unless context&.app_loaded?
            return true unless rule&.enabled?

            false
          end

          def handle_operation context, database, action, operation
            data = extract_mongo_data(operation)
            return unless data && !data.empty?

            logger.debug(nil, "applying nosqli rule #{ database }##{ action }")
            rule.infilter(context, database, data)
          end

          def extract_mongo_data operation
            if operation.cs__respond_to? :selector
              operation.selector
            elsif operation.cs__respond_to? :documents
              operation.documents
            end
          end
        end
      end
    end
  end
end