# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/utils/object_share' require 'contrast/agent/reporting/input_analysis/input_type' require 'contrast/agent/reporting/input_analysis/score_level' require 'contrast/agent/protect/input_analyzer/input_analyzer' require 'contrast/components/logger' module Contrast module Agent module Protect module Rule # This module will include all the similar information for all input classifications # between different rules module InputClassificationBase UNKNOWN_KEY = 'unknown' THRESHOLD = 90.cs__freeze WORTHWATCHING_THRESHOLD = 10.cs__freeze include Contrast::Agent::Reporting::InputType include Contrast::Agent::Reporting::ScoreLevel include Contrast::Components::Logger::InstanceMethods KEYS_NEEDED = [COOKIE_VALUE, PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE].cs__freeze # Input Classification stage is done to determine if an user input is # DEFINITEATTACK or to be ignored. # # @param rule_id [String] Name of the protect rule. # @param input_type [Symbol, Contrast::Agent::Reporting::InputType] The type of the user input. # @param value [String, Array<String>] the value of the input. # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the # agent analysis from the current # Request. # @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results. def classify rule_id, input_type, value, input_analysis return unless (rule = Contrast::PROTECT.rule(rule_id)) return unless rule.applicable_user_inputs.include?(input_type) return unless input_analysis.request Array(value).each do |val| Array(val).each do |v| next unless v result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, v) append_result(input_analysis, result) end end input_analysis rescue StandardError => e logger.debug("An Error was recorded in the input classification of the #{ rule_id }") logger.debug(e) nil end # Creates new isntance of InputAnalysisResult with basic info. # # @param rule_id [String] The name of the Protect Rule. # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input. # @param value [String, Array<String>] the value of the input. # @param path [String] the path of the current request context. # # @return res [Contrast::Agent::Reporting::InputAnalysisResult] def new_ia_result rule_id, input_type, path, value = nil res = Contrast::Agent::Reporting::InputAnalysisResult.new res.rule_id = rule_id res.input_type = input_type res.path = path res.value = value res end # This methods checks if input is value that matches a key in the input. # # @param request [Contrast::Agent::Request] the current request context. # @param result [Contrast::Agent::Reporting::InputAnalysisResult] result to be updated. # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input. # @param value [String, Array<String>] the value of the input. # # @return result [Contrast::Agent::Reporting::InputAnalysisResult] updated with key result. def add_needed_key request, result, input_type, value case input_type when COOKIE_VALUE result.key = request.cookies.key(value) when PARAMETER_VALUE, URL_PARAMETER result.key = request.parameters.key(value) when HEADER result.key = request.headers.key(value) when UNKNOWN result.key = UNKNOWN_KEY else result.key end rescue StandardError => e logger.warn('Could not find proper key for input traced value', message: e) end # Some input types are not yet supported from the AgentLib. # This will convert the type to the closet possible if viable, # so that the input tracing could be done. # # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input. # @return [Integer<Contrast::AgentLib::Interface::INPUT_SET>] def convert_input_type input_type case input_type when URI, URL_PARAMETER Contrast::AGENT_LIB.input_set[:URI_PATH] when BODY, DWR_VALUE, SOCKET, UNDEFINED_TYPE, UNKNOWN, REQUEST, QUERYSTRING Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE] when HEADER Contrast::AGENT_LIB.input_set[:HEADER_VALUE] when MULTIPART_VALUE, MULTIPART_FIELD_NAME Contrast::AGENT_LIB.input_set[:MULTIPART_NAME] when JSON_ARRAYED_VALUE Contrast::AGENT_LIB.input_set[:JSON_KEY] when PARAMETER_NAME Contrast::AGENT_LIB.input_set[:PARAMETER_KEY] else Contrast::AGENT_LIB.input_set[input_type] end rescue StandardError => e logger.debug('Protect Input classification could not determine input type, falling back to default', error: e) Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE] end private # This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's # key if needed and Creates new instance of InputAnalysisResult. # # @param request [Contrast::Agent::Request] the current request context. # @param rule_id [String] The name of the Protect Rule. # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input. # @param value [String, Array<String>] the value of the input. # # @return res [Contrast::Agent::Reporting::InputAnalysisResult, nil] def create_new_input_result request, rule_id, input_type, value return unless Contrast::AGENT_LIB input_eval = Contrast::AGENT_LIB.eval_input(value, convert_input_type(input_type), Contrast::AGENT_LIB.rule_set[rule_id], Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING]) ia_result = new_ia_result(rule_id, input_type, request.path, value) score = input_eval&.score || 0 if score >= WORTHWATCHING_THRESHOLD ia_result.score_level = WORTHWATCHING ia_result.ids << self::WORTHWATCHING_MATCH else ia_result.score_level = IGNORE return end add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type) ia_result end def append_result ia_analysis, result ia_analysis.results << result if result ia_analysis end end end end end end