contrast-agent 4.10.0 → 4.13.1
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/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 +24 -7
- data/ext/cs__common/cs__common.h +12 -2
- data/ext/cs__contrast_patch/cs__contrast_patch.c +48 -11
- data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -2
- 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__protect_kernel → cs__os_information}/extconf.rb +0 -0
- data/lib/contrast/agent/assess/contrast_event.rb +1 -1
- data/lib/contrast/agent/assess/contrast_object.rb +1 -4
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
- data/lib/contrast/agent/assess/policy/preshift.rb +25 -11
- 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/database_write.rb +2 -0
- data/lib/contrast/agent/assess/policy/propagator/match_data.rb +4 -4
- data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
- data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
- data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -107
- data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
- data/lib/contrast/agent/assess/property/tagged.rb +15 -132
- data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
- data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +2 -1
- data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
- data/lib/contrast/agent/middleware.rb +22 -0
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +0 -1
- data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
- data/lib/contrast/agent/patching/policy/patch.rb +37 -238
- data/lib/contrast/agent/patching/policy/patcher.rb +3 -42
- data/lib/contrast/agent/request.rb +5 -3
- data/lib/contrast/agent/request_context.rb +32 -11
- data/lib/contrast/agent/request_handler.rb +7 -3
- data/lib/contrast/agent/rule_set.rb +2 -4
- data/lib/contrast/agent/scope.rb +32 -20
- data/lib/contrast/agent/startup_metrics_telemetry_event.rb +71 -0
- data/lib/contrast/agent/static_analysis.rb +4 -2
- data/lib/contrast/agent/telemetry.rb +129 -0
- data/lib/contrast/agent/telemetry_event.rb +34 -0
- data/lib/contrast/agent/thread_watcher.rb +43 -14
- data/lib/contrast/agent/tracepoint_hook.rb +11 -3
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +6 -1
- data/lib/contrast/components/api.rb +34 -0
- data/lib/contrast/components/app_context.rb +24 -0
- data/lib/contrast/components/assess.rb +7 -0
- data/lib/contrast/components/config.rb +90 -11
- data/lib/contrast/components/contrast_service.rb +6 -0
- data/lib/contrast/config/api_configuration.rb +22 -0
- data/lib/contrast/config/assess_configuration.rb +1 -0
- data/lib/contrast/config/env_variables.rb +25 -0
- data/lib/contrast/config/root_configuration.rb +1 -0
- data/lib/contrast/config/service_configuration.rb +2 -1
- data/lib/contrast/config.rb +1 -0
- data/lib/contrast/configuration.rb +3 -0
- data/lib/contrast/framework/manager.rb +14 -12
- 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/logger/application.rb +4 -0
- data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
- data/lib/contrast/utils/assess/property/tagged_utils.rb +142 -0
- data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
- data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
- data/lib/contrast/utils/class_util.rb +58 -44
- data/lib/contrast/utils/exclude_key.rb +20 -0
- data/lib/contrast/utils/io_util.rb +42 -34
- data/lib/contrast/utils/lru_cache.rb +45 -0
- data/lib/contrast/utils/metrics_hash.rb +59 -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/requests_client.rb +150 -0
- data/lib/contrast/utils/ruby_ast_rewriter.rb +1 -1
- data/lib/contrast/utils/telemetry.rb +77 -0
- data/lib/contrast/utils/telemetry_identifier.rb +137 -0
- data/lib/contrast.rb +19 -1
- data/resources/assess/policy.json +12 -6
- data/resources/deadzone/policy.json +86 -5
- data/ruby-agent.gemspec +2 -1
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +32 -14
- data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
- data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
- data/lib/contrast/extension/protect/kernel.rb +0 -29
@@ -0,0 +1,26 @@
|
|
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/utils/metrics_hash'
|
5
|
+
require 'contrast/agent/telemetry_event'
|
6
|
+
|
7
|
+
module Contrast
|
8
|
+
module Agent
|
9
|
+
# This class will hold the basic information for a Telemetry Event
|
10
|
+
class MetricTelemetryEvent < Contrast::Agent::TelemetryEvent
|
11
|
+
include Contrast::Utils
|
12
|
+
|
13
|
+
attr_reader :fields
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
super
|
17
|
+
@fields = MetricsHash.new(Numeric)
|
18
|
+
@fields['_filter'] = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_json **_args
|
22
|
+
super.merge!({ fields: @fields })
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -10,8 +10,10 @@ require 'contrast/utils/object_share'
|
|
10
10
|
require 'contrast/components/logger'
|
11
11
|
require 'contrast/components/scope'
|
12
12
|
require 'contrast/utils/heap_dump_util'
|
13
|
+
require 'contrast/utils/telemetry'
|
13
14
|
require 'contrast/agent/request_handler'
|
14
15
|
require 'contrast/agent/static_analysis'
|
16
|
+
require 'contrast/agent/startup_metrics_telemetry_event'
|
15
17
|
|
16
18
|
require 'contrast/utils/timer'
|
17
19
|
|
@@ -68,6 +70,7 @@ module Contrast
|
|
68
70
|
::Contrast::SETTINGS.reset_state
|
69
71
|
|
70
72
|
inform_deprecations
|
73
|
+
telemetry_disclaimer
|
71
74
|
|
72
75
|
if ::Contrast::CONFIG.invalid?
|
73
76
|
::Contrast::AGENT.disable!
|
@@ -89,6 +92,13 @@ module Contrast
|
|
89
92
|
Contrast::Agent.thread_watcher.ensure_running?
|
90
93
|
end
|
91
94
|
|
95
|
+
if Contrast::Agent::Telemetry.enabled?
|
96
|
+
logger.debug_with_time('middleware: sending startup metrics telemetry event') do
|
97
|
+
event = Contrast::Agent::StartupMetricsTelemetryEvent.new
|
98
|
+
Contrast::Agent.thread_watcher.telemetry_queue.send_event(event)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
92
102
|
logger.debug_with_time('middleware: instrument shared libraries and patch') do
|
93
103
|
Contrast::Agent::Patching::Policy::Patcher.patch
|
94
104
|
end
|
@@ -225,6 +235,18 @@ module Contrast
|
|
225
235
|
Kernel.warn('[Contrast Security] [DEPRECATION] Support for Ruby 2.5 will be removed in April 2021. '\
|
226
236
|
'Please contact Customer Support prior if you require continued support.')
|
227
237
|
end
|
238
|
+
|
239
|
+
# Displays Telemetry disclaimer if Telemetry is enabled.
|
240
|
+
# if .telemetry file doesn't exist we create one and then show the disclaimer.
|
241
|
+
# if the file already exists we do nothing.
|
242
|
+
def telemetry_disclaimer
|
243
|
+
return unless Contrast::Agent::Telemetry.enabled?
|
244
|
+
return unless Contrast::Utils::Telemetry.create_telemetry_file
|
245
|
+
|
246
|
+
logger.info Contrast::Utils::Telemetry.disclaimer
|
247
|
+
$stdout.print Contrast::Utils::Telemetry.disclaimer
|
248
|
+
true
|
249
|
+
end
|
228
250
|
end
|
229
251
|
end
|
230
252
|
end
|
@@ -46,7 +46,6 @@ module Contrast
|
|
46
46
|
path_part = "cs__assess_#{ p }"
|
47
47
|
Contrast::Extension::Assess::InstrumentHelper.instrument "#{ path_part }/#{ path_part }"
|
48
48
|
end
|
49
|
-
Contrast::Extension::Assess::InstrumentHelper.instrument 'cs__protect_kernel/cs__protect_kernel'
|
50
49
|
true
|
51
50
|
end
|
52
51
|
end
|
@@ -81,15 +81,21 @@ module Contrast
|
|
81
81
|
deadzone_node = find_method_node(module_policy.deadzone_nodes, method_name, instance_method)
|
82
82
|
method_visibility = find_visibility(source_node, propagation_node, trigger_node, protect_node,
|
83
83
|
inventory_node, deadzone_node)
|
84
|
-
MethodPolicy.new(method_name: method_name,
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
84
|
+
method_policy = MethodPolicy.new(method_name: method_name,
|
85
|
+
method_visibility: method_visibility,
|
86
|
+
instance_method: instance_method,
|
87
|
+
source_node: source_node,
|
88
|
+
propagation_node: propagation_node,
|
89
|
+
trigger_node: trigger_node,
|
90
|
+
protect_node: protect_node,
|
91
|
+
inventory_node: inventory_node,
|
92
|
+
deadzone_node: deadzone_node)
|
93
|
+
|
94
|
+
return method_policy unless check_method_policy_nodes_empty? source_node, propagation_node, trigger_node,
|
95
|
+
protect_node, inventory_node, deadzone_node
|
96
|
+
|
97
|
+
create_new_node(module_policy, method_policy) if module_policy.deadzone_nodes&.any?
|
98
|
+
method_policy
|
93
99
|
end
|
94
100
|
|
95
101
|
def find_method_node nodes, method_name, is_instance_method
|
@@ -103,6 +109,45 @@ module Contrast
|
|
103
109
|
def find_visibility *nodes
|
104
110
|
nodes.find { |node| node }&.method_visibility
|
105
111
|
end
|
112
|
+
|
113
|
+
def check_method_policy_nodes_empty?(source_node, propagation_node, trigger_node, protect_node,
|
114
|
+
inventory_node, deadzone_node)
|
115
|
+
return false unless source_node.nil? && propagation_node.nil? && trigger_node.nil? && protect_node.nil? &&
|
116
|
+
inventory_node.nil? && deadzone_node.nil?
|
117
|
+
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def create_new_node module_policy, method_policy
|
124
|
+
return if module_policy.deadzone_nodes.empty?
|
125
|
+
|
126
|
+
module_policy.deadzone_nodes.map do |node|
|
127
|
+
next unless node.method_name.nil?
|
128
|
+
|
129
|
+
klass = Module.cs__const_get(node.class_name)
|
130
|
+
next unless it_defined? klass, method_policy.method_name
|
131
|
+
|
132
|
+
new_node = {}
|
133
|
+
new_node['instance_method'] = method_policy.instance_method
|
134
|
+
new_node['method_visibility'] =
|
135
|
+
klass.private_method_defined?(method_policy.method_name) ? 'private' : 'public'
|
136
|
+
new_node['method_name'] = method_policy.method_name
|
137
|
+
new_node['class_name'] = node.class_name
|
138
|
+
new_node = Contrast::Agent::Deadzone::Policy::DeadzoneNode.new(new_node)
|
139
|
+
method_policy.instance_variable_set(:@method_visibility, new_node.method_visibility)
|
140
|
+
method_policy.instance_variable_set(:@deadzone_node, new_node)
|
141
|
+
module_policy.deadzone_nodes << new_node
|
142
|
+
break unless method_policy.deadzone_node.nil?
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def it_defined? klass, method_name
|
147
|
+
klass.instance_methods(false).include?(method_name) ||
|
148
|
+
klass.private_instance_methods(false).include?(method_name) ||
|
149
|
+
klass.singleton_methods(false).include?(method_name)
|
150
|
+
end
|
106
151
|
end
|
107
152
|
end
|
108
153
|
end
|
@@ -4,6 +4,7 @@
|
|
4
4
|
require 'monitor'
|
5
5
|
require 'contrast/components/logger'
|
6
6
|
require 'contrast/components/scope'
|
7
|
+
require 'contrast/utils/patching/policy/patch_utils'
|
7
8
|
|
8
9
|
require 'contrast/agent'
|
9
10
|
require 'contrast/logger/log'
|
@@ -32,6 +33,8 @@ module Contrast
|
|
32
33
|
# provides a map for which methods our renamed functions need to call
|
33
34
|
# and how.
|
34
35
|
module Patch
|
36
|
+
extend Contrast::Utils::Patching::PatchUtils
|
37
|
+
|
35
38
|
class << self
|
36
39
|
include Contrast::Agent::Assess::Policy::SourceMethod
|
37
40
|
include Contrast::Agent::Assess::Policy::PropagationMethod
|
@@ -57,226 +60,6 @@ module Contrast
|
|
57
60
|
end
|
58
61
|
end
|
59
62
|
|
60
|
-
# THIS IS CALLED FROM C. Do not change the signature lightly.
|
61
|
-
#
|
62
|
-
# This method functions to call the infilter methods from our
|
63
|
-
# patches, allowing for analysis and reporting at the point just
|
64
|
-
# before the patched code is invoked.
|
65
|
-
#
|
66
|
-
# @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
|
67
|
-
# Mapping of the triggers on the given method.
|
68
|
-
# @param method [Symbol] The method into which we're patching
|
69
|
-
# @param exception [StandardError] Any exception raised during the
|
70
|
-
# call of the patched method.
|
71
|
-
# @param object [Object] The object on which the method is invoked,
|
72
|
-
# typically what would be returned by self.
|
73
|
-
# @param args [Array<Object>] The arguments passed to the method
|
74
|
-
# being invoked.
|
75
|
-
def apply_pre_patch method_policy, method, exception, object, args
|
76
|
-
apply_protect(method_policy, method, exception, object, args)
|
77
|
-
apply_inventory(method_policy, method, exception, object, args)
|
78
|
-
rescue Contrast::SecurityException => e
|
79
|
-
# We were told to block something, so we gotta. Don't catch this
|
80
|
-
# one, let it get back to our Middleware or even all the way out to
|
81
|
-
# the framework
|
82
|
-
raise e
|
83
|
-
rescue StandardError => e
|
84
|
-
# Anything else was our bad and we gotta catch that to allow for
|
85
|
-
# normal application flow
|
86
|
-
logger.error('Unable to apply pre patch to method.', e)
|
87
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
88
|
-
# This is something like NoMemoryError that we can't
|
89
|
-
# hope to handle. Nonetheless, shouldn't leak scope.
|
90
|
-
exit_contrast_scope!
|
91
|
-
raise e
|
92
|
-
end
|
93
|
-
|
94
|
-
# THIS IS CALLED FROM C. Do not change the signature lightly.
|
95
|
-
#
|
96
|
-
# This method functions to call the infilter methods from our
|
97
|
-
# patches, allowing for analysis and reporting at the point just
|
98
|
-
# after the patched code is invoked
|
99
|
-
#
|
100
|
-
# @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
|
101
|
-
# Mapping of the triggers on the given method.
|
102
|
-
# @param preshift [Contrast::Agent::Assess::PreShift] The capture
|
103
|
-
# of the state of the code just prior to the invocation of the
|
104
|
-
# patched method.
|
105
|
-
# @param object [Object] The object on which the method was
|
106
|
-
# invoked, typically what would be returned by self.
|
107
|
-
# @param ret [Object] The return of the method that was invoked.
|
108
|
-
# @param args [Array<Object>] The arguments passed to the method
|
109
|
-
# being invoked.
|
110
|
-
# @param block [Proc] The block passed to the method that was
|
111
|
-
# invoked.
|
112
|
-
def apply_post_patch method_policy, preshift, object, ret, args, block
|
113
|
-
apply_assess(method_policy, preshift, object, ret, args, block)
|
114
|
-
rescue StandardError => e
|
115
|
-
logger.error('Unable to apply post patch to method.', e)
|
116
|
-
end
|
117
|
-
|
118
|
-
# Apply the Protect patch which applies to the given method.
|
119
|
-
#
|
120
|
-
# @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
|
121
|
-
# Mapping of the triggers on the given method.
|
122
|
-
# @param method [Symbol] The method into which we're patching
|
123
|
-
# @param exception [StandardError] Any exception raised during the
|
124
|
-
# call of the patched method.
|
125
|
-
# @param object [Object] The object on which the method is invoked,
|
126
|
-
# typically what would be returned by self.
|
127
|
-
# @param args [Array<Object>] The arguments passed to the method
|
128
|
-
# being invoked.
|
129
|
-
def apply_protect method_policy, method, exception, object, args
|
130
|
-
return unless ::Contrast::AGENT.enabled?
|
131
|
-
return unless ::Contrast::PROTECT.enabled?
|
132
|
-
|
133
|
-
apply_trigger_only(method_policy&.protect_node, method, exception, object, args)
|
134
|
-
end
|
135
|
-
|
136
|
-
# Apply the Inventory patch which applies to the given method.
|
137
|
-
#
|
138
|
-
# @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
|
139
|
-
# Mapping of the triggers on the given method.
|
140
|
-
# @param method [Symbol] The method into which we're patching
|
141
|
-
# @param exception [StandardError] Any exception raised during the
|
142
|
-
# call of the patched method.
|
143
|
-
# @param object [Object] The object on which the method is invoked,
|
144
|
-
# typically what would be returned by self.
|
145
|
-
# @param args [Array<Object>] The arguments passed to the method
|
146
|
-
# being invoked.
|
147
|
-
def apply_inventory method_policy, method, exception, object, args
|
148
|
-
return unless ::Contrast::INVENTORY.enabled?
|
149
|
-
|
150
|
-
apply_trigger_only(method_policy&.inventory_node, method, exception, object, args)
|
151
|
-
end
|
152
|
-
|
153
|
-
# Apply the Assess patches which apply to the given method.
|
154
|
-
#
|
155
|
-
# @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
|
156
|
-
# Mapping of the triggers on the given method.
|
157
|
-
# @param preshift [Contrast::Agent::Assess::PreShift] The capture
|
158
|
-
# of the state of the code just prior to the invocation of the
|
159
|
-
# patched method.
|
160
|
-
# @param object [Object] The object on which the method was
|
161
|
-
# invoked, typically what would be returned by self.
|
162
|
-
# @param ret [Object] The return of the method that was invoked.
|
163
|
-
# @param args [Array<Object>] The arguments passed to the method
|
164
|
-
# being invoked.
|
165
|
-
# @param block [Proc] The block passed to the method that was
|
166
|
-
# invoked.
|
167
|
-
def apply_assess method_policy, preshift, object, ret, args, block
|
168
|
-
source_ret = nil
|
169
|
-
propagated_ret = nil
|
170
|
-
return ret unless method_policy && ::Contrast::ASSESS.enabled?
|
171
|
-
|
172
|
-
current_context = Contrast::Agent::REQUEST_TRACKER.current
|
173
|
-
return ret if current_context && !current_context.analyze_request?
|
174
|
-
|
175
|
-
trigger_node = method_policy.trigger_node
|
176
|
-
if trigger_node
|
177
|
-
Contrast::Agent::Assess::Policy::TriggerMethod.apply_trigger_rule(trigger_node, object, ret, args)
|
178
|
-
end
|
179
|
-
if method_policy.source_node
|
180
|
-
# If we were given a frozen return, and it was the target of a
|
181
|
-
# source, and we have frozen sources enabled, we'll need to
|
182
|
-
# replace the return. Note, this is not the default case.
|
183
|
-
source_ret = Contrast::Agent::Assess::Policy::SourceMethod.source_patchers(method_policy, object, ret,
|
184
|
-
args)
|
185
|
-
end
|
186
|
-
if method_policy.propagation_node
|
187
|
-
propagated_ret = Contrast::Agent::Assess::Policy::PropagationMethod.apply_propagation(
|
188
|
-
method_policy,
|
189
|
-
preshift,
|
190
|
-
object,
|
191
|
-
source_ret || ret,
|
192
|
-
args,
|
193
|
-
block)
|
194
|
-
end
|
195
|
-
handle_return(propagated_ret, source_ret, ret)
|
196
|
-
rescue StandardError => e
|
197
|
-
logger.error('Unable to assess method call.', e)
|
198
|
-
handle_return(propagated_ret, source_ret, ret)
|
199
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
200
|
-
logger.error('Unable to assess method call.', e)
|
201
|
-
handle_return(propagated_ret, source_ret, ret)
|
202
|
-
raise e
|
203
|
-
end
|
204
|
-
|
205
|
-
# Generic invocation of the Inventory or Protect patch which apply
|
206
|
-
# to the given method.
|
207
|
-
#
|
208
|
-
# @param trigger_node [Contrast::Agent::Inventory::Policy::TriggerNode]
|
209
|
-
# Mapping of the specific trigger on the given method.
|
210
|
-
# @param method [Symbol] The method into which we're patching
|
211
|
-
# @param exception [StandardError] Any exception raised during the
|
212
|
-
# call of the patched method.
|
213
|
-
# @param object [Object] The object on which the method is invoked,
|
214
|
-
# typically what would be returned by self.
|
215
|
-
# @param args [Array<Object>] The arguments passed to the method
|
216
|
-
# being invoked.
|
217
|
-
def apply_trigger_only trigger_node, method, exception, object, args
|
218
|
-
return unless trigger_node
|
219
|
-
|
220
|
-
# If that rule only applies in the case of an exception being
|
221
|
-
# thrown and there's no exception here, move along, or vice versa
|
222
|
-
return if trigger_node.on_exception && !exception
|
223
|
-
return if !trigger_node.on_exception && exception
|
224
|
-
|
225
|
-
# Each patch has an applicator that handles logic for it. Think
|
226
|
-
# of this as being similar to propagator actions, most closely
|
227
|
-
# resembling CUSTOM - they all have a common interface but their
|
228
|
-
# own logic based on what's in the method(s) they've been patched
|
229
|
-
# into.
|
230
|
-
# Each patch also knows the method of its applicator. Some
|
231
|
-
# things, like AppliesXxeRule, have different methods depending
|
232
|
-
# on the library patched. This lets us handle the boilerplate of
|
233
|
-
# patching while still allowing for custom handling of the
|
234
|
-
# methods.
|
235
|
-
applicator_method = trigger_node.applicator_method
|
236
|
-
# By calling send like this, we can reuse all the patching.
|
237
|
-
# We `send` to the given method of the given class
|
238
|
-
# (applicator) since they all accept the same inputs
|
239
|
-
trigger_node.applicator.send(applicator_method, method, exception, trigger_node.properties, object, args)
|
240
|
-
end
|
241
|
-
|
242
|
-
# Method to choose which replaced return from the post_patch to
|
243
|
-
# actually return
|
244
|
-
#
|
245
|
-
# @param propagated_ret [Object, nil] The replaced return from the
|
246
|
-
# propagation patch.
|
247
|
-
# @param source_ret [Object, nil] The replaced return from the
|
248
|
-
# source patch.
|
249
|
-
# @param ret [Object, nil] The original return of the patched
|
250
|
-
# method.
|
251
|
-
# @return [Object, nil] The thing to return from the post patch.
|
252
|
-
def handle_return propagated_ret, source_ret, ret
|
253
|
-
safe_return = propagated_ret || source_ret || ret
|
254
|
-
safe_return.rewind if Contrast::Utils::IOUtil.should_rewind?(safe_return)
|
255
|
-
safe_return
|
256
|
-
end
|
257
|
-
|
258
|
-
# Given a module and method, construct an expected name for the
|
259
|
-
# alias by which Contrast will reference the original.
|
260
|
-
#
|
261
|
-
# @param patched_class [Module] the module being patched
|
262
|
-
# @param patched_method [Symbol] the method being patched
|
263
|
-
# @return [Symbol]
|
264
|
-
def build_method_name patched_class, patched_method
|
265
|
-
(Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START +
|
266
|
-
patched_class.cs__name.gsub('::', '_').downcase +
|
267
|
-
Contrast::Utils::ObjectShare::UNDERSCORE +
|
268
|
-
patched_method.to_s).to_sym
|
269
|
-
end
|
270
|
-
|
271
|
-
# Given a method, return a symbol in the format
|
272
|
-
# <method_start>_unbound_<method_name>
|
273
|
-
def build_unbound_method_name patcher_method
|
274
|
-
(Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START +
|
275
|
-
'unbound' +
|
276
|
-
Contrast::Utils::ObjectShare::UNDERSCORE +
|
277
|
-
patcher_method.to_s).to_sym
|
278
|
-
end
|
279
|
-
|
280
63
|
# @param mod [Module] the module in which the patch should be
|
281
64
|
# placed.
|
282
65
|
# @param methods [Array(Symbol)] all the instance or singleton
|
@@ -343,7 +126,7 @@ module Contrast
|
|
343
126
|
underlying_method_name = build_unbound_method_name(method_name).to_sym
|
344
127
|
|
345
128
|
target_module = Module.cs__const_get(target_module_name)
|
346
|
-
|
129
|
+
target_module = target_module.cs__singleton_class if %i[prepend_singleton prepend].include? impl
|
347
130
|
target_module = target_module.cs__singleton_class if %i[alias_singleton prepend].include? impl
|
348
131
|
|
349
132
|
visibility = if target_module.private_instance_methods(false).include?(method_name)
|
@@ -360,6 +143,30 @@ module Contrast
|
|
360
143
|
ERR
|
361
144
|
end
|
362
145
|
|
146
|
+
reflect_implementation impl, target_module, unbound_method, visibility
|
147
|
+
# Ougai::Logger.create_item_with_2args calls Hash#[]=, so we
|
148
|
+
# can't invoke this logging method or we'll seg fault as we'd
|
149
|
+
# change the method definition mid-call
|
150
|
+
# if method_name != :[]= &&
|
151
|
+
# Contrast::Agent::Logger.defined!
|
152
|
+
# logger.trace(
|
153
|
+
# 'Registered C-defined patch',
|
154
|
+
# implementation: impl,
|
155
|
+
# module: target_mod,
|
156
|
+
# method: method_name,
|
157
|
+
# visibility: visibility)
|
158
|
+
# end
|
159
|
+
underlying_method_name
|
160
|
+
end
|
161
|
+
|
162
|
+
# @param impl [Symbol] Strategy for applying the patch: { :alias_instance, :alias_singleton, or :prepend }:
|
163
|
+
# @param target_module [Module] The targeted module
|
164
|
+
# @param unbound_method [UnboundMethod] An unbound method, to be patched into target_module.
|
165
|
+
# @param visibility [Symbol] method visibility
|
166
|
+
def reflect_implementation impl, target_module, unbound_method, visibility
|
167
|
+
method_name = unbound_method.name.to_sym # rubocop:disable Security/Module/Name -- ruby built in attribute.
|
168
|
+
underlying_method_name = build_unbound_method_name(method_name).to_sym
|
169
|
+
|
363
170
|
case impl
|
364
171
|
when :alias_instance, :alias_singleton
|
365
172
|
# Core to patching. Ignore define method usage cop.
|
@@ -370,27 +177,19 @@ module Contrast
|
|
370
177
|
target_module.send(:define_method, method_name, unbound_method.bind(target_module))
|
371
178
|
end
|
372
179
|
target_module.send(visibility, method_name) # e.g., module.private(:my_method)
|
373
|
-
when :
|
374
|
-
|
375
|
-
|
376
|
-
|
180
|
+
when :prepend_instance, :prepend_singleton
|
181
|
+
|
182
|
+
unless target_module.instance_methods(false).include? underlying_method_name
|
183
|
+
|
184
|
+
prepending_module = Module.new
|
185
|
+
prepending_module.send(:define_method, method_name, unbound_method.bind(target_module))
|
186
|
+
prepending_module.send(visibility, method_name)
|
187
|
+
|
188
|
+
end
|
377
189
|
# This prepends to the singleton class (it patches a class method)
|
378
190
|
target_module.prepend prepending_module
|
379
191
|
# rubocop:enable Performance/Kernel/DefineMethod
|
380
192
|
end
|
381
|
-
# Ougai::Logger.create_item_with_2args calls Hash#[]=, so we
|
382
|
-
# can't invoke this logging method or we'll seg fault as we'd
|
383
|
-
# change the method definition mid-call
|
384
|
-
# if method_name != :[]= &&
|
385
|
-
# Contrast::Agent::Logger.defined!
|
386
|
-
# logger.trace(
|
387
|
-
# 'Registered C-defined patch',
|
388
|
-
# implementation: impl,
|
389
|
-
# module: target_module_name,
|
390
|
-
# method: method_name,
|
391
|
-
# visibility: visibility)
|
392
|
-
# end
|
393
|
-
underlying_method_name
|
394
193
|
end
|
395
194
|
|
396
195
|
# @return [Boolean]
|
@@ -10,6 +10,7 @@ require 'contrast/agent/patching/policy/module_policy'
|
|
10
10
|
require 'contrast/components/logger'
|
11
11
|
require 'contrast/components/scope'
|
12
12
|
require 'contrast/utils/class_util'
|
13
|
+
require 'contrast/utils/patching/policy/patcher_utils'
|
13
14
|
|
14
15
|
# assess
|
15
16
|
require 'contrast/agent/assess/policy/policy'
|
@@ -44,6 +45,7 @@ module Contrast
|
|
44
45
|
# and how.
|
45
46
|
module Patcher
|
46
47
|
extend Contrast::Agent::Patching::Policy::AfterLoadPatcher
|
48
|
+
extend Contrast::Utils::Patching::PatcherUtils
|
47
49
|
extend Contrast::Components::Logger::InstanceMethods
|
48
50
|
extend Contrast::Components::Scope::InstanceMethods
|
49
51
|
|
@@ -77,47 +79,6 @@ module Contrast
|
|
77
79
|
end
|
78
80
|
end
|
79
81
|
|
80
|
-
# This method is called by TracePointHook to instrument a specific class during a require
|
81
|
-
# or eval of dynamic class definition
|
82
|
-
def patch_specific_module mod
|
83
|
-
with_contrast_scope do
|
84
|
-
mod_name = mod.cs__name
|
85
|
-
return unless Contrast::Utils::ClassUtil.truly_defined?(mod_name)
|
86
|
-
return if ::Contrast::AGENT.skip_instrumentation?(mod_name)
|
87
|
-
|
88
|
-
load_patches_for_module(mod_name)
|
89
|
-
|
90
|
-
return if all_module_names.none?(mod_name)
|
91
|
-
|
92
|
-
module_data = Contrast::Agent::ModuleData.new(mod, mod_name)
|
93
|
-
patch_into_module(module_data)
|
94
|
-
all_module_names.delete(mod_name) if status_type.get_status(mod).patched?
|
95
|
-
rescue StandardError => e
|
96
|
-
logger.error('Unable to patch module', e, module: mod_name)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
# We did it, team. We found a patcher(s) that applies to the given
|
101
|
-
# class (or module) and the given method. Time to do some tracking.
|
102
|
-
#
|
103
|
-
# @param mod [Module] the module in which the patch should be
|
104
|
-
# placed.
|
105
|
-
# @param methods [Array(Symbol)] all the instance or singleton
|
106
|
-
# methods in this mod.
|
107
|
-
# @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
|
108
|
-
# the policy that applies to the given method_name.
|
109
|
-
# @return [Boolean] if patched, either by this invocation or a
|
110
|
-
# previous, or not
|
111
|
-
def patch_method mod, methods, method_policy
|
112
|
-
return false unless methods&.any? # don't even build the name if there are no methods
|
113
|
-
|
114
|
-
if Contrast::Utils::ClassUtil.prepended_method?(mod, method_policy)
|
115
|
-
Contrast::Agent::Patching::Policy::Patch.instrument_with_prepend(mod, method_policy)
|
116
|
-
else
|
117
|
-
Contrast::Agent::Patching::Policy::Patch.instrument_with_alias(mod, methods, method_policy)
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
82
|
private
|
122
83
|
|
123
84
|
POLICIES = [
|
@@ -246,6 +207,7 @@ module Contrast
|
|
246
207
|
def patch_into_instance_methods module_data, module_policy
|
247
208
|
mod = module_data.mod
|
248
209
|
methods = all_instance_methods(mod, true)
|
210
|
+
methods.delete(:initialize) if mod.to_s.starts_with?('RSpec') && mod.to_s.include?('Matchers')
|
249
211
|
patch_into_methods(mod, methods, module_policy, true)
|
250
212
|
end
|
251
213
|
|
@@ -309,6 +271,5 @@ require 'contrast/extension/module'
|
|
309
271
|
require 'contrast/extension/assess'
|
310
272
|
require 'contrast/extension/inventory'
|
311
273
|
require 'contrast/extension/protect'
|
312
|
-
require 'contrast/extension/protect/kernel'
|
313
274
|
|
314
275
|
require 'cs__contrast_patch/cs__contrast_patch'
|
@@ -92,10 +92,12 @@ module Contrast
|
|
92
92
|
defined?(Rack::Multipart::UploadedFile) &&
|
93
93
|
body.is_a?(Rack::Multipart::UploadedFile)
|
94
94
|
|
95
|
-
logger.trace(
|
95
|
+
logger.trace('not parsing uploaded file body',
|
96
|
+
file_name: body.original_filename,
|
97
|
+
content_type: body.content_type)
|
96
98
|
@_body = nil
|
97
99
|
else
|
98
|
-
logger.trace(
|
100
|
+
logger.trace('parsing body from request', body_type: body.cs__class.cs__name)
|
99
101
|
@_body = Contrast::Utils::StringUtils.force_utf8(read_body(body))
|
100
102
|
end
|
101
103
|
|
@@ -185,7 +187,7 @@ module Contrast
|
|
185
187
|
when Enumerable
|
186
188
|
idx = 0
|
187
189
|
res = {}
|
188
|
-
while idx < val.
|
190
|
+
while idx < val.length
|
189
191
|
res.merge! normalize_params(val[idx], prefix: "#{ prefix }[#{ idx }]")
|
190
192
|
idx += 1
|
191
193
|
end
|
@@ -60,14 +60,10 @@ module Contrast
|
|
60
60
|
# generic holder for properties that can be set throughout this request
|
61
61
|
@_properties = {}
|
62
62
|
|
63
|
-
@sample = true
|
64
|
-
|
65
63
|
if ::Contrast::ASSESS.enabled?
|
66
|
-
@
|
64
|
+
@sample_req, @sample_res = Contrast::Utils::Assess::SamplingUtil.instance.sample?(@request)
|
67
65
|
end
|
68
66
|
|
69
|
-
@sample_response &&= ::Contrast::ASSESS.scan_response?
|
70
|
-
|
71
67
|
append_route_coverage(Contrast::Agent.framework_manager.get_route_dtm(@request))
|
72
68
|
end
|
73
69
|
end
|
@@ -77,11 +73,31 @@ module Contrast
|
|
77
73
|
end
|
78
74
|
|
79
75
|
def analyze_request?
|
80
|
-
|
76
|
+
analyze_request_assess? || analyze_req_res_protect?
|
81
77
|
end
|
82
78
|
|
83
79
|
def analyze_response?
|
84
|
-
|
80
|
+
analyze_response_assess? || analyze_req_res_protect?
|
81
|
+
end
|
82
|
+
|
83
|
+
def analyze_req_res_protect?
|
84
|
+
::Contrast::PROTECT.enabled?
|
85
|
+
end
|
86
|
+
|
87
|
+
def analyze_request_assess?
|
88
|
+
return false unless analyze_req_res_assess?
|
89
|
+
|
90
|
+
@sample_req
|
91
|
+
end
|
92
|
+
|
93
|
+
def analyze_response_assess?
|
94
|
+
return false unless analyze_req_res_assess?
|
95
|
+
|
96
|
+
@sample_res &&= ::Contrast::ASSESS.scan_response?
|
97
|
+
end
|
98
|
+
|
99
|
+
def analyze_req_res_assess?
|
100
|
+
::Contrast::ASSESS.enabled?
|
85
101
|
end
|
86
102
|
|
87
103
|
# Convert the discovered route for this request to appropriate forms and disseminate it to those locations
|
@@ -131,8 +147,10 @@ module Contrast
|
|
131
147
|
handle_protect_state(service_response)
|
132
148
|
ia = service_response.input_analysis
|
133
149
|
if ia
|
134
|
-
logger.trace
|
135
|
-
|
150
|
+
if logger.trace?
|
151
|
+
logger.trace('Analysis from Contrast Service', evaluations: ia.results.length)
|
152
|
+
logger.trace('Results', input_analysis: ia.inspect)
|
153
|
+
end
|
136
154
|
@speedracer_input_analysis = ia
|
137
155
|
speedracer_input_analysis.request = request
|
138
156
|
else
|
@@ -169,7 +187,7 @@ module Contrast
|
|
169
187
|
# that has been accumulated since the last request
|
170
188
|
def extract_after rack_response
|
171
189
|
@response = Contrast::Agent::Response.new(rack_response)
|
172
|
-
activity.http_response = @response.dtm if @
|
190
|
+
activity.http_response = @response.dtm if @sample_res
|
173
191
|
rescue StandardError => e
|
174
192
|
logger.error('Unable to extract information after request', e)
|
175
193
|
end
|
@@ -202,7 +220,10 @@ module Contrast
|
|
202
220
|
rule = ::Contrast::PROTECT.rule(rule_id)
|
203
221
|
next unless rule
|
204
222
|
|
205
|
-
logger.debug
|
223
|
+
if logger.debug?
|
224
|
+
logger.debug('Building attack result from Contrast Service input analysis result',
|
225
|
+
result: ia_result.inspect)
|
226
|
+
end
|
206
227
|
|
207
228
|
attack_result = if rule.mode == :BLOCK
|
208
229
|
# special case for rules (like reflected xss) that used to have an infilter / block mode
|