contrast-agent 4.9.1 → 4.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.rspec_parallel +6 -0
  4. data/ext/cs__assess_module/cs__assess_module.c +48 -0
  5. data/ext/cs__assess_module/cs__assess_module.h +7 -0
  6. data/ext/cs__common/cs__common.c +24 -7
  7. data/ext/cs__common/cs__common.h +12 -2
  8. data/ext/cs__contrast_patch/cs__contrast_patch.c +48 -12
  9. data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -4
  10. data/ext/cs__os_information/cs__os_information.c +31 -0
  11. data/ext/cs__os_information/cs__os_information.h +7 -0
  12. data/ext/{cs__protect_kernel → cs__os_information}/extconf.rb +0 -0
  13. data/lib/contrast/agent/assess/contrast_event.rb +1 -2
  14. data/lib/contrast/agent/assess/contrast_object.rb +1 -4
  15. data/lib/contrast/agent/assess/finalizers/hash.rb +0 -1
  16. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
  17. data/lib/contrast/agent/assess/policy/patcher.rb +0 -1
  18. data/lib/contrast/agent/assess/policy/policy_scanner.rb +0 -2
  19. data/lib/contrast/agent/assess/policy/preshift.rb +29 -12
  20. data/lib/contrast/agent/assess/policy/propagation_method.rb +71 -142
  21. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  22. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -2
  23. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +31 -11
  24. data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
  25. data/lib/contrast/agent/assess/policy/propagator/split.rb +3 -2
  26. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +1 -0
  27. data/lib/contrast/agent/assess/policy/rewriter_patch.rb +0 -1
  28. data/lib/contrast/agent/assess/policy/source_method.rb +15 -88
  29. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +0 -1
  30. data/lib/contrast/agent/assess/policy/trigger_method.rb +45 -172
  31. data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
  32. data/lib/contrast/agent/assess/property/evented.rb +2 -1
  33. data/lib/contrast/agent/assess/property/tagged.rb +15 -132
  34. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +0 -1
  35. data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
  36. data/lib/contrast/agent/disable_reaction.rb +1 -1
  37. data/lib/contrast/agent/exclusion_matcher.rb +0 -4
  38. data/lib/contrast/agent/inventory/database_config.rb +117 -0
  39. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +7 -5
  40. data/lib/contrast/agent/inventory/policy/datastores.rb +2 -2
  41. data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
  42. data/lib/contrast/agent/middleware.rb +23 -0
  43. data/lib/contrast/agent/patching/policy/after_load_patch.rb +3 -0
  44. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +17 -12
  45. data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
  46. data/lib/contrast/agent/patching/policy/module_policy.rb +2 -4
  47. data/lib/contrast/agent/patching/policy/patch.rb +42 -238
  48. data/lib/contrast/agent/patching/policy/patch_status.rb +3 -7
  49. data/lib/contrast/agent/patching/policy/patcher.rb +10 -49
  50. data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +1 -1
  51. data/lib/contrast/agent/protect/rule/no_sqli.rb +7 -53
  52. data/lib/contrast/agent/protect/rule/sql_sample_builder.rb +137 -0
  53. data/lib/contrast/agent/protect/rule/sqli.rb +7 -70
  54. data/lib/contrast/agent/reaction_processor.rb +1 -1
  55. data/lib/contrast/agent/request.rb +9 -4
  56. data/lib/contrast/agent/request_context.rb +51 -33
  57. data/lib/contrast/agent/request_handler.rb +7 -3
  58. data/lib/contrast/agent/rule_set.rb +2 -4
  59. data/lib/contrast/agent/scope.rb +32 -20
  60. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +71 -0
  61. data/lib/contrast/agent/static_analysis.rb +5 -3
  62. data/lib/contrast/agent/telemetry.rb +129 -0
  63. data/lib/contrast/agent/telemetry_event.rb +34 -0
  64. data/lib/contrast/agent/thread_watcher.rb +43 -14
  65. data/lib/contrast/agent/tracepoint_hook.rb +16 -3
  66. data/lib/contrast/agent/version.rb +1 -1
  67. data/lib/contrast/agent.rb +6 -1
  68. data/lib/contrast/api/communication/messaging_queue.rb +12 -6
  69. data/lib/contrast/api/communication/service_lifecycle.rb +4 -1
  70. data/lib/contrast/api/communication/socket_client.rb +4 -4
  71. data/lib/contrast/api/decorators/agent_startup.rb +4 -4
  72. data/lib/contrast/api/decorators/application_startup.rb +6 -5
  73. data/lib/contrast/api/decorators/route_coverage.rb +24 -1
  74. data/lib/contrast/components/agent.rb +5 -2
  75. data/lib/contrast/components/api.rb +34 -0
  76. data/lib/contrast/components/app_context.rb +24 -0
  77. data/lib/contrast/components/assess.rb +13 -3
  78. data/lib/contrast/components/base.rb +2 -2
  79. data/lib/contrast/components/config.rb +91 -11
  80. data/lib/contrast/components/contrast_service.rb +10 -2
  81. data/lib/contrast/components/logger.rb +13 -8
  82. data/lib/contrast/components/scope.rb +9 -28
  83. data/lib/contrast/config/api_configuration.rb +22 -0
  84. data/lib/contrast/config/assess_configuration.rb +1 -0
  85. data/lib/contrast/config/base_configuration.rb +14 -6
  86. data/lib/contrast/config/env_variables.rb +25 -0
  87. data/lib/contrast/config/root_configuration.rb +1 -0
  88. data/lib/contrast/config/service_configuration.rb +2 -1
  89. data/lib/contrast/config.rb +1 -0
  90. data/lib/contrast/configuration.rb +22 -15
  91. data/lib/contrast/extension/assess/array.rb +1 -11
  92. data/lib/contrast/extension/assess/eval_trigger.rb +0 -20
  93. data/lib/contrast/extension/assess/fiber.rb +0 -11
  94. data/lib/contrast/extension/assess/hash.rb +0 -10
  95. data/lib/contrast/extension/assess/kernel.rb +1 -10
  96. data/lib/contrast/extension/assess/marshal.rb +3 -11
  97. data/lib/contrast/extension/assess/regexp.rb +0 -11
  98. data/lib/contrast/extension/assess/string.rb +1 -26
  99. data/lib/contrast/extension/extension.rb +61 -0
  100. data/lib/contrast/framework/grape/support.rb +174 -0
  101. data/lib/contrast/framework/manager.rb +56 -18
  102. data/lib/contrast/framework/rack/support.rb +1 -1
  103. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
  104. data/lib/contrast/framework/rails/patch/assess_configuration.rb +0 -1
  105. data/lib/contrast/framework/rails/patch/support.rb +35 -30
  106. data/lib/contrast/framework/rails/railtie.rb +1 -1
  107. data/lib/contrast/framework/rails/rewrite/active_record_named.rb +1 -0
  108. data/lib/contrast/framework/rails/support.rb +60 -13
  109. data/lib/contrast/framework/sinatra/support.rb +1 -1
  110. data/lib/contrast/logger/application.rb +4 -0
  111. data/lib/contrast/logger/log.rb +89 -15
  112. data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
  113. data/lib/contrast/utils/assess/property/tagged_utils.rb +142 -0
  114. data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
  115. data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
  116. data/lib/contrast/utils/class_util.rb +58 -44
  117. data/lib/contrast/utils/exclude_key.rb +20 -0
  118. data/lib/contrast/utils/io_util.rb +43 -35
  119. data/lib/contrast/utils/lru_cache.rb +45 -0
  120. data/lib/contrast/utils/metrics_hash.rb +59 -0
  121. data/lib/contrast/utils/os.rb +23 -0
  122. data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
  123. data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
  124. data/lib/contrast/utils/requests_client.rb +150 -0
  125. data/lib/contrast/utils/ruby_ast_rewriter.rb +16 -13
  126. data/lib/contrast/utils/tag_util.rb +2 -1
  127. data/lib/contrast/utils/telemetry.rb +78 -0
  128. data/lib/contrast/utils/telemetry_identifier.rb +137 -0
  129. data/lib/contrast.rb +19 -1
  130. data/resources/assess/policy.json +208 -7
  131. data/resources/deadzone/policy.json +91 -0
  132. data/ruby-agent.gemspec +12 -2
  133. data/service_executables/VERSION +1 -1
  134. data/service_executables/linux/contrast-service +0 -0
  135. data/service_executables/mac/contrast-service +0 -0
  136. metadata +102 -18
  137. data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
  138. data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
  139. data/lib/contrast/extension/protect/kernel.rb +0 -39
  140. data/lib/contrast/utils/inventory_util.rb +0 -113
@@ -13,6 +13,8 @@ module Contrast
13
13
  class Support
14
14
  extend Contrast::Framework::BaseSupport
15
15
  extend Contrast::Framework::Rails::Patch::Support
16
+ include Contrast::Components::Logger::InstanceMethods
17
+ extend Contrast::Components::Logger::InstanceMethods
16
18
 
17
19
  class << self
18
20
  RAILS_MODULE_NAME_VERSION = Gem::Version.new('6.0.0')
@@ -49,31 +51,36 @@ module Contrast
49
51
  # Find the current route, based on the provided Request wrapper
50
52
  #
51
53
  # @param request[Contrast::Agent::Request]
52
- # @return [Contrast::Api::Dtm::RouteCoverage]
54
+ # @return [Contrast::Api::Dtm::RouteCoverage, nil]
53
55
  def current_route request
54
56
  return unless ::Rails.cs__respond_to?(:application)
55
57
 
58
+ # ActionDispatch::Journey::Path::Pattern::MatchData, Hash, ActionDispatch::Journey::Route, Array<String>
56
59
  match, _params, route, path = get_full_route(request.rack_request)
60
+ unless route
61
+ logger.warn('Unable to determine the current route of this request')
62
+ return
63
+ end
57
64
 
58
65
  original_url = request.rack_request.path_info
59
-
66
+ mounted_app = route&.app&.app
60
67
  # Route is either the final rails route, or a router that points to a Sinatra controller.
61
- if Contrast::Framework::Sinatra::Support.sinatra_controller?(route.app.app)
62
- # Create a request copied from current request, but with the base path removed from path_info.
63
- new_req = ::ActionDispatch::Request.new(request.env)
64
- new_req.path_info = new_req.path_info.gsub((path << match).join, '')
65
-
66
- return Contrast::Framework::Sinatra::Support.current_route(new_req, route.app.app, original_url)
68
+ if mounted_app && Contrast::Framework::Sinatra::Support.sinatra_controller?(mounted_app)
69
+ return mounted_sinatra_route(request, match, path, route, original_url)
70
+ end
71
+ if mounted_app && Contrast::Framework::Grape::Support.grape_controller?(mounted_app)
72
+ return mounted_grape_route(request, match, path, route, original_url)
67
73
  end
68
74
 
69
75
  Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route, original_url)
70
- rescue StandardError => _e
76
+ rescue StandardError => e
77
+ logger.warn('Unable to determine the current route of this request', e)
71
78
  nil
72
79
  end
73
80
 
74
81
  # Copy a request for modification.
75
82
  #
76
- # @param [::ActionDispatch::Request] original env.
83
+ # @param env [::ActionDispatch::Request] original env.
77
84
  # @return [::ActionDispatch::Request] a copy of original env with rails env merged.
78
85
  def retrieve_request env
79
86
  rails_env = ::Rails.application.env_config.merge(env)
@@ -91,18 +98,21 @@ module Contrast
91
98
 
92
99
  # Determine if route is a Rails engine route.
93
100
  #
94
- # @param [Object] app or route that points to a ::Rails::Engine
101
+ # @param route [Object] app or route that points to a ::Rails::Engine
95
102
  # @return [bool] whether the router is an engine or not.
96
103
  def engine_route? route
104
+ return false unless route&.app&.app
105
+
97
106
  route.app.is_a?(::ActionDispatch::Routing::Mapper::Constraints) && route.app.app < ::Rails::Engine
98
107
  end
99
108
 
100
109
  # Recursively get final route traversing engines as required.
101
110
  #
102
111
  # @param request [::Rack::Request] the rack request as will be handed to rails controller.
103
- # @param top_router [::ActionDispatch::Journer::Router] the current router relative to the previous.
112
+ # @param top_router [::ActionDispatch::Journey::Router] the current router relative to the previous.
104
113
  # @param path [Array<String>] the chunks of path that have been seen.
105
- # @return [Array<array>] the final set of rails route classes.
114
+ # @return [Array<Object>] the final set of rails route classes.
115
+ # ActionDispatch::Journey::Path::Pattern::MatchData, Hash, ActionDispatch::Journey::Route, Array<String>
106
116
  def get_full_route request, top_router = ::Rails.application.routes.router, path = []
107
117
  return if (route_matches = top_router.send(:find_routes, request)).empty?
108
118
 
@@ -132,6 +142,43 @@ module Contrast
132
142
  end
133
143
  route_list
134
144
  end
145
+
146
+ # @param request[Contrast::Agent::Request]
147
+ # @param match [ActionDispatch::Journey::Path::Pattern::MatchData]
148
+ # @param path [Array<String>] the path of this request, built out from each nested
149
+ # ActionDispatch::Journey::Path::Pattern::MatchData
150
+ # @param route [::ActionDispatch::Journey::Route]
151
+ # @param original_url [String] the full url of this request, including the mount
152
+ # @return [Contrast::Api::Dtm::RouteCoverage, nil]
153
+ def mounted_sinatra_route request, match, path, route, original_url
154
+ new_req = unmounted_route(request, match, path)
155
+ Contrast::Framework::Sinatra::Support.current_route(new_req, route.app.app, original_url)
156
+ end
157
+
158
+ # @param request[Contrast::Agent::Request]
159
+ # @param match [ActionDispatch::Journey::Path::Pattern::MatchData]
160
+ # @param path [Array<String>] the path of this request, built out from each nested
161
+ # ActionDispatch::Journey::Path::Pattern::MatchData
162
+ # @param route [::ActionDispatch::Journey::Route]
163
+ # @param original_url [String] the full url of this request, including the mount
164
+ # @return [Contrast::Api::Dtm::RouteCoverage, nil]
165
+ def mounted_grape_route request, match, path, route, original_url
166
+ new_req = unmounted_route(request, match, path)
167
+ Contrast::Framework::Grape::Support.current_route(new_req, route.app.app, original_url)
168
+ end
169
+
170
+ # Create a request copied from current request, but with the base path removed from path_info, as that's
171
+ # the mount.
172
+ #
173
+ # @param request[Contrast::Agent::Request]
174
+ # @param match []
175
+ # @param path [String] the path of this request
176
+ # @return [::ActionDispatch::Request]
177
+ def unmounted_route request, match, path
178
+ new_req = ::ActionDispatch::Request.new(request.env)
179
+ new_req.path_info = new_req.path_info.gsub((path << match).join, '')
180
+ new_req
181
+ end
135
182
  end
136
183
  end
137
184
  end
@@ -106,7 +106,7 @@ module Contrast
106
106
  def _route_recurse controller, method, route
107
107
  return if controller.nil? || controller.cs__class == NilClass
108
108
 
109
- route_patterns = controller.routes.fetch(method, []).map(&:first)
109
+ route_patterns = controller.routes.fetch(method) { [] }.map(&:first)
110
110
  route_pattern = route_patterns&.find do |matcher|
111
111
  matcher.params(route) # ::Mustermann::Sinatra match.
112
112
  end
@@ -1,6 +1,8 @@
1
1
  # Copyright (c) 2021 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/utils/exclude_key'
5
+
4
6
  module Contrast
5
7
  module Logger
6
8
  # Our decorator for the Ougai logger allowing for the logging of the
@@ -29,6 +31,8 @@ module Contrast
29
31
  loggable = ::Contrast::CONFIG.loggable
30
32
  info('Current configuration', configuration: loggable)
31
33
  env_keys = ENV.keys.select do |env_key|
34
+ next if Contrast::Utils::ExcludeKey.excludable? env_key.to_s
35
+
32
36
  env_key&.to_s&.start_with?(Contrast::Components::Config::CONTRAST_ENV_MARKER)
33
37
  end
34
38
  env_items = env_keys.map { |env_key| Contrast::Utils::EnvConfigurationItem.new(env_key, nil) }
@@ -13,6 +13,75 @@ require 'contrast/logger/time'
13
13
  require 'contrast/components/config'
14
14
 
15
15
  module Contrast
16
+ # This module allows us to dynamically weave timing into our code, so that only when the time is actually needed do
17
+ # we pay the penalty for that timing block
18
+ module TraceTiming
19
+ def methods_to_time
20
+ @_methods_to_time ||= []
21
+ end
22
+
23
+ # Store info about methods for later patching.
24
+ METHOD_INFO = Struct.new(:clazz, :method_name, :custom_msg, :aliased)
25
+
26
+ # Add a method to the list of methods to be trace timed if logger set to TRACE. Enables trace timing after if
27
+ # logger set to TRACE.
28
+ #
29
+ # @params: clazz [Class] the class of the method to time.
30
+ # @params: method [Symbol] the method to time.
31
+ # @params: method [String] optional custom logging message.
32
+ def add_method_to_trace_timing clazz, method, msg = nil
33
+ methods_to_time.append(METHOD_INFO.new(clazz, method, msg, false))
34
+ enable_trace_timing if logger.level == ::Ougai::Logging::TRACE
35
+ end
36
+
37
+ # Add a method to the list of methods to be trace timed if logger set to TRACE. Enables trace timing after if
38
+ # logger set to TRACE.
39
+ #
40
+ # @params: method_spec [METHOD_INFO] specs about the method to be timed.
41
+ # @params: class_method [Boolean] whether this is or isn't a class/module method.
42
+ def trace_time_class_method meth_spec, class_method
43
+ untimed_func_symbol = "untimed_#{ meth_spec.method_name }".to_sym
44
+ send_to = class_method ? meth_spec.clazz.cs__singleton_class : meth_spec.clazz
45
+ meth_spec.clazz.class_eval do
46
+ include Contrast::Components::Logger::InstanceMethods
47
+ extend Contrast::Components::Logger::InstanceMethods
48
+
49
+ send_to.send(:alias_method, untimed_func_symbol, meth_spec.method_name)
50
+ meth_spec.aliased = true
51
+
52
+ log_message = "Elapsed time for #{ meth_spec.method_name }."
53
+ log_message = meth_spec.custom_message if meth_spec.custom_msg
54
+
55
+ send_to.send(:define_method, meth_spec.method_name) do |*args, **kwargs, &block| # rubocop:disable Performance/Kernel/DefineMethod
56
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
57
+ rv = if kwargs.empty?
58
+ send(untimed_func_symbol, *args, &block)
59
+ else
60
+ send(untimed_func_symbol, *args, **kwargs, &block)
61
+ end
62
+ delta = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
63
+ logger.trace(log_message, elapsed: delta * 1000)
64
+ rv
65
+ end
66
+ end
67
+ end
68
+
69
+ # Enable trace timing of methods specified in @_methods_to_time via aliasing.
70
+ def enable_trace_timing
71
+ methods_to_time.each do |meth_spec|
72
+ next if meth_spec.aliased
73
+
74
+ is_class_method = meth_spec.clazz.singleton_methods(false).include?(meth_spec.method_name)
75
+ trace_time_class_method meth_spec, is_class_method
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ module Contrast
82
+ # Used as a wrapper around our logging. The module option specifically adds in a new method for error that raises the
83
+ # logged exception, used in testing so that we can see if anything unexpected happens without it being swallowed
84
+ # while still providing safe options for customers.
16
85
  module Logger
17
86
  # For development set following env var to raise logged exceptions instead of just logging.
18
87
  if ENV['CONTRAST__AGENT__RUBY_MORE_COWBELL']
@@ -20,22 +89,22 @@ module Contrast
20
89
  alias_method :cs__error, :error
21
90
  alias_method :cs__warn, :warn
22
91
 
23
- def error msg, exc, **kwargs
24
- cs__error(msg, exc, **kwargs)
25
- raise exc if exc && exc.cs__class < Exception
26
- end
27
-
28
- def warn msg, exc, **kwargs
29
- cs__warn(msg, exc, **kwargs)
30
- raise exc if exc && exc.cs__class < Exception
92
+ def error *args, **kwargs
93
+ if kwargs.empty?
94
+ cs__error(*args)
95
+ else
96
+ cs__error(*args, **kwargs)
97
+ end
98
+ args.each { |arg| raise arg if arg && arg.cs__class < Exception }
31
99
  end
32
100
  end
33
101
  end
34
102
 
35
- # This class functions to serve as a wrapper around our logging, as we need
36
- # to be able to dynamically update level based on updates to TeamServer.
103
+ # This class functions to serve as a wrapper around our logging, as we need to be able to dynamically update
104
+ # level based on updates to TeamServer.
37
105
  class Log
38
106
  include Singleton
107
+ include ::Contrast::TraceTiming
39
108
 
40
109
  DEFAULT_NAME = 'contrast.log'
41
110
  DEFAULT_LEVEL = ::Ougai::Logging::Severity::INFO
@@ -49,8 +118,8 @@ module Contrast
49
118
  update
50
119
  end
51
120
 
52
- # Given new settings from TeamServer, update our logging to use the new
53
- # file and level, assuming they weren't set by local configuration.
121
+ # Given new settings from TeamServer, update our logging to use the new file and level, assuming they weren't
122
+ # set by local configuration.
54
123
  #
55
124
  # @param log_file [String] the file to which to log, as provided by TeamServer settings
56
125
  # @param log_level [String] the level at which to log, as provided by TeamServer settings
@@ -67,6 +136,8 @@ module Contrast
67
136
  @previous_path = current_path
68
137
  @previous_level = current_level_const
69
138
 
139
+ enable_trace_timing if current_level_const == ::Ougai::Logging::TRACE
140
+
70
141
  @_logger = build(path: current_path, level_const: current_level_const)
71
142
  # If we're logging to a new path, then let's start it w/ our helpful
72
143
  # data gathering messages
@@ -76,6 +147,9 @@ module Contrast
76
147
  logger.error('Unable to process update to LoggerManager.', e)
77
148
  else
78
149
  puts 'Unable to process update to LoggerManager.'
150
+ raise e if ENV['CONTRAST__AGENT__RUBY_MORE_COWBELL']
151
+
152
+ puts e.message
79
153
  puts e.backtrace.join("\n")
80
154
  end
81
155
  end
@@ -158,7 +232,8 @@ module Contrast
158
232
  # @return [::Ougai::Logging::Severity] the level at which to log
159
233
  def find_valid_level log_level
160
234
  config = ::Contrast::CONFIG.root.agent.logger
161
- config_level = config.level&.length&.positive? ? config.level : nil
235
+ config_level = config&.level&.length&.positive? ? config.level : nil
236
+
162
237
  valid_level(config_level || log_level)
163
238
  end
164
239
 
@@ -174,8 +249,7 @@ module Contrast
174
249
  DEFAULT_LEVEL
175
250
  end
176
251
 
177
- # Log that the Agent log has changed and include some default information
178
- # at the start of the log.
252
+ # Log that the Agent log has changed and include some default information at the start of the log.
179
253
  def log_update
180
254
  logger.debug('Initialized new contrast agent logger')
181
255
  logger.debug_with_time('middleware: log environment') do
@@ -0,0 +1,129 @@
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
+ module Contrast
5
+ module Utils
6
+ module Assess
7
+ # This module will include all methods for some internal validations in the PropagationMethod module
8
+ # and some other module methods from the same place, so we can ease the main module
9
+ module PropagationMethodUtils
10
+ APPEND_ACTION = 'APPEND'
11
+ CENTER_ACTION = 'CENTER'
12
+ INSERT_ACTION = 'INSERT'
13
+ KEEP_ACTION = 'KEEP'
14
+ NEXT_ACTION = 'NEXT'
15
+ NOOP_ACTION = 'NOOP'
16
+ PREPEND_ACTION = 'PREPEND'
17
+ REPLACE_ACTION = 'REPLACE'
18
+ REMOVE_ACTION = 'REMOVE'
19
+ REVERSE_ACTION = 'REVERSE'
20
+ SPLAT_ACTION = 'SPLAT'
21
+ SPLIT_ACTION = 'SPLIT'
22
+ DB_WRITE_ACTION = 'DB_WRITE'
23
+ CUSTOM_ACTION = 'CUSTOM'
24
+
25
+ ZERO_LENGTH_ACTIONS = [DB_WRITE_ACTION, CUSTOM_ACTION, KEEP_ACTION, REPLACE_ACTION, SPLAT_ACTION].cs__freeze
26
+
27
+ PROPAGATION_ACTIONS = {
28
+ APPEND_ACTION => Contrast::Agent::Assess::Policy::Propagator::Append,
29
+ CENTER_ACTION => Contrast::Agent::Assess::Policy::Propagator::Center,
30
+ INSERT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Insert,
31
+ KEEP_ACTION => Contrast::Agent::Assess::Policy::Propagator::Keep,
32
+ NEXT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Next,
33
+ NOOP_ACTION => nil,
34
+ PREPEND_ACTION => Contrast::Agent::Assess::Policy::Propagator::Prepend,
35
+ REPLACE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Replace,
36
+ REMOVE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Remove,
37
+ REVERSE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Reverse,
38
+ SPLAT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Splat,
39
+ SPLIT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Split
40
+ }.cs__freeze
41
+
42
+ def determine_target propagation_node, ret, object, args
43
+ target = propagation_node.targets[0]
44
+ case target
45
+ when Contrast::Utils::ObjectShare::OBJECT_KEY
46
+ object
47
+ when Contrast::Utils::ObjectShare::RETURN_KEY
48
+ ret
49
+ else
50
+ args[target]
51
+ end
52
+ end
53
+
54
+ # Custom actions tend to be the more complex of our propagations. Often, the method has to make decisions
55
+ # about the target based on the context with which the method was called. As such, defer determining if the
56
+ # target is valid to that method.
57
+ #
58
+ # In all other cases, a target is valid for propagation if it is not nil
59
+ #
60
+ # @param target [Object] the thing to which to propagate
61
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
62
+ # propagation event.
63
+ # @return [Boolean]
64
+ def valid_target? target, propagation_node
65
+ return true if propagation_node.action == CUSTOM_ACTION
66
+
67
+ !!target
68
+ end
69
+
70
+ # If the action required needs a length and the target does not have one, the length is not valid
71
+ #
72
+ # @param target [Object] the thing to which to propagate
73
+ # @param action [String] the name of the action taken during this propagation
74
+ # @return [Boolean]
75
+ def valid_length? target, action
76
+ return true if ZERO_LENGTH_ACTIONS.include?(action)
77
+
78
+ if Contrast::Utils::DuckUtils.quacks_to?(target, :length)
79
+ target.length != 0 # rubocop:disable Style/ZeroLengthPredicate
80
+ else
81
+ !target.to_s.empty?
82
+ end
83
+ end
84
+
85
+ # Before we do any work, we should check if we even need to. If the source and target of this patcher are
86
+ # not tracked, there's no need to do anything. A copy of nothing is still nothing.
87
+ #
88
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
89
+ # propagation event.
90
+ # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to
91
+ # the invocation of the patched method.
92
+ # @param target [Object] the thing to which to propagate
93
+ # @return [Boolean]
94
+ def can_propagate? propagation_node, preshift, target
95
+ return false unless appropriate_target?(propagation_node, target)
96
+ return true if Contrast::Utils::Assess::TrackingUtil.tracked?(target)
97
+ return false unless preshift
98
+
99
+ propagation_node.sources.each do |source|
100
+ case source
101
+ when Contrast::Utils::ObjectShare::OBJECT_KEY
102
+ return true if Contrast::Utils::Assess::TrackingUtil.tracked?(preshift.object)
103
+ else
104
+ # has to be P, there's no ret source type (yet? ever?)
105
+ return true if preshift.args && Contrast::Utils::Assess::TrackingUtil.tracked?(preshift.args[source])
106
+ end
107
+ end
108
+ false
109
+ end
110
+
111
+ # We cannot propagate to frozen things that have not been updated to work with our property tracking,
112
+ # unless they're duplicable and the return. We probably shouldn't propagate to frozen things at all, as
113
+ # they're supposed to be immutable, but third parties do jenky things, so allow it as long as it is safe to
114
+ # do.
115
+ #
116
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
117
+ # propagation event.
118
+ # @param target [Object] the Target to which to propagate.
119
+ # @return [Boolean] if the target can be propagated to
120
+ def appropriate_target? propagation_node, target
121
+ # special handle Returns b/c we can do unfreezing magic during propagation
122
+ return true if propagation_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY
123
+
124
+ Contrast::Agent::Assess::Tracker.trackable?(target)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,142 @@
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
+ module Contrast
5
+ module Utils
6
+ module Assess
7
+ # This module will include all methods for some internal validations in the Tagged property module
8
+ # and some other module methods from the same place, so we can ease the main module
9
+ # This module includes simple methods for the tags like
10
+ # adding tags, getting tags, deleting tags and similar
11
+ module TaggedUtils
12
+ # Given a tag name and range object, add a new tag to this
13
+ # collection. If the given range touches an existing tag,
14
+ # we'll combine the two, adjusting the existing one and
15
+ # dropping this new one.
16
+ #
17
+ # @param label [String] the name of the tag
18
+ # @param range [Range] the Range that the tag covers, inclusive to
19
+ # exclusive
20
+ def add_tag label, range
21
+ length = range.end - range.begin
22
+ tag = Contrast::Agent::Assess::Tag.new(label, length, range.begin)
23
+ existing = fetch_tag(label)
24
+ tags[label] = Contrast::Utils::TagUtil.ordered_merge(existing, tag)
25
+ end
26
+
27
+ def set_tags label, tag_ranges
28
+ tags[label] = tag_ranges
29
+ end
30
+
31
+ # Returns a list of all current tags.
32
+ #
33
+ # @return [Hash<Contrast::Agent::Assess::Tag>]
34
+ def get_tags # rubocop:disable Naming/AccessorMethodName
35
+ return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
36
+
37
+ tags
38
+ end
39
+
40
+ # We'll use this as a helper method to retrieve tags from the hash.
41
+ # Because the hash auto-populates an empty array when we try to
42
+ # access a tag in it, we cannot use the [] method without side
43
+ # effect. To get around this, we'll use a fetch work around.
44
+ #
45
+ # @param label [Symbol] the label to look up
46
+ # @return [Array<Contrast::Agent::Assess::Tag>] all the tags with
47
+ # that label
48
+ def fetch_tag label
49
+ get_tags.fetch(label, nil) if tracked?
50
+ end
51
+
52
+ # Remove all tags with a given label
53
+ def delete_tags label
54
+ tags.delete(label) if tracked?
55
+ end
56
+
57
+ # Reset the tag hash
58
+ def clear_tags
59
+ tags.clear if tracked?
60
+ end
61
+
62
+ # Returns a list of all current tag labels, most likely to be used for
63
+ # a splat operation
64
+ def tag_keys
65
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
66
+
67
+ tags.keys
68
+ end
69
+
70
+ # Calls merge to combine touching or overlapping tags
71
+ # Deletes empty tags
72
+ def cleanup_tags
73
+ return unless tracked?
74
+
75
+ Contrast::Utils::TagUtil.merge_tags(tags)
76
+ tags.delete_if { |_, value| value.empty? }
77
+ end
78
+
79
+ # Find all of the ranges that span a given index. This is used
80
+ # in propagation when we need to shift tags about. For instance, in
81
+ # the append method when we need to see if any tag at the end needs
82
+ # to be expanded out to the size of the new String.
83
+ #
84
+ # Note: Tags do not know their key, so this is only the range covered
85
+ #
86
+ # @param idx [Integer] the index to check for tags
87
+ # @return [Array<Contrast::Agent::Assess::Tag>] a set of all the Tags
88
+ # covering the given index. This is only the ranges, not the names.
89
+ def tags_at idx
90
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
91
+
92
+ at = []
93
+ tags.each_value do |tag_array|
94
+ tag_array.each do |tag|
95
+ if tag.covers?(idx)
96
+ at << tag
97
+ elsif tag.above?(idx)
98
+ break
99
+ end
100
+ end
101
+ end
102
+ at
103
+ end
104
+
105
+ # given a range, select all tags in that range the selected tags are
106
+ # shifted such that the start index of the new tag (0) aligns with
107
+ # the given start index in the range
108
+ #
109
+ # current tags: 5-15
110
+ # range : 5-10
111
+ # result : 0-05
112
+ #
113
+ # Note that we disable Lint/DuplicateBranch in this branch in order
114
+ # list out all tag range cases in the proper order to make this
115
+ # easier to understand
116
+ #
117
+ # @param range [Range] the span to check, inclusive to exclusive
118
+ # @return [Hash{String => Contrast::Agent::Assess::Tag}] the hash of
119
+ # key to tags
120
+ def tags_at_range range
121
+ return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
122
+
123
+ at = Hash.new { |h, k| h[k] = [] }
124
+ tags.each_pair do |key, value|
125
+ add = nil
126
+ value.each do |tag|
127
+ within_range = resize_to_range(tag, range)
128
+ if within_range
129
+ add ||= []
130
+ add << within_range
131
+ end
132
+ end
133
+ next unless add&.any?
134
+
135
+ at[key] = add
136
+ end
137
+ at
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,83 @@
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
+ module Contrast
5
+ module Utils
6
+ module Assess
7
+ # This module will include all methods for some internal validations in the SourceMethod module
8
+ # and some other module methods from the same place, so we can ease the main module
9
+ module SourceMethodUtils
10
+ # Safely duplicate the target, or return nil
11
+ #
12
+ # @param target [Object] the thing to check for duplication
13
+ def safe_dup target
14
+ target.dup
15
+ rescue StandardError => _e
16
+ nil
17
+ end
18
+
19
+ # Find the name of the source
20
+ #
21
+ # @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the node to direct applying this source
22
+ # event
23
+ # @param object [Object] the Object on which the method was invoked
24
+ # @param ret [Object] the Return of the invoked method
25
+ # @param args [Array<Object>] the Arguments with which the method was invoked
26
+ # @return [String, nil] the human readable name of the target to which this source event applies, or nil if
27
+ # none provided by the node
28
+ def determine_source_name source_node, object, ret, *args
29
+ return source_node.get_property('dynamic_source_name') if source_node.type == 'UNTRUSTED_DATABASE'
30
+
31
+ source_node_source = source_node.sources[0]
32
+ case source_node_source
33
+ when nil
34
+ nil
35
+ when Contrast::Utils::ObjectShare::RETURN_KEY
36
+ ret
37
+ when Contrast::Utils::ObjectShare::OBJECT_KEY
38
+ object
39
+ else
40
+ args[source_node_source]
41
+ end
42
+ end
43
+
44
+ # Determine if we should analyze this method invocation for a Source or not. We should if we have enough
45
+ # information to build the context of this invocation, we're not disabled, and we can't immediately
46
+ # determine the invocation was done safely.
47
+ #
48
+ # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] the policy that applies to the
49
+ # method being called
50
+ # @param object [Object] the Object on which the method was invoked
51
+ # @param ret [Object] the Return of the invoked method
52
+ # @param args [Array<Object>] the Arguments with which the method was invoked
53
+ # @return [boolean] if the invocation of this method should be analyzed
54
+ def analyze? method_policy, object, ret, args
55
+ return false unless method_policy&.source_node
56
+ return false unless ::Contrast::ASSESS.enabled?
57
+ return false unless Contrast::Agent::REQUEST_TRACKER.current&.analyze_request?
58
+
59
+ !safe_invocation?(method_policy.source_node, object, ret, args)
60
+ end
61
+
62
+ # Determine if the method was invoked safely.
63
+ #
64
+ # @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the node to direct applying this source
65
+ # event
66
+ # @param _object [Object] the Object on which the method was invoked
67
+ # @param _ret [Object] the Return of the invoked method
68
+ # @param args [Array<Object>] the Arguments with which the method was invoked
69
+ # @return [boolean] if the invocation of this method was safe
70
+ def safe_invocation? source_node, _object, _ret, args
71
+ # According the the Rack Specification https://github.com/rack/rack/blob/master/SPEC.rdoc, any header
72
+ # from the Request will start with HTTP_. As such, only Headers with that key should be considered for
73
+ # tracking, as the others have come from the Framework or Middleware stashing in the ENV. Rails, for
74
+ # instance, uses action_dispatch. to store several values. Technically, you can't call
75
+ # Rack::Request#get_header without a parameter, and that parameter should be a String, but trust no one.
76
+ source_node.id == 'Assess:Source:Rack::Request::Env#get_header' &&
77
+ args&.any? &&
78
+ !args[0].to_s.start_with?('HTTP_')
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end