# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/utils/env_configuration_item' require 'ougai' require 'contrast/configuration' module Contrast module Components module Config # This component encapsulates storing the source for each entry in the config, # so that we can report on where the value was set from. class Sources # [ CORPORATE_RULE, COMMAND_LINE, JAVA_SYSTEM_PROPERTY, ENVIRONMENT_VARIABLE, APP_CONFIGURATION_FILE, # USER_CONFIGURATION_FILE, CONTRAST_UI, DEFAULT_VALUE ] ENVIRONMENT_VARIABLE = 'ENVIRONMENT_VARIABLE' COMMAND_LINE = 'COMMAND_LINE' CONTRAST_UI = 'CONTRAST_UI' DEFAULT_VALUE = 'DEFAULT_VALUE' APP_CONFIGURATION_FILE = 'USER_CONFIGURATION_FILE' # Order matters for the Configurations files. This is read when Agent starts up and will always go # through the YAML as priority. # Do not change the order! APP_CONFIGURATION_EXTENSIONS = %w[yaml yml].cs__freeze # @return [Hash] attr_reader :data def initialize data = {} @data = data end # Retrieves the current config source for the specified config path. If no source is # set then returns the Default value. # # @param path [String] the canonical name for the config entry (such as api.proxy.enable) # @return [String] the source for the entry def get path data.dig(*parts_for(path)) || DEFAULT_VALUE rescue TypeError DEFAULT_VALUE end # Assigns the config source for a specified config path. # # @param path [String] the canonical name for the config entry (such as api.proxy.enable) # @param source[String] the source for the entry # @return [String] the source type for the entry def set path, source assign_value(data, parts_for(path), source) end # Finds entries within config source data for the specified type, and returns # them along with the current values for each. # # @param type [String] a source type (ENV, CLI, ContrastUI, YAML) # @return [Hash] the entries for the provided source, along with the associated values def for type deep_select(data.dup, type, []) end # @param path [String] the canonical name for the config entry (such as api.proxy.enable) or source # if the source(CONTRAST_UI, ENVIRONMENT_VARIABLE...) flag is set true. # @param source [Boolean] flag to specify we are passing in a source, no need to look for it. # @return [Boolean] true if the entry is from a YAML file def configuration_file_source? path, source: false s = source ? path : Contrast::CONFIG.sources.get(path) return true if s.include?(APP_CONFIGURATION_EXTENSIONS[0]) || s.include?(APP_CONFIGURATION_EXTENSIONS[1]) false end # Check to see whether the source has been overridden by local settings. # @param path [String] the canonical name for the config entry (such as api.proxy.enable) def source_overridden? path source = Contrast::CONFIG.sources.get(path) return true if [ENVIRONMENT_VARIABLE, COMMAND_LINE].include?(source) return true if configuration_file_source?(source, source: true) false end private # @param path [String] the canonical name for a config entry (such as api.proxy.enable) # @return [Array] the path split on periods, and converted to symbols def parts_for path path.split('.').map(&:to_sym) end # @param current_level [Hash] all, or some of, the config source information # @param parts [Array] the parts for canonical name of the config entry # @param source [String] the source to be set for the specified entry # @return [String] the path split on periods, and converted to symbols def assign_value current_level, parts, source parts[0...-1].each do |segment| current_level[segment] ||= {} current_level = current_level[segment] end return unless current_level.cs__is_a?(Hash) current_level[parts[-1]] = source end # @param sources [Hash] all, or some of, the config source information # @param type [Array] the source type to look for entries of # @param path [String] the entries followed to get to this part of the config # @return [Hash] the entries for the provided source, along with the associated values def deep_select sources, type, path sources.each_with_object({}) do |(k, v), grouping| if v.cs__is_a?(Hash) nested_data = deep_select(v, type, path.dup.append(k.to_sym)) grouping[k] = nested_data unless nested_data.empty? elsif v == type grouping[k] = Contrast::CONFIG.config.loaded_config.dig(*path, k) end end end end end end end