# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'yaml' cs__scoped_require 'fileutils' cs__scoped_require 'contrast/config' cs__scoped_require 'contrast/utils/object_share' cs__scoped_require 'contrast/components/interface' module Contrast # This is how we read in the local settings for the Agent, both ENV/ CMD line # and from properties files, in order to determine which settings, if any, # the user has overridden. class Configuration extend Forwardable include Contrast::Components::Interface access_component :scope def_delegator :root, :assign_value_to_path_array attr_reader :default_name, :root DEFAULT_YAML_PATH = 'contrast_security.yaml' DEFAULT_HOST = '127.0.0.1' DEFAULT_PORT = '30555' MILLISECOND_MARKER = '_ms' CONVERSION = { 'agent.service.enable' => 'agent.start_bundled_service' }.cs__freeze CONFIG_BASE_PATHS = [ '', 'config/', '/etc/contrast/ruby/', '/etc/contrast/', '/etc/' ].cs__freeze REMOVE_FIELDS = [ 'contrast' ].cs__freeze def initialize cli_options = nil, default_name = DEFAULT_YAML_PATH @default_name = default_name # Load config_kv from file config_kv = deep_stringify_all_keys(load_config) # Overlay CLI options - they take precedence over config file cli_options = deep_stringify_all_keys(cli_options) config_kv = deep_merge(cli_options, config_kv) if cli_options # Some in-flight rewrites to maintain backwards compatibility config_kv = update_prop_keys(config_kv) config_kv = deprecate_fields(config_kv) @root = Contrast::Config::RootConfiguration.new(config_kv) end # Because we call this method to determine the need for scoping, it itself # must be executed inside a Contrast scope. Failure to do so could result # in an infinite loop on the to_sym method used later. def method_missing symbol, *args with_contrast_scope do root.public_send(symbol, *args) rescue NoMethodError => _e super end end def respond_to_missing? method_name, *args root&.cs__respond_to?(method_name) || super end protected # TODO: RUBY-546 move utility methods to auxiliary classes def load_config config = {} configuration_paths.find do |path| found = File.exist?(path) next unless found readable = File.readable?(path) unless readable puts "!!! Contrast - Configuration file at #{ path } is not readable by current user" next end config = yaml_to_hash(path) || {} break end if config.empty? puts "!!! Contrast - working directory: #{ Dir.pwd }" puts '!!! Contrast - valid configuration file could not be found at any of the search paths' puts 'Valid configuration paths are: ' configuration_paths.each do |path| puts(path) end end config end def yaml_to_hash path if path && File.readable?(path) begin yaml = IO.read(path) yaml = ERB.new(yaml).result if defined?(ERB) return YAML.safe_load(yaml) rescue RuntimeError => e puts "ERROR: unable to load configuration from path due to #{ e }" puts "ERROR: path=#{ path } pwd=#{ Dir.pwd }" end end {} end # We're updating properties loaded from the configuration # files to match the new agreed upon standard configuration # names, so that one file works for all agents def update_prop_keys config converted = false CONVERSION.each_pair do |old_method, new_method| # See if the old value was set and needs to be translated deprecated_keys = old_method.split('.') old_value = config deprecated_keys.each do |key| old_value = old_value[key] break if old_value.nil? end next if old_value.nil? converted = true puts "The deprecated property #{ old_method } is being set." puts "Please update your config to use the property #{ new_method } instead." new_keys = new_method.split('.') # We changed the seconds values into ms values. Multiply them accordingly old_value = old_value.to_i * 1000 if new_method.end_with?(MILLISECOND_MARKER) new_value = config end_idx = new_keys.length - 1 new_keys.each_with_index do |new_key, index| if index == end_idx new_value[new_key] = old_value if new_value[new_key].nil? else new_value = {} if new_value.nil? new_value[new_key] = {} if new_value[new_key].nil? new_value = new_value[new_key] end end end config end def deprecate_fields hash REMOVE_FIELDS.each do |field| path = field.split('.') active_path = hash path.each_with_index do |delete_path, index| if index == path.length - 1 && active_path active_path.delete(delete_path) elsif active_path active_path = active_path[delete_path] end end end hash end # Base paths to check for the contrast configuration file, sorted by # reverse order of precedence (first is most important). def configuration_paths @_configuration_paths ||= begin basename = default_name.split('.').first names = %w[yml yaml].map { |suffix| "#{ basename }.#{ suffix }" } paths = [] paths << ENV['CONTRAST_CONFIG_PATH'] if ENV['CONTRAST_CONFIG_PATH'] paths << ENV['CONTRAST_SECURITY_CONFIG'] if ENV['CONTRAST_SECURITY_CONFIG'] tmp = CONFIG_BASE_PATHS.product(names) paths += tmp.map!(&:join) paths end end def deep_merge cli_config, file_config cli_config.merge(file_config) do |_key, cli_value, file_value| cli_value.is_a?(Hash) && file_value.is_a?(Hash) ? deep_merge(cli_value, file_value) : cli_value end end def deep_stringify_all_keys hash return if hash.nil? new_hash = {} hash.each do |key, value| new_hash[key.to_s] = value.is_a?(Hash) ? deep_stringify_all_keys(value) : value end new_hash end end end