# Copyright (c) 2022 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' require 'contrast/config/diagnostics' module Contrast module Components # This component encapsulates reference to the configuration file. # At the time of writing, the configuration file is a yaml file reflecting # the 'common agent configuration' specification. # 'Config' and 'configuration' are to be interpreted as referring # specifically to these files and specifications, they are not generic # terms for other avenues of user configuration. # # This component is responsible for... # - encapsulating file access & concomitant error conditions # - implementing validity checks with respect to the specification # - memoizing/streamline field accesses # # Config fails fast. if it's not valid, the agent should break, and # it should break LOUDLY. Better to waste half an hour of the sysadmin's # time than to silently fail to deliver functionality. module Config CONTRAST_ENV_MARKER = 'CONTRAST__' CONTRAST_LOG = 'contrast_agent.log' CONTRAST_NAME = 'Contrast Agent' DATE_TIME = '%Y-%m-%dT%H:%M:%S.%L%z' class Interface # :nodoc: # rubocop:disable Metrics/ClassLength SESSION_VARIABLES = 'Invalid configuration. ' \ "Setting both application.session_id and application.session_metadata is not allowed.\n" API_URL = "Invalid configuration. Missing a required connection value 'url' is not set." API_KEY = "Invalid configuration. Missing a required connection value 'api_key' is not set." API_SERVICE_KEY = "Invalid configuration. Missing a required connection value 'service_tag' is not set." API_USERNAME = "Invalid configuration. Missing a required connection value 'user_name' is not set." attr_reader :config def initialize build end # Basic logger for handling configuration validation logging # the file to log is determined by the default one or set # by the config file, if that configuration is found def proto_logger @_proto_logger ||= begin @_proto_logger = ::Ougai::Logger.new(logger_path || CONTRAST_LOG) @_proto_logger.progname = CONTRAST_NAME @_proto_logger.level = ::Ougai::Logging::Severity::WARN @_proto_logger.formatter = Contrast::Logger::Format.new @_proto_logger.formatter.datetime_format = DATE_TIME @_proto_logger end end def build @_valid = nil @config = Contrast::Configuration.new env_overrides validate rescue ArgumentError => e proto_logger.error('Configuration failed with error: ', e) end alias_method :rebuild, :build # @return [Contrast::Components::Api::Interface] def api @config.api end # @return [Contrast::Components::Agent::Interface] def agent @config.agent end # @return [Contrast::Components::AppContext::Interface] def application @config.application end # @return [Contrast::Config::ServerConfiguration] def server @config.server end # @return [Contrast::Components::Assess::Interface] def assess @config.assess end # @return [Contrast::Components::Inventory::Interface] def inventory @config.inventory end # @return [Contrast::Components::Protect::Interface] def protect @config.protect end def valid? @_valid = validate if @_valid.nil? @_valid end def enable @config.enable end def sources @config.sources end def invalid? !valid? end def loggable @config.loggable end # Typically, this would be accessed through Contrast::SETTINGS, but we're specifically checking for the user # provided value here rather than that echoed back by TeamServer. # # @return [String,nil] the value of the session id set in the configuration, or nil if unset def session_id application.session_id end # @return [String,nil] the value of the session metadata set in the configuration, or nil if unset def session_metadata application.session_metadata end # @return [String, nil] the path to the YAML config file, if any. def config_file_path config.config_file end private # The config has information about how to construct the logger. If the config is invalid, and you want to know # about it, then you have a circular dependency if you try to log it, so we use basic proto_logger to do this # job. def validate return false unless valid_session_metadata? valid_api? end # The use can set either the application's session id or session metadata or neither, but never both. # # @return [boolean] def valid_session_metadata? if !session_id&.empty? && !session_metadata&.empty? proto_logger.error(SESSION_VARIABLES) return false end true end # The agent must have the configuration values required for the connection to TeamServer. # # @return [boolean] def valid_api? msg = [] msg << API_URL unless api_url msg << API_KEY unless api_key msg << API_SERVICE_KEY unless api_service_key msg << API_USERNAME unless api_username msg.any? { |m| proto_logger.error(m) } msg.empty? end def env_overrides # For env variables resembling CONTRAST__WHATEVER__NESTED_VALUE # override raw.whatever.nested_value ENV.each do |env_key, env_value| next unless env_key.to_s.start_with?(CONTRAST_ENV_MARKER) config_item = Contrast::Utils::EnvConfigurationItem.new(env_key, env_value) assign_value_to_path_array(self, config_item.dot_path_array, config_item.value) end end # Typically, the following values would be accessed through Contrast::Components::AppContext # and Contrast::Components::API, but we're too early in the initialization of the Agent to use # that mechanism, so we look it up directly for ourselves. # # @return [String, nil] def api_url api.url end # Typically, the following values would be accessed through Contrast::Components::AppContext # and Contrast::Components::API, but we're too early in the initialization of the Agent to use # that mechanism, so we look it up directly for ourselves. # # @return [String, nil] def api_key api.api_key end # Typically, the following values would be accessed through Contrast::Components::AppContext # and Contrast::Components::API, but we're too early in the initialization of the Agent to use # that mechanism, so we look it up directly for ourselves. # # @return [String, nil] def api_service_key api.service_key end # Typically, the following values would be accessed through Contrast::Components::AppContext # and Contrast::Components::API, but we're too early in the initialization of the Agent to use # that mechanism, so we look it up directly for ourselves. # # @return [String, nil] def api_username api.user_name end # Typically, the following values would be accessed through Contrast::Components::AppContext # and Contrast::Components::Logger, but we're too early in the initialization of the Agent to use # that mechanism, so we look it up directly for ourselves. # # @return [String, nil] def logger_path agent.logger.path end # This methods is here to add the proper forward towards @config def enable= value @config.enable = value end # Assign the value from an ENV variable to the Contrast::Config::RootConfiguration object, when # appropriate. # # @return nil def assign_value_to_path_array current_level, dot_path_array, value dot_path_array[0...-1].each do |segment| segment = segment.tr('-', '_') current_level = current_level.send(segment) if current_level.cs__respond_to?(segment) end return unless current_level.nil? == false && current_level.cs__respond_to?(dot_path_array[-1]) current_level.send("#{ dot_path_array[-1] }=", value) sources.set(dot_path_array.join('.'), Contrast::Components::Config::Sources::ENVIRONMENT) end end end end end