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.
- checksums.yaml +4 -4
- data/ext/extconf_common.rb +88 -14
- data/lib/contrast/agent/assess/policy/policy.rb +1 -1
- data/lib/contrast/agent/assess/policy/source_method.rb +13 -4
- data/lib/contrast/agent/assess/policy/trigger_method.rb +12 -18
- data/lib/contrast/agent/deadzone/policy/policy.rb +1 -1
- data/lib/contrast/agent/excluder/excluder.rb +64 -31
- data/lib/contrast/agent/patching/policy/policy.rb +2 -2
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +3 -0
- data/lib/contrast/agent/protect/rule/base.rb +4 -6
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +1 -1
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +1 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +1 -1
- data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +2 -2
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli.rb +1 -1
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +1 -1
- data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +1 -1
- data/lib/contrast/agent/protect/rule/utils/filters.rb +6 -6
- data/lib/contrast/agent/protect/rule/xxe/xxe.rb +1 -1
- data/lib/contrast/agent/reporting/client/interface.rb +132 -0
- data/lib/contrast/agent/reporting/client/interface_base.rb +27 -0
- data/lib/contrast/agent/reporting/connection_status.rb +0 -1
- data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +6 -4
- data/lib/contrast/agent/reporting/reporter.rb +23 -23
- data/lib/contrast/agent/reporting/reporting_events/agent_effective_config.rb +32 -0
- data/lib/contrast/agent/reporting/reporting_events/discovered_route.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +10 -3
- data/lib/contrast/agent/reporting/reporting_utilities/endpoints.rb +7 -0
- data/lib/contrast/agent/reporting/reporting_utilities/headers.rb +3 -1
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +57 -12
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +55 -38
- data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +144 -0
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +35 -13
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler_mode.rb +14 -1
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +13 -12
- data/lib/contrast/agent/reporting/reporting_workers/application_server_worker.rb +3 -0
- data/lib/contrast/agent/reporting/reporting_workers/reporter_heartbeat.rb +3 -0
- data/lib/contrast/agent/reporting/reporting_workers/server_settings_worker.rb +3 -0
- data/lib/contrast/agent/request/request.rb +27 -12
- data/lib/contrast/agent/telemetry/base.rb +55 -31
- data/lib/contrast/agent/telemetry/client.rb +1 -3
- data/lib/contrast/agent/telemetry/exception/obfuscate.rb +97 -0
- data/lib/contrast/agent/telemetry/exception.rb +1 -0
- data/lib/contrast/agent/telemetry/telemetry.rb +0 -7
- data/lib/contrast/agent/thread/thread_watcher.rb +2 -2
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/agent.rb +1 -1
- data/lib/contrast/components/api.rb +2 -2
- data/lib/contrast/components/app_context.rb +1 -1
- data/lib/contrast/components/assess.rb +1 -1
- data/lib/contrast/components/assess_rules.rb +1 -1
- data/lib/contrast/components/base.rb +3 -3
- data/lib/contrast/components/config/sources.rb +13 -9
- data/lib/contrast/components/config.rb +2 -2
- data/lib/contrast/components/protect.rb +2 -2
- data/lib/contrast/components/sampling.rb +6 -4
- data/lib/contrast/components/settings.rb +10 -1
- data/lib/contrast/config/certification_configuration.rb +1 -1
- data/lib/contrast/config/configuration_files.rb +47 -0
- data/lib/contrast/config/diagnostics/command_line.rb +24 -0
- data/lib/contrast/config/{config.rb → diagnostics/config.rb} +21 -6
- data/lib/contrast/config/diagnostics/contrast_ui.rb +24 -0
- data/lib/contrast/config/diagnostics/effective_config.rb +28 -0
- data/lib/contrast/config/diagnostics/effective_config_value.rb +14 -0
- data/lib/contrast/config/diagnostics/environment_variables.rb +51 -0
- data/lib/contrast/config/{diagnostics.rb → diagnostics/monitor.rb} +10 -10
- data/lib/contrast/config/diagnostics/source_config_value.rb +55 -0
- data/lib/contrast/config/diagnostics/tools.rb +188 -0
- data/lib/contrast/config/diagnostics/user_configuration_file.rb +44 -0
- data/lib/contrast/config/request_audit_configuration.rb +1 -1
- data/lib/contrast/config/server_configuration.rb +1 -1
- data/lib/contrast/config/validate.rb +2 -2
- data/lib/contrast/configuration.rb +82 -57
- data/lib/contrast/framework/grape/support.rb +1 -2
- data/lib/contrast/framework/manager.rb +17 -8
- data/lib/contrast/framework/rack/support.rb +99 -1
- data/lib/contrast/framework/rails/support.rb +1 -2
- data/lib/contrast/framework/sinatra/support.rb +1 -2
- data/lib/contrast/logger/aliased_logging.rb +18 -9
- data/lib/contrast/utils/hash_utils.rb +62 -0
- data/lib/contrast/utils/json.rb +46 -0
- data/lib/contrast/utils/net_http_base.rb +75 -26
- data/lib/contrast/utils/request_utils.rb +14 -0
- data/resources/assess/policy.json +11 -0
- metadata +20 -8
- data/lib/contrast/agent/reporting/input_analysis/details/bot_blocker_details.rb +0 -27
- data/lib/contrast/config/diagnostics_tools.rb +0 -99
- data/lib/contrast/config/effective_config.rb +0 -131
- 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
|
-
|
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 =
|
83
|
-
|
84
|
-
|
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(
|
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
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
167
|
+
extensions.each do |ext|
|
168
|
+
places = CONFIG_BASE_PATHS.product(["#{ basename }.#{ ext }"])
|
169
|
+
paths += places.map!(&:join)
|
157
170
|
end
|
158
|
-
|
159
|
-
@config_file = path
|
160
|
-
break
|
171
|
+
paths
|
161
172
|
end
|
173
|
+
end
|
162
174
|
|
163
|
-
|
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::
|
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]
|
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
|
-
|
91
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
'
|
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]
|
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]
|
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
|
-
|
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
|
-
|
80
|
-
stack_frame_type =
|
81
|
-
stack_frame_function =
|
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,
|
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
|
-
|
145
|
-
|
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
|