contrast-agent 4.1.0 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.c +22 -10
  4. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.h +4 -3
  5. data/lib/contrast/agent.rb +5 -1
  6. data/lib/contrast/agent/assess.rb +0 -9
  7. data/lib/contrast/agent/assess/contrast_event.rb +49 -132
  8. data/lib/contrast/agent/assess/contrast_object.rb +54 -0
  9. data/lib/contrast/agent/assess/events/source_event.rb +4 -9
  10. data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
  11. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
  12. data/lib/contrast/agent/assess/policy/patcher.rb +4 -3
  13. data/lib/contrast/agent/assess/policy/policy_node.rb +31 -59
  14. data/lib/contrast/agent/assess/policy/preshift.rb +3 -3
  15. data/lib/contrast/agent/assess/policy/propagation_method.rb +41 -32
  16. data/lib/contrast/agent/assess/policy/propagation_node.rb +12 -24
  17. data/lib/contrast/agent/assess/policy/propagator/append.rb +29 -15
  18. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -2
  19. data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
  20. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -18
  21. data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -2
  22. data/lib/contrast/agent/assess/policy/propagator/keep.rb +1 -2
  23. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +3 -2
  24. data/lib/contrast/agent/assess/policy/propagator/next.rb +1 -2
  25. data/lib/contrast/agent/assess/policy/propagator/prepend.rb +1 -2
  26. data/lib/contrast/agent/assess/policy/propagator/remove.rb +2 -4
  27. data/lib/contrast/agent/assess/policy/propagator/replace.rb +1 -2
  28. data/lib/contrast/agent/assess/policy/propagator/reverse.rb +1 -2
  29. data/lib/contrast/agent/assess/policy/propagator/select.rb +3 -4
  30. data/lib/contrast/agent/assess/policy/propagator/splat.rb +25 -17
  31. data/lib/contrast/agent/assess/policy/propagator/split.rb +83 -120
  32. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +41 -25
  33. data/lib/contrast/agent/assess/policy/propagator/trim.rb +3 -7
  34. data/lib/contrast/agent/assess/policy/source_method.rb +2 -14
  35. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +5 -8
  36. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
  37. data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
  38. data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
  39. data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
  40. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +2 -3
  41. data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
  42. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
  43. data/lib/contrast/agent/assess/properties.rb +0 -2
  44. data/lib/contrast/agent/assess/property/tagged.rb +56 -32
  45. data/lib/contrast/agent/assess/tracker.rb +16 -18
  46. data/lib/contrast/agent/deadzone/policy/deadzone_node.rb +7 -0
  47. data/lib/contrast/agent/middleware.rb +134 -55
  48. data/lib/contrast/agent/patching/policy/method_policy.rb +1 -1
  49. data/lib/contrast/agent/patching/policy/patch.rb +6 -0
  50. data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
  51. data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
  52. data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
  53. data/lib/contrast/agent/protect/policy/applies_deserialization_rule.rb +47 -1
  54. data/lib/contrast/agent/protect/policy/rule_applicator.rb +53 -0
  55. data/lib/contrast/agent/protect/rule/base.rb +63 -14
  56. data/lib/contrast/agent/protect/rule/cmd_injection.rb +12 -28
  57. data/lib/contrast/agent/protect/rule/default_scanner.rb +1 -4
  58. data/lib/contrast/agent/protect/rule/deserialization.rb +4 -1
  59. data/lib/contrast/agent/protect/rule/no_sqli.rb +3 -3
  60. data/lib/contrast/agent/protect/rule/sqli.rb +20 -14
  61. data/lib/contrast/agent/protect/rule/xxe.rb +32 -11
  62. data/lib/contrast/agent/protect/rule/xxe/entity_wrapper.rb +10 -6
  63. data/lib/contrast/agent/reaction_processor.rb +1 -1
  64. data/lib/contrast/agent/request_context.rb +12 -0
  65. data/lib/contrast/agent/response.rb +5 -5
  66. data/lib/contrast/agent/rewriter.rb +3 -3
  67. data/lib/contrast/agent/scope.rb +81 -55
  68. data/lib/contrast/agent/static_analysis.rb +13 -7
  69. data/lib/contrast/agent/thread.rb +1 -1
  70. data/lib/contrast/agent/thread_watcher.rb +20 -5
  71. data/lib/contrast/agent/version.rb +1 -1
  72. data/lib/contrast/api/communication/messaging_queue.rb +18 -21
  73. data/lib/contrast/api/communication/response_processor.rb +8 -1
  74. data/lib/contrast/api/communication/socket_client.rb +22 -14
  75. data/lib/contrast/api/decorators.rb +2 -0
  76. data/lib/contrast/api/decorators/agent_startup.rb +58 -0
  77. data/lib/contrast/api/decorators/application_startup.rb +51 -0
  78. data/lib/contrast/api/decorators/library.rb +1 -0
  79. data/lib/contrast/api/decorators/library_usage_update.rb +1 -0
  80. data/lib/contrast/api/decorators/route_coverage.rb +15 -5
  81. data/lib/contrast/api/decorators/trace_event.rb +58 -42
  82. data/lib/contrast/api/decorators/trace_event_object.rb +11 -3
  83. data/lib/contrast/api/decorators/trace_event_signature.rb +27 -5
  84. data/lib/contrast/api/decorators/user_input.rb +2 -1
  85. data/lib/contrast/common_agent_configuration.rb +2 -1
  86. data/lib/contrast/components/agent.rb +2 -0
  87. data/lib/contrast/components/app_context.rb +4 -22
  88. data/lib/contrast/components/assess.rb +36 -0
  89. data/lib/contrast/components/interface.rb +5 -3
  90. data/lib/contrast/components/sampling.rb +48 -6
  91. data/lib/contrast/components/scope.rb +72 -6
  92. data/lib/contrast/components/settings.rb +11 -7
  93. data/lib/contrast/config/assess_configuration.rb +2 -1
  94. data/lib/contrast/extension/assess/array.rb +2 -3
  95. data/lib/contrast/extension/assess/erb.rb +1 -3
  96. data/lib/contrast/extension/assess/exec_trigger.rb +1 -4
  97. data/lib/contrast/extension/assess/fiber.rb +2 -3
  98. data/lib/contrast/extension/assess/hash.rb +4 -2
  99. data/lib/contrast/extension/assess/kernel.rb +1 -2
  100. data/lib/contrast/extension/assess/marshal.rb +34 -26
  101. data/lib/contrast/extension/assess/regexp.rb +3 -8
  102. data/lib/contrast/extension/assess/string.rb +1 -2
  103. data/lib/contrast/framework/base_support.rb +51 -53
  104. data/lib/contrast/framework/manager.rb +16 -14
  105. data/lib/contrast/framework/rack/patch/session_cookie.rb +1 -1
  106. data/lib/contrast/framework/rack/support.rb +2 -1
  107. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +1 -1
  108. data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +1 -1
  109. data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +1 -1
  110. data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +1 -1
  111. data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +1 -1
  112. data/lib/contrast/framework/rails/support.rb +44 -44
  113. data/lib/contrast/framework/sinatra/support.rb +102 -42
  114. data/lib/contrast/logger/application.rb +0 -3
  115. data/lib/contrast/logger/log.rb +31 -15
  116. data/lib/contrast/utils/class_util.rb +3 -1
  117. data/lib/contrast/utils/duck_utils.rb +1 -1
  118. data/lib/contrast/utils/heap_dump_util.rb +103 -87
  119. data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
  120. data/lib/contrast/utils/object_share.rb +3 -3
  121. data/lib/contrast/utils/preflight_util.rb +1 -1
  122. data/lib/contrast/utils/resource_loader.rb +1 -1
  123. data/lib/contrast/utils/sha256_builder.rb +2 -2
  124. data/lib/contrast/utils/string_utils.rb +1 -1
  125. data/lib/contrast/utils/tag_util.rb +9 -13
  126. data/resources/assess/policy.json +12 -18
  127. data/resources/deadzone/policy.json +156 -0
  128. data/resources/protect/policy.json +12 -0
  129. data/ruby-agent.gemspec +61 -19
  130. data/service_executables/VERSION +1 -1
  131. data/service_executables/linux/contrast-service +0 -0
  132. data/service_executables/mac/contrast-service +0 -0
  133. metadata +126 -113
  134. data/lib/contrast/agent/assess/rule.rb +0 -18
  135. data/lib/contrast/agent/assess/rule/base.rb +0 -52
  136. data/lib/contrast/agent/assess/rule/redos.rb +0 -67
  137. data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
  138. data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
  139. data/lib/contrast/utils/prevent_serialization.rb +0 -52
@@ -89,7 +89,7 @@ module Contrast
89
89
  end
90
90
 
91
91
  def find_method_node nodes, method_name, is_instance_method
92
- return nil unless nodes
92
+ return unless nodes
93
93
 
94
94
  nodes.find do |node|
95
95
  node.instance_method? == is_instance_method && node.method_name == method_name
@@ -365,12 +365,18 @@ module Contrast
365
365
  unless target_module.instance_methods(false).include? underlying_method_name
366
366
  # alias_method may be private
367
367
  target_module.send(:alias_method, underlying_method_name, method_name)
368
+ # TODO: RUBY-1052
369
+ # rubocop:disable Performance/Kernel/DefineMethod
368
370
  target_module.send(:define_method, method_name, unbound_method.bind(target_module))
371
+ # rubocop:enable Performance/Kernel/DefineMethod
369
372
  end
370
373
  target_module.send(visibility, method_name) # e.g., module.private(:my_method)
371
374
  when :prepend
372
375
  prepending_module = Module.new
376
+ # TODO: RUBY-1052
377
+ # rubocop:disable Performance/Kernel/DefineMethod
373
378
  prepending_module.send(:define_method, method_name, unbound_method.bind(target_module))
379
+ # rubocop:enable Performance/Kernel/DefineMethod
374
380
  prepending_module.send(visibility, method_name)
375
381
  # This prepends to the singleton class (it patches a class method)
376
382
  target_module.prepend prepending_module
@@ -13,7 +13,7 @@ module Contrast
13
13
  # one does not exist.
14
14
  #
15
15
  # @param mod [Module] the Module for which the status is asked
16
- # @return [Contrast::Agent::Patching::Policy::PolicyStatus]
16
+ # @return [Contrast::Agent::Patching::Policy::PatchStatus]
17
17
  def get_status mod
18
18
  if mod.cs__const_defined?(status_key, false)
19
19
  mod.cs__const_get(status_key, false)
@@ -52,7 +52,7 @@ module Contrast
52
52
  # startup to catchup on everything we didn't see get loaded
53
53
  def patch
54
54
  catchup_after_load_patches
55
- patch_methods
55
+ catchup_loaded_methods
56
56
  Contrast::Agent::Assess::Policy::RewriterPatch.rewrite_interpolations
57
57
  end
58
58
 
@@ -62,10 +62,11 @@ module Contrast
62
62
  # where only a subset of the Assess changes are needed.
63
63
  PATCH_MONITOR = Monitor.new
64
64
 
65
- def patch_methods
65
+ # Iterate over and patch those Modules and Methods which were loaded before the Agent was enabled.
66
+ def catchup_loaded_methods
66
67
  PATCH_MONITOR.synchronize do
67
68
  t = Contrast::Agent::Thread.new do
68
- synchronized_patch_methods
69
+ synchronized_catchup_loaded_methods
69
70
  end
70
71
  # aborting on exception makes exceptions propagate.
71
72
  t.abort_on_exception = true
@@ -100,7 +101,7 @@ module Contrast
100
101
  # @param mod [Module] the module in which the patch should be
101
102
  # placed.
102
103
  # @param methods [Array(Symbol)] all the instance or singleton
103
- # methods in this clazz.
104
+ # methods in this mod.
104
105
  # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
105
106
  # the policy that applies to the given method_name.
106
107
  # @return [Boolean] if patched, either by this invocation or a
@@ -149,7 +150,7 @@ module Contrast
149
150
  # other functions, like rewriting or scanning. This method should
150
151
  # only be invoked by the patch_methods method above in order to
151
152
  # ensure it is wrapped in a synchronize call
152
- def synchronized_patch_methods
153
+ def synchronized_catchup_loaded_methods
153
154
  logger.trace_with_time('Running patching') do
154
155
  patched = []
155
156
  all_module_names.each do |patchable_name|
@@ -166,53 +167,37 @@ module Contrast
166
167
  end
167
168
  end
168
169
 
169
- # Given the patchers that apply to this class that may apply, patch
170
- # Contrast method calls into the methods for which we have rules.
170
+ # Given the patchers that apply to this class that may apply, patch Contrast method calls into the methods
171
+ # for which we have rules.
171
172
  #
172
- # @param module_data [Contrast::Agent::ModuleData] the module, and
173
- # its name, that's being patched into
174
- # @param redo_patch [Boolean] a trigger to force patching
175
- # regardless of the state of the
176
- # Contrast::Agent::Patching::Policy::PatchStatus status on the
177
- # Module
173
+ # @param module_data [Contrast::Agent::ModuleData] the module, and its name, that's being patched into
174
+ # @param redo_patch [Boolean] a trigger to force patching regardless of the state of the
175
+ # Contrast::Agent::Patching::Policy::PatchStatus status on the Module
178
176
  def patch_into_module module_data, redo_patch = false
179
177
  status = status_type.get_status(module_data.mod)
180
178
  return if (status&.patched? || status&.patching?) && !redo_patch
181
179
 
182
- # Begin patching our sources into the given clazz (or module)
183
- # Any patcher that has the name of the clazz will be evaluated for
184
- # patching.
185
- # Find all the patchers that apply to this class, sorted by type.
180
+ # Begin patching our sources into the given module. Any patcher that has the name of the module will be
181
+ # evaluated for patching. Find all the patchers that apply to this class, sorted by type.
186
182
  module_policy = Contrast::Agent::Patching::Policy::ModulePolicy.create_module_policy(module_data.name)
187
-
188
- clazz = module_data.mod
183
+ # If there's nothing to match, then set that status and exit
184
+ if module_policy.empty?
185
+ status.no_patch!
186
+ return
187
+ end
189
188
 
190
189
  status.patching!
191
- patched = false
192
-
193
- counts = 0
194
- # Monkey patch any methods in this class that have matching nodes in the policy
195
- unless module_policy.empty?
196
- instance_methods = all_instance_methods(clazz, true)
197
- singleton_methods = clazz.singleton_methods(false)
198
- counts += patch_into_methods(clazz, instance_methods, module_policy, true)
199
- counts += patch_into_methods(clazz, singleton_methods, module_policy, false)
200
- counts = module_policy.num_expected_patches if adjust_for_prepend(clazz)
201
- patched = true
202
- end
190
+ num_applied_patches = patch_into_instance_methods(module_data, module_policy)
191
+ num_applied_patches += patch_into_singleton_methods(module_data, module_policy)
192
+ if adjust_for_prepend(module_data) ||
193
+ module_policy.num_expected_patches == num_applied_patches
203
194
 
204
- if patched
205
- if module_policy.num_expected_patches == counts
206
- status.patched!
207
- else
208
- status.partial_patch!
209
- end
195
+ status.patched!
210
196
  else
211
- status.no_patch!
197
+ status.partial_patch!
212
198
  end
213
199
  rescue StandardError => e
214
- status ||= status_type.get_status(module_data.mod)
215
- status.failed_patch!
200
+ status&.failed_patch!
216
201
  logger.warn('Patching failed', e, module: module_data.name)
217
202
  ensure
218
203
  logger.trace('Patching complete',
@@ -249,6 +234,29 @@ module Contrast
249
234
  instance_methods
250
235
  end
251
236
 
237
+ # Patch into the Instance Methods, including private, of the given Module that match the ModulePolicy
238
+ # provided.
239
+ #
240
+ # @param module_data [Contrast::Agent::ModuleData] the module, and its name, that's being patched into
241
+ # @param module_policy [Contrast::Agent::Patching::Policy::ModulePolicy] All the patchers that apply to
242
+ # this module, sorted by type.
243
+ def patch_into_instance_methods module_data, module_policy
244
+ mod = module_data.mod
245
+ methods = all_instance_methods(mod, true)
246
+ patch_into_methods(mod, methods, module_policy, true)
247
+ end
248
+
249
+ # Patch into the Singleton Methods of the given Module that match the ModulePolicy provided.
250
+ #
251
+ # @param module_data [Contrast::Agent::ModuleData] the module, and its name, that's being patched into
252
+ # @param module_policy [Contrast::Agent::Patching::Policy::ModulePolicy] All the patchers that apply to
253
+ # this module, sorted by type.
254
+ def patch_into_singleton_methods module_data, module_policy
255
+ mod = module_data.mod
256
+ methods = mod.singleton_methods(false)
257
+ patch_into_methods(mod, methods, module_policy, false)
258
+ end
259
+
252
260
  # We've found the patchers that apply to this class (or module). Now we'll
253
261
  # filter on the given method.
254
262
  #
@@ -279,11 +287,10 @@ module Contrast
279
287
  # it has to be reapplied.
280
288
  # TODO: RUBY-620 should remove the need for this
281
289
  #
282
- # @param mod[Module] the Module for which a prepend action needs to
283
- # be accounted
290
+ # @param module_data [Contrast::Agent::ModuleData] the module, and its name, that's being patched into
284
291
  # @return [Boolean] if an adjustment was made or not
285
- def adjust_for_prepend mod
286
- return false unless mod.cs__name == 'CGI::Util'
292
+ def adjust_for_prepend module_data
293
+ return false unless module_data.mod.cs__name == 'CGI::Util'
287
294
 
288
295
  CGI.include(CGI::Util)
289
296
  CGI.extend(CGI::Util)
@@ -46,6 +46,11 @@ module Contrast
46
46
  raise(ArgumentError,
47
47
  "#{ id } did not have a proper applicator method: #{ applicator } does not respond to #{ applicator_method }. Unable to create.")
48
48
  end
49
+ validate_properties
50
+ validate_rule
51
+ end
52
+
53
+ def validate_properties
49
54
  if (required_properties & optional_properties).any?
50
55
  raise(ArgumentError, "#{ rule_id } had overlapping elements between required and optional properties. Unable to create.")
51
56
  end
@@ -53,8 +58,6 @@ module Contrast
53
58
  raise(ArgumentError, "#{ id } had an unexpected property. Unable to create.")
54
59
  end
55
60
  raise(ArgumentError, "#{ id } did not have a required property. Unable to create.") if (required_properties - properties.keys).any?
56
-
57
- validate_rule
58
61
  end
59
62
 
60
63
  def validate_rule
@@ -16,6 +16,23 @@ module Contrast
16
16
  extend Contrast::Agent::Protect::Policy::RuleApplicator
17
17
 
18
18
  class << self
19
+ # Calls the actual rule for this applicator, if required. Most rules
20
+ # invoke this from within their apply_rule method after doing
21
+ # whatever transformations they need to get into this common format.
22
+ #
23
+ # @param _method [Symbol] the name of the method for which this rule
24
+ # is invoked
25
+ # @param _exception [Exception] any exception raised; used for rules
26
+ # like Padding Oracle Attack (now defunct), which determine if the
27
+ # number and type of exceptions are an attack
28
+ # @param _properties [Hash] set of extra information provided by the
29
+ # applicator in an attempt to build a better story for the user
30
+ # @param _object [Object] the thing on which the triggering method
31
+ # was invoked
32
+ # @param args [Array<Object>] the arguments passed to the triggering
33
+ # method at invocation
34
+ # @raise [Contrast::SecurityException] on block, will pass the
35
+ # exception from the rule
19
36
  def invoke _method, _exception, _properties, _object, args
20
37
  return unless valid_input?(args)
21
38
  return if skip_analysis?
@@ -23,6 +40,28 @@ module Contrast
23
40
  rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, args[0])
24
41
  end
25
42
 
43
+ # Calls the actual rule for this applicator, if required, when the
44
+ # triggering method is called from Marshal.load when it has been
45
+ # prepended.
46
+ #
47
+ # @param arg [Object] the argument passed to the triggering method
48
+ # at invocation
49
+ # @raise [Contrast::SecurityException] on block, will pass the
50
+ # exception from the rule
51
+ def prepended_invoke arg
52
+ return unless arg&.cs__is_a?(String)
53
+ return if skip_analysis?
54
+
55
+ rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, arg)
56
+ end
57
+
58
+ # Allow the rule to check if the given input is an attempt to
59
+ # deserialize something in a way that will result in a command
60
+ # execution
61
+ #
62
+ # @param command [String] user input that potentially contains a
63
+ # Gadget, a Module that results in code execution on
64
+ # deserialization, and some form of command.
26
65
  def apply_deserialization_command_check command
27
66
  return unless command
28
67
  return if skip_analysis?
@@ -38,11 +77,18 @@ module Contrast
38
77
 
39
78
  private
40
79
 
80
+ # Determine if the rule would care about the arguments passed in
81
+ # this method invocation. Required b/c Ruby doesn't do type
82
+ # checking on its method signatures.
83
+ #
84
+ # @param args [Array<Object>] the arguments provided to the
85
+ # instrumented method
86
+ # @return [Boolean]
41
87
  def valid_input? args
42
88
  return false unless args&.any?
43
89
 
44
90
  input = args[0]
45
- input.is_a?(String)
91
+ input.cs__is_a?(String)
46
92
  end
47
93
  end
48
94
  end
@@ -15,6 +15,28 @@ module Contrast
15
15
 
16
16
  access_component :analysis, :logging
17
17
 
18
+ # Calls the actual invocation for this applicator, if required. Will
19
+ # attempt to transform the data as required prior to invocation and
20
+ # provides a common interface for those rules that have the same
21
+ # implementation regardless of the method patched.
22
+ #
23
+ # For those methods with different transformations depending on the
24
+ # method instrumented, variations of this method, including an
25
+ # indication of for which instrumented method they apply, will exist.
26
+ #
27
+ # @param method [Symbol] the name of the method for which this rule
28
+ # is invoked
29
+ # @param exception [Exception] any exception raised; used for rules
30
+ # like Padding Oracle Attack (now defunct), which determine if the
31
+ # number and type of exceptions are an attack
32
+ # @param properties [Hash] set of extra information provided by the
33
+ # applicator in an attempt to build a better story for the user
34
+ # @param object [Object] the thing on which the triggering method was
35
+ # invoked
36
+ # @param args [Array<Object>] the arguments passed to the triggering
37
+ # method at invocation
38
+ # @raise [Contrast::SecurityException] on block, will pass the
39
+ # exception from the rule
18
40
  def apply_rule method, exception, properties, object, args
19
41
  invoke(method, exception, properties, object, args)
20
42
  rescue Contrast::SecurityException => e
@@ -25,18 +47,49 @@ module Contrast
25
47
 
26
48
  protected
27
49
 
50
+ # Calls the actual rule for this applicator, if required. Most rules
51
+ # invoke this from within their apply_rule method after doing
52
+ # whatever transformations they need to get into this common format.
53
+ #
54
+ # @param _method [Symbol] the name of the method for which this rule
55
+ # is invoked
56
+ # @param _exception [Exception] any exception raised; used for rules
57
+ # like Padding Oracle Attack (now defunct), which determine if the
58
+ # number and type of exceptions are an attack
59
+ # @param _properties [Hash] set of extra information provided by the
60
+ # applicator in an attempt to build a better story for the user
61
+ # @param _object [Object] the thing on which the triggering method
62
+ # was invoked
63
+ # @param _args [Array<Object>] the arguments passed to the triggering
64
+ # method at invocation
65
+ # @raise [Contrast::SecurityException] on block, will pass the
66
+ # exception from the rule
28
67
  def invoke _method, _exception, _properties, _object, _args
29
68
  raise NoMethodError, 'This is abstract, override it.'
30
69
  end
31
70
 
71
+ # The name of the rule, as expected by the Contrast Service and
72
+ # Contrast UI.
73
+ #
74
+ # @return [String]
32
75
  def name
33
76
  raise NoMethodError, 'This is abstract, override it.'
34
77
  end
35
78
 
79
+ # The rule for which this applicator applies. It'll be a concrete
80
+ # sub-class of Contrast::Agent::Protect::Rule::Base, found based on
81
+ # the value of Contrast::Agent::Protect::Policy::RuleApplicator#name.
82
+ #
83
+ # @return [Contrast::Agent::Protect::Rule::Base]
36
84
  def rule
37
85
  PROTECT.rule name
38
86
  end
39
87
 
88
+ # Should we skip analysis for this rule for this method invocation?
89
+ # This allows us to short circuit in those cases for which the rule
90
+ # will not apply.
91
+ #
92
+ # @return [Boolean]
40
93
  def skip_analysis?
41
94
  context = Contrast::Agent::REQUEST_TRACKER.current
42
95
  return true unless context&.app_loaded?
@@ -10,7 +10,7 @@ module Contrast
10
10
  # This is a basic rule for Protect. It's the abstract class which all other
11
11
  # protect rules extend in order to function.
12
12
  #
13
- # @abstract Subclass and override {#prefilter}, {#infilter}, {#find_attacker}, {#postfilter} and {#build_details} to implement
13
+ # @abstract Subclass and override {#prefilter}, {#infilter}, {#find_attacker}, {#postfilter} to implement
14
14
  class Base
15
15
  include Contrast::Components::Interface
16
16
 
@@ -20,13 +20,19 @@ module Contrast
20
20
  user_input.input_type = :UNKNOWN
21
21
  end
22
22
 
23
- BLOCKING_MODES = Set.new([Contrast::Api::Settings::ProtectionRule::Mode::BLOCK,
24
- Contrast::Api::Settings::ProtectionRule::Mode::BLOCK_AT_PERIMETER]).cs__freeze
25
- POSTFILTER_MODES = Set.new([Contrast::Api::Settings::ProtectionRule::Mode::BLOCK,
26
- Contrast::Api::Settings::ProtectionRule::Mode::PERMIT,
27
- Contrast::Api::Settings::ProtectionRule::Mode::MONITOR]).cs__freeze
28
- STACK_COLLECTION_RESULTS = Set.new([Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED,
29
- Contrast::Api::Dtm::AttackResult::ResponseType::MONITORED]).cs__freeze
23
+ BLOCKING_MODES = Set.new([
24
+ Contrast::Api::Settings::ProtectionRule::Mode::BLOCK,
25
+ Contrast::Api::Settings::ProtectionRule::Mode::BLOCK_AT_PERIMETER
26
+ ]).cs__freeze
27
+ POSTFILTER_MODES = Set.new([
28
+ Contrast::Api::Settings::ProtectionRule::Mode::BLOCK,
29
+ Contrast::Api::Settings::ProtectionRule::Mode::PERMIT,
30
+ Contrast::Api::Settings::ProtectionRule::Mode::MONITOR
31
+ ]).cs__freeze
32
+ STACK_COLLECTION_RESULTS = Set.new([
33
+ Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED,
34
+ Contrast::Api::Dtm::AttackResult::ResponseType::MONITORED
35
+ ]).cs__freeze
30
36
 
31
37
  attr_reader :mode
32
38
 
@@ -116,6 +122,26 @@ module Contrast
116
122
  # the current request
117
123
  def postfilter _context; end
118
124
 
125
+ # A given input, candidate_string, was determined to violate a
126
+ # protect rule and did exploit the application, or at least made it
127
+ # to exploitable code in the case where we blocked the attack. As
128
+ # such, we need to build a result to report this violation to the
129
+ # Service.
130
+ #
131
+ # @param context [Contrast::Agent::RequestContext] the context of the
132
+ # request in which this input is evaluated.
133
+ # @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the
134
+ # analysis of the input that was determined to be an attack
135
+ # @param result [Contrast::Api::Dtm::AttackResult, nil] previous
136
+ # attack result for this rule, if one exists, in the case of
137
+ # multiple inputs being found to violate the protection criteria
138
+ # @param candidate_string [String] the value of the input which may
139
+ # be an attack
140
+ # @param kwargs [Hash] key - value pairs of context individual rules
141
+ # need to build out details to send to the Service to tell the
142
+ # story of the attack
143
+ # @return [Contrast::Api::Dtm::AttackResult] the attack result from
144
+ # this input
119
145
  def build_attack_with_match context, ia_result, result, candidate_string, **kwargs
120
146
  result ||= build_attack_result(context)
121
147
  update_successful_attack_response(context, ia_result, result, candidate_string)
@@ -124,6 +150,22 @@ module Contrast
124
150
  result
125
151
  end
126
152
 
153
+ # A given input, candidate_string, was determined to violate a
154
+ # protect rule but did not exploit the application. As such, we need
155
+ # to build a result to report this violation to the Service.
156
+ #
157
+ # @param context [Contrast::Agent::RequestContext] the context of the
158
+ # request in which this input is evaluated.
159
+ # @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the
160
+ # analysis of the input that was determined to be an attack
161
+ # @param result [Contrast::Api::Dtm::AttackResult, nil] previous
162
+ # attack result for this rule, if one exists, in the case of
163
+ # multiple inputs being found to violate the protection criteria
164
+ # @param kwargs [Hash] key - value pairs of context individual rules
165
+ # need to build out details to send to the Service to tell the
166
+ # story of the attack
167
+ # @return [Contrast::Api::Dtm::AttackResult] the attack result from
168
+ # this input
127
169
  def build_attack_without_match context, ia_result, result, **kwargs
128
170
  result ||= build_attack_result(context)
129
171
  update_perimeter_attack_response(context, ia_result, result)
@@ -132,16 +174,18 @@ module Contrast
132
174
  result
133
175
  end
134
176
 
177
+ # Attach the given result to the current request's context to report
178
+ # it to the Service
179
+ #
180
+ # @param context [Contrast::Agent::RequestContext] the context of the
181
+ # request in which this input is evaluated.
182
+ # @param result [Contrast::Api::Dtm::AttackResult]
135
183
  def append_to_activity context, result
136
184
  context.activity.results << result if result
137
185
  end
138
186
 
139
187
  protected
140
188
 
141
- def build_details _input_string, _ia_result
142
- raise NoMethodError, "Rule #{ name } did not implement build_details"
143
- end
144
-
145
189
  def mode_from_settings
146
190
  PROTECT.rule_mode(name).tap do |mode|
147
191
  logger.trace('Retrieving rule mode', rule: name, mode: mode)
@@ -209,6 +253,11 @@ module Contrast
209
253
  result
210
254
  end
211
255
 
256
+ # Set up an attack result for the current rule
257
+ #
258
+ # @param _context [Contrast::Agent::RequestContext] the context of
259
+ # the current request
260
+ # @return [Contrast::Api::Dtm::AttackResult]
212
261
  def build_attack_result _context
213
262
  result = Contrast::Api::Dtm::AttackResult.new
214
263
  result.rule_id = name
@@ -226,10 +275,10 @@ module Contrast
226
275
  end
227
276
 
228
277
  def append_sample context, ia_result, result, candidate_string, **kwargs
229
- return nil unless result
278
+ return unless result
230
279
 
231
280
  sample = build_sample(context, ia_result, candidate_string, **kwargs)
232
- return nil unless sample
281
+ return unless sample
233
282
 
234
283
  append_stack(sample, result)
235
284