contrast-agent 4.3.2 → 4.4.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent.rb +5 -1
  3. data/lib/contrast/agent/assess.rb +0 -9
  4. data/lib/contrast/agent/assess/contrast_event.rb +0 -2
  5. data/lib/contrast/agent/assess/contrast_object.rb +5 -2
  6. data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
  7. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
  8. data/lib/contrast/agent/assess/policy/propagation_method.rb +28 -13
  9. data/lib/contrast/agent/assess/policy/propagator/append.rb +28 -13
  10. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -16
  11. data/lib/contrast/agent/assess/policy/propagator/splat.rb +23 -13
  12. data/lib/contrast/agent/assess/policy/propagator/split.rb +14 -7
  13. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +30 -14
  14. data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
  15. data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
  16. data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
  17. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -2
  18. data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
  19. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
  20. data/lib/contrast/agent/assess/properties.rb +0 -2
  21. data/lib/contrast/agent/assess/property/tagged.rb +37 -19
  22. data/lib/contrast/agent/assess/tracker.rb +1 -1
  23. data/lib/contrast/agent/middleware.rb +85 -55
  24. data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
  25. data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
  26. data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
  27. data/lib/contrast/agent/protect/rule/sqli.rb +17 -11
  28. data/lib/contrast/agent/request_context.rb +12 -0
  29. data/lib/contrast/agent/thread.rb +1 -1
  30. data/lib/contrast/agent/thread_watcher.rb +20 -5
  31. data/lib/contrast/agent/version.rb +1 -1
  32. data/lib/contrast/api/communication/messaging_queue.rb +18 -21
  33. data/lib/contrast/api/communication/response_processor.rb +8 -1
  34. data/lib/contrast/api/communication/socket_client.rb +22 -14
  35. data/lib/contrast/api/decorators.rb +2 -0
  36. data/lib/contrast/api/decorators/agent_startup.rb +58 -0
  37. data/lib/contrast/api/decorators/application_startup.rb +51 -0
  38. data/lib/contrast/api/decorators/route_coverage.rb +15 -5
  39. data/lib/contrast/api/decorators/trace_event.rb +42 -14
  40. data/lib/contrast/components/agent.rb +2 -0
  41. data/lib/contrast/components/app_context.rb +4 -22
  42. data/lib/contrast/components/sampling.rb +48 -6
  43. data/lib/contrast/components/settings.rb +5 -4
  44. data/lib/contrast/framework/manager.rb +13 -12
  45. data/lib/contrast/framework/rails/support.rb +42 -43
  46. data/lib/contrast/framework/sinatra/support.rb +100 -41
  47. data/lib/contrast/logger/log.rb +31 -15
  48. data/lib/contrast/utils/class_util.rb +3 -1
  49. data/lib/contrast/utils/heap_dump_util.rb +103 -87
  50. data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
  51. data/resources/assess/policy.json +3 -9
  52. data/resources/deadzone/policy.json +6 -0
  53. data/ruby-agent.gemspec +54 -16
  54. metadata +105 -136
  55. data/lib/contrast/agent/assess/rule.rb +0 -18
  56. data/lib/contrast/agent/assess/rule/base.rb +0 -52
  57. data/lib/contrast/agent/assess/rule/redos.rb +0 -67
  58. data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
  59. data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
  60. data/lib/contrast/utils/prevent_serialization.rb +0 -52
@@ -101,18 +101,12 @@ module Contrast
101
101
  # conditions were not met
102
102
  def build_finding context, trigger_node, source, object, ret, *args
103
103
  return unless Contrast::Agent::Assess::Policy::TriggerValidation.valid?(trigger_node, object, ret, args)
104
-
105
- request = context.request
106
- env = request.env
107
- return if defined?(ActionController::Live) &&
108
- env &&
109
- env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live)
104
+ return unless reportable?(context)
110
105
 
111
106
  finding = Contrast::Api::Dtm::Finding.new
112
107
  finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(trigger_node.rule_id)
113
108
  build_from_source(finding, source)
114
- trigger_event = Contrast::Agent::Assess::Events::EventFactory.build(trigger_node, source, object, ret, args).to_dtm_event
115
- finding.events << trigger_event
109
+ finding.events << Contrast::Agent::Assess::Events::EventFactory.build(trigger_node, source, object, ret, args).to_dtm_event
116
110
  build_hash(finding, source)
117
111
  finding.routes << context.route if context.route
118
112
  finding.version = determine_compliance_version(finding)
@@ -127,6 +121,17 @@ module Contrast
127
121
 
128
122
  private
129
123
 
124
+ # A request is reportable if it is not from ActionController::Live
125
+ #
126
+ # @param context [Contrast::Agent::RequestContext] the current request context
127
+ # @return [Boolean]
128
+ def reportable? context
129
+ env = context.request.env
130
+ !(defined?(ActionController::Live) &&
131
+ env &&
132
+ env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live))
133
+ end
134
+
130
135
  # This is our method that actually checks the taint on the object
131
136
  # our trigger_node targets.
132
137
  #
@@ -185,11 +185,7 @@ module Contrast
185
185
  # @return [Array<Contrast::Agent::Assess::Tag>] the ranges satisfied
186
186
  # by the given conditions
187
187
  def ranges_with_all_tags length, properties, required_tags
188
- # if there are no tags, not required tags, or the tags don't have
189
- # all the required tags, we can just return here.
190
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless properties.tracked?
191
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless required_tags&.any?
192
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless required_tags.all? { |tag| properties.tag_keys.include?(tag) }
188
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless matches_tags?(properties, required_tags)
193
189
 
194
190
  ranges = []
195
191
  chunking = false
@@ -229,8 +225,7 @@ module Contrast
229
225
  # by the given conditions
230
226
  def ranges_with_any_tag properties, tags
231
227
  # if there aren't any all_tags or tags, break early
232
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless properties.tracked?
233
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags&.any?
228
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless search_tags?(properties, tags)
234
229
 
235
230
  ranges = []
236
231
  tags.each do |desired|
@@ -243,6 +238,32 @@ module Contrast
243
238
  end
244
239
  ranges
245
240
  end
241
+
242
+ # We should only try to match tags on properties if those properties have any tags (are tracked) and there
243
+ # are tags to try and match on. Some rules, like regexp rules, have no tags. Some rules, like trigger, have
244
+ # no properties.
245
+ #
246
+ # @param properties [Contrast::Agent::Assess::Properties] the properties to check for the tags
247
+ # @param tags [Set<String>] the list of tags on which to match
248
+ # @return [Boolean] if the given properties has instances of every tag in tags
249
+ def search_tags? properties, tags
250
+ return false unless properties.tracked?
251
+ return false unless tags&.any?
252
+
253
+ true
254
+ end
255
+
256
+ # Determine if the given properties have instances of all the given tags or not.
257
+ #
258
+ # @param properties [Contrast::Agent::Assess::Properties] the properties to check for the tags
259
+ # @param tags [Set<String>] the list of tags on which to match
260
+ # @return [Boolean] if the given properties has instances of every tag in tags
261
+ def matches_tags? properties, tags
262
+ return false unless search_tags?(properties, tags)
263
+ return false unless tags.all? { |tag| properties.tag_keys.include?(tag) }
264
+
265
+ true
266
+ end
246
267
  end
247
268
  end
248
269
  end
@@ -0,0 +1,59 @@
1
+ # Copyright (c) 2020 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 Agent
6
+ module Assess
7
+ module Policy
8
+ module TriggerValidation
9
+ # Validator used to assert a REDOS finding is actually vulnerable
10
+ # before serializing that finding as a DTM to report to the service.
11
+ module REDOSValidator
12
+ RULE_NAME = 'redos'
13
+
14
+ class << self
15
+ def valid? _patcher, object, _ret, args
16
+ # Can arrive here from either:
17
+ # regexp =~ string
18
+ # string =~ regexp
19
+ # regexp.match string
20
+ #
21
+ # Thus object/args[0] can be string/regexp or regexp/string.
22
+ regexp = object.is_a?(Regexp) ? object : args[0]
23
+
24
+ # regexp must be exploitable.
25
+ return false unless regexp_vulnerable?(regexp)
26
+
27
+ true
28
+ end
29
+
30
+ protected
31
+
32
+ VULNERABLE_PATTERN = /[\[(].*?[\[(].*?[\])][*+?].*?[\])][*+?]/.cs__freeze
33
+
34
+ # Does the regexp
35
+ # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/rules/dataflow/redos.md
36
+ def regexp_vulnerable? regexp
37
+ # A pattern is considered vulnerable if it has 2 or more levels of nested multi-matching.
38
+ # A level is defined as any set of opening and closing control characters immediately followed by a multi match control character.
39
+ # A control character is defined as one of the OPENING_CHARS, CLOSING_CHARS,
40
+ # or MULTI_MATCH_CHARS that is not immediately preceded by an escaping \ character.
41
+ # OPENING_CHARS are ( and [ CLOSING_CHARS are ) and ] MULTI_MATCH_CHARS are +, *, and ?
42
+
43
+ # Nota bene about Regexp#to_s: it doesn't necessarily give you the original Regexp back
44
+ # (in the sense of `my_str == Regexp.new(my_str).to_s`), it gives you a Regexp that
45
+ # will have the same functional characteristics as the original.
46
+ # Regexp#inspect gives you a "more nicely formatted" version than #to_s.
47
+ # Regexp#source will give you the original source.
48
+
49
+ # Use #match? because it doesn't fill out global variables
50
+ # in the way match or =~ do.
51
+ VULNERABLE_PATTERN.match? regexp.source
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -9,7 +9,7 @@ module Contrast
9
9
  # Validator used to assert a SSRF finding is actually vulnerable
10
10
  # before serializing that finding as a DTM to report to the service.
11
11
  module SSRFValidator
12
- SSRF_RULE = 'ssrf'
12
+ RULE_NAME = 'ssrf'
13
13
  URL_PATTERN =
14
14
  %r{(?<protocol>http|https|ftp|sftp|telnet|gopher|rtsp|rtsps|ssh|svn)://(?<host>[^/?]+)(?<path>/?[^?]*)(?<query_string>\?.*)?}i.cs__freeze
15
15
  # The Net::HTTP class validates host format on instantiation. Since
@@ -23,7 +23,6 @@ module Contrast
23
23
  # querystring
24
24
  # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/rules/dataflow/server_side_request_forgery.md
25
25
  def self.valid? patcher, _object, _ret, args
26
- return true unless SSRF_RULE == patcher&.rule_id
27
26
  return true if patcher.id.to_s.start_with?(PATH_ONLY_PATCH_MARKER)
28
27
 
29
28
  url = args[0].to_s
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'contrast/agent/assess/policy/trigger_validation/ssrf_validator'
5
5
  require 'contrast/agent/assess/policy/trigger_validation/xss_validator'
6
+ require 'contrast/agent/assess/policy/trigger_validation/redos_validator'
6
7
 
7
8
  module Contrast
8
9
  module Agent
@@ -15,7 +16,8 @@ module Contrast
15
16
  module TriggerValidation
16
17
  VALIDATORS = [
17
18
  Contrast::Agent::Assess::Policy::TriggerValidation::SSRFValidator,
18
- Contrast::Agent::Assess::Policy::TriggerValidation::XSSValidator
19
+ Contrast::Agent::Assess::Policy::TriggerValidation::XSSValidator,
20
+ Contrast::Agent::Assess::Policy::TriggerValidation::REDOSValidator
19
21
  ].cs__freeze
20
22
 
21
23
  # Determines if the conditions in which this trigger was called are
@@ -32,9 +34,9 @@ module Contrast
32
34
  # @return [Boolean] if the conditions are valid for the generation of
33
35
  # a Contrast::Api::Dtm::Finding
34
36
  def self.valid? patcher, object, ret, args
35
- VALIDATORS.each do |validator|
36
- return false unless validator.valid?(patcher, object, ret, args)
37
- end
37
+ specific_validator = VALIDATORS.find { |validator| validator::RULE_NAME == patcher&.rule_id }
38
+ return specific_validator.valid?(patcher, object, ret, args) if specific_validator
39
+
38
40
  true
39
41
  end
40
42
  end
@@ -10,7 +10,7 @@ module Contrast
10
10
  # vulnerable before serializing that finding as a DTM to report to
11
11
  # the service.
12
12
  module XSSValidator
13
- XSS_RULE = 'reflected-xss'
13
+ RULE_NAME = 'reflected-xss'
14
14
  SAFE_CONTENT_TYPES = %w[
15
15
  /csv
16
16
  /javascript
@@ -23,9 +23,7 @@ module Contrast
23
23
  # A finding is valid for XSS if the response type is not one of
24
24
  # those assumed to be safe
25
25
  # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/rules/dataflow/reflected_xss.md
26
- def self.valid? patcher, _object, _ret, _args
27
- return true unless XSS_RULE == patcher&.rule_id
28
-
26
+ def self.valid? _patcher, _object, _ret, _args
29
27
  content_type = Contrast::Agent::REQUEST_TRACKER.current&.response&.content_type
30
28
  return true unless content_type
31
29
 
@@ -6,7 +6,6 @@ require 'set'
6
6
  require 'contrast/agent/assess/property/evented'
7
7
  require 'contrast/agent/assess/property/tagged'
8
8
  require 'contrast/agent/assess/property/updated'
9
- require 'contrast/utils/prevent_serialization'
10
9
 
11
10
  module Contrast
12
11
  module Agent
@@ -18,7 +17,6 @@ module Contrast
18
17
  # to properly convey the events that lead up to the state of the tracked
19
18
  # user input.
20
19
  class Properties
21
- include Contrast::Utils::PreventSerialization
22
20
  include Contrast::Agent::Assess::Property::Evented
23
21
  include Contrast::Agent::Assess::Property::Tagged
24
22
  include Contrast::Agent::Assess::Property::Updated
@@ -91,28 +91,15 @@ module Contrast
91
91
 
92
92
  at = Hash.new { |h, k| h[k] = [] }
93
93
  tags.each_pair do |key, value|
94
- add = []
94
+ add = nil
95
95
  value.each do |tag|
96
- comparison = tag.compare_range(range.begin, range.end)
97
- # BELOW and ABOVE are applicable to this check and are removed.
98
- case comparison
99
- # part of the tag is being selected
100
- when Contrast::Agent::Assess::Tag::LOW_SPAN
101
- add << Contrast::Agent::Assess::Tag.new(tag.label, range.size)
102
- # the tag exists in the requested range, figure out the boundaries
103
- when Contrast::Agent::Assess::Tag::WITHIN
104
- start = tag.start_idx - range.begin
105
- finish = range.size - start
106
- add << Contrast::Agent::Assess::Tag.new(tag.label, finish, start)
107
- # the tag spans the requested range.
108
- when Contrast::Agent::Assess::Tag::WITHOUT # rubocop:disable Lint/DuplicateBranch
109
- add << Contrast::Agent::Assess::Tag.new(tag.label, range.size)
110
- # part of the tag is being selected
111
- when Contrast::Agent::Assess::Tag::HIGH_SPAN # rubocop:disable Lint/DuplicateBranch
112
- add << Contrast::Agent::Assess::Tag.new(tag.label, range.size)
96
+ within_range = resize_to_range(tag, range)
97
+ if within_range
98
+ add ||= []
99
+ add << within_range
113
100
  end
114
101
  end
115
- next if add.empty?
102
+ next unless add&.any?
116
103
 
117
104
  at[key] = add
118
105
  end
@@ -344,6 +331,37 @@ module Contrast
344
331
  Contrast::Utils::TagUtil.ordered_merge(value, add)
345
332
  end
346
333
  end
334
+
335
+ private
336
+
337
+ # Given a tag, compare it to a given range and, if any part of that tag is within the range, return a new tag
338
+ # covering the union of the original tag and the range. This new tag will start at the
339
+ # max(tag.start, range.start) and end at min(tag.end, range.end)
340
+ #
341
+ # @param tag [Contrast::Agent::Assess::Tag] the Tag that may be in this range
342
+ # @param range [Range] the span to check, inclusive to exclusive
343
+ # @return [Contrast::Agent::Assess::Tag, nil] a new tag, truncated to only span within the given range or nil
344
+ # if no overlap exists
345
+ def resize_to_range tag, range
346
+ comparison = tag.compare_range(range.begin, range.end)
347
+ # BELOW and ABOVE are not applicable to this check and result in nil
348
+ case comparison
349
+ # part of the tag is being selected
350
+ when Contrast::Agent::Assess::Tag::LOW_SPAN
351
+ Contrast::Agent::Assess::Tag.new(tag.label, range.size)
352
+ # the tag exists in the requested range, figure out the boundaries
353
+ when Contrast::Agent::Assess::Tag::WITHIN
354
+ start = tag.start_idx - range.begin
355
+ finish = range.size - start
356
+ Contrast::Agent::Assess::Tag.new(tag.label, finish, start)
357
+ # the tag spans the requested range.
358
+ when Contrast::Agent::Assess::Tag::WITHOUT # rubocop:disable Lint/DuplicateBranch
359
+ Contrast::Agent::Assess::Tag.new(tag.label, range.size)
360
+ # part of the tag is being selected
361
+ when Contrast::Agent::Assess::Tag::HIGH_SPAN # rubocop:disable Lint/DuplicateBranch
362
+ Contrast::Agent::Assess::Tag.new(tag.label, range.size)
363
+ end
364
+ end
347
365
  end
348
366
  end
349
367
  end
@@ -16,7 +16,7 @@ module Contrast
16
16
  class << self
17
17
  # Retrieve the properties of the given Object, iff they exist.
18
18
  #
19
- # @param source [Object] the thing for which to look up properties.
19
+ # @param source [Object, nil] the thing for which to look up properties.
20
20
  # @return [Contrast::Agent::Assess::Properties, nil]
21
21
  def properties source
22
22
  PROPERTIES_HASH[source]
@@ -46,27 +46,6 @@ module Contrast
46
46
  agent_startup_routine
47
47
  end
48
48
 
49
- # Startup the Agent as part of the initialization process:
50
- # - start the service sending thread, responsible for sending and
51
- # processing messages
52
- # - start the heartbeat thread, which triggers service startup
53
- # - start instrumenting libraries and do a 'catchup' patch for everything
54
- # we didn't see get loaded
55
- # - enable TracePoint, which handles all class loads and required
56
- # instrumentation going forward
57
- def agent_startup_routine
58
- logger.debug_with_time('middleware: starting service') do
59
- Contrast::Agent.thread_watcher.ensure_running?
60
- end
61
-
62
- logger.debug_with_time('middleware: instrument shared libraries and patch') do
63
- Contrast::Agent::Patching::Policy::Patcher.patch
64
- end
65
-
66
- AGENT.enable_tracepoint
67
- Contrast::Agent::AtExitHook.exit_hook
68
- end
69
-
70
49
  # This is where we're hooked into the middleware stack. If the agent is enabled, we're ready
71
50
  # to do some processing on a per request basis. If not, we just pass the request along to the
72
51
  # next middleware in the stack.
@@ -76,14 +55,11 @@ module Contrast
76
55
  # @return [Array,Rack::Response] the Response of this and subsequent Middlewares to be passed back
77
56
  # to the user up the Rack framework.
78
57
  def call env
79
- Contrast::Utils::HeapDumpUtil.run
58
+ return app.call(env) unless AGENT.enabled?
80
59
 
81
- if AGENT.enabled?
82
- handle_first_request
83
- call_with_agent(env)
84
- else
85
- app.call(env)
86
- end
60
+ Contrast::Agent.heapdump_util.start_thread!
61
+ handle_first_request
62
+ call_with_agent(env)
87
63
  end
88
64
 
89
65
  private
@@ -103,6 +79,29 @@ module Contrast
103
79
  end
104
80
  end
105
81
 
82
+ # Startup the Agent as part of the initialization process:
83
+ # - start the service sending thread, responsible for sending and
84
+ # processing messages
85
+ # - start the heartbeat thread, which triggers service startup
86
+ # - start instrumenting libraries and do a 'catchup' patch for everything
87
+ # we didn't see get loaded
88
+ # - enable TracePoint, which handles all class loads and required
89
+ # instrumentation going forward
90
+ def agent_startup_routine
91
+ logger.debug_with_time('middleware: starting service') do
92
+ Contrast::Agent.thread_watcher.ensure_running?
93
+ end
94
+
95
+ logger.debug_with_time('middleware: instrument shared libraries and patch') do
96
+ Contrast::Agent::Patching::Policy::Patcher.patch
97
+ end
98
+
99
+ logger.debug_with_time('middleware: enabling tracepoint') do
100
+ AGENT.enable_tracepoint
101
+ end
102
+ Contrast::Agent::AtExitHook.exit_hook
103
+ end
104
+
106
105
  # Some things have to wait until first request to happen, either because
107
106
  # resolution is not complete or because the framework will preload
108
107
  # classes, which confuses some of our instrumentation.
@@ -135,10 +134,12 @@ module Contrast
135
134
  # This is where we process each request we intercept as a middleware. We make the request context
136
135
  # available globally so that it can be accessed from anywhere. A RequestHandler object is made
137
136
  # for each request, which handles prefilter and postfilter operations.
137
+ # @param env [Hash] the various variables stored by this and other Middlewares to know the state
138
+ # and values of this Request
139
+ # @return [Array,Rack::Response] the Response of this and subsequent Middlewares to be passed back
140
+ # to the user up the Rack framework.
138
141
  def call_with_agent env
139
142
  Contrast::Agent.thread_watcher.ensure_running?
140
- return unless AGENT.enabled?
141
-
142
143
  framework_request = Contrast::Agent.framework_manager.retrieve_request(env)
143
144
  context = Contrast::Agent::RequestContext.new(framework_request)
144
145
  response = nil
@@ -148,28 +149,9 @@ module Contrast
148
149
  logger.request_start
149
150
  request_handler = Contrast::Agent::RequestHandler.new(context)
150
151
 
151
- # prefilter sequence
152
- with_contrast_scope do
153
- context.service_extract_request
154
- request_handler.ruleset.prefilter
155
- end
156
-
157
- response = application_code(env) # pass request down the Rack chain with original env
158
-
159
- # postfilter sequence
160
- with_contrast_scope do
161
- context.extract_after(response) # update context with final response information
162
-
163
- if Contrast::Agent.framework_manager.streaming?(env)
164
- context.reset_activity
165
- request_handler.stream_safe_postfilter
166
- else
167
- request_handler.ruleset.postfilter
168
- # return response stored in the context in case any postfilter rules updated the response data
169
- response = context.response&.rack_response || response
170
- request_handler.send_activity_messages
171
- end
172
- end
152
+ pre_call_with_agent(context, request_handler)
153
+ response = application_code(env)
154
+ post_call_with_agent(context, env, request_handler, response)
173
155
  ensure
174
156
  logger.request_end
175
157
  end
@@ -179,6 +161,47 @@ module Contrast
179
161
  handle_exception(e)
180
162
  end
181
163
 
164
+ # Handle the operations the Agent needs to accomplish prior to the Application code executing during this
165
+ # request.
166
+ #
167
+ # @param context [Contrast::Agent::RequestContext]
168
+ # @param request_handler [Contrast::Agent::RequestHandler]
169
+ def pre_call_with_agent context, request_handler
170
+ with_contrast_scope do
171
+ context.service_extract_request
172
+ request_handler.ruleset.prefilter
173
+ end
174
+ rescue StandardError => e
175
+ raise e if security_exception?(e)
176
+
177
+ logger.error('Unable to execute agent pre_call', e)
178
+ end
179
+
180
+ # Handle the operations the Agent needs to accomplish after the Application code executes during this request.
181
+ #
182
+ # @param context [Contrast::Agent::RequestContext]
183
+ # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
184
+ # this Request
185
+ # @param request_handler [Contrast::Agent::RequestHandler]
186
+ # @param response [Array,Rack::Response]
187
+ def post_call_with_agent context, env, request_handler, response
188
+ with_contrast_scope do
189
+ context.extract_after(response) # update context with final response information
190
+
191
+ if Contrast::Agent.framework_manager.streaming?(env)
192
+ context.reset_activity
193
+ request_handler.stream_safe_postfilter
194
+ else
195
+ request_handler.ruleset.postfilter
196
+ request_handler.send_activity_messages
197
+ end
198
+ end
199
+ rescue StandardError => e
200
+ raise e if security_exception?(e)
201
+
202
+ logger.error('Unable to execute agent post_call', e)
203
+ end
204
+
182
205
  def application_code env
183
206
  logger.trace_with_time('application') do
184
207
  app.call(env)
@@ -192,9 +215,7 @@ module Contrast
192
215
  # We're only going to suppress SecurityExceptions indicating a blocked attack.
193
216
  # And, only if the config.agent.ruby.exceptions.capture? is set
194
217
  def handle_exception exception
195
- if exception.is_a?(Contrast::SecurityException) ||
196
- exception.message&.include?(SECURITY_EXCEPTION_MARKER)
197
-
218
+ if security_exception?(exception)
198
219
  exception_control = AGENT.exception_control
199
220
  raise exception unless exception_control[:enable]
200
221
 
@@ -205,6 +226,15 @@ module Contrast
205
226
  end
206
227
  end
207
228
 
229
+ # Is the given exception one raised by our Protect code?
230
+ #
231
+ # @param exception [Exception]
232
+ # @return [Boolean]
233
+ def security_exception? exception
234
+ exception.is_a?(Contrast::SecurityException) ||
235
+ exception.message&.include?(SECURITY_EXCEPTION_MARKER)
236
+ end
237
+
208
238
  # As we deprecate support to prepare to remove dead code, we need to
209
239
  # inform our users still relying on the now deprecated and soon to be
210
240
  # removed functionality. This method handles doing that by leveraging the