datadog 2.2.0 → 2.4.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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -2
  3. data/ext/datadog_profiling_loader/datadog_profiling_loader.c +9 -1
  4. data/ext/datadog_profiling_loader/extconf.rb +14 -26
  5. data/ext/datadog_profiling_native_extension/clock_id.h +1 -0
  6. data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +1 -2
  7. data/ext/datadog_profiling_native_extension/clock_id_noop.c +1 -2
  8. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +257 -69
  9. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +53 -28
  10. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.h +34 -4
  11. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +4 -0
  12. data/ext/datadog_profiling_native_extension/collectors_stack.c +136 -81
  13. data/ext/datadog_profiling_native_extension/collectors_stack.h +2 -2
  14. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +661 -48
  15. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +10 -1
  16. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +83 -0
  17. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +53 -0
  18. data/ext/datadog_profiling_native_extension/extconf.rb +91 -69
  19. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +50 -0
  20. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +75 -0
  21. data/ext/datadog_profiling_native_extension/heap_recorder.c +54 -12
  22. data/ext/datadog_profiling_native_extension/heap_recorder.h +3 -1
  23. data/ext/datadog_profiling_native_extension/helpers.h +6 -17
  24. data/ext/datadog_profiling_native_extension/http_transport.c +41 -9
  25. data/ext/datadog_profiling_native_extension/libdatadog_helpers.c +0 -86
  26. data/ext/datadog_profiling_native_extension/libdatadog_helpers.h +2 -23
  27. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +61 -172
  28. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +116 -139
  29. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +20 -11
  30. data/ext/datadog_profiling_native_extension/profiling.c +1 -3
  31. data/ext/datadog_profiling_native_extension/ruby_helpers.c +0 -33
  32. data/ext/datadog_profiling_native_extension/ruby_helpers.h +1 -26
  33. data/ext/datadog_profiling_native_extension/setup_signal_handler.h +1 -0
  34. data/ext/datadog_profiling_native_extension/stack_recorder.c +14 -2
  35. data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -0
  36. data/ext/datadog_profiling_native_extension/time_helpers.c +0 -15
  37. data/ext/datadog_profiling_native_extension/time_helpers.h +36 -6
  38. data/ext/{datadog_profiling_native_extension → libdatadog_api}/crashtracker.c +37 -22
  39. data/ext/libdatadog_api/datadog_ruby_common.c +83 -0
  40. data/ext/libdatadog_api/datadog_ruby_common.h +53 -0
  41. data/ext/libdatadog_api/extconf.rb +108 -0
  42. data/ext/libdatadog_api/macos_development.md +26 -0
  43. data/ext/libdatadog_extconf_helpers.rb +130 -0
  44. data/lib/datadog/appsec/assets/waf_rules/recommended.json +2184 -108
  45. data/lib/datadog/appsec/assets/waf_rules/strict.json +1430 -2
  46. data/lib/datadog/appsec/component.rb +29 -8
  47. data/lib/datadog/appsec/configuration/settings.rb +2 -2
  48. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +1 -0
  49. data/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb +21 -0
  50. data/lib/datadog/appsec/contrib/devise/patcher.rb +12 -2
  51. data/lib/datadog/appsec/contrib/graphql/appsec_trace.rb +35 -0
  52. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +109 -0
  53. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +71 -0
  54. data/lib/datadog/appsec/contrib/graphql/integration.rb +54 -0
  55. data/lib/datadog/appsec/contrib/graphql/patcher.rb +37 -0
  56. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +59 -0
  57. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +3 -6
  58. data/lib/datadog/appsec/event.rb +1 -1
  59. data/lib/datadog/appsec/processor/actions.rb +1 -1
  60. data/lib/datadog/appsec/processor/rule_loader.rb +3 -1
  61. data/lib/datadog/appsec/processor/rule_merger.rb +33 -15
  62. data/lib/datadog/appsec/processor.rb +36 -37
  63. data/lib/datadog/appsec/rate_limiter.rb +25 -40
  64. data/lib/datadog/appsec/remote.rb +7 -3
  65. data/lib/datadog/appsec/response.rb +15 -1
  66. data/lib/datadog/appsec.rb +3 -2
  67. data/lib/datadog/core/configuration/components.rb +18 -15
  68. data/lib/datadog/core/configuration/settings.rb +135 -9
  69. data/lib/datadog/core/crashtracking/agent_base_url.rb +21 -0
  70. data/lib/datadog/core/crashtracking/component.rb +111 -0
  71. data/lib/datadog/core/crashtracking/tag_builder.rb +39 -0
  72. data/lib/datadog/core/diagnostics/environment_logger.rb +8 -11
  73. data/lib/datadog/core/environment/execution.rb +5 -5
  74. data/lib/datadog/core/metrics/client.rb +7 -0
  75. data/lib/datadog/core/rate_limiter.rb +183 -0
  76. data/lib/datadog/core/remote/client/capabilities.rb +4 -3
  77. data/lib/datadog/core/remote/component.rb +4 -2
  78. data/lib/datadog/core/remote/negotiation.rb +4 -4
  79. data/lib/datadog/core/remote/tie.rb +2 -0
  80. data/lib/datadog/core/runtime/metrics.rb +1 -1
  81. data/lib/datadog/core/telemetry/component.rb +51 -2
  82. data/lib/datadog/core/telemetry/emitter.rb +9 -11
  83. data/lib/datadog/core/telemetry/event.rb +37 -1
  84. data/lib/datadog/core/telemetry/ext.rb +1 -0
  85. data/lib/datadog/core/telemetry/http/adapters/net.rb +10 -12
  86. data/lib/datadog/core/telemetry/http/ext.rb +3 -0
  87. data/lib/datadog/core/telemetry/http/transport.rb +38 -9
  88. data/lib/datadog/core/telemetry/logger.rb +51 -0
  89. data/lib/datadog/core/telemetry/logging.rb +71 -0
  90. data/lib/datadog/core/telemetry/request.rb +13 -1
  91. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +102 -0
  92. data/lib/datadog/core/utils/time.rb +12 -0
  93. data/lib/datadog/di/code_tracker.rb +168 -0
  94. data/lib/datadog/di/configuration/settings.rb +163 -0
  95. data/lib/datadog/di/configuration.rb +11 -0
  96. data/lib/datadog/di/error.rb +31 -0
  97. data/lib/datadog/di/extensions.rb +16 -0
  98. data/lib/datadog/di/probe.rb +133 -0
  99. data/lib/datadog/di/probe_builder.rb +41 -0
  100. data/lib/datadog/di/redactor.rb +188 -0
  101. data/lib/datadog/di/serializer.rb +193 -0
  102. data/lib/datadog/di.rb +14 -0
  103. data/lib/datadog/kit/appsec/events.rb +2 -4
  104. data/lib/datadog/opentelemetry/sdk/propagator.rb +2 -0
  105. data/lib/datadog/opentelemetry/sdk/span_processor.rb +10 -0
  106. data/lib/datadog/opentelemetry/sdk/trace/span.rb +23 -0
  107. data/lib/datadog/profiling/collectors/code_provenance.rb +7 -7
  108. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +28 -26
  109. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +11 -13
  110. data/lib/datadog/profiling/collectors/info.rb +15 -6
  111. data/lib/datadog/profiling/collectors/thread_context.rb +30 -2
  112. data/lib/datadog/profiling/component.rb +89 -95
  113. data/lib/datadog/profiling/exporter.rb +3 -3
  114. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -3
  115. data/lib/datadog/profiling/ext.rb +21 -21
  116. data/lib/datadog/profiling/flush.rb +1 -1
  117. data/lib/datadog/profiling/http_transport.rb +14 -7
  118. data/lib/datadog/profiling/load_native_extension.rb +5 -5
  119. data/lib/datadog/profiling/preload.rb +1 -1
  120. data/lib/datadog/profiling/profiler.rb +5 -8
  121. data/lib/datadog/profiling/scheduler.rb +33 -25
  122. data/lib/datadog/profiling/stack_recorder.rb +3 -0
  123. data/lib/datadog/profiling/tag_builder.rb +2 -2
  124. data/lib/datadog/profiling/tasks/exec.rb +5 -5
  125. data/lib/datadog/profiling/tasks/setup.rb +16 -35
  126. data/lib/datadog/profiling.rb +4 -5
  127. data/lib/datadog/single_step_instrument.rb +12 -0
  128. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +8 -12
  129. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +5 -0
  130. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +78 -0
  131. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/patcher.rb +33 -0
  132. data/lib/datadog/tracing/contrib/action_pack/patcher.rb +2 -0
  133. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +4 -0
  134. data/lib/datadog/tracing/contrib/active_record/events/instantiation.rb +3 -1
  135. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +4 -1
  136. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +5 -1
  137. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +5 -0
  138. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  139. data/lib/datadog/tracing/contrib/ext.rb +14 -0
  140. data/lib/datadog/tracing/contrib/faraday/middleware.rb +9 -0
  141. data/lib/datadog/tracing/contrib/grape/endpoint.rb +19 -0
  142. data/lib/datadog/tracing/contrib/graphql/patcher.rb +9 -12
  143. data/lib/datadog/tracing/contrib/graphql/trace_patcher.rb +3 -3
  144. data/lib/datadog/tracing/contrib/graphql/tracing_patcher.rb +3 -3
  145. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +14 -10
  146. data/lib/datadog/tracing/contrib/graphql/unified_trace_patcher.rb +10 -4
  147. data/lib/datadog/tracing/contrib/http/instrumentation.rb +18 -15
  148. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +6 -5
  149. data/lib/datadog/tracing/contrib/httpclient/patcher.rb +1 -14
  150. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +5 -0
  151. data/lib/datadog/tracing/contrib/httprb/patcher.rb +1 -14
  152. data/lib/datadog/tracing/contrib/lograge/patcher.rb +15 -0
  153. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
  154. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +5 -0
  155. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +17 -13
  156. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +13 -6
  157. data/lib/datadog/tracing/contrib/patcher.rb +2 -1
  158. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +5 -0
  159. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +4 -1
  160. data/lib/datadog/tracing/contrib/presto/patcher.rb +1 -13
  161. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +28 -0
  162. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +5 -1
  163. data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +22 -10
  164. data/lib/datadog/tracing/contrib/rack/middlewares.rb +27 -0
  165. data/lib/datadog/tracing/contrib/redis/tags.rb +4 -0
  166. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +4 -0
  167. data/lib/datadog/tracing/contrib/stripe/request.rb +3 -2
  168. data/lib/datadog/tracing/contrib/trilogy/configuration/settings.rb +5 -0
  169. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +4 -1
  170. data/lib/datadog/tracing/diagnostics/environment_logger.rb +14 -16
  171. data/lib/datadog/tracing/distributed/propagation.rb +7 -0
  172. data/lib/datadog/tracing/metadata/errors.rb +9 -1
  173. data/lib/datadog/tracing/metadata/ext.rb +6 -0
  174. data/lib/datadog/tracing/pipeline/span_filter.rb +2 -2
  175. data/lib/datadog/tracing/remote.rb +5 -2
  176. data/lib/datadog/tracing/sampling/matcher.rb +6 -1
  177. data/lib/datadog/tracing/sampling/rate_sampler.rb +1 -1
  178. data/lib/datadog/tracing/sampling/rule.rb +2 -0
  179. data/lib/datadog/tracing/sampling/rule_sampler.rb +9 -5
  180. data/lib/datadog/tracing/sampling/span/ext.rb +1 -1
  181. data/lib/datadog/tracing/sampling/span/rule.rb +2 -2
  182. data/lib/datadog/tracing/span.rb +9 -2
  183. data/lib/datadog/tracing/span_event.rb +41 -0
  184. data/lib/datadog/tracing/span_operation.rb +6 -2
  185. data/lib/datadog/tracing/trace_operation.rb +26 -2
  186. data/lib/datadog/tracing/tracer.rb +14 -12
  187. data/lib/datadog/tracing/transport/http/client.rb +1 -0
  188. data/lib/datadog/tracing/transport/io/client.rb +1 -0
  189. data/lib/datadog/tracing/transport/serializable_trace.rb +3 -0
  190. data/lib/datadog/tracing/workers/trace_writer.rb +1 -1
  191. data/lib/datadog/tracing/workers.rb +1 -1
  192. data/lib/datadog/version.rb +1 -1
  193. metadata +46 -11
  194. data/lib/datadog/profiling/crashtracker.rb +0 -91
  195. data/lib/datadog/profiling/ext/forking.rb +0 -98
  196. data/lib/datadog/tracing/sampling/rate_limiter.rb +0 -185
@@ -10,10 +10,11 @@ module Datadog
10
10
  # Core-pluggable component for AppSec
11
11
  class Component
12
12
  class << self
13
- def build_appsec_component(settings)
14
- return unless settings.respond_to?(:appsec) && settings.appsec.enabled
13
+ def build_appsec_component(settings, telemetry:)
14
+ return if !settings.respond_to?(:appsec) || !settings.appsec.enabled
15
+ return if incompatible_ffi_version?
15
16
 
16
- processor = create_processor(settings)
17
+ processor = create_processor(settings, telemetry)
17
18
 
18
19
  # We want to always instrument user events when AppSec is enabled.
19
20
  # There could be cases in which users use the DD_APPSEC_ENABLED Env variable to
@@ -28,8 +29,27 @@ module Datadog
28
29
 
29
30
  private
30
31
 
31
- def create_processor(settings)
32
- rules = AppSec::Processor::RuleLoader.load_rules(ruleset: settings.appsec.ruleset)
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
+ def create_processor(settings, telemetry)
49
+ rules = AppSec::Processor::RuleLoader.load_rules(
50
+ telemetry: telemetry,
51
+ ruleset: settings.appsec.ruleset
52
+ )
33
53
  return nil unless rules
34
54
 
35
55
  actions = rules['actions']
@@ -47,9 +67,10 @@ module Datadog
47
67
  rules: [rules],
48
68
  data: data,
49
69
  exclusions: exclusions,
70
+ telemetry: telemetry
50
71
  )
51
72
 
52
- processor = Processor.new(ruleset: ruleset)
73
+ processor = Processor.new(ruleset: ruleset, telemetry: telemetry)
53
74
  return nil unless processor.ready?
54
75
 
55
76
  processor
@@ -63,11 +84,11 @@ module Datadog
63
84
  @mutex = Mutex.new
64
85
  end
65
86
 
66
- def reconfigure(ruleset:, actions:)
87
+ def reconfigure(ruleset:, actions:, telemetry:)
67
88
  @mutex.synchronize do
68
89
  AppSec::Processor::Actions.merge(actions)
69
90
 
70
- new = Processor.new(ruleset: ruleset)
91
+ new = Processor.new(ruleset: ruleset, telemetry: telemetry)
71
92
 
72
93
  if new && new.ready?
73
94
  old = @processor
@@ -9,8 +9,8 @@ module Datadog
9
9
  # Settings
10
10
  module Settings
11
11
  # rubocop:disable Layout/LineLength
12
- DEFAULT_OBFUSCATOR_KEY_REGEX = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?)key)|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization'
13
- DEFAULT_OBFUSCATOR_VALUE_REGEX = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:\s*=[^;]|"\s*:\s*"[^"]+")|bearer\s+[a-z0-9\._\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\w=-]+\.ey[I-L][\w=-]+(?:\.[\w.+\/=-]+)?|[\-]{5}BEGIN[a-z\s]+PRIVATE\sKEY[\-]{5}[^\-]+[\-]{5}END[a-z\s]+PRIVATE\sKEY|ssh-rsa\s*[a-z0-9\/\.+]{100,}'
12
+ DEFAULT_OBFUSCATOR_KEY_REGEX = '(?i)pass|pw(?:or)?d|secret|(?:api|private|public|access)[_-]?key|token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization|jsessionid|phpsessid|asp\.net[_-]sessionid|sid|jwt'
13
+ DEFAULT_OBFUSCATOR_VALUE_REGEX = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:[_-]?phrase)?|secret(?:[_-]?key)?|(?:(?:api|private|public|access)[_-]?)key(?:[_-]?id)?|(?:(?:auth|access|id|refresh)[_-]?)?token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|jsessionid|phpsessid|asp\.net(?:[_-]|-)sessionid|sid|jwt)(?:\s*=[^;]|"\s*:\s*"[^"]+")|bearer\s+[a-z0-9\._\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\w=-]+\.ey[I-L][\w=-]+(?:\.[\w.+\/=-]+)?|[\-]{5}BEGIN[a-z\s]+PRIVATE\sKEY[\-]{5}[^\-]+[\-]{5}END[a-z\s]+PRIVATE\sKEY|ssh-rsa\s*[a-z0-9\/\.+]{100,}'
14
14
  # rubocop:enable Layout/LineLength
15
15
  APPSEC_VALID_TRACK_USER_EVENTS_MODE = [
16
16
  'safe',
@@ -15,6 +15,7 @@ module Datadog
15
15
  def validate(resource, &block)
16
16
  result = super
17
17
  return result unless AppSec.enabled?
18
+ return result if @_datadog_skip_track_login_event
18
19
 
19
20
  track_user_events_configuration = Datadog.configuration.appsec.track_user_events
20
21
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AppSec
5
+ module Contrib
6
+ module Devise
7
+ module Patcher
8
+ # To avoid tracking new sessions that are created by
9
+ # Rememberable strategy as Login Success events.
10
+ module RememberablePatch
11
+ def validate(*args)
12
+ @_datadog_skip_track_login_event = true
13
+
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../patcher'
4
4
  require_relative 'patcher/authenticatable_patch'
5
+ require_relative 'patcher/rememberable_patch'
5
6
  require_relative 'patcher/registration_controller_patch'
6
7
 
7
8
  module Datadog
@@ -23,16 +24,25 @@ module Datadog
23
24
  end
24
25
 
25
26
  def patch
26
- patch_authenticable_strategy
27
+ patch_authenticatable_strategy
28
+ patch_rememberable_strategy
27
29
  patch_registration_controller
28
30
 
29
31
  Patcher.instance_variable_set(:@patched, true)
30
32
  end
31
33
 
32
- def patch_authenticable_strategy
34
+ def patch_authenticatable_strategy
33
35
  ::Devise::Strategies::Authenticatable.prepend(AuthenticatablePatch)
34
36
  end
35
37
 
38
+ def patch_rememberable_strategy
39
+ return unless ::Devise::STRATEGIES.include?(:rememberable)
40
+
41
+ # Rememberable strategy is required in autoloaded Rememberable model
42
+ ::Devise::Models::Rememberable # rubocop:disable Lint/Void
43
+ ::Devise::Strategies::Rememberable.prepend(RememberablePatch)
44
+ end
45
+
36
46
  def patch_registration_controller
37
47
  ::ActiveSupport.on_load(:after_initialize) do
38
48
  ::Devise::RegistrationsController.prepend(RegistrationControllerPatch)
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'gateway/multiplex'
5
+ require_relative '../../instrumentation/gateway'
6
+
7
+ module Datadog
8
+ module AppSec
9
+ module Contrib
10
+ module GraphQL
11
+ # These methods will be called by the GraphQL runtime to send the variables to the WAF.
12
+ # We actually don't need to create any span/trace.
13
+ module AppSecTrace
14
+ def execute_multiplex(multiplex:)
15
+ return super unless Datadog::AppSec.enabled?
16
+
17
+ gateway_multiplex = Gateway::Multiplex.new(multiplex)
18
+
19
+ multiplex_return, multiplex_response = Instrumentation.gateway.push('graphql.multiplex', gateway_multiplex) do
20
+ super
21
+ end
22
+
23
+ # Returns an error * the number of queries so that the entire multiplex is blocked
24
+ if multiplex_response
25
+ blocked_event = multiplex_response.find { |action, _options| action == :block }
26
+ multiplex_return = AppSec::Response.graphql_response(gateway_multiplex) if blocked_event
27
+ end
28
+
29
+ multiplex_return
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql'
4
+
5
+ require_relative '../../../instrumentation/gateway/argument'
6
+
7
+ module Datadog
8
+ module AppSec
9
+ module Contrib
10
+ module GraphQL
11
+ module Gateway
12
+ # Gateway Request argument. Normalized extration of data from Rack::Request
13
+ class Multiplex < Instrumentation::Gateway::Argument
14
+ attr_reader :multiplex
15
+
16
+ def initialize(multiplex)
17
+ super()
18
+ @multiplex = multiplex
19
+ end
20
+
21
+ def arguments
22
+ @arguments ||= build_arguments_hash
23
+ end
24
+
25
+ def queries
26
+ @multiplex.queries
27
+ end
28
+
29
+ private
30
+
31
+ # This method builds an array of argument hashes for each field with arguments in the query.
32
+ #
33
+ # For example, given the following query:
34
+ # query ($postSlug: ID = "my-first-post", $withComments: Boolean!) {
35
+ # firstPost: post(slug: $postSlug) {
36
+ # title
37
+ # comments @include(if: $withComments) {
38
+ # author { name }
39
+ # content
40
+ # }
41
+ # }
42
+ # }
43
+ #
44
+ # The result would be:
45
+ # {"post"=>[{"slug"=>"my-first-post"}], "comments"=>[{"include"=>{"if"=>true}}]}
46
+ #
47
+ # Note that the `comments` "include" directive is included in the arguments list
48
+ def build_arguments_hash
49
+ queries.each_with_object({}) do |query, args_hash|
50
+ next unless query.selected_operation
51
+
52
+ arguments_from_selections(query.selected_operation.selections, query.variables, args_hash)
53
+ end
54
+ end
55
+
56
+ def arguments_from_selections(selections, query_variables, args_hash)
57
+ selections.each do |selection|
58
+ # rubocop:disable Style/ClassEqualityComparison
59
+ next unless selection.class.name == Integration::AST_NODE_CLASS_NAMES[:field]
60
+ # rubocop:enable Style/ClassEqualityComparison
61
+
62
+ selection_name = selection.alias || selection.name
63
+
64
+ if !selection.arguments.empty? || !selection.directives.empty?
65
+ args_hash[selection_name] ||= []
66
+ args_hash[selection_name] <<
67
+ arguments_hash(selection.arguments, query_variables).merge!(
68
+ arguments_from_directives(selection.directives, query_variables)
69
+ )
70
+ end
71
+
72
+ arguments_from_selections(selection.selections, query_variables, args_hash)
73
+ end
74
+ end
75
+
76
+ def arguments_from_directives(directives, query_variables)
77
+ directives.each_with_object({}) do |directive, args_hash|
78
+ # rubocop:disable Style/ClassEqualityComparison
79
+ next unless directive.class.name == Integration::AST_NODE_CLASS_NAMES[:directive]
80
+ # rubocop:enable Style/ClassEqualityComparison
81
+
82
+ args_hash[directive.name] = arguments_hash(directive.arguments, query_variables)
83
+ end
84
+ end
85
+
86
+ def arguments_hash(arguments, query_variables)
87
+ arguments.each_with_object({}) do |argument, args_hash|
88
+ args_hash[argument.name] = argument_value(argument, query_variables)
89
+ end
90
+ end
91
+
92
+ def argument_value(argument, query_variables)
93
+ case argument.value.class.name
94
+ when Integration::AST_NODE_CLASS_NAMES[:variable_identifier]
95
+ # we need to pass query.variables here instead of query.provided_variables,
96
+ # since #provided_variables don't know anything about variable default value
97
+ query_variables[argument.value.name]
98
+ when Integration::AST_NODE_CLASS_NAMES[:input_object]
99
+ arguments_hash(argument.value.arguments, query_variables)
100
+ else
101
+ argument.value
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../../../instrumentation/gateway'
5
+ require_relative '../reactive/multiplex'
6
+ require_relative '../../../reactive/operation'
7
+
8
+ module Datadog
9
+ module AppSec
10
+ module Contrib
11
+ module GraphQL
12
+ module Gateway
13
+ # Watcher for Rack gateway events
14
+ module Watcher
15
+ class << self
16
+ def watch
17
+ gateway = Instrumentation.gateway
18
+
19
+ watch_multiplex(gateway)
20
+ end
21
+
22
+ # This time we don't throw but use next
23
+ def watch_multiplex(gateway = Instrumentation.gateway)
24
+ gateway.watch('graphql.multiplex', :appsec) do |stack, gateway_multiplex|
25
+ block = false
26
+ event = nil
27
+
28
+ scope = AppSec::Scope.active_scope
29
+
30
+ if scope
31
+ AppSec::Reactive::Operation.new('graphql.multiplex') do |op|
32
+ GraphQL::Reactive::Multiplex.subscribe(op, scope.processor_context) do |result|
33
+ event = {
34
+ waf_result: result,
35
+ trace: scope.trace,
36
+ span: scope.service_entry_span,
37
+ multiplex: gateway_multiplex,
38
+ actions: result.actions
39
+ }
40
+
41
+ if scope.service_entry_span
42
+ scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
43
+ scope.service_entry_span.set_tag('appsec.event', 'true')
44
+ end
45
+
46
+ scope.processor_context.events << event
47
+ end
48
+
49
+ block = GraphQL::Reactive::Multiplex.publish(op, gateway_multiplex)
50
+ end
51
+ end
52
+
53
+ next [nil, [[:block, event]]] if block
54
+
55
+ ret, res = stack.call(gateway_multiplex.arguments)
56
+
57
+ if event
58
+ res ||= []
59
+ res << [:monitor, event]
60
+ end
61
+
62
+ [ret, res]
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../integration'
4
+ require_relative 'patcher'
5
+
6
+ module Datadog
7
+ module AppSec
8
+ module Contrib
9
+ module GraphQL
10
+ # Description of GraphQL integration
11
+ class Integration
12
+ include Datadog::AppSec::Contrib::Integration
13
+
14
+ MINIMUM_VERSION = Gem::Version.new('2.0.19')
15
+
16
+ AST_NODE_CLASS_NAMES = {
17
+ field: 'GraphQL::Language::Nodes::Field',
18
+ directive: 'GraphQL::Language::Nodes::Directive',
19
+ variable_identifier: 'GraphQL::Language::Nodes::VariableIdentifier',
20
+ input_object: 'GraphQL::Language::Nodes::InputObject',
21
+ }.freeze
22
+
23
+ register_as :graphql, auto_patch: false
24
+
25
+ def self.version
26
+ Gem.loaded_specs['graphql'] && Gem.loaded_specs['graphql'].version
27
+ end
28
+
29
+ def self.loaded?
30
+ !defined?(::GraphQL).nil?
31
+ end
32
+
33
+ def self.compatible?
34
+ super && version >= MINIMUM_VERSION && ast_node_classes_defined?
35
+ end
36
+
37
+ def self.auto_instrument?
38
+ true
39
+ end
40
+
41
+ def self.ast_node_classes_defined?
42
+ AST_NODE_CLASS_NAMES.all? do |_, class_name|
43
+ Object.const_defined?(class_name)
44
+ end
45
+ end
46
+
47
+ def patcher
48
+ Patcher
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../patcher'
4
+ require_relative 'gateway/watcher'
5
+
6
+ if Gem.loaded_specs['graphql'] && Gem.loaded_specs['graphql'].version >= Gem::Version.new('2.0.19')
7
+ require_relative 'appsec_trace'
8
+ end
9
+
10
+ module Datadog
11
+ module AppSec
12
+ module Contrib
13
+ module GraphQL
14
+ # Patcher for AppSec on GraphQL
15
+ module Patcher
16
+ include Datadog::AppSec::Contrib::Patcher
17
+
18
+ module_function
19
+
20
+ def patched?
21
+ Patcher.instance_variable_get(:@patched)
22
+ end
23
+
24
+ def target_version
25
+ Integration.version
26
+ end
27
+
28
+ def patch
29
+ Gateway::Watcher.watch
30
+ ::GraphQL::Schema.trace_with(AppSecTrace)
31
+ Patcher.instance_variable_set(:@patched, true)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AppSec
5
+ module Contrib
6
+ module GraphQL
7
+ module Reactive
8
+ # Dispatch data from a GraphQL resolve query to the WAF context
9
+ module Multiplex
10
+ ADDRESSES = [
11
+ 'graphql.server.all_resolvers'
12
+ ].freeze
13
+ private_constant :ADDRESSES
14
+
15
+ def self.publish(op, gateway_multiplex)
16
+ catch(:block) do
17
+ op.publish('graphql.server.all_resolvers', gateway_multiplex.arguments)
18
+
19
+ nil
20
+ end
21
+ end
22
+
23
+ def self.subscribe(op, waf_context)
24
+ op.subscribe(*ADDRESSES) do |*values|
25
+ Datadog.logger.debug { "reacted to #{ADDRESSES.inspect}: #{values.inspect}" }
26
+ arguments = values[0]
27
+
28
+ waf_args = {
29
+ 'graphql.server.all_resolvers' => arguments
30
+ }
31
+
32
+ waf_timeout = Datadog.configuration.appsec.waf_timeout
33
+ result = waf_context.run(waf_args, waf_timeout)
34
+
35
+ Datadog.logger.debug { "WAF TIMEOUT: #{result.inspect}" } if result.timeout
36
+
37
+ case result.status
38
+ when :match
39
+ Datadog.logger.debug { "WAF: #{result.inspect}" }
40
+
41
+ yield result
42
+ throw(:block, true) unless result.actions.empty?
43
+ when :ok
44
+ Datadog.logger.debug { "WAF OK: #{result.inspect}" }
45
+ when :invalid_call
46
+ Datadog.logger.debug { "WAF CALL ERROR: #{result.inspect}" }
47
+ when :invalid_rule, :invalid_flow, :no_rule
48
+ Datadog.logger.debug { "WAF RULE ERROR: #{result.inspect}" }
49
+ else
50
+ Datadog.logger.debug { "WAF UNKNOWN: #{result.status.inspect} #{result.inspect}" }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -41,18 +41,15 @@ module Datadog
41
41
 
42
42
  def headers
43
43
  result = request.env.each_with_object({}) do |(k, v), h|
44
- h[k.gsub(/^HTTP_/, '').downcase!.tr('_', '-')] = v if k =~ /^HTTP_/
44
+ h[k.delete_prefix('HTTP_').tap(&:downcase!).tap { |s| s.tr!('_', '-') }] = v if k.start_with?('HTTP_')
45
45
  end
46
46
 
47
47
  result['content-type'] = request.content_type if request.content_type
48
- result['content-length'] = request.content_length if request.content_length
48
+ # Since Rack 3.1, content-length is nil if the body is empty, but we still want to send it to the WAF.
49
+ result['content-length'] = request.content_length || '0'
49
50
  result
50
51
  end
51
52
 
52
- def body
53
- request.body.read.tap { request.body.rewind }
54
- end
55
-
56
53
  def url
57
54
  request.url
58
55
  end
@@ -52,7 +52,7 @@ module Datadog
52
52
  # ensure rate limiter is called only when there are events to record
53
53
  return if events.empty? || span.nil?
54
54
 
55
- Datadog::AppSec::RateLimiter.limit(:traces) do
55
+ Datadog::AppSec::RateLimiter.thread_local.limit do
56
56
  record_via_span(span, *events)
57
57
  end
58
58
  end
@@ -11,7 +11,7 @@ module Datadog
11
11
  @actions ||= []
12
12
  end
13
13
 
14
- def fecth_configuration(action)
14
+ def fetch_configuration(action)
15
15
  actions.find { |action_configuration| action_configuration['id'] == action }
16
16
  end
17
17
 
@@ -9,7 +9,7 @@ module Datadog
9
9
  # that load appsec rules and data from settings
10
10
  module RuleLoader
11
11
  class << self
12
- def load_rules(ruleset:)
12
+ def load_rules(ruleset:, telemetry:)
13
13
  begin
14
14
  case ruleset
15
15
  when :recommended, :strict
@@ -35,6 +35,8 @@ module Datadog
35
35
  "libddwaf ruleset failed to load, ruleset: #{ruleset.inspect} error: #{e.inspect}"
36
36
  end
37
37
 
38
+ telemetry.report(e, description: 'libddwaf ruleset failed to load')
39
+
38
40
  nil
39
41
  end
40
42
  end
@@ -18,25 +18,35 @@ module Datadog
18
18
  end
19
19
  end
20
20
 
21
- DEFAULT_WAF_PROCESSORS = begin
22
- JSON.parse(Datadog::AppSec::Assets.waf_processors)
23
- rescue StandardError => e
24
- Datadog.logger.error { "libddwaf rulemerger failed to parse default waf processors. Error: #{e.inspect}" }
25
- []
26
- end
27
-
28
- DEFAULT_WAF_SCANNERS = begin
29
- JSON.parse(Datadog::AppSec::Assets.waf_scanners)
30
- rescue StandardError => e
31
- Datadog.logger.error { "libddwaf rulemerger failed to parse default waf scanners. Error: #{e.inspect}" }
32
- []
33
- end
34
-
35
21
  class << self
22
+ # TODO: `processors` and `scanners` are not provided by the caller, consider removing them
36
23
  def merge(
24
+ telemetry:,
37
25
  rules:, data: [], overrides: [], exclusions: [], custom_rules: [],
38
- processors: DEFAULT_WAF_PROCESSORS, scanners: DEFAULT_WAF_SCANNERS
26
+ processors: nil, scanners: nil
39
27
  )
28
+ processors ||= begin
29
+ default_waf_processors
30
+ rescue StandardError => e
31
+ Datadog.logger.error("libddwaf rulemerger failed to parse default waf processors. Error: #{e.inspect}")
32
+ telemetry.report(
33
+ e,
34
+ description: 'libddwaf rulemerger failed to parse default waf processors'
35
+ )
36
+ []
37
+ end
38
+
39
+ scanners ||= begin
40
+ default_waf_scanners
41
+ rescue StandardError => e
42
+ Datadog.logger.error("libddwaf rulemerger failed to parse default waf scanners. Error: #{e.inspect}")
43
+ telemetry.report(
44
+ e,
45
+ description: 'libddwaf rulemerger failed to parse default waf scanners'
46
+ )
47
+ []
48
+ end
49
+
40
50
  combined_rules = combine_rules(rules)
41
51
 
42
52
  combined_data = combine_data(data) if data.any?
@@ -53,6 +63,14 @@ module Datadog
53
63
  combined_rules
54
64
  end
55
65
 
66
+ def default_waf_processors
67
+ @default_waf_processors ||= JSON.parse(Datadog::AppSec::Assets.waf_processors)
68
+ end
69
+
70
+ def default_waf_scanners
71
+ @default_waf_scanners ||= JSON.parse(Datadog::AppSec::Assets.waf_scanners)
72
+ end
73
+
56
74
  private
57
75
 
58
76
  def combine_rules(rules)