# 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' 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 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::CONTRAST_SERVICE.use_agent_communication? 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('Starting background Reporter thread.') loop do next unless connected? next unless app_create_complete? 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('Attempted to queue event with Agent disabled', caller: caller, event: event) return end return unless event 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('Could not send message to TeamServer from Reporter 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('No client/connection; sleeping', client: client, connection: connection) sleep(5) false end # Unless we're in bypass mode, we need to make sure the service has started and built the application on # TeamServer since we're doing a split style here. # # @return [Boolean] def app_create_complete? return true if Contrast::CONTRAST_SERVICE.use_agent_communication? return true if Contrast::Agent.messaging_queue&.speedracer&.status&.startup_messages_sent? logger.debug('Service startup incomplete; Application may not be created; sleeping') 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('Could not send message to TeamServer from Reporter queue.', e) end end end end