# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/base' require 'contrast/utils/duck_utils' module Contrast module Components module Sampling module Constants DEFAULT_SAMPLING_ENABLED = false DEFAULT_SAMPLING_BASELINE = 5 DEFAULT_SAMPLING_REQUEST_FREQUENCY = 5 DEFAULT_SAMPLING_RESPONSE_FREQUENCY = 25 DEFAULT_SAMPLING_WINDOW_MS = 180_000 end module ClassMethods # :nodoc: include Contrast::Components::ComponentBase include Constants def sampling_enabled? sampling_control[:enabled] end def sampling_control @_sampling_control ||= begin config_settings = ::Contrast::CONFIG.assess&.sampling settings = ::Contrast::SETTINGS&.assess_state&.sampling_settings { enabled: enabled?(config_settings&.enable, settings&.enabled), baseline: check_baseline(config_settings, settings), request_frequency: check_request_frequency(config_settings, settings), response_frequency: check_response_frequency(config_settings, settings), window: check_window(config_settings, settings) } end end # Used to reset sampling when settings from TeamServer change def reset_sampling_control @_sampling_control = nil end private # @param config_value [Boolean, nil] the Sampling configuration as provided by # local user input # @param settings_value [Boolean, nil] the Sampling settings as provided by # TeamServer # @return [Boolean] the resolution of the config_settings, settings, and default value def enabled? config_value, settings_value # Check the Assess State received from TS: sampling_enable = settings_value unless Contrast::Utils::DuckUtils.empty_duck?(settings_value) # Check for local settings, YAML, ENV, CLI (it's with higher priority than Web Interface) # see: https://docs.contrastsecurity.com/en/order-of-precedence.html sampling_enable = config_value unless Contrast::Utils::DuckUtils.empty_duck?(config_value) # Use default value, unless a false or true is set from local or TS settings. sampling_enable = DEFAULT_SAMPLING_ENABLED if Contrast::Utils::DuckUtils.empty_duck?(sampling_enable) true?(sampling_enable) end # @param config_settings [Contrast::Config::SamplingConfiguration] the Sampling configuration as provided by # local user input # @param settings [Contrast::Agent::Reporting::Settings::Sampling] the Sampling settings as provided by # TeamServer # @return [Integer] the resolution of the config_settings, settings, and default value def check_baseline config_settings, settings [config_settings&.baseline, settings&.baseline].map(&:to_i).find(&:positive?) || DEFAULT_SAMPLING_BASELINE end # @param config_settings [Contrast::Config::SamplingConfiguration] the Sampling configuration as provided by # local user input # @param settings [Contrast::Agent::Reporting::Settings::Sampling] the Sampling settings as provided by # TeamServer # @return [Integer] the resolution of the config_settings, settings, and default value def check_request_frequency config_settings, settings [config_settings&.request_frequency, settings&.request_frequency].map(&:to_i).find(&:positive?) || DEFAULT_SAMPLING_REQUEST_FREQUENCY end # @param config_settings [Contrast::Config::SamplingConfiguration] the Sampling configuration as provided by # local user input # @param settings [Contrast::Agent::Reporting::Settings::Sampling] the Sampling settings as provided by # TeamServer # @return [Integer] the resolution of the config_settings, settings, and default value def check_response_frequency config_settings, settings [config_settings&.response_frequency, settings&.response_frequency].map(&:to_i).find(&:positive?) || DEFAULT_SAMPLING_RESPONSE_FREQUENCY end # @param config_settings [Contrast::Config::SamplingConfiguration] the Sampling configuration as provided by # local user input # @param settings [Contrast::Agent::Reporting::Settings::Sampling] the Sampling settings as provided by # TeamServer # @return [Integer] the resolution of the config_settings, settings, and default value def check_window config_settings, settings [config_settings&.window_ms, settings&.window_ms].map(&:to_i).find(&:positive?) || DEFAULT_SAMPLING_WINDOW_MS end end module InstanceMethods # :nodoc: include Contrast::Components::ComponentBase include Constants include ClassMethods end class Interface # :nodoc: include InstanceMethods extend ClassMethods include Contrast::Config::BaseConfiguration CANON_NAME = 'assess.sampling' NAME_PREFIX = "#{ CONTRAST }.#{ CANON_NAME }".cs__freeze CONFIG_VALUES = %w[enable baseline request_frequency response_frequency window_ms].cs__freeze # @return [Boolean, nil] attr_accessor :enable # @return [Integer, nil] attr_accessor :baseline # @return [Integer, nil] attr_accessor :request_frequency # @return [Integer, nil] attr_accessor :response_frequency # @return [Integer, nil] attr_accessor :window_ms # @return [String] attr_accessor :canon_name def initialize hsh = {} @canon_name = CANON_NAME return unless hsh @enable = hsh[:enable] @baseline = hsh[:baseline] @request_frequency = hsh[:request_frequency] @response_frequency = hsh[:response_frequency] @window_ms = hsh[:window_ms] end # Converts current configuration to effective config values class and appends them to # EffectiveConfig class. # # @param effective_config [Contrast::Config::Diagnostics::EffectiveConfig] def to_effective_config effective_config confirm_sources add_single_effective_value(effective_config, 'enable', sampling_control[:enabled], canon_name, NAME_PREFIX) add_single_effective_value(effective_config, 'baseline', sampling_control[:baseline], canon_name, NAME_PREFIX) add_single_effective_value(effective_config, 'window_ms', sampling_control[:window], canon_name, NAME_PREFIX) add_single_effective_value(effective_config, 'request_frequency', sampling_control[:request_frequency], canon_name, NAME_PREFIX) add_single_effective_value(effective_config, 'response_frequency', sampling_control[:response_frequency], canon_name, NAME_PREFIX) end private # Confirm the sources for the sample entries to ensure that we use 'Default' if we've fallen # back to the default values. def confirm_sources if sampling_control[:enabled] == DEFAULT_SAMPLING_ENABLED Contrast::CONFIG.sources.set('assess.sampling.enable', Contrast::Components::Config::Sources::DEFAULT_VALUE) end if sampling_control[:window] == DEFAULT_SAMPLING_WINDOW_MS Contrast::CONFIG.sources.set('assess.sampling.window_ms', Contrast::Components::Config::Sources::DEFAULT_VALUE) end { 'baseline' => :baseline, 'request_frequency' => :request_frequency, 'response_frequency' => :response_frequency }.each do |k, v| if sampling_control[v] == cs__class.cs__const_get("DEFAULT_SAMPLING_#{ v.upcase }") Contrast::CONFIG.sources.set("assess.sampling.#{ k }", Contrast::Components::Config::Sources::DEFAULT_VALUE) end end end end end end end