# 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' require 'contrast/agent/reporting/client/interface' 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::Client::Interface.new end def start_thread! return unless attempt_to_start? return if running? @connection_attempts = 0 @_thread = Contrast::Agent::Thread.new do logger.debug('[Reporter] Starting background Reporter thread.') client.startup loop do break unless attempt_to_start? next unless connected? 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 client.handle_resend(event) 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 return unless connected? client.send_event(event) 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 # @return [Boolean] def connected? return true if client.connected? 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) handle_resend(event) if client.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 stack_frame_type = Contrast::Agent::Telemetry::Exception::Obfuscate.obfuscate_path(stack_frame_type) Contrast::Agent::Telemetry::Exception::StackFrame.build(stack_frame_function, stack_frame_type, nil) end end end end