contrast-agent 4.7.0 → 4.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -1
  3. data/.rspec +0 -1
  4. data/.rspec_parallel +6 -0
  5. data/.simplecov +1 -0
  6. data/ext/cs__contrast_patch/cs__contrast_patch.c +0 -1
  7. data/ext/cs__contrast_patch/cs__contrast_patch.h +0 -2
  8. data/lib/contrast/agent/assess/contrast_event.rb +1 -5
  9. data/lib/contrast/agent/assess/finalizers/hash.rb +2 -5
  10. data/lib/contrast/agent/assess/policy/patcher.rb +5 -4
  11. data/lib/contrast/agent/assess/policy/policy.rb +1 -1
  12. data/lib/contrast/agent/assess/policy/policy_scanner.rb +2 -6
  13. data/lib/contrast/agent/assess/policy/preshift.rb +11 -8
  14. data/lib/contrast/agent/assess/policy/propagation_method.rb +102 -59
  15. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -7
  16. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +31 -11
  17. data/lib/contrast/agent/assess/policy/propagator/rack_protection.rb +73 -0
  18. data/lib/contrast/agent/assess/policy/propagator/split.rb +10 -6
  19. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +3 -3
  20. data/lib/contrast/agent/assess/policy/propagator.rb +1 -0
  21. data/lib/contrast/agent/assess/policy/rewriter_patch.rb +6 -7
  22. data/lib/contrast/agent/assess/policy/source_method.rb +18 -22
  23. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +0 -4
  24. data/lib/contrast/agent/assess/policy/trigger_method.rb +61 -86
  25. data/lib/contrast/agent/assess/policy/trigger_node.rb +1 -1
  26. data/lib/contrast/agent/assess/property/evented.rb +2 -1
  27. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +3 -4
  28. data/lib/contrast/agent/at_exit_hook.rb +3 -3
  29. data/lib/contrast/agent/class_reopener.rb +6 -5
  30. data/lib/contrast/agent/disable_reaction.rb +4 -5
  31. data/lib/contrast/agent/exclusion_matcher.rb +2 -7
  32. data/lib/contrast/agent/inventory/database_config.rb +117 -0
  33. data/lib/contrast/agent/inventory/dependency_analysis.rb +2 -6
  34. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +8 -9
  35. data/lib/contrast/agent/inventory/policy/datastores.rb +5 -6
  36. data/lib/contrast/agent/inventory/policy/policy.rb +1 -1
  37. data/lib/contrast/agent/middleware.rb +15 -13
  38. data/lib/contrast/agent/patching/policy/after_load_patch.rb +6 -3
  39. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +21 -16
  40. data/lib/contrast/agent/patching/policy/module_policy.rb +2 -4
  41. data/lib/contrast/agent/patching/policy/patch.rb +13 -8
  42. data/lib/contrast/agent/patching/policy/patch_status.rb +3 -7
  43. data/lib/contrast/agent/patching/policy/patcher.rb +14 -14
  44. data/lib/contrast/agent/patching/policy/policy.rb +2 -4
  45. data/lib/contrast/agent/patching/policy/policy_node.rb +2 -3
  46. data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +1 -1
  47. data/lib/contrast/agent/protect/policy/policy.rb +1 -1
  48. data/lib/contrast/agent/protect/policy/rule_applicator.rb +3 -5
  49. data/lib/contrast/agent/protect/rule/base.rb +10 -10
  50. data/lib/contrast/agent/protect/rule/cmd_injection.rb +4 -5
  51. data/lib/contrast/agent/protect/rule/no_sqli.rb +7 -53
  52. data/lib/contrast/agent/protect/rule/path_traversal.rb +1 -5
  53. data/lib/contrast/agent/protect/rule/sql_sample_builder.rb +137 -0
  54. data/lib/contrast/agent/protect/rule/sqli.rb +7 -70
  55. data/lib/contrast/agent/reaction_processor.rb +3 -4
  56. data/lib/contrast/agent/request.rb +9 -5
  57. data/lib/contrast/agent/request_context.rb +28 -31
  58. data/lib/contrast/agent/request_handler.rb +5 -3
  59. data/lib/contrast/agent/response.rb +2 -3
  60. data/lib/contrast/agent/rewriter.rb +4 -3
  61. data/lib/contrast/agent/rule_set.rb +5 -4
  62. data/lib/contrast/agent/service_heartbeat.rb +2 -3
  63. data/lib/contrast/agent/static_analysis.rb +7 -6
  64. data/lib/contrast/agent/thread.rb +2 -4
  65. data/lib/contrast/agent/thread_watcher.rb +3 -4
  66. data/lib/contrast/agent/tracepoint_hook.rb +10 -5
  67. data/lib/contrast/agent/version.rb +1 -1
  68. data/lib/contrast/api/communication/messaging_queue.rb +16 -11
  69. data/lib/contrast/api/communication/response_processor.rb +11 -11
  70. data/lib/contrast/api/communication/service_lifecycle.rb +9 -5
  71. data/lib/contrast/api/communication/socket_client.rb +18 -14
  72. data/lib/contrast/api/communication/speedracer.rb +5 -6
  73. data/lib/contrast/api/decorators/address.rb +2 -3
  74. data/lib/contrast/api/decorators/agent_startup.rb +7 -9
  75. data/lib/contrast/api/decorators/application_startup.rb +9 -10
  76. data/lib/contrast/api/decorators/application_update.rb +0 -4
  77. data/lib/contrast/api/decorators/http_request.rb +3 -7
  78. data/lib/contrast/api/decorators/instrumentation_mode.rb +3 -5
  79. data/lib/contrast/api/decorators/message.rb +7 -7
  80. data/lib/contrast/api/decorators/route_coverage.rb +24 -1
  81. data/lib/contrast/api/decorators/trace_event_object.rb +2 -3
  82. data/lib/contrast/components/agent.rb +13 -15
  83. data/lib/contrast/components/app_context.rb +7 -11
  84. data/lib/contrast/components/assess.rb +19 -16
  85. data/lib/contrast/components/base.rb +40 -0
  86. data/lib/contrast/components/config.rb +1 -2
  87. data/lib/contrast/components/contrast_service.rb +8 -11
  88. data/lib/contrast/components/heap_dump.rb +5 -4
  89. data/lib/contrast/components/inventory.rb +2 -7
  90. data/lib/contrast/components/logger.rb +14 -10
  91. data/lib/contrast/components/protect.rb +10 -13
  92. data/lib/contrast/components/sampling.rb +5 -5
  93. data/lib/contrast/components/scope.rb +9 -32
  94. data/lib/contrast/components/settings.rb +1 -5
  95. data/lib/contrast/config/base_configuration.rb +14 -6
  96. data/lib/contrast/configuration.rb +22 -19
  97. data/lib/contrast/extension/assess/array.rb +3 -15
  98. data/lib/contrast/extension/assess/eval_trigger.rb +2 -23
  99. data/lib/contrast/extension/assess/fiber.rb +6 -16
  100. data/lib/contrast/extension/assess/hash.rb +3 -13
  101. data/lib/contrast/extension/assess/kernel.rb +3 -14
  102. data/lib/contrast/extension/assess/marshal.rb +6 -14
  103. data/lib/contrast/extension/assess/regexp.rb +5 -15
  104. data/lib/contrast/extension/assess/string.rb +6 -31
  105. data/lib/contrast/extension/extension.rb +61 -0
  106. data/lib/contrast/extension/kernel.rb +2 -4
  107. data/lib/contrast/extension/protect/kernel.rb +0 -15
  108. data/lib/contrast/framework/grape/support.rb +174 -0
  109. data/lib/contrast/framework/manager.rb +44 -9
  110. data/lib/contrast/framework/rack/patch/session_cookie.rb +6 -6
  111. data/lib/contrast/framework/rack/support.rb +1 -1
  112. data/lib/contrast/framework/rails/patch/assess_configuration.rb +5 -8
  113. data/lib/contrast/framework/rails/patch/support.rb +44 -37
  114. data/lib/contrast/framework/rails/railtie.rb +34 -0
  115. data/lib/contrast/framework/rails/rewrite/active_record_named.rb +4 -4
  116. data/lib/contrast/framework/rails/support.rb +60 -13
  117. data/lib/contrast/framework/sinatra/support.rb +1 -1
  118. data/lib/contrast/funchook/funchook.rb +4 -3
  119. data/lib/contrast/logger/application.rb +1 -6
  120. data/lib/contrast/logger/log.rb +103 -13
  121. data/lib/contrast/logger/request.rb +0 -4
  122. data/lib/contrast/tasks/config.rb +0 -1
  123. data/lib/contrast/tasks/service.rb +1 -6
  124. data/lib/contrast/utils/assess/sampling_util.rb +2 -3
  125. data/lib/contrast/utils/assess/tracking_util.rb +2 -4
  126. data/lib/contrast/utils/heap_dump_util.rb +5 -3
  127. data/lib/contrast/utils/invalid_configuration_util.rb +4 -3
  128. data/lib/contrast/utils/io_util.rb +3 -5
  129. data/lib/contrast/utils/job_servers_running.rb +4 -3
  130. data/lib/contrast/utils/os.rb +2 -3
  131. data/lib/contrast/utils/ruby_ast_rewriter.rb +16 -13
  132. data/lib/contrast/utils/string_utils.rb +2 -3
  133. data/lib/contrast/utils/tag_util.rb +26 -19
  134. data/lib/contrast.rb +24 -14
  135. data/resources/assess/policy.json +252 -2
  136. data/resources/deadzone/policy.json +10 -0
  137. data/ruby-agent.gemspec +14 -3
  138. data/service_executables/VERSION +1 -1
  139. data/service_executables/linux/contrast-service +0 -0
  140. data/service_executables/mac/contrast-service +0 -0
  141. metadata +104 -24
  142. data/lib/contrast/agent/railtie.rb +0 -31
  143. data/lib/contrast/components/interface.rb +0 -196
  144. data/lib/contrast/delegators/input_analysis.rb +0 -12
  145. data/lib/contrast/utils/inventory_util.rb +0 -114
@@ -2,7 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/agent/assess/policy/propagation_node'
5
- require 'contrast/components/interface'
5
+ require 'contrast/components/logger'
6
+ require 'contrast/components/scope'
6
7
 
7
8
  module Contrast
8
9
  module Extension
@@ -11,10 +12,9 @@ module Contrast
11
12
  # methods which are too complex to fit into one of the standard
12
13
  # Contrast::Agent::Assess::Policy::Propagator molds without cluttering up the
13
14
  # String Class or exposing our methods there.
14
- class StringPropagator
15
- include Contrast::Components::Interface
16
-
17
- access_component :agent, :analysis, :logging, :scope
15
+ class StringPropagator # rubocop:disable Style/StaticClass
16
+ extend Contrast::Components::Logger::InstanceMethods
17
+ extend Contrast::Components::Scope::InstanceMethods
18
18
 
19
19
  NODE_HASH = {
20
20
  'class_name' => 'String',
@@ -31,7 +31,7 @@ module Contrast
31
31
 
32
32
  class << self
33
33
  def track_interpolation inputs, result
34
- return unless AGENT.interpolation_enabled?
34
+ return unless ::Contrast::AGENT.interpolation_enabled?
35
35
  return if in_contrast_scope?
36
36
  return unless inputs.any? { |input| Contrast::Agent::Assess::Tracker.tracked?(input) }
37
37
 
@@ -52,31 +52,6 @@ module Contrast
52
52
  rescue StandardError => e
53
53
  logger.error('Unable to track interpolation', e)
54
54
  end
55
-
56
- def instrument_string
57
- @_instrument_string ||= begin
58
- require 'cs__assess_string/cs__assess_string'
59
- true
60
- end
61
- rescue StandardError, LoadError => e
62
- logger.error('Error loading hash track patch', e)
63
- false
64
- end
65
-
66
- def instrument_string_interpolation
67
- if @_instrument_string_interpolation.nil?
68
- @_instrument_string_interpolation = begin
69
- if AGENT.patch_interpolation? && Funchook.available?
70
- require 'cs__assess_string_interpolation26/cs__assess_string_interpolation26'
71
- end
72
- true
73
- rescue StandardError, LoadError => e
74
- logger.error('Error loading interpolation patch', e)
75
- false
76
- end
77
- end
78
- @_instrument_string_interpolation
79
- end
80
55
  end
81
56
  end
82
57
  end
@@ -0,0 +1,61 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/components/logger'
5
+
6
+ module Contrast
7
+ module Extension
8
+ # Our top level Assess namespace in the Core Extension section of our
9
+ # code. These patches are those that are invoked directly from a patched
10
+ # Class.
11
+ #
12
+ module Assess
13
+ # This is the main instrument helper giving the method of requiring C patches
14
+ #
15
+ module InstrumentHelper
16
+ class << self
17
+ include Contrast::Components::Logger::InstanceMethods
18
+
19
+ # Unites the different require methods into one, using only
20
+ # the provided path for the C patches
21
+ # parameters
22
+ # @param path[String] Path to the required patch
23
+ #
24
+ def instrument path
25
+ var_name, extracted_name = gen_name(path)
26
+ return if instance_variable_get(var_name) == true
27
+
28
+ instance_variable_set(var_name, assign_value(path))
29
+ rescue StandardError, LoadError => e
30
+ logger.error("Error loading #{ extracted_name&.nil? ? '' : extracted_name } patch", e)
31
+ false
32
+ end
33
+
34
+ # Some of the requires have some extra conditions for them to require
35
+ # the C patches, so this method is helping us move the logic by making some
36
+ # conditions
37
+ def assign_value path
38
+ case path
39
+ when /fiber/
40
+ require path if Funchook.available?
41
+ when /interpolation26/
42
+ require path if ::Contrast::AGENT.patch_interpolation? && Funchook.available?
43
+ else
44
+ require path
45
+ end
46
+ true
47
+ end
48
+
49
+ # Generate the needed instance variable name and return the extracted name
50
+ def gen_name path
51
+ extracted_name = path.split(%r{[\s_/]})&.uniq&.delete_if do |s|
52
+ s.empty? || s == 'cs' || s == 'assess' || s == 'track'
53
+ end
54
+ extracted_name = (extracted_name&.length || 0) > 1 ? extracted_name&.join('_') : extracted_name&.pop
55
+ ["@_instrument_#{ extracted_name }_track", extracted_name]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -41,15 +41,13 @@ module Kernel # :nodoc:
41
41
 
42
42
  def catch *args, &block
43
43
  # Save current scope level
44
- scope_level =
45
- Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.instance_variable_get(:@contrast_scope)
44
+ scope_level = ::Contrast::SCOPE.scope_for_current_ec.instance_variable_get(:@contrast_scope)
46
45
 
47
46
  # Run original catch with block.
48
47
  retval = cs__catch(*args, &block)
49
48
 
50
49
  # Restore scope.
51
- Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.instance_variable_set(:@contrast_scope,
52
- scope_level)
50
+ ::Contrast::SCOPE.scope_for_current_ec.instance_variable_set(:@contrast_scope, scope_level)
53
51
 
54
52
  retval
55
53
  end
@@ -1,8 +1,6 @@
1
1
  # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'contrast/components/interface'
5
-
6
4
  module Contrast
7
5
  module Extension
8
6
  module Protect
@@ -10,9 +8,6 @@ module Contrast
10
8
  # allowing us to track activity as it crosses spawned processes.
11
9
  module Kernel
12
10
  class << self
13
- include Contrast::Components::Interface
14
- access_component :contrast_service
15
-
16
11
  def build_wrapper
17
12
  lambda {
18
13
  proc_start
@@ -27,16 +22,6 @@ module Contrast
27
22
 
28
23
  context.reset_activity
29
24
  end
30
-
31
- def instrument
32
- @_instrument ||= begin
33
- require 'cs__protect_kernel/cs__protect_kernel'
34
- true
35
- end
36
- rescue StandardError, LoadError => e
37
- logger.error('Error loading kernel protect patch', e)
38
- false
39
- end
40
25
  end
41
26
  end
42
27
  end
@@ -0,0 +1,174 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/framework/base_support'
5
+ require 'contrast/components/logger'
6
+
7
+ module Contrast
8
+ module Framework
9
+ module Grape
10
+ # Used when Grape is present to define framework specific behaviour
11
+ class Support
12
+ extend Contrast::Framework::BaseSupport
13
+ class << self
14
+ include Contrast::Components::Logger::InstanceMethods
15
+ def detection_class
16
+ 'Grape::API'
17
+ end
18
+
19
+ def version
20
+ ::Grape::VERSION
21
+ end
22
+
23
+ def application_name
24
+ app_class&.cs__name
25
+ end
26
+
27
+ def application_root
28
+ app_instance&.root
29
+ end
30
+
31
+ def server_type
32
+ 'grape'
33
+ end
34
+
35
+ # Given an object, determine if it is a Grape controller.
36
+ # Which could include cases of ::Grape::API subclass or actual class
37
+ #
38
+ # @param app [Object] suspected Grape app.
39
+ # @return [Boolean]
40
+ def grape_controller? app
41
+ # Grape is loaded?
42
+ return false unless grape_defined?
43
+
44
+ # App is a subclass of or actually is ::Grape::API.
45
+ return false unless app.cs__respond_to?(:<=) && app <= ::Grape::API
46
+
47
+ true
48
+ end
49
+
50
+ # Find all classes that subclass ::Grape::API, Gather their routes
51
+ #
52
+ # @return [Array<Contrast::Api::Dtm::RouteCoverage>, Array]- founded routes as Dtms
53
+ def collect_routes
54
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless grape_defined?
55
+
56
+ # Each Grape controller has endpoints and each endpoints has routes
57
+ # and that's why we need to go through each one and create separate RouteCoverage object
58
+ routes = []
59
+ grape_controllers.each do |c|
60
+ c&.endpoints&.each do |endpoint|
61
+ endpoint&.routes&.map do |r|
62
+ pattern = r.pattern.pattern
63
+ temp = Contrast::Api::Dtm::RouteCoverage.from_grape_controller(c, r.request_method, pattern, r.path)
64
+ routes << temp
65
+ end
66
+ end
67
+ end
68
+ routes
69
+ end
70
+
71
+ # Given the current request return a RouteCoverage dtm.
72
+ #
73
+ # @param request [Contrast::Agent::Request] a contrast tracked request.
74
+ # @param controller [::Grape::API] optionally use this controller instead of global ::Grape::API.
75
+ # @return [Contrast::Api::Dtm::RouteCoverage, nil] a Dtm describing the route
76
+ # matched to the request if a match was found.
77
+ def current_route request, controller = ::Grape::API, full_route = nil
78
+ return unless grape_controller?(controller)
79
+
80
+ method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc...
81
+
82
+ # Find final controller - actually we gotta match the route to the scanned application
83
+ # Initially Grape compiles all routes on startup, so we can use the url from the request
84
+ # and create the observed route
85
+ # Class < Grape::API, Grape::Router::Route
86
+ final_controller, route_pattern = _route_recurse(method, _cleaned_route(request), grape_controllers)
87
+ return unless final_controller
88
+
89
+ full_route ||= request.env[::Rack::PATH_INFO]
90
+
91
+ Contrast::Api::Dtm::RouteCoverage.from_grape_controller(final_controller, method, route_pattern, full_route)
92
+ end
93
+
94
+ # Search object space for grape controllers--any class that subclasses ::Grape::API.
95
+ #
96
+ # @return [Array<::Grape::API>] grape controllers
97
+ def grape_controllers
98
+ ObjectSpace.each_object(Class).select { |klass| klass < ::Grape::API }
99
+ end
100
+
101
+ # Grape Request inherits the same as the Sinatra, so we can easily call it as it's called in Sinatra
102
+ def retrieve_request env
103
+ ::Grape::Request.new(env)
104
+ end
105
+
106
+ private
107
+
108
+ # Determine if, at the time of our Framework Support determination, Grape has been defined.
109
+ #
110
+ # @return [Boolean]
111
+ def grape_defined?
112
+ @_grape_defined = !!(defined?(::Grape) && defined?(::Grape::API)) if @_grape_defined.nil?
113
+ @_grape_defined
114
+ end
115
+
116
+ # @param method [::Rack::REQUEST_METHOD] GET, POST, PUT, etc...
117
+ # @param route [String] the relative route passed from Rack.
118
+ # @param controllers [Array<::Grape::API>] All Grape controllers found
119
+ # @return [Array[::Grape::API]], nil] Either the controller that
120
+ # will handle the route along with the route pattern or nil if no match.
121
+ def _route_recurse method, route, controllers = grape_controllers
122
+ # return if there aren't any controllers
123
+ return unless controllers&.any?
124
+
125
+ # Here we can go through the all detected controllers
126
+ # and find the one that's routes include the current one
127
+ # Grape controller actually has endpoints and each endpoint
128
+ # has routes and that's why we need to do it that way
129
+ controller = controllers.pop
130
+ return _route_recurse method, route, controllers unless controller
131
+
132
+ contr_routes = controller.endpoints&.map(&:routes)&.flatten || []
133
+ route_pattern = contr_routes&.find do |r|
134
+ r.pattern.to_regexp.match(route) # ::Mustermann::Grape match
135
+ end
136
+ return controller, route_pattern unless route_pattern.nil?
137
+
138
+ _route_recurse method, route, controllers
139
+ end
140
+
141
+ # Get route and do some cleaning
142
+ #
143
+ # @param request [Contrast::Agent::Request] a contrast tracked request.
144
+ # @return [String] the extracted and cleaned relative route.
145
+ def _cleaned_route request
146
+ route = request.env[::Rack::PATH_INFO]
147
+ return '/' if route.empty?
148
+
149
+ route.end_with?('/') ? route[0..-2] : route
150
+ end
151
+
152
+ def app_class
153
+ return unless grape_defined?
154
+
155
+ app_instance.cs__class
156
+ end
157
+
158
+ # Search the object space for the controller handling this request which will be
159
+ # the class inheriting from ::Grape::API with @app=nil
160
+ #
161
+ # @return [::Grape::API] the current controller as routed by Rack.
162
+ def app_instance
163
+ return unless grape_defined?
164
+
165
+ @_app_instance ||= begin
166
+ grape_layers = ObjectSpace.each_object(::Grape::API).to_a
167
+ grape_layers.find { |layer| layer.app.nil? }
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -1,26 +1,27 @@
1
1
  # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'contrast/components/logger'
5
+ require 'contrast/extension/module'
4
6
  require 'contrast/framework/platform_version'
5
7
  require 'contrast/framework/rack/support'
6
8
  require 'contrast/framework/rails/support'
9
+ require 'contrast/framework/grape/support'
7
10
  require 'contrast/framework/sinatra/support'
8
- require 'contrast/components/interface'
9
11
  require 'contrast/utils/class_util'
10
12
 
11
13
  module Contrast
12
14
  module Framework
13
15
  # Allows access to framework specific information
14
16
  class Manager
15
- include Contrast::Components::Interface
16
- access_component :analysis, :logging
17
+ include Contrast::Components::Logger::InstanceMethods
17
18
 
18
19
  # Order here does matter as the first framework listed will be the first one we pull information from
19
20
  # Rack will be a special case that may involve updating some logic to handle only applying Rack if Rails/Sinatra
20
21
  # do not exist
21
22
  SUPPORTED_FRAMEWORKS = [
22
23
  Contrast::Framework::Rails::Support, Contrast::Framework::Sinatra::Support,
23
- Contrast::Framework::Rack::Support
24
+ Contrast::Framework::Grape::Support, Contrast::Framework::Rack::Support
24
25
  ].cs__freeze
25
26
 
26
27
  def initialize
@@ -78,15 +79,24 @@ module Contrast
78
79
  found || ::Rack::Directory.new('').root
79
80
  end
80
81
 
81
- # If we have 0 or n > 1 frameworks, we need to use the default rack request
82
+ # Build a request from the provided env, based on the framework(s) we're currently supporting.
82
83
  #
83
84
  # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values
84
85
  # of this particular Request
85
86
  # @return [::Rack::Request] either a rack request or subclass thereof.
86
87
  def retrieve_request env
88
+ # If we're mounted on Rails, use Rails.
89
+ if @_frameworks.include?(Contrast::Framework::Rails::Support)
90
+ return Contrast::Framework::Rails::Support.retrieve_request(env)
91
+ end
92
+
93
+ # If we know the framework, use it.
87
94
  return @_frameworks[0].retrieve_request(env) if @_frameworks.length == 1
88
95
 
96
+ # Fall back on a regular Rack::Request
89
97
  ::Rack::Request.new(env)
98
+ rescue StandardError => e
99
+ logger.warn('Unable to retrieve_request', e)
90
100
  end
91
101
 
92
102
  # @param env [Hash] the various variables stored by this and other Middlewares to know the state
@@ -110,6 +120,34 @@ module Contrast
110
120
  @_frameworks.lazy.map { |framework_support| framework_support.current_route(request) }.reject(&:nil?).first
111
121
  end
112
122
 
123
+ # Sometimes the framework we want to instrument is loaded after our agent code. To catch that case, we'll detect
124
+ # if the loaded_module is the marker class for any of our supported frameworks. If it is, and we don't already
125
+ # have support enabled, we'll enable it now. We'll also need to catch up on any other startup actions that we've
126
+ # missed. Most likely, this is only necessary for those applications which have applications mounted on them.
127
+ #
128
+ # @param mod [Module] the module or class that was just loaded
129
+ def register_late_framework mod
130
+ return unless mod
131
+
132
+ module_name = mod.cs__name
133
+ # Otherwise, check if the provided module_name requires us to register a new support
134
+ SUPPORTED_FRAMEWORKS.each do |framework|
135
+ next if @_frameworks.include?(framework)
136
+ next unless module_name == framework.detection_class
137
+
138
+ @_frameworks << framework
139
+ # Report the registered routes of that framework now that we know we need to find them
140
+ app_update_msg = Contrast::Api::Dtm::ApplicationUpdate.build
141
+ Contrast::Agent.messaging_queue.send_event_eventually(app_update_msg)
142
+ logger.info('Framework detected after initialization. Enabling support.',
143
+ framework: framework.detection_class,
144
+ frameworks: @_frameworks)
145
+ break
146
+ end
147
+ rescue StandardError => e
148
+ logger.warn('Unable to register a late framework', e, module: mod.cs__name)
149
+ end
150
+
113
151
  private
114
152
 
115
153
  def enable_framework_support? klass
@@ -125,10 +163,7 @@ module Contrast
125
163
  # @param method_name [Symbol] the method to call on each FrameworkSupport class
126
164
  # @return [Array]
127
165
  def data_for_all_frameworks method_name
128
- data = @_frameworks.flat_map do |framework|
129
- framework.send(method_name)
130
- end
131
- data.compact
166
+ @_frameworks.flat_map { |framework| framework.send(method_name) }.compact
132
167
  end
133
168
 
134
169
  # This returns a single object from the first framework to successfully respond