# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/worker_thread' require 'contrast/agent/reporting/report' require 'contrast/components/logger' require 'contrast/agent/reporting/reporting_events/agent_startup' require 'contrast/agent/telemetry/events/exceptions/telemetry_exceptions' require 'contrast/agent/telemetry/events/exceptions/obfuscate' module Contrast module Agent # This module will hold everything essential to reporting to TeamServer class Reporter < WorkerThread include Contrast::Components::Logger::InstanceMethods include Contrast::Utils::ObjectShare 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 if running? client.startup!(connection) @_thread = Contrast::Agent::Thread.new do logger.debug('[Reporter] Starting background Reporter thread.') loop do 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 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 # # @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? return true if client && connection logger.debug('[Reporter] No client/connection; sleeping...', client: client, connection: connection) sleep(5) 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::TelemetryException::Event] def queue_limit_telemetry_event message_exception = Contrast::Agent::Telemetry::TelemetryException::MessageException.build( 'String', "[Reporter] Maximum queue size (#{ MAX_QUEUE_SIZE }) reached for reporting events", nil, stack_frame) message = Contrast::Agent::Telemetry::TelemetryException::Message.build({}, [message_exception]) Contrast::Agent::Telemetry::TelemetryException::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::TelemetryException::StackFrame.build(stack_frame_function, stack_frame_type, nil) end end end end