contrast-agent 4.12.0 → 4.14.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +1 -0
- data/ext/cs__assess_module/cs__assess_module.c +48 -0
- data/ext/cs__assess_module/cs__assess_module.h +7 -0
- data/ext/cs__common/cs__common.c +5 -0
- data/ext/cs__common/cs__common.h +8 -0
- data/ext/cs__contrast_patch/cs__contrast_patch.c +16 -1
- data/ext/cs__os_information/cs__os_information.c +31 -0
- data/ext/cs__os_information/cs__os_information.h +7 -0
- data/ext/cs__os_information/extconf.rb +5 -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/propagation_method.rb +2 -116
- data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
- 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/source_method.rb +2 -71
- data/lib/contrast/agent/assess/policy/trigger_method.rb +45 -110
- 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 +53 -185
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
- data/lib/contrast/agent/deadzone/policy/policy.rb +1 -1
- data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +1 -0
- data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
- data/lib/contrast/agent/middleware.rb +14 -62
- 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/patch.rb +28 -235
- data/lib/contrast/agent/patching/policy/patcher.rb +14 -49
- 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/request_handler.rb +7 -3
- data/lib/contrast/agent/response.rb +2 -73
- data/lib/contrast/agent/startup_metrics_telemetry_event.rb +94 -0
- data/lib/contrast/agent/static_analysis.rb +5 -3
- data/lib/contrast/agent/telemetry.rb +137 -0
- data/lib/contrast/agent/telemetry_event.rb +33 -0
- data/lib/contrast/agent/thread_watcher.rb +66 -11
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +21 -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 +90 -0
- data/lib/contrast/components/app_context.rb +10 -41
- data/lib/contrast/components/app_context_extend.rb +78 -0
- data/lib/contrast/components/base.rb +23 -0
- data/lib/contrast/components/config.rb +92 -13
- data/lib/contrast/components/contrast_service.rb +11 -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 +27 -0
- 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 +18 -0
- 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/root_configuration.rb +1 -0
- data/lib/contrast/config/ruby_configuration.rb +6 -6
- data/lib/contrast/config/service_configuration.rb +2 -2
- data/lib/contrast/config.rb +1 -1
- data/lib/contrast/configuration.rb +4 -2
- data/lib/contrast/extension/assess/array.rb +5 -7
- data/lib/contrast/extension/thread.rb +31 -12
- data/lib/contrast/framework/manager.rb +22 -44
- data/lib/contrast/framework/manager_extend.rb +50 -0
- data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
- data/lib/contrast/framework/rails/patch/support.rb +31 -29
- data/lib/contrast/framework/rails/railtie.rb +1 -1
- data/lib/contrast/framework/sinatra/support.rb +2 -1
- data/lib/contrast/logger/application.rb +4 -0
- data/lib/contrast/logger/log.rb +8 -103
- data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
- data/lib/contrast/utils/assess/property/tagged_utils.rb +165 -0
- data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
- data/lib/contrast/utils/assess/tracking_util.rb +20 -15
- data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
- data/lib/contrast/utils/class_util.rb +18 -14
- data/lib/contrast/utils/exclude_key.rb +20 -0
- 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/metrics_hash.rb +59 -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/os.rb +23 -0
- data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
- data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -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 +79 -0
- data/lib/contrast/utils/telemetry_client.rb +90 -0
- data/lib/contrast/utils/telemetry_identifier.rb +130 -0
- data/lib/contrast.rb +18 -0
- data/ruby-agent.gemspec +7 -6
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +69 -22
- data/lib/contrast/config/default_value.rb +0 -17
@@ -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
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require 'contrast/utils/exclude_key'
|
5
|
+
|
4
6
|
module Contrast
|
5
7
|
module Logger
|
6
8
|
# Our decorator for the Ougai logger allowing for the logging of the
|
@@ -29,6 +31,8 @@ module Contrast
|
|
29
31
|
loggable = ::Contrast::CONFIG.loggable
|
30
32
|
info('Current configuration', configuration: loggable)
|
31
33
|
env_keys = ENV.keys.select do |env_key|
|
34
|
+
next if Contrast::Utils::ExcludeKey.excludable? env_key.to_s
|
35
|
+
|
32
36
|
env_key&.to_s&.start_with?(Contrast::Components::Config::CONTRAST_ENV_MARKER)
|
33
37
|
end
|
34
38
|
env_items = env_keys.map { |env_key| Contrast::Utils::EnvConfigurationItem.new(env_key, nil) }
|
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
|
@@ -0,0 +1,129 @@
|
|
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
|
+
module Contrast
|
5
|
+
module Utils
|
6
|
+
module Assess
|
7
|
+
# This module will include all methods for some internal validations in the PropagationMethod module
|
8
|
+
# and some other module methods from the same place, so we can ease the main module
|
9
|
+
module PropagationMethodUtils
|
10
|
+
APPEND_ACTION = 'APPEND'
|
11
|
+
CENTER_ACTION = 'CENTER'
|
12
|
+
INSERT_ACTION = 'INSERT'
|
13
|
+
KEEP_ACTION = 'KEEP'
|
14
|
+
NEXT_ACTION = 'NEXT'
|
15
|
+
NOOP_ACTION = 'NOOP'
|
16
|
+
PREPEND_ACTION = 'PREPEND'
|
17
|
+
REPLACE_ACTION = 'REPLACE'
|
18
|
+
REMOVE_ACTION = 'REMOVE'
|
19
|
+
REVERSE_ACTION = 'REVERSE'
|
20
|
+
SPLAT_ACTION = 'SPLAT'
|
21
|
+
SPLIT_ACTION = 'SPLIT'
|
22
|
+
DB_WRITE_ACTION = 'DB_WRITE'
|
23
|
+
CUSTOM_ACTION = 'CUSTOM'
|
24
|
+
|
25
|
+
ZERO_LENGTH_ACTIONS = [DB_WRITE_ACTION, CUSTOM_ACTION, KEEP_ACTION, REPLACE_ACTION, SPLAT_ACTION].cs__freeze
|
26
|
+
|
27
|
+
PROPAGATION_ACTIONS = {
|
28
|
+
APPEND_ACTION => Contrast::Agent::Assess::Policy::Propagator::Append,
|
29
|
+
CENTER_ACTION => Contrast::Agent::Assess::Policy::Propagator::Center,
|
30
|
+
INSERT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Insert,
|
31
|
+
KEEP_ACTION => Contrast::Agent::Assess::Policy::Propagator::Keep,
|
32
|
+
NEXT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Next,
|
33
|
+
NOOP_ACTION => nil,
|
34
|
+
PREPEND_ACTION => Contrast::Agent::Assess::Policy::Propagator::Prepend,
|
35
|
+
REPLACE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Replace,
|
36
|
+
REMOVE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Remove,
|
37
|
+
REVERSE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Reverse,
|
38
|
+
SPLAT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Splat,
|
39
|
+
SPLIT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Split
|
40
|
+
}.cs__freeze
|
41
|
+
|
42
|
+
def determine_target propagation_node, ret, object, args
|
43
|
+
target = propagation_node.targets[0]
|
44
|
+
case target
|
45
|
+
when Contrast::Utils::ObjectShare::OBJECT_KEY
|
46
|
+
object
|
47
|
+
when Contrast::Utils::ObjectShare::RETURN_KEY
|
48
|
+
ret
|
49
|
+
else
|
50
|
+
args[target]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Custom actions tend to be the more complex of our propagations. Often, the method has to make decisions
|
55
|
+
# about the target based on the context with which the method was called. As such, defer determining if the
|
56
|
+
# target is valid to that method.
|
57
|
+
#
|
58
|
+
# In all other cases, a target is valid for propagation if it is not nil
|
59
|
+
#
|
60
|
+
# @param target [Object] the thing to which to propagate
|
61
|
+
# @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
|
62
|
+
# propagation event.
|
63
|
+
# @return [Boolean]
|
64
|
+
def valid_target? target, propagation_node
|
65
|
+
return true if propagation_node.action == CUSTOM_ACTION
|
66
|
+
|
67
|
+
!!target
|
68
|
+
end
|
69
|
+
|
70
|
+
# If the action required needs a length and the target does not have one, the length is not valid
|
71
|
+
#
|
72
|
+
# @param target [Object] the thing to which to propagate
|
73
|
+
# @param action [String] the name of the action taken during this propagation
|
74
|
+
# @return [Boolean]
|
75
|
+
def valid_length? target, action
|
76
|
+
return true if ZERO_LENGTH_ACTIONS.include?(action)
|
77
|
+
|
78
|
+
if Contrast::Utils::DuckUtils.quacks_to?(target, :length)
|
79
|
+
target.length != 0 # rubocop:disable Style/ZeroLengthPredicate
|
80
|
+
else
|
81
|
+
!target.to_s.empty?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Before we do any work, we should check if we even need to. If the source and target of this patcher are
|
86
|
+
# not tracked, there's no need to do anything. A copy of nothing is still nothing.
|
87
|
+
#
|
88
|
+
# @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
|
89
|
+
# propagation event.
|
90
|
+
# @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to
|
91
|
+
# the invocation of the patched method.
|
92
|
+
# @param target [Object] the thing to which to propagate
|
93
|
+
# @return [Boolean]
|
94
|
+
def can_propagate? propagation_node, preshift, target
|
95
|
+
return false unless appropriate_target?(propagation_node, target)
|
96
|
+
return true if Contrast::Utils::Assess::TrackingUtil.tracked?(target)
|
97
|
+
return false unless preshift
|
98
|
+
|
99
|
+
propagation_node.sources.each do |source|
|
100
|
+
case source
|
101
|
+
when Contrast::Utils::ObjectShare::OBJECT_KEY
|
102
|
+
return true if Contrast::Utils::Assess::TrackingUtil.tracked?(preshift.object)
|
103
|
+
else
|
104
|
+
# has to be P, there's no ret source type (yet? ever?)
|
105
|
+
return true if preshift.args && Contrast::Utils::Assess::TrackingUtil.tracked?(preshift.args[source])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
# We cannot propagate to frozen things that have not been updated to work with our property tracking,
|
112
|
+
# unless they're duplicable and the return. We probably shouldn't propagate to frozen things at all, as
|
113
|
+
# they're supposed to be immutable, but third parties do jenky things, so allow it as long as it is safe to
|
114
|
+
# do.
|
115
|
+
#
|
116
|
+
# @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
|
117
|
+
# propagation event.
|
118
|
+
# @param target [Object] the Target to which to propagate.
|
119
|
+
# @return [Boolean] if the target can be propagated to
|
120
|
+
def appropriate_target? propagation_node, target
|
121
|
+
# special handle Returns b/c we can do unfreezing magic during propagation
|
122
|
+
return true if propagation_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY
|
123
|
+
|
124
|
+
Contrast::Agent::Assess::Tracker.trackable?(target)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,165 @@
|
|
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
|
+
module Contrast
|
5
|
+
module Utils
|
6
|
+
module Assess
|
7
|
+
# This module will include all methods for some internal validations in the Tagged property module
|
8
|
+
# and some other module methods from the same place, so we can ease the main module
|
9
|
+
# This module includes simple methods for the tags like
|
10
|
+
# adding tags, getting tags, deleting tags and similar
|
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
|
+
|
35
|
+
# Given a tag name and range object, add a new tag to this
|
36
|
+
# collection. If the given range touches an existing tag,
|
37
|
+
# we'll combine the two, adjusting the existing one and
|
38
|
+
# dropping this new one.
|
39
|
+
#
|
40
|
+
# @param label [String] the name of the tag
|
41
|
+
# @param range [Range] the Range that the tag covers, inclusive to
|
42
|
+
# exclusive
|
43
|
+
def add_tag label, range
|
44
|
+
length = range.end - range.begin
|
45
|
+
tag = Contrast::Agent::Assess::Tag.new(label, length, range.begin)
|
46
|
+
existing = fetch_tag(label)
|
47
|
+
tags[label] = Contrast::Utils::TagUtil.ordered_merge(existing, tag)
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_tags label, tag_ranges
|
51
|
+
tags[label] = tag_ranges
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a list of all current tags.
|
55
|
+
#
|
56
|
+
# @return [Hash<Contrast::Agent::Assess::Tag>]
|
57
|
+
def get_tags # rubocop:disable Naming/AccessorMethodName
|
58
|
+
return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
|
59
|
+
|
60
|
+
tags
|
61
|
+
end
|
62
|
+
|
63
|
+
# We'll use this as a helper method to retrieve tags from the hash.
|
64
|
+
# Because the hash auto-populates an empty array when we try to
|
65
|
+
# access a tag in it, we cannot use the [] method without side
|
66
|
+
# effect. To get around this, we'll use a fetch work around.
|
67
|
+
#
|
68
|
+
# @param label [Symbol] the label to look up
|
69
|
+
# @return [Array<Contrast::Agent::Assess::Tag>] all the tags with
|
70
|
+
# that label
|
71
|
+
def fetch_tag label
|
72
|
+
get_tags.fetch(label, nil) if tracked?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Remove all tags with a given label
|
76
|
+
def delete_tags label
|
77
|
+
tags.delete(label) if tracked?
|
78
|
+
end
|
79
|
+
|
80
|
+
# Reset the tag hash
|
81
|
+
def clear_tags
|
82
|
+
tags.clear if tracked?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns a list of all current tag labels, most likely to be used for
|
86
|
+
# a splat operation
|
87
|
+
def tag_keys
|
88
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
|
89
|
+
|
90
|
+
tags.keys
|
91
|
+
end
|
92
|
+
|
93
|
+
# Calls merge to combine touching or overlapping tags
|
94
|
+
# Deletes empty tags
|
95
|
+
def cleanup_tags
|
96
|
+
return unless tracked?
|
97
|
+
|
98
|
+
Contrast::Utils::TagUtil.merge_tags(tags)
|
99
|
+
tags.delete_if { |_, value| value.empty? }
|
100
|
+
end
|
101
|
+
|
102
|
+
# Find all of the ranges that span a given index. This is used
|
103
|
+
# in propagation when we need to shift tags about. For instance, in
|
104
|
+
# the append method when we need to see if any tag at the end needs
|
105
|
+
# to be expanded out to the size of the new String.
|
106
|
+
#
|
107
|
+
# Note: Tags do not know their key, so this is only the range covered
|
108
|
+
#
|
109
|
+
# @param idx [Integer] the index to check for tags
|
110
|
+
# @return [Array<Contrast::Agent::Assess::Tag>] a set of all the Tags
|
111
|
+
# covering the given index. This is only the ranges, not the names.
|
112
|
+
def tags_at idx
|
113
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
|
114
|
+
|
115
|
+
at = []
|
116
|
+
tags.each_value do |tag_array|
|
117
|
+
tag_array.each do |tag|
|
118
|
+
if tag.covers?(idx)
|
119
|
+
at << tag
|
120
|
+
elsif tag.above?(idx)
|
121
|
+
break
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
at
|
126
|
+
end
|
127
|
+
|
128
|
+
# given a range, select all tags in that range the selected tags are
|
129
|
+
# shifted such that the start index of the new tag (0) aligns with
|
130
|
+
# the given start index in the range
|
131
|
+
#
|
132
|
+
# current tags: 5-15
|
133
|
+
# range : 5-10
|
134
|
+
# result : 0-05
|
135
|
+
#
|
136
|
+
# Note that we disable Lint/DuplicateBranch in this branch in order
|
137
|
+
# list out all tag range cases in the proper order to make this
|
138
|
+
# easier to understand
|
139
|
+
#
|
140
|
+
# @param range [Range] the span to check, inclusive to exclusive
|
141
|
+
# @return [Hash{String => Contrast::Agent::Assess::Tag}] the hash of
|
142
|
+
# key to tags
|
143
|
+
def tags_at_range range
|
144
|
+
return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
|
145
|
+
|
146
|
+
at = Hash.new { |h, k| h[k] = [] }
|
147
|
+
tags.each_pair do |key, value|
|
148
|
+
add = nil
|
149
|
+
value.each do |tag|
|
150
|
+
within_range = resize_to_range(tag, range)
|
151
|
+
if within_range
|
152
|
+
add ||= []
|
153
|
+
add << within_range
|
154
|
+
end
|
155
|
+
end
|
156
|
+
next unless add&.any?
|
157
|
+
|
158
|
+
at[key] = add
|
159
|
+
end
|
160
|
+
at
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,83 @@
|
|
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
|
+
module Contrast
|
5
|
+
module Utils
|
6
|
+
module Assess
|
7
|
+
# This module will include all methods for some internal validations in the SourceMethod module
|
8
|
+
# and some other module methods from the same place, so we can ease the main module
|
9
|
+
module SourceMethodUtils
|
10
|
+
# Safely duplicate the target, or return nil
|
11
|
+
#
|
12
|
+
# @param target [Object] the thing to check for duplication
|
13
|
+
def safe_dup target
|
14
|
+
target.dup
|
15
|
+
rescue StandardError => _e
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Find the name of the source
|
20
|
+
#
|
21
|
+
# @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the node to direct applying this source
|
22
|
+
# event
|
23
|
+
# @param object [Object] the Object on which the method was invoked
|
24
|
+
# @param ret [Object] the Return of the invoked method
|
25
|
+
# @param args [Array<Object>] the Arguments with which the method was invoked
|
26
|
+
# @return [String, nil] the human readable name of the target to which this source event applies, or nil if
|
27
|
+
# none provided by the node
|
28
|
+
def determine_source_name source_node, object, ret, *args
|
29
|
+
return source_node.get_property('dynamic_source_name') if source_node.type == 'UNTRUSTED_DATABASE'
|
30
|
+
|
31
|
+
source_node_source = source_node.sources[0]
|
32
|
+
case source_node_source
|
33
|
+
when nil
|
34
|
+
nil
|
35
|
+
when Contrast::Utils::ObjectShare::RETURN_KEY
|
36
|
+
ret
|
37
|
+
when Contrast::Utils::ObjectShare::OBJECT_KEY
|
38
|
+
object
|
39
|
+
else
|
40
|
+
args[source_node_source]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Determine if we should analyze this method invocation for a Source or not. We should if we have enough
|
45
|
+
# information to build the context of this invocation, we're not disabled, and we can't immediately
|
46
|
+
# determine the invocation was done safely.
|
47
|
+
#
|
48
|
+
# @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] the policy that applies to the
|
49
|
+
# method being called
|
50
|
+
# @param object [Object] the Object on which the method was invoked
|
51
|
+
# @param ret [Object] the Return of the invoked method
|
52
|
+
# @param args [Array<Object>] the Arguments with which the method was invoked
|
53
|
+
# @return [boolean] if the invocation of this method should be analyzed
|
54
|
+
def analyze? method_policy, object, ret, args
|
55
|
+
return false unless method_policy&.source_node
|
56
|
+
return false unless ::Contrast::ASSESS.enabled?
|
57
|
+
return false unless Contrast::Agent::REQUEST_TRACKER.current&.analyze_request?
|
58
|
+
|
59
|
+
!safe_invocation?(method_policy.source_node, object, ret, args)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Determine if the method was invoked safely.
|
63
|
+
#
|
64
|
+
# @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the node to direct applying this source
|
65
|
+
# event
|
66
|
+
# @param _object [Object] the Object on which the method was invoked
|
67
|
+
# @param _ret [Object] the Return of the invoked method
|
68
|
+
# @param args [Array<Object>] the Arguments with which the method was invoked
|
69
|
+
# @return [boolean] if the invocation of this method was safe
|
70
|
+
def safe_invocation? source_node, _object, _ret, args
|
71
|
+
# According the the Rack Specification https://github.com/rack/rack/blob/master/SPEC.rdoc, any header
|
72
|
+
# from the Request will start with HTTP_. As such, only Headers with that key should be considered for
|
73
|
+
# tracking, as the others have come from the Framework or Middleware stashing in the ENV. Rails, for
|
74
|
+
# instance, uses action_dispatch. to store several values. Technically, you can't call
|
75
|
+
# Rack::Request#get_header without a parameter, and that parameter should be a String, but trust no one.
|
76
|
+
source_node.id == 'Assess:Source:Rack::Request::Env#get_header' &&
|
77
|
+
args&.any? &&
|
78
|
+
!args[0].to_s.start_with?('HTTP_')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -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
|