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

Returns:

  • (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
# 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
  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

Returns:

  • (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



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 148

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



164
165
166
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 164

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

.subscribe_to_routes_loadedObject



142
143
144
145
146
# File 'lib/datadog/appsec/contrib/rails/patcher.rb', line 142

def subscribe_to_routes_loaded
  ::ActiveSupport.on_load(:after_routes_loaded) do |app|
    Datadog::AppSec::Contrib::Rails::Patcher.report_routes_via_telemetry(app.routes.routes)
  end
end

.target_versionObject



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

def target_version
  Integration.version
end