# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/config/env_variables' require 'contrast/components/logger' require 'contrast/utils/requests_client' require 'contrast/agent/worker_thread' require 'contrast/utils/telemetry' module Contrast module Agent # This class will initialize and hold everything needed for the telemetry class Telemetry < WorkerThread include Contrast::Utils::RequestsClient include Contrast::Components::Logger::InstanceMethods # this is where we will send the data from the agents URL = 'https://telemetry.ruby.contrastsecurity.com/' # Suggested timeout after each send is to be 3 hours (10800 seconds) SUGGESTED_TIMEOUT = 10_800 class << self include Contrast::Components::Logger::InstanceMethods include Contrast::Config::EnvVariables def application_id @_application_id ||= begin id = nil mac = Contrast::Utils::Telemetry::Identifier.mac app_name = Contrast::Utils::Telemetry::Identifier.app_name id = mac + app_name if mac && app_name Digest::SHA2.new(256).hexdigest(id || '_' + SecureRandom.uuid) end end def instance_id @_instance_id ||= Digest::SHA2.new(256).hexdigest(Contrast::Utils::Telemetry::Identifier.mac || '_' + SecureRandom.uuid) end def enabled? @_enabled = telemetry_enabled? if @_enabled.nil? @_enabled end private def telemetry_enabled? opt_out_telemetry = return_value(:telemetry_opt_outs) return false if opt_out_telemetry.to_s.casecmp('true').zero? return false if opt_out_telemetry.to_s.casecmp('1').zero? # In case of connection error, do not create the background thread or queue, # as if the opt-out env var was set ip_opt_out_telemetry = Contrast::Utils::RequestsClient.initialize_connection(URL) if ip_opt_out_telemetry.nil? logger.warn('Connection was not established properly!!!') logger.warn('THE SERVICE IS GOING TO BE TERMINATED!!') return false end true end end def attempt_to_start? unless cs__class.enabled? logger.warn('Telemetry service is disabled!') return false end logger.debug('Attempting to start telemetry thread') unless running? true end def start_thread! return if running? # It is recommended that implementations send a single payload of # general metrics every 3 hours, starting from implementation startup. @_thread = Contrast::Agent::Thread.new do logger.debug('Starting background telemetry thread.') event = queue.pop begin logger.debug('This is the current processed event', event) res = Contrast::Utils::RequestsClient.send_request event, connection sleep_time = Contrast::Utils::RequestsClient.handle_response res sleep(sleep_time) unless sleep_time.nil? rescue StandardError => e logger.error('Could not send message to service from telemetry queue.', e) end sleep(SUGGESTED_TIMEOUT) end end 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 cs__class.enabled? logger.debug('Enqueued event for sending', event_type: event.cs__class) queue << event if event 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 end end end