# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/reporting/reporting_utilities/response' require 'contrast/agent/reporting/reporting_utilities/response_handler_utils' require 'contrast/agent/reporting/reporting_utilities/response_handler_mode' require 'contrast/components/logger' require 'json' module Contrast module Agent module Reporting # This class will facilitate the Response capture and analysis functionality. class ResponseHandler include Contrast::Components::Logger::InstanceMethods include Contrast::Agent::Reporting::ResponseHandlerUtils # 15 min TIMEOUT = 900.cs__freeze # Process the response from TS # # @param response [Net::HTTPResponse, nil] # @param event [Contrast::Agent::Reporting::ReportingEvent] The event sent to TeamServer. # @return response [Net::HTTPResponse, nil] def process response, event logger.debug('[Reporter] Received a response') return if event&.cs__is_a?(Contrast::Agent::Reporting::Finding) return unless analyze_response?(response) # Handle the response body and obtain server_features or app_settings report_response = convert_response(response, event) return unless report_response # Update Server Features and Application Settings to provide current agent settings update_agent_settings(report_response) # Process any reactions, including message logging and shutting down update_reaction(report_response) update_ruleset(report_response) logger.trace('Agent settings updated in response to TeamServer', protect_on: ::Contrast::PROTECT.enabled?, assess_on: ::Contrast::ASSESS.enabled?) response rescue StandardError => e logger.error('[Reporter] Unable to process response from TeamServer', e) nil end # If sleep is true puts reporting service to sleep. def sleep? @_sleep = wake_up if @_sleep.nil? @_sleep end # For how long the agent should wait until retry # to send message # # @return timeout [Integer] the time for reporter # to be suspended. def timeout @_timeout ||= TIMEOUT end def mode @_mode ||= Contrast::Agent::Reporting::ResponseHandlerMode.new end # Puts the reporting service to sleep def put_to_sleep @_sleep = true end # Wakes the reporting service def wake_up @_sleep = false end # Suspend the reporter and try again in time. If not set # the timeout is set to 15 min As default: # # @param message [String] Message to log. # @param timeout [Integer,nil] The timeout to wait and retry after. # @param error_message [String, nil] Error message if any received. # @param event [Contrast::Agent::Reporting::ReportingEvent, nil] The event sent to TeamServer if set # @param log_error [Boolean] Whether to log the error or not. # @param startup [Boolean] Whether this is a startup error or not. def suspend_reporting message, timeout, error_message, event: nil, log_error: true, startup: false @_timeout = timeout || Contrast::Agent::Reporting::ResponseHandler::TIMEOUT if log_error log_error_msg(message, timeout: @_timeout, error_message: error_message || 'none', event_id: event.nil? ? 'none' : event&.__id__, event_type: event.nil? ? 'none' : event&.cs__class&.cs__name) end if startup logger.info(message, connection_error: error_message, client: Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME, event_id: event&.__id__, timeout: timeout, event_type: event&.cs__class&.cs__name) end put_to_sleep end private # Handles the errors code received from TS and takes appropriate action. # If we are here the response.code is an error that needs handling [4XX] # # @param response [Net::HTTPResponse] def handle_error response case response&.code when ERROR_CODES[:message_not_sent] handle_response_errors(response, UNSUCCESSFULLY_RECEIVED_MSG, mode.running) when ERROR_CODES[:access_forbidden] handle_response_errors(response, FORBIDDEN_MSG, mode.running) when ERROR_CODES[:access_forbidden_no_action] handle_response_errors(response, FORBIDDEN_NO_ACTION_MSG, mode.running) when ERROR_CODES[:application_do_not_exist] handle_response_errors(response, APP_NON_EXISTENT_MSG, mode.disabled) when ERROR_CODES[:unprocessable_entity] handle_response_errors(response, UNPROCESSABLE_ENTITY_MSG, mode.disabled) when ERROR_CODES[:too_many_requests] handle_response_errors(response, RETRY_AFTER_MSG, mode.resending) else logger.error('Unable to execute agent post_call') end end # Log what we've received. # # @param message [String] Message to log # @param info_hash [Hash] information about the context to log. def log_error_msg message, info_hash logger.error(message, info_hash) end end end end end