# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true module Contrast module Config module Diagnostics # Tools to help with the config diagnostics, called directly from the module. module SingletonTools API_CREDENTIALS = %w[api_key service_key].cs__freeze CONTRAST_MARK = 'CONTRAST_' # 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. # @param cli [Boolean] flag to check if the value comes from cli. # @return [Array] def to_config_values flats, source: false, cli: 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 = key if cli && key.to_s.include?(CONTRAST_MARK) config_value.canonical_name = key.gsub(Contrast::Utils::ObjectShare::DOUBLE_UNDERSCORE, Contrast::Utils::ObjectShare::PERIOD).downcase end config_value.key = key config_value.value = if API_CREDENTIALS.include?(key.to_s) Contrast::Configuration::EFFECTIVE_REDACTED else value_to_s(value) end end next unless efc_value settings << efc_value end end settings end # Flattens out the read settings from file, env or contrast ui. # example: {"agent.polling.server_settings_ms"=>"50000"} # # If cli is set we avoid adding the path and additional '.' to the key. # # @param data [Hash, nil] # @param path [String] where to look for settings. # @param config [Hash] symbolized config to fetch keys from. # @param cli [Boolean] does the config come from cli. def flatten_settings data, path = [], config: Contrast::CONFIG.config.loaded_config, cli: false 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 if API_CREDENTIALS.include?(k.to_s) entries << { k.to_s => Contrast::Configuration::EFFECTIVE_REDACTED } if cli entries << { "#{ path.join('.') }.#{ k }" => Contrast::Configuration::EFFECTIVE_REDACTED } unless cli next end entries << { k.to_s => value_to_s(config.dig(*path, k)) } if cli entries << { "#{ path.join('.') }.#{ k }" => value_to_s(config.dig(*path, k)) } unless cli end end.flatten # rubocop:disable Style/MethodCalledOnDoEndBlock end # Update the stored config values to ensure that we know about the correct values, # and that the sources are correct for entries updated from the UI. # # @param parts [Array, String] the path to the setting in config # Accepts Array: [:agent :enable] or String: 'agent.enable' # @param value [String, Integer, Array, nil] the value for the configuration setting # @param source_type [String] the source of the configuration setting def update_config parts, value, source_type parts_array, string = handle_parts_array(parts) path = string ? parts : parts_array.join('.') return unless parts_array # Check to see whether the source has been overridden by local settings, # Before updating from Contrast UI. if source_type == Contrast::Components::Config::Sources::CONTRAST_UI && Contrast::CONFIG.sources.source_overridden?(path) return end level = Contrast::CONFIG.config.loaded_config parts_array[0...-1]&.each do |segment| level[segment] ||= {} level = level[segment] end return unless level.cs__is_a?(Hash) level[parts_array[-1]] = value Contrast::CONFIG.sources.set(path, source_type) end # Recursively converts each value to string. # # @param value [Hash, nil] def value_to_s value case value when String if Contrast::Utils::DuckUtils.empty_duck?(value) Contrast::Config::Diagnostics::SourceConfigValue::NULL else value end when Array handle_array_to_s(value) when Hash handle_hash_to_s(value) when TrueClass, FalseClass, Symbol, Integer value.to_s else Contrast::Config::Diagnostics::SourceConfigValue::NULL end end private # Checks the type of path and converts it to array. # If the path is string it splits it by '.' and converts each element to symbol. # # @param parts [Array, String] the path to the setting in config # @return [Array, String] def handle_parts_array parts string = false arr = if parts.cs__is_a?(String) string = true parts.split('.')&.map&.each(&:to_sym) else parts end [arr, string] end # @param hash [Hash] # @return [Hash] def handle_hash_to_s hash hash&.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 # @param array [Array] # @return [String] def handle_array_to_s array return Contrast::Config::Diagnostics::SourceConfigValue::NULL if Contrast::Utils::DuckUtils.empty_duck?(array) array.join(',') end end end end end