contrast-agent 4.12.0 → 4.14.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +1 -0
  3. data/ext/cs__assess_module/cs__assess_module.c +48 -0
  4. data/ext/cs__assess_module/cs__assess_module.h +7 -0
  5. data/ext/cs__common/cs__common.c +5 -0
  6. data/ext/cs__common/cs__common.h +8 -0
  7. data/ext/cs__contrast_patch/cs__contrast_patch.c +16 -1
  8. data/ext/cs__os_information/cs__os_information.c +31 -0
  9. data/ext/cs__os_information/cs__os_information.h +7 -0
  10. data/ext/cs__os_information/extconf.rb +5 -0
  11. data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
  12. data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
  13. data/lib/contrast/agent/assess/policy/propagation_method.rb +2 -116
  14. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  15. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -1
  16. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
  17. data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
  18. data/lib/contrast/agent/assess/policy/trigger_method.rb +45 -110
  19. data/lib/contrast/agent/assess/policy/trigger_node.rb +14 -6
  20. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
  21. data/lib/contrast/agent/assess/property/tagged.rb +53 -185
  22. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
  23. data/lib/contrast/agent/deadzone/policy/policy.rb +1 -1
  24. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +1 -0
  25. data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
  26. data/lib/contrast/agent/middleware.rb +14 -62
  27. data/lib/contrast/agent/patching/policy/method_policy.rb +3 -89
  28. data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
  29. data/lib/contrast/agent/patching/policy/patch.rb +28 -235
  30. data/lib/contrast/agent/patching/policy/patcher.rb +14 -49
  31. data/lib/contrast/agent/reporting/report.rb +21 -0
  32. data/lib/contrast/agent/reporting/reporter.rb +142 -0
  33. data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
  34. data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
  35. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
  36. data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
  37. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
  38. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
  39. data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
  40. data/lib/contrast/agent/request.rb +2 -81
  41. data/lib/contrast/agent/request_context.rb +4 -128
  42. data/lib/contrast/agent/request_context_extend.rb +138 -0
  43. data/lib/contrast/agent/request_handler.rb +7 -3
  44. data/lib/contrast/agent/response.rb +2 -73
  45. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +94 -0
  46. data/lib/contrast/agent/static_analysis.rb +5 -3
  47. data/lib/contrast/agent/telemetry.rb +137 -0
  48. data/lib/contrast/agent/telemetry_event.rb +33 -0
  49. data/lib/contrast/agent/thread_watcher.rb +66 -11
  50. data/lib/contrast/agent/version.rb +1 -1
  51. data/lib/contrast/agent.rb +21 -0
  52. data/lib/contrast/api/communication/connection_status.rb +10 -7
  53. data/lib/contrast/api/communication/messaging_queue.rb +37 -3
  54. data/lib/contrast/api/communication/response_processor.rb +15 -8
  55. data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
  56. data/lib/contrast/api/communication/socket.rb +6 -8
  57. data/lib/contrast/api/communication/socket_client.rb +29 -12
  58. data/lib/contrast/api/communication/speedracer.rb +37 -1
  59. data/lib/contrast/api/communication/tcp_socket.rb +4 -3
  60. data/lib/contrast/api/communication/unix_socket.rb +1 -0
  61. data/lib/contrast/api/decorators/finding.rb +45 -0
  62. data/lib/contrast/components/api.rb +90 -0
  63. data/lib/contrast/components/app_context.rb +10 -41
  64. data/lib/contrast/components/app_context_extend.rb +78 -0
  65. data/lib/contrast/components/base.rb +23 -0
  66. data/lib/contrast/components/config.rb +92 -13
  67. data/lib/contrast/components/contrast_service.rb +11 -0
  68. data/lib/contrast/components/sampling.rb +2 -2
  69. data/lib/contrast/config/agent_configuration.rb +1 -1
  70. data/lib/contrast/config/api_configuration.rb +27 -0
  71. data/lib/contrast/config/api_proxy_configuration.rb +14 -0
  72. data/lib/contrast/config/application_configuration.rb +2 -3
  73. data/lib/contrast/config/assess_configuration.rb +3 -3
  74. data/lib/contrast/config/base_configuration.rb +17 -28
  75. data/lib/contrast/config/certification_configuration.rb +15 -0
  76. data/lib/contrast/config/env_variables.rb +18 -0
  77. data/lib/contrast/config/heap_dump_configuration.rb +6 -6
  78. data/lib/contrast/config/inventory_configuration.rb +1 -5
  79. data/lib/contrast/config/protect_rule_configuration.rb +1 -1
  80. data/lib/contrast/config/request_audit_configuration.rb +18 -0
  81. data/lib/contrast/config/root_configuration.rb +1 -0
  82. data/lib/contrast/config/ruby_configuration.rb +6 -6
  83. data/lib/contrast/config/service_configuration.rb +2 -2
  84. data/lib/contrast/config.rb +1 -1
  85. data/lib/contrast/configuration.rb +4 -2
  86. data/lib/contrast/extension/assess/array.rb +5 -7
  87. data/lib/contrast/extension/thread.rb +31 -12
  88. data/lib/contrast/framework/manager.rb +22 -44
  89. data/lib/contrast/framework/manager_extend.rb +50 -0
  90. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
  91. data/lib/contrast/framework/rails/patch/support.rb +31 -29
  92. data/lib/contrast/framework/rails/railtie.rb +1 -1
  93. data/lib/contrast/framework/sinatra/support.rb +2 -1
  94. data/lib/contrast/logger/application.rb +4 -0
  95. data/lib/contrast/logger/log.rb +8 -103
  96. data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
  97. data/lib/contrast/utils/assess/property/tagged_utils.rb +165 -0
  98. data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
  99. data/lib/contrast/utils/assess/tracking_util.rb +20 -15
  100. data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
  101. data/lib/contrast/utils/class_util.rb +18 -14
  102. data/lib/contrast/utils/exclude_key.rb +20 -0
  103. data/lib/contrast/utils/findings.rb +62 -0
  104. data/lib/contrast/utils/hash_digest.rb +10 -73
  105. data/lib/contrast/utils/hash_digest_extend.rb +86 -0
  106. data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
  107. data/lib/contrast/utils/heap_dump_util.rb +2 -65
  108. data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
  109. data/lib/contrast/utils/io_util.rb +1 -1
  110. data/lib/contrast/utils/log_utils.rb +108 -0
  111. data/lib/contrast/utils/metrics_hash.rb +59 -0
  112. data/lib/contrast/utils/middleware_utils.rb +87 -0
  113. data/lib/contrast/utils/net_http_base.rb +158 -0
  114. data/lib/contrast/utils/object_share.rb +1 -0
  115. data/lib/contrast/utils/os.rb +23 -0
  116. data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
  117. data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
  118. data/lib/contrast/utils/request_utils.rb +88 -0
  119. data/lib/contrast/utils/response_utils.rb +97 -0
  120. data/lib/contrast/utils/substitution_utils.rb +167 -0
  121. data/lib/contrast/utils/tag_util.rb +9 -9
  122. data/lib/contrast/utils/telemetry.rb +79 -0
  123. data/lib/contrast/utils/telemetry_client.rb +90 -0
  124. data/lib/contrast/utils/telemetry_identifier.rb +130 -0
  125. data/lib/contrast.rb +18 -0
  126. data/ruby-agent.gemspec +7 -6
  127. data/service_executables/VERSION +1 -1
  128. data/service_executables/linux/contrast-service +0 -0
  129. data/service_executables/mac/contrast-service +0 -0
  130. metadata +69 -22
  131. data/lib/contrast/config/default_value.rb +0 -17
@@ -106,7 +106,8 @@ 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) { [] }.
110
+ map(&:first)
110
111
  route_pattern = route_patterns&.find do |matcher|
111
112
  matcher.params(route) # ::Mustermann::Sinatra match.
112
113
  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) }
@@ -11,6 +11,7 @@ require 'contrast/logger/format'
11
11
  require 'contrast/logger/request'
12
12
  require 'contrast/logger/time'
13
13
  require 'contrast/components/config'
14
+ require 'contrast/utils/log_utils'
14
15
 
15
16
  module Contrast
16
17
  # This module allows us to dynamically weave timing into our code, so that only when the time is actually needed do
@@ -26,9 +27,9 @@ module Contrast
26
27
  # Add a method to the list of methods to be trace timed if logger set to TRACE. Enables trace timing after if
27
28
  # logger set to TRACE.
28
29
  #
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.
30
+ # @param: clazz [Class] the class of the method to time.
31
+ # @param: method [Symbol] the method to time.
32
+ # @param: method [String] optional custom logging message.
32
33
  def add_method_to_trace_timing clazz, method, msg = nil
33
34
  methods_to_time.append(METHOD_INFO.new(clazz, method, msg, false))
34
35
  enable_trace_timing if logger.level == ::Ougai::Logging::TRACE
@@ -37,9 +38,9 @@ module Contrast
37
38
  # Add a method to the list of methods to be trace timed if logger set to TRACE. Enables trace timing after if
38
39
  # logger set to TRACE.
39
40
  #
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
41
+ # @param: meth_spec [METHOD_INFO] specs about the method to be timed.
42
+ # @param: class_method [Boolean] whether this is or isn't a class/module method.
43
+ def trace_time_class_method meth_spec, class_method # rubocop:disable Metrics/AbcSize
43
44
  untimed_func_symbol = "untimed_#{ meth_spec.method_name }".to_sym
44
45
  send_to = class_method ? meth_spec.clazz.cs__singleton_class : meth_spec.clazz
45
46
  meth_spec.clazz.class_eval do
@@ -105,12 +106,7 @@ module Contrast
105
106
  class Log
106
107
  include Singleton
107
108
  include ::Contrast::TraceTiming
108
-
109
- DEFAULT_NAME = 'contrast.log'
110
- DEFAULT_LEVEL = ::Ougai::Logging::Severity::INFO
111
- VALID_LEVELS = ::Ougai::Logging::Severity::SEV_LABEL
112
- STDOUT_STR = 'STDOUT'
113
- STDERR_STR = 'STDERR'
109
+ include Contrast::Utils::LogUtils
114
110
 
115
111
  attr_reader :previous_path, :previous_level
116
112
 
@@ -167,97 +163,6 @@ module Contrast
167
163
  dir_name = File.dirname(File.absolute_path(path))
168
164
  File.writable?(dir_name)
169
165
  end
170
-
171
- private
172
-
173
- def build path: STDOUT_STR, level_const: DEFAULT_LEVEL
174
- logger = case path
175
- when STDOUT_STR, STDERR_STR
176
- ::Ougai::Logger.new(Object.cs__const_get(path))
177
- else
178
- ::Ougai::Logger.new(path)
179
- end
180
- add_contrast_loggers(logger)
181
- logger.progname = 'Contrast Agent'
182
- logger.level = level_const
183
- logger.formatter = Contrast::Logger::Format.new
184
- logger.formatter.datetime_format = '%Y-%m-%dT%H:%M:%S.%L%z'
185
- logger
186
- end
187
-
188
- def add_contrast_loggers logger
189
- logger.extend(Contrast::Logger::Application)
190
- logger.extend(Contrast::Logger::Request)
191
- logger.extend(Contrast::Logger::Time)
192
- end
193
-
194
- # Determine the valid path to which to log, given the precedence of config > settings > default.
195
- #
196
- # @param log_file [String, nil] the file to which to log as provided by the settings retrieved from the
197
- # TeamServer.
198
- # @return [String] the path to which to log or STDOUT / STDERR if one of those values provided.
199
- def find_valid_path log_file
200
- config = ::Contrast::CONFIG.root.agent.logger
201
- config_path = config&.path&.length.to_i.positive? ? config.path : nil
202
- valid_path(config_path || log_file)
203
- end
204
-
205
- def valid_path path
206
- path = path.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : path
207
- return path if path == STDOUT_STR
208
- return path if path == STDERR_STR
209
-
210
- path = DEFAULT_NAME if path.empty?
211
- if write_permission?(path)
212
- path
213
- elsif write_permission?(DEFAULT_NAME)
214
- # Log once when the path is invalid. We'll change to this path, so no
215
- # need to log again.
216
- if previous_path != DEFAULT_NAME
217
- puts "[!] Unable to write to '#{ path }'. Writing to default log '#{ DEFAULT_NAME }' instead."
218
- end
219
- DEFAULT_NAME
220
- else
221
- # Log once when the path is invalid. We'll change to this path, so no
222
- # need to log again.
223
- puts "[!] Unable to write to '#{ path }'. Writing to standard out instead." if previous_path != STDOUT_STR
224
- STDOUT_STR
225
- end
226
- end
227
-
228
- # Determine the valid level to which to log, given the precedence of config > settings > default.
229
- #
230
- # @param log_level [String, nil] the level at which to log as provided by the settings retrieved from the
231
- # TeamServer.
232
- # @return [::Ougai::Logging::Severity] the level at which to log
233
- def find_valid_level log_level
234
- config = ::Contrast::CONFIG.root.agent.logger
235
- config_level = config&.level&.length&.positive? ? config.level : nil
236
-
237
- valid_level(config_level || log_level)
238
- end
239
-
240
- def valid_level level
241
- level ||= DEFAULT_LEVEL
242
- level = level.upcase
243
- if VALID_LEVELS.include?(level)
244
- Object.cs__const_get("::Ougai::Logging::Severity::#{ level }")
245
- else
246
- DEFAULT_LEVEL
247
- end
248
- rescue StandardError
249
- DEFAULT_LEVEL
250
- end
251
-
252
- # Log that the Agent log has changed and include some default information at the start of the log.
253
- def log_update
254
- logger.debug('Initialized new contrast agent logger')
255
- logger.debug_with_time('middleware: log environment') do
256
- logger.application_environment
257
- logger.application_configuration
258
- logger.application_libraries
259
- end
260
- end
261
166
  end
262
167
  end
263
168
  end
@@ -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,165 @@
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
+ # Is the given tag present?
13
+ # Used in testing, so found by `be_tagged`, if you're grepping for it
14
+ #
15
+ # @param label [Symbol] the tag to check for
16
+ # @return [Boolean]
17
+ def tagged? label
18
+ tracked? && tags.key?(label)
19
+ end
20
+
21
+ # Similar to #tracked?, but limited to a given range.
22
+ #
23
+ # @param start [Integer] the inclusive start index to check.
24
+ # @param finish [Integer] the exclusive end index to check.
25
+ # @return [Boolean]
26
+ def any_tags_between? start, finish
27
+ return false unless tracked?
28
+
29
+ tags.each_value do |tag_array|
30
+ return true if tag_array.any? { |tag| tag.overlaps?(start, finish) }
31
+ end
32
+ false
33
+ end
34
+
35
+ # Given a tag name and range object, add a new tag to this
36
+ # collection. If the given range touches an existing tag,
37
+ # we'll combine the two, adjusting the existing one and
38
+ # dropping this new one.
39
+ #
40
+ # @param label [String] the name of the tag
41
+ # @param range [Range] the Range that the tag covers, inclusive to
42
+ # exclusive
43
+ def add_tag label, range
44
+ length = range.end - range.begin
45
+ tag = Contrast::Agent::Assess::Tag.new(label, length, range.begin)
46
+ existing = fetch_tag(label)
47
+ tags[label] = Contrast::Utils::TagUtil.ordered_merge(existing, tag)
48
+ end
49
+
50
+ def set_tags label, tag_ranges
51
+ tags[label] = tag_ranges
52
+ end
53
+
54
+ # Returns a list of all current tags.
55
+ #
56
+ # @return [Hash<Contrast::Agent::Assess::Tag>]
57
+ def get_tags # rubocop:disable Naming/AccessorMethodName
58
+ return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
59
+
60
+ tags
61
+ end
62
+
63
+ # We'll use this as a helper method to retrieve tags from the hash.
64
+ # Because the hash auto-populates an empty array when we try to
65
+ # access a tag in it, we cannot use the [] method without side
66
+ # effect. To get around this, we'll use a fetch work around.
67
+ #
68
+ # @param label [Symbol] the label to look up
69
+ # @return [Array<Contrast::Agent::Assess::Tag>] all the tags with
70
+ # that label
71
+ def fetch_tag label
72
+ get_tags.fetch(label, nil) if tracked?
73
+ end
74
+
75
+ # Remove all tags with a given label
76
+ def delete_tags label
77
+ tags.delete(label) if tracked?
78
+ end
79
+
80
+ # Reset the tag hash
81
+ def clear_tags
82
+ tags.clear if tracked?
83
+ end
84
+
85
+ # Returns a list of all current tag labels, most likely to be used for
86
+ # a splat operation
87
+ def tag_keys
88
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
89
+
90
+ tags.keys
91
+ end
92
+
93
+ # Calls merge to combine touching or overlapping tags
94
+ # Deletes empty tags
95
+ def cleanup_tags
96
+ return unless tracked?
97
+
98
+ Contrast::Utils::TagUtil.merge_tags(tags)
99
+ tags.delete_if { |_, value| value.empty? }
100
+ end
101
+
102
+ # Find all of the ranges that span a given index. This is used
103
+ # in propagation when we need to shift tags about. For instance, in
104
+ # the append method when we need to see if any tag at the end needs
105
+ # to be expanded out to the size of the new String.
106
+ #
107
+ # Note: Tags do not know their key, so this is only the range covered
108
+ #
109
+ # @param idx [Integer] the index to check for tags
110
+ # @return [Array<Contrast::Agent::Assess::Tag>] a set of all the Tags
111
+ # covering the given index. This is only the ranges, not the names.
112
+ def tags_at idx
113
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
114
+
115
+ at = []
116
+ tags.each_value do |tag_array|
117
+ tag_array.each do |tag|
118
+ if tag.covers?(idx)
119
+ at << tag
120
+ elsif tag.above?(idx)
121
+ break
122
+ end
123
+ end
124
+ end
125
+ at
126
+ end
127
+
128
+ # given a range, select all tags in that range the selected tags are
129
+ # shifted such that the start index of the new tag (0) aligns with
130
+ # the given start index in the range
131
+ #
132
+ # current tags: 5-15
133
+ # range : 5-10
134
+ # result : 0-05
135
+ #
136
+ # Note that we disable Lint/DuplicateBranch in this branch in order
137
+ # list out all tag range cases in the proper order to make this
138
+ # easier to understand
139
+ #
140
+ # @param range [Range] the span to check, inclusive to exclusive
141
+ # @return [Hash{String => Contrast::Agent::Assess::Tag}] the hash of
142
+ # key to tags
143
+ def tags_at_range range
144
+ return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
145
+
146
+ at = Hash.new { |h, k| h[k] = [] }
147
+ tags.each_pair do |key, value|
148
+ add = nil
149
+ value.each do |tag|
150
+ within_range = resize_to_range(tag, range)
151
+ if within_range
152
+ add ||= []
153
+ add << within_range
154
+ end
155
+ end
156
+ next unless add&.any?
157
+
158
+ at[key] = add
159
+ end
160
+ at
161
+ end
162
+ end
163
+ end
164
+ end
165
+ 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
@@ -50,14 +50,9 @@ module Contrast
50
50
 
51
51
  idx += 1
52
52
  if Contrast::Utils::DuckUtils.iterable_hash?(obj)
53
- obj.each_pair do |k, v|
54
- return true if _tracked?(k, idx) || _tracked?(v, idx)
55
- end
56
- false
53
+ handle_hash obj, idx
57
54
  elsif Contrast::Utils::DuckUtils.iterable_enumerable?(obj)
58
- obj.any? do |ele|
59
- _tracked?(ele, idx) unless obj == ele
60
- end
55
+ handle_enumerable obj, idx
61
56
  else
62
57
  Contrast::Agent::Assess::Tracker.tracked?(obj)
63
58
  end
@@ -84,15 +79,9 @@ module Contrast
84
79
 
85
80
  idx += 1
86
81
  if Contrast::Utils::DuckUtils.iterable_hash?(obj)
87
- obj.each_pair do |k, v|
88
- return true if _trackable?(k, idx)
89
- return true if _trackable?(v, idx)
90
- end
91
- false
82
+ handle_hash obj, idx
92
83
  elsif Contrast::Utils::DuckUtils.iterable_enumerable?(obj)
93
- obj.any? do |ele|
94
- _trackable?(ele, idx) unless obj == ele
95
- end
84
+ handle_enumerable obj, idx
96
85
  else
97
86
  Contrast::Agent::Assess::Tracker.trackable?(obj)
98
87
  end
@@ -103,6 +92,22 @@ module Contrast
103
92
  logger.warn('Failed to determine trackable', e, module: obj.cs__class)
104
93
  false
105
94
  end
95
+
96
+ def handle_hash obj, idx
97
+ caller_method = caller(1..1).first[/`.*'/][1..-2].to_sym
98
+ obj.each_pair do |k, v|
99
+ return true if send(caller_method, k, idx)
100
+ return true if send(caller_method, v, idx)
101
+ end
102
+ false
103
+ end
104
+
105
+ def handle_enumerable obj, idx
106
+ caller_method = caller(1..1).first[/`.*'/][1..-2].to_sym
107
+ obj.any? do |ele|
108
+ send(caller_method, ele, idx) unless obj == ele
109
+ end
110
+ end
106
111
  end
107
112
  end
108
113
  end