Module: Datadog::AppSec::Contrib::Rails::Patcher

Defined in:
lib/datadog/appsec/contrib/rails/patcher.rb

Overview

Patcher for AppSec on Rails

Constant Summary collapse

GUARD_ACTION_CONTROLLER_ONCE_PER_APP =
Hash.new { |h, key| h[key] = Datadog::Core::Utils::OnlyOnce.new }
GUARD_ROUTES_REPORTING_ONCE_PER_APP =
Hash.new { |h, key| h[key] = Datadog::Core::Utils::OnlyOnce.new }
BEFORE_INITIALIZE_ONLY_ONCE_PER_APP =
Hash.new { |h, key| h[key] = Datadog::Core::Utils::OnlyOnce.new }
AFTER_INITIALIZE_ONLY_ONCE_PER_APP =
Hash.new { |h, key| h[key] = Datadog::Core::Utils::OnlyOnce.new }

Class Method Summary collapse

Class Method Details

.add_middleware(app) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 64

def add_middleware(app)
  # Add trace middleware
  if include_middleware?(Datadog::Tracing::Contrib::Rack::TraceMiddleware, app)
    app.middleware.insert_after(
      Datadog::Tracing::Contrib::Rack::TraceMiddleware,
      Datadog::AppSec::Contrib::Rack::RequestMiddleware
    )
  else
    app.middleware.insert_before(0, Datadog::AppSec::Contrib::Rack::RequestMiddleware)
  end
end

.after_initialize(app) ⇒ Object



120
121
122
123
124
125
126
127
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 120

def after_initialize(app)
  AFTER_INITIALIZE_ONLY_ONCE_PER_APP[app].run do
    # Finish configuring the tracer after the application is initialized.
    # We need to wait for some things, like application name, middleware stack, etc.
    setup_security
    inspect_middlewares(app)
  end
end

.before_initialize(app) ⇒ Object



54
55
56
57
58
59
60
61
62
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 54

def before_initialize(app)
  BEFORE_INITIALIZE_ONLY_ONCE_PER_APP[app].run do
    # Middleware must be added before the application is initialized.
    # Otherwise the middleware stack will be frozen.
    add_middleware(app) if Datadog.configuration.tracing[:rails][:middleware]

    ::ActionController::Metal.prepend(Patches::ProcessActionPatch)
  end
end

.include_middleware?(middleware, app) ⇒ Boolean



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 76

def include_middleware?(middleware, app)
  found = false

  # find tracer middleware reference in Rails::Configuration::MiddlewareStackProxy
  app.middleware.instance_variable_get(:@operations).each do |operation|
    args = case operation
    when Array
      # rails 5.2
      _op, args = operation
      args
    when Proc
      if operation.binding.local_variables.include?(:args)
        # rails 6.0, 6.1
        operation.binding.local_variable_get(:args)
      else
        # rails 7.0 uses ... to pass args
        args_getter = Class.new do
          def method_missing(_op, *args) # standard:disable Style/MissingRespondToMissing
            args
          end
        end.new
        operation.call(args_getter)
      end
    else
      # unknown, pass through
      []
    end

    found = true if args.include?(middleware)
  end

  found
end

.inspect_middlewares(app) ⇒ Object



110
111
112
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 110

def inspect_middlewares(app)
  Datadog.logger.debug { +'Rails middlewares: ' << app.middleware.map(&:inspect).inspect }
end

.patchObject



38
39
40
41
42
43
44
45
46
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 38

def patch
  Gateway::Watcher.watch
  patch_before_initialize
  patch_after_initialize
  patch_action_controller
  subscribe_to_routes_loaded

  Patcher.instance_variable_set(:@patched, true)
end

.patch_action_controllerObject



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 129

def patch_action_controller
  ::ActiveSupport.on_load(:action_controller) do
    GUARD_ACTION_CONTROLLER_ONCE_PER_APP[self].run do
      ::ActionController::Base.prepend(Patches::RenderToBodyPatch)
    end

    # Rails 7.1 adds `after_routes_loaded` hook
    if Datadog::AppSec::Contrib::Rails::Patcher.target_version < Gem::Version.new('7.1')
      Datadog::AppSec::Contrib::Rails::Patcher.report_routes_via_telemetry(::Rails.application.routes.routes)
    end
  rescue => e
    error_message = 'Failed to get application routes'
    Datadog.logger.error("#{error_message}, #{e.class}: #{e.message}")
    AppSec.telemetry.report(e, description: error_message)
  end
end

.patch_after_initializeObject



114
115
116
117
118
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 114

def patch_after_initialize
  ::ActiveSupport.on_load(:after_initialize) do
    Datadog::AppSec::Contrib::Rails::Patcher.after_initialize(self)
  end
end

.patch_before_initializeObject



48
49
50
51
52
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 48

def patch_before_initialize
  ::ActiveSupport.on_load(:before_initialize) do
    Datadog::AppSec::Contrib::Rails::Patcher.before_initialize(self)
  end
end

.patched?Boolean



30
31
32
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 30

def patched?
  Patcher.instance_variable_get(:@patched)
end

.report_routes_via_telemetry(routes) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 156

def report_routes_via_telemetry(routes)
  # We do not support Rails 4.x for Endpoint Collection,
  # mainly because the Route#verb was a Regexp before Rails 5.0
  return if target_version < Gem::Version.new('5.0')
  return unless Datadog.configuration.appsec.api_security.endpoint_collection.enabled
  return unless AppSec.telemetry

  GUARD_ROUTES_REPORTING_ONCE_PER_APP[::Rails.application].run do
    AppSec.telemetry.app_endpoints_loaded(
      APISecurity::EndpointCollection::RailsCollector.new(routes).to_enum
    )
  end
rescue => e
  AppSec.telemetry&.report(e, description: 'failed to report application endpoints')
end

.setup_securityObject



172
173
174
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 172

def setup_security
  Datadog::AppSec::Contrib::Rails::Framework.setup
end

.subscribe_to_routes_loadedObject



146
147
148
149
150
151
152
153
154
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 146

def subscribe_to_routes_loaded
  ::ActiveSupport.on_load(:after_routes_loaded) do
    Datadog::AppSec::Contrib::Rails::Patcher.report_routes_via_telemetry(::Rails.application.routes.routes)
  rescue => e
    error_message = 'Failed to get application routes'
    Datadog.logger.error("#{error_message}, #{e.class}: #{e.message}")
    AppSec.telemetry.report(e, description: error_message)
  end
end

.target_versionObject



34
35
36
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 34

def target_version
  Integration.version
end