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.
- checksums.yaml +4 -4
- data/lib/contrast/agent.rb +5 -1
- data/lib/contrast/agent/assess.rb +0 -9
- data/lib/contrast/agent/assess/contrast_event.rb +0 -2
- data/lib/contrast/agent/assess/contrast_object.rb +5 -2
- data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
- data/lib/contrast/agent/assess/policy/propagation_method.rb +28 -13
- data/lib/contrast/agent/assess/policy/propagator/append.rb +28 -13
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -16
- data/lib/contrast/agent/assess/policy/propagator/splat.rb +23 -13
- data/lib/contrast/agent/assess/policy/propagator/split.rb +14 -7
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +30 -14
- data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
- data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
- data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
- data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -2
- data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
- data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
- data/lib/contrast/agent/assess/properties.rb +0 -2
- data/lib/contrast/agent/assess/property/tagged.rb +37 -19
- data/lib/contrast/agent/assess/tracker.rb +1 -1
- data/lib/contrast/agent/middleware.rb +85 -55
- data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
- data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
- data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
- data/lib/contrast/agent/protect/rule/sqli.rb +17 -11
- data/lib/contrast/agent/request_context.rb +12 -0
- data/lib/contrast/agent/thread.rb +1 -1
- data/lib/contrast/agent/thread_watcher.rb +20 -5
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/api/communication/messaging_queue.rb +18 -21
- data/lib/contrast/api/communication/response_processor.rb +8 -1
- data/lib/contrast/api/communication/socket_client.rb +22 -14
- data/lib/contrast/api/decorators.rb +2 -0
- data/lib/contrast/api/decorators/agent_startup.rb +58 -0
- data/lib/contrast/api/decorators/application_startup.rb +51 -0
- data/lib/contrast/api/decorators/route_coverage.rb +15 -5
- data/lib/contrast/api/decorators/trace_event.rb +42 -14
- data/lib/contrast/components/agent.rb +2 -0
- data/lib/contrast/components/app_context.rb +4 -22
- data/lib/contrast/components/sampling.rb +48 -6
- data/lib/contrast/components/settings.rb +5 -4
- data/lib/contrast/framework/manager.rb +13 -12
- data/lib/contrast/framework/rails/support.rb +42 -43
- data/lib/contrast/framework/sinatra/support.rb +100 -41
- data/lib/contrast/logger/log.rb +31 -15
- data/lib/contrast/utils/class_util.rb +3 -1
- data/lib/contrast/utils/heap_dump_util.rb +103 -87
- data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
- data/resources/assess/policy.json +3 -9
- data/resources/deadzone/policy.json +6 -0
- data/ruby-agent.gemspec +54 -16
- metadata +105 -136
- data/lib/contrast/agent/assess/rule.rb +0 -18
- data/lib/contrast/agent/assess/rule/base.rb +0 -52
- data/lib/contrast/agent/assess/rule/redos.rb +0 -67
- data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
- data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
- 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
|
-
|
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
|
-
|
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
|
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
|
-
|
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.
|
36
|
-
|
37
|
-
|
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
|
-
|
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?
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
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
|
-
|
58
|
+
return app.call(env) unless AGENT.enabled?
|
80
59
|
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
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
|
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
|