contrast-agent 4.9.1 → 4.10.0

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.rspec_parallel +6 -0
  4. data/ext/cs__contrast_patch/cs__contrast_patch.c +0 -1
  5. data/ext/cs__contrast_patch/cs__contrast_patch.h +0 -2
  6. data/lib/contrast/agent/assess/contrast_event.rb +0 -1
  7. data/lib/contrast/agent/assess/finalizers/hash.rb +0 -1
  8. data/lib/contrast/agent/assess/policy/patcher.rb +0 -1
  9. data/lib/contrast/agent/assess/policy/policy_scanner.rb +0 -2
  10. data/lib/contrast/agent/assess/policy/preshift.rb +8 -5
  11. data/lib/contrast/agent/assess/policy/propagation_method.rb +100 -57
  12. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +0 -2
  13. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +31 -11
  14. data/lib/contrast/agent/assess/policy/propagator/split.rb +3 -2
  15. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +1 -0
  16. data/lib/contrast/agent/assess/policy/rewriter_patch.rb +0 -1
  17. data/lib/contrast/agent/assess/policy/source_method.rb +13 -17
  18. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +0 -1
  19. data/lib/contrast/agent/assess/policy/trigger_method.rb +59 -83
  20. data/lib/contrast/agent/assess/property/evented.rb +2 -1
  21. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +0 -1
  22. data/lib/contrast/agent/disable_reaction.rb +1 -1
  23. data/lib/contrast/agent/exclusion_matcher.rb +0 -4
  24. data/lib/contrast/agent/inventory/database_config.rb +117 -0
  25. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +5 -4
  26. data/lib/contrast/agent/inventory/policy/datastores.rb +2 -2
  27. data/lib/contrast/agent/middleware.rb +1 -0
  28. data/lib/contrast/agent/patching/policy/after_load_patch.rb +3 -0
  29. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +18 -12
  30. data/lib/contrast/agent/patching/policy/module_policy.rb +2 -4
  31. data/lib/contrast/agent/patching/policy/patch.rb +5 -0
  32. data/lib/contrast/agent/patching/policy/patch_status.rb +3 -7
  33. data/lib/contrast/agent/patching/policy/patcher.rb +8 -8
  34. data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +1 -1
  35. data/lib/contrast/agent/protect/rule/no_sqli.rb +7 -53
  36. data/lib/contrast/agent/protect/rule/sql_sample_builder.rb +137 -0
  37. data/lib/contrast/agent/protect/rule/sqli.rb +7 -70
  38. data/lib/contrast/agent/reaction_processor.rb +1 -1
  39. data/lib/contrast/agent/request.rb +5 -2
  40. data/lib/contrast/agent/request_context.rb +19 -22
  41. data/lib/contrast/agent/static_analysis.rb +1 -1
  42. data/lib/contrast/agent/tracepoint_hook.rb +6 -1
  43. data/lib/contrast/agent/version.rb +1 -1
  44. data/lib/contrast/api/communication/messaging_queue.rb +12 -6
  45. data/lib/contrast/api/communication/service_lifecycle.rb +4 -1
  46. data/lib/contrast/api/communication/socket_client.rb +4 -4
  47. data/lib/contrast/api/decorators/agent_startup.rb +4 -4
  48. data/lib/contrast/api/decorators/application_startup.rb +6 -5
  49. data/lib/contrast/api/decorators/route_coverage.rb +24 -1
  50. data/lib/contrast/components/agent.rb +5 -2
  51. data/lib/contrast/components/assess.rb +6 -3
  52. data/lib/contrast/components/base.rb +2 -2
  53. data/lib/contrast/components/config.rb +1 -0
  54. data/lib/contrast/components/contrast_service.rb +4 -2
  55. data/lib/contrast/components/logger.rb +13 -8
  56. data/lib/contrast/components/scope.rb +9 -28
  57. data/lib/contrast/config/base_configuration.rb +14 -6
  58. data/lib/contrast/configuration.rb +19 -15
  59. data/lib/contrast/extension/assess/array.rb +1 -11
  60. data/lib/contrast/extension/assess/eval_trigger.rb +0 -20
  61. data/lib/contrast/extension/assess/fiber.rb +0 -11
  62. data/lib/contrast/extension/assess/hash.rb +0 -10
  63. data/lib/contrast/extension/assess/kernel.rb +1 -10
  64. data/lib/contrast/extension/assess/marshal.rb +3 -11
  65. data/lib/contrast/extension/assess/regexp.rb +0 -11
  66. data/lib/contrast/extension/assess/string.rb +1 -26
  67. data/lib/contrast/extension/extension.rb +61 -0
  68. data/lib/contrast/extension/protect/kernel.rb +0 -10
  69. data/lib/contrast/framework/grape/support.rb +174 -0
  70. data/lib/contrast/framework/manager.rb +42 -6
  71. data/lib/contrast/framework/rack/support.rb +1 -1
  72. data/lib/contrast/framework/rails/patch/assess_configuration.rb +0 -1
  73. data/lib/contrast/framework/rails/patch/support.rb +6 -3
  74. data/lib/contrast/framework/rails/railtie.rb +1 -1
  75. data/lib/contrast/framework/rails/rewrite/active_record_named.rb +1 -0
  76. data/lib/contrast/framework/rails/support.rb +60 -13
  77. data/lib/contrast/framework/sinatra/support.rb +1 -1
  78. data/lib/contrast/logger/log.rb +89 -15
  79. data/lib/contrast/utils/io_util.rb +1 -1
  80. data/lib/contrast/utils/ruby_ast_rewriter.rb +16 -13
  81. data/lib/contrast/utils/tag_util.rb +2 -1
  82. data/resources/assess/policy.json +197 -2
  83. data/resources/deadzone/policy.json +10 -0
  84. data/ruby-agent.gemspec +10 -1
  85. metadata +78 -12
  86. data/lib/contrast/utils/inventory_util.rb +0 -113
@@ -7,12 +7,9 @@ require 'contrast/agent/scope'
7
7
 
8
8
  # This is the Scope component.
9
9
  #
10
- # It tracks /Contrast/ scope. That is, "are we currently doing assess
11
- # or protect stuff within a patched method?" -- this is how we avoid doing
12
- # Contrast stuff on Contrast code.
13
- #
14
- # Separately from this component, there is also require scope, which is an
15
- # optimization on how we implement patching to `require`.
10
+ # It tracks /Contrast/ scope. That is, "are we currently doing assess or protect stuff within a patched method?" --
11
+ # this is how we avoid doing Contrast stuff on Contrast code or creating infinite loops -- or "are we in some other
12
+ # execution context for which we need to special case?".
16
13
  module Contrast
17
14
  module Components
18
15
  module Scope # :nodoc:
@@ -25,10 +22,8 @@ module Contrast
25
22
  EXECUTION_CONTEXT[Fiber.current] = Contrast::Agent::Scope.new
26
23
  end
27
24
 
28
- # This returns the scope governing the current execution context.
29
- # Use this sparingly, preferring the instance & class methods to
30
- # access and query scope, rather than interacting with the scope
31
- # object directly.
25
+ # This returns the scope governing the current execution context. Use this sparingly, preferring the instance
26
+ # & class methods to access and query scope, rather than interacting with the scope object directly.
32
27
  def scope_for_current_ec
33
28
  MONITOR.synchronize do
34
29
  return EXECUTION_CONTEXT[Fiber.current] ||= Contrast::Agent::Scope.new
@@ -37,9 +32,7 @@ module Contrast
37
32
  end
38
33
 
39
34
  module InstanceMethods # :nodoc:
40
- # For each instance method on a scope, define a forwarder
41
- # to the scope on the current execution context's scope.
42
-
35
+ # For each instance method on a scope, define a forwarder to the scope on the current execution context's scope.
43
36
  def scope_for_current_ec
44
37
  MONITOR.synchronize do
45
38
  return EXECUTION_CONTEXT[Fiber.current] ||= Contrast::Agent::Scope.new
@@ -118,24 +111,12 @@ module Contrast
118
111
  ensure
119
112
  scope_for_current_ec.exit_split_scope!
120
113
  end
121
-
122
- # TODO: RUBY-572
123
- #
124
- # Current behavior is to no-op if we're not "in a request context".
125
- # Our C functions were previously checking to see if we had a scope, because
126
- # scope was tacked on to a request context -- so "we have a scope, therefore,
127
- # we have a request context." We've decoupled scopes from request contexts,
128
- # so now it checks "do we have a request context."
129
- # RUBY-290 should remove all of that, including this method.
130
- def in_request_context?
131
- !!Contrast::Agent::REQUEST_TRACKER.current
132
- end
133
114
  end
134
115
 
135
116
  def self.sweep_dead_ecs
136
- # TODO: RUBY-571, #sweep_dead_ecs compensates for a lack of weak tables
137
- # 'ec' for execution context. in this case, it's a Fiber.
138
- # Threads rely on Fibers, so two birds, one stone.
117
+ # TODO: RUBY-534, #sweep_dead_ecs compensates for a lack of weak tables. when we can use WeakRef, we should
118
+ # investigate removing this call and instead use the WeakRef for the Execution Context's Keys or using our
119
+ # Finalizers Hash for Fibers
139
120
  MONITOR.synchronize do
140
121
  EXECUTION_CONTEXT.delete_if do |ec, _scope|
141
122
  !ec.alive?
@@ -12,7 +12,7 @@ module Contrast
12
12
  class BaseConfiguration
13
13
  extend Forwardable
14
14
 
15
- BOOLEANS = [true, false].cs__freeze
15
+ STRING_BOOLEANS = %w[false true].cs__freeze
16
16
 
17
17
  attr_reader :map
18
18
 
@@ -73,8 +73,18 @@ module Contrast
73
73
  spec_value.new(user_provided_value)
74
74
  elsif spec_value.is_a?(Contrast::Config::DefaultValue) && user_provided_value == EMPTY_VALUE
75
75
  spec_value.value
76
- elsif BOOLEANS.include?(user_provided_value)
77
- user_provided_value.to_s
76
+ elsif user_provided_value.cs__is_a?(String)
77
+ value = user_provided_value.downcase
78
+ # converts string values to 'true' => true or 'false' => false
79
+ case value
80
+ when STRING_BOOLEANS[1]
81
+ true
82
+ when STRING_BOOLEANS[0]
83
+ false
84
+ else
85
+ # returns non boolean string values
86
+ user_provided_value
87
+ end
78
88
  else
79
89
  user_provided_value
80
90
  end
@@ -95,9 +105,7 @@ module Contrast
95
105
 
96
106
  def define_setter str_key
97
107
  define_singleton_method "#{ str_key }=".to_sym do |new_value|
98
- boolean_value = new_value == true
99
- boolean_value ||= new_value == false
100
- @map[str_key] = boolean_value ? new_value.to_s : new_value
108
+ @map[str_key] = new_value
101
109
  end
102
110
  end
103
111
  end
@@ -48,9 +48,7 @@ module Contrast
48
48
  # in an infinite loop on the to_sym method used later.
49
49
  def method_missing symbol, *args
50
50
  with_contrast_scope do
51
- root.public_send(symbol, *args)
52
- rescue NoMethodError => _e
53
- super
51
+ root.public_send(symbol, *args) if root.cs__respond_to?(symbol)
54
52
  end
55
53
  end
56
54
 
@@ -101,8 +99,7 @@ module Contrast
101
99
  {}
102
100
  end
103
101
 
104
- # We're updating properties loaded from the configuration
105
- # files to match the new agreed upon standard configuration
102
+ # We're updating properties loaded from the configuration files to match the new agreed upon standard configuration
106
103
  # names, so that one file works for all agents
107
104
  def update_prop_keys config
108
105
  CONVERSION.each_pair do |old_method, new_method|
@@ -120,16 +117,7 @@ module Contrast
120
117
  # We changed the seconds values into ms values. Multiply them accordingly
121
118
  old_value = old_value.to_i * 1000 if new_method.end_with?(MILLISECOND_MARKER)
122
119
  new_value = config
123
- end_idx = new_keys.length - 1
124
- new_keys.each_with_index do |new_key, index|
125
- if index == end_idx
126
- new_value[new_key] = old_value if new_value[new_key].nil?
127
- else
128
- new_value = {} if new_value.nil?
129
- new_value[new_key] = {} if new_value[new_key].nil?
130
- new_value = new_value[new_key]
131
- end
132
- end
120
+ replace_props(new_keys, new_value, old_value)
133
121
  end
134
122
 
135
123
  config
@@ -237,5 +225,21 @@ module Contrast
237
225
  convert
238
226
  end
239
227
  end
228
+
229
+ def replace_props new_keys, new_value, old_value
230
+ idx = 0
231
+ end_idx = new_keys.length - 1
232
+ while idx < new_keys.length
233
+ new_key = new_keys[idx]
234
+ if idx == end_idx
235
+ new_value[new_key] = old_value if new_value[new_key].nil?
236
+ else
237
+ new_value = {} if new_value.nil?
238
+ new_value[new_key] = {} if new_value[new_key].nil?
239
+ new_value = new_value[new_key]
240
+ end
241
+ idx += 1
242
+ end
243
+ end
240
244
  end
241
245
  end
@@ -11,7 +11,7 @@ module Contrast
11
11
  # This is our patch of the Array class required to handle propagation
12
12
  # Disclaimer: there may be a better way, but we're in a 'get it work' state.
13
13
  # Hopefully, we'll be in a 'get it right' state soon.
14
- class ArrayPropagator
14
+ class ArrayPropagator # rubocop:disable Style/StaticClass
15
15
  extend Contrast::Components::Scope::InstanceMethods
16
16
 
17
17
  ARRAY_JOIN_HASH = {
@@ -59,16 +59,6 @@ module Contrast
59
59
  ret
60
60
  end
61
61
  end
62
-
63
- def instrument_array_track
64
- @_instrument_array_track ||= begin
65
- require 'cs__assess_array/cs__assess_array'
66
- true
67
- end
68
- rescue StandardError, LoadError => e
69
- logger.error('Error loading assess track patch', e)
70
- false
71
- end
72
62
  end
73
63
  end
74
64
  end
@@ -34,26 +34,6 @@ module Contrast
34
34
  ret, source)
35
35
  end
36
36
 
37
- def instrument_basic_object_track
38
- @_instrument_basic_object_track ||= begin
39
- require 'cs__assess_basic_object/cs__assess_basic_object'
40
- true
41
- end
42
- rescue StandardError, LoadError => e
43
- logger.error('Error loading basic object track patch', e)
44
- false
45
- end
46
-
47
- def instrument_module_track
48
- @_instrument_module_track ||= begin
49
- require 'cs__assess_module/cs__assess_module'
50
- true
51
- end
52
- rescue StandardError, LoadError => e
53
- logger.error('Error loading module track patch', e)
54
- false
55
- end
56
-
57
37
  private
58
38
 
59
39
  def trigger_node clazz, method
@@ -20,7 +20,6 @@ module Contrast
20
20
  extend Contrast::Components::Logger::InstanceMethods
21
21
  extend Contrast::Components::Scope::InstanceMethods
22
22
 
23
-
24
23
  # we use funchook to patch rb_fiber_new the initialize method is not exposed by Ruby core
25
24
  FIBER_NEW_NODE_HASH = {
26
25
  'class_name' => 'Fiber',
@@ -86,16 +85,6 @@ module Contrast
86
85
  rescue Exception => e # rubocop:disable Lint/RescueException
87
86
  logger.error('Unable to propagate during Fiber.new', e)
88
87
  end
89
-
90
- def instrument_fiber_track
91
- @_instrument_fiber_variables ||= begin
92
- require 'cs__assess_fiber_track/cs__assess_fiber_track' if Funchook.available?
93
- true
94
- end
95
- rescue StandardError, LoadError => e
96
- logger.error('Error loading fiber track patch', e)
97
- false
98
- end
99
88
  end
100
89
  end
101
90
  end
@@ -25,16 +25,6 @@ module Contrast
25
25
  # result in a seg fault
26
26
  object
27
27
  end
28
-
29
- def instrument_hash_track
30
- @_instrument_hash_track ||= begin
31
- require 'cs__assess_hash/cs__assess_hash'
32
- true
33
- end
34
- rescue StandardError, LoadError => e
35
- logger.error('Error loading hash track patch', e)
36
- false
37
- end
38
28
  end
39
29
  end
40
30
  end
@@ -14,6 +14,7 @@ module Contrast
14
14
  module KernelPropagator
15
15
  class << self
16
16
  extend Contrast::Components::Logger::InstanceMethods
17
+ include Contrast::Components::Logger::InstanceMethods
17
18
  include Contrast::Extension::Assess::ExecTrigger
18
19
 
19
20
  # We're 'tracking' sprintf now, meaning if anything is tracked on the way
@@ -65,16 +66,6 @@ module Contrast
65
66
  logger.error('Unable to track dataflow through sprintf', e)
66
67
  end
67
68
 
68
- def instrument_kernel_track
69
- @_instrument_fiber_variables ||= begin
70
- require 'cs__assess_kernel/cs__assess_kernel'
71
- true
72
- end
73
- rescue StandardError, LoadError => e
74
- logger.error('Error loading kernel track patch', e)
75
- false
76
- end
77
-
78
69
  private
79
70
 
80
71
  def handle_sprintf_value value, result, parent_events
@@ -12,10 +12,12 @@ module Contrast
12
12
  # Hopefully, we'll be in a 'get it right' state soon.
13
13
  # This module is used for our Marshal.load patches
14
14
  class MarshalPropagator
15
- extend Contrast::Components::Logger::InstanceMethods
16
15
  extend Contrast::Components::Scope::InstanceMethods
17
16
 
18
17
  class << self
18
+ extend Contrast::Components::Logger::InstanceMethods
19
+ include Contrast::Components::Logger::InstanceMethods
20
+
19
21
  def cs__load_protect arg
20
22
  return if in_contrast_scope?
21
23
 
@@ -44,16 +46,6 @@ module Contrast
44
46
  end
45
47
  end
46
48
 
47
- def instrument_marshal_load
48
- @_instrument_marshal_load ||= begin
49
- require 'cs__assess_marshal_module/cs__assess_marshal_module'
50
- true
51
- end
52
- rescue StandardError, LoadError => e
53
- logger.error('Error loading marshal load patch', e)
54
- false
55
- end
56
-
57
49
  def trigger_node clazz, method
58
50
  triggers = Contrast::Agent::Assess::Policy::Policy.instance.triggers
59
51
  return unless triggers
@@ -16,7 +16,6 @@ module Contrast
16
16
  extend Contrast::Components::Logger::InstanceMethods
17
17
  extend Contrast::Components::Scope::InstanceMethods
18
18
 
19
-
20
19
  REGEXP_EQUAL_SQUIGGLE_HASH = {
21
20
  'id' => 'regexp_100',
22
21
  'class_name' => 'Regexp',
@@ -59,16 +58,6 @@ module Contrast
59
58
  rescue Exception => e # rubocop:disable Lint/RescueException
60
59
  logger.error('Unable to propagate during Regexp#=~', e)
61
60
  end
62
-
63
- def instrument_regexp_track
64
- @_instrument_regexp_track ||= begin
65
- require 'cs__assess_regexp/cs__assess_regexp'
66
- true
67
- end
68
- rescue StandardError, LoadError => e
69
- logger.error('Error loading regexp track patch', e)
70
- false
71
- end
72
61
  end
73
62
  end
74
63
  end
@@ -12,7 +12,7 @@ module Contrast
12
12
  # methods which are too complex to fit into one of the standard
13
13
  # Contrast::Agent::Assess::Policy::Propagator molds without cluttering up the
14
14
  # String Class or exposing our methods there.
15
- class StringPropagator
15
+ class StringPropagator # rubocop:disable Style/StaticClass
16
16
  extend Contrast::Components::Logger::InstanceMethods
17
17
  extend Contrast::Components::Scope::InstanceMethods
18
18
 
@@ -52,31 +52,6 @@ module Contrast
52
52
  rescue StandardError => e
53
53
  logger.error('Unable to track interpolation', e)
54
54
  end
55
-
56
- def instrument_string
57
- @_instrument_string ||= begin
58
- require 'cs__assess_string/cs__assess_string'
59
- true
60
- end
61
- rescue StandardError, LoadError => e
62
- logger.error('Error loading hash track patch', e)
63
- false
64
- end
65
-
66
- def instrument_string_interpolation
67
- if @_instrument_string_interpolation.nil?
68
- @_instrument_string_interpolation = begin
69
- if ::Contrast::AGENT.patch_interpolation? && Funchook.available?
70
- require 'cs__assess_string_interpolation26/cs__assess_string_interpolation26'
71
- end
72
- true
73
- rescue StandardError, LoadError => e
74
- logger.error('Error loading interpolation patch', e)
75
- false
76
- end
77
- end
78
- @_instrument_string_interpolation
79
- end
80
55
  end
81
56
  end
82
57
  end
@@ -0,0 +1,61 @@
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/components/logger'
5
+
6
+ module Contrast
7
+ module Extension
8
+ # Our top level Assess namespace in the Core Extension section of our
9
+ # code. These patches are those that are invoked directly from a patched
10
+ # Class.
11
+ #
12
+ module Assess
13
+ # This is the main instrument helper giving the method of requiring C patches
14
+ #
15
+ module InstrumentHelper
16
+ class << self
17
+ include Contrast::Components::Logger::InstanceMethods
18
+
19
+ # Unites the different require methods into one, using only
20
+ # the provided path for the C patches
21
+ # parameters
22
+ # @param path[String] Path to the required patch
23
+ #
24
+ def instrument path
25
+ var_name, extracted_name = gen_name(path)
26
+ return if instance_variable_get(var_name) == true
27
+
28
+ instance_variable_set(var_name, assign_value(path))
29
+ rescue StandardError, LoadError => e
30
+ logger.error("Error loading #{ extracted_name&.nil? ? '' : extracted_name } patch", e)
31
+ false
32
+ end
33
+
34
+ # Some of the requires have some extra conditions for them to require
35
+ # the C patches, so this method is helping us move the logic by making some
36
+ # conditions
37
+ def assign_value path
38
+ case path
39
+ when /fiber/
40
+ require path if Funchook.available?
41
+ when /interpolation26/
42
+ require path if ::Contrast::AGENT.patch_interpolation? && Funchook.available?
43
+ else
44
+ require path
45
+ end
46
+ true
47
+ end
48
+
49
+ # Generate the needed instance variable name and return the extracted name
50
+ def gen_name path
51
+ extracted_name = path.split(%r{[\s_/]})&.uniq&.delete_if do |s|
52
+ s.empty? || s == 'cs' || s == 'assess' || s == 'track'
53
+ end
54
+ extracted_name = (extracted_name&.length || 0) > 1 ? extracted_name&.join('_') : extracted_name&.pop
55
+ ["@_instrument_#{ extracted_name }_track", extracted_name]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end