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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/ext/cs__assess_module/cs__assess_module.c +48 -0
  3. data/ext/cs__assess_module/cs__assess_module.h +7 -0
  4. data/ext/cs__common/cs__common.c +24 -7
  5. data/ext/cs__common/cs__common.h +12 -2
  6. data/ext/cs__contrast_patch/cs__contrast_patch.c +48 -11
  7. data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -2
  8. data/ext/cs__os_information/cs__os_information.c +31 -0
  9. data/ext/cs__os_information/cs__os_information.h +7 -0
  10. data/ext/{cs__protect_kernel → cs__os_information}/extconf.rb +0 -0
  11. data/lib/contrast/agent/assess/contrast_event.rb +1 -1
  12. data/lib/contrast/agent/assess/contrast_object.rb +1 -4
  13. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
  14. data/lib/contrast/agent/assess/policy/preshift.rb +25 -11
  15. data/lib/contrast/agent/assess/policy/propagation_method.rb +2 -116
  16. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  17. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -0
  18. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +4 -4
  19. data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
  20. data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
  21. data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -107
  22. data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
  23. data/lib/contrast/agent/assess/property/tagged.rb +15 -132
  24. data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
  25. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +2 -1
  26. data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
  27. data/lib/contrast/agent/middleware.rb +22 -0
  28. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +0 -1
  29. data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
  30. data/lib/contrast/agent/patching/policy/patch.rb +37 -238
  31. data/lib/contrast/agent/patching/policy/patcher.rb +3 -42
  32. data/lib/contrast/agent/request.rb +5 -3
  33. data/lib/contrast/agent/request_context.rb +32 -11
  34. data/lib/contrast/agent/request_handler.rb +7 -3
  35. data/lib/contrast/agent/rule_set.rb +2 -4
  36. data/lib/contrast/agent/scope.rb +32 -20
  37. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +71 -0
  38. data/lib/contrast/agent/static_analysis.rb +4 -2
  39. data/lib/contrast/agent/telemetry.rb +129 -0
  40. data/lib/contrast/agent/telemetry_event.rb +34 -0
  41. data/lib/contrast/agent/thread_watcher.rb +43 -14
  42. data/lib/contrast/agent/tracepoint_hook.rb +11 -3
  43. data/lib/contrast/agent/version.rb +1 -1
  44. data/lib/contrast/agent.rb +6 -1
  45. data/lib/contrast/components/api.rb +34 -0
  46. data/lib/contrast/components/app_context.rb +24 -0
  47. data/lib/contrast/components/assess.rb +7 -0
  48. data/lib/contrast/components/config.rb +90 -11
  49. data/lib/contrast/components/contrast_service.rb +6 -0
  50. data/lib/contrast/config/api_configuration.rb +22 -0
  51. data/lib/contrast/config/assess_configuration.rb +1 -0
  52. data/lib/contrast/config/env_variables.rb +25 -0
  53. data/lib/contrast/config/root_configuration.rb +1 -0
  54. data/lib/contrast/config/service_configuration.rb +2 -1
  55. data/lib/contrast/config.rb +1 -0
  56. data/lib/contrast/configuration.rb +3 -0
  57. data/lib/contrast/framework/manager.rb +14 -12
  58. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
  59. data/lib/contrast/framework/rails/patch/support.rb +31 -29
  60. data/lib/contrast/logger/application.rb +4 -0
  61. data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
  62. data/lib/contrast/utils/assess/property/tagged_utils.rb +142 -0
  63. data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
  64. data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
  65. data/lib/contrast/utils/class_util.rb +58 -44
  66. data/lib/contrast/utils/exclude_key.rb +20 -0
  67. data/lib/contrast/utils/io_util.rb +42 -34
  68. data/lib/contrast/utils/lru_cache.rb +45 -0
  69. data/lib/contrast/utils/metrics_hash.rb +59 -0
  70. data/lib/contrast/utils/os.rb +23 -0
  71. data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
  72. data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
  73. data/lib/contrast/utils/requests_client.rb +150 -0
  74. data/lib/contrast/utils/ruby_ast_rewriter.rb +1 -1
  75. data/lib/contrast/utils/telemetry.rb +77 -0
  76. data/lib/contrast/utils/telemetry_identifier.rb +137 -0
  77. data/lib/contrast.rb +19 -1
  78. data/resources/assess/policy.json +12 -6
  79. data/resources/deadzone/policy.json +86 -5
  80. data/ruby-agent.gemspec +2 -1
  81. data/service_executables/VERSION +1 -1
  82. data/service_executables/linux/contrast-service +0 -0
  83. data/service_executables/mac/contrast-service +0 -0
  84. metadata +32 -14
  85. data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
  86. data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
  87. 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
- 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)
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 :prepend
374
- prepending_module = Module.new
375
- prepending_module.send(:define_method, method_name, unbound_method.bind(target_module))
376
- prepending_module.send(visibility, method_name)
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("not parsing uploaded file body :: #{ body.original_filename }::#{ body.content_type }")
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("parsing body from request :: #{ body.cs__class.cs__name }")
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.size
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
- @sample_request, @sample_response = Contrast::Utils::Assess::SamplingUtil.instance.sample?(@request)
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
- @sample_request
76
+ analyze_request_assess? || analyze_req_res_protect?
81
77
  end
82
78
 
83
79
  def analyze_response?
84
- @sample_response
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("Analysis from Contrast Service: evaluations=#{ ia.results.length }")
135
- logger.trace('Results', input_analysis: ia.inspect)
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 @sample_response
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('Building attack result from Contrast Service input analysis result', result: ia_result.inspect)
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