contrast-agent 4.9.0 → 4.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.rspec_parallel +6 -0
  4. data/ext/cs__common/cs__common.c +19 -7
  5. data/ext/cs__common/cs__common.h +4 -2
  6. data/ext/cs__contrast_patch/cs__contrast_patch.c +32 -11
  7. data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -4
  8. data/lib/contrast/agent/assess/contrast_event.rb +1 -2
  9. data/lib/contrast/agent/assess/contrast_object.rb +1 -4
  10. data/lib/contrast/agent/assess/finalizers/hash.rb +0 -1
  11. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
  12. data/lib/contrast/agent/assess/policy/patcher.rb +0 -1
  13. data/lib/contrast/agent/assess/policy/policy_scanner.rb +0 -2
  14. data/lib/contrast/agent/assess/policy/preshift.rb +29 -12
  15. data/lib/contrast/agent/assess/policy/propagation_method.rb +100 -57
  16. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -2
  17. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +31 -11
  18. data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
  19. data/lib/contrast/agent/assess/policy/propagator/split.rb +3 -2
  20. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +1 -0
  21. data/lib/contrast/agent/assess/policy/rewriter_patch.rb +0 -1
  22. data/lib/contrast/agent/assess/policy/source_method.rb +13 -17
  23. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +0 -1
  24. data/lib/contrast/agent/assess/policy/trigger_method.rb +60 -85
  25. data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
  26. data/lib/contrast/agent/assess/property/evented.rb +2 -1
  27. data/lib/contrast/agent/assess/property/tagged.rb +34 -25
  28. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +0 -1
  29. data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
  30. data/lib/contrast/agent/disable_reaction.rb +1 -1
  31. data/lib/contrast/agent/exclusion_matcher.rb +0 -4
  32. data/lib/contrast/agent/inventory/database_config.rb +117 -0
  33. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +6 -5
  34. data/lib/contrast/agent/inventory/policy/datastores.rb +2 -2
  35. data/lib/contrast/agent/middleware.rb +1 -0
  36. data/lib/contrast/agent/patching/policy/after_load_patch.rb +3 -0
  37. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +17 -12
  38. data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
  39. data/lib/contrast/agent/patching/policy/module_policy.rb +2 -4
  40. data/lib/contrast/agent/patching/policy/patch.rb +17 -6
  41. data/lib/contrast/agent/patching/policy/patch_status.rb +3 -7
  42. data/lib/contrast/agent/patching/policy/patcher.rb +9 -9
  43. data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +1 -1
  44. data/lib/contrast/agent/protect/rule/no_sqli.rb +7 -53
  45. data/lib/contrast/agent/protect/rule/sql_sample_builder.rb +137 -0
  46. data/lib/contrast/agent/protect/rule/sqli.rb +7 -70
  47. data/lib/contrast/agent/reaction_processor.rb +1 -1
  48. data/lib/contrast/agent/request.rb +9 -4
  49. data/lib/contrast/agent/request_context.rb +51 -33
  50. data/lib/contrast/agent/rule_set.rb +2 -4
  51. data/lib/contrast/agent/scope.rb +32 -20
  52. data/lib/contrast/agent/static_analysis.rb +1 -1
  53. data/lib/contrast/agent/tracepoint_hook.rb +16 -3
  54. data/lib/contrast/agent/version.rb +1 -1
  55. data/lib/contrast/agent.rb +0 -1
  56. data/lib/contrast/api/communication/messaging_queue.rb +12 -6
  57. data/lib/contrast/api/communication/service_lifecycle.rb +4 -1
  58. data/lib/contrast/api/communication/socket_client.rb +4 -4
  59. data/lib/contrast/api/decorators/agent_startup.rb +4 -4
  60. data/lib/contrast/api/decorators/application_startup.rb +6 -5
  61. data/lib/contrast/api/decorators/route_coverage.rb +24 -1
  62. data/lib/contrast/components/agent.rb +5 -2
  63. data/lib/contrast/components/assess.rb +13 -3
  64. data/lib/contrast/components/base.rb +2 -2
  65. data/lib/contrast/components/config.rb +1 -0
  66. data/lib/contrast/components/contrast_service.rb +4 -2
  67. data/lib/contrast/components/logger.rb +13 -8
  68. data/lib/contrast/components/scope.rb +9 -28
  69. data/lib/contrast/config/assess_configuration.rb +1 -0
  70. data/lib/contrast/config/base_configuration.rb +14 -6
  71. data/lib/contrast/configuration.rb +19 -15
  72. data/lib/contrast/extension/assess/array.rb +1 -11
  73. data/lib/contrast/extension/assess/eval_trigger.rb +0 -20
  74. data/lib/contrast/extension/assess/fiber.rb +0 -11
  75. data/lib/contrast/extension/assess/hash.rb +0 -10
  76. data/lib/contrast/extension/assess/kernel.rb +1 -10
  77. data/lib/contrast/extension/assess/marshal.rb +3 -11
  78. data/lib/contrast/extension/assess/regexp.rb +0 -11
  79. data/lib/contrast/extension/assess/string.rb +1 -26
  80. data/lib/contrast/extension/extension.rb +61 -0
  81. data/lib/contrast/framework/grape/support.rb +174 -0
  82. data/lib/contrast/framework/manager.rb +42 -6
  83. data/lib/contrast/framework/rack/support.rb +1 -1
  84. data/lib/contrast/framework/rails/patch/assess_configuration.rb +0 -1
  85. data/lib/contrast/framework/rails/patch/support.rb +6 -3
  86. data/lib/contrast/framework/rails/railtie.rb +1 -1
  87. data/lib/contrast/framework/rails/rewrite/active_record_named.rb +1 -0
  88. data/lib/contrast/framework/rails/support.rb +60 -13
  89. data/lib/contrast/framework/sinatra/support.rb +1 -1
  90. data/lib/contrast/logger/log.rb +89 -15
  91. data/lib/contrast/tasks/config.rb +0 -1
  92. data/lib/contrast/utils/class_util.rb +58 -44
  93. data/lib/contrast/utils/io_util.rb +43 -35
  94. data/lib/contrast/utils/lru_cache.rb +45 -0
  95. data/lib/contrast/utils/ruby_ast_rewriter.rb +16 -13
  96. data/lib/contrast/utils/tag_util.rb +2 -1
  97. data/lib/contrast.rb +1 -1
  98. data/resources/assess/policy.json +208 -7
  99. data/resources/deadzone/policy.json +91 -0
  100. data/ruby-agent.gemspec +10 -2
  101. data/service_executables/VERSION +1 -1
  102. data/service_executables/linux/contrast-service +0 -0
  103. data/service_executables/mac/contrast-service +0 -0
  104. metadata +74 -26
  105. data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
  106. data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
  107. data/ext/cs__protect_kernel/extconf.rb +0 -5
  108. data/lib/contrast/extension/protect/kernel.rb +0 -39
  109. data/lib/contrast/utils/inventory_util.rb +0 -113
@@ -16,7 +16,6 @@ module Contrast
16
16
  extend Contrast::Components::Logger::InstanceMethods
17
17
  extend Contrast::Components::Scope::InstanceMethods
18
18
 
19
-
20
19
  REGEXP_EQUAL_SQUIGGLE_HASH = {
21
20
  'id' => 'regexp_100',
22
21
  'class_name' => 'Regexp',
@@ -59,16 +58,6 @@ module Contrast
59
58
  rescue Exception => e # rubocop:disable Lint/RescueException
60
59
  logger.error('Unable to propagate during Regexp#=~', e)
61
60
  end
62
-
63
- def instrument_regexp_track
64
- @_instrument_regexp_track ||= begin
65
- require 'cs__assess_regexp/cs__assess_regexp'
66
- true
67
- end
68
- rescue StandardError, LoadError => e
69
- logger.error('Error loading regexp track patch', e)
70
- false
71
- end
72
61
  end
73
62
  end
74
63
  end
@@ -12,7 +12,7 @@ module Contrast
12
12
  # methods which are too complex to fit into one of the standard
13
13
  # Contrast::Agent::Assess::Policy::Propagator molds without cluttering up the
14
14
  # String Class or exposing our methods there.
15
- class StringPropagator
15
+ class StringPropagator # rubocop:disable Style/StaticClass
16
16
  extend Contrast::Components::Logger::InstanceMethods
17
17
  extend Contrast::Components::Scope::InstanceMethods
18
18
 
@@ -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 ::Contrast::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
@@ -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
@@ -2,9 +2,11 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/components/logger'
5
+ require 'contrast/extension/module'
5
6
  require 'contrast/framework/platform_version'
6
7
  require 'contrast/framework/rack/support'
7
8
  require 'contrast/framework/rails/support'
9
+ require 'contrast/framework/grape/support'
8
10
  require 'contrast/framework/sinatra/support'
9
11
  require 'contrast/utils/class_util'
10
12
 
@@ -19,7 +21,7 @@ module Contrast
19
21
  # do not exist
20
22
  SUPPORTED_FRAMEWORKS = [
21
23
  Contrast::Framework::Rails::Support, Contrast::Framework::Sinatra::Support,
22
- Contrast::Framework::Rack::Support
24
+ Contrast::Framework::Grape::Support, Contrast::Framework::Rack::Support
23
25
  ].cs__freeze
24
26
 
25
27
  def initialize
@@ -77,15 +79,24 @@ module Contrast
77
79
  found || ::Rack::Directory.new('').root
78
80
  end
79
81
 
80
- # 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.
81
83
  #
82
84
  # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values
83
85
  # of this particular Request
84
86
  # @return [::Rack::Request] either a rack request or subclass thereof.
85
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.
86
94
  return @_frameworks[0].retrieve_request(env) if @_frameworks.length == 1
87
95
 
96
+ # Fall back on a regular Rack::Request
88
97
  ::Rack::Request.new(env)
98
+ rescue StandardError => e
99
+ logger.warn('Unable to retrieve_request', e)
89
100
  end
90
101
 
91
102
  # @param env [Hash] the various variables stored by this and other Middlewares to know the state
@@ -109,6 +120,34 @@ module Contrast
109
120
  @_frameworks.lazy.map { |framework_support| framework_support.current_route(request) }.reject(&:nil?).first
110
121
  end
111
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
+
112
151
  private
113
152
 
114
153
  def enable_framework_support? klass
@@ -124,10 +163,7 @@ module Contrast
124
163
  # @param method_name [Symbol] the method to call on each FrameworkSupport class
125
164
  # @return [Array]
126
165
  def data_for_all_frameworks method_name
127
- data = @_frameworks.flat_map do |framework|
128
- framework.send(method_name)
129
- end
130
- data.compact
166
+ @_frameworks.flat_map { |framework| framework.send(method_name) }.compact
131
167
  end
132
168
 
133
169
  # This returns a single object from the first framework to successfully respond
@@ -14,7 +14,7 @@ module Contrast
14
14
  extend Contrast::Framework::Rack::Patch::Support
15
15
  class << self
16
16
  def detection_class
17
- 'don\'t let me be detected'
17
+ 'rack -- don\'t let me be detected'
18
18
  end
19
19
  end
20
20
  end
@@ -11,7 +11,6 @@ module Contrast
11
11
  module AssessConfiguration
12
12
  include Contrast::Components::Logger::InstanceMethods
13
13
 
14
-
15
14
  CS__SESSION_TIMEOUT_NAME = 'session-timeout'
16
15
  SAFE_SESSION_TIMEOUT = (30 * 60 * 1000)
17
16
  CS__SECURE_RULE_NAME = 'secure-flag-missing'
@@ -29,12 +29,14 @@ module Contrast
29
29
  Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
30
30
  'ActionController::Live::Buffer',
31
31
  'contrast/framework/rails/patch/action_controller_live_buffer',
32
- instrumenting_module: 'Contrast::Framework::Rails::Patch::ActionControllerLiveBuffer'),
32
+ instrumenting_module:
33
+ 'Contrast::Framework::Rails::Patch::ActionControllerLiveBuffer'),
33
34
  Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
34
35
  'Rails::Application::Configuration',
35
36
  'contrast/framework/rails/patch/rails_application_configuration',
36
37
  method_to_instrument: :session_store,
37
- instrumenting_module: 'Contrast::Framework::Rails::Patch::RailsApplicationConfiguration')
38
+ instrumenting_module:
39
+ 'Contrast::Framework::Rails::Patch::RailsApplicationConfiguration')
38
40
  ])
39
41
  if RUBY_VERSION < '2.6.0'
40
42
  patches.merge([
@@ -61,7 +63,8 @@ module Contrast
61
63
  'ActiveRecord::AttributeMethods::TimeZoneConversion::ClassMethods',
62
64
  'contrast/framework/rails/rewrite/active_record_time_zone_inherited',
63
65
  method_to_instrument: :inherited,
64
- instrumenting_module: 'Contrast::Framework::Rails::Rewrite::ActiveRecordTimeZoneInherited')
66
+ instrumenting_module:
67
+ 'Contrast::Framework::Rails::Rewrite::ActiveRecordTimeZoneInherited')
65
68
  ])
66
69
  end
67
70
  patches
@@ -12,7 +12,7 @@ module Contrast
12
12
  include Contrast::Components::Logger::InstanceMethods
13
13
 
14
14
  initializer 'Contrast Ruby Agent Initializer' do |app|
15
- log_rails = defined?(Rails) && defined?(Rails.logger)
15
+ log_rails = defined?(Rails) && defined?(Rails.logger)
16
16
 
17
17
  Rails.logger.debug("In railtie ::#{ app.middleware.inspect }") if log_rails
18
18
 
@@ -18,6 +18,7 @@ module Contrast
18
18
  # being phased out with support for those language versions.
19
19
  class ActiveRecordNamed
20
20
  include Contrast::Components::Logger::InstanceMethods
21
+ extend Contrast::Components::Logger::InstanceMethods
21
22
 
22
23
  class << self
23
24
  def rewrite mod, method_name, body
@@ -13,6 +13,8 @@ module Contrast
13
13
  class Support
14
14
  extend Contrast::Framework::BaseSupport
15
15
  extend Contrast::Framework::Rails::Patch::Support
16
+ include Contrast::Components::Logger::InstanceMethods
17
+ extend Contrast::Components::Logger::InstanceMethods
16
18
 
17
19
  class << self
18
20
  RAILS_MODULE_NAME_VERSION = Gem::Version.new('6.0.0')
@@ -49,31 +51,36 @@ module Contrast
49
51
  # Find the current route, based on the provided Request wrapper
50
52
  #
51
53
  # @param request[Contrast::Agent::Request]
52
- # @return [Contrast::Api::Dtm::RouteCoverage]
54
+ # @return [Contrast::Api::Dtm::RouteCoverage, nil]
53
55
  def current_route request
54
56
  return unless ::Rails.cs__respond_to?(:application)
55
57
 
58
+ # ActionDispatch::Journey::Path::Pattern::MatchData, Hash, ActionDispatch::Journey::Route, Array<String>
56
59
  match, _params, route, path = get_full_route(request.rack_request)
60
+ unless route
61
+ logger.warn('Unable to determine the current route of this request')
62
+ return
63
+ end
57
64
 
58
65
  original_url = request.rack_request.path_info
59
-
66
+ mounted_app = route&.app&.app
60
67
  # Route is either the final rails route, or a router that points to a Sinatra controller.
61
- if Contrast::Framework::Sinatra::Support.sinatra_controller?(route.app.app)
62
- # Create a request copied from current request, but with the base path removed from path_info.
63
- new_req = ::ActionDispatch::Request.new(request.env)
64
- new_req.path_info = new_req.path_info.gsub((path << match).join, '')
65
-
66
- return Contrast::Framework::Sinatra::Support.current_route(new_req, route.app.app, original_url)
68
+ if mounted_app && Contrast::Framework::Sinatra::Support.sinatra_controller?(mounted_app)
69
+ return mounted_sinatra_route(request, match, path, route, original_url)
70
+ end
71
+ if mounted_app && Contrast::Framework::Grape::Support.grape_controller?(mounted_app)
72
+ return mounted_grape_route(request, match, path, route, original_url)
67
73
  end
68
74
 
69
75
  Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route, original_url)
70
- rescue StandardError => _e
76
+ rescue StandardError => e
77
+ logger.warn('Unable to determine the current route of this request', e)
71
78
  nil
72
79
  end
73
80
 
74
81
  # Copy a request for modification.
75
82
  #
76
- # @param [::ActionDispatch::Request] original env.
83
+ # @param env [::ActionDispatch::Request] original env.
77
84
  # @return [::ActionDispatch::Request] a copy of original env with rails env merged.
78
85
  def retrieve_request env
79
86
  rails_env = ::Rails.application.env_config.merge(env)
@@ -91,18 +98,21 @@ module Contrast
91
98
 
92
99
  # Determine if route is a Rails engine route.
93
100
  #
94
- # @param [Object] app or route that points to a ::Rails::Engine
101
+ # @param route [Object] app or route that points to a ::Rails::Engine
95
102
  # @return [bool] whether the router is an engine or not.
96
103
  def engine_route? route
104
+ return false unless route&.app&.app
105
+
97
106
  route.app.is_a?(::ActionDispatch::Routing::Mapper::Constraints) && route.app.app < ::Rails::Engine
98
107
  end
99
108
 
100
109
  # Recursively get final route traversing engines as required.
101
110
  #
102
111
  # @param request [::Rack::Request] the rack request as will be handed to rails controller.
103
- # @param top_router [::ActionDispatch::Journer::Router] the current router relative to the previous.
112
+ # @param top_router [::ActionDispatch::Journey::Router] the current router relative to the previous.
104
113
  # @param path [Array<String>] the chunks of path that have been seen.
105
- # @return [Array<array>] the final set of rails route classes.
114
+ # @return [Array<Object>] the final set of rails route classes.
115
+ # ActionDispatch::Journey::Path::Pattern::MatchData, Hash, ActionDispatch::Journey::Route, Array<String>
106
116
  def get_full_route request, top_router = ::Rails.application.routes.router, path = []
107
117
  return if (route_matches = top_router.send(:find_routes, request)).empty?
108
118
 
@@ -132,6 +142,43 @@ module Contrast
132
142
  end
133
143
  route_list
134
144
  end
145
+
146
+ # @param request[Contrast::Agent::Request]
147
+ # @param match [ActionDispatch::Journey::Path::Pattern::MatchData]
148
+ # @param path [Array<String>] the path of this request, built out from each nested
149
+ # ActionDispatch::Journey::Path::Pattern::MatchData
150
+ # @param route [::ActionDispatch::Journey::Route]
151
+ # @param original_url [String] the full url of this request, including the mount
152
+ # @return [Contrast::Api::Dtm::RouteCoverage, nil]
153
+ def mounted_sinatra_route request, match, path, route, original_url
154
+ new_req = unmounted_route(request, match, path)
155
+ Contrast::Framework::Sinatra::Support.current_route(new_req, route.app.app, original_url)
156
+ end
157
+
158
+ # @param request[Contrast::Agent::Request]
159
+ # @param match [ActionDispatch::Journey::Path::Pattern::MatchData]
160
+ # @param path [Array<String>] the path of this request, built out from each nested
161
+ # ActionDispatch::Journey::Path::Pattern::MatchData
162
+ # @param route [::ActionDispatch::Journey::Route]
163
+ # @param original_url [String] the full url of this request, including the mount
164
+ # @return [Contrast::Api::Dtm::RouteCoverage, nil]
165
+ def mounted_grape_route request, match, path, route, original_url
166
+ new_req = unmounted_route(request, match, path)
167
+ Contrast::Framework::Grape::Support.current_route(new_req, route.app.app, original_url)
168
+ end
169
+
170
+ # Create a request copied from current request, but with the base path removed from path_info, as that's
171
+ # the mount.
172
+ #
173
+ # @param request[Contrast::Agent::Request]
174
+ # @param match []
175
+ # @param path [String] the path of this request
176
+ # @return [::ActionDispatch::Request]
177
+ def unmounted_route request, match, path
178
+ new_req = ::ActionDispatch::Request.new(request.env)
179
+ new_req.path_info = new_req.path_info.gsub((path << match).join, '')
180
+ new_req
181
+ end
135
182
  end
136
183
  end
137
184
  end
@@ -106,7 +106,7 @@ module Contrast
106
106
  def _route_recurse controller, method, route
107
107
  return if controller.nil? || controller.cs__class == NilClass
108
108
 
109
- route_patterns = controller.routes.fetch(method, []).map(&:first)
109
+ route_patterns = controller.routes.fetch(method) { [] }.map(&:first)
110
110
  route_pattern = route_patterns&.find do |matcher|
111
111
  matcher.params(route) # ::Mustermann::Sinatra match.
112
112
  end