datadog 2.12.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -2
  3. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +16 -14
  4. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +3 -0
  5. data/ext/datadog_profiling_native_extension/encoded_profile.c +69 -0
  6. data/ext/datadog_profiling_native_extension/encoded_profile.h +7 -0
  7. data/ext/datadog_profiling_native_extension/http_transport.c +25 -32
  8. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  9. data/ext/datadog_profiling_native_extension/stack_recorder.c +22 -21
  10. data/ext/libdatadog_api/datadog_ruby_common.h +3 -0
  11. data/lib/datadog/appsec/actions_handler/serializable_backtrace.rb +89 -0
  12. data/lib/datadog/appsec/actions_handler.rb +22 -1
  13. data/lib/datadog/appsec/anonymizer.rb +16 -0
  14. data/lib/datadog/appsec/assets/waf_rules/README.md +50 -5
  15. data/lib/datadog/appsec/assets/waf_rules/processors.json +239 -10
  16. data/lib/datadog/appsec/assets/waf_rules/recommended.json +0 -1344
  17. data/lib/datadog/appsec/assets/waf_rules/scanners.json +926 -17
  18. data/lib/datadog/appsec/assets/waf_rules/strict.json +0 -1344
  19. data/lib/datadog/appsec/component.rb +19 -17
  20. data/lib/datadog/appsec/compressed_json.rb +40 -0
  21. data/lib/datadog/appsec/configuration/settings.rb +62 -10
  22. data/lib/datadog/appsec/contrib/active_record/integration.rb +1 -1
  23. data/lib/datadog/appsec/contrib/auto_instrument.rb +1 -1
  24. data/lib/datadog/appsec/contrib/devise/configuration.rb +7 -31
  25. data/lib/datadog/appsec/contrib/devise/data_extractor.rb +79 -0
  26. data/lib/datadog/appsec/contrib/devise/ext.rb +21 -0
  27. data/lib/datadog/appsec/contrib/devise/integration.rb +0 -1
  28. data/lib/datadog/appsec/contrib/devise/patcher.rb +36 -23
  29. data/lib/datadog/appsec/contrib/devise/patches/signin_tracking_patch.rb +102 -0
  30. data/lib/datadog/appsec/contrib/devise/patches/signup_tracking_patch.rb +69 -0
  31. data/lib/datadog/appsec/contrib/devise/{patcher/rememberable_patch.rb → patches/skip_signin_tracking_patch.rb} +2 -2
  32. data/lib/datadog/appsec/contrib/devise/tracking_middleware.rb +93 -0
  33. data/lib/datadog/appsec/contrib/rack/ext.rb +14 -0
  34. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +10 -3
  35. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +0 -2
  36. data/lib/datadog/appsec/event.rb +22 -51
  37. data/lib/datadog/appsec/ext.rb +4 -2
  38. data/lib/datadog/appsec/instrumentation/gateway/argument.rb +4 -2
  39. data/lib/datadog/appsec/monitor/gateway/watcher.rb +8 -3
  40. data/lib/datadog/appsec/remote.rb +4 -0
  41. data/lib/datadog/appsec/security_engine/runner.rb +2 -2
  42. data/lib/datadog/appsec/utils.rb +0 -2
  43. data/lib/datadog/core/configuration/components.rb +2 -1
  44. data/lib/datadog/core/configuration/ext.rb +4 -0
  45. data/lib/datadog/core/configuration/options.rb +2 -2
  46. data/lib/datadog/core/configuration/settings.rb +53 -30
  47. data/lib/datadog/core/diagnostics/environment_logger.rb +1 -1
  48. data/lib/datadog/core/environment/agent_info.rb +4 -3
  49. data/lib/datadog/core/metrics/client.rb +1 -1
  50. data/lib/datadog/core/remote/client.rb +1 -1
  51. data/lib/datadog/core/remote/component.rb +3 -6
  52. data/lib/datadog/core/remote/configuration/repository.rb +2 -1
  53. data/lib/datadog/core/remote/negotiation.rb +9 -9
  54. data/lib/datadog/core/remote/transport/config.rb +4 -3
  55. data/lib/datadog/core/remote/transport/http/client.rb +4 -3
  56. data/lib/datadog/core/remote/transport/http/config.rb +6 -32
  57. data/lib/datadog/core/remote/transport/http/negotiation.rb +6 -32
  58. data/lib/datadog/core/remote/transport/http.rb +22 -57
  59. data/lib/datadog/core/remote/transport/negotiation.rb +4 -3
  60. data/lib/datadog/core/runtime/metrics.rb +8 -1
  61. data/lib/datadog/core/telemetry/http/adapters/net.rb +1 -1
  62. data/lib/datadog/core/telemetry/metric.rb +5 -5
  63. data/lib/datadog/core/telemetry/request.rb +1 -1
  64. data/lib/datadog/core/transport/http/api/instance.rb +17 -0
  65. data/lib/datadog/core/transport/http/api/spec.rb +17 -0
  66. data/lib/datadog/core/transport/http/builder.rb +5 -3
  67. data/lib/datadog/core/transport/http.rb +39 -2
  68. data/lib/datadog/di/component.rb +0 -2
  69. data/lib/datadog/di/probe_notification_builder.rb +1 -1
  70. data/lib/datadog/di/probe_notifier_worker.rb +16 -16
  71. data/lib/datadog/di/transport/diagnostics.rb +4 -3
  72. data/lib/datadog/di/transport/http/api.rb +2 -12
  73. data/lib/datadog/di/transport/http/client.rb +4 -3
  74. data/lib/datadog/di/transport/http/diagnostics.rb +7 -34
  75. data/lib/datadog/di/transport/http/input.rb +7 -34
  76. data/lib/datadog/di/transport/http.rb +14 -62
  77. data/lib/datadog/di/transport/input.rb +4 -3
  78. data/lib/datadog/di/utils.rb +5 -0
  79. data/lib/datadog/kit/appsec/events.rb +12 -0
  80. data/lib/datadog/kit/identity.rb +5 -1
  81. data/lib/datadog/opentelemetry/api/baggage.rb +90 -0
  82. data/lib/datadog/opentelemetry/api/baggage.rbs +26 -0
  83. data/lib/datadog/opentelemetry/api/context.rb +16 -2
  84. data/lib/datadog/opentelemetry/sdk/trace/span.rb +1 -1
  85. data/lib/datadog/opentelemetry.rb +2 -1
  86. data/lib/datadog/profiling/collectors/info.rb +3 -0
  87. data/lib/datadog/profiling/collectors/thread_context.rb +1 -1
  88. data/lib/datadog/profiling/encoded_profile.rb +11 -0
  89. data/lib/datadog/profiling/exporter.rb +2 -3
  90. data/lib/datadog/profiling/ext.rb +0 -1
  91. data/lib/datadog/profiling/flush.rb +4 -7
  92. data/lib/datadog/profiling/http_transport.rb +10 -59
  93. data/lib/datadog/profiling/stack_recorder.rb +4 -4
  94. data/lib/datadog/profiling.rb +6 -2
  95. data/lib/datadog/tracing/component.rb +15 -12
  96. data/lib/datadog/tracing/configuration/ext.rb +7 -1
  97. data/lib/datadog/tracing/configuration/settings.rb +18 -2
  98. data/lib/datadog/tracing/context_provider.rb +1 -1
  99. data/lib/datadog/tracing/contrib/active_record/integration.rb +1 -1
  100. data/lib/datadog/tracing/contrib/configuration/settings.rb +1 -1
  101. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -5
  102. data/lib/datadog/tracing/contrib/excon/middleware.rb +5 -3
  103. data/lib/datadog/tracing/contrib/ext.rb +1 -0
  104. data/lib/datadog/tracing/contrib/faraday/middleware.rb +5 -3
  105. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +7 -1
  106. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +3 -0
  107. data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +0 -15
  108. data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +4 -1
  109. data/lib/datadog/tracing/contrib/http/instrumentation.rb +5 -5
  110. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +5 -11
  111. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +6 -10
  112. data/lib/datadog/tracing/contrib/karafka/configuration/settings.rb +27 -0
  113. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +46 -0
  114. data/lib/datadog/tracing/contrib/karafka/ext.rb +27 -0
  115. data/lib/datadog/tracing/contrib/karafka/integration.rb +45 -0
  116. data/lib/datadog/tracing/contrib/karafka/monitor.rb +66 -0
  117. data/lib/datadog/tracing/contrib/karafka/patcher.rb +71 -0
  118. data/lib/datadog/tracing/contrib/karafka.rb +37 -0
  119. data/lib/datadog/tracing/contrib/opensearch/configuration/settings.rb +17 -0
  120. data/lib/datadog/tracing/contrib/opensearch/ext.rb +9 -0
  121. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +5 -1
  122. data/lib/datadog/tracing/contrib/rack/request_queue.rb +1 -1
  123. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +5 -3
  124. data/lib/datadog/tracing/contrib/sidekiq/client_tracer.rb +6 -1
  125. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +3 -0
  126. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +1 -1
  127. data/lib/datadog/tracing/contrib.rb +1 -0
  128. data/lib/datadog/tracing/correlation.rb +9 -2
  129. data/lib/datadog/tracing/distributed/baggage.rb +131 -0
  130. data/lib/datadog/tracing/distributed/datadog.rb +2 -0
  131. data/lib/datadog/tracing/distributed/propagation.rb +25 -4
  132. data/lib/datadog/tracing/distributed/propagation_policy.rb +42 -0
  133. data/lib/datadog/tracing/metadata/ext.rb +5 -0
  134. data/lib/datadog/tracing/sampling/span/rule.rb +0 -1
  135. data/lib/datadog/tracing/span_event.rb +1 -1
  136. data/lib/datadog/tracing/span_operation.rb +2 -1
  137. data/lib/datadog/tracing/sync_writer.rb +1 -2
  138. data/lib/datadog/tracing/trace_digest.rb +9 -2
  139. data/lib/datadog/tracing/trace_operation.rb +29 -17
  140. data/lib/datadog/tracing/trace_segment.rb +6 -4
  141. data/lib/datadog/tracing/tracer.rb +38 -2
  142. data/lib/datadog/tracing/transport/http/api.rb +2 -10
  143. data/lib/datadog/tracing/transport/http/client.rb +5 -4
  144. data/lib/datadog/tracing/transport/http/traces.rb +13 -41
  145. data/lib/datadog/tracing/transport/http.rb +11 -44
  146. data/lib/datadog/tracing/transport/trace_formatter.rb +7 -0
  147. data/lib/datadog/tracing/transport/traces.rb +26 -9
  148. data/lib/datadog/tracing/workers/trace_writer.rb +2 -6
  149. data/lib/datadog/tracing/writer.rb +2 -6
  150. data/lib/datadog/tracing.rb +16 -3
  151. data/lib/datadog/version.rb +2 -2
  152. data/lib/datadog.rb +1 -1
  153. metadata +28 -13
  154. data/lib/datadog/appsec/contrib/devise/event.rb +0 -54
  155. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +0 -72
  156. data/lib/datadog/appsec/contrib/devise/patcher/registration_controller_patch.rb +0 -47
  157. data/lib/datadog/appsec/contrib/devise/resource.rb +0 -35
  158. data/lib/datadog/appsec/contrib/devise/tracking.rb +0 -57
  159. data/lib/datadog/appsec/utils/trace_operation.rb +0 -15
@@ -12,7 +12,25 @@ module Datadog
12
12
  class << self
13
13
  def build_appsec_component(settings, telemetry:)
14
14
  return if !settings.respond_to?(:appsec) || !settings.appsec.enabled
15
- return if incompatible_ffi_version?
15
+
16
+ ffi_version = Gem.loaded_specs['ffi'] && Gem.loaded_specs['ffi'].version
17
+ unless ffi_version
18
+ Datadog.logger.warn('FFI gem is not loaded, AppSec will be disabled.')
19
+ telemetry.error('AppSec: Component not loaded, due to missing FFI gem')
20
+
21
+ return
22
+ end
23
+
24
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3') && ffi_version < Gem::Version.new('1.16.0')
25
+ Datadog.logger.warn(
26
+ 'AppSec is not supported in Ruby versions above 3.3.0 when using `ffi` versions older than 1.16.0, ' \
27
+ 'and will be forcibly disabled due to a memory leak in `ffi`. ' \
28
+ 'Please upgrade your `ffi` version to 1.16.0 or higher.'
29
+ )
30
+ telemetry.error('AppSec: Component not loaded, ffi version is leaky with ruby > 3.3.0')
31
+
32
+ return
33
+ end
16
34
 
17
35
  processor = create_processor(settings, telemetry)
18
36
 
@@ -29,22 +47,6 @@ module Datadog
29
47
 
30
48
  private
31
49
 
32
- def incompatible_ffi_version?
33
- ffi_version = Gem.loaded_specs['ffi'] && Gem.loaded_specs['ffi'].version
34
- return true unless ffi_version
35
-
36
- return false unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3') &&
37
- ffi_version < Gem::Version.new('1.16.0')
38
-
39
- Datadog.logger.warn(
40
- 'AppSec is not supported in Ruby versions above 3.3.0 when using `ffi` versions older than 1.16.0, ' \
41
- 'and will be forcibly disabled due to a memory leak in `ffi`. ' \
42
- 'Please upgrade your `ffi` version to 1.16.0 or higher.'
43
- )
44
-
45
- true
46
- end
47
-
48
50
  def create_processor(settings, telemetry)
49
51
  rules = AppSec::Processor::RuleLoader.load_rules(
50
52
  telemetry: telemetry,
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'zlib'
5
+ require 'stringio'
6
+
7
+ require_relative '../core/utils/base64'
8
+
9
+ module Datadog
10
+ module AppSec
11
+ # Converts derivative schema payloads into JSON and compresses them into a
12
+ # base64 encoded string if the payload is worth compressing.
13
+ #
14
+ # See: https://github.com/DataDog/dd-trace-rb/pull/3177#issuecomment-1747221082
15
+ module CompressedJson
16
+ MIN_SIZE_FOR_COMPRESSION = 260
17
+
18
+ def self.dump(payload)
19
+ value = JSON.dump(payload)
20
+ return value if value.bytesize < MIN_SIZE_FOR_COMPRESSION
21
+
22
+ compress_and_encode(value)
23
+ rescue ArgumentError, Encoding::UndefinedConversionError, JSON::JSONError => e
24
+ AppSec.telemetry.report(e, description: 'AppSec: Failed to convert value into JSON')
25
+
26
+ nil
27
+ end
28
+
29
+ private_class_method def self.compress_and_encode(payload)
30
+ Core::Utils::Base64.strict_encode64(
31
+ Zlib.gzip(payload, level: Zlib::BEST_SPEED, strategy: Zlib::DEFAULT_STRATEGY)
32
+ )
33
+ rescue Zlib::Error, TypeError => e
34
+ AppSec.telemetry.report(e, description: 'AppSec: Failed to compress and encode value')
35
+
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ end
@@ -164,6 +164,66 @@ module Datadog
164
164
  end
165
165
  end
166
166
 
167
+ settings :stack_trace do
168
+ option :enabled do |o|
169
+ o.type :bool
170
+ o.env 'DD_APPSEC_STACK_TRACE_ENABLED'
171
+ o.default true
172
+ end
173
+
174
+ # The maximum number of stack trace frames to collect for each stack trace.
175
+ #
176
+ # If the stack trace exceeds this limit, the frames are dropped from the middle of the stack trace:
177
+ # 75% of the frames are kept from the top of the stack trace and 25% from the bottom
178
+ # (this percentage is also configurable).
179
+ #
180
+ # Minimum value is 10.
181
+ # Set to zero if you don't want any frames to be dropped.
182
+ #
183
+ # Default value is 32
184
+ option :max_depth do |o|
185
+ o.type :int
186
+ o.env 'DD_APPSEC_MAX_STACK_TRACE_DEPTH'
187
+ o.default 32
188
+
189
+ o.setter do |value|
190
+ value = 0 if value < 0
191
+ value
192
+ end
193
+ end
194
+
195
+ # The percentage of frames to keep from the top of the stack trace.
196
+ #
197
+ # Default value is 75
198
+ option :top_percentage do |o|
199
+ o.type :int
200
+ o.env 'DD_APPSEC_MAX_STACK_TRACE_DEPTH_TOP_PERCENT'
201
+ o.default 75
202
+
203
+ o.setter do |value|
204
+ value = 100 if value > 100
205
+ value = 0 if value.negative?
206
+ value
207
+ end
208
+ end
209
+
210
+ # Maximum number of stack traces to collect per span.
211
+ #
212
+ # Set to zero if you want to collect all stack traces.
213
+ #
214
+ # Default value is 2
215
+ option :max_stack_traces do |o|
216
+ o.type :int
217
+ o.env 'DD_APPSEC_MAX_STACK_TRACES'
218
+ o.default 2
219
+
220
+ o.setter do |value|
221
+ value = 0 if value < 0
222
+ value
223
+ end
224
+ end
225
+ end
226
+
167
227
  settings :auto_user_instrumentation do
168
228
  define_method(:enabled?) { get_option(:mode) != DISABLED_AUTO_USER_INSTRUMENTATION_MODE }
169
229
 
@@ -178,10 +238,10 @@ module Datadog
178
238
  Datadog.logger.warn(
179
239
  'The appsec.auto_user_instrumentation.mode value provided is not supported. ' \
180
240
  "Supported values are: #{AUTO_USER_INSTRUMENTATION_MODES.join(' | ')}. " \
181
- "Using default value: #{IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE}."
241
+ "Using value: #{DISABLED_AUTO_USER_INSTRUMENTATION_MODE}."
182
242
  )
183
243
 
184
- IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE
244
+ DISABLED_AUTO_USER_INSTRUMENTATION_MODE
185
245
  end
186
246
  end
187
247
  end
@@ -259,14 +319,6 @@ module Datadog
259
319
  o.type :bool, nilable: true
260
320
  o.env 'DD_APPSEC_SCA_ENABLED'
261
321
  end
262
-
263
- settings :standalone do
264
- option :enabled do |o|
265
- o.type :bool
266
- o.env 'DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED'
267
- o.default false
268
- end
269
- end
270
322
  end
271
323
  end
272
324
  end
@@ -13,7 +13,7 @@ module Datadog
13
13
 
14
14
  MINIMUM_VERSION = Gem::Version.new('4')
15
15
 
16
- register_as :active_record, auto_patch: false
16
+ register_as :active_record, auto_patch: true
17
17
 
18
18
  def self.version
19
19
  Gem.loaded_specs['activerecord'] && Gem.loaded_specs['activerecord'].version
@@ -9,7 +9,7 @@ module Datadog
9
9
  def self.patch_all
10
10
  integrations = []
11
11
 
12
- Datadog::AppSec::Contrib::Integration.registry.each do |_name, integration|
12
+ Datadog::AppSec::Contrib::Integration.registry.each_value do |integration|
13
13
  next unless integration.klass.auto_instrument?
14
14
 
15
15
  integrations << integration.name
@@ -7,19 +7,11 @@ module Datadog
7
7
  # A temporary configuration module to accomodate new RFC changes.
8
8
  # NOTE: DEV-3 Remove module
9
9
  module Configuration
10
- MODES_CONVERSION_RULES = {
11
- track_user_to_auto_instrumentation: {
12
- AppSec::Configuration::Settings::SAFE_TRACK_USER_EVENTS_MODE =>
10
+ TRACK_USER_EVENTS_CONVERSION_RULES = {
11
+ AppSec::Configuration::Settings::SAFE_TRACK_USER_EVENTS_MODE =>
13
12
  AppSec::Configuration::Settings::ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE,
14
- AppSec::Configuration::Settings::EXTENDED_TRACK_USER_EVENTS_MODE =>
13
+ AppSec::Configuration::Settings::EXTENDED_TRACK_USER_EVENTS_MODE =>
15
14
  AppSec::Configuration::Settings::IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE
16
- }.freeze,
17
- auto_instrumentation_to_track_user: {
18
- AppSec::Configuration::Settings::ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE =>
19
- AppSec::Configuration::Settings::SAFE_TRACK_USER_EVENTS_MODE,
20
- AppSec::Configuration::Settings::IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE =>
21
- AppSec::Configuration::Settings::EXTENDED_TRACK_USER_EVENTS_MODE
22
- }.freeze
23
15
  }.freeze
24
16
 
25
17
  module_function
@@ -44,30 +36,14 @@ module Datadog
44
36
  appsec.auto_user_instrumentation.mode
45
37
  appsec.track_user_events.mode
46
38
 
47
- if !appsec.auto_user_instrumentation.options[:mode].default_precedence? &&
48
- appsec.track_user_events.options[:mode].default_precedence?
49
- return appsec.auto_user_instrumentation.mode
50
- end
51
-
52
- if appsec.auto_user_instrumentation.options[:mode].default_precedence?
53
- return MODES_CONVERSION_RULES[:track_user_to_auto_instrumentation].fetch(
39
+ if !appsec.track_user_events.options[:mode].default_precedence? &&
40
+ appsec.auto_user_instrumentation.options[:mode].default_precedence?
41
+ return TRACK_USER_EVENTS_CONVERSION_RULES.fetch(
54
42
  appsec.track_user_events.mode, appsec.auto_user_instrumentation.mode
55
43
  )
56
44
  end
57
45
 
58
- identification_mode = AppSec::Configuration::Settings::IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE
59
- if appsec.auto_user_instrumentation.mode == identification_mode ||
60
- appsec.track_user_events.mode == AppSec::Configuration::Settings::EXTENDED_TRACK_USER_EVENTS_MODE
61
- return identification_mode
62
- end
63
-
64
- AppSec::Configuration::Settings::ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE
65
- end
66
-
67
- # NOTE: Remove in next version of tracking
68
- def track_user_events_mode
69
- MODES_CONVERSION_RULES[:auto_instrumentation_to_track_user]
70
- .fetch(auto_user_instrumentation_mode, Datadog.configuration.appsec.track_user_events.mode)
46
+ appsec.auto_user_instrumentation.mode
71
47
  end
72
48
  end
73
49
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../anonymizer'
4
+
5
+ module Datadog
6
+ module AppSec
7
+ module Contrib
8
+ module Devise
9
+ # Extracts user identification data from Devise resources.
10
+ # Supports both regular and anonymized data extraction modes.
11
+ class DataExtractor
12
+ PRIORITY_ORDERED_ID_KEYS = [:id, 'id', :uuid, 'uuid'].freeze
13
+ PRIORITY_ORDERED_LOGIN_KEYS = [:email, 'email', :username, 'username', :login, 'login'].freeze
14
+
15
+ def initialize(mode:)
16
+ @mode = mode
17
+ @devise_scopes = {}
18
+ end
19
+
20
+ def extract_id(object)
21
+ return if object.nil?
22
+
23
+ if object.respond_to?(:[])
24
+ id = object[PRIORITY_ORDERED_ID_KEYS.find { |key| object[key] }]
25
+ scope = find_devise_scope(object)
26
+
27
+ id = "#{scope}:#{id}" if id && scope
28
+ return transform(id)
29
+ end
30
+
31
+ id = object.id if object.respond_to?(:id)
32
+ id ||= object.uuid if object.respond_to?(:uuid)
33
+
34
+ scope = find_devise_scope(object)
35
+ id = "#{scope}:#{id}" if id && scope
36
+
37
+ transform(id)
38
+ end
39
+
40
+ def extract_login(object)
41
+ return if object.nil?
42
+
43
+ if object.respond_to?(:[])
44
+ login = object[PRIORITY_ORDERED_LOGIN_KEYS.find { |key| object[key] }]
45
+ return transform(login)
46
+ end
47
+
48
+ login = object.email if object.respond_to?(:email)
49
+ login ||= object.username if object.respond_to?(:username)
50
+ login ||= object.login if object.respond_to?(:login)
51
+
52
+ transform(login)
53
+ end
54
+
55
+ private
56
+
57
+ def find_devise_scope(object)
58
+ return if ::Devise.mappings.count == 1
59
+
60
+ @devise_scopes[object.class.name] ||= begin
61
+ ::Devise.mappings.each_value.find { |mapping| mapping.class_name == object.class.name }&.name
62
+ end
63
+ end
64
+
65
+ def transform(value)
66
+ return if value.nil?
67
+ return value.to_s unless anonymize?
68
+
69
+ Anonymizer.anonymize(value.to_s)
70
+ end
71
+
72
+ def anonymize?
73
+ @mode == AppSec::Configuration::Settings::ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -6,6 +6,27 @@ module Datadog
6
6
  module Devise
7
7
  # Devise integration constants
8
8
  module Ext
9
+ EVENT_LOGIN_SUCCESS = 'users.login.success'
10
+ EVENT_LOGIN_FAILURE = 'users.login.failure'
11
+ EVENT_SIGNUP = 'users.signup'
12
+
13
+ TAG_DD_USR_ID = '_dd.appsec.usr.id'
14
+ TAG_DD_USR_LOGIN = '_dd.appsec.usr.login'
15
+ TAG_DD_SIGNUP_MODE = '_dd.appsec.events.users.signup.auto.mode'
16
+ TAG_DD_COLLECTION_MODE = '_dd.appsec.user.collection_mode'
17
+ TAG_DD_LOGIN_SUCCESS_MODE = '_dd.appsec.events.users.login.success.auto.mode'
18
+ TAG_DD_LOGIN_FAILURE_MODE = '_dd.appsec.events.users.login.failure.auto.mode'
19
+
20
+ TAG_USR_ID = 'usr.id'
21
+ TAG_SIGNUP_TRACK = 'appsec.events.users.signup.track'
22
+ TAG_SIGNUP_USR_ID = 'appsec.events.users.signup.usr.id'
23
+ TAG_SIGNUP_USR_LOGIN = 'appsec.events.users.signup.usr.login'
24
+ TAG_LOGIN_FAILURE_TRACK = 'appsec.events.users.login.failure.track'
25
+ TAG_LOGIN_FAILURE_USR_ID = 'appsec.events.users.login.failure.usr.id'
26
+ TAG_LOGIN_FAILURE_USR_LOGIN = 'appsec.events.users.login.failure.usr.login'
27
+ TAG_LOGIN_FAILURE_USR_EXISTS = 'appsec.events.users.login.failure.usr.exists'
28
+ TAG_LOGIN_SUCCESS_TRACK = 'appsec.events.users.login.success.track'
29
+ TAG_LOGIN_SUCCESS_USR_LOGIN = 'appsec.events.users.login.success.usr.login'
9
30
  end
10
31
  end
11
32
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../integration'
4
-
5
4
  require_relative 'patcher'
6
5
 
7
6
  module Datadog
@@ -1,15 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'patcher/authenticatable_patch'
4
- require_relative 'patcher/rememberable_patch'
5
- require_relative 'patcher/registration_controller_patch'
3
+ require_relative '../../../core/utils/only_once'
4
+
5
+ require_relative 'tracking_middleware'
6
+ require_relative 'patches/signup_tracking_patch'
7
+ require_relative 'patches/signin_tracking_patch'
8
+ require_relative 'patches/skip_signin_tracking_patch'
6
9
 
7
10
  module Datadog
8
11
  module AppSec
9
12
  module Contrib
10
13
  module Devise
11
- # Patcher for AppSec on Devise
14
+ # Devise patcher
12
15
  module Patcher
16
+ GUARD_ONCE_PER_APP = Hash.new do |hash, key|
17
+ hash[key] = Datadog::Core::Utils::OnlyOnce.new
18
+ end
19
+
13
20
  module_function
14
21
 
15
22
  def patched?
@@ -21,29 +28,35 @@ module Datadog
21
28
  end
22
29
 
23
30
  def patch
24
- patch_authenticatable_strategy
25
- patch_rememberable_strategy
26
- patch_registration_controller
27
-
28
- Patcher.instance_variable_set(:@patched, true)
29
- end
30
-
31
- def patch_authenticatable_strategy
32
- ::Devise::Strategies::Authenticatable.prepend(AuthenticatablePatch)
33
- end
31
+ ::ActiveSupport.on_load(:before_initialize) do |app|
32
+ GUARD_ONCE_PER_APP[app].run do
33
+ begin
34
+ app.middleware.insert_after(Warden::Manager, TrackingMiddleware)
35
+ rescue RuntimeError
36
+ AppSec.telemetry.error('AppSec: unable to insert Devise TrackingMiddleware')
37
+ end
38
+ end
39
+ end
34
40
 
35
- def patch_rememberable_strategy
36
- return unless ::Devise::STRATEGIES.include?(:rememberable)
41
+ ::ActiveSupport.on_load(:after_initialize) do
42
+ if ::Devise::RegistrationsController.descendants.empty?
43
+ ::Devise::RegistrationsController.prepend(Patches::SignupTrackingPatch)
44
+ else
45
+ ::Devise::RegistrationsController.descendants.each do |controller|
46
+ controller.prepend(Patches::SignupTrackingPatch)
47
+ end
48
+ end
49
+ end
37
50
 
38
- # Rememberable strategy is required in autoloaded Rememberable model
39
- ::Devise::Models::Rememberable # rubocop:disable Lint/Void
40
- ::Devise::Strategies::Rememberable.prepend(RememberablePatch)
41
- end
51
+ ::Devise::Strategies::Authenticatable.prepend(Patches::SigninTrackingPatch)
42
52
 
43
- def patch_registration_controller
44
- ::ActiveSupport.on_load(:after_initialize) do
45
- ::Devise::RegistrationsController.prepend(RegistrationControllerPatch)
53
+ if ::Devise::STRATEGIES.include?(:rememberable)
54
+ # Rememberable strategy is required in autoloaded Rememberable model
55
+ require 'devise/models/rememberable'
56
+ ::Devise::Strategies::Rememberable.prepend(Patches::SkipSigninTrackingPatch)
46
57
  end
58
+
59
+ Patcher.instance_variable_set(:@patched, true)
47
60
  end
48
61
  end
49
62
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../ext'
4
+ require_relative '../configuration'
5
+ require_relative '../data_extractor'
6
+
7
+ module Datadog
8
+ module AppSec
9
+ module Contrib
10
+ module Devise
11
+ module Patches
12
+ # A patch for Devise::Authenticatable strategy with tracking functionality
13
+ module SigninTrackingPatch
14
+ def validate(resource, &block)
15
+ result = super
16
+
17
+ return result unless AppSec.enabled?
18
+ return result if @_datadog_appsec_skip_track_login_event
19
+ return result unless Configuration.auto_user_instrumentation_enabled?
20
+ return result unless AppSec.active_context
21
+
22
+ context = AppSec.active_context
23
+ if context.trace.nil? || context.span.nil?
24
+ Datadog.logger.debug { 'AppSec: unable to track signin events, due to missing trace or span' }
25
+ return result
26
+ end
27
+
28
+ context.trace.keep!
29
+
30
+ if result
31
+ record_successful_signin(context, resource)
32
+ Instrumentation.gateway.push('appsec.events.user_lifecycle', Ext::EVENT_LOGIN_SUCCESS)
33
+
34
+ return result
35
+ end
36
+
37
+ record_failed_signin(context, resource)
38
+ Instrumentation.gateway.push('appsec.events.user_lifecycle', Ext::EVENT_LOGIN_FAILURE)
39
+
40
+ result
41
+ end
42
+
43
+ private
44
+
45
+ def record_successful_signin(context, resource)
46
+ extractor = DataExtractor.new(mode: Configuration.auto_user_instrumentation_mode)
47
+
48
+ id = extractor.extract_id(resource)
49
+ login = extractor.extract_login(authentication_hash) || extractor.extract_login(resource)
50
+
51
+ if id
52
+ context.span[Ext::TAG_USR_ID] ||= id
53
+ context.span[Ext::TAG_DD_USR_ID] = id
54
+ end
55
+
56
+ context.span[Ext::TAG_LOGIN_SUCCESS_USR_LOGIN] ||= login
57
+ context.span[Ext::TAG_LOGIN_SUCCESS_TRACK] = 'true'
58
+ context.span[Ext::TAG_DD_USR_LOGIN] = login
59
+ context.span[Ext::TAG_DD_LOGIN_SUCCESS_MODE] = Configuration.auto_user_instrumentation_mode
60
+
61
+ # NOTE: We don't have a way to make one-shot receivers for events,
62
+ # and because of that we will trigger an additional event even
63
+ # if it was already done via the SDK
64
+ AppSec::Instrumentation.gateway.push(
65
+ 'identity.set_user', AppSec::Instrumentation::Gateway::User.new(id, login)
66
+ )
67
+ end
68
+
69
+ def record_failed_signin(context, resource)
70
+ extractor = DataExtractor.new(mode: Configuration.auto_user_instrumentation_mode)
71
+
72
+ context.span[Ext::TAG_LOGIN_FAILURE_TRACK] = 'true'
73
+ context.span[Ext::TAG_DD_LOGIN_FAILURE_MODE] = Configuration.auto_user_instrumentation_mode
74
+
75
+ unless resource
76
+ login = extractor.extract_login(authentication_hash)
77
+
78
+ context.span[Ext::TAG_DD_USR_LOGIN] = login
79
+ context.span[Ext::TAG_LOGIN_FAILURE_USR_LOGIN] ||= login
80
+ context.span[Ext::TAG_LOGIN_FAILURE_USR_EXISTS] ||= 'false'
81
+
82
+ return
83
+ end
84
+
85
+ id = extractor.extract_id(resource)
86
+ login = extractor.extract_login(authentication_hash) || extractor.extract_login(resource)
87
+
88
+ if id
89
+ context.span[Ext::TAG_DD_USR_ID] = id
90
+ context.span[Ext::TAG_LOGIN_FAILURE_USR_ID] ||= id
91
+ end
92
+
93
+ context.span[Ext::TAG_DD_USR_LOGIN] = login
94
+ context.span[Ext::TAG_LOGIN_FAILURE_USR_LOGIN] ||= login
95
+ context.span[Ext::TAG_LOGIN_FAILURE_USR_EXISTS] ||= 'true'
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../ext'
4
+ require_relative '../configuration'
5
+ require_relative '../data_extractor'
6
+
7
+ module Datadog
8
+ module AppSec
9
+ module Contrib
10
+ module Devise
11
+ module Patches
12
+ # A patch for Devise::RegistrationsController with tracking functionality
13
+ module SignupTrackingPatch
14
+ def create
15
+ return super unless AppSec.enabled?
16
+ return super unless Configuration.auto_user_instrumentation_enabled?
17
+ return super unless AppSec.active_context
18
+
19
+ super do |resource|
20
+ context = AppSec.active_context
21
+
22
+ if context.trace.nil? || context.span.nil?
23
+ Datadog.logger.debug { 'AppSec: unable to track signup events, due to missing trace or span' }
24
+ next yield(resource) if block_given?
25
+ end
26
+
27
+ next yield(resource) if resource.new_record? && block_given?
28
+
29
+ context.trace.keep!
30
+ record_successful_signup(context, resource)
31
+ Instrumentation.gateway.push('appsec.events.user_lifecycle', Ext::EVENT_SIGNUP)
32
+
33
+ yield(resource) if block_given?
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def record_successful_signup(context, resource)
40
+ extractor = DataExtractor.new(mode: Configuration.auto_user_instrumentation_mode)
41
+
42
+ id = extractor.extract_id(resource)
43
+ login = extractor.extract_login(resource_params) || extractor.extract_login(resource)
44
+
45
+ context.span[Ext::TAG_SIGNUP_TRACK] = 'true'
46
+ context.span[Ext::TAG_DD_USR_LOGIN] = login
47
+ context.span[Ext::TAG_SIGNUP_USR_LOGIN] ||= login
48
+ context.span[Ext::TAG_DD_SIGNUP_MODE] = Configuration.auto_user_instrumentation_mode
49
+
50
+ if id
51
+ context.span[Ext::TAG_DD_USR_ID] = id
52
+
53
+ id_tag = resource.active_for_authentication? ? Ext::TAG_USR_ID : Ext::TAG_SIGNUP_USR_ID
54
+ context.span[id_tag] ||= id
55
+ end
56
+
57
+ # NOTE: We don't have a way to make one-shot receivers for events,
58
+ # and because of that we will trigger an additional event even
59
+ # if it was already done via the SDK
60
+ AppSec::Instrumentation.gateway.push(
61
+ 'identity.set_user', AppSec::Instrumentation::Gateway::User.new(id, login)
62
+ )
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -4,10 +4,10 @@ module Datadog
4
4
  module AppSec
5
5
  module Contrib
6
6
  module Devise
7
- module Patcher
7
+ module Patches
8
8
  # To avoid tracking new sessions that are created by
9
9
  # Rememberable strategy as Login Success events.
10
- module RememberablePatch
10
+ module SkipSigninTrackingPatch
11
11
  def validate(*args)
12
12
  @_datadog_appsec_skip_track_login_event = true
13
13