# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'zlib' require 'stringio' require 'contrast/components/logger' require 'contrast/components/scope' require 'contrast/agent/reporting/reporting_events/application_startup' require 'contrast/agent/reporting/reporting_utilities/reporter_client' require 'contrast/agent/reporting/reporting_utilities/endpoints' require 'contrast/agent/reporting/reporting_utilities/resend' module Contrast module Agent module Reporting # This module holds utilities required by the reporting service client module ReporterClientUtils include Contrast::Agent::Reporting::Resend include Contrast::Components::Logger::InstanceMethods include Contrast::Components::Scope::InstanceMethods include Contrast::Agent::Reporting::Endpoints STRING_ENCODING = 'BINARY' # List the events that need to be sent when agent starts up. # The order here matters -- DO NOT CHANGE IT!!! >_< - HM # # If you add more, update the test in reporter_client_spec.rb STARTUP_EVENTS = [ Contrast::Agent::Reporting::AgentStartup, Contrast::Agent::Reporting::ApplicationStartup ].cs__freeze def audit @_audit ||= Contrast::Agent::Reporting::Audit.new end private # Send Agent Startup event. If error occurs, it will try to resend the message. # # @param connection [Net::HTTP] open connection def send_agent_startup connection logger.debug('[Reporter] Preparing to send startup messages') STARTUP_EVENTS.each { |event| send_event(event.new, connection) } logger.debug('[Reporter] Startup messages sent.') if status.startup_messages_sent? end # Disable reporting and log the error # # @param event [Contrast::Agent::Reporting::ReportingEvent] # @param error [StandardError] def disable_reporting event, error status.failure! mode.resend.reset_rescue_attempts mode.status = mode.disabled message = '[Reporter] Unable to send message.' response_handler.stop_reporting(message, application: Contrast::APP_CONTEXT.name, # rubocop:disable Security/Module/Name connection_error: error, client: Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME, event_id: event&.__id__, event_type: event&.cs__class&.cs__name) nil end def response_success! status.success! mode.enter_run_mode mode.resend.reset_rescue_attempts end # This method will build headers of the request required for TS communication # # @param request [Net::HTTPRequest] # @return [Net::HTTPRequest] def build_headers request build_application_headers(request) build_encode_and_compress_headers(request) request['Authorization'] = @headers.authorization request['Server-Name'] = @headers.server_name request['Server-Path'] = @headers.server_path request['Server-Type'] = @headers.server_type request['X-Contrast-Agent'] = @headers.agent_version request['X-Contrast-Header-Encoding'] = @headers.encoding request['Session-ID'] = @headers.session_id request end # Handles response processing and sets status # # @param event [Contrast::Agent::Reporting::ReportingEvent] The event sent to TeamServer. # @param response [Net::HTTP::Response] def process_settings_response response, event res = response_handler.process(response, event) if res status.success! mode.resend.reset_rescue_attempts else status.failure! end res end # Given a response from preflght, when the finding hash is desired, then send the finding to which it pertains. # The method accepts any Contrast::Agent::Reporting::ReportingEvent, but will short circuit if it is not a # Contrast::Agent::Reporting::Preflight. # # @param event [Contrast::Agent::Reporting::ReportingEvent] The event to send to TeamServer. Really a # child of the ReportingEvent rather than a literal one. # @param response [Net::HTTPResponse,nil] The response we handle and read from # @param connection [Net::HTTP] open connection def process_preflight_response event, response, connection return unless event.cs__is_a?(Contrast::Agent::Reporting::Preflight) return unless response&.body && connection findings_to_return = response.body.split(',').delete_if { |el| el.include?('*') } mode.resend.reset_rescue_attempts findings_to_return.each do |index| preflight_message = event.messages[index.to_i] corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message&.data) next unless corresponding_finding send_event(corresponding_finding, connection) end rescue StandardError => e logger.error('[Reporter] Unable to handle preflight response', e) end # Convert the given event into an appropriate Net::HTTPRequest object, setting the request headers and # assigning endpoint the endpoint appropriate for the event and casting its hash to a JSON body. # # @param event event [Contrast::Agent::Reporting::ReportingEvent] The event to send to TeamServer. Really a # child of the ReportingEvent rather than a literal one. # @return [Net::HTTP::Post,Net::HTTP::Put] def build_request event with_contrast_scope do request = case event.event_method when :PUT Net::HTTP::Put.new(event.event_endpoint) when :GET Net::HTTP::Get.new(event.event_endpoint) else # :POST Net::HTTP::Post.new(event.event_endpoint) end build_headers(request) event.attach_headers(request) request.body = compress_event(event) request end end # Adds the compression and encoding Headers required for sending # compress and encoded body payload. # # @param request [Net::HTTPRequest] def build_encode_and_compress_headers request request['Content-Type'] = @headers.content_type request['X-Contrast-Header-Encoding'] = @headers.encoding request['X-Contrast-Encoding'] = @headers.compression request['Content-Encoding'] = @headers.compression end # Adds corresponding application headers to request. # # @param request [Net::HTTPRequest] def build_application_headers request app_version = @headers.app_version request['API-Key'] = @headers.api_key request['Application-Language'] = @headers.app_language request['Application-Name'] = @headers.app_name request['Application-Path'] = @headers.app_path request['Application-Version'] = app_version if app_version end end end end end