contrast-agent 4.2.0 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.c +22 -10
  4. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.h +4 -3
  5. data/lib/contrast/agent.rb +5 -1
  6. data/lib/contrast/agent/assess.rb +0 -9
  7. data/lib/contrast/agent/assess/contrast_event.rb +49 -132
  8. data/lib/contrast/agent/assess/contrast_object.rb +54 -0
  9. data/lib/contrast/agent/assess/events/source_event.rb +4 -9
  10. data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
  11. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
  12. data/lib/contrast/agent/assess/policy/patcher.rb +4 -3
  13. data/lib/contrast/agent/assess/policy/policy_node.rb +31 -59
  14. data/lib/contrast/agent/assess/policy/preshift.rb +3 -3
  15. data/lib/contrast/agent/assess/policy/propagation_method.rb +41 -32
  16. data/lib/contrast/agent/assess/policy/propagation_node.rb +12 -24
  17. data/lib/contrast/agent/assess/policy/propagator/append.rb +29 -15
  18. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -2
  19. data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
  20. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -18
  21. data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -2
  22. data/lib/contrast/agent/assess/policy/propagator/keep.rb +1 -2
  23. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +3 -2
  24. data/lib/contrast/agent/assess/policy/propagator/next.rb +1 -2
  25. data/lib/contrast/agent/assess/policy/propagator/prepend.rb +1 -2
  26. data/lib/contrast/agent/assess/policy/propagator/remove.rb +2 -4
  27. data/lib/contrast/agent/assess/policy/propagator/replace.rb +1 -2
  28. data/lib/contrast/agent/assess/policy/propagator/reverse.rb +1 -2
  29. data/lib/contrast/agent/assess/policy/propagator/select.rb +3 -4
  30. data/lib/contrast/agent/assess/policy/propagator/splat.rb +25 -17
  31. data/lib/contrast/agent/assess/policy/propagator/split.rb +83 -120
  32. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +41 -25
  33. data/lib/contrast/agent/assess/policy/propagator/trim.rb +3 -7
  34. data/lib/contrast/agent/assess/policy/source_method.rb +2 -14
  35. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +5 -8
  36. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
  37. data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
  38. data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
  39. data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
  40. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +2 -3
  41. data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
  42. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
  43. data/lib/contrast/agent/assess/properties.rb +0 -2
  44. data/lib/contrast/agent/assess/property/tagged.rb +56 -32
  45. data/lib/contrast/agent/assess/tracker.rb +16 -18
  46. data/lib/contrast/agent/deadzone/policy/deadzone_node.rb +7 -0
  47. data/lib/contrast/agent/middleware.rb +134 -55
  48. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +4 -0
  49. data/lib/contrast/agent/patching/policy/method_policy.rb +1 -1
  50. data/lib/contrast/agent/patching/policy/patch.rb +4 -4
  51. data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
  52. data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
  53. data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
  54. data/lib/contrast/agent/protect/policy/applies_deserialization_rule.rb +47 -1
  55. data/lib/contrast/agent/protect/policy/rule_applicator.rb +53 -0
  56. data/lib/contrast/agent/protect/rule/base.rb +63 -14
  57. data/lib/contrast/agent/protect/rule/cmd_injection.rb +3 -3
  58. data/lib/contrast/agent/protect/rule/default_scanner.rb +1 -4
  59. data/lib/contrast/agent/protect/rule/deserialization.rb +4 -1
  60. data/lib/contrast/agent/protect/rule/no_sqli.rb +3 -3
  61. data/lib/contrast/agent/protect/rule/sqli.rb +20 -14
  62. data/lib/contrast/agent/protect/rule/xxe.rb +32 -11
  63. data/lib/contrast/agent/protect/rule/xxe/entity_wrapper.rb +10 -6
  64. data/lib/contrast/agent/reaction_processor.rb +1 -1
  65. data/lib/contrast/agent/request_context.rb +12 -0
  66. data/lib/contrast/agent/response.rb +5 -5
  67. data/lib/contrast/agent/rewriter.rb +3 -3
  68. data/lib/contrast/agent/scope.rb +33 -13
  69. data/lib/contrast/agent/static_analysis.rb +13 -7
  70. data/lib/contrast/agent/thread.rb +1 -1
  71. data/lib/contrast/agent/thread_watcher.rb +20 -5
  72. data/lib/contrast/agent/version.rb +1 -1
  73. data/lib/contrast/api/communication/messaging_queue.rb +18 -21
  74. data/lib/contrast/api/communication/response_processor.rb +8 -1
  75. data/lib/contrast/api/communication/socket_client.rb +22 -14
  76. data/lib/contrast/api/decorators.rb +2 -0
  77. data/lib/contrast/api/decorators/agent_startup.rb +58 -0
  78. data/lib/contrast/api/decorators/application_startup.rb +51 -0
  79. data/lib/contrast/api/decorators/library.rb +1 -0
  80. data/lib/contrast/api/decorators/library_usage_update.rb +1 -0
  81. data/lib/contrast/api/decorators/route_coverage.rb +15 -5
  82. data/lib/contrast/api/decorators/trace_event.rb +58 -42
  83. data/lib/contrast/api/decorators/trace_event_object.rb +11 -3
  84. data/lib/contrast/api/decorators/trace_event_signature.rb +27 -5
  85. data/lib/contrast/api/decorators/user_input.rb +2 -1
  86. data/lib/contrast/common_agent_configuration.rb +1 -1
  87. data/lib/contrast/components/agent.rb +2 -0
  88. data/lib/contrast/components/app_context.rb +4 -22
  89. data/lib/contrast/components/assess.rb +36 -0
  90. data/lib/contrast/components/interface.rb +5 -3
  91. data/lib/contrast/components/sampling.rb +48 -6
  92. data/lib/contrast/components/scope.rb +23 -0
  93. data/lib/contrast/components/settings.rb +8 -7
  94. data/lib/contrast/config/assess_configuration.rb +2 -1
  95. data/lib/contrast/extension/assess/array.rb +1 -2
  96. data/lib/contrast/extension/assess/erb.rb +1 -3
  97. data/lib/contrast/extension/assess/exec_trigger.rb +1 -1
  98. data/lib/contrast/extension/assess/fiber.rb +2 -3
  99. data/lib/contrast/extension/assess/hash.rb +4 -2
  100. data/lib/contrast/extension/assess/kernel.rb +1 -2
  101. data/lib/contrast/extension/assess/marshal.rb +34 -26
  102. data/lib/contrast/extension/assess/regexp.rb +3 -8
  103. data/lib/contrast/extension/assess/string.rb +1 -2
  104. data/lib/contrast/framework/base_support.rb +51 -53
  105. data/lib/contrast/framework/manager.rb +16 -14
  106. data/lib/contrast/framework/rack/patch/session_cookie.rb +1 -1
  107. data/lib/contrast/framework/rack/support.rb +2 -1
  108. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +1 -1
  109. data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +1 -1
  110. data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +1 -1
  111. data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +1 -1
  112. data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +1 -1
  113. data/lib/contrast/framework/rails/support.rb +44 -44
  114. data/lib/contrast/framework/sinatra/support.rb +102 -42
  115. data/lib/contrast/logger/application.rb +0 -3
  116. data/lib/contrast/logger/log.rb +31 -15
  117. data/lib/contrast/utils/class_util.rb +3 -1
  118. data/lib/contrast/utils/duck_utils.rb +1 -1
  119. data/lib/contrast/utils/heap_dump_util.rb +103 -87
  120. data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
  121. data/lib/contrast/utils/object_share.rb +3 -3
  122. data/lib/contrast/utils/preflight_util.rb +1 -1
  123. data/lib/contrast/utils/resource_loader.rb +1 -1
  124. data/lib/contrast/utils/sha256_builder.rb +2 -2
  125. data/lib/contrast/utils/string_utils.rb +1 -1
  126. data/lib/contrast/utils/tag_util.rb +9 -13
  127. data/resources/assess/policy.json +12 -18
  128. data/resources/deadzone/policy.json +150 -0
  129. data/resources/protect/policy.json +12 -0
  130. data/ruby-agent.gemspec +60 -19
  131. data/service_executables/VERSION +1 -1
  132. data/service_executables/linux/contrast-service +0 -0
  133. data/service_executables/mac/contrast-service +0 -0
  134. metadata +124 -112
  135. data/lib/contrast/agent/assess/rule.rb +0 -18
  136. data/lib/contrast/agent/assess/rule/base.rb +0 -52
  137. data/lib/contrast/agent/assess/rule/redos.rb +0 -67
  138. data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
  139. data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
  140. data/lib/contrast/utils/prevent_serialization.rb +0 -52
@@ -23,10 +23,10 @@ module Contrast
23
23
  end
24
24
 
25
25
  def infilter context, classname, method, command
26
- return nil unless infilter?(context)
26
+ return unless infilter?(context)
27
27
 
28
28
  ia_results = gather_ia_results(context)
29
- return nil if ia_results.empty?
29
+ return if ia_results.empty?
30
30
 
31
31
  if APP_CONTEXT.in_new_process?
32
32
  logger.trace('Running cmd-injection infilter within new process - creating new context')
@@ -36,7 +36,7 @@ module Contrast
36
36
 
37
37
  result = find_attacker_with_results(context, command, ia_results, **{ classname: classname, method: method })
38
38
  result ||= report_command_execution(context, command, **{ classname: classname, method: method })
39
- return nil unless result
39
+ return unless result
40
40
 
41
41
  append_to_activity(context, result)
42
42
  return unless blocked?
@@ -123,10 +123,7 @@ class Contrast::Agent::Protect::Rule::DefaultScanner # rubocop:disable Style/Cla
123
123
  elsif start_block_comment?(char, index, query)
124
124
  boundaries << index
125
125
  :STATE_INSIDE_BLOCK_COMMENT
126
- elsif operator?(char)
127
- boundaries << index
128
- :STATE_EXPECTING_TOKEN
129
- elsif char.match?(Contrast::Utils::ObjectShare::WHITE_SPACE_REGEXP)
126
+ elsif operator?(char) || char.match?(Contrast::Utils::ObjectShare::WHITE_SPACE_REGEXP)
130
127
  boundaries << index
131
128
  :STATE_EXPECTING_TOKEN
132
129
  else
@@ -17,19 +17,22 @@ module Contrast
17
17
  BLOCK_MESSAGE = 'Untrusted Deserialization rule triggered. Deserialization blocked.'
18
18
 
19
19
  # Gadgets that map to ERB modules
20
- ERB_GADGETS = %w[
20
+ ERB_GADGETS = %W[
21
21
  object:ERB
22
+ o:\bERB
22
23
  ].cs__freeze
23
24
 
24
25
  # Gadgets that map to ActionDispatch modules
25
26
  ACTION_DISPATCH_GADGETS = %w[
26
27
  object:ActionDispatch::Routing::RouteSet::NamedRouteCollection
28
+ o:\bActionDispatch::Routing::RouteSet::NamedRouteCollection
27
29
  ].cs__freeze
28
30
 
29
31
  # Gadgets that map to Arel Modules
30
32
  AREL_GADGETS = %w[
31
33
  string:Arel::Nodes::SqlLiteral
32
34
  object:Arel::Nodes
35
+ o:\bArel::Nodes
33
36
  ].cs__freeze
34
37
 
35
38
  # Used to indicate to TeamServer the gadget is an ERB module
@@ -21,10 +21,10 @@ module Contrast
21
21
  end
22
22
 
23
23
  def infilter context, database, query_string
24
- return nil unless infilter?(context)
24
+ return unless infilter?(context)
25
25
 
26
26
  result = find_attacker(context, query_string, database: database)
27
- return nil unless result
27
+ return unless result
28
28
 
29
29
  append_to_activity(context, result)
30
30
 
@@ -36,7 +36,7 @@ module Contrast
36
36
 
37
37
  attack_string = input_analysis_result.value
38
38
  regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
39
- return nil unless query_string.match?(regexp)
39
+ return unless query_string.match?(regexp)
40
40
 
41
41
  scanner = select_scanner
42
42
  ss = StringScanner.new(query_string)
@@ -22,10 +22,10 @@ module Contrast
22
22
  end
23
23
 
24
24
  def infilter context, database, query_string
25
- return nil unless infilter?(context)
25
+ return unless infilter?(context)
26
26
 
27
27
  result = find_attacker(context, query_string, database: database)
28
- return nil unless result
28
+ return unless result
29
29
 
30
30
  append_to_activity(context, result)
31
31
 
@@ -36,7 +36,7 @@ module Contrast
36
36
  attack_string = input_analysis_result.value
37
37
  regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
38
38
 
39
- return nil unless query_string.match?(regexp)
39
+ return unless query_string.match?(regexp)
40
40
 
41
41
  database = kwargs[:database]
42
42
  scanner = select_scanner(database)
@@ -50,16 +50,9 @@ module Contrast
50
50
  last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
51
51
  next unless last_boundary && boundary
52
52
 
53
- input_analysis_result.attack_count = input_analysis_result.attack_count + 1
54
-
55
- kwargs[:start_idx] = idx
56
- kwargs[:end_idx] = idx + length
57
- kwargs[:boundary_overrun_idx] = boundary
58
- kwargs[:input_boundary_idx] = last_boundary
59
-
60
53
  result ||= build_attack_result(context)
61
- update_successful_attack_response(context, input_analysis_result, result, query_string)
62
- append_sample(context, input_analysis_result, result, query_string, **kwargs)
54
+ record_match(idx, length, boundary, last_boundary, kwargs)
55
+ append_match(context, input_analysis_result, result, query_string, kwargs)
63
56
  end
64
57
 
65
58
  result
@@ -75,13 +68,26 @@ module Contrast
75
68
  sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
76
69
  sample.sqli.start_idx = sample.sqli.query.index(input).to_i
77
70
  sample.sqli.end_idx = sample.sqli.start_idx + input.length
78
- sample.sqli.boundary_overrun_idx = kwargs[:boundary].to_i
79
- sample.sqli.input_boundary_idx = kwargs[:last_boundary].to_i
71
+ sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
72
+ sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
80
73
  sample
81
74
  end
82
75
 
83
76
  private
84
77
 
78
+ def record_match idx, length, boundary, last_boundary, kwargs
79
+ kwargs[:start_idx] = idx
80
+ kwargs[:end_idx] = idx + length
81
+ kwargs[:boundary_overrun_idx] = boundary
82
+ kwargs[:input_boundary_idx] = last_boundary
83
+ end
84
+
85
+ def append_match context, input_analysis_result, result, query_string, **kwargs
86
+ input_analysis_result.attack_count = input_analysis_result.attack_count + 1
87
+ update_successful_attack_response(context, input_analysis_result, result, query_string)
88
+ append_sample(context, input_analysis_result, result, query_string, **kwargs)
89
+ end
90
+
85
91
  def select_scanner database
86
92
  @sql_scanners ||= {
87
93
  Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
@@ -19,7 +19,9 @@ module Contrast
19
19
  NAME
20
20
  end
21
21
 
22
- # Given an xml, evaluate it for an XXE attack.
22
+ # Given an xml, evaluate it for an XXE attack. There's no return here
23
+ # as this method handles appending the evaluation to the request
24
+ # context, connecting it to the reporting mechanism at request end.
23
25
  #
24
26
  # @param context [Contrast::Agent::RequestContext] the context of the
25
27
  # request in which this input is evaluated.
@@ -29,7 +31,7 @@ module Contrast
29
31
  # attack is found and the rule is in block mode.
30
32
  def infilter context, framework, xml
31
33
  result = find_attacker(context, xml, framework: framework)
32
- return nil unless result
34
+ return unless result
33
35
 
34
36
  append_to_activity(context, result)
35
37
  return unless blocked?
@@ -39,16 +41,23 @@ module Contrast
39
41
 
40
42
  protected
41
43
 
44
+ # Given an XML, find any externally resolved entities and create an
45
+ # Attack Result for them
46
+ #
47
+ # @param context [Contrast::Agent::RequestContext] the context of the
48
+ # request in which this input is evaluated.
49
+ # @param xml [String] the literal value of the XML being checked for
50
+ # external entity resolution
51
+ # @param _kwargs [Hash]
52
+ # @return [Contrast::Api::Dtm::AttackResult, nil] the determination
53
+ # as to whether or not this XML has an XXE attack in it.
42
54
  def find_attacker context, xml, **_kwargs
43
- return nil unless xml
44
- return nil if protect_excluded_by_code?
55
+ return unless xml
56
+ return if protect_excluded_by_code?
45
57
 
46
- xxe_details, last_idx = build_details(xml)
47
- return nil unless xxe_details
58
+ xxe_details = build_details(xml)
59
+ return unless xxe_details
48
60
 
49
- # For our definition, the prolog goes from the start of the XML
50
- # string to the end of the last entity declaration.
51
- xxe_details.xml = Contrast::Utils::StringUtils.protobuf_safe_string(xml[0, last_idx])
52
61
  ia_result = build_evaluation(xxe_details.xml)
53
62
  build_attack_with_match(
54
63
  context,
@@ -58,7 +67,15 @@ module Contrast
58
67
  details: xxe_details)
59
68
  end
60
69
 
61
- def build_details xml, _evaluation = nil
70
+ # Given an XML determined to be unsafe, build out the details of the
71
+ # attack. The details will include a substring of the given XML up to
72
+ # the end of the prolog, where the external entities are declared.
73
+ #
74
+ # @param xml [String] the literal value of the XML being checked for
75
+ # external entity resolution
76
+ # @return [Contrast::Api::Dtm::XxeDetails] The details of
77
+ # the XXE attack and the index of the last entity discovered
78
+ def build_details xml
62
79
  last_idx = 0
63
80
  ss = StringScanner.new(xml)
64
81
  while ss.scan_until(EXTERNAL_ENTITY_PATTERN)
@@ -70,7 +87,11 @@ module Contrast
70
87
  xxe_details.declared_entities << build_match(ss)
71
88
  xxe_details.entities_resolved << build_wrapper(entity_wrapper)
72
89
  end
73
- [xxe_details, last_idx]
90
+ # For our definition, the prolog goes from the start of the XML
91
+ # string to the end of the last entity declaration.
92
+ xxe_details.xml = Contrast::Utils::StringUtils.protobuf_safe_string(xml[0, last_idx]) if xxe_details
93
+
94
+ xxe_details
74
95
  end
75
96
 
76
97
  def build_sample context, ia_result, _url, **kwargs
@@ -18,12 +18,16 @@ module Contrast
18
18
  end
19
19
 
20
20
  def external_entity?
21
- @_external_entity ||= begin
22
- return external_id?(@system_id) if @system_id
23
- return external_id?(@public_id) if @public_id
24
-
25
- false
21
+ if @_external_entity.nil?
22
+ @_external_entity ||= if @system_id
23
+ external_id?(@system_id)
24
+ elsif @public_id
25
+ external_id?(@public_id)
26
+ else
27
+ false
28
+ end
26
29
  end
30
+ @_external_entity
27
31
  end
28
32
 
29
33
  # <!ENTITY name SYSTEM "URI">
@@ -48,7 +52,7 @@ module Contrast
48
52
  UP_DIR_LINUX = '../'
49
53
  UP_DIR_WIN = '..\\'
50
54
  # we only use this against lowercase strings, removed A-Z for speed
51
- FILE_PATTERN_WINDOWS = /^[\\\\]*[a-z]{1,3}:.*/.cs__freeze
55
+ FILE_PATTERN_WINDOWS = /^\\*[a-z]{1,3}:.*/.cs__freeze
52
56
  def external_id? entity_id
53
57
  return false unless entity_id
54
58
 
@@ -22,7 +22,7 @@ module Contrast
22
22
  # @param application_settings [Contrast::Api::Settings::ApplicationSettings]
23
23
  # those settings which the Service has relayed from TeamServer.
24
24
  def self.process application_settings
25
- return nil unless application_settings&.reactions&.any?
25
+ return unless application_settings&.reactions&.any?
26
26
 
27
27
  application_settings.reactions.each do |reaction|
28
28
  # the enums are all uppercase, we need to downcase them before attempting to log
@@ -12,6 +12,18 @@ module Contrast
12
12
  # This class acts to encapsulate information about the currently executed
13
13
  # request, making it available to the Agent for the duration of the request
14
14
  # in a standardized and normalized format which the Agent understands.
15
+ #
16
+ # @attr_reader timer [Contrast::Utils::Timer] when the context was created
17
+ # @attr_reader logging_hash [Hash] context used to log the request
18
+ # @attr_reader speedracer_input_analysis [Contrast::Api::Settings::InputAnalysis] the protect input analysis of
19
+ # sources on this request
20
+ # @attr_reader request [Contrast::Agent::Request] our wrapper around the Rack::Request for this context
21
+ # @attr_reader response [Contrast::Agent::Response] our wrapper aroudn the Rack::Response or Array for this context,
22
+ # only available after the application has finished its processing
23
+ # @attr_reader activity [Contrast::Api::Dtm::Activity] the application activity found in this request
24
+ # @attr_reader server_activity [Contrast::Api::Dtm::ServerActivity] the server activity found in this request
25
+ # @attr_reader route [Contrast::Api::Dtm::RouteCoverage] the route, used for findings, of this request
26
+ # @attr_reader observed_route [Contrast::Api::Dtm::ObservedRoute] the route, used for coverage, of this request
15
27
  class RequestContext
16
28
  include Contrast::Components::Interface
17
29
  access_component :agent, :analysis, :logging, :scope
@@ -118,16 +118,16 @@ module Contrast
118
118
  # holds, wraps, or is the body of the Response
119
119
  # @return [nil, String] the content of the body
120
120
  def extract_body body
121
- return nil unless body
121
+ return unless body
122
122
 
123
123
  if defined?(Rack::File) && body.is_a?(Rack::File)
124
124
  # not sure what to do in this situation, so don't do anything.
125
125
  nil
126
126
  elsif body.is_a?(Rack::BodyProxy)
127
127
  handle_rack_body_proxy(body)
128
- elsif defined?(ActionDispatch::Response::RackBody) && body.is_a?(ActionDispatch::Response::RackBody)
129
- extract_body(body.body)
130
- elsif body.is_a?(Rack::Response)
128
+ elsif (defined?(ActionDispatch::Response::RackBody) && body.is_a?(ActionDispatch::Response::RackBody)) ||
129
+ body.is_a?(Rack::Response)
130
+
131
131
  extract_body(body.body)
132
132
  elsif Contrast::Utils::DuckUtils.quacks_to?(body, :each)
133
133
  acc = []
@@ -152,7 +152,7 @@ module Contrast
152
152
  end
153
153
 
154
154
  def read_or_string obj
155
- return nil unless obj
155
+ return unless obj
156
156
 
157
157
  if Contrast::Utils::DuckUtils.quacks_to?(obj, :read)
158
158
  tmp = obj.read
@@ -96,11 +96,11 @@ module Contrast
96
96
  return unless method_exists?(clazz, method_name, type)
97
97
 
98
98
  method_instance = method_instance(clazz, method_name, type)
99
- return nil if method_instance.nil?
99
+ return if method_instance.nil?
100
100
 
101
101
  location = method_instance.source_location
102
- return nil unless location_available?(location)
103
- return nil if opener.written_from_location?(location)
102
+ return unless location_available?(location)
103
+ return if opener.written_from_location?(location)
104
104
 
105
105
  opener.written_from_location!(location)
106
106
  opener.source_code(location, method_name)
@@ -15,29 +15,40 @@ module Contrast
15
15
  # Instead, we should say "If I'm already doing Contrast things, don't track
16
16
  # this"
17
17
  class Scope
18
- SCOPE_LIST = %i[contrast deserialization].cs__freeze
18
+ SCOPE_LIST = %i[contrast deserialization split].cs__freeze
19
19
 
20
20
  def initialize
21
- instance_variable_set(:@contrast_scope, 0)
22
- instance_variable_set(:@deserialization_scope, 0)
21
+ @contrast_scope = 0
22
+ @deserialization_scope = 0
23
+ @split_scope = 0
23
24
  end
24
25
 
25
26
  def in_contrast_scope?
26
- instance_variable_get(:@contrast_scope).positive?
27
+ @contrast_scope.positive?
27
28
  end
28
29
 
29
30
  def in_deserialization_scope?
30
- instance_variable_get(:@deserialization_scope).positive?
31
+ @deserialization_scope.positive?
32
+ end
33
+
34
+ def in_split_scope?
35
+ @split_scope.positive?
31
36
  end
32
37
 
33
38
  def enter_contrast_scope!
34
- level = instance_variable_get(:@contrast_scope)
35
- instance_variable_set(:@contrast_scope, level + 1)
39
+ @contrast_scope += 1
36
40
  end
37
41
 
38
42
  def enter_deserialization_scope!
39
- level = instance_variable_get(:@deserialization_scope)
40
- instance_variable_set(:@deserialization_scope, level + 1)
43
+ @deserialization_scope += 1
44
+ end
45
+
46
+ def enter_split_scope!
47
+ @split_scope += 1
48
+ end
49
+
50
+ def split_scope_depth
51
+ @split_scope
41
52
  end
42
53
 
43
54
  # Scope Exits...
@@ -61,13 +72,15 @@ module Contrast
61
72
  # exit = 1
62
73
  # scope = 1
63
74
  def exit_contrast_scope!
64
- level = instance_variable_get(:@contrast_scope)
65
- instance_variable_set(:@contrast_scope, level - 1)
75
+ @contrast_scope -= 1
66
76
  end
67
77
 
68
78
  def exit_deserialization_scope!
69
- level = instance_variable_get(:@deserialization_scope)
70
- instance_variable_set(:@deserialization_scope, level - 1)
79
+ @deserialization_scope -= 1
80
+ end
81
+
82
+ def exit_split_scope!
83
+ @split_scope -= 1
71
84
  end
72
85
 
73
86
  def with_contrast_scope
@@ -84,6 +97,13 @@ module Contrast
84
97
  exit_deserialization_scope!
85
98
  end
86
99
 
100
+ def with_split_scope
101
+ enter_split_scope!
102
+ yield
103
+ ensure
104
+ exit_split_scope!
105
+ end
106
+
87
107
  # Dynamic versions of the above.
88
108
  # These are equivalent, but they're slower and riskier.
89
109
  # Prefer the static methods if you know what scope you need at the call site.
@@ -17,14 +17,9 @@ module Contrast
17
17
  # report the already-loaded gems.
18
18
  def catchup
19
19
  @_catchup ||= begin
20
- with_contrast_scope do
21
- Contrast::Agent::Inventory::DependencyUsageAnalysis.instance.catchup
22
- send_inventory_message
23
- true
24
- end
20
+ threaded_analysis!
21
+ true
25
22
  end
26
- rescue StandardError => e
27
- logger.warn('Unable to run post-initialization static analysis', e)
28
23
  end
29
24
 
30
25
  def send_inventory_message
@@ -35,6 +30,17 @@ module Contrast
35
30
  Contrast::Utils::InventoryUtil.append_db_config(app_update_msg)
36
31
  Contrast::Agent.messaging_queue.send_event_eventually(app_update_msg)
37
32
  end
33
+
34
+ private
35
+
36
+ def threaded_analysis!
37
+ Contrast::Agent::Thread.new do
38
+ Contrast::Agent::Inventory::DependencyUsageAnalysis.instance.catchup
39
+ send_inventory_message
40
+ rescue StandardError => e
41
+ logger.warn('Unable to run post-initialization static analysis', e)
42
+ end
43
+ end
38
44
  end
39
45
  end
40
46
  end