# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'json' require 'net/http' require 'contrast/components/logger' require 'contrast/utils/net_http_base' require 'contrast/config/diagnostics/monitor' require 'contrast/agent/reporting/connection_status' require 'contrast/agent/reporting/reporting_utilities/headers' require 'contrast/agent/reporting/reporting_utilities/endpoints' require 'contrast/agent/reporting/reporting_events/server_settings' require 'contrast/agent/reporting/reporting_utilities/response_handler' require 'contrast/agent/reporting/reporting_events/application_settings' require 'contrast/agent/reporting/reporting_events/agent_effective_config' require 'contrast/agent/reporting/reporting_utilities/reporter_client_utils' require 'contrast/agent/reporting/reporting_utilities/resend' module Contrast module Agent module Reporting # This class creates a Net::HTTP client and initiates a connection to the provided result # @attr_reader headers [Contrast::Agent::Reporting::Headers] class ReporterClient < Contrast::Utils::NetHttpBase attr_reader :headers include Contrast::Agent::Reporting::Endpoints include Contrast::Agent::Reporting::ReporterClientUtils include Contrast::Agent::Reporting::ResponseHandlerUtils include Contrast::Components::Logger::InstanceMethods # @return [Array] events that may result in configuration changes RECORDABLE_EVENTS = [ Contrast::Agent::Reporting::ServerSettings, Contrast::Agent::Reporting::ApplicationSettings, Contrast::Agent::Reporting::AgentStartup, Contrast::Agent::Reporting::ApplicationStartup ].cs__freeze SERVICE_NAME = 'Reporter' REPORT_CONFIG_WHEN = %w[200 304].cs__freeze def initialize @headers = Contrast::Agent::Reporting::Headers.new super() end # This method initializes the Net::HTTP client we'll need. it will validate # the connection and make the first request. If connection is valid and response # is available then the open connection is returned. # # @return [Net::HTTP, nil] Return open connection or nil def initialize_connection # for this client we would use proxy and custom certificate file if available super(SERVICE_NAME, Contrast::API.api_url, use_proxy: true, use_custom_cert: true) end # Start the client for first time and sent startup event # # @param connection [Net::HTTP] open connection def startup! connection return if status.startup_messages_sent? return unless connection send_agent_startup(connection) end # Check event type and send it to appropriate TS endpoint # # @param event [Contrast::Agent::Reporting::ReportingEvent] The event to send to TeamServer. Really a # child of the ReportingEvent rather than a literal one. # @param connection [Net::HTTP] open connection # @return response [Net::HTTP::Response, nil] response from TS if no response def send_event event, connection return unless connection response = nil logger.debug('[Reporter] Preparing to send reporting event', event_class: event.cs__class) request = build_request(event) response = connection.request(request) audit.audit_event(event, response) if ::Contrast::API.request_audit_enable process_settings_response(response, event) process_preflight_response(event, response, connection) report_configuration(response, event) response rescue StandardError => e handle_response_error(event, connection, e) end # Write effective config to file: # If we are here the create and server messages are sent and the code received is # 200 or 304. In case of 304 there will be no new settings and we can write current ones. # This is done on every settings request. # # @param response [Contrast::Agent::Reporting::Response, nil] # @param event [Contrast::Agent::Reporting::ReportingEvent] def report_configuration response, event return unless response diagnostics.config.determine_config_status(response_handler.last_response_code || response.code) return unless REPORT_CONFIG_WHEN.include?(response_handler.last_response_code || response.code) return unless RECORDABLE_EVENTS.include?(event&.cs__class) logger.info('[Reporter Diagnostics] last response code:', response_code: response_handler.last_response_code) diagnostics.write_to_file config_event = Contrast::Agent::Reporting::AgentEffectiveConfig.new(diagnostics) Contrast::Agent.reporter.send_event(config_event) rescue StandardError => e # Don't let this error bubble up and stop the agent reporting, with resending triggered. logger.error('[Reporter] Error while reporting configuration', error: e, event_type: event&.cs__class) end def status @_status ||= Contrast::Agent::Reporting::ConnectionStatus.new end def response_handler @_response_handler ||= Contrast::Agent::Reporting::ResponseHandler.new end def diagnostics @_diagnostics ||= Contrast::Config::Diagnostics::Monitor.new(Contrast::LOGGER.path) end # Compress data with Zlib # # @param event [Contrast::Agent::Reporting::ReportingEvent] # @param level [Integer] compression level # @param strategy [Integer] compression strategy # @return [String] compressed data def compress_event event, level = Zlib::DEFAULT_COMPRESSION, strategy = Zlib::DEFAULT_STRATEGY compressed_data = StringIO.new.set_encoding(STRING_ENCODING) gzip = Zlib::GzipWriter.new(compressed_data, level, strategy) gzip.write(event.event_json) gzip.close gzip = nil compressed_event = compressed_data.string.dup compressed_data.close compressed_data = nil compressed_event ensure gzip&.close compressed_data&.close compressed_event end # Reads compressed data # # @param compresed_data [String] def decompress compresed_data Zlib::GzipReader.wrap(StringIO.new(compresed_data), &:read) end ############## # Forwarders # ############## def sleep? response_handler.sleep? end def put_to_sleep response_handler.put_to_sleep end def timeout response_handler.timeout end def mode response_handler.mode end def enter_run_mode response_handler.enter_run_mode end def wake_up response_handler.wake_up end end end end end