contrast-agent 3.15.0 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) 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 +4 -12
  6. data/lib/contrast/agent/assess/contrast_event.rb +121 -130
  7. data/lib/contrast/agent/assess/contrast_object.rb +51 -0
  8. data/lib/contrast/agent/assess/events/source_event.rb +5 -10
  9. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +10 -3
  10. data/lib/contrast/agent/assess/policy/patcher.rb +4 -3
  11. data/lib/contrast/agent/assess/policy/policy_node.rb +46 -69
  12. data/lib/contrast/agent/assess/policy/policy_scanner.rb +19 -2
  13. data/lib/contrast/agent/assess/policy/preshift.rb +3 -3
  14. data/lib/contrast/agent/assess/policy/propagation_method.rb +13 -19
  15. data/lib/contrast/agent/assess/policy/propagation_node.rb +12 -24
  16. data/lib/contrast/agent/assess/policy/propagator/append.rb +1 -2
  17. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -2
  18. data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
  19. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +1 -3
  20. data/lib/contrast/agent/assess/policy/propagator/insert.rb +2 -3
  21. data/lib/contrast/agent/assess/policy/propagator/keep.rb +1 -2
  22. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +3 -5
  23. data/lib/contrast/agent/assess/policy/propagator/next.rb +1 -2
  24. data/lib/contrast/agent/assess/policy/propagator/prepend.rb +1 -2
  25. data/lib/contrast/agent/assess/policy/propagator/remove.rb +2 -4
  26. data/lib/contrast/agent/assess/policy/propagator/replace.rb +1 -2
  27. data/lib/contrast/agent/assess/policy/propagator/reverse.rb +1 -2
  28. data/lib/contrast/agent/assess/policy/propagator/select.rb +4 -7
  29. data/lib/contrast/agent/assess/policy/propagator/splat.rb +2 -9
  30. data/lib/contrast/agent/assess/policy/propagator/split.rb +77 -122
  31. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +32 -25
  32. data/lib/contrast/agent/assess/policy/propagator/trim.rb +3 -7
  33. data/lib/contrast/agent/assess/policy/source_method.rb +2 -14
  34. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +9 -13
  35. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
  36. data/lib/contrast/agent/assess/policy/trigger_method.rb +39 -14
  37. data/lib/contrast/agent/assess/policy/trigger_node.rb +31 -37
  38. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -1
  39. data/lib/contrast/agent/assess/property/evented.rb +5 -18
  40. data/lib/contrast/agent/assess/property/tagged.rb +28 -16
  41. data/lib/contrast/agent/assess/property/updated.rb +0 -5
  42. data/lib/contrast/agent/assess/rule/provider/hardcoded_key.rb +58 -5
  43. data/lib/contrast/agent/assess/rule/provider/hardcoded_password.rb +23 -8
  44. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +83 -14
  45. data/lib/contrast/agent/assess/rule/redos.rb +1 -1
  46. data/lib/contrast/agent/assess/tag.rb +1 -1
  47. data/lib/contrast/agent/assess/tracker.rb +16 -18
  48. data/lib/contrast/agent/at_exit_hook.rb +5 -5
  49. data/lib/contrast/agent/deadzone/policy/deadzone_node.rb +7 -0
  50. data/lib/contrast/agent/inventory.rb +15 -0
  51. data/lib/contrast/agent/inventory/dependencies.rb +50 -0
  52. data/lib/contrast/agent/inventory/dependency_analysis.rb +37 -0
  53. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +104 -0
  54. data/lib/contrast/agent/inventory/gemfile_digest_cache.rb +38 -0
  55. data/lib/contrast/agent/middleware.rb +51 -3
  56. data/lib/contrast/agent/patching/policy/after_load_patch.rb +5 -5
  57. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +20 -20
  58. data/lib/contrast/agent/patching/policy/method_policy.rb +1 -1
  59. data/lib/contrast/agent/patching/policy/module_policy.rb +10 -10
  60. data/lib/contrast/agent/patching/policy/patch.rb +6 -0
  61. data/lib/contrast/agent/patching/policy/policy.rb +16 -2
  62. data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +3 -5
  63. data/lib/contrast/agent/protect/policy/applies_deserialization_rule.rb +47 -1
  64. data/lib/contrast/agent/protect/policy/applies_path_traversal_rule.rb +4 -3
  65. data/lib/contrast/agent/protect/policy/applies_xxe_rule.rb +1 -1
  66. data/lib/contrast/agent/protect/policy/rule_applicator.rb +53 -0
  67. data/lib/contrast/agent/protect/rule/base.rb +63 -14
  68. data/lib/contrast/agent/protect/rule/cmd_injection.rb +12 -28
  69. data/lib/contrast/agent/protect/rule/default_scanner.rb +1 -4
  70. data/lib/contrast/agent/protect/rule/deserialization.rb +4 -1
  71. data/lib/contrast/agent/protect/rule/no_sqli.rb +3 -3
  72. data/lib/contrast/agent/protect/rule/no_sqli/mongo_no_sql_scanner.rb +1 -0
  73. data/lib/contrast/agent/protect/rule/sqli.rb +3 -3
  74. data/lib/contrast/agent/protect/rule/xxe.rb +32 -11
  75. data/lib/contrast/agent/protect/rule/xxe/entity_wrapper.rb +10 -6
  76. data/lib/contrast/agent/reaction_processor.rb +1 -1
  77. data/lib/contrast/agent/request.rb +34 -34
  78. data/lib/contrast/agent/request_handler.rb +1 -1
  79. data/lib/contrast/agent/response.rb +5 -5
  80. data/lib/contrast/agent/rewriter.rb +3 -3
  81. data/lib/contrast/agent/scope.rb +81 -55
  82. data/lib/contrast/agent/static_analysis.rb +15 -9
  83. data/lib/contrast/agent/tracepoint_hook.rb +1 -1
  84. data/lib/contrast/agent/version.rb +1 -1
  85. data/lib/contrast/api/communication/socket_client.rb +36 -1
  86. data/lib/contrast/api/decorators.rb +3 -0
  87. data/lib/contrast/api/decorators/address.rb +13 -14
  88. data/lib/contrast/api/decorators/application_update.rb +1 -1
  89. data/lib/contrast/api/decorators/library.rb +54 -0
  90. data/lib/contrast/api/decorators/library_usage_update.rb +31 -0
  91. data/lib/contrast/api/decorators/message.rb +1 -0
  92. data/lib/contrast/api/decorators/trace_event.rb +31 -41
  93. data/lib/contrast/api/decorators/trace_event_object.rb +11 -3
  94. data/lib/contrast/api/decorators/trace_event_signature.rb +27 -5
  95. data/lib/contrast/api/decorators/user_input.rb +2 -1
  96. data/lib/contrast/common_agent_configuration.rb +2 -1
  97. data/lib/contrast/components/agent.rb +6 -5
  98. data/lib/contrast/components/app_context.rb +39 -30
  99. data/lib/contrast/components/assess.rb +36 -0
  100. data/lib/contrast/components/config.rb +29 -37
  101. data/lib/contrast/components/contrast_service.rb +9 -9
  102. data/lib/contrast/components/interface.rb +30 -6
  103. data/lib/contrast/components/inventory.rb +6 -1
  104. data/lib/contrast/components/scope.rb +72 -6
  105. data/lib/contrast/components/settings.rb +23 -23
  106. data/lib/contrast/config/assess_configuration.rb +2 -1
  107. data/lib/contrast/config/inventory_configuration.rb +2 -2
  108. data/lib/contrast/config/service_configuration.rb +4 -2
  109. data/lib/contrast/configuration.rb +1 -1
  110. data/lib/contrast/extension/assess/array.rb +9 -6
  111. data/lib/contrast/extension/assess/erb.rb +6 -3
  112. data/lib/contrast/extension/assess/eval_trigger.rb +6 -6
  113. data/lib/contrast/extension/assess/exec_trigger.rb +0 -3
  114. data/lib/contrast/extension/assess/fiber.rb +5 -6
  115. data/lib/contrast/extension/assess/hash.rb +7 -5
  116. data/lib/contrast/extension/assess/kernel.rb +19 -22
  117. data/lib/contrast/extension/assess/marshal.rb +40 -28
  118. data/lib/contrast/extension/assess/regexp.rb +6 -11
  119. data/lib/contrast/extension/assess/string.rb +14 -13
  120. data/lib/contrast/extension/protect/kernel.rb +3 -3
  121. data/lib/contrast/framework/base_support.rb +51 -53
  122. data/lib/contrast/framework/manager.rb +6 -5
  123. data/lib/contrast/framework/rack/patch/session_cookie.rb +10 -10
  124. data/lib/contrast/framework/rack/support.rb +2 -1
  125. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +14 -14
  126. data/lib/contrast/framework/rails/patch/assess_configuration.rb +1 -1
  127. data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +11 -11
  128. data/lib/contrast/framework/rails/patch/support.rb +1 -1
  129. data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +12 -12
  130. data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +13 -13
  131. data/lib/contrast/framework/rails/rewrite/active_record_named.rb +3 -3
  132. data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +13 -13
  133. data/lib/contrast/framework/rails/support.rb +5 -1
  134. data/lib/contrast/framework/sinatra/patch/base.rb +11 -11
  135. data/lib/contrast/framework/sinatra/support.rb +7 -6
  136. data/lib/contrast/logger/application.rb +1 -4
  137. data/lib/contrast/logger/log.rb +7 -2
  138. data/lib/contrast/utils/duck_utils.rb +1 -1
  139. data/lib/contrast/utils/heap_dump_util.rb +1 -1
  140. data/lib/contrast/utils/invalid_configuration_util.rb +2 -5
  141. data/lib/contrast/utils/inventory_util.rb +0 -7
  142. data/lib/contrast/utils/object_share.rb +3 -3
  143. data/lib/contrast/utils/preflight_util.rb +1 -1
  144. data/lib/contrast/utils/prevent_serialization.rb +1 -1
  145. data/lib/contrast/utils/resource_loader.rb +1 -1
  146. data/lib/contrast/utils/sha256_builder.rb +2 -14
  147. data/lib/contrast/utils/string_utils.rb +1 -1
  148. data/lib/contrast/utils/tag_util.rb +9 -13
  149. data/resources/assess/policy.json +31 -12
  150. data/resources/deadzone/policy.json +156 -0
  151. data/resources/protect/policy.json +12 -0
  152. data/ruby-agent.gemspec +11 -6
  153. data/service_executables/VERSION +1 -1
  154. data/service_executables/linux/contrast-service +0 -0
  155. data/service_executables/mac/contrast-service +0 -0
  156. metadata +91 -28
  157. data/lib/contrast/utils/boolean_util.rb +0 -30
  158. data/lib/contrast/utils/gemfile_reader.rb +0 -193
@@ -61,16 +61,16 @@ module Contrast
61
61
  # @return [Integer] count of methods to be patched
62
62
  def num_expected_patches
63
63
  @_num_expected_patches ||= begin
64
- instance_methods = Set.new
65
- singleton_methods = Set.new
66
- sort_method_names(source_nodes, instance_methods, singleton_methods)
67
- sort_method_names(propagator_nodes, instance_methods, singleton_methods)
68
- sort_method_names(trigger_nodes, instance_methods, singleton_methods)
69
- sort_method_names(inventory_nodes, instance_methods, singleton_methods)
70
- sort_method_names(protect_nodes, instance_methods, singleton_methods)
71
- sort_method_names(deadzone_nodes, instance_methods, singleton_methods)
72
- instance_methods.length + singleton_methods.length
73
- end
64
+ instance_methods = Set.new
65
+ singleton_methods = Set.new
66
+ sort_method_names(source_nodes, instance_methods, singleton_methods)
67
+ sort_method_names(propagator_nodes, instance_methods, singleton_methods)
68
+ sort_method_names(trigger_nodes, instance_methods, singleton_methods)
69
+ sort_method_names(inventory_nodes, instance_methods, singleton_methods)
70
+ sort_method_names(protect_nodes, instance_methods, singleton_methods)
71
+ sort_method_names(deadzone_nodes, instance_methods, singleton_methods)
72
+ instance_methods.length + singleton_methods.length
73
+ end
74
74
  end
75
75
 
76
76
  private
@@ -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
@@ -124,12 +124,26 @@ module Contrast
124
124
  end
125
125
 
126
126
  def find_source_node class_name, method_name, instance_method
127
- sources.find { |source| source.class_name == class_name && source.method_name == method_name && source.instance_method == instance_method }
127
+ sources.find do |source|
128
+ source.class_name == class_name &&
129
+ source.method_name == method_name &&
130
+ source.instance_method == instance_method
131
+ end
132
+ end
133
+
134
+ def find_propagator_node class_name, method_name, instance_method
135
+ propagators.find do |propagator|
136
+ propagator.class_name == class_name &&
137
+ propagator.method_name == method_name &&
138
+ propagator.instance_method == instance_method
139
+ end
128
140
  end
129
141
 
130
142
  def find_node rule_id, class_name, method_name, instance_method
131
143
  find_triggers_by_rule(rule_id).find do |node|
132
- node.class_name == class_name && node.method_name == method_name && node.instance_method == instance_method
144
+ node.class_name == class_name &&
145
+ node.method_name == method_name &&
146
+ node.instance_method == instance_method
133
147
  end
134
148
  end
135
149
  end
@@ -30,11 +30,9 @@ module Contrast
30
30
  Contrast::Agent::Protect::Policy::AppliesDeserializationRule.apply_deserialization_command_check(command)
31
31
  return if skip_analysis?
32
32
 
33
- begin
34
- clazz = object.is_a?(Module) ? object : object.cs__class
35
- class_name = clazz.cs__name
36
- rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, class_name, method, command)
37
- end
33
+ clazz = object.is_a?(Module) ? object : object.cs__class
34
+ class_name = clazz.cs__name
35
+ rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, class_name, method, command)
38
36
  end
39
37
 
40
38
  protected
@@ -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
@@ -26,7 +26,7 @@ module Contrast
26
26
 
27
27
  action = properties['action']
28
28
  write_marker = write?(action, *args)
29
- possible_write = write_marker && possible_write(write_marker)
29
+ possible_write = write_marker && possible_write?(write_marker)
30
30
  path_traversal_rule(path, possible_write, object, method)
31
31
 
32
32
  # If the action was copy, we need to handle the write half of it.
@@ -48,7 +48,7 @@ module Contrast
48
48
 
49
49
  private
50
50
 
51
- def possible_write input
51
+ def possible_write? input
52
52
  input.cs__respond_to?(:to_s) &&
53
53
  input.to_s.include?(Contrast::Utils::ObjectShare::WRITE_FLAG)
54
54
  end
@@ -62,7 +62,7 @@ module Contrast
62
62
  return true if action == WRITE
63
63
 
64
64
  write_marker = args.length > 1 ? args[1] : nil
65
- write_marker && possible_write(write_marker)
65
+ write_marker && possible_write?(write_marker)
66
66
  end
67
67
 
68
68
  def path_traversal_rule path, possible_write, object, method
@@ -83,6 +83,7 @@ module Contrast
83
83
  tmp = CS__SAFER_REL_PATHS.map { |r| "#{ pwd }/#{ r }" }
84
84
  gems = ENV['GEM_PATH']
85
85
  tmp += gems.split(Contrast::Utils::ObjectShare::COLON) if gems
86
+ tmp.map!(&:downcase)
86
87
  tmp
87
88
  else
88
89
  []
@@ -27,7 +27,7 @@ module Contrast
27
27
  def apply_rule__io method, _exception, _properties, object, args
28
28
  need_rewind = false
29
29
  potential_xml = args[0]
30
- return unless potential_xml&.respond_to?(:rewind)
30
+ return unless potential_xml.cs__respond_to?(:rewind)
31
31
 
32
32
  xml = potential_xml.read
33
33
  need_rewind = true
@@ -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
 
@@ -1,7 +1,6 @@
1
1
  # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'contrast/agent/at_exit_hook'
5
4
  require 'contrast/agent/protect/rule/base_service'
6
5
  require 'contrast/utils/stack_trace_utils'
7
6
  require 'contrast/utils/object_share'
@@ -24,10 +23,10 @@ module Contrast
24
23
  end
25
24
 
26
25
  def infilter context, classname, method, command
27
- return nil unless infilter?(context)
26
+ return unless infilter?(context)
28
27
 
29
28
  ia_results = gather_ia_results(context)
30
- return nil if ia_results.empty?
29
+ return if ia_results.empty?
31
30
 
32
31
  if APP_CONTEXT.in_new_process?
33
32
  logger.trace('Running cmd-injection infilter within new process - creating new context')
@@ -37,7 +36,7 @@ module Contrast
37
36
 
38
37
  result = find_attacker_with_results(context, command, ia_results, **{ classname: classname, method: method })
39
38
  result ||= report_command_execution(context, command, **{ classname: classname, method: method })
40
- return nil unless result
39
+ return unless result
41
40
 
42
41
  append_to_activity(context, result)
43
42
  return unless blocked?
@@ -45,11 +44,6 @@ module Contrast
45
44
  raise Contrast::SecurityException.new(
46
45
  self,
47
46
  "Command Injection rule triggered. Call to #{ classname }.#{ method } blocked.")
48
- ensure
49
- # Kernel#exec replaces the current process and does not go through
50
- # at_exit hooks. Kernel#` runs as a subshell - messages appended
51
- # here do not seem to be present in the original process.
52
- Contrast::Agent::AtExitHook.on_exit if %i[exec `].include?(method.to_sym)
53
47
  end
54
48
 
55
49
  def build_attack_with_match context, input_analysis_result, result, candidate_string, **kwargs
@@ -104,37 +98,27 @@ module Contrast
104
98
 
105
99
  def report_command_execution context, command, **kwargs
106
100
  return unless report_any_command_execution?
107
- return nil if protect_excluded_by_code?
101
+ return if protect_excluded_by_code?
108
102
 
109
103
  build_attack_with_match(context, nil, nil, command, **kwargs)
110
104
  end
111
105
 
112
106
  def find_probable_attacker context, potential_attack_string, ia_results, **kwargs
113
- result = nil
114
- if chained_command?(potential_attack_string) # this is probably an attack
115
- most_likely = nil
116
- ia_results.each do |input_analysis_result|
117
- next unless chained_command?(input_analysis_result.value)
118
-
119
- most_likely = input_analysis_result
120
- break
121
- end
122
- end
123
- return result unless most_likely
107
+ return unless chained_command?(potential_attack_string)
108
+
109
+ likely_attacker = ia_results.find { |input_analysis_result| chained_command?(input_analysis_result.value) }
110
+ return unless likely_attacker
124
111
 
125
- result ||= build_attack_with_match(
112
+ build_attack_with_match(
126
113
  context,
127
- most_likely,
128
- result,
114
+ likely_attacker,
115
+ nil,
129
116
  potential_attack_string,
130
117
  **kwargs)
131
- result
132
118
  end
133
119
 
134
120
  def chained_command? command
135
- return true if CHAINED_COMMAND_CHARS.match(command)
136
-
137
- false
121
+ CHAINED_COMMAND_CHARS.match(command)
138
122
  end
139
123
 
140
124
  # Part of the Hardening for Command Injection detection is the