contrast-agent 7.1.0 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/ext/extconf_common.rb +88 -14
  3. data/lib/contrast/agent/assess/policy/source_method.rb +13 -4
  4. data/lib/contrast/agent/assess/policy/trigger_method.rb +12 -18
  5. data/lib/contrast/agent/excluder/excluder.rb +64 -31
  6. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
  7. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
  8. data/lib/contrast/agent/protect/rule/base.rb +9 -7
  9. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +1 -1
  10. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +29 -13
  11. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +1 -1
  12. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
  13. data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
  14. data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +2 -2
  15. data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
  16. data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
  17. data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
  18. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
  19. data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
  20. data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
  21. data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
  22. data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
  23. data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
  24. data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
  25. data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
  26. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
  27. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
  28. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +1 -1
  29. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
  30. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +1 -1
  31. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
  32. data/lib/contrast/agent/protect/rule/utils/filters.rb +6 -6
  33. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
  34. data/lib/contrast/agent/protect/rule/xxe/xxe.rb +1 -1
  35. data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
  36. data/lib/contrast/agent/reporting/client/interface.rb +132 -0
  37. data/lib/contrast/agent/reporting/client/interface_base.rb +27 -0
  38. data/lib/contrast/agent/reporting/connection_status.rb +0 -1
  39. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
  40. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +17 -4
  41. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
  42. data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
  43. data/lib/contrast/agent/reporting/reporter.rb +11 -26
  44. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
  45. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
  46. data/lib/contrast/agent/reporting/reporting_events/discovered_route.rb +1 -1
  47. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +10 -3
  48. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +47 -6
  49. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +41 -32
  50. data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +144 -0
  51. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +35 -13
  52. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_mode.rb +14 -1
  53. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +11 -11
  54. data/lib/contrast/agent/request/request.rb +27 -12
  55. data/lib/contrast/agent/telemetry/base.rb +44 -19
  56. data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
  57. data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
  58. data/lib/contrast/agent/telemetry/client.rb +10 -2
  59. data/lib/contrast/agent/telemetry/exception/obfuscate.rb +97 -0
  60. data/lib/contrast/agent/telemetry/exception.rb +1 -0
  61. data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
  62. data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
  63. data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
  64. data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
  65. data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
  66. data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
  67. data/lib/contrast/agent/version.rb +1 -1
  68. data/lib/contrast/components/config/sources.rb +6 -5
  69. data/lib/contrast/components/config.rb +4 -4
  70. data/lib/contrast/components/protect.rb +11 -1
  71. data/lib/contrast/components/sampling.rb +15 -10
  72. data/lib/contrast/components/settings.rb +9 -0
  73. data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
  74. data/lib/contrast/config/diagnostics/source_config_value.rb +5 -1
  75. data/lib/contrast/config/diagnostics/tools.rb +4 -4
  76. data/lib/contrast/config/validate.rb +2 -2
  77. data/lib/contrast/config/yaml_file.rb +8 -0
  78. data/lib/contrast/configuration.rb +11 -19
  79. data/lib/contrast/framework/grape/support.rb +1 -2
  80. data/lib/contrast/framework/manager.rb +17 -8
  81. data/lib/contrast/framework/rack/support.rb +99 -1
  82. data/lib/contrast/framework/rails/support.rb +4 -2
  83. data/lib/contrast/framework/sinatra/support.rb +1 -2
  84. data/lib/contrast/logger/aliased_logging.rb +18 -9
  85. data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
  86. data/lib/contrast/utils/hash_utils.rb +21 -2
  87. data/lib/contrast/utils/metrics_hash.rb +1 -1
  88. data/lib/contrast/utils/object_share.rb +2 -1
  89. data/lib/contrast/utils/request_utils.rb +14 -0
  90. data/lib/contrast/utils/response_utils.rb +12 -0
  91. data/lib/contrast/utils/timer.rb +2 -0
  92. data/lib/contrast.rb +9 -2
  93. data/resources/assess/policy.json +11 -0
  94. data/ruby-agent.gemspec +1 -1
  95. metadata +25 -7
  96. data/lib/contrast/agent/reporting/input_analysis/details/bot_blocker_details.rb +0 -27
  97. data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -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
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Contrast
5
5
  module Agent
6
- VERSION = '7.1.0'
6
+ VERSION = '7.3.0'
7
7
  end
8
8
  end
@@ -11,14 +11,15 @@ module Contrast
11
11
  # This component encapsulates storing the source for each entry in the config,
12
12
  # so that we can report on where the value was set from.
13
13
  class Sources
14
- ENVIRONMENT_VARIABLE = 'ENV'
15
- COMMAND_LINE = 'CLI'
16
- CONTRAST_UI = 'ContrastUI'
17
- DEFAULT_VALUE = 'Default'
14
+ ENVIRONMENT_VARIABLE = 'ENVIRONMENT_VARIABLE'
15
+ COMMAND_LINE = 'COMMAND_LINE'
16
+ CONTRAST_UI = 'CONTRAST_UI'
17
+ DEFAULT_VALUE = 'DEFAULT_VALUE'
18
+ APP_CONFIGURATION_FILE = 'USER_CONFIGURATION_FILE'
18
19
  # Order matters for the Configurations files. This is read when Agent starts up and will always go
19
20
  # through the YAML as priority.
20
21
  # Do not change the order!
21
- APP_CONFIGURATION_FILE = %w[YAML YML].cs__freeze
22
+ APP_CONFIGURATION_EXTENSIONS = %w[yaml yml].cs__freeze
22
23
 
23
24
  # @return [Hash]
24
25
  attr_reader :data
@@ -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 = 'contrast_agent.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 config_settings [Contrast::Config::SamplingConfiguration] the Sampling configuration as provided by
47
+ # @param config_value [Boolean, nil] the Sampling configuration as provided by
47
48
  # local user input
48
- # @param settings [Contrast::Agent::Reporting::Settings::Sampling, nil] the Sampling settings as provided by
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? config_settings, settings
52
- true?([config_settings&.enable, settings&.enabled, DEFAULT_SAMPLING_ENABLED].compact[0])
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
  #
@@ -5,6 +5,7 @@ require 'contrast/agent/excluder/excluder'
5
5
  require 'contrast/agent/reporting/settings/sensitive_data_masking'
6
6
  require 'contrast/components/config'
7
7
  require 'contrast/components/logger'
8
+ require 'contrast/utils/duck_utils'
8
9
 
9
10
  module Contrast
10
11
  module Components
@@ -219,6 +220,14 @@ module Contrast
219
220
  exclusions.input_exclusions.each do |exclusion|
220
221
  matchers << Contrast::Agent::ExclusionMatcher.new(exclusion)
221
222
  end
223
+ # Do not populate the matchers unless we have any. There are certain checks in
224
+ # SourceMethod that will safe-guard return if there are no exclusions received.
225
+ # The matching operation is expensive, and the excluder calls are made for each
226
+ # source, and we do not want to check for exclusions if they are empty. This is
227
+ # probably redundant as all exclusions default to empty, but will save useless
228
+ # new object creation at very least.
229
+ return if Contrast::Utils::DuckUtils.empty_duck?(matchers)
230
+
222
231
  @excluder = Contrast::Agent::Excluder.new(matchers)
223
232
  end
224
233
 
@@ -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[CONTRAST_CONFIG_PATH CONTRAST_AGENT_TELEMETRY_OPTOUT].cs__freeze
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
  #
@@ -41,7 +41,11 @@ module Contrast
41
41
  # @param source [String] name of the source file yaml | yml
42
42
  # @return [Array<String>, nil]
43
43
  def assign_filename source
44
- Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE.each do |type|
44
+ Contrast::Components::Config::Sources::APP_CONFIGURATION_EXTENSIONS.each do |type|
45
+ # We use the source to transfer the file's name from the mapping of the extensions.
46
+ # This is done b/c the user_configuration file has a second field to be filled,
47
+ # and other sources don't. Transfer the source as filename and set the default value
48
+ # for it later when we find the sources.
45
49
  instance_variable_set(:@filename, source) if source.include?(".#{ type.downcase }")
46
50
  end
47
51
  end
@@ -164,10 +164,10 @@ module Contrast
164
164
  # For files we keep the whole path as source.
165
165
  source = Contrast::CONFIG.sources.get(new_effective_value.canonical_name)
166
166
  new_effective_value.assign_filename(source)
167
- new_source = if source.include?(Contrast::Config::LocalSourceValue::YAML_EXT)
168
- Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE[0]
169
- elsif source.include?(Contrast::Config::LocalSourceValue::YML_EXT)
170
- Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE[1]
167
+ new_source = if source.include?(Contrast::Config::LocalSourceValue::YAML_EXT) ||
168
+ source.include?(Contrast::Config::LocalSourceValue::YML_EXT)
169
+
170
+ Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE
171
171
  else
172
172
  Contrast::Components::Config::Sources::DEFAULT_VALUE
173
173
  end
@@ -108,9 +108,9 @@ module Contrast
108
108
 
109
109
  def test_connection reporter
110
110
  puts(" Connection failed #{ FAIL }") unless reporter
111
- connection = reporter.connection
111
+ connection = reporter.client.send(:reporting_connection)
112
112
  abort("Failed to Initialize Connection please check error logs for details #{ FAIL } ") unless connection
113
- abort('Failed to Start Client please check error logs for details') unless reporter.client.startup!(connection)
113
+ abort('Failed to Start Client please check error logs for details') unless reporter.client.startup
114
114
  last_response = reporter.client.response_handler.last_response_code
115
115
  if last_response.include?('40')
116
116
  puts(" Last response code: #{ last_response } #{ FAIL }")
@@ -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
@@ -81,11 +81,11 @@ module Contrast
81
81
  # Overlay CLI options - they take precedence over config file
82
82
  cli_options = Contrast::Utils::HashUtils.deep_symbolize_all_keys(cli_options)
83
83
  if cli_options
84
- config_kv = Contrast::Utils::HashUtils.deep_merge(cli_options, config_kv)
84
+ config_kv = Contrast::Utils::HashUtils.precedence_merge(cli_options, config_kv)
85
85
  @_source_file_extensions = Contrast::Utils::HashUtils.
86
- deep_merge(assign_source_to(cli_options,
87
- Contrast::Components::Config::Sources::COMMAND_LINE),
88
- @_source_file_extensions)
86
+ precedence_merge(assign_source_to(cli_options,
87
+ Contrast::Components::Config::Sources::COMMAND_LINE),
88
+ @_source_file_extensions)
89
89
  end
90
90
 
91
91
  # Some in-flight rewrites to maintain backwards compatibility
@@ -157,7 +157,7 @@ module Contrast
157
157
  @_configuration_paths ||= begin
158
158
  basename = default_name.split('.').first
159
159
  # Order of extensions comes from here:
160
- extensions = Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE.map(&:downcase)
160
+ extensions = Contrast::Components::Config::Sources::APP_CONFIGURATION_EXTENSIONS
161
161
 
162
162
  paths = []
163
163
  # Environment paths takes precedence here. Look first through them.
@@ -216,7 +216,6 @@ module Contrast
216
216
  end
217
217
  origin.add_source_file(path, (yaml_to_hash(path) || {}))
218
218
  end
219
-
220
219
  # Legacy usage: Assign main configuration file for reference.
221
220
  @config_file = origin.main_file
222
221
  # merge all settings keeping the top yaml files values as priority.
@@ -225,13 +224,15 @@ module Contrast
225
224
  # precedence of paths: see Contrast::Configuration::CONFIG_BASE_PATHS
226
225
  extensions_maps = []
227
226
  origin.source_files.each do |file|
228
- # config.merge!(file.values) { |_key, oldval, _newval| oldval = oldval }
229
- precedence_merge!(config, file.values)
227
+ config = Contrast::Utils::HashUtils.precedence_merge(config, file.values)
230
228
  # assign source values extentions:
231
229
  extensions_maps << assign_source_to(Contrast::Utils::HashUtils.deep_symbolize_all_keys(file.values), file.path)
232
230
  end
231
+
233
232
  # merge all origin paths to be used as extension classification to preserve the precedence of config files:
234
- extensions_maps.each { |path| @_source_file_extensions = precedence_merge!(@_source_file_extensions, path) }
233
+ extensions_maps.each do |path|
234
+ @_source_file_extensions = Contrast::Utils::HashUtils.precedence_merge!(@_source_file_extensions, path)
235
+ end
235
236
 
236
237
  config
237
238
  end
@@ -357,7 +358,7 @@ module Contrast
357
358
  KEYS_TO_REDACT.include?(key.to_sym)
358
359
  end
359
360
 
360
- def assign_source_to hash, source = Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE[0]
361
+ def assign_source_to hash, source = Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE
361
362
  hash.transform_values do |value|
362
363
  if value.is_a?(Hash)
363
364
  assign_source_to(value, source)
@@ -366,14 +367,5 @@ module Contrast
366
367
  end
367
368
  end
368
369
  end
369
-
370
- # Merges two hashes, first hash will preserve it's values and will only add unique values.
371
- #
372
- # @param hsh [Hash]
373
- # @param other_hsh [Hash]
374
- # @return [Hash]
375
- def precedence_merge! hsh, other_hsh
376
- hsh.merge!(other_hsh) { |_key, old_val, _new_val| old_val }
377
- end
378
370
  end
379
371
  end
@@ -72,8 +72,7 @@ module Contrast
72
72
 
73
73
  # @param request [Contrast::Agent::Request] a contrast tracked request.
74
74
  # @param controller [::Grape::API] optionally use this controller instead of global ::Grape::API.
75
- # @return [Contrast::Agent::Reporting::RouteCoverage, nil] a Dtm describing the route
76
- # matched to the request if a match was found.
75
+ # @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route
77
76
  def current_route_coverage request, controller = ::Grape::API, full_route = nil
78
77
  return unless grape_controller?(controller)
79
78
 
@@ -37,6 +37,9 @@ module Contrast
37
37
  logger.info('Framework detected. Enabling support.', framework: framework_klass.detection_class)
38
38
  framework_klass
39
39
  end
40
+
41
+ # Delete Rack if we have more than one framework detected
42
+ @_frameworks.delete(Contrast::Framework::Rack::Support) if @_frameworks.length > 1
40
43
  @_frameworks.compact!
41
44
  end
42
45
 
@@ -87,16 +90,22 @@ module Contrast
87
90
  # this particular Request
88
91
  # @return [::Rack::Request] either a rack request or subclass thereof.
89
92
  def retrieve_request env
90
- # If we're mounted on Rails, use Rails.
91
- if @_frameworks.include?(Contrast::Framework::Rails::Support)
92
- return Contrast::Framework::Rails::Support.retrieve_request(env)
93
+ if Contrast::Utils::DuckUtils.empty_duck?(@_frameworks)
94
+ return Contrast::Framework::Rack::Support.retrieve_request(env)
93
95
  end
94
96
 
95
- # If we know the framework, use it.
96
- return @_frameworks[0].retrieve_request(env) if @_frameworks.length == 1
97
-
98
- # Fall back on a regular Rack::Request
99
- ::Rack::Request.new(env)
97
+ framework = @_frameworks[0]
98
+
99
+ case framework.cs__name
100
+ when 'Contrast::Framework::Rails::Support'
101
+ Contrast::Framework::Rails::Support.retrieve_request(env)
102
+ when 'Contrast::Framework::Grape::Support'
103
+ Contrast::Framework::Grape::Support.retrieve_request(env)
104
+ when 'Contrast::Framework::Sinatra::Support'
105
+ Contrast::Framework::Sinatra::Support.retrieve_request(env)
106
+ else
107
+ Contrast::Framework::Rack::Support.retrieve_request(env)
108
+ end
100
109
  rescue StandardError => e
101
110
  logger.warn('Unable to retrieve_request', e)
102
111
  end
@@ -14,7 +14,105 @@ module Contrast
14
14
  extend Contrast::Framework::Rack::Patch::Support
15
15
  class << self
16
16
  def detection_class
17
- 'rack -- don\'t let me be detected'
17
+ 'Rack'
18
+ end
19
+
20
+ # @return [String] the Rack version
21
+ def version
22
+ ::Rack.version
23
+ rescue StandardError
24
+ ''
25
+ end
26
+
27
+ # @return [String] the Rack application name
28
+ def application_name
29
+ 'Rack Application'
30
+ end
31
+
32
+ def application_root
33
+ Dir.pwd
34
+ end
35
+
36
+ # @return [String] the server type
37
+ def server_type
38
+ 'Rack'
39
+ end
40
+
41
+ # Find all the predefined routes for this application
42
+ #
43
+ # Extracting the Rack application routes is not trivial. Routes are evaluated dynamically
44
+ # when a request comes in, so they are not loaded before and stored in a data structure
45
+ # available somewhere. This mean that route discovery is only available through the rack map,
46
+ # but this is limited as not showing the actual method (GET, POST, etc...). For now The Agent
47
+ # will use only the current_route_coverage for Rack applications.
48
+ #
49
+ # @return [Array<Contrast::Agent::Reporting::DiscoveredRoute>]
50
+ # @raise [NoMethodError] raises error if subclass does not implement this method
51
+ def collect_routes
52
+ # return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless defined?(Rack)
53
+ # Rack::URLMap is used for mapping different rack apps to different paths.
54
+ # The Rack app could be separated into smaller rack applications.
55
+ # Rack::Builder is another option.
56
+ # return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless rack_map
57
+
58
+ # This method is disabled for now, as it is not returning the actual routes. Code is left for as
59
+ # comment for future reference.
60
+ #
61
+ # routes = []
62
+ # rack_map.any? do |path, meta|
63
+ # routes << Contrast::Agent::Reporting::DiscoveredRoute.from_rack_route(meta[1], meta[0], path)
64
+ # end
65
+ # routes
66
+ Contrast::Utils::ObjectShare::EMPTY_ARRAY
67
+ end
68
+
69
+ # Given the current request - return a RouteCoverage object
70
+
71
+ # @param request [Contrast::Agent::Request] a contrast tracked request.
72
+ # @param _controller [::Sinatra::Base] optionally use this controller instead of global ::Sinatra::Base.
73
+ # @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route
74
+ def current_route_coverage request, _controller = nil, full_route = nil
75
+ method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc...
76
+
77
+ full_route ||= request.env[::Rack::PATH_INFO]
78
+ return unless full_route && method
79
+
80
+ route_coverage = Contrast::Agent::Reporting::RouteCoverage.new
81
+ # We might not have controller, or even if there is defined one, it could not bare the name of the
82
+ # route to match as an object, it could be one router class with base controller with several methods
83
+ # describing each class, search for final controller might be resource heavy, and not efficient.
84
+ # For now to identify the controller the Agent will use the route name, this may lead to recording
85
+ # of false routes, but it is better than nothing. If route do no match a pattern it is a good practice
86
+ # to notify the user by displaying a not found page, in a sense this is a exercise of the application, but
87
+ # not correctly recorded controller name. Try to see if there is a define Rack::URLMap, and use it first.
88
+ mapped_controller = rack_map[full_route]&.last
89
+ final_controller = mapped_controller || full_route
90
+ route_coverage.attach_rack_based_data(final_controller, method, nil, full_route)
91
+ route_coverage
92
+ end
93
+
94
+ # Try and get map of Rack application { "path" => ["pattern", "controller"] }.
95
+ #
96
+ # @return [Hash<String, Array<String>>] the rack map
97
+ def rack_map
98
+ rack_map = {}
99
+ maps = ObjectSpace.each_object(::Rack::URLMap).to_a
100
+ maps.any? do |map|
101
+ mapping = map.instance_variable_get(:@mapping)
102
+ mapping.any? do |arr|
103
+ path = arr[1]
104
+ pattern = arr[2]
105
+ controller = arr[3]&.cs__class&.cs__name
106
+ rack_map[path] = [pattern, controller] if path&.cs__is_a?(String) && controller
107
+ end
108
+ end
109
+ rack_map
110
+ rescue StandardError
111
+ {}
112
+ end
113
+
114
+ def retrieve_request env
115
+ ::Rack::Request.new(env)
18
116
  end
19
117
  end
20
118
  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
@@ -51,8 +52,7 @@ module Contrast
51
52
  # Find the current route, based on the provided Request wrapper
52
53
  #
53
54
  # @param request[Contrast::Agent::Request]
54
- # @return [Contrast::Agent::Reporting::RouteCoverage, nil] a Dtm describing the route
55
- # matched to the request if a match was found.
55
+ # @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route
56
56
  def current_route_coverage request
57
57
  return unless ::Rails.cs__respond_to?(:application)
58
58
 
@@ -130,6 +130,8 @@ module Contrast
130
130
  if engine_route?(route)
131
131
  new_req = retrieve_request(request.env)
132
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)
133
135
  get_full_route(new_req, route.app.app.routes.router, path << match.to_s)
134
136
  else
135
137
  [match, params, route, path]
@@ -68,8 +68,7 @@ module Contrast
68
68
 
69
69
  # @param request [Contrast::Agent::Request] a contrast tracked request.
70
70
  # @param _controller [::Sinatra::Base] optionally use this controller instead of global ::Sinatra::Base.
71
- # @return [Contrast::Agent::Reporting::RouteCoverage, nil] a Dtm describing the route
72
- # matched to the request if a match was found.
71
+ # @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route
73
72
  def current_route_coverage request, _controller = ::Sinatra::Base, full_route = nil
74
73
  method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc...
75
74
  route = _cleaned_route(request)
@@ -72,13 +72,11 @@ module Contrast
72
72
  def build_exception type, message = nil, exception = nil, data = nil
73
73
  return unless buildable?
74
74
 
75
- stack_trace = wrapped_caller_locations
76
- caller_idx = stack_trace&.find_index { |stack| stack.to_s.include?(type) } || 0
77
- # The caller_stack is the method in which the error occurred, so has to be above this method
75
+ caller_idx = wrapped_caller_locations&.find_index { |stack| stack.to_s.include?(type) } || 0
78
76
  caller_idx += 1
79
- caller_frame = stack_trace[caller_idx]
80
- stack_frame_type = caller_frame.path.delete_prefix(Dir.pwd)
81
- stack_frame_function = caller_frame.label
77
+ caller = wrapped_caller_locations[caller_idx]
78
+ stack_frame_type = obfuscate_type(caller)
79
+ stack_frame_function = caller.label
82
80
  key = "#{ stack_frame_type }|#{ stack_frame_function }|#{ message }"
83
81
  if Contrast::TELEMETRY_EXCEPTIONS[key]
84
82
  Contrast::TELEMETRY_EXCEPTIONS.increment(key)
@@ -92,7 +90,7 @@ module Contrast
92
90
  stack_frame_type, message_exception_type,
93
91
  data, exception,
94
92
  message)
95
- build_stack(event_message, stack_trace, caller_idx)
93
+ build_stack(event_message, wrapped_caller_locations, caller_idx)
96
94
  TELEMETRY_EXCEPTIONS[key] = event_message
97
95
  rescue StandardError => e
98
96
  debug('[Telemetry] Unable to report exception', e)
@@ -141,8 +139,11 @@ module Contrast
141
139
  caller_stack.each_with_index do |caller, idx|
142
140
  next unless idx > caller_idx
143
141
 
144
- stack_frame = Contrast::Agent::Telemetry::Exception::StackFrame.build(caller.label,
145
- caller.path.delete_prefix(Dir.pwd))
142
+ obfuscated_label = Contrast::Agent::Telemetry::Exception::Obfuscate.obfuscate_path(caller.label)
143
+ obfuscated_path = Contrast::Agent::Telemetry::Exception::Obfuscate.
144
+ obfuscate_path(caller.path.delete_prefix(Dir.pwd))
145
+
146
+ stack_frame = Contrast::Agent::Telemetry::Exception::StackFrame.build(obfuscated_label, obfuscated_path)
146
147
  event_exception_message.push(stack_frame)
147
148
  end
148
149
  end
@@ -153,6 +154,14 @@ module Contrast
153
154
  def wrapped_caller_locations
154
155
  caller_locations
155
156
  end
157
+
158
+ # @param caller [Thread::Backtrace::Location, nil]
159
+ # @return [String]
160
+ def obfuscate_type caller
161
+ return '' unless caller.cs__respond_to?(:path)
162
+
163
+ Contrast::Agent::Telemetry::Exception::Obfuscate.obfuscate_path(caller.path.delete_prefix(Dir.pwd).to_s)
164
+ end
156
165
  end
157
166
  end
158
167
  end