contrast-agent 4.13.1 → 4.14.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +1 -0
  3. data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
  4. data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
  5. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -1
  6. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
  7. data/lib/contrast/agent/assess/policy/trigger_method.rb +44 -7
  8. data/lib/contrast/agent/assess/policy/trigger_node.rb +14 -6
  9. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
  10. data/lib/contrast/agent/assess/property/tagged.rb +51 -57
  11. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
  12. data/lib/contrast/agent/metric_telemetry_event.rb +2 -2
  13. data/lib/contrast/agent/middleware.rb +5 -75
  14. data/lib/contrast/agent/patching/policy/method_policy.rb +3 -89
  15. data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
  16. data/lib/contrast/agent/patching/policy/patcher.rb +12 -8
  17. data/lib/contrast/agent/reporting/report.rb +21 -0
  18. data/lib/contrast/agent/reporting/reporter.rb +142 -0
  19. data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
  20. data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
  21. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
  22. data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
  23. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
  24. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
  25. data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
  26. data/lib/contrast/agent/request.rb +2 -81
  27. data/lib/contrast/agent/request_context.rb +4 -128
  28. data/lib/contrast/agent/request_context_extend.rb +138 -0
  29. data/lib/contrast/agent/response.rb +2 -73
  30. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +39 -16
  31. data/lib/contrast/agent/static_analysis.rb +1 -1
  32. data/lib/contrast/agent/telemetry.rb +15 -7
  33. data/lib/contrast/agent/telemetry_event.rb +8 -9
  34. data/lib/contrast/agent/thread_watcher.rb +31 -5
  35. data/lib/contrast/agent/version.rb +1 -1
  36. data/lib/contrast/agent.rb +15 -0
  37. data/lib/contrast/api/communication/connection_status.rb +10 -7
  38. data/lib/contrast/api/communication/messaging_queue.rb +37 -3
  39. data/lib/contrast/api/communication/response_processor.rb +15 -8
  40. data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
  41. data/lib/contrast/api/communication/socket.rb +6 -8
  42. data/lib/contrast/api/communication/socket_client.rb +29 -12
  43. data/lib/contrast/api/communication/speedracer.rb +37 -1
  44. data/lib/contrast/api/communication/tcp_socket.rb +4 -3
  45. data/lib/contrast/api/communication/unix_socket.rb +1 -0
  46. data/lib/contrast/api/decorators/finding.rb +45 -0
  47. data/lib/contrast/components/api.rb +56 -0
  48. data/lib/contrast/components/app_context.rb +10 -65
  49. data/lib/contrast/components/app_context_extend.rb +78 -0
  50. data/lib/contrast/components/base.rb +23 -0
  51. data/lib/contrast/components/config.rb +8 -8
  52. data/lib/contrast/components/contrast_service.rb +5 -0
  53. data/lib/contrast/components/sampling.rb +2 -2
  54. data/lib/contrast/config/agent_configuration.rb +1 -1
  55. data/lib/contrast/config/api_configuration.rb +9 -4
  56. data/lib/contrast/config/api_proxy_configuration.rb +14 -0
  57. data/lib/contrast/config/application_configuration.rb +2 -3
  58. data/lib/contrast/config/assess_configuration.rb +3 -3
  59. data/lib/contrast/config/base_configuration.rb +17 -28
  60. data/lib/contrast/config/certification_configuration.rb +15 -0
  61. data/lib/contrast/config/env_variables.rb +2 -9
  62. data/lib/contrast/config/heap_dump_configuration.rb +6 -6
  63. data/lib/contrast/config/inventory_configuration.rb +1 -5
  64. data/lib/contrast/config/protect_rule_configuration.rb +1 -1
  65. data/lib/contrast/config/request_audit_configuration.rb +18 -0
  66. data/lib/contrast/config/ruby_configuration.rb +6 -6
  67. data/lib/contrast/config/service_configuration.rb +1 -2
  68. data/lib/contrast/config.rb +0 -1
  69. data/lib/contrast/configuration.rb +1 -2
  70. data/lib/contrast/extension/assess/array.rb +5 -7
  71. data/lib/contrast/framework/manager.rb +8 -32
  72. data/lib/contrast/framework/manager_extend.rb +50 -0
  73. data/lib/contrast/framework/rails/railtie.rb +1 -1
  74. data/lib/contrast/framework/sinatra/support.rb +2 -1
  75. data/lib/contrast/logger/log.rb +8 -103
  76. data/lib/contrast/utils/assess/property/tagged_utils.rb +23 -0
  77. data/lib/contrast/utils/assess/tracking_util.rb +20 -15
  78. data/lib/contrast/utils/assess/trigger_method_utils.rb +1 -1
  79. data/lib/contrast/utils/class_util.rb +18 -14
  80. data/lib/contrast/utils/findings.rb +62 -0
  81. data/lib/contrast/utils/hash_digest.rb +10 -73
  82. data/lib/contrast/utils/hash_digest_extend.rb +86 -0
  83. data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
  84. data/lib/contrast/utils/heap_dump_util.rb +2 -65
  85. data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
  86. data/lib/contrast/utils/io_util.rb +1 -1
  87. data/lib/contrast/utils/log_utils.rb +108 -0
  88. data/lib/contrast/utils/middleware_utils.rb +87 -0
  89. data/lib/contrast/utils/net_http_base.rb +158 -0
  90. data/lib/contrast/utils/object_share.rb +1 -0
  91. data/lib/contrast/utils/request_utils.rb +88 -0
  92. data/lib/contrast/utils/response_utils.rb +97 -0
  93. data/lib/contrast/utils/substitution_utils.rb +167 -0
  94. data/lib/contrast/utils/tag_util.rb +9 -9
  95. data/lib/contrast/utils/telemetry.rb +4 -2
  96. data/lib/contrast/utils/telemetry_client.rb +90 -0
  97. data/lib/contrast/utils/telemetry_identifier.rb +17 -24
  98. data/ruby-agent.gemspec +5 -5
  99. metadata +48 -23
  100. data/lib/contrast/config/default_value.rb +0 -17
  101. data/lib/contrast/utils/requests_client.rb +0 -150
@@ -87,7 +87,7 @@ module Contrast
87
87
  def yaml_to_hash path
88
88
  if path && File.readable?(path)
89
89
  begin
90
- yaml = IO.read(path)
90
+ yaml = File.read(path)
91
91
  yaml = ERB.new(yaml).result if defined?(ERB)
92
92
  return YAML.safe_load(yaml)
93
93
  rescue Psych::Exception => e
@@ -205,7 +205,6 @@ module Contrast
205
205
  # in the thing to convert and setting them in the given hash. For now, this
206
206
  # logs every possible key, whether set or not. If we want to change that
207
207
  # behavior, we can skip adding keys to the hash if the value is nil, blank,
208
- # or Contrast::Config::DefaultValue depending on desired behavior
209
208
  #
210
209
  # @param hash [Hash] the hash to populate
211
210
  # @param convert [Contrast::Config::BaseConfiguration, Object] the level of
@@ -42,13 +42,11 @@ module Contrast
42
42
  shift = 0
43
43
  separator_length = separator.nil? ? 0 : separator.to_s.length
44
44
  parent_events = []
45
- ary.each do |obj|
46
- if obj # skip nil here
47
- properties.copy_from(obj, ret, shift)
48
- shift += obj.to_s.length
49
- parent_event = Contrast::Agent::Assess::Tracker.properties(obj)&.event
50
- parent_events << parent_event if parent_event
51
- end
45
+ ary.compact.each do |obj|
46
+ properties.copy_from(obj, ret, shift)
47
+ shift += obj.to_s.length
48
+ parent_event = Contrast::Agent::Assess::Tracker.properties(obj)&.event
49
+ parent_events << parent_event if parent_event
52
50
  shift += separator_length
53
51
  end
54
52
  return ret unless Contrast::Agent::Assess::Tracker.tracked?(ret)
@@ -9,12 +9,14 @@ require 'contrast/framework/rails/support'
9
9
  require 'contrast/framework/grape/support'
10
10
  require 'contrast/framework/sinatra/support'
11
11
  require 'contrast/utils/class_util'
12
+ require 'contrast/framework/manager_extend'
12
13
 
13
14
  module Contrast
14
15
  module Framework
15
16
  # Allows access to framework specific information
16
17
  class Manager
17
18
  include Contrast::Components::Logger::InstanceMethods
19
+ include Contrast::Framework::ManagerExtend
18
20
 
19
21
  # Order here does matter as the first framework listed will be the first one we pull information from Rack will
20
22
  # be a special case that may involve updating some logic to handle only applying Rack if Rails/Sinatra do not
@@ -65,6 +67,10 @@ module Contrast
65
67
  Contrast::Framework::PlatformVersion.from_string(framework_version)
66
68
  end
67
69
 
70
+ def platform_version_string
71
+ first_framework_result :version, ''
72
+ end
73
+
68
74
  def server_type
69
75
  first_framework_result :server_type, 'rack'
70
76
  end
@@ -116,7 +122,8 @@ module Contrast
116
122
  # @param request [Contrast::Agent::Request] the current request.
117
123
  # @return [Contrast::Api::Dtm::RouteCoverage] the current route as a Dtm.
118
124
  def get_route_dtm request
119
- @_frameworks.lazy.map { |framework_support| framework_support.current_route(request) }.reject(&:nil?).first
125
+ @_frameworks.lazy.map { |framework_support| framework_support.current_route(request) }.
126
+ reject(&:nil?).first
120
127
  end
121
128
 
122
129
  # Sometimes the framework we want to instrument is loaded after our agent code. To catch that case, we'll detect
@@ -149,37 +156,6 @@ module Contrast
149
156
  rescue StandardError => e
150
157
  logger.warn('Unable to register a late framework', e, module: mod.cs__name)
151
158
  end
152
-
153
- private
154
-
155
- def enable_framework_support? klass
156
- Contrast::Utils::ClassUtil.truly_defined?(klass)
157
- end
158
-
159
- def routes_for_all_frameworks
160
- data_for_all_frameworks :collect_routes
161
- end
162
-
163
- # This returns an array of all data from each framework in a flat, no-nil values array
164
- #
165
- # @param method_name [Symbol] the method to call on each FrameworkSupport class
166
- # @return [Array]
167
- def data_for_all_frameworks method_name
168
- @_frameworks.flat_map { |framework| framework.send(method_name) }.compact
169
- end
170
-
171
- # This returns a single object from the first framework to successfully respond
172
- #
173
- # @param method_name [Symbol] the method to call on each FrameworkSupport class
174
- # @return [Object] - Determined by method to be invoked
175
- def first_framework_result method_name, default_value
176
- result = nil
177
- @_frameworks.each do |framework|
178
- result = framework.send(method_name)
179
- break if result
180
- end
181
- result || default_value
182
- end
183
159
  end
184
160
  end
185
161
  end
@@ -0,0 +1,50 @@
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
+ require 'contrast/extension/module'
6
+ require 'contrast/framework/platform_version'
7
+ require 'contrast/framework/rack/support'
8
+ require 'contrast/framework/rails/support'
9
+ require 'contrast/framework/grape/support'
10
+ require 'contrast/framework/sinatra/support'
11
+ require 'contrast/utils/class_util'
12
+
13
+ module Contrast
14
+ module Framework
15
+ # Allows access to framework specific information
16
+ module ManagerExtend
17
+ private
18
+
19
+ def enable_framework_support? klass
20
+ Contrast::Utils::ClassUtil.truly_defined?(klass)
21
+ end
22
+
23
+ def routes_for_all_frameworks
24
+ data_for_all_frameworks :collect_routes
25
+ end
26
+
27
+ # This returns an array of all data from each framework in a flat, no-nil values array
28
+ #
29
+ # @param method_name [Symbol] the method to call on each FrameworkSupport class
30
+ # @return [Array]
31
+ def data_for_all_frameworks method_name
32
+ @_frameworks.flat_map { |framework| framework.send(method_name) }.
33
+ compact
34
+ end
35
+
36
+ # This returns a single object from the first framework to successfully respond
37
+ #
38
+ # @param method_name [Symbol] the method to call on each FrameworkSupport class
39
+ # @return [Object] - Determined by method to be invoked
40
+ def first_framework_result method_name, default_value
41
+ result = nil
42
+ @_frameworks.each do |framework|
43
+ result = framework.send(method_name)
44
+ break if result
45
+ end
46
+ result || default_value
47
+ end
48
+ end
49
+ end
50
+ end
@@ -14,7 +14,7 @@ module Contrast
14
14
  initializer 'Contrast Ruby Agent Initializer' do |app|
15
15
  log_rails = defined?(Rails) && defined?(Rails.logger)
16
16
 
17
- Rails.logger.debug("In railtie ::#{ app.middleware.inspect }") if log_rails
17
+ Rails.logger.debug { "In railtie ::#{ app.middleware.inspect }" } if log_rails
18
18
 
19
19
  if ::Contrast::APP_CONTEXT.instrument_middleware_stack?
20
20
  ::Contrast::AGENT.insert_middleware(app)
@@ -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
@@ -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
@@ -9,6 +9,29 @@ module Contrast
9
9
  # This module includes simple methods for the tags like
10
10
  # adding tags, getting tags, deleting tags and similar
11
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
+
12
35
  # Given a tag name and range object, add a new tag to this
13
36
  # collection. If the given range touches an existing tag,
14
37
  # we'll combine the two, adjusting the existing one and
@@ -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
@@ -103,7 +103,7 @@ module Contrast
103
103
  # @param object [Object] the Object on which the method was invoked
104
104
  # @param ret [Object] the Return of the invoked method
105
105
  # @param args [Array<Object>] the Arguments with which the method was invoked
106
- def apply_dataflow_rule trigger_node, source, object, ret, *args
106
+ def apply_dataflow_rule trigger_node, source, object, ret, *args # rubocop:disable Metrics/PerceivedComplexity
107
107
  return unless source
108
108
 
109
109
  if Contrast::Agent::Assess::Tracker.trackable?(source)
@@ -54,7 +54,7 @@ module Contrast
54
54
  # Once we move to 2.7+, we can combine the caches using ID b/c the memory location stops being the id
55
55
  #
56
56
  # @param object [Object, nil] the entity to convert to a String
57
- # @return [String] the human readable form of the String, as defined by
57
+ # @return [String, Object] the human readable form of the String, as defined by
58
58
  # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/capture-snapshot.md
59
59
  def to_contrast_string object
60
60
  # Only treat object like a string if it actually is a string+ some subclasses of String override string
@@ -66,19 +66,23 @@ module Contrast
66
66
  else
67
67
  return @lru_cache[object.__id__] if @lru_cache.key? object.__id__
68
68
 
69
- @lru_cache[object.__id__] = if object.nil?
70
- Contrast::Utils::ObjectShare::NIL_STRING
71
- elsif object.cs__is_a?(Symbol)
72
- ":#{ object }"
73
- elsif object.cs__is_a?(Module) || object.cs__is_a?(Class)
74
- "#{ object.cs__name }@#{ object.__id__ }"
75
- elsif object.cs__is_a?(Regexp)
76
- object.source
77
- elsif use_to_s?(object)
78
- object.to_s
79
- else
80
- "#{ object.cs__class.cs__name }@#{ object.__id__ }"
81
- end
69
+ @lru_cache[object.__id__] = convert_object object
70
+ end
71
+ end
72
+
73
+ def convert_object object
74
+ if object.nil?
75
+ Contrast::Utils::ObjectShare::NIL_STRING
76
+ elsif object.cs__is_a?(Symbol)
77
+ ":#{ object }"
78
+ elsif object.cs__is_a?(Module) || object.cs__is_a?(Class)
79
+ "#{ object.cs__name }@#{ object.__id__ }"
80
+ elsif object.cs__is_a?(Regexp)
81
+ object.source
82
+ elsif use_to_s?(object)
83
+ object.to_s
84
+ else
85
+ "#{ object.cs__class.cs__name }@#{ object.__id__ }"
82
86
  end
83
87
  end
84
88
 
@@ -0,0 +1,62 @@
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 Utils
8
+ # Utility for saving raw findings for later
9
+ class Findings
10
+ include Contrast::Components::Logger::InstanceMethods
11
+
12
+ def initialize
13
+ @_collection = []
14
+ end
15
+
16
+ def collection
17
+ @_collection ||= []
18
+ end
19
+
20
+ def push trigger_node, source, object, ret, *args
21
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless trigger_node.collectable?
22
+
23
+ @_collection << { trigger_node: trigger_node, source: source, object: object, ret: ret, args: args }
24
+ end
25
+
26
+ # Some rules requires response to be available before validating them correctly,
27
+ # so we check if trigger_node.rule_id is collectable and then save them for
28
+ # later report, when we have the response.
29
+ #
30
+ # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
31
+ # trigger event
32
+ # @param source [Object] the source of the Trigger Event
33
+ # @param object [Object] the Object on which the method was invoked
34
+ # @param ret [Object] the Return of the invoked method
35
+ # @param args [Array<Object>] the Arguments with which the method was invoked
36
+ def collect_finding trigger_node, source, object, ret, *args
37
+ push trigger_node, source, object, ret, args
38
+ logger.trace('Finding collected', node_id: trigger_node.id,
39
+ source_id: source.__id__,
40
+ rule: trigger_node.rule_id)
41
+ end
42
+
43
+ # Build and report all collected findings for the collectable rules.
44
+ #
45
+ # We make sure the content-type is present before reporting, because some
46
+ # findings do require it for validation.
47
+ def report_collected_findings
48
+ return if Contrast::Agent::REQUEST_TRACKER.current&.response&.content_type.nil?
49
+ return if @_collection.empty?
50
+
51
+ while @_collection.any?
52
+ finding = @_collection.pop
53
+ Contrast::Agent::Assess::Policy::TriggerMethod.build_finding finding[:trigger_node],
54
+ finding[:source],
55
+ finding[:object],
56
+ finding[:ret],
57
+ finding[:args]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'digest'
5
+ require 'contrast/utils/hash_digest_extend'
5
6
 
6
7
  module Contrast
7
8
  module Utils
@@ -13,6 +14,7 @@ module Contrast
13
14
  # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/preflight.md
14
15
  class HashDigest < Digest::Class
15
16
  include Digest::Instance
17
+ extend Contrast::Utils::HashDigestExtend
16
18
 
17
19
  CONTENT_LENGTH_HEADER = 'Content-Length'
18
20
  CRYPTO_RULES = %w[crypto-bad-ciphers crypto-bad-mac].cs__freeze
@@ -21,76 +23,6 @@ module Contrast
21
23
  CLASS_SOURCE_KEY = 'source'
22
24
  CLASS_CONSTANT_NAME_KEY = 'name'
23
25
  CLASS_LINE_NO_KEY = 'lineNo'
24
- class << self
25
- def generate_request_hash request
26
- hash = new
27
- hash.update(request.request_method)
28
- hash.update(request.normalized_uri)
29
- request.parameters.each_key do |name|
30
- hash.update(name)
31
- end
32
- cl = request.headers[CONTENT_LENGTH_HEADER]
33
- hash.update_on_content_length(cl) if cl
34
- hash.finish
35
- end
36
-
37
- def generate_event_hash finding, source, request
38
- return generate_dataflow_hash(finding, request) if finding.events.length.to_i > 1
39
-
40
- id = finding.rule_id
41
- return generate_crypto_hash(finding, source, request) if CRYPTO_RULES.include?(id)
42
-
43
- generate_trigger_hash(finding, request)
44
- end
45
-
46
- def generate_config_hash finding
47
- hash = new
48
- hash.update(finding.rule_id)
49
- path = finding.properties[CONFIG_PATH_KEY]
50
- hash.update(path)
51
- method = finding.properties[CONFIG_SESSION_ID_KEY]
52
- hash.update(method)
53
- hash.finish
54
- end
55
-
56
- def generate_class_scanning_hash finding
57
- hash = new
58
- hash.update(finding.rule_id)
59
- module_name = finding.properties[CLASS_SOURCE_KEY]
60
- hash.update(module_name)
61
- # We're not currently collecting this. 30/7/19 HM
62
- line_no = finding.properties[CLASS_LINE_NO_KEY]
63
- hash.update(line_no)
64
- field = finding.properties[CLASS_CONSTANT_NAME_KEY]
65
- hash.update(field)
66
- hash.finish
67
- end
68
-
69
- private
70
-
71
- def generate_crypto_hash finding, algorithm, request
72
- hash = new
73
- hash.update(finding.rule_id)
74
- hash.update(algorithm)
75
- hash.update_on_request(finding, request)
76
- hash.finish
77
- end
78
-
79
- def generate_dataflow_hash finding, request
80
- hash = new
81
- hash.update(finding.rule_id)
82
- hash.update_on_sources(finding.events)
83
- hash.update_on_request(finding, request)
84
- hash.finish
85
- end
86
-
87
- def generate_trigger_hash finding, request
88
- hash = new
89
- hash.update(finding.rule_id)
90
- hash.update_on_request(finding, request)
91
- hash.finish
92
- end
93
- end
94
26
 
95
27
  def update_on_request finding, request
96
28
  if (route = finding.routes[0])
@@ -106,9 +38,14 @@ module Contrast
106
38
  return unless events&.any?
107
39
 
108
40
  events.each do |event|
109
- event.event_sources.each do |source|
110
- update(source.type)
111
- update(source.name) # rubocop:disable Security/Module/Name -- API attribute.
41
+ if event.cs__is_a?(Contrast::Api::Dtm::TraceEvent)
42
+ event.event_sources&.each do |source|
43
+ update(source.type)
44
+ update(source.name) # rubocop:disable Security/Module/Name
45
+ end
46
+ elsif event.cs__is_a?(Contrast::Agent::Assess::Events::SourceEvent)
47
+ update(event.source_type)
48
+ update(event.source_name)
112
49
  end
113
50
  end
114
51
  end