# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/utils/object_share' require 'contrast/config/diagnostics/effective_config_value' require 'contrast/utils/duck_utils' module Contrast module Config module Diagnostics # Diagnostics tools to be included in config components. module Tools CHECK = 'd' class << self # Creates new config instances for each read config entry from the flat generated configs. # # @param flats [Array] of flatten configs produced by #flatten_settings # @param source [Boolean] flag to set the desired value class, it may be a effective or source value. # @return [Array] def to_config_values flats, source: false config_value_klass = if source Contrast::Config::Diagnostics::SourceConfigValue else Contrast::Config::Diagnostics::EffectiveConfigValue end settings = [] flats.each do |entry| entry.each do |key, value| efc_value = config_value_klass.new.tap do |config_value| config_value.canonical_name = Contrast::Utils::ObjectShare::CONTRAST_DOT + key config_value.key = key config_value.value = value_to_s(value) end settings << efc_value if efc_value end end settings end # Flattens out the read settings from file, env or contrast ui. # example: {"agent.polling.server_settings_ms"=>"50000"} # # @param data [Hash, nil] # @param path [String] where to look for settings. # @param config [Hash] symbolized config to fetch keys from. def flatten_settings data, path = [], config: Contrast::CONFIG.config.loaded_config return [] unless data data.each_with_object([]) do |(k, v), entries| if v.cs__is_a?(Hash) entries.concat(flatten_settings(v, path.dup.append(k.to_sym))) else entries << { "#{ path.join('.') }.#{ k }" => config.dig(*path, k).to_s } end end.flatten # rubocop:disable Style/MethodCalledOnDoEndBlock end # Recursively converts each value to string. # # @param value [Hash, nil] def value_to_s value return if value.nil? return value if value.cs__is_a?(String) value.each_with_object({}) do |(k, v), m| # rubocop:disable Style/HashTransformValues m[k] = if v.cs__is_a?(Hash) value_to_s(v) elsif v.cs__is_a?(Array) v.map(&:to_s) else v.to_s end end end end # Converts current configuration from array of values to effective config values class and appends them to # EffectiveConfig class. Must be used inside Config Components only. # # @param effective_config [Contrast::Config::Diagnostics::EffectiveConfig] # @param config_values [] array of the names of values. # @param canonical_prefix [String] starting of the path to config => api.proxy... # @param name_prefix [String] the name of the config prefix => contrast.api_key, contrast.url def add_effective_config_values effective_config, config_values, canonical_prefix, name_prefix return if config_values.to_s.empty? config_values.each do |config_value_name| Contrast::Config::Diagnostics::EffectiveConfigValue.new.tap do |new_effective_value| next if Contrast::Utils::DuckUtils.empty_duck?((config_value = send(config_value_name.to_sym))) fill_effective_value(new_effective_value, config_value, config_value_name, canonical_prefix, name_prefix) effective_config.values << new_effective_value rescue StandardError => e log_error(e) next end end end # Converts current configuration from single value to effective config values class and appends them to # EffectiveConfig class. Must be used inside Config Components only. # # @param effective_config [Contrast::Config::Diagnostics::EffectiveConfig] # @param config_name [String] name of the config. # @param config_value [String, Boolean] value of the config. # @param canonical_prefix [String] starting of the path to config => api.proxy... # @param name_prefix [String] the name of the config prefix => contrast.api_key, contrast.url def add_single_effective_value effective_config, config_name, config_value, canonical_prefix, name_prefix Contrast::Config::Diagnostics::EffectiveConfigValue.new.tap do |new_effective_value| break if Contrast::Utils::DuckUtils.empty_duck?(config_value) fill_effective_value(new_effective_value, config_value, config_name, canonical_prefix, name_prefix) effective_config.values << new_effective_value rescue StandardError => e log_error(e) next end end private # Fills instance of effective configuration value from read configuration. # # @param new_effective_value [Contrast::Config::Diagnostics::EffectiveConfigValue] # @param config_value [String, Boolean] value of the config # @param config_value_name [string] name of the value e.g. enabled? # @param canonical_prefix [String] starting of the path to config => api.proxy... # @param name_prefix [String] the name of the config prefix => contrast.api_key, contrast.url # @return filled_new_effective_config [Contrast::Config::Diagnostics::EffectiveConfigValue] def fill_effective_value new_effective_value, config_value, config_value_name, canonical_prefix, name_prefix find_source(new_effective_value, canonical_prefix, assign_name(config_value_name), name_prefix) new_effective_value.value = config_value new_effective_value end # Assigns a proper name for the config removing '?' out of method names. # # @param config [String] name of the configuration # @return [String] def assign_name config return Contrast::Utils::ObjectShare::EMPTY_STRING unless config name = config.dup if name.end_with?(Contrast::Utils::ObjectShare::QUESTION_MARK) # check and remove '?' : start_bundled_service? => start_bundled_service name.delete!(Contrast::Utils::ObjectShare::QUESTION_MARK) # converts name e.g. enabled => enable name.chop! if name.end_with?(CHECK) name end name end # Retrieves the config value from sources by canonical name and sets the key value. # # @param new_effective_value [Contrast::Config::Diagnostics::EffectiveConfigValue] # @param canonical_prefix [String] starting of the path to config => api.proxy... # @param config_name [String] name of the config. # @param name_prefix [String] the name of the config prefix => contrast.api_key, contrast.url # @return [Contrast::Config::Diagnostics::EffectiveConfigValue] def find_source new_effective_value, canonical_prefix, config_name, name_prefix new_effective_value.key = "#{ name_prefix }.#{ config_name }" new_effective_value.canonical_name = "#{ canonical_prefix }.#{ config_name }" # For files we keep the whole path as source. source = Contrast::CONFIG.sources.get(new_effective_value.canonical_name) new_effective_value.assign_filename(source) new_source = if source.include?(Contrast::Config::LocalSourceValue::YAML_EXT) Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE[0] elsif source.include?(Contrast::Config::LocalSourceValue::YML_EXT) Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE[1] else Contrast::Components::Config::Sources::DEFAULT_VALUE end new_effective_value.source = new_source new_effective_value end # Logs any caught error. # # @param error [StandardError] def log_error error Contrast::CONFIG.proto_logger.warn(Contrast::Config::Diagnostics::Monitor::ERROR_MESSAGE, error: error, backtrace: error.backtrace) end end end end end