# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'logger' require 'singleton' require 'contrast/extension/module' require 'contrast/logger/application' require 'contrast/logger/format' require 'contrast/logger/request' require 'contrast/logger/time' require 'contrast/logger/log' require 'contrast/components/config' require 'contrast/utils/log_utils' module Contrast # Used as a wrapper around our logging. The module option specifically adds in a new method for error that raises the # logged exception, used in testing so that we can see if anything unexpected happens without it being swallowed # while still providing safe options for customers. module Logger # For development set following env var to raise logged exceptions instead of just logging. if ENV['CONTRAST__AGENT__RUBY_MORE_COWBELL'] ::Logger.class_eval do alias_method :cs__error, :error alias_method :cs__warn, :warn def error *args, **kwargs if kwargs.empty? cs__error(*args) else cs__error(*args, **kwargs) end args.each { |arg| raise arg if arg && arg.cs__class < Exception } end end end # This is the CEF Logger implementation. It uses the default ::Logger. class CEFLog include Singleton include ::Contrast::Utils::LogUtils include ::Contrast::Utils::CEFLogUtils attr_reader :previous_path, :previous_level def initialize build_logger end # Given new settings from TeamServer, update our logging to use the new file and level, assuming they weren't # set by local configuration. # # @param log_level [String] the level at which to log, as provided by TeamServer settings def build_logger log_level = nil current_level_const = find_valid_level(log_level) level_change = current_level_const != previous_level # don't needlessly recreate logger return if @cef_logger && !level_change @previous_level = current_level_const @_cef_logger = build(path: DEFAULT_CEF_NAME, level_const: current_level_const) # If we're logging to a new path, then let's start it w/ our helpful # data gathering messages # log_update if path_change rescue StandardError => e # rubocop:disable Rails/Output if @_cef_logger @_cef_logger.error('Unable to process update to LoggerManager.', e) else puts 'Unable to process update to LoggerManager.' raise e if ENV['CONTRAST__AGENT__RUBY_MORE_COWBELL'] puts e.message puts e.backtrace.join("\n") end # rubocop:enable Rails/Output end def cef_logger @_cef_logger end def log msg, level = @_cef_logger.level case level when ::Logger::Severity::INFO @_cef_logger.info(msg) when ::Logger::Severity::ERROR @_cef_logger.error(msg) when ::Logger::Severity::WARN @_cef_logger.warn(msg) when ::Logger::Severity::FATAL @_cef_logger.fatal(msg) else @_cef_logger.debug(msg) end end def virtual_patch_message patch, outcome message = "Virtual Patch #{ patch.fetch(:name, '') } - #{ patch[:uuid] } was triggered by this request." log [message, patch, outcome], ::Logger::Severity::DEBUG end def bot_blocking_message matching_bot, outcome message = "User agent #{ matching_bot[:user_agent] } matched the disallowed value #{ matching_bot[:bot] }" log [message, matching_bot, outcome], ::Logger::Severity::DEBUG end def ip_denylisted_message remote_ip, block_entry, outcome message = "IP Address #{ remote_ip } matched the disallowed value" \ "#{ block_entry[:ip] } in the IP Blacklist #{ block_entry[:uuid] }" log [message, block_entry, outcome], ::Logger::Severity::DEBUG end def successful_attack rule_id, outcome, input_type = nil, input_value = nil if input_type.present? && input_value.present? successful_attack_with_input = "#{ input_type } had a value that successfully exploited" \ "#{ rule_id } - #{ input_value }" log [successful_attack_with_input, rule_id, outcome], ::Logger::Severity::WARN else successful_attack_wo_input = "An effective attack was detected against #{ rule_id }" log [successful_attack_wo_input, rule_id, outcome], ::Logger::Severity::WARN end end def ineffective_attack rule_id, outcome, input_type = nil, input_value = nil if input_type.present? && input_value.present? ineffective_attack_with_input = "#{ input_type } had a value that matched a signature for, " \ "but did not successfully exploit #{ rule_id } - #{ input_value }" log [ineffective_attack_with_input, rule_id, outcome], ::Logger::Severity::WARN else ineffective_attack_wo_input = "An unsuccessful attack was detected against #{ rule_id }" log [ineffective_attack_wo_input, rule_id, outcome], ::Logger::Severity::WARN end end # newer - currently not in the agent, currently is a probe for us def suspicious_attack rule_id, outcome, input_type = nil, input_value = nil if input_type.present? && input_value.present? suspicious_attack_with = "#{ input_type } included a potential attack value that was detected" \ "as suspicious using #{ rule_id } - #{ input_value }" log [suspicious_attack_with, rule_id, outcome], ::Logger::WARN elsif input_value.present? suspicious_attack_without = "Suspicious activity indicates a potential attack using #{ rule_id }" log [suspicious_attack_without, rule_id, outcome], ::Logger::WARN end end end end end