# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/telemetry/events/exceptions/telemetry_exceptions' require 'contrast/agent/telemetry/events/exceptions/obfuscate' module Contrast module Logger # Our decorator for the Ougai logger allowing for the catching, creating and saving Telemetry exceptions module AliasedLogging ALIASED_WARN = 'warn'.cs__freeze ALIASED_ERROR = 'error'.cs__freeze ALIASED_FATAL = 'fatal'.cs__freeze # @param message [String] The message to log. Use default_message if not specified. # @param exception [Exception] The exception or the error # @param data [Object] Any structured data def warn message = nil, exception = nil, data = nil, &block # build Telemetry Exclusion build_exception(ALIASED_WARN, message, exception, data) super(message, exception, data, &block) end # @param message [String] The message to log. Use default_message if not specified. # @param exception [Exception] The exception or the error # @param data [Object] Any structured data def error message = nil, exception = nil, data = nil, &block # build Telemetry Exclusion build_exception(ALIASED_ERROR, message, exception, data) super(message, exception, data, &block) end # @param message [String] The message to log. Use default_message if not specified. # @param exception [Exception] The exception or the error # @param data [Object] Any structured data def fatal message = nil, exception = nil, data = nil, &block # build Telemetry Exclusion build_exception(ALIASED_FATAL, message, exception, data) super(message, exception, data, &block) end private # @param type [ALIASED_FATAL, ALIASED_ERROR, ALIASED_WARN] the type of error, used to indicate the function used # for logging # @param message [String] the exception message # @param exception [Exception] The exception or error # @param data [Object] Any structured data def build_exception type, message = nil, exception = nil, data = nil stack_trace = wrapped_caller_locations caller_idx = stack_trace&.find_index { |stack| stack.to_s.include?(type) } || 0 # The caller_stack is the method in which the error occurred, so has to be above this method caller_idx += 1 caller_frame = stack_trace[caller_idx] stack_frame_type = caller_frame.path.delete_prefix(Dir.pwd) stack_frame_function = caller_frame.label key = "#{ stack_frame_type }|#{ stack_frame_function }|#{ message }" if Contrast::TELEMETRY_EXCEPTIONS[key] Contrast::TELEMETRY_EXCEPTIONS.increment(key) return end return if Contrast::TELEMETRY_EXCEPTIONS.exception_limit? message_exception_type = exception ? exception.cs__class.to_s : stack_frame_type.split('/').last event_message = create_message(stack_frame_function, stack_frame_type, message_exception_type, data, exception, message) build_stack(event_message, stack_trace, caller_idx) TELEMETRY_EXCEPTIONS[key] = event_message rescue StandardError => e debug('[Telemetry] Unable to report exception', e) end # @return [Contrast::Agent::Telemetry::TelemetryException::Event] def create_message stack_frame_function, stack_frame_type, message_exception_type, data, exception, message message_for_exception = if exception exception.cs__respond_to?(:message) ? exception.message : exception else message end module_name = message_exception_type ? message_exception_type.split('::')[0] : nil stack_frame = Contrast::Agent::Telemetry::TelemetryException::StackFrame.build(stack_frame_function, stack_frame_type, module_name) message_exception = Contrast::Agent::Telemetry::TelemetryException::MessageException.build( message_exception_type, message_for_exception, module_name, stack_frame) tags = if data data else exception.cs__is_a?(Hash) ? exception : {} end message = Contrast::Agent::Telemetry::TelemetryException::Message.build(tags, [message_exception]) Contrast::Agent::Telemetry::TelemetryException::Event.new(message) rescue ArgumentError => e debug('[Telemetry] TelemetryException failed from aliased logging with: ', e) end # Convert the given caller_stack from the event into the exception StackFrame format for reporting, appending it # to the Exception wrapped in the Event. # # @param event_message [Contrast::Agent::Telemetry::TelemetryException::Event] # @param caller_stack [(Array, nil] # @param caller_idx [Integer] the starting location for the exception, which allows filtering out the logger code # from the stack def build_stack event_message, caller_stack, caller_idx = 0 return unless caller_stack event_exception = event_message.exceptions[0] event_exception_message = event_exception.exceptions[0] caller_stack.each_with_index do |caller, idx| next unless idx > caller_idx stack_frame = Contrast::Agent::Telemetry::TelemetryException::StackFrame.build(caller.label, caller.path.delete_prefix(Dir.pwd)) event_exception_message.push(stack_frame) end end # This is purely a wrapper around caller_locations used for testing # # @return [Array, nil] def wrapped_caller_locations caller_locations end end end end