# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/thread/worker_thread' require 'contrast/agent/reporting/report' require 'contrast/components/logger' require 'contrast/agent/reporting/reporting_events/agent_startup' require 'contrast/agent/telemetry/exception' module Contrast module Agent # This module will hold everything essential to reporting to TeamServer class Reporter < WorkerThread # rubocop:disable Metrics/ClassLength include Contrast::Components::Logger::InstanceMethods include Contrast::Utils::ObjectShare # How many tries to reconnect the Reporter should make. RETRY_ATTEMPTS = 10 MAX_QUEUE_SIZE = 1000 class << self # check if we can report to TS # # @return[Boolean] true if bypass is enabled, or false if bypass disabled def enabled? @_enabled = ::Contrast::AGENT.enabled? if @_enabled.nil? @_enabled end end def client @_client ||= Contrast::Agent::Reporting::ReporterClient.new end def connection @_connection ||= client.initialize_connection end def start_thread! return unless attempt_to_start? return if running? @connection_attempts = 0 client.startup!(connection) @_thread = Contrast::Agent::Thread.new do logger.debug('[Reporter] Starting background Reporter thread.') loop do next unless connected? break unless attempt_to_start? process_event(queue.pop) rescue StandardError => e logger.debug('[Reporter] thread could not process because of:', e) end end end # Suspend the Reporter and try sending the event after the timeout. # The timeout is either default 15 min or received via TS response. # # @param event [Contrast::Agent::Reporting::ReportingEvent] Freshly pop-ed event. def handle_resend event sleep(client.timeout) if client.sleep? # Retry once than discard the event. This is trigger on too many events of # same kind error. client.send_event(event, connection) if client.mode.status == client.mode.resending client.mode.reset_mode client.wake_up end # @param event [Contrast::Agent::Reporting::ReportingEvent] def send_event event if ::Contrast::AGENT.disabled? logger.warn('[Reporter] Attempted to queue event with Agent disabled', caller: caller, event: event) return end return unless event if queue.size >= MAX_QUEUE_SIZE Contrast::Agent::Telemetry::Base.enabled? && Contrast::Agent.thread_watcher.telemetry_queue. send_event(queue_limit_telemetry_event) return end queue << event end # Use this to bypass the messaging queue and leave response processing to the caller. # Do not use this in separate threads since this will create a race condition, when # two threads need to use the same OpenSSL::SSL::SSLSocket raising frozen error. # # @param event [Contrast::Agent::Reporting::ReportingEvent] # @return [Net::HTTPResponse, nil] def send_event_immediately event if ::Contrast::AGENT.disabled? logger.warn('[Reporter] attempted to send event immediately with Agent disabled', caller: caller, event: event) return end return unless event client.send_event(event, connection) rescue StandardError => e logger.error('[Reporter] Could not send message to TeamServer from reporting queue.', e) end def delete_queue! @_queue&.clear @_queue&.close @_queue = nil end def stop! return unless running? super delete_queue! end private def queue @_queue ||= Queue.new end # TODO: RUBY-99999 # The client and connection are being used in multiple threads/ concurrently, and that's not okay. We need # to figure out why that is and lock it so that it isn't. # # @return [Boolean] def connected? if client && connection # Try to resend startup messages now with connection: client.startup!(connection) unless client.status.startup_messages_sent? return true end logger.debug('[Reporter] No client/connection; sleeping...') @connection_attempts += 1 if @connection_attempts >= RETRY_ATTEMPTS logger.debug('[Reporter] shutting down..') Contrast::AGENT.disable! end sleep(5) unless Contrast::AGENT.disabled? false end # @param event [Contrast::Agent::Reporting::ReportingEvent] def process_event event client.send_event(event, connection) handle_resend(event) if client.mode.status == client.mode.resending rescue StandardError => e logger.error('[Reporter] Could not send message to TeamServer from reporting queue.', e) end # @return [Contrast::Agent::Telemetry::Exception::Event] def queue_limit_telemetry_event message_exception = Contrast::Agent::Telemetry::Exception::MessageException.build( 'String', "[Reporter] Maximum queue size (#{ MAX_QUEUE_SIZE }) reached for reporting events", nil, stack_frame) message = Contrast::Agent::Telemetry::Exception::Message.build({}, [message_exception]) Contrast::Agent::Telemetry::Exception::Event.new(message) end def stack_frame stack_trace = caller_locations(20, 20) stack_frame_type = if stack_trace.nil? || stack_trace[1].nil? 'none' else stack_trace[1].path.delete_prefix(Dir.pwd) end stack_frame_function = stack_trace.nil? || stack_trace[1].nil? ? 'none' : stack_trace[1].label Contrast::Agent::Telemetry::Exception::StackFrame.build(stack_frame_function, stack_frame_type, nil) end end end end