# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/assess/rule/response/autocomplete_rule' require 'contrast/agent/assess/rule/response/hsts_header_rule' require 'contrast/agent/assess/rule/response/cachecontrol_rule' require 'contrast/agent/assess/rule/response/clickjacking_rule' require 'contrast/agent/assess/rule/response/x_content_type_rule' require 'contrast/agent/assess/rule/response/parameters_pollution_rule' module Contrast module Agent # This class extends RequestContexts: this class acts to encapsulate information about the currently # executed request, making it available to the Agent for the duration of the request in a standardized # and normalized format which the Agent understands. module RequestContextExtend BUILD_ATTACK_LOGGER_MESSAGE = 'Building attack result from Contrast Service input analysis result' # Convert the discovered route for this request to appropriate forms and disseminate it to those locations # where it is necessary for our route coverage and finding vulnerability discovery features to function. # # @param route [Contrast::Api::Dtm::RouteCoverage, nil] the route of the current request, as determined from the # framework def append_route_coverage route return unless route # For our findings @route = route # For SR findings @activity.routes << route # For TS routes @observed_route.signature = route.route @observed_route.verb = route.verb @observed_route.url = route.url if route.url @request.route = route @request.observed_route = @observed_route end # Convert the discovered route for this request to appropriate forms and disseminate it to those locations # where it is necessary for our route coverage and finding vulnerability discovery features to function. # def append_to_new_observed_route route return unless route @new_observed_route.signature = route.route @new_observed_route.verb = route.verb @new_observed_route.url = route.url if route.url @request.new_observed_route = @new_observed_route end # Collect the results for the given rule with the given action # # @param rule [String] the id of the rule to which the results apply # @param response_type [Symbol] the result of the response, matching a value of # Contrast::Api::Dtm::AttackResult::ResponseType # @return [Array] def results_for rule, response_type = nil if response_type.nil? activity.results.select { |r| r.rule_id == rule } else activity.results.select { |r| r.rule_id == rule && r.response == response_type } end end def service_extract_request return false unless ::Contrast::AGENT.enabled? return false unless ::Contrast::PROTECT.enabled? return false if @do_not_track service_response = Contrast::Agent.messaging_queue.send_event_immediately(@activity.http_request) return false unless service_response handle_protect_state(service_response) ia = service_response.input_analysis if ia if logger.trace? logger.trace('Analysis from Contrast Service', evaluations: ia.results.length) logger.trace('Results', input_analysis: ia.inspect) end @speedracer_input_analysis = ia speedracer_input_analysis.request = request else logger.trace('Analysis from Contrast Service was empty.') false end rescue Contrast::SecurityException => e raise e rescue StandardError => e logger.warn('Unable to extract Contrast Service information from request', e) false end # NOTE: this method is only used as a backstop if Speedracer sends Input Evaluations when the protect state # indicates a security exception should be thrown. This method ensures that the attack reports are generated. # Normally these should be generated on Speedracer for any attacks detected during prefilter. # # @param agent_settings [Contrast::Api::Settings::AgentSettings] def handle_protect_state agent_settings return unless agent_settings&.protect_state state = agent_settings.protect_state @uuid = state.uuid @do_not_track = true unless state.track_request return unless state.security_exception # If Contrast Service has NOT handled the input analysis, handle them here build_attack_results(agent_settings) logger.debug('Contrast Service said to block this request') raise Contrast::SecurityException.new(nil, (state.security_message || 'Blocking suspicious behavior')) end # append anything we've learned to the request seen message this is the sum-total of all inventory information # that has been accumulated since the last request def extract_after rack_response @response = Contrast::Agent::Response.new(rack_response) activity.http_response = @response.dtm if @sample_res return unless Contrast::Agent::Reporter.enabled? Contrast::Agent::Assess::Rule::Response::Autocomplete.new.analyze(@response) Contrast::Agent::Assess::Rule::Response::HSTSHeader.new.analyze(@response) Contrast::Agent::Assess::Rule::Response::Cachecontrol.new.analyze(@response) Contrast::Agent::Assess::Rule::Response::XXssProtection.new.analyze(@response) Contrast::Agent::Assess::Rule::Response::CspHeaderMissing.new.analyze(@response) Contrast::Agent::Assess::Rule::Response::CspHeaderInsecure.new.analyze(@response) Contrast::Agent::Assess::Rule::Response::Clickjacking.new.analyze(@response) Contrast::Agent::Assess::Rule::Response::XContentType.new.analyze(@response) Contrast::Agent::Assess::Rule::Response::ParametersPollution.new.analyze(@response) rescue StandardError => e logger.error('Unable to extract information after request', e) end private # Generate attack results directly from any evaluations on the agent settings object. # # @param agent_settings [Contrast::Api::Settings::AgentSettings] def build_attack_results agent_settings return unless agent_settings&.input_analysis&.results&.any? results_by_rule = {} agent_settings.input_analysis.results.each do |ia_result| rule_id = ia_result.rule_id rule = ::Contrast::PROTECT.rule(rule_id) next unless rule logger.debug(BUILD_ATTACK_LOGGER_MESSAGE, result: ia_result.inspect) if logger.debug? results_by_rule[rule_id] = attack_result rule, rule_id, ia_result, results_by_rule end results_by_rule.each_pair do |_, attack_result| logger.info('Blocking attack result', rule: attack_result.rule_id) activity.results << attack_result end end # Generates the attack result # # @param rule [Contrast::Agent::Protect::Rule, Contrast::Agent::Assess::Rule] # @param rule_id [String] String name of the rule # @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the # analysis of the input that was determined to be an attack # @param results_by_rule [Hash] attack results from any evaluations on the agent settings object. # @return [Contrast::Api::Dtm::AttackResult] the attack result from this input def attack_result rule, rule_id, ia_result, results_by_rule @_attack_result = if rule.mode == :BLOCK # special case for rules (like reflected xss) that used to have an infilter / block mode # but now are just block at perimeter rule.build_attack_with_match(self, ia_result, results_by_rule[rule_id], ia_result.value) else rule.build_attack_without_match(self, ia_result, results_by_rule[rule_id]) end end end end end