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

require 'contrast/agent/thread/worker_thread'
require 'contrast/agent/reporting/input_analysis/input_analysis_result'
require 'contrast/agent/reporting/input_analysis/score_level'
require 'contrast/agent/reporting/reporting_events/application_activity'
require 'contrast/utils/input_classification_base'

module Contrast
  module Agent
    module Protect
      # WorthWatchingInputAnalyzer Perform analysis of input tracing v2 worthwatching results in a
      # separate thread, should only be run at the end of the request.
      # Currently only includes: cmd_injection & sqli_injection rules
      class WorthWatchingInputAnalyzer < WorkerThread
        include Timeout
        include Contrast::Agent::Protect::Rule::InputClassificationBase

        QUEUE_SIZE = 1000.cs__freeze
        AGENTLIB_TIMEOUT = 5.cs__freeze
        # max size of inputs to evaluate
        INPUT_BYTESIZE_THRESHOLD = 100_000.cs__freeze
        REPORT_INTERVAL_SECOND = 30.cs__freeze

        # Thread that will process all the InputAnalysisResults that have a score level of WORTHWATCHING and
        # sends results to TeamServer
        def start_thread!
          return unless attempt_to_start?
          return if running?

          @_thread = Contrast::Agent::Thread.new do
            logger.info('[WorthWatchingAnalyzer] Starting thread.')
            loop do
              break unless attempt_to_start?

              sleep(REPORT_INTERVAL_SECOND)
              next if queue.empty?

              report = false
              # build attack_results for all infilter active protect rules.
              stored_ia = queue.pop
              results = build_results(stored_ia)
              activity = Contrast::Agent::Reporting::ApplicationActivity.new(ia_request: stored_ia.request)
              results.each do |result|
                next unless (attack_result = eval_input(result))

                activity.attach_defend(attack_result)
                report = true
              end
              Contrast::Agent::Reporting::Masker.mask(activity)
              Contrast::Agent.reporter.send_event(activity) if report
            rescue StandardError => e
              logger.error('[WorthWatchingAnalyzer] thread could not process result because of:', e)
            end
          end
        end

        # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
        def add_to_queue input_analysis
          return unless input_analysis

          if queue.size >= QUEUE_SIZE
            logger.debug('[WorthWatchingAnalyzer] queue at max size, skip input_result')
            return
          end
          # There will be no results here because of the delay of the protect rule analysis,
          # we need to save the ia which contains the request and saved extracted user inputs to
          # be evaluated on the thread rather than building results here. This way we allow the
          # request to continue and will build the attack results later.
          queue << input_analysis.dup
        end

        private

        # This method will build the attack results from the saved ia.
        #
        # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
        # @return attack_results [array<Contrast::Agent::Reporting::InputAnalysisResult>] all the results
        # from the input analysis.
        def build_results input_analysis
          # Construct the input analysis for the all the infilter rules that were not triggered.
          # There is a set timeout for each rule to be analyzed in. The infilter flag will make
          # sure that if a rule is already triggered during the infilter phase it will not be analyzed
          # now, making sure we don't report same rule twice.
          Contrast::Agent::Protect::InputAnalyzer.input_classification(input_analysis, infilter: true)
          results = input_analysis.results.reject do |val|
            val.score_level == Contrast::Agent::Reporting::InputAnalysisResult::SCORE_LEVEL::IGNORE
          end
          return results if results

          []
        end

        # @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the WorthWatching InputAnalysisResult
        # @return [Contrast::Agent::Reporting::AttackResult, nil] InputAnalysisResult updated Result or nil
        def eval_input ia_result
          return build_attack_result(ia_result) unless ia_result.value.to_s.bytesize >= INPUT_BYTESIZE_THRESHOLD

          logger.debug("[WorthWatchingAnalyzer] Skipping analysis: Input size is larger than
                      #{ INPUT_BYTESIZE_THRESHOLD / 1024 }KB")
          nil
        end

        # @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the updated InputAnalysisResult
        # with a score of :DEFINITEATTACK
        # @return [Contrast::Agent::Reporting::AttackResult] the attack result from
        #   this input
        def build_attack_result ia_result
          Contrast::PROTECT.rule(ia_result.rule_id).build_attack_without_match(nil, ia_result, nil)
        end

        def queue
          @_queue ||= Queue.new
        end

        def delete_queue!
          @_queue&.clear
          @_queue&.close
          @_queue = nil
        end
      end
    end
  end
end