# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/logger' module Contrast module Api module Communication # Wraps all connection data to SpeedRacer. SpeedRacer, also known as the Contrast Service, is a standalone # executable that sits between the Agent and TeamServer. It handles converting our Protobuf messages into a # format consumable by TeamServer. The Agent requires a SpeedRacer process to be running somewhere to which it # can connect, as specified by the user configuration, in order to function. class Speedracer include Contrast::Api::Communication::ServiceLifecycle include Contrast::Components::Logger::InstanceMethods attr_reader :status, :response_processor, :socket_client, :ensure_running def initialize @status = Contrast::Api::Communication::ConnectionStatus.new @socket_client = Contrast::Api::Communication::SocketClient.new @response_processor = Contrast::Api::Communication::ResponseProcessor.new @ensure_running = Mutex.new end # If there is not a SpeedRacer at the location specified by the configuration of this Agent and the Agent is # set such that it should manage one, start up a new child process to run the SpeedRacer executable. If a # connection has not already been made to that process, after starting it, this method will also send the # messages necessary to create a context for this Agent process in the SpeedRacer as well as trigger # SpeedRacer's sending of startup messages which will return features and settings from TeamServer. # # This operation is synchronous and blocking, so it will only happen one at a time per process and will halt # Thread execution here until completion. def ensure_startup! return if status.connected? ensure_running.synchronize do if ::Contrast::CONTRAST_SERVICE.use_bundled_service? logger.info('Attempting to start local service') unless attempt_local_service_startup logger.error('Failed to start local service') return end end unless status.startup_messages_sent? || Contrast::CONTRAST_SERVICE.unnecessary? startup_responses = send_initialization_messages return false unless startup_responses startup_responses.each { |response| response_processor.process(response) } end end end # Send the given Event to SpeedRacer, returning the response from it. This response will either be new settings # if anything's changed in TeamServer meaning the Agent needs to replace its current settings or there is # Protect analysis information or nil if the current Agent settings do not need updating. # # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of # Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity # @return [Contrast::Api::Settings::AgentSettings,nil] def return_response event send_to_speedracer(event) do |response| return response end end # Send the given Event to SpeedRacer and pass the result to our Contrast::Api::Communication::ResponseProcessor # as there is no immediate action required from sending this Event. # # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of # Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity def process_internally event send_to_speedracer(event) do |response| response_processor.process(response) end end private # Ensure there is a running SpeedRacer and then send the given Event to it. It is necessary to ensure the # SpeedRacer is running as, if the process has crashed or restarted, we must rebuild our context there. # # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of # Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity # @return [Contrast::Api::Settings::AgentSettings, nil] def send_to_speedracer event ensure_startup! logger.debug_with_time(event.cs__class.cs__name) do response = socket_client.send_one(event) status.success! yield(response) end rescue StandardError => e status.failure! logger.error('Unable to send message.', e, event_id: event.__id__, event_type: event.cs__class.cs__name) nil end # Send those messages which are required to build a context for this Agent process on SpeedRacer as well as # report server and application startup to TeamServer. With these messages, the SpeedRacer will be able to # retrieve settings from TeamServer and provide those for the Agent to complete its initialization. def send_initialization_messages agent_startup_msg = ::Contrast::APP_CONTEXT.build_agent_startup_message logger.debug('Preparing to send startup messages') # 1 initial attempt, + 3 potential retries. # The agent-service-api REQUIREMENTS.md spec mandates this #. 4.times do log_send_event(agent_startup_msg) next unless (agent_response = socket_client.send_one(agent_startup_msg)) # Connection was successful; send app create with the resolved features. app_startup_msg = ::Contrast::APP_CONTEXT.build_app_startup_message log_send_event(app_startup_msg) app_response = socket_client.send_one(app_startup_msg) status.success! logger.debug('Successfully sent startup messages to service.') return [agent_response, app_response] end status.failure! logger.error('Could not send agent startup message context') nil rescue StandardError => e logger.error('Could not build service context', e) status.failure! nil end # Log the startup message we're sending to SpeedRacer # # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of # Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity def log_send_event event logger.debug('Immediately sending event.', event_id: event.__id__, event_type: event.cs__class.cs__name) end end end end end