# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'json' require 'net/http' require 'contrast/components/logger' require 'contrast/utils/net_http_base' require 'contrast/api/communication/connection_status' require 'contrast/agent/reporting/reporting_utilities/response_handler' require 'contrast/agent/reporting/reporting_utilities/reporter_client_utils' require 'contrast/agent/reporting/reporting_utilities/endpoints' require 'contrast/agent/reporting/reporting_utilities/headers' require 'contrast/agent/reporting/reporting_events/server_settings' require 'contrast/agent/reporting/reporting_events/application_settings' require 'contrast/config/diagnostics' module Contrast module Agent module Reporting # This class creates a Net::HTTP client and initiates a connection to the provided result # @attr_reader headers [Contrast::Agent::Reporting::Headers] class ReporterClient < Contrast::Utils::NetHttpBase attr_reader :headers include Contrast::Agent::Reporting::Endpoints include Contrast::Agent::Reporting::ReporterClientUtils include Contrast::Agent::Reporting::ResponseHandlerUtils include Contrast::Components::Logger::InstanceMethods # @return [Array] events that may result in configuration changes RECORDABLE_EVENTS = [ Contrast::Agent::Reporting::ServerSettings, Contrast::Agent::Reporting::ApplicationSettings, Contrast::Agent::Reporting::AgentStartup, Contrast::Agent::Reporting::ApplicationStartup ].cs__freeze SERVICE_NAME = 'Reporter' REPORT_CONFIG_WHEN = %w[200 304].cs__freeze def initialize @headers = Contrast::Agent::Reporting::Headers.new super() end # This method initializes the Net::HTTP client we'll need. it will validate # the connection and make the first request. If connection is valid and response # is available then the open connection is returned. # # @return [Net::HTTP, nil] Return open connection or nil def initialize_connection # for this client we would use proxy and custom certificate file if available super(SERVICE_NAME, Contrast::API.api_url, use_proxy: true, use_custom_cert: true) end # Start the client for first time and sent startup event # # @param connection [Net::HTTP] open connection def startup! connection return if status.startup_messages_sent? send_agent_startup(connection) end # Check event type and send it to appropriate TS endpoint # # @param event [Contrast::Agent::Reporting::ReportingEvent] The event to send to TeamServer. Really a # child of the ReportingEvent rather than a literal one. # @param connection [Net::HTTP] open connection # @return response [Net::HTTP::Response, nil] response from TS if no response def send_event event, connection return unless connection logger.debug('[Reporter] Preparing to send reporting event', event_class: event.cs__class) request = build_request(event) response = connection.request(request) audit&.audit_event(event, response) if ::Contrast::API.request_audit_enable process_settings_response(response, event) record_status(response, event) record_configuration(response, event) process_preflight_response(event, response, connection) response rescue StandardError => e handle_error(event, e) end # This is going to populate the config status value in the diagnostics json # # @param response [Contrast::Agent::Reporting::Response, nil] # @param event [Contrast::Agent::Reporting::ReportingEvent] def record_status response, event return unless response return unless event&.cs__class == Contrast::Agent::Reporting::AgentStartup || event&.cs__class == Contrast::Agent::Reporting::ApplicationStartup diagnostics.config.determine_config_status(response) nil end # Write effective config to file: # If we are here the create and server messages are sent and the code received is # 200 or 304. In case of 304 there will be no new settings and we can write current ones. # This is done on every settings request. # # @param response [Contrast::Agent::Reporting::Response, nil] # @param event [Contrast::Agent::Reporting::ReportingEvent] def record_configuration response, event return unless response return unless REPORT_CONFIG_WHEN.include?(response_handler.last_response_code) return unless RECORDABLE_EVENTS.include?(event&.cs__class) logger.info('[Reporter Diagnostics] last response code:', response_code: response_handler.last_response_code) diagnostics.write_to_file status.server_response_success! end def status @_status ||= Contrast::Api::Communication::ConnectionStatus.new end def response_handler @_response_handler ||= Contrast::Agent::Reporting::ResponseHandler.new end def diagnostics @_diagnostics ||= Contrast::Agent::DiagnosticsConfig::Diagnostics.new(Contrast::LOGGER.path) end def sleep? response_handler.sleep? end def timeout response_handler.timeout end def mode response_handler.mode end def reset_mode response_handler.reset_mode end def wake_up response_handler.wake_up end end end end end