contrast-agent 4.13.1 → 4.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.simplecov +1 -0
- data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
- data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
- data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
- data/lib/contrast/agent/assess/policy/trigger_method.rb +44 -7
- data/lib/contrast/agent/assess/policy/trigger_node.rb +14 -6
- data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
- data/lib/contrast/agent/assess/property/tagged.rb +51 -57
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
- data/lib/contrast/agent/metric_telemetry_event.rb +2 -2
- data/lib/contrast/agent/middleware.rb +5 -75
- data/lib/contrast/agent/patching/policy/method_policy.rb +3 -89
- data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
- data/lib/contrast/agent/patching/policy/patcher.rb +12 -8
- data/lib/contrast/agent/reporting/report.rb +21 -0
- data/lib/contrast/agent/reporting/reporter.rb +142 -0
- data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
- data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
- data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
- data/lib/contrast/agent/request.rb +2 -81
- data/lib/contrast/agent/request_context.rb +4 -128
- data/lib/contrast/agent/request_context_extend.rb +138 -0
- data/lib/contrast/agent/response.rb +2 -73
- data/lib/contrast/agent/startup_metrics_telemetry_event.rb +39 -16
- data/lib/contrast/agent/static_analysis.rb +1 -1
- data/lib/contrast/agent/telemetry.rb +15 -7
- data/lib/contrast/agent/telemetry_event.rb +8 -9
- data/lib/contrast/agent/thread_watcher.rb +31 -5
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +15 -0
- data/lib/contrast/api/communication/connection_status.rb +10 -7
- data/lib/contrast/api/communication/messaging_queue.rb +37 -3
- data/lib/contrast/api/communication/response_processor.rb +15 -8
- data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
- data/lib/contrast/api/communication/socket.rb +6 -8
- data/lib/contrast/api/communication/socket_client.rb +29 -12
- data/lib/contrast/api/communication/speedracer.rb +37 -1
- data/lib/contrast/api/communication/tcp_socket.rb +4 -3
- data/lib/contrast/api/communication/unix_socket.rb +1 -0
- data/lib/contrast/api/decorators/finding.rb +45 -0
- data/lib/contrast/components/api.rb +56 -0
- data/lib/contrast/components/app_context.rb +10 -65
- data/lib/contrast/components/app_context_extend.rb +78 -0
- data/lib/contrast/components/base.rb +23 -0
- data/lib/contrast/components/config.rb +8 -8
- data/lib/contrast/components/contrast_service.rb +5 -0
- data/lib/contrast/components/sampling.rb +2 -2
- data/lib/contrast/config/agent_configuration.rb +1 -1
- data/lib/contrast/config/api_configuration.rb +9 -4
- data/lib/contrast/config/api_proxy_configuration.rb +14 -0
- data/lib/contrast/config/application_configuration.rb +2 -3
- data/lib/contrast/config/assess_configuration.rb +3 -3
- data/lib/contrast/config/base_configuration.rb +17 -28
- data/lib/contrast/config/certification_configuration.rb +15 -0
- data/lib/contrast/config/env_variables.rb +2 -9
- data/lib/contrast/config/heap_dump_configuration.rb +6 -6
- data/lib/contrast/config/inventory_configuration.rb +1 -5
- data/lib/contrast/config/protect_rule_configuration.rb +1 -1
- data/lib/contrast/config/request_audit_configuration.rb +18 -0
- data/lib/contrast/config/ruby_configuration.rb +6 -6
- data/lib/contrast/config/service_configuration.rb +1 -2
- data/lib/contrast/config.rb +0 -1
- data/lib/contrast/configuration.rb +1 -2
- data/lib/contrast/extension/assess/array.rb +5 -7
- data/lib/contrast/framework/manager.rb +8 -32
- data/lib/contrast/framework/manager_extend.rb +50 -0
- data/lib/contrast/framework/rails/railtie.rb +1 -1
- data/lib/contrast/framework/sinatra/support.rb +2 -1
- data/lib/contrast/logger/log.rb +8 -103
- data/lib/contrast/utils/assess/property/tagged_utils.rb +23 -0
- data/lib/contrast/utils/assess/tracking_util.rb +20 -15
- data/lib/contrast/utils/assess/trigger_method_utils.rb +1 -1
- data/lib/contrast/utils/class_util.rb +18 -14
- data/lib/contrast/utils/findings.rb +62 -0
- data/lib/contrast/utils/hash_digest.rb +10 -73
- data/lib/contrast/utils/hash_digest_extend.rb +86 -0
- data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
- data/lib/contrast/utils/heap_dump_util.rb +2 -65
- data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
- data/lib/contrast/utils/io_util.rb +1 -1
- data/lib/contrast/utils/log_utils.rb +108 -0
- data/lib/contrast/utils/middleware_utils.rb +87 -0
- data/lib/contrast/utils/net_http_base.rb +158 -0
- data/lib/contrast/utils/object_share.rb +1 -0
- data/lib/contrast/utils/request_utils.rb +88 -0
- data/lib/contrast/utils/response_utils.rb +97 -0
- data/lib/contrast/utils/substitution_utils.rb +167 -0
- data/lib/contrast/utils/tag_util.rb +9 -9
- data/lib/contrast/utils/telemetry.rb +4 -2
- data/lib/contrast/utils/telemetry_client.rb +90 -0
- data/lib/contrast/utils/telemetry_identifier.rb +17 -24
- data/ruby-agent.gemspec +5 -5
- metadata +48 -23
- data/lib/contrast/config/default_value.rb +0 -17
- data/lib/contrast/utils/requests_client.rb +0 -150
|
@@ -87,7 +87,7 @@ module Contrast
|
|
|
87
87
|
def yaml_to_hash path
|
|
88
88
|
if path && File.readable?(path)
|
|
89
89
|
begin
|
|
90
|
-
yaml =
|
|
90
|
+
yaml = File.read(path)
|
|
91
91
|
yaml = ERB.new(yaml).result if defined?(ERB)
|
|
92
92
|
return YAML.safe_load(yaml)
|
|
93
93
|
rescue Psych::Exception => e
|
|
@@ -205,7 +205,6 @@ module Contrast
|
|
|
205
205
|
# in the thing to convert and setting them in the given hash. For now, this
|
|
206
206
|
# logs every possible key, whether set or not. If we want to change that
|
|
207
207
|
# behavior, we can skip adding keys to the hash if the value is nil, blank,
|
|
208
|
-
# or Contrast::Config::DefaultValue depending on desired behavior
|
|
209
208
|
#
|
|
210
209
|
# @param hash [Hash] the hash to populate
|
|
211
210
|
# @param convert [Contrast::Config::BaseConfiguration, Object] the level of
|
|
@@ -42,13 +42,11 @@ module Contrast
|
|
|
42
42
|
shift = 0
|
|
43
43
|
separator_length = separator.nil? ? 0 : separator.to_s.length
|
|
44
44
|
parent_events = []
|
|
45
|
-
ary.each do |obj|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
parent_events << parent_event if parent_event
|
|
51
|
-
end
|
|
45
|
+
ary.compact.each do |obj|
|
|
46
|
+
properties.copy_from(obj, ret, shift)
|
|
47
|
+
shift += obj.to_s.length
|
|
48
|
+
parent_event = Contrast::Agent::Assess::Tracker.properties(obj)&.event
|
|
49
|
+
parent_events << parent_event if parent_event
|
|
52
50
|
shift += separator_length
|
|
53
51
|
end
|
|
54
52
|
return ret unless Contrast::Agent::Assess::Tracker.tracked?(ret)
|
|
@@ -9,12 +9,14 @@ require 'contrast/framework/rails/support'
|
|
|
9
9
|
require 'contrast/framework/grape/support'
|
|
10
10
|
require 'contrast/framework/sinatra/support'
|
|
11
11
|
require 'contrast/utils/class_util'
|
|
12
|
+
require 'contrast/framework/manager_extend'
|
|
12
13
|
|
|
13
14
|
module Contrast
|
|
14
15
|
module Framework
|
|
15
16
|
# Allows access to framework specific information
|
|
16
17
|
class Manager
|
|
17
18
|
include Contrast::Components::Logger::InstanceMethods
|
|
19
|
+
include Contrast::Framework::ManagerExtend
|
|
18
20
|
|
|
19
21
|
# Order here does matter as the first framework listed will be the first one we pull information from Rack will
|
|
20
22
|
# be a special case that may involve updating some logic to handle only applying Rack if Rails/Sinatra do not
|
|
@@ -65,6 +67,10 @@ module Contrast
|
|
|
65
67
|
Contrast::Framework::PlatformVersion.from_string(framework_version)
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
def platform_version_string
|
|
71
|
+
first_framework_result :version, ''
|
|
72
|
+
end
|
|
73
|
+
|
|
68
74
|
def server_type
|
|
69
75
|
first_framework_result :server_type, 'rack'
|
|
70
76
|
end
|
|
@@ -116,7 +122,8 @@ module Contrast
|
|
|
116
122
|
# @param request [Contrast::Agent::Request] the current request.
|
|
117
123
|
# @return [Contrast::Api::Dtm::RouteCoverage] the current route as a Dtm.
|
|
118
124
|
def get_route_dtm request
|
|
119
|
-
@_frameworks.lazy.map { |framework_support| framework_support.current_route(request) }.
|
|
125
|
+
@_frameworks.lazy.map { |framework_support| framework_support.current_route(request) }.
|
|
126
|
+
reject(&:nil?).first
|
|
120
127
|
end
|
|
121
128
|
|
|
122
129
|
# Sometimes the framework we want to instrument is loaded after our agent code. To catch that case, we'll detect
|
|
@@ -149,37 +156,6 @@ module Contrast
|
|
|
149
156
|
rescue StandardError => e
|
|
150
157
|
logger.warn('Unable to register a late framework', e, module: mod.cs__name)
|
|
151
158
|
end
|
|
152
|
-
|
|
153
|
-
private
|
|
154
|
-
|
|
155
|
-
def enable_framework_support? klass
|
|
156
|
-
Contrast::Utils::ClassUtil.truly_defined?(klass)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def routes_for_all_frameworks
|
|
160
|
-
data_for_all_frameworks :collect_routes
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# This returns an array of all data from each framework in a flat, no-nil values array
|
|
164
|
-
#
|
|
165
|
-
# @param method_name [Symbol] the method to call on each FrameworkSupport class
|
|
166
|
-
# @return [Array]
|
|
167
|
-
def data_for_all_frameworks method_name
|
|
168
|
-
@_frameworks.flat_map { |framework| framework.send(method_name) }.compact
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# This returns a single object from the first framework to successfully respond
|
|
172
|
-
#
|
|
173
|
-
# @param method_name [Symbol] the method to call on each FrameworkSupport class
|
|
174
|
-
# @return [Object] - Determined by method to be invoked
|
|
175
|
-
def first_framework_result method_name, default_value
|
|
176
|
-
result = nil
|
|
177
|
-
@_frameworks.each do |framework|
|
|
178
|
-
result = framework.send(method_name)
|
|
179
|
-
break if result
|
|
180
|
-
end
|
|
181
|
-
result || default_value
|
|
182
|
-
end
|
|
183
159
|
end
|
|
184
160
|
end
|
|
185
161
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'contrast/components/logger'
|
|
5
|
+
require 'contrast/extension/module'
|
|
6
|
+
require 'contrast/framework/platform_version'
|
|
7
|
+
require 'contrast/framework/rack/support'
|
|
8
|
+
require 'contrast/framework/rails/support'
|
|
9
|
+
require 'contrast/framework/grape/support'
|
|
10
|
+
require 'contrast/framework/sinatra/support'
|
|
11
|
+
require 'contrast/utils/class_util'
|
|
12
|
+
|
|
13
|
+
module Contrast
|
|
14
|
+
module Framework
|
|
15
|
+
# Allows access to framework specific information
|
|
16
|
+
module ManagerExtend
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def enable_framework_support? klass
|
|
20
|
+
Contrast::Utils::ClassUtil.truly_defined?(klass)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def routes_for_all_frameworks
|
|
24
|
+
data_for_all_frameworks :collect_routes
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# This returns an array of all data from each framework in a flat, no-nil values array
|
|
28
|
+
#
|
|
29
|
+
# @param method_name [Symbol] the method to call on each FrameworkSupport class
|
|
30
|
+
# @return [Array]
|
|
31
|
+
def data_for_all_frameworks method_name
|
|
32
|
+
@_frameworks.flat_map { |framework| framework.send(method_name) }.
|
|
33
|
+
compact
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# This returns a single object from the first framework to successfully respond
|
|
37
|
+
#
|
|
38
|
+
# @param method_name [Symbol] the method to call on each FrameworkSupport class
|
|
39
|
+
# @return [Object] - Determined by method to be invoked
|
|
40
|
+
def first_framework_result method_name, default_value
|
|
41
|
+
result = nil
|
|
42
|
+
@_frameworks.each do |framework|
|
|
43
|
+
result = framework.send(method_name)
|
|
44
|
+
break if result
|
|
45
|
+
end
|
|
46
|
+
result || default_value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -14,7 +14,7 @@ module Contrast
|
|
|
14
14
|
initializer 'Contrast Ruby Agent Initializer' do |app|
|
|
15
15
|
log_rails = defined?(Rails) && defined?(Rails.logger)
|
|
16
16
|
|
|
17
|
-
Rails.logger.debug
|
|
17
|
+
Rails.logger.debug { "In railtie ::#{ app.middleware.inspect }" } if log_rails
|
|
18
18
|
|
|
19
19
|
if ::Contrast::APP_CONTEXT.instrument_middleware_stack?
|
|
20
20
|
::Contrast::AGENT.insert_middleware(app)
|
|
@@ -106,7 +106,8 @@ module Contrast
|
|
|
106
106
|
def _route_recurse controller, method, route
|
|
107
107
|
return if controller.nil? || controller.cs__class == NilClass
|
|
108
108
|
|
|
109
|
-
route_patterns = controller.routes.fetch(method) { [] }.
|
|
109
|
+
route_patterns = controller.routes.fetch(method) { [] }.
|
|
110
|
+
map(&:first)
|
|
110
111
|
route_pattern = route_patterns&.find do |matcher|
|
|
111
112
|
matcher.params(route) # ::Mustermann::Sinatra match.
|
|
112
113
|
end
|
data/lib/contrast/logger/log.rb
CHANGED
|
@@ -11,6 +11,7 @@ require 'contrast/logger/format'
|
|
|
11
11
|
require 'contrast/logger/request'
|
|
12
12
|
require 'contrast/logger/time'
|
|
13
13
|
require 'contrast/components/config'
|
|
14
|
+
require 'contrast/utils/log_utils'
|
|
14
15
|
|
|
15
16
|
module Contrast
|
|
16
17
|
# This module allows us to dynamically weave timing into our code, so that only when the time is actually needed do
|
|
@@ -26,9 +27,9 @@ module Contrast
|
|
|
26
27
|
# Add a method to the list of methods to be trace timed if logger set to TRACE. Enables trace timing after if
|
|
27
28
|
# logger set to TRACE.
|
|
28
29
|
#
|
|
29
|
-
# @
|
|
30
|
-
# @
|
|
31
|
-
# @
|
|
30
|
+
# @param: clazz [Class] the class of the method to time.
|
|
31
|
+
# @param: method [Symbol] the method to time.
|
|
32
|
+
# @param: method [String] optional custom logging message.
|
|
32
33
|
def add_method_to_trace_timing clazz, method, msg = nil
|
|
33
34
|
methods_to_time.append(METHOD_INFO.new(clazz, method, msg, false))
|
|
34
35
|
enable_trace_timing if logger.level == ::Ougai::Logging::TRACE
|
|
@@ -37,9 +38,9 @@ module Contrast
|
|
|
37
38
|
# Add a method to the list of methods to be trace timed if logger set to TRACE. Enables trace timing after if
|
|
38
39
|
# logger set to TRACE.
|
|
39
40
|
#
|
|
40
|
-
# @
|
|
41
|
-
# @
|
|
42
|
-
def trace_time_class_method meth_spec, class_method
|
|
41
|
+
# @param: meth_spec [METHOD_INFO] specs about the method to be timed.
|
|
42
|
+
# @param: class_method [Boolean] whether this is or isn't a class/module method.
|
|
43
|
+
def trace_time_class_method meth_spec, class_method # rubocop:disable Metrics/AbcSize
|
|
43
44
|
untimed_func_symbol = "untimed_#{ meth_spec.method_name }".to_sym
|
|
44
45
|
send_to = class_method ? meth_spec.clazz.cs__singleton_class : meth_spec.clazz
|
|
45
46
|
meth_spec.clazz.class_eval do
|
|
@@ -105,12 +106,7 @@ module Contrast
|
|
|
105
106
|
class Log
|
|
106
107
|
include Singleton
|
|
107
108
|
include ::Contrast::TraceTiming
|
|
108
|
-
|
|
109
|
-
DEFAULT_NAME = 'contrast.log'
|
|
110
|
-
DEFAULT_LEVEL = ::Ougai::Logging::Severity::INFO
|
|
111
|
-
VALID_LEVELS = ::Ougai::Logging::Severity::SEV_LABEL
|
|
112
|
-
STDOUT_STR = 'STDOUT'
|
|
113
|
-
STDERR_STR = 'STDERR'
|
|
109
|
+
include Contrast::Utils::LogUtils
|
|
114
110
|
|
|
115
111
|
attr_reader :previous_path, :previous_level
|
|
116
112
|
|
|
@@ -167,97 +163,6 @@ module Contrast
|
|
|
167
163
|
dir_name = File.dirname(File.absolute_path(path))
|
|
168
164
|
File.writable?(dir_name)
|
|
169
165
|
end
|
|
170
|
-
|
|
171
|
-
private
|
|
172
|
-
|
|
173
|
-
def build path: STDOUT_STR, level_const: DEFAULT_LEVEL
|
|
174
|
-
logger = case path
|
|
175
|
-
when STDOUT_STR, STDERR_STR
|
|
176
|
-
::Ougai::Logger.new(Object.cs__const_get(path))
|
|
177
|
-
else
|
|
178
|
-
::Ougai::Logger.new(path)
|
|
179
|
-
end
|
|
180
|
-
add_contrast_loggers(logger)
|
|
181
|
-
logger.progname = 'Contrast Agent'
|
|
182
|
-
logger.level = level_const
|
|
183
|
-
logger.formatter = Contrast::Logger::Format.new
|
|
184
|
-
logger.formatter.datetime_format = '%Y-%m-%dT%H:%M:%S.%L%z'
|
|
185
|
-
logger
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def add_contrast_loggers logger
|
|
189
|
-
logger.extend(Contrast::Logger::Application)
|
|
190
|
-
logger.extend(Contrast::Logger::Request)
|
|
191
|
-
logger.extend(Contrast::Logger::Time)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Determine the valid path to which to log, given the precedence of config > settings > default.
|
|
195
|
-
#
|
|
196
|
-
# @param log_file [String, nil] the file to which to log as provided by the settings retrieved from the
|
|
197
|
-
# TeamServer.
|
|
198
|
-
# @return [String] the path to which to log or STDOUT / STDERR if one of those values provided.
|
|
199
|
-
def find_valid_path log_file
|
|
200
|
-
config = ::Contrast::CONFIG.root.agent.logger
|
|
201
|
-
config_path = config&.path&.length.to_i.positive? ? config.path : nil
|
|
202
|
-
valid_path(config_path || log_file)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def valid_path path
|
|
206
|
-
path = path.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : path
|
|
207
|
-
return path if path == STDOUT_STR
|
|
208
|
-
return path if path == STDERR_STR
|
|
209
|
-
|
|
210
|
-
path = DEFAULT_NAME if path.empty?
|
|
211
|
-
if write_permission?(path)
|
|
212
|
-
path
|
|
213
|
-
elsif write_permission?(DEFAULT_NAME)
|
|
214
|
-
# Log once when the path is invalid. We'll change to this path, so no
|
|
215
|
-
# need to log again.
|
|
216
|
-
if previous_path != DEFAULT_NAME
|
|
217
|
-
puts "[!] Unable to write to '#{ path }'. Writing to default log '#{ DEFAULT_NAME }' instead."
|
|
218
|
-
end
|
|
219
|
-
DEFAULT_NAME
|
|
220
|
-
else
|
|
221
|
-
# Log once when the path is invalid. We'll change to this path, so no
|
|
222
|
-
# need to log again.
|
|
223
|
-
puts "[!] Unable to write to '#{ path }'. Writing to standard out instead." if previous_path != STDOUT_STR
|
|
224
|
-
STDOUT_STR
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Determine the valid level to which to log, given the precedence of config > settings > default.
|
|
229
|
-
#
|
|
230
|
-
# @param log_level [String, nil] the level at which to log as provided by the settings retrieved from the
|
|
231
|
-
# TeamServer.
|
|
232
|
-
# @return [::Ougai::Logging::Severity] the level at which to log
|
|
233
|
-
def find_valid_level log_level
|
|
234
|
-
config = ::Contrast::CONFIG.root.agent.logger
|
|
235
|
-
config_level = config&.level&.length&.positive? ? config.level : nil
|
|
236
|
-
|
|
237
|
-
valid_level(config_level || log_level)
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def valid_level level
|
|
241
|
-
level ||= DEFAULT_LEVEL
|
|
242
|
-
level = level.upcase
|
|
243
|
-
if VALID_LEVELS.include?(level)
|
|
244
|
-
Object.cs__const_get("::Ougai::Logging::Severity::#{ level }")
|
|
245
|
-
else
|
|
246
|
-
DEFAULT_LEVEL
|
|
247
|
-
end
|
|
248
|
-
rescue StandardError
|
|
249
|
-
DEFAULT_LEVEL
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# Log that the Agent log has changed and include some default information at the start of the log.
|
|
253
|
-
def log_update
|
|
254
|
-
logger.debug('Initialized new contrast agent logger')
|
|
255
|
-
logger.debug_with_time('middleware: log environment') do
|
|
256
|
-
logger.application_environment
|
|
257
|
-
logger.application_configuration
|
|
258
|
-
logger.application_libraries
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
166
|
end
|
|
262
167
|
end
|
|
263
168
|
end
|
|
@@ -9,6 +9,29 @@ module Contrast
|
|
|
9
9
|
# This module includes simple methods for the tags like
|
|
10
10
|
# adding tags, getting tags, deleting tags and similar
|
|
11
11
|
module TaggedUtils
|
|
12
|
+
# Is the given tag present?
|
|
13
|
+
# Used in testing, so found by `be_tagged`, if you're grepping for it
|
|
14
|
+
#
|
|
15
|
+
# @param label [Symbol] the tag to check for
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def tagged? label
|
|
18
|
+
tracked? && tags.key?(label)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Similar to #tracked?, but limited to a given range.
|
|
22
|
+
#
|
|
23
|
+
# @param start [Integer] the inclusive start index to check.
|
|
24
|
+
# @param finish [Integer] the exclusive end index to check.
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def any_tags_between? start, finish
|
|
27
|
+
return false unless tracked?
|
|
28
|
+
|
|
29
|
+
tags.each_value do |tag_array|
|
|
30
|
+
return true if tag_array.any? { |tag| tag.overlaps?(start, finish) }
|
|
31
|
+
end
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
12
35
|
# Given a tag name and range object, add a new tag to this
|
|
13
36
|
# collection. If the given range touches an existing tag,
|
|
14
37
|
# we'll combine the two, adjusting the existing one and
|
|
@@ -50,14 +50,9 @@ module Contrast
|
|
|
50
50
|
|
|
51
51
|
idx += 1
|
|
52
52
|
if Contrast::Utils::DuckUtils.iterable_hash?(obj)
|
|
53
|
-
obj
|
|
54
|
-
return true if _tracked?(k, idx) || _tracked?(v, idx)
|
|
55
|
-
end
|
|
56
|
-
false
|
|
53
|
+
handle_hash obj, idx
|
|
57
54
|
elsif Contrast::Utils::DuckUtils.iterable_enumerable?(obj)
|
|
58
|
-
obj
|
|
59
|
-
_tracked?(ele, idx) unless obj == ele
|
|
60
|
-
end
|
|
55
|
+
handle_enumerable obj, idx
|
|
61
56
|
else
|
|
62
57
|
Contrast::Agent::Assess::Tracker.tracked?(obj)
|
|
63
58
|
end
|
|
@@ -84,15 +79,9 @@ module Contrast
|
|
|
84
79
|
|
|
85
80
|
idx += 1
|
|
86
81
|
if Contrast::Utils::DuckUtils.iterable_hash?(obj)
|
|
87
|
-
obj
|
|
88
|
-
return true if _trackable?(k, idx)
|
|
89
|
-
return true if _trackable?(v, idx)
|
|
90
|
-
end
|
|
91
|
-
false
|
|
82
|
+
handle_hash obj, idx
|
|
92
83
|
elsif Contrast::Utils::DuckUtils.iterable_enumerable?(obj)
|
|
93
|
-
obj
|
|
94
|
-
_trackable?(ele, idx) unless obj == ele
|
|
95
|
-
end
|
|
84
|
+
handle_enumerable obj, idx
|
|
96
85
|
else
|
|
97
86
|
Contrast::Agent::Assess::Tracker.trackable?(obj)
|
|
98
87
|
end
|
|
@@ -103,6 +92,22 @@ module Contrast
|
|
|
103
92
|
logger.warn('Failed to determine trackable', e, module: obj.cs__class)
|
|
104
93
|
false
|
|
105
94
|
end
|
|
95
|
+
|
|
96
|
+
def handle_hash obj, idx
|
|
97
|
+
caller_method = caller(1..1).first[/`.*'/][1..-2].to_sym
|
|
98
|
+
obj.each_pair do |k, v|
|
|
99
|
+
return true if send(caller_method, k, idx)
|
|
100
|
+
return true if send(caller_method, v, idx)
|
|
101
|
+
end
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def handle_enumerable obj, idx
|
|
106
|
+
caller_method = caller(1..1).first[/`.*'/][1..-2].to_sym
|
|
107
|
+
obj.any? do |ele|
|
|
108
|
+
send(caller_method, ele, idx) unless obj == ele
|
|
109
|
+
end
|
|
110
|
+
end
|
|
106
111
|
end
|
|
107
112
|
end
|
|
108
113
|
end
|
|
@@ -103,7 +103,7 @@ module Contrast
|
|
|
103
103
|
# @param object [Object] the Object on which the method was invoked
|
|
104
104
|
# @param ret [Object] the Return of the invoked method
|
|
105
105
|
# @param args [Array<Object>] the Arguments with which the method was invoked
|
|
106
|
-
def apply_dataflow_rule trigger_node, source, object, ret, *args
|
|
106
|
+
def apply_dataflow_rule trigger_node, source, object, ret, *args # rubocop:disable Metrics/PerceivedComplexity
|
|
107
107
|
return unless source
|
|
108
108
|
|
|
109
109
|
if Contrast::Agent::Assess::Tracker.trackable?(source)
|
|
@@ -54,7 +54,7 @@ module Contrast
|
|
|
54
54
|
# Once we move to 2.7+, we can combine the caches using ID b/c the memory location stops being the id
|
|
55
55
|
#
|
|
56
56
|
# @param object [Object, nil] the entity to convert to a String
|
|
57
|
-
# @return [String] the human readable form of the String, as defined by
|
|
57
|
+
# @return [String, Object] the human readable form of the String, as defined by
|
|
58
58
|
# https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/capture-snapshot.md
|
|
59
59
|
def to_contrast_string object
|
|
60
60
|
# Only treat object like a string if it actually is a string+ some subclasses of String override string
|
|
@@ -66,19 +66,23 @@ module Contrast
|
|
|
66
66
|
else
|
|
67
67
|
return @lru_cache[object.__id__] if @lru_cache.key? object.__id__
|
|
68
68
|
|
|
69
|
-
@lru_cache[object.__id__] =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
@lru_cache[object.__id__] = convert_object object
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def convert_object object
|
|
74
|
+
if object.nil?
|
|
75
|
+
Contrast::Utils::ObjectShare::NIL_STRING
|
|
76
|
+
elsif object.cs__is_a?(Symbol)
|
|
77
|
+
":#{ object }"
|
|
78
|
+
elsif object.cs__is_a?(Module) || object.cs__is_a?(Class)
|
|
79
|
+
"#{ object.cs__name }@#{ object.__id__ }"
|
|
80
|
+
elsif object.cs__is_a?(Regexp)
|
|
81
|
+
object.source
|
|
82
|
+
elsif use_to_s?(object)
|
|
83
|
+
object.to_s
|
|
84
|
+
else
|
|
85
|
+
"#{ object.cs__class.cs__name }@#{ object.__id__ }"
|
|
82
86
|
end
|
|
83
87
|
end
|
|
84
88
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'contrast/components/logger'
|
|
5
|
+
|
|
6
|
+
module Contrast
|
|
7
|
+
module Utils
|
|
8
|
+
# Utility for saving raw findings for later
|
|
9
|
+
class Findings
|
|
10
|
+
include Contrast::Components::Logger::InstanceMethods
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@_collection = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def collection
|
|
17
|
+
@_collection ||= []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def push trigger_node, source, object, ret, *args
|
|
21
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless trigger_node.collectable?
|
|
22
|
+
|
|
23
|
+
@_collection << { trigger_node: trigger_node, source: source, object: object, ret: ret, args: args }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Some rules requires response to be available before validating them correctly,
|
|
27
|
+
# so we check if trigger_node.rule_id is collectable and then save them for
|
|
28
|
+
# later report, when we have the response.
|
|
29
|
+
#
|
|
30
|
+
# @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
|
|
31
|
+
# trigger event
|
|
32
|
+
# @param source [Object] the source of the Trigger Event
|
|
33
|
+
# @param object [Object] the Object on which the method was invoked
|
|
34
|
+
# @param ret [Object] the Return of the invoked method
|
|
35
|
+
# @param args [Array<Object>] the Arguments with which the method was invoked
|
|
36
|
+
def collect_finding trigger_node, source, object, ret, *args
|
|
37
|
+
push trigger_node, source, object, ret, args
|
|
38
|
+
logger.trace('Finding collected', node_id: trigger_node.id,
|
|
39
|
+
source_id: source.__id__,
|
|
40
|
+
rule: trigger_node.rule_id)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Build and report all collected findings for the collectable rules.
|
|
44
|
+
#
|
|
45
|
+
# We make sure the content-type is present before reporting, because some
|
|
46
|
+
# findings do require it for validation.
|
|
47
|
+
def report_collected_findings
|
|
48
|
+
return if Contrast::Agent::REQUEST_TRACKER.current&.response&.content_type.nil?
|
|
49
|
+
return if @_collection.empty?
|
|
50
|
+
|
|
51
|
+
while @_collection.any?
|
|
52
|
+
finding = @_collection.pop
|
|
53
|
+
Contrast::Agent::Assess::Policy::TriggerMethod.build_finding finding[:trigger_node],
|
|
54
|
+
finding[:source],
|
|
55
|
+
finding[:object],
|
|
56
|
+
finding[:ret],
|
|
57
|
+
finding[:args]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require 'digest'
|
|
5
|
+
require 'contrast/utils/hash_digest_extend'
|
|
5
6
|
|
|
6
7
|
module Contrast
|
|
7
8
|
module Utils
|
|
@@ -13,6 +14,7 @@ module Contrast
|
|
|
13
14
|
# https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/preflight.md
|
|
14
15
|
class HashDigest < Digest::Class
|
|
15
16
|
include Digest::Instance
|
|
17
|
+
extend Contrast::Utils::HashDigestExtend
|
|
16
18
|
|
|
17
19
|
CONTENT_LENGTH_HEADER = 'Content-Length'
|
|
18
20
|
CRYPTO_RULES = %w[crypto-bad-ciphers crypto-bad-mac].cs__freeze
|
|
@@ -21,76 +23,6 @@ module Contrast
|
|
|
21
23
|
CLASS_SOURCE_KEY = 'source'
|
|
22
24
|
CLASS_CONSTANT_NAME_KEY = 'name'
|
|
23
25
|
CLASS_LINE_NO_KEY = 'lineNo'
|
|
24
|
-
class << self
|
|
25
|
-
def generate_request_hash request
|
|
26
|
-
hash = new
|
|
27
|
-
hash.update(request.request_method)
|
|
28
|
-
hash.update(request.normalized_uri)
|
|
29
|
-
request.parameters.each_key do |name|
|
|
30
|
-
hash.update(name)
|
|
31
|
-
end
|
|
32
|
-
cl = request.headers[CONTENT_LENGTH_HEADER]
|
|
33
|
-
hash.update_on_content_length(cl) if cl
|
|
34
|
-
hash.finish
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def generate_event_hash finding, source, request
|
|
38
|
-
return generate_dataflow_hash(finding, request) if finding.events.length.to_i > 1
|
|
39
|
-
|
|
40
|
-
id = finding.rule_id
|
|
41
|
-
return generate_crypto_hash(finding, source, request) if CRYPTO_RULES.include?(id)
|
|
42
|
-
|
|
43
|
-
generate_trigger_hash(finding, request)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def generate_config_hash finding
|
|
47
|
-
hash = new
|
|
48
|
-
hash.update(finding.rule_id)
|
|
49
|
-
path = finding.properties[CONFIG_PATH_KEY]
|
|
50
|
-
hash.update(path)
|
|
51
|
-
method = finding.properties[CONFIG_SESSION_ID_KEY]
|
|
52
|
-
hash.update(method)
|
|
53
|
-
hash.finish
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def generate_class_scanning_hash finding
|
|
57
|
-
hash = new
|
|
58
|
-
hash.update(finding.rule_id)
|
|
59
|
-
module_name = finding.properties[CLASS_SOURCE_KEY]
|
|
60
|
-
hash.update(module_name)
|
|
61
|
-
# We're not currently collecting this. 30/7/19 HM
|
|
62
|
-
line_no = finding.properties[CLASS_LINE_NO_KEY]
|
|
63
|
-
hash.update(line_no)
|
|
64
|
-
field = finding.properties[CLASS_CONSTANT_NAME_KEY]
|
|
65
|
-
hash.update(field)
|
|
66
|
-
hash.finish
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
def generate_crypto_hash finding, algorithm, request
|
|
72
|
-
hash = new
|
|
73
|
-
hash.update(finding.rule_id)
|
|
74
|
-
hash.update(algorithm)
|
|
75
|
-
hash.update_on_request(finding, request)
|
|
76
|
-
hash.finish
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def generate_dataflow_hash finding, request
|
|
80
|
-
hash = new
|
|
81
|
-
hash.update(finding.rule_id)
|
|
82
|
-
hash.update_on_sources(finding.events)
|
|
83
|
-
hash.update_on_request(finding, request)
|
|
84
|
-
hash.finish
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def generate_trigger_hash finding, request
|
|
88
|
-
hash = new
|
|
89
|
-
hash.update(finding.rule_id)
|
|
90
|
-
hash.update_on_request(finding, request)
|
|
91
|
-
hash.finish
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
26
|
|
|
95
27
|
def update_on_request finding, request
|
|
96
28
|
if (route = finding.routes[0])
|
|
@@ -106,9 +38,14 @@ module Contrast
|
|
|
106
38
|
return unless events&.any?
|
|
107
39
|
|
|
108
40
|
events.each do |event|
|
|
109
|
-
event.
|
|
110
|
-
|
|
111
|
-
|
|
41
|
+
if event.cs__is_a?(Contrast::Api::Dtm::TraceEvent)
|
|
42
|
+
event.event_sources&.each do |source|
|
|
43
|
+
update(source.type)
|
|
44
|
+
update(source.name) # rubocop:disable Security/Module/Name
|
|
45
|
+
end
|
|
46
|
+
elsif event.cs__is_a?(Contrast::Agent::Assess::Events::SourceEvent)
|
|
47
|
+
update(event.source_type)
|
|
48
|
+
update(event.source_name)
|
|
112
49
|
end
|
|
113
50
|
end
|
|
114
51
|
end
|