# 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