# 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' require 'contrast/components/scope' require 'contrast/agent/reporting/reporting_utilities/reporter_client' require 'contrast/agent/reporting/reporting_utilities/endpoints' module Contrast module Agent module Reporting # This module holds utilities required by the reporting service client module ReporterClientUtils include Contrast::Components::Logger::InstanceMethods include Contrast::Components::Scope::InstanceMethods include Contrast::Agent::Reporting::Endpoints private # This method will build headers of the request required for TS communication # # @param request [Net::HTTPRequest] def build_headers request app_version = @headers.app_version request['API-Key'] = @headers.api_key request['Application-Language'] = @headers.app_language request['Application-Name'] = @headers.app_name request['Application-Path'] = @headers.app_path request['Application-Version'] = app_version if app_version request['Authorization'] = @headers.authorization request['Content-Type'] = @headers.content_type request['Server-Name'] = @headers.server_name request['Server-Path'] = @headers.server_path request['Server-Type'] = @headers.server_type request['X-Contrast-Agent'] = @headers.agent_version request['X-Contrast-Header-Encoding'] = @headers.encoding request end # log the event sent immediately # # @param event [Contrast::Api::Dtm,Contrast::Agent::Reporting::ReportingEvent] 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("#{ Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME } immediately sending event.", event_id: event.__id__, event_type: event.cs__class.cs__name) end # Handles standard error case, logs and set status for failure # # @param event [Contrast::Api::Dtm, Contrast::Agent::Reporting::ReportingEvent] # One of the DTMs valid for the event field of Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity # @param error_msg [StandardError] # @return nil [NilClass] to be passed as response def handle_error event, error_msg status.failure! logger.error('Unable to send message.', error_msg, client: Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME, event_id: event&.__id__, event_type: event&.cs__class&.cs__name) nil end # Handles response processing and sets status # # @param response [Net::HTTP::Response] # @return response [Net::HTTP::Response] def process_response response response_handler.process response logger.debug('Successfully sent startup messages to service.') status.success! response end # build the request headers and assign endpoint # # @param event event [Contrast::Api::Dtm::AgentStartup, Contrast::Agent::Reporting::ReportingEvent] # One of the DTMs valid for the event field of Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity # @return [Net::HTTP::Post,Net::HTTP::Put] def build_request event with_contrast_scope do request = if event.cs__is_a?(Contrast::Agent::Reporting::ReportingEvent) case event.event_method when :PUT Net::HTTP::Put.new(event.event_endpoint) when :POST Net::HTTP::Post.new(event.event_endpoint) end else # TODO: RUBY-1438 -- remove Net::HTTP::Put.new(Contrast::Agent::Reporting::Endpoints::NG_ENDPOINTS[:agent_startup]) end build_headers(request) if event.cs__is_a?(Contrast::Agent::Reporting::ReportingEvent) event.attach_headers(request) request.body = event.to_controlled_hash.to_json else request.body = event.to_json end request end end # Send Agent Startup event # # @param connection [Net::HTTP] open connection # @return response [Net::HTTP::Response] response from TS def send_agent_startup connection logger.debug('Preparing to send startup messages') startup = Contrast::APP_CONTEXT.build_agent_startup_message request = build_request(startup) response = connection.request(request) process_response(response) rescue StandardError => e handle_error(startup, e) end # Sent different events to different endpoints # # @param event [Contrast::Api::Dtm,Contrast::Agent::Reporting::ReportingEvent] Main reporting event, all events # inherit it # @param connection [Net::HTTP] open connection def send_events event, connection request = build_request(event) response = connection.request(request) process_response(response) rescue StandardError => e handle_error(event, e) end # Eventually here we'll handle more response types and etc # @param event [Contrast::Agent::Reporting::Preflight] the preflight we handle here # @param response [Net::HTTP::Response,nil] The response we handle and read from # @param connection [Net::HTTP] open connection def handle_response event, response, connection return unless event || response || connection return unless event.cs__is_a?(Contrast::Agent::Reporting::Preflight) preflight_message = event.messages[0] # for handling multiple findings # we'll only extract the indexes without * # findings_to_return = response.body.split(',').delete_if { |el| el.include?('*') } # after that we'll do some magic and return them the same way we do for corresponding_finding corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message.hash_code) return unless corresponding_finding send_event corresponding_finding, connection, true nil end end end end end