# 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/components/config' require 'contrast/components/assess_rules' module Contrast module Components module Assess # A wrapper build around the Common Agent Configuration project to allow # for access of the values contained in its # parent_configuration_spec.yaml. # Specifically, this allows for querying the state of the Assess product. class Interface # rubocop:disable Metrics/ClassLength include Contrast::Components::ComponentBase # @return [Boolean, nil] attr_accessor :enable # @return [Boolean, nil] attr_writer :enable_scan_response # @return [Boolean, nil] attr_writer :enable_dynamic_sources # @return [Contrast::Components::Sampling::Interface] attr_writer :sampling # @return [Contrast::Components::AssessRules::Interface] attr_writer :rules # @return [String, nil] attr_writer :stacktraces # @return [Array, nil] attr_writer :tags # @return [String] attr_reader :canon_name # @return [Array] attr_reader :config_values # @return [Boolean] attr_writer :enable_original_object # @return [Boolean] attr_writer :enable_response_as_source # @return [Integer] attr_writer :max_context_source_events # @return [Integer] attr_writer :max_propagation_events # @return [Integer] attr_writer :max_rule_reported # @return [Integer] attr_writer :time_limit_threshold DEFAULT_STACKTRACES = 'ALL' DEFAULT_MAX_SOURCE_EVENTS = 50_000 DEFAULT_MAX_PROPAGATION_EVENTS = 50_000 DEFAULT_MAX_RULE_REPORTED = 100 DEFAULT_MAX_RULE_TIME_THRESHOLD = 300_000 CANON_NAME = 'assess' CONFIG_VALUES = %w[ enabled? tags enable_scan_response enable_original_object enable_dynamic_sources enable_response_as_source stacktraces max_context_source_events max_propagation_events max_rule_reported time_limit_threshold ].cs__freeze # rubocop:disable Naming/MemoizedInstanceVariableName def initialize hsh = {} @config_values = CONFIG_VALUES @canon_name = CANON_NAME return unless hsh @enable = hsh[:enable] @tags = hsh[:tags] @enable_scan_response = hsh[:enable_scan_response] @enable_dynamic_sources = hsh[:enable_dynamic_sources] @enable_original_object = hsh[:enable_original_object] @enable_response_as_source = hsh[:enable_response_as_source] @sampling = Contrast::Components::Sampling::Interface.new(hsh[:sampling]) @rules = Contrast::Components::AssessRules::Interface.new(hsh[:rules]) @stacktraces = hsh[:stacktraces] assign_limits(hsh) end # @return [Boolean] def enable_scan_response @enable_scan_response.nil? ? true : @enable_scan_response end # @return [Boolean] def enable_dynamic_sources @enable_dynamic_sources.nil? ? true : @enable_dynamic_sources end # @return [Boolean] def enable_original_object @enable_original_object.nil? ? true : @enable_original_object end # @return [Boolean] def enable_response_as_source @enable_response_as_source.nil? ? false : @enable_response_as_source end # @return [Contrast::Components::Sampling::Interface] def sampling @sampling ||= Contrast::Components::Sampling::Interface.new end # @return [Contrast::Components::AssessRules::Interface] def rules @rules ||= Contrast::Components::AssessRules::Interface.new end def stacktraces @stacktraces ||= DEFAULT_STACKTRACES end def max_rule_reported @max_rule_reported ||= DEFAULT_MAX_RULE_REPORTED end def time_limit_threshold @time_limit_threshold ||= DEFAULT_MAX_RULE_TIME_THRESHOLD end def max_propagation_events @max_propagation_events ||= DEFAULT_MAX_PROPAGATION_EVENTS end def max_context_source_events @max_context_source_events ||= DEFAULT_MAX_SOURCE_EVENTS end # @return [String, nil] def tags stringify_array(@tags) end def enabled? # config overrides if forcibly set return false if forcibly_disabled? return true if forcibly_enabled? ::Contrast::SETTINGS.assess_state.enabled == true end def tainted_columns ::Contrast::SETTINGS.tainted_columns end def forcibly_disabled? @_forcibly_disabled = false?(enable) if @_forcibly_disabled.nil? @_forcibly_disabled end def rule_disabled? name disabled_rules.include?(name) end # The value of the stacktrace should be treated as an ENUM. We upcase it for # faster comparisons when we use it. Anything not one of the known values of # 'NONE', 'SOME', or 'ALL' is treated as 'ALL' # # @return [Symbol] the normalized value of ::Contrast::CONFIG.assess.stacktraces def capture_stacktrace_value @_capture_stacktrace_value ||= case stacktraces&.upcase when 'NONE' :NONE when 'SOME' :SOME else :ALL end end # Consider capture_stacktrace_value along with the node type # to determine whether stacktraces should be captured. # # capture_stacktrace_value -> (:ALL, :NONE, :SOME) # node types (SourceNode, PolicyNode, TriggerNode, PropagationNode) # # @param policy_node [Contrast::Agent::Assess::Policy::PolicyNode] The node in question. # @return [Boolean] to capture or not to capture, that is the question. def capture_stacktrace? policy_node return true if capture_stacktrace_value == :ALL return false if capture_stacktrace_value == :NONE # Below here capture_stacktrace_value must be :SOME. return true if policy_node.cs__is_a?(Contrast::Agent::Assess::Policy::SourceNode) return true if policy_node.cs__is_a?(Contrast::Agent::Assess::Policy::TriggerNode) false end def scan_response? @_scan_response = !false?(enable_scan_response) if @_scan_response.nil? @_scan_response end def require_scan? @_require_scan = !false?(::Contrast::CONFIG.agent.ruby.require_scan) if @_require_scan.nil? @_require_scan end def require_dynamic_sources? return @_require_dynamic_sources unless @_require_dynamic_sources.nil? @_require_dynamic_sources = !false?(enable_dynamic_sources) end def non_request_tracking? @_non_request_tracking = true?(::Contrast::CONFIG.agent.ruby.non_request_tracking) if @_non_request_tracking.nil? @_non_request_tracking end def disabled_rules rules&.disabled_rules || ::Contrast::SETTINGS.assess_state.disabled_assess_rules || [] end def track_original_object? @_track_original_object = !false?(enable_original_object) if @_track_original_object.nil? @_track_original_object end def track_response_as_source? @track_response_as_source = !false?(enable_response_as_source) if @track_response_as_source.nil? @track_response_as_source end # The id for this process, based on the session metadata or id provided by the user, as indicated in # application startup. # # The ID of the current application run, as returned by the application settings endpoint or set by # application.session_id. If there is no session associated with this run, this field should be omitted # when reporting to TS. def session_id ::Contrast::SETTINGS.assess_state.session_id 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 super sampling&.to_effective_config(effective_config) rules&.to_effective_config(effective_config) end # rubocop:enable Naming/MemoizedInstanceVariableName private def forcibly_enabled? @_forcibly_enabled = true?(::Contrast::CONFIG.assess.enable) if @_forcibly_enabled.nil? @_forcibly_enabled end # Sets Event limits from configuration and converts string numbers to integers. # @param hsh [Hash] the configuration hash def assign_limits hsh return unless hsh source_limit = hsh[:max_context_source_events]&.to_i propagation_limit = hsh[:max_propagation_events]&.to_i max_rule_reporter = hsh[:max_rule_reported]&.to_i time_limit = hsh[:time_limit_threshold]&.to_i @max_context_source_events = source_limit if source_limit @max_propagation_events = propagation_limit if propagation_limit @max_rule_reported = max_rule_reporter if max_rule_reporter @time_limit_threshold = time_limit if time_limit end end end end end