contrast-agent 4.3.2 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
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