# 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 'fileutils' require 'json' module Contrast module Agent module Reporting # This class will facilitate the Audit functionality and it will be # controlled from the configuration classes class Audit include Contrast::Components::Logger::InstanceMethods attr_reader :path_for_requests, :path_for_responses def initialize generate_paths if enabled? && Contrast::CONTRAST_SERVICE.use_agent_communication? end # This method will be handling the auditing of the requests and responses we send to SpeedRacer. If the audit # feature is enabled, we'll log to file the request and/or response protobuf objects. # # @param event [Contrast::Api::Dtm|Contrast::Agent::Reporting::ReportingEvent] One of the DTMs valid for the # event field of Contrast::Api::Dtm::Message|Contrast::Agent::Reporting::ReportingEvent # @param response_data [Contrast::Api::Settings::AgentSettings,nil] def audit_event event, response_data = nil return unless ::Contrast::API.request_audit_requests? || ::Contrast::API.request_audit_responses? type = event.cs__respond_to?(:file_name) ? event.file_name : event.cs__class.cs__name.to_s.downcase if ::Contrast::API.request_audit_requests? data = if event.cs__class < Contrast::Agent::Reporting::ReportingEvent event.to_controlled_hash.to_json else # TODO: RUBY-1438 -- remove event.to_s end log_data :request, type, data if data end return unless ::Contrast::API.request_audit_responses? data = response_data.to_s || event.http_response.try(:body) || 'There is no available response' log_data :response, type, data end private # This method will proceed with passing the data with to the writing method # @param type [Symbol] This is the type of the file /:request, :response/ # @param data_type[String] DTM type String representation # @param data[String] String representation if the logged data def log_data type, data_type, data = nil return unless enabled? return unless Contrast::CONTRAST_SERVICE.use_agent_communication? write_to_file type, data_type, data end # This method will be actually writing to the file # @param type [Symbol] This is the type of the file /:request, :response/ # @param data_type [String] Data type /Activity/Finding../ # @param data [any] The data to be written to the file def write_to_file type, data_type, data = nil time = Time.now.to_i destination = type == :request ? path_for_requests : path_for_responses # If the feature is disabled or we have yet to create the directory structure, then we could have a nil # destination. In that case, take no action return unless destination filename = "#{ time }-#{ Thread.current.object_id }-#{ data_type.gsub('::', '_') }-teamserver.json" filepath = File.join(destination, filename) # Here is use append mode, because of a slightly possibility of overwriting an existing file File.open(filepath, 'a') do |f| if data_type.include?('reporting') f.write(Contrast::Utils::StringUtils.force_utf8(data)) else f.write({ data_type: Contrast::Utils::StringUtils.force_utf8(data) }.to_json) end end rescue StandardError => e logger.warn('Saving audit failed', e: e) end # Here we will generate the directories for the requests and responses def generate_paths message_directories = File.expand_path(path_to_audits) FileUtils.mkdir_p(message_directories) unless Dir.exist?(message_directories) requests_destination = File.expand_path(File.join(message_directories, '/requests')) responses_destination = File.expand_path(File.join(message_directories, '/responses')) Dir.mkdir(requests_destination) if enabled_for_requests? && !Dir.exist?(requests_destination) Dir.mkdir(responses_destination) if enabled_for_responses? && !Dir.exist?(responses_destination) @path_for_requests ||= requests_destination if enabled_for_requests? @path_for_responses ||= responses_destination if enabled_for_responses? rescue StandardError => e logger.warn('Generating the paths failed with: ', e: e) end # Retrieves the configuration value if the request audit is enabled # @return [Boolean] def enabled? ::Contrast::API.request_audit_enable? end # The boolean values for the requests and the responses should be taken under # consideration only if it's in combination with enabled # So in order for us to actually audit the requests, we need: # - enabled? -> ture and enabled_for_requests? -> true # The same is for the responses # # # Retrieve the configuration value if the audit for requests is enabled # @return [Boolean] def enabled_for_requests? ::Contrast::API.request_audit_requests? end # Retrieve the configuration value if the audit for responses is enabled # @return [Boolean] def enabled_for_responses? ::Contrast::API.request_audit_requests? end # Retrieve the configuration value for the path of the audits # The value will be read from the configuration yml # but if it isn't defined any - here will be returned the default path from # Contrast::Config::RequestAuditConfiguration::DEFAULT_PATH # @return [String] def path_to_audits ::Contrast::API.request_audit_path end end end end end