contrast-agent 7.2.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
- data/lib/contrast/agent/protect/rule/base.rb +5 -1
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +27 -11
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
- data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
- data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
- data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
- data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
- data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
- data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
- data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
- data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
- data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
- data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +11 -0
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
- data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +1 -1
- data/lib/contrast/agent/telemetry/base.rb +28 -2
- data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/client.rb +10 -2
- data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
- data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
- data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
- data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
- data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
- data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/config.rb +4 -4
- data/lib/contrast/components/protect.rb +11 -1
- data/lib/contrast/components/sampling.rb +15 -10
- data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
- data/lib/contrast/config/yaml_file.rb +8 -0
- data/lib/contrast/framework/rails/support.rb +3 -0
- data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
- data/lib/contrast/utils/metrics_hash.rb +1 -1
- data/lib/contrast/utils/object_share.rb +2 -1
- data/lib/contrast/utils/response_utils.rb +12 -0
- data/lib/contrast/utils/timer.rb +2 -0
- data/lib/contrast.rb +9 -2
- data/ruby-agent.gemspec +1 -1
- metadata +21 -6
- data/lib/contrast/utils/input_classification_base.rb +0 -169
| @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'contrast/agent/telemetry/input_analysis_cache_event'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Contrast
         | 
| 7 | 
            +
              module Agent
         | 
| 8 | 
            +
                module Telemetry
         | 
| 9 | 
            +
                  # This hash will store the telemetry data for the Protect InputAnalysis cache.
         | 
| 10 | 
            +
                  class CacheHash < Hash
         | 
| 11 | 
            +
                    include Contrast::Components::Logger::InstanceMethods
         | 
| 12 | 
            +
                    # Set per request:
         | 
| 13 | 
            +
                    HASH_SIZE_LIMIT = 100
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    # Wrapper to set a value in this Telemetry::Hash only if the provided value is of the data_type for this
         | 
| 16 | 
            +
                    # Telemetry::CacheHash or the hash has not reached its limit for unique keys.
         | 
| 17 | 
            +
                    # Saves Array of reportable events.
         | 
| 18 | 
            +
                    #
         | 
| 19 | 
            +
                    # @param key [Object] the key to which to associate the value
         | 
| 20 | 
            +
                    # @param events [array<Object>]
         | 
| 21 | 
            +
                    # @return [Object, nil] echo back out the value as the Hash#[]= method does, or nil if not of the expected
         | 
| 22 | 
            +
                    #   data_type
         | 
| 23 | 
            +
                    def []= key, events
         | 
| 24 | 
            +
                      # If telemetry is not running, do not add more as we want to avoid a memory leak.
         | 
| 25 | 
            +
                      return unless Contrast::Agent.telemetry_queue&.running?
         | 
| 26 | 
            +
                      # If the Hash is full, do not add more as we want to avoid consuming all application resources.
         | 
| 27 | 
            +
                      return if at_limit?
         | 
| 28 | 
            +
                      # If the given value is of unexpected type, do not add it to avoid issues later where type is assumed.
         | 
| 29 | 
            +
                      return unless valid_event?(events)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                      super(key, events)
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    # Determine if hash has reached exception event limit.
         | 
| 35 | 
            +
                    #
         | 
| 36 | 
            +
                    # @return [Boolean]
         | 
| 37 | 
            +
                    def at_limit?
         | 
| 38 | 
            +
                      unless length < HASH_SIZE_LIMIT
         | 
| 39 | 
            +
                        logger.debug("[Telemetry] Number of IA cache events exceeds limit of #{ HASH_SIZE_LIMIT }")
         | 
| 40 | 
            +
                        return true
         | 
| 41 | 
            +
                      end
         | 
| 42 | 
            +
                      false
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    private
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    # Checks to see if the given object is a valid event.
         | 
| 48 | 
            +
                    # @param event [Contrast::Agent::Telemetry::InputAnalysisCacheEvent]
         | 
| 49 | 
            +
                    def valid_event? events
         | 
| 50 | 
            +
                      events&.all?(Contrast::Agent::Telemetry::InputAnalysisCacheEvent)
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| @@ -65,6 +65,7 @@ module Contrast | |
| 65 65 | 
             
                                    else
         | 
| 66 66 | 
             
                                      60
         | 
| 67 67 | 
             
                                    end
         | 
| 68 | 
            +
                      logger.debug('[Telemetry] received response.', response_code: status_code)
         | 
| 68 69 | 
             
                      ready_after if status_code == 429
         | 
| 69 70 | 
             
                    end
         | 
| 70 71 |  | 
| @@ -76,6 +77,8 @@ module Contrast | |
| 76 77 | 
             
                      return true if event.cs__is_a?(Contrast::Agent::Telemetry::Event)
         | 
| 77 78 | 
             
                      return true if event.cs__is_a?(Contrast::Agent::Telemetry::StartupMetricsEvent)
         | 
| 78 79 | 
             
                      return true if event.cs__is_a?(Contrast::Agent::Telemetry::Exception::Event)
         | 
| 80 | 
            +
                      return true if event.cs__is_a?(Contrast::Agent::Telemetry::InputAnalysisCacheEvent)
         | 
| 81 | 
            +
                      return true if event.cs__is_a?(Contrast::Agent::Telemetry::InputAnalysisEncodingEvent)
         | 
| 79 82 |  | 
| 80 83 | 
             
                      false
         | 
| 81 84 | 
             
                    end
         | 
| @@ -93,12 +96,17 @@ module Contrast | |
| 93 96 | 
             
                      "#{ Contrast::Agent::Telemetry::Base::URL }#{ endpoint }#{ path }"
         | 
| 94 97 | 
             
                    end
         | 
| 95 98 |  | 
| 96 | 
            -
                    # Helper Method to get json representation of Telemetry Event data, handles error on to_json
         | 
| 99 | 
            +
                    # Helper Method to get json representation of Telemetry Event data, handles error on to_json.
         | 
| 100 | 
            +
                    # Generating bodies for exceptions and startup metrics is different.
         | 
| 97 101 | 
             
                    #
         | 
| 98 102 | 
             
                    # @param event [Contrast::Agent::Telemetry::Event, Array<Contrast::Agent::Telemetry::Exception::Event>]
         | 
| 99 103 | 
             
                    # @return [String] - JSON
         | 
| 100 104 | 
             
                    def get_event_json event
         | 
| 101 | 
            -
                       | 
| 105 | 
            +
                      if event.cs__is_a?(Contrast::Agent::Telemetry::Exception::Event)
         | 
| 106 | 
            +
                        return Array(event.to_controlled_hash).to_json
         | 
| 107 | 
            +
                      end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                      [event.to_controlled_hash].to_json
         | 
| 102 110 | 
             
                    rescue Exception => e # rubocop:disable Lint/RescueException
         | 
| 103 111 | 
             
                      logger.error('[Telemetry] Unable to convert TelemetryEvent to JSON string', e, hsh)
         | 
| 104 112 | 
             
                      raise(e)
         | 
| @@ -10,7 +10,7 @@ module Contrast | |
| 10 10 | 
             
                  # This is the Telemetry::Hash, which will store Contrast::Agent::Telemetry::Exception::Event, so we can push
         | 
| 11 11 | 
             
                  # freely, without worrying about validating the event before that. Telemetry::Hash has a max size of events,
         | 
| 12 12 | 
             
                  # default is 10 events
         | 
| 13 | 
            -
                  class  | 
| 13 | 
            +
                  class ExceptionHash < Hash
         | 
| 14 14 | 
             
                    include Contrast::Components::Logger::InstanceMethods
         | 
| 15 15 | 
             
                    HASH_SIZE_LIMIT = 10
         | 
| 16 16 |  | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'contrast/agent/telemetry/input_analysis_event'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Contrast
         | 
| 7 | 
            +
              module Agent
         | 
| 8 | 
            +
                module Telemetry
         | 
| 9 | 
            +
                  # Event to report all gather information from the Input Analysis Cache statistics, hits and misses.
         | 
| 10 | 
            +
                  class InputAnalysisCacheEvent < Contrast::Agent::Telemetry::InputAnalysisEvent
         | 
| 11 | 
            +
                    NAME = 'InputAnalysis cache event'
         | 
| 12 | 
            +
                    PATH = '/protect_input_analysis_cache'
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    private
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    # Creates the tags for the event
         | 
| 17 | 
            +
                    #
         | 
| 18 | 
            +
                    # @param rule_id [String]
         | 
| 19 | 
            +
                    # @param match_rates [Contrast::Agent::Protect::Rule::InputClassification::MatchRates]
         | 
| 20 | 
            +
                    def add_tags rule_id, match_rates
         | 
| 21 | 
            +
                      super(rule_id, match_rates)
         | 
| 22 | 
            +
                      @tags['score_level'] = match_rates&.score_level.dup&.to_s || NOT_APPLICABLE
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'contrast/agent/telemetry/input_analysis_event'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Contrast
         | 
| 7 | 
            +
              module Agent
         | 
| 8 | 
            +
                module Telemetry
         | 
| 9 | 
            +
                  # Event to report all gather information from the Input Analysis Cache statistics, hits and misses.
         | 
| 10 | 
            +
                  class InputAnalysisEncodingEvent < Contrast::Agent::Telemetry::InputAnalysisEvent
         | 
| 11 | 
            +
                    NAME = 'InputAnalysis encoding event'
         | 
| 12 | 
            +
                    PATH = '/protect_input_analysis_encoding'
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    private
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    # Creates the tags for the event
         | 
| 17 | 
            +
                    #
         | 
| 18 | 
            +
                    # @param _rule_id [String]
         | 
| 19 | 
            +
                    # @param encode_rates [Contrast::Agent::Protect::Rule::InputClassification::EncodingRates]
         | 
| 20 | 
            +
                    def add_tags _rule_id, encode_rates
         | 
| 21 | 
            +
                      super(nil, encode_rates)
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
| @@ -0,0 +1,91 @@ | |
| 1 | 
            +
            # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'contrast/utils/metrics_hash'
         | 
| 5 | 
            +
            require 'contrast/agent/telemetry/metric_event'
         | 
| 6 | 
            +
            require 'contrast/agent/version'
         | 
| 7 | 
            +
            require 'contrast/components/logger'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            module Contrast
         | 
| 10 | 
            +
              module Agent
         | 
| 11 | 
            +
                module Telemetry
         | 
| 12 | 
            +
                  # Event to report all gather information from the Input Analysis metrics.
         | 
| 13 | 
            +
                  class InputAnalysisEvent < Contrast::Agent::Telemetry::MetricEvent
         | 
| 14 | 
            +
                    include Contrast::Components::Logger::InstanceMethods
         | 
| 15 | 
            +
                    NOT_APPLICABLE = 'n/a'
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    attr_reader :fields
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    # Override the name for any derived classes
         | 
| 20 | 
            +
                    NAME = 'InputAnalysis event'
         | 
| 21 | 
            +
                    # Override the path for any derived classes
         | 
| 22 | 
            +
                    PATH = '/protect_input_analysis'
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    # @param rule_id [String] the rule name.
         | 
| 25 | 
            +
                    # @param rates [Contrast::Agent::Protect::Rule::InputClassification::Rates] base class for all rates.
         | 
| 26 | 
            +
                    def initialize rule_id, rates
         | 
| 27 | 
            +
                      super()
         | 
| 28 | 
            +
                      @fields = MetricsHash.new(Integer)
         | 
| 29 | 
            +
                      add_tags(rule_id, rates)
         | 
| 30 | 
            +
                      generate_fields(rates)
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    # Returns the name of the event.
         | 
| 34 | 
            +
                    #
         | 
| 35 | 
            +
                    # @return [String]
         | 
| 36 | 
            +
                    def name
         | 
| 37 | 
            +
                      cs__class::NAME
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    # Returns the path to report the event.
         | 
| 41 | 
            +
                    #
         | 
| 42 | 
            +
                    # @return [String]
         | 
| 43 | 
            +
                    def path
         | 
| 44 | 
            +
                      cs__class::PATH
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    # Override the empty check for any derived classes if needed.
         | 
| 48 | 
            +
                    def empty?
         | 
| 49 | 
            +
                      super && Contrast::Utils::DuckUtils.empty_duck?(@tags)
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    private
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    # Creates the tags for the event. Overrides the base class to add the rule_id and match_rate.
         | 
| 55 | 
            +
                    #
         | 
| 56 | 
            +
                    # @param rule_id [String, nil]
         | 
| 57 | 
            +
                    # @param rates [Contrast::Agent::Protect::Rule::InputClassification::Rates]
         | 
| 58 | 
            +
                    def add_tags rule_id, rates
         | 
| 59 | 
            +
                      return if Contrast::Utils::DuckUtils.empty_duck?(rates)
         | 
| 60 | 
            +
                      return unless rates.cs__is_a?(Contrast::Agent::Protect::Rule::InputClassification::Rates)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                      @tags['event_type'] = name # rubocop:disable Security/Module/Name
         | 
| 63 | 
            +
                      @tags['test_environment'] = ENV['CONTRAST_AGENT_TELEMETRY_TEST'] == '1' ? 'true' : 'false'
         | 
| 64 | 
            +
                      @tags['rule_id'] = rule_id if rule_id
         | 
| 65 | 
            +
                      @tags['input_type'] = rates&.input_type.dup&.to_s || NOT_APPLICABLE
         | 
| 66 | 
            +
                      add_system_tags
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    # Creates the fields for the event.
         | 
| 70 | 
            +
                    # Override if needed.
         | 
| 71 | 
            +
                    #
         | 
| 72 | 
            +
                    # @param rates [Contrast::Agent::Protect::Rule::InputClassification::Rates] base class for all rates.
         | 
| 73 | 
            +
                    def generate_fields rates
         | 
| 74 | 
            +
                      return if Contrast::Utils::DuckUtils.empty_duck?(rates)
         | 
| 75 | 
            +
                      return unless rates.cs__is_a?(Contrast::Agent::Protect::Rule::InputClassification::Rates)
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                      rates.to_fields.each { |field, value| @fields[field] = value.dup }
         | 
| 78 | 
            +
                    end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    # Adds the system tags to the event.
         | 
| 81 | 
            +
                    def add_system_tags
         | 
| 82 | 
            +
                      @tags['agent_version'] = VERSION
         | 
| 83 | 
            +
                      @tags['ruby_version'] = RUBY_VERSION
         | 
| 84 | 
            +
                      @tags['os_type'] = sys_info['os_type'] == 'Darwin' ? 'MacOS' : 'Linux'
         | 
| 85 | 
            +
                      @tags['os_arch'] = sys_info['os_arch']
         | 
| 86 | 
            +
                      @tags['os_version'] = sys_info['os_version']
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
            end
         | 
| @@ -3,6 +3,8 @@ | |
| 3 3 |  | 
| 4 4 | 
             
            require 'contrast/utils/metrics_hash'
         | 
| 5 5 | 
             
            require 'contrast/agent/telemetry/event'
         | 
| 6 | 
            +
            require 'contrast/utils/duck_utils'
         | 
| 7 | 
            +
            require 'contrast/utils/os'
         | 
| 6 8 |  | 
| 7 9 | 
             
            module Contrast
         | 
| 8 10 | 
             
              module Agent
         | 
| @@ -10,6 +12,7 @@ module Contrast | |
| 10 12 | 
             
                  # This class will hold the basic information for a Telemetry Event
         | 
| 11 13 | 
             
                  class MetricEvent < Contrast::Agent::Telemetry::Event
         | 
| 12 14 | 
             
                    include Contrast::Utils
         | 
| 15 | 
            +
                    include Contrast::Utils::OS
         | 
| 13 16 |  | 
| 14 17 | 
             
                    attr_reader :fields
         | 
| 15 18 |  | 
| @@ -19,6 +22,15 @@ module Contrast | |
| 19 22 | 
             
                      @fields['_filler'] = 0
         | 
| 20 23 | 
             
                    end
         | 
| 21 24 |  | 
| 25 | 
            +
                    def sys_info
         | 
| 26 | 
            +
                      @sys_info ||= get_system_information if @sys_info.nil?
         | 
| 27 | 
            +
                      @sys_info
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    def empty?
         | 
| 31 | 
            +
                      Contrast::Utils::DuckUtils.empty_duck?(@fields)
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
             | 
| 22 34 | 
             
                    def to_controlled_hash **_args
         | 
| 23 35 | 
             
                      super.merge!({ fields: @fields })
         | 
| 24 36 | 
             
                    end
         | 
| @@ -4,7 +4,6 @@ | |
| 4 4 | 
             
            require 'contrast/utils/metrics_hash'
         | 
| 5 5 | 
             
            require 'contrast/agent/telemetry/metric_event'
         | 
| 6 6 | 
             
            require 'contrast/agent/version'
         | 
| 7 | 
            -
            require 'contrast/utils/os'
         | 
| 8 7 |  | 
| 9 8 | 
             
            module Contrast
         | 
| 10 9 | 
             
              module Agent
         | 
| @@ -15,8 +14,6 @@ module Contrast | |
| 15 14 | 
             
                  # application framework and version and server framework
         | 
| 16 15 | 
             
                  # It will be initialized and send in Middleware#agent_startup_routine
         | 
| 17 16 | 
             
                  class StartupMetricsEvent < Contrast::Agent::Telemetry::MetricEvent
         | 
| 18 | 
            -
                    include Contrast::Utils::OS
         | 
| 19 | 
            -
             | 
| 20 17 | 
             
                    APP_AND_SERVER_DATA = ::Contrast::APP_CONTEXT.app_and_server_information.cs__freeze
         | 
| 21 18 | 
             
                    # Multi-tenant Production Environments
         | 
| 22 19 | 
             
                    SAAS_DEFAULT = { addr: 'app.contrastsecurity.com', type: 'SAAS_DEFAULT' }.cs__freeze
         | 
| @@ -82,11 +79,6 @@ module Contrast | |
| 82 79 | 
             
                      end
         | 
| 83 80 | 
             
                    end
         | 
| 84 81 |  | 
| 85 | 
            -
                    def sys_info
         | 
| 86 | 
            -
                      @sys_info ||= get_system_information if @sys_info.nil?
         | 
| 87 | 
            -
                      @sys_info
         | 
| 88 | 
            -
                    end
         | 
| 89 | 
            -
             | 
| 90 82 | 
             
                    private
         | 
| 91 83 |  | 
| 92 84 | 
             
                    # Here we extract the TeamServer url type
         | 
| @@ -25,7 +25,7 @@ module Contrast | |
| 25 25 | 
             
                # time than to silently fail to deliver functionality.
         | 
| 26 26 | 
             
                module Config
         | 
| 27 27 | 
             
                  CONTRAST_ENV_MARKER = 'CONTRAST__'
         | 
| 28 | 
            -
                  CONTRAST_LOG = ' | 
| 28 | 
            +
                  CONTRAST_LOG = 'contrast.log'
         | 
| 29 29 | 
             
                  CONTRAST_NAME = 'Contrast Agent'
         | 
| 30 30 | 
             
                  DATE_TIME = '%Y-%m-%dT%H:%M:%S.%L%z'
         | 
| 31 31 |  | 
| @@ -63,7 +63,7 @@ module Contrast | |
| 63 63 | 
             
                      env_overrides
         | 
| 64 64 | 
             
                      validate
         | 
| 65 65 | 
             
                    rescue ArgumentError => e
         | 
| 66 | 
            -
                      proto_logger.error('Configuration failed with error: ', e)
         | 
| 66 | 
            +
                      proto_logger.error('[PROTO_LOGGER] Configuration failed with error: ', e)
         | 
| 67 67 | 
             
                    end
         | 
| 68 68 | 
             
                    alias_method :rebuild, :build
         | 
| 69 69 |  | 
| @@ -157,7 +157,7 @@ module Contrast | |
| 157 157 | 
             
                    # @return [boolean]
         | 
| 158 158 | 
             
                    def valid_session_metadata?
         | 
| 159 159 | 
             
                      if !session_id&.empty? && !session_metadata&.empty?
         | 
| 160 | 
            -
                        proto_logger.error(SESSION_VARIABLES)
         | 
| 160 | 
            +
                        proto_logger.error("[PROTO_LOGGER] #{ SESSION_VARIABLES }")
         | 
| 161 161 | 
             
                        return false
         | 
| 162 162 | 
             
                      end
         | 
| 163 163 | 
             
                      true
         | 
| @@ -172,7 +172,7 @@ module Contrast | |
| 172 172 | 
             
                      msg << API_KEY unless api_key
         | 
| 173 173 | 
             
                      msg << API_SERVICE_KEY unless api_service_key
         | 
| 174 174 | 
             
                      msg << API_USERNAME unless api_username
         | 
| 175 | 
            -
                      msg.any? { |m| proto_logger.error(m) }
         | 
| 175 | 
            +
                      msg.any? { |m| proto_logger.error("[PROTO_LOGGER] #{ m }") }
         | 
| 176 176 | 
             
                      msg.empty?
         | 
| 177 177 | 
             
                    end
         | 
| 178 178 |  | 
| @@ -15,12 +15,14 @@ module Contrast | |
| 15 15 | 
             
                    include Contrast::Config::BaseConfiguration
         | 
| 16 16 |  | 
| 17 17 | 
             
                    CANON_NAME = 'protect'
         | 
| 18 | 
            -
                    CONFIG_VALUES = %w[enabled?].cs__freeze
         | 
| 18 | 
            +
                    CONFIG_VALUES = %w[enabled? normalize_base64?].cs__freeze
         | 
| 19 19 | 
             
                    RULES = 'rules'
         | 
| 20 20 | 
             
                    MODE = 'mode'
         | 
| 21 21 |  | 
| 22 22 | 
             
                    # @return [Boolean, nil]
         | 
| 23 23 | 
             
                    attr_accessor :enable
         | 
| 24 | 
            +
                    # @return [Boolean, nil]
         | 
| 25 | 
            +
                    attr_accessor :normalize_base64
         | 
| 24 26 | 
             
                    # @return [String]
         | 
| 25 27 | 
             
                    attr_reader :canon_name
         | 
| 26 28 | 
             
                    # @return [Array]
         | 
| @@ -36,6 +38,7 @@ module Contrast | |
| 36 38 | 
             
                      @_exceptions = Contrast::Config::ExceptionConfiguration.new(hsh[:exceptions])
         | 
| 37 39 | 
             
                      @_rules = Contrast::Config::ProtectRulesConfiguration.new(hsh[:rules])
         | 
| 38 40 | 
             
                      @enable = hsh[:enable]
         | 
| 41 | 
            +
                      @normalize_base64 = hsh[:normalize_base64]
         | 
| 39 42 | 
             
                      @agent_lib = hsh[:agent_lib]
         | 
| 40 43 | 
             
                    end
         | 
| 41 44 |  | 
| @@ -68,6 +71,13 @@ module Contrast | |
| 68 71 | 
             
                      ::Contrast::SETTINGS.protect_state.enabled == true
         | 
| 69 72 | 
             
                    end
         | 
| 70 73 |  | 
| 74 | 
            +
                    # Check to determine if the base64 decoding is required for user inputs.
         | 
| 75 | 
            +
                    def normalize_base64?
         | 
| 76 | 
            +
                      @normalize_base64 = Contrast::CONFIG.protect.normalize_base64 if @normalize_base64.nil?
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                      true?(@normalize_base64)
         | 
| 79 | 
            +
                    end
         | 
| 80 | 
            +
             | 
| 71 81 | 
             
                    # Current Configuration for the protect rules
         | 
| 72 82 | 
             
                    #
         | 
| 73 83 | 
             
                    # @return [Contrast::Config::ProtectRulesConfiguration]
         | 
| @@ -2,6 +2,7 @@ | |
| 2 2 | 
             
            # frozen_string_literal: true
         | 
| 3 3 |  | 
| 4 4 | 
             
            require 'contrast/components/base'
         | 
| 5 | 
            +
            require 'contrast/utils/duck_utils'
         | 
| 5 6 |  | 
| 6 7 | 
             
            module Contrast
         | 
| 7 8 | 
             
              module Components
         | 
| @@ -27,7 +28,7 @@ module Contrast | |
| 27 28 | 
             
                        config_settings = ::Contrast::CONFIG.assess&.sampling
         | 
| 28 29 | 
             
                        settings = ::Contrast::SETTINGS&.assess_state&.sampling_settings
         | 
| 29 30 | 
             
                        {
         | 
| 30 | 
            -
                            enabled: enabled?(config_settings, settings),
         | 
| 31 | 
            +
                            enabled: enabled?(config_settings&.enable, settings&.enabled),
         | 
| 31 32 | 
             
                            baseline: check_baseline(config_settings, settings),
         | 
| 32 33 | 
             
                            request_frequency: check_request_frequency(config_settings, settings),
         | 
| 33 34 | 
             
                            response_frequency: check_response_frequency(config_settings, settings),
         | 
| @@ -43,13 +44,20 @@ module Contrast | |
| 43 44 |  | 
| 44 45 | 
             
                    private
         | 
| 45 46 |  | 
| 46 | 
            -
                    # @param  | 
| 47 | 
            +
                    # @param config_value [Boolean, nil] the Sampling configuration as provided by
         | 
| 47 48 | 
             
                    #   local user input
         | 
| 48 | 
            -
                    # @param  | 
| 49 | 
            +
                    # @param settings_value [Boolean, nil] the Sampling settings as provided by
         | 
| 49 50 | 
             
                    # TeamServer
         | 
| 50 51 | 
             
                    # @return [Boolean] the resolution of the config_settings, settings, and default value
         | 
| 51 | 
            -
                    def enabled?  | 
| 52 | 
            -
                       | 
| 52 | 
            +
                    def enabled? config_value, settings_value
         | 
| 53 | 
            +
                      # Check the Assess State received from TS:
         | 
| 54 | 
            +
                      sampling_enable = settings_value unless Contrast::Utils::DuckUtils.empty_duck?(settings_value)
         | 
| 55 | 
            +
                      # Check for local settings, YAML, ENV, CLI (it's with higher priority than Web Interface)
         | 
| 56 | 
            +
                      # see: https://docs.contrastsecurity.com/en/order-of-precedence.html
         | 
| 57 | 
            +
                      sampling_enable = config_value unless Contrast::Utils::DuckUtils.empty_duck?(config_value)
         | 
| 58 | 
            +
                      # Use default value, unless a false or true is set from local or TS settings.
         | 
| 59 | 
            +
                      sampling_enable = DEFAULT_SAMPLING_ENABLED if Contrast::Utils::DuckUtils.empty_duck?(sampling_enable)
         | 
| 60 | 
            +
                      true?(sampling_enable)
         | 
| 53 61 | 
             
                    end
         | 
| 54 62 |  | 
| 55 63 | 
             
                    # @param config_settings [Contrast::Config::SamplingConfiguration] the Sampling configuration as provided by
         | 
| @@ -106,6 +114,8 @@ module Contrast | |
| 106 114 | 
             
                    NAME_PREFIX = "#{ CONTRAST }.#{ CANON_NAME }".cs__freeze
         | 
| 107 115 | 
             
                    CONFIG_VALUES = %w[enable baseline request_frequency response_frequency window_ms].cs__freeze
         | 
| 108 116 |  | 
| 117 | 
            +
                    # @return [Boolean, nil]
         | 
| 118 | 
            +
                    attr_accessor :enable
         | 
| 109 119 | 
             
                    # @return [Integer, nil]
         | 
| 110 120 | 
             
                    attr_accessor :baseline
         | 
| 111 121 | 
             
                    # @return [Integer, nil]
         | 
| @@ -128,11 +138,6 @@ module Contrast | |
| 128 138 | 
             
                      @window_ms = hsh[:window_ms]
         | 
| 129 139 | 
             
                    end
         | 
| 130 140 |  | 
| 131 | 
            -
                    # @return [Boolean, false]
         | 
| 132 | 
            -
                    def enable
         | 
| 133 | 
            -
                      !!@enable
         | 
| 134 | 
            -
                    end
         | 
| 135 | 
            -
             | 
| 136 141 | 
             
                    # Converts current configuration to effective config values class and appends them to
         | 
| 137 142 | 
             
                    # EffectiveConfig class.
         | 
| 138 143 | 
             
                    #
         | 
| @@ -11,7 +11,9 @@ module Contrast | |
| 11 11 | 
             
                  # Reads All ENV variables.
         | 
| 12 12 | 
             
                  module EnvironmentVariables
         | 
| 13 13 | 
             
                    class << self
         | 
| 14 | 
            -
                      NON_COMMON_ENV = %w[ | 
| 14 | 
            +
                      NON_COMMON_ENV = %w[
         | 
| 15 | 
            +
                        CONTRAST_CONFIG_PATH CONTRAST_AGENT_TELEMETRY_OPTOUT CONTRAST_AGENT_TELEMETRY_TEST
         | 
| 16 | 
            +
                      ].cs__freeze
         | 
| 15 17 |  | 
| 16 18 | 
             
                      # This method will fill the canonical name for each env var and will check for any uncommon ones.
         | 
| 17 19 | 
             
                      #
         | 
| @@ -11,6 +11,8 @@ module Contrast | |
| 11 11 | 
             
                # instrumentation.
         | 
| 12 12 | 
             
                module YamlFile
         | 
| 13 13 | 
             
                  CONFIG_FILE_NAME = 'contrast_security'
         | 
| 14 | 
            +
                  CONTRAST_ENV_MARKER = 'CONTRAST__'
         | 
| 15 | 
            +
             | 
| 14 16 | 
             
                  EXT = { yml: 'yml', yaml: 'yaml' }.freeze # rubocop:disable Security/Object/Freeze
         | 
| 15 17 |  | 
| 16 18 | 
             
                  POSSIBLE_TARGET_PATHS = %w[
         | 
| @@ -89,6 +91,8 @@ module Contrast | |
| 89 91 | 
             
                    #
         | 
| 90 92 | 
             
                    def create
         | 
| 91 93 | 
             
                      # rubocop:disable Rails/Output
         | 
| 94 | 
            +
                      return puts("\u{02C3}  Contrast configuration set by ENV variables.") if env_config_set?
         | 
| 95 | 
            +
             | 
| 92 96 | 
             
                      puts("\u{1F48E} Generating: Contrast Configuration file.")
         | 
| 93 97 | 
             
                      if Contrast::Config::YamlFile.created?
         | 
| 94 98 | 
             
                        puts("\u{2705}  Configuration file already exists: #{ Contrast::Config::YamlFile.find! }")
         | 
| @@ -123,6 +127,10 @@ module Contrast | |
| 123 127 | 
             
                    def file_name
         | 
| 124 128 | 
             
                      ENV['CONTRAST_YAML_FILE_TEST_CREATE_CONFIG_FILE_NAME_VALUE'] || CONFIG_FILE_NAME
         | 
| 125 129 | 
             
                    end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                    def env_config_set?
         | 
| 132 | 
            +
                      ENV.keys&.select { |config| config.include?(CONTRAST_ENV_MARKER) }&.any?
         | 
| 133 | 
            +
                    end
         | 
| 126 134 | 
             
                  end
         | 
| 127 135 | 
             
                end
         | 
| 128 136 | 
             
              end
         | 
| @@ -4,6 +4,7 @@ | |
| 4 4 | 
             
            require 'contrast/framework/base_support'
         | 
| 5 5 | 
             
            require 'contrast/framework/rails/patch/support'
         | 
| 6 6 | 
             
            require 'contrast/utils/string_utils'
         | 
| 7 | 
            +
            require 'contrast/utils/duck_utils'
         | 
| 7 8 |  | 
| 8 9 | 
             
            module Contrast
         | 
| 9 10 | 
             
              module Framework
         | 
| @@ -129,6 +130,8 @@ module Contrast | |
| 129 130 | 
             
                        if engine_route?(route)
         | 
| 130 131 | 
             
                          new_req = retrieve_request(request.env)
         | 
| 131 132 | 
             
                          new_req.path_info = new_req.path_info.gsub(match.to_s, '')
         | 
| 133 | 
            +
                          # solves the issue when requiring base path '/' without the slash
         | 
| 134 | 
            +
                          new_req.path_info = '/' if Contrast::Utils::DuckUtils.empty_duck?(new_req.path_info)
         | 
| 132 135 | 
             
                          get_full_route(new_req, route.app.app.routes.router, path << match.to_s)
         | 
| 133 136 | 
             
                        else
         | 
| 134 137 | 
             
                          [match, params, route, path]
         | 
| @@ -70,13 +70,13 @@ module Contrast | |
| 70 70 | 
             
                      if current_count == event_max
         | 
| 71 71 | 
             
                        return if event_limit_counts.key?(get_event_limit_key(method_policy, context))
         | 
| 72 72 |  | 
| 73 | 
            -
                        logger. | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 76 | 
            -
             | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 73 | 
            +
                        logger.debug('Event Limit Reached:',
         | 
| 74 | 
            +
                                     {
         | 
| 75 | 
            +
                                         count: current_count,
         | 
| 76 | 
            +
                                         max: event_max,
         | 
| 77 | 
            +
                                         policy: method_policy.method_name,
         | 
| 78 | 
            +
                                         node: method_policy
         | 
| 79 | 
            +
                                     })
         | 
| 80 80 | 
             
                        # increment to be over count for logging purposes
         | 
| 81 81 | 
             
                        increment_event_count(method_policy)
         | 
| 82 82 | 
             
                        increment_event_limit_logs(method_policy, context)
         | 
| @@ -87,12 +87,12 @@ module Contrast | |
| 87 87 | 
             
                        return if event_limit_counts.key?(get_event_limit_key(method_policy, context))
         | 
| 88 88 |  | 
| 89 89 | 
             
                        # increment to be over count for logging purposes
         | 
| 90 | 
            -
                        logger. | 
| 91 | 
            -
             | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 90 | 
            +
                        logger.debug('Event Limit Exceeded:',
         | 
| 91 | 
            +
                                     {
         | 
| 92 | 
            +
                                         count: current_count,
         | 
| 93 | 
            +
                                         policy: method_policy.method_name,
         | 
| 94 | 
            +
                                         node: method_policy
         | 
| 95 | 
            +
                                     })
         | 
| 96 96 | 
             
                        increment_event_limit_logs(method_policy, context)
         | 
| 97 97 | 
             
                        return true
         | 
| 98 98 | 
             
                      end
         | 
| @@ -17,7 +17,7 @@ module Contrast | |
| 17 17 | 
             
                    'The key extends the allowed length.',
         | 
| 18 18 | 
             
                    'The provided value is not the right data type'
         | 
| 19 19 | 
             
                  ].cs__freeze
         | 
| 20 | 
            -
                  KEY_REGEXP = /[a-zA-Z0- | 
| 20 | 
            +
                  KEY_REGEXP = /[a-zA-Z0-9._-]{1,63}/.cs__freeze
         | 
| 21 21 |  | 
| 22 22 | 
             
                  def initialize data_type, *several_variants
         | 
| 23 23 | 
             
                    super
         | 
| @@ -19,6 +19,7 @@ module Contrast | |
| 19 19 | 
             
                  def extract_body body
         | 
| 20 20 | 
             
                    return unless body
         | 
| 21 21 | 
             
                    return if body_is_file?(body)
         | 
| 22 | 
            +
                    return if body_is_sprockets?(body)
         | 
| 22 23 |  | 
| 23 24 | 
             
                    return handle_rack_body_proxy(body) if body.is_a?(Rack::BodyProxy)
         | 
| 24 25 | 
             
                    return extract_body(body.body) if sub_extractable?(body)
         | 
| @@ -74,6 +75,17 @@ module Contrast | |
| 74 75 |  | 
| 75 76 | 
             
                    false
         | 
| 76 77 | 
             
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  # Detects Rails' Sprockets asset pipeline objects passed as body.
         | 
| 80 | 
            +
                  # Returns false if Sprockets is passed as body, the Agent does not
         | 
| 81 | 
            +
                  # support Sprockets::Asset for body extraction.
         | 
| 82 | 
            +
                  #
         | 
| 83 | 
            +
                  # @param body [String, Rack::File, Rack::BodyProxy]
         | 
| 84 | 
            +
                  def body_is_sprockets? body
         | 
| 85 | 
            +
                    return body.cs__is_a?(Sprockets::Asset) if defined?(Sprockets) && defined?(Sprockets::Asset)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    false
         | 
| 88 | 
            +
                  end
         | 
| 77 89 | 
             
                end
         | 
| 78 90 | 
             
              end
         | 
| 79 91 | 
             
            end
         | 
    
        data/lib/contrast/utils/timer.rb
    CHANGED
    
    | @@ -1,6 +1,8 @@ | |
| 1 1 | 
             
            # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
         | 
| 2 2 | 
             
            # frozen_string_literal: true
         | 
| 3 3 |  | 
| 4 | 
            +
            require 'time'
         | 
| 5 | 
            +
             | 
| 4 6 | 
             
            module Contrast
         | 
| 5 7 | 
             
              module Utils
         | 
| 6 8 | 
             
                # Timer is class that can track state about when an event starts and how long it takes
         | 
    
        data/lib/contrast.rb
    CHANGED
    
    | @@ -61,10 +61,12 @@ require 'contrast/components/sampling' | |
| 61 61 | 
             
            require 'contrast/components/scope'
         | 
| 62 62 | 
             
            require 'contrast/components/settings'
         | 
| 63 63 | 
             
            require 'contrast/utils/routes_sent'
         | 
| 64 | 
            -
            require 'contrast/agent/telemetry/ | 
| 64 | 
            +
            require 'contrast/agent/telemetry/exception_hash'
         | 
| 65 65 | 
             
            require 'contrast/agent/telemetry/telemetry'
         | 
| 66 66 | 
             
            require 'contrast/agent/telemetry/exception/event'
         | 
| 67 67 | 
             
            require 'contrast/agent_lib/interface'
         | 
| 68 | 
            +
            require 'contrast/agent/telemetry/cache_hash'
         | 
| 69 | 
            +
            require 'contrast/agent/telemetry/base64_hash'
         | 
| 68 70 |  | 
| 69 71 | 
             
            module Contrast # :nodoc:
         | 
| 70 72 | 
             
              CONFIG = Contrast::Components::Config::Interface.new
         | 
| @@ -82,7 +84,12 @@ module Contrast # :nodoc: | |
| 82 84 | 
             
            end
         | 
| 83 85 |  | 
| 84 86 | 
             
            module Contrast
         | 
| 85 | 
            -
              TELEMETRY_EXCEPTIONS = ( | 
| 87 | 
            +
              TELEMETRY_EXCEPTIONS = (if Contrast::Agent::Telemetry.exceptions_enabled?
         | 
| 88 | 
            +
                                        Contrast::Agent::Telemetry::ExceptionHash.new
         | 
| 89 | 
            +
                                      end)
         | 
| 90 | 
            +
              TELEMETRY_IA_CACHE = (Contrast::Agent::Telemetry::CacheHash.new if Contrast::Agent::Telemetry::Base.enabled?)
         | 
| 91 | 
            +
              TELEMETRY_BASE64_HASH = (Contrast::Agent::Telemetry::Base64Hash.new if Contrast::Agent::Telemetry::Base.enabled? &&
         | 
| 92 | 
            +
                Contrast::PROTECT.normalize_base64?)
         | 
| 86 93 | 
             
              ROUTES_SENT = Contrast::Utils::RoutesSent.new
         | 
| 87 94 | 
             
            end
         | 
| 88 95 |  | 
    
        data/ruby-agent.gemspec
    CHANGED
    
    | @@ -121,7 +121,7 @@ def self.add_dependencies spec | |
| 121 121 | 
             
              spec.add_dependency 'rack', '~> 2.0'
         | 
| 122 122 |  | 
| 123 123 | 
             
              # bind this directly as we've had issues w/ build changes on bug release
         | 
| 124 | 
            -
              spec.add_dependency 'contrast-agent-lib',  '1.1. | 
| 124 | 
            +
              spec.add_dependency 'contrast-agent-lib',  '1.1.1'
         | 
| 125 125 | 
             
            end
         | 
| 126 126 |  | 
| 127 127 | 
             
            # Enumerate the files required to build the Agent.
         |