contrast-agent 7.0.0 → 7.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/ext/extconf_common.rb +88 -14
  3. data/lib/contrast/agent/assess/policy/policy.rb +1 -1
  4. data/lib/contrast/agent/assess/policy/source_method.rb +13 -4
  5. data/lib/contrast/agent/assess/policy/trigger_method.rb +12 -18
  6. data/lib/contrast/agent/deadzone/policy/policy.rb +1 -1
  7. data/lib/contrast/agent/excluder/excluder.rb +64 -31
  8. data/lib/contrast/agent/patching/policy/policy.rb +2 -2
  9. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +3 -0
  10. data/lib/contrast/agent/protect/rule/base.rb +4 -6
  11. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +1 -1
  12. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +2 -2
  13. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +1 -1
  14. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +1 -1
  15. data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +2 -2
  16. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli.rb +1 -1
  17. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +1 -1
  18. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +1 -1
  19. data/lib/contrast/agent/protect/rule/utils/filters.rb +6 -6
  20. data/lib/contrast/agent/protect/rule/xxe/xxe.rb +1 -1
  21. data/lib/contrast/agent/reporting/client/interface.rb +132 -0
  22. data/lib/contrast/agent/reporting/client/interface_base.rb +27 -0
  23. data/lib/contrast/agent/reporting/connection_status.rb +0 -1
  24. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +6 -4
  25. data/lib/contrast/agent/reporting/reporter.rb +23 -23
  26. data/lib/contrast/agent/reporting/reporting_events/agent_effective_config.rb +32 -0
  27. data/lib/contrast/agent/reporting/reporting_events/discovered_route.rb +1 -1
  28. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +10 -3
  29. data/lib/contrast/agent/reporting/reporting_utilities/endpoints.rb +7 -0
  30. data/lib/contrast/agent/reporting/reporting_utilities/headers.rb +3 -1
  31. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +57 -12
  32. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +55 -38
  33. data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +144 -0
  34. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +35 -13
  35. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_mode.rb +14 -1
  36. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +13 -12
  37. data/lib/contrast/agent/reporting/reporting_workers/application_server_worker.rb +3 -0
  38. data/lib/contrast/agent/reporting/reporting_workers/reporter_heartbeat.rb +3 -0
  39. data/lib/contrast/agent/reporting/reporting_workers/server_settings_worker.rb +3 -0
  40. data/lib/contrast/agent/request/request.rb +27 -12
  41. data/lib/contrast/agent/telemetry/base.rb +55 -31
  42. data/lib/contrast/agent/telemetry/client.rb +1 -3
  43. data/lib/contrast/agent/telemetry/exception/obfuscate.rb +97 -0
  44. data/lib/contrast/agent/telemetry/exception.rb +1 -0
  45. data/lib/contrast/agent/telemetry/telemetry.rb +0 -7
  46. data/lib/contrast/agent/thread/thread_watcher.rb +2 -2
  47. data/lib/contrast/agent/version.rb +1 -1
  48. data/lib/contrast/components/agent.rb +1 -1
  49. data/lib/contrast/components/api.rb +2 -2
  50. data/lib/contrast/components/app_context.rb +1 -1
  51. data/lib/contrast/components/assess.rb +1 -1
  52. data/lib/contrast/components/assess_rules.rb +1 -1
  53. data/lib/contrast/components/base.rb +3 -3
  54. data/lib/contrast/components/config/sources.rb +13 -9
  55. data/lib/contrast/components/config.rb +2 -2
  56. data/lib/contrast/components/protect.rb +2 -2
  57. data/lib/contrast/components/sampling.rb +6 -4
  58. data/lib/contrast/components/settings.rb +10 -1
  59. data/lib/contrast/config/certification_configuration.rb +1 -1
  60. data/lib/contrast/config/configuration_files.rb +47 -0
  61. data/lib/contrast/config/diagnostics/command_line.rb +24 -0
  62. data/lib/contrast/config/{config.rb → diagnostics/config.rb} +21 -6
  63. data/lib/contrast/config/diagnostics/contrast_ui.rb +24 -0
  64. data/lib/contrast/config/diagnostics/effective_config.rb +28 -0
  65. data/lib/contrast/config/diagnostics/effective_config_value.rb +14 -0
  66. data/lib/contrast/config/diagnostics/environment_variables.rb +51 -0
  67. data/lib/contrast/config/{diagnostics.rb → diagnostics/monitor.rb} +10 -10
  68. data/lib/contrast/config/diagnostics/source_config_value.rb +55 -0
  69. data/lib/contrast/config/diagnostics/tools.rb +188 -0
  70. data/lib/contrast/config/diagnostics/user_configuration_file.rb +44 -0
  71. data/lib/contrast/config/request_audit_configuration.rb +1 -1
  72. data/lib/contrast/config/server_configuration.rb +1 -1
  73. data/lib/contrast/config/validate.rb +2 -2
  74. data/lib/contrast/configuration.rb +82 -57
  75. data/lib/contrast/framework/grape/support.rb +1 -2
  76. data/lib/contrast/framework/manager.rb +17 -8
  77. data/lib/contrast/framework/rack/support.rb +99 -1
  78. data/lib/contrast/framework/rails/support.rb +1 -2
  79. data/lib/contrast/framework/sinatra/support.rb +1 -2
  80. data/lib/contrast/logger/aliased_logging.rb +18 -9
  81. data/lib/contrast/utils/hash_utils.rb +62 -0
  82. data/lib/contrast/utils/json.rb +46 -0
  83. data/lib/contrast/utils/net_http_base.rb +75 -26
  84. data/lib/contrast/utils/request_utils.rb +14 -0
  85. data/resources/assess/policy.json +11 -0
  86. metadata +20 -8
  87. data/lib/contrast/agent/reporting/input_analysis/details/bot_blocker_details.rb +0 -27
  88. data/lib/contrast/config/diagnostics_tools.rb +0 -99
  89. data/lib/contrast/config/effective_config.rb +0 -131
  90. data/lib/contrast/config/effective_config_value.rb +0 -32
@@ -15,6 +15,8 @@ require 'contrast/components/protect'
15
15
  require 'contrast/components/assess'
16
16
  require 'contrast/components/config/sources'
17
17
  require 'contrast/config/server_configuration'
18
+ require 'contrast/config/configuration_files'
19
+ require 'contrast/utils/hash_utils'
18
20
 
19
21
  module Contrast
20
22
  # This is how we read in the local settings for the Agent, both ENV/ CMD line
@@ -57,7 +59,8 @@ module Contrast
57
59
  DEFAULT_YAML_PATH = 'contrast_security.yaml'
58
60
  MILLISECOND_MARKER = '_ms'
59
61
  CONVERSION = {}.cs__freeze
60
- CONFIG_BASE_PATHS = ['', 'config/', '/etc/contrast/ruby/', '/etc/contrast/', '/etc/'].cs__freeze
62
+ # Precedence of paths, shift if config file values needs to go up the chain.
63
+ CONFIG_BASE_PATHS = %w[./ config/ /etc/contrast/ruby/ /etc/contrast/ /etc/].cs__freeze
61
64
  KEYS_TO_REDACT = %i[api_key url service_key user_name].cs__freeze
62
65
  REDACTED = '**REDACTED**'
63
66
 
@@ -65,9 +68,7 @@ module Contrast
65
68
  @default_name = default_name
66
69
 
67
70
  # Load config_kv from file
68
- config_kv = deep_symbolize_all_keys(load_config)
69
- config_sources = assign_source_to(config_kv, Contrast::Components::Config::Sources::YAML)
70
-
71
+ config_kv = Contrast::Utils::HashUtils.deep_symbolize_all_keys(load_config)
71
72
  unless cli_options
72
73
  cli_options = {}
73
74
  ENV.each do |key, value|
@@ -76,19 +77,22 @@ module Contrast
76
77
  cli_options[key] = value
77
78
  end
78
79
  end
80
+
79
81
  # Overlay CLI options - they take precedence over config file
80
- cli_options = deep_symbolize_all_keys(cli_options)
82
+ cli_options = Contrast::Utils::HashUtils.deep_symbolize_all_keys(cli_options)
81
83
  if cli_options
82
- config_kv = deep_merge(cli_options, config_kv)
83
- config_sources = deep_merge(assign_source_to(cli_options, Contrast::Components::Config::Sources::CLI),
84
- config_sources)
84
+ config_kv = Contrast::Utils::HashUtils.precedence_merge(cli_options, config_kv)
85
+ @_source_file_extensions = Contrast::Utils::HashUtils.
86
+ precedence_merge(assign_source_to(cli_options,
87
+ Contrast::Components::Config::Sources::COMMAND_LINE),
88
+ @_source_file_extensions)
85
89
  end
86
90
 
87
91
  # Some in-flight rewrites to maintain backwards compatibility
88
92
  config_kv = update_prop_keys(config_kv)
89
93
  @loaded_config = config_kv
90
94
 
91
- @sources = Contrast::Components::Config::Sources.new(config_sources)
95
+ @sources = Contrast::Components::Config::Sources.new(@_source_file_extensions)
92
96
 
93
97
  @api = Contrast::Components::Api::Interface.new(config_kv[:api])
94
98
  @enable = config_kv[:enable]
@@ -107,6 +111,11 @@ module Contrast
107
111
  convert_to_hash.to_yaml
108
112
  end
109
113
 
114
+ # @return [Hash] map of all extensions for each config key.
115
+ def source_file_extensions
116
+ @_source_file_extensions ||= {}
117
+ end
118
+
110
119
  # @return [Contrast::Components::Api::Interface]
111
120
  def api
112
121
  @api ||= Contrast::Components::Api::Interface.new # rubocop:disable Naming/MemoizedInstanceVariableName
@@ -142,27 +151,36 @@ module Contrast
142
151
  @protect ||= Contrast::Components::Protect::Interface.new # rubocop:disable Naming/MemoizedInstanceVariableName
143
152
  end
144
153
 
145
- protected
146
-
147
- # TODO: RUBY-546 move utility methods to auxiliary classes
154
+ # Base paths to check for the contrast configuration file, sorted by
155
+ # reverse order of precedence (first is most important).
156
+ def configuration_paths
157
+ @_configuration_paths ||= begin
158
+ basename = default_name.split('.').first
159
+ # Order of extensions comes from here:
160
+ extensions = Contrast::Components::Config::Sources::APP_CONFIGURATION_EXTENSIONS
148
161
 
149
- def load_config
150
- config = {}
151
- configuration_paths.find do |path|
152
- next unless File.exist?(path)
162
+ paths = []
163
+ # Environment paths takes precedence here. Look first through them.
164
+ paths << ENV['CONTRAST_CONFIG_PATH'] if ENV['CONTRAST_CONFIG_PATH']
165
+ paths << ENV['CONTRAST_SECURITY_CONFIG'] if ENV['CONTRAST_SECURITY_CONFIG']
153
166
 
154
- unless File.readable?(path)
155
- log_file_read_error(path)
156
- next
167
+ extensions.each do |ext|
168
+ places = CONFIG_BASE_PATHS.product(["#{ basename }.#{ ext }"])
169
+ paths += places.map!(&:join)
157
170
  end
158
- config = yaml_to_hash(path) || {}
159
- @config_file = path
160
- break
171
+ paths
161
172
  end
173
+ end
162
174
 
163
- config
175
+ # List of all read configuration files.
176
+ #
177
+ # @return [Contrast::Config::ConfigurationFiles] of paths
178
+ def origin
179
+ @_origin ||= Contrast::Config::ConfigurationFiles.new
164
180
  end
165
181
 
182
+ protected
183
+
166
184
  def yaml_to_hash path
167
185
  if path && File.readable?(path)
168
186
  begin
@@ -179,6 +197,46 @@ module Contrast
179
197
  {}
180
198
  end
181
199
 
200
+ # TODO: RUBY-546 move utility methods to auxiliary classes
201
+
202
+ # Read through all the paths we know config may live. Merge all values from all files found.
203
+ # Priority is given to yaml over yml. If same keys are found on two configs with different extensions,
204
+ # the Agent will read first from the yaml file and ignore the values for same key on the yml file.
205
+ #
206
+ # @return config [Hash] All source configurations from files.
207
+ def load_config
208
+ config = {}
209
+ @_source_file_extensions = {}
210
+ configuration_paths.each do |path|
211
+ next unless File.exist?(path)
212
+
213
+ unless File.readable?(path)
214
+ log_file_read_error(path)
215
+ next
216
+ end
217
+ origin.add_source_file(path, (yaml_to_hash(path) || {}))
218
+ end
219
+ # Legacy usage: Assign main configuration file for reference.
220
+ @config_file = origin.main_file
221
+ # merge all settings keeping the top yaml files values as priority.
222
+ # If in top file a key exists it's value won't be changed if same key has different value in one
223
+ # of the other config files. Only unique values will be taken in consideration.w
224
+ # precedence of paths: see Contrast::Configuration::CONFIG_BASE_PATHS
225
+ extensions_maps = []
226
+ origin.source_files.each do |file|
227
+ config = Contrast::Utils::HashUtils.precedence_merge(config, file.values)
228
+ # assign source values extentions:
229
+ extensions_maps << assign_source_to(Contrast::Utils::HashUtils.deep_symbolize_all_keys(file.values), file.path)
230
+ end
231
+
232
+ # merge all origin paths to be used as extension classification to preserve the precedence of config files:
233
+ extensions_maps.each do |path|
234
+ @_source_file_extensions = Contrast::Utils::HashUtils.precedence_merge!(@_source_file_extensions, path)
235
+ end
236
+
237
+ config
238
+ end
239
+
182
240
  # We're updating properties loaded from the configuration files to match the new agreed upon standard configuration
183
241
  # names, so that one file works for all agents
184
242
  def update_prop_keys config
@@ -203,39 +261,6 @@ module Contrast
203
261
  config
204
262
  end
205
263
 
206
- # Base paths to check for the contrast configuration file, sorted by
207
- # reverse order of precedence (first is most important).
208
- def configuration_paths
209
- @_configuration_paths ||= begin
210
- basename = default_name.split('.').first
211
- names = %w[yml yaml].map { |suffix| "#{ basename }.#{ suffix }" }
212
-
213
- paths = []
214
- paths << ENV['CONTRAST_CONFIG_PATH'] if ENV['CONTRAST_CONFIG_PATH']
215
- paths << ENV['CONTRAST_SECURITY_CONFIG'] if ENV['CONTRAST_SECURITY_CONFIG']
216
-
217
- tmp = CONFIG_BASE_PATHS.product(names)
218
- paths += tmp.map!(&:join)
219
- paths
220
- end
221
- end
222
-
223
- def deep_merge cli_config, file_config
224
- cli_config.merge(file_config) do |_key, cli_value, file_value|
225
- cli_value.is_a?(Hash) && file_value.is_a?(Hash) ? deep_merge(cli_value, file_value) : cli_value
226
- end
227
- end
228
-
229
- def deep_symbolize_all_keys hash
230
- return if hash.nil?
231
-
232
- new_hash = {}
233
- hash.each do |key, value|
234
- new_hash[key.to_sym] = value.is_a?(Hash) ? deep_symbolize_all_keys(value) : value
235
- end
236
- new_hash
237
- end
238
-
239
264
  private
240
265
 
241
266
  # We cannot use all access components at this point, unfortunately, as they
@@ -333,7 +358,7 @@ module Contrast
333
358
  KEYS_TO_REDACT.include?(key.to_sym)
334
359
  end
335
360
 
336
- def assign_source_to hash, source = Contrast::Components::Config::Sources::YAML
361
+ def assign_source_to hash, source = Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE
337
362
  hash.transform_values do |value|
338
363
  if value.is_a?(Hash)
339
364
  assign_source_to(value, source)
@@ -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
@@ -51,8 +51,7 @@ module Contrast
51
51
  # Find the current route, based on the provided Request wrapper
52
52
  #
53
53
  # @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.
54
+ # @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route
56
55
  def current_route_coverage request
57
56
  return unless ::Rails.cs__respond_to?(:application)
58
57
 
@@ -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
@@ -0,0 +1,62 @@
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
+ module Contrast
5
+ module Utils
6
+ # Module to hold various hash utils methods
7
+ module HashUtils
8
+ class << self
9
+ # Merges two hashes, first hash will preserve it's values and will only add unique values.
10
+ #
11
+ # @param hsh [Hash]
12
+ # @param other_hsh [Hash]
13
+ def precedence_merge hsh, other_hsh
14
+ hsh.merge(other_hsh) do |_key, old_value, new_value|
15
+ if old_value.is_a?(Hash) || new_value.is_a?(Hash)
16
+ Contrast::Utils::HashUtils.precedence_merge(old_value, new_value)
17
+ else
18
+ old_value
19
+ end
20
+ end
21
+ end
22
+
23
+ # Merges two hashes, first hash will preserve it's values and will only add unique values.
24
+ #
25
+ # @param hsh [Hash]
26
+ # @param other_hsh [Hash]
27
+ # @return [Hash]
28
+ def precedence_merge! hsh, other_hsh
29
+ hsh.merge!(other_hsh) do |_key, old_val, new_val|
30
+ if old_val.is_a?(Hash) || new_val.is_a?(Hash)
31
+ Contrast::Utils::HashUtils.precedence_merge!(old_val, new_val)
32
+ else
33
+ old_val
34
+ end
35
+ end
36
+ end
37
+
38
+ # Deep symbolizes all keys
39
+ # @param value [Hash]
40
+ # @return [Hash]
41
+ def deep_symbolize_all_keys value
42
+ new_hash = {}
43
+ value.each { |key, v| new_hash[key.to_sym] = map_value(v) }
44
+ new_hash
45
+ end
46
+
47
+ private
48
+
49
+ def map_value value
50
+ case value
51
+ when Hash
52
+ deep_symbolize_all_keys(value)
53
+ when Array
54
+ value.map { |v| map_value(v) }
55
+ else
56
+ value
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,46 @@
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 'json'
5
+ require 'contrast/utils/hash_utils'
6
+ require 'contrast/components/logger'
7
+
8
+ module Contrast
9
+ module Utils
10
+ # Module to hold Agent's custom JSON.parse logic.
11
+ module Json
12
+ class << self
13
+ include Contrast::Components::Logger::InstanceMethods
14
+
15
+ # Add any known cases where parsing error might arise from older json parser:
16
+ # @return [Array<String>]
17
+ SPECIAL_CASES = ["\"\""].cs__freeze # rubocop:disable Style/StringLiterals
18
+
19
+ # Parses a string using JSON.parser. This method is used instead of standard JSON.parse to
20
+ # support older versions of json gem => not supporting key-value second parameter, which is
21
+ # supported after json 2.3.0.
22
+ #
23
+ # @param string [String]
24
+ # @param deep_symbolize [Boolean] flag to set if keys needs to be deep symbolized.
25
+ # @return [Hash]
26
+ def parse string, deep_symbolize: false
27
+ # The Agent receives empty responses from TS sometimes.
28
+ # There is a special case to handle this.
29
+ return {} if SPECIAL_CASES.include?(string)
30
+
31
+ symbolized_hash = {}
32
+ hash = JSON::Parser.new(string).parse
33
+ symbolized_hash = Contrast::Utils::HashUtils.deep_symbolize_all_keys(hash) if deep_symbolize
34
+ return hash unless deep_symbolize
35
+
36
+ symbolized_hash
37
+ rescue JSON::ParserError => e
38
+ # Any parsing error will produce empty {}. Since older JSON might pose support issues with newer Ruby,
39
+ # We might just log any miss-parsings and allow Agent to continue.
40
+ logger.warn("[JSON] parse error: #{ e }")
41
+ {}
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end