# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/logger' module Contrast module Agent module Reporting # This is the new FindingRequest class which will include all the needed information for the new reporting system # to relay this information in the Finding/Trace messages. These requests are used by TeamServer to construct the # HTTP information for the assess feature. They represent the literal request made that resulted in the # vulnerability being triggered. class FindingRequest include Contrast::Components::Logger::InstanceMethods OMITTED_BODY = '{{body-omitted-by-contrast}}' # @return [String] the body of this request attr_accessor :body # @return [Hash>] the headers of this request attr_accessor :headers # @return [String] the HTTP verb of this request attr_reader :method # @return [Hash>] the parameters of this request attr_reader :parameters # @return [Integer] the port to which this request connected attr_reader :port # @return [String] the HTTP(S) protocol of this request attr_reader :protocol # @return [String] the query string of this request attr_accessor :query_string # @return [String] the url, including path and script, of this request attr_reader :uri # @return [String] the HTTP version of this request attr_reader :version # @return [Integer] attr_reader :ip # @return [String] Byte representation of the body attr_accessor :body_binary # @return [Hash] attr_reader :cookies class << self # @param request [Contrast::Agent::Request] # @return [Contrast::Agent::Reporting::FindingRequest] def convert request return unless request report = new report.attach_data(request) report end end # Parse the data from a Contrast::Agent::Request to attach what is required for reporting to TeamServer to this # Contrast::Agent::Reporting::FindingRequest # # @param request [Contrast::Agent::Request] def attach_data request @body = request.body @headers = {} extract_headers(request) @method = request.request_method @parameters = {} request.parameters.each_pair { |key, value| @parameters[key] = Array(value) } @port = request.port || 0 @protocol = request.scheme @query_string = request.query_string @uri = request.normalized_uri @version = request.version @ip = request.ip || '' @body_binary = if omit_body?(request) OMITTED_BODY else Contrast::Utils::StringUtils.force_utf8(request.body) end @cookies = {} @cookies = request.cookies unless request.cookies.empty? end # Convert the instance variables on the class, and other information, into the identifiers required for # TeamServer to process the JSON form of this message. # # @return [Hash] # @raise [ArgumentError] def to_controlled_hash begin validate rescue ArgumentError => e logger.error('FindingRequest validation failed with: ', e) return end { body: body, headers: headers, method: method, # rubocop:disable Security/Object/Method parameters: parameters, port: port || 0, protocol: protocol, queryString: query_string, uri: uri, version: version } end def omit_body? request return true if ::Contrast::AGENT.omit_body? return false if request.document_type != :NORMAL request.media_type&.include?('multipart/form-data') end def validate unless method && !method.empty? # rubocop:disable Security/Object/Method raise(ArgumentError, "#{ self } did not have a proper method. Unable to continue.") end raise(ArgumentError, "#{ self } did not have a proper uri. Unable to continue.") unless uri && !uri.empty? end # @param request [Contrast::Agent::Request] def extract_headers request request.headers.each_pair do |key, value| # We need to change from the uppercase _ format to capitalized - format. header = key.split('_') header.each(&:capitalize!) header = header.join('-') headers[header] = value.split end end end end end end