datadog 2.3.0 → 2.5.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 (173) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -2
  3. data/ext/datadog_profiling_loader/datadog_profiling_loader.c +9 -1
  4. data/ext/datadog_profiling_loader/extconf.rb +10 -22
  5. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +3 -3
  6. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +198 -41
  7. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +4 -2
  8. data/ext/datadog_profiling_native_extension/collectors_stack.c +89 -46
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +645 -107
  10. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +15 -1
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +0 -27
  12. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -4
  13. data/ext/datadog_profiling_native_extension/extconf.rb +42 -25
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +50 -0
  15. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +75 -0
  16. data/ext/datadog_profiling_native_extension/heap_recorder.c +194 -34
  17. data/ext/datadog_profiling_native_extension/heap_recorder.h +11 -0
  18. data/ext/datadog_profiling_native_extension/http_transport.c +38 -6
  19. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +1 -1
  20. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +53 -2
  21. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +3 -0
  22. data/ext/datadog_profiling_native_extension/profiling.c +1 -1
  23. data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -11
  24. data/ext/datadog_profiling_native_extension/stack_recorder.c +58 -22
  25. data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -0
  26. data/ext/libdatadog_api/crashtracker.c +20 -18
  27. data/ext/libdatadog_api/datadog_ruby_common.c +0 -27
  28. data/ext/libdatadog_api/datadog_ruby_common.h +0 -4
  29. data/ext/libdatadog_extconf_helpers.rb +1 -1
  30. data/lib/datadog/appsec/assets/waf_rules/recommended.json +2184 -108
  31. data/lib/datadog/appsec/assets/waf_rules/strict.json +1430 -2
  32. data/lib/datadog/appsec/component.rb +29 -8
  33. data/lib/datadog/appsec/configuration/settings.rb +10 -2
  34. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +1 -0
  35. data/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb +21 -0
  36. data/lib/datadog/appsec/contrib/devise/patcher.rb +12 -2
  37. data/lib/datadog/appsec/contrib/graphql/appsec_trace.rb +0 -14
  38. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +67 -31
  39. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +14 -15
  40. data/lib/datadog/appsec/contrib/graphql/integration.rb +14 -1
  41. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +7 -20
  42. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +2 -5
  43. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -15
  44. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +6 -18
  45. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +7 -20
  46. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +5 -18
  47. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +3 -1
  48. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +3 -5
  49. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +5 -18
  50. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +6 -10
  51. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +7 -20
  52. data/lib/datadog/appsec/event.rb +25 -1
  53. data/lib/datadog/appsec/ext.rb +4 -0
  54. data/lib/datadog/appsec/monitor/gateway/watcher.rb +3 -5
  55. data/lib/datadog/appsec/monitor/reactive/set_user.rb +7 -20
  56. data/lib/datadog/appsec/processor/context.rb +109 -0
  57. data/lib/datadog/appsec/processor/rule_loader.rb +3 -1
  58. data/lib/datadog/appsec/processor/rule_merger.rb +33 -15
  59. data/lib/datadog/appsec/processor.rb +42 -107
  60. data/lib/datadog/appsec/rate_limiter.rb +25 -40
  61. data/lib/datadog/appsec/remote.rb +7 -3
  62. data/lib/datadog/appsec/scope.rb +1 -4
  63. data/lib/datadog/appsec/utils/trace_operation.rb +15 -0
  64. data/lib/datadog/appsec/utils.rb +2 -0
  65. data/lib/datadog/appsec.rb +3 -2
  66. data/lib/datadog/core/configuration/agent_settings_resolver.rb +26 -25
  67. data/lib/datadog/core/configuration/components.rb +4 -3
  68. data/lib/datadog/core/configuration/settings.rb +96 -5
  69. data/lib/datadog/core/configuration.rb +1 -3
  70. data/lib/datadog/core/crashtracking/component.rb +9 -6
  71. data/lib/datadog/core/environment/execution.rb +5 -5
  72. data/lib/datadog/core/environment/yjit.rb +5 -0
  73. data/lib/datadog/core/metrics/client.rb +7 -0
  74. data/lib/datadog/core/rate_limiter.rb +183 -0
  75. data/lib/datadog/core/remote/client/capabilities.rb +4 -3
  76. data/lib/datadog/core/remote/component.rb +4 -2
  77. data/lib/datadog/core/remote/negotiation.rb +4 -4
  78. data/lib/datadog/core/remote/tie.rb +2 -0
  79. data/lib/datadog/core/remote/transport/http.rb +5 -0
  80. data/lib/datadog/core/remote/worker.rb +1 -1
  81. data/lib/datadog/core/runtime/ext.rb +1 -0
  82. data/lib/datadog/core/runtime/metrics.rb +5 -1
  83. data/lib/datadog/core/semaphore.rb +35 -0
  84. data/lib/datadog/core/telemetry/component.rb +2 -0
  85. data/lib/datadog/core/telemetry/event.rb +12 -7
  86. data/lib/datadog/core/telemetry/logger.rb +51 -0
  87. data/lib/datadog/core/telemetry/logging.rb +50 -14
  88. data/lib/datadog/core/telemetry/request.rb +13 -1
  89. data/lib/datadog/core/transport/ext.rb +1 -0
  90. data/lib/datadog/core/utils/time.rb +12 -0
  91. data/lib/datadog/core/workers/async.rb +1 -1
  92. data/lib/datadog/di/code_tracker.rb +166 -0
  93. data/lib/datadog/di/configuration/settings.rb +163 -0
  94. data/lib/datadog/di/configuration.rb +11 -0
  95. data/lib/datadog/di/error.rb +31 -0
  96. data/lib/datadog/di/extensions.rb +16 -0
  97. data/lib/datadog/di/instrumenter.rb +301 -0
  98. data/lib/datadog/di/probe.rb +162 -0
  99. data/lib/datadog/di/probe_builder.rb +47 -0
  100. data/lib/datadog/di/probe_notification_builder.rb +207 -0
  101. data/lib/datadog/di/probe_notifier_worker.rb +244 -0
  102. data/lib/datadog/di/redactor.rb +188 -0
  103. data/lib/datadog/di/serializer.rb +215 -0
  104. data/lib/datadog/di/transport.rb +67 -0
  105. data/lib/datadog/di/utils.rb +39 -0
  106. data/lib/datadog/di.rb +57 -0
  107. data/lib/datadog/opentelemetry/sdk/propagator.rb +2 -0
  108. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +12 -10
  109. data/lib/datadog/profiling/collectors/info.rb +12 -3
  110. data/lib/datadog/profiling/collectors/thread_context.rb +32 -8
  111. data/lib/datadog/profiling/component.rb +21 -4
  112. data/lib/datadog/profiling/http_transport.rb +6 -1
  113. data/lib/datadog/profiling/scheduler.rb +2 -0
  114. data/lib/datadog/profiling/stack_recorder.rb +40 -9
  115. data/lib/datadog/single_step_instrument.rb +12 -0
  116. data/lib/datadog/tracing/component.rb +13 -0
  117. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +8 -12
  118. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +5 -0
  119. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +78 -0
  120. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/patcher.rb +33 -0
  121. data/lib/datadog/tracing/contrib/action_pack/patcher.rb +2 -0
  122. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +4 -0
  123. data/lib/datadog/tracing/contrib/active_record/events/instantiation.rb +3 -1
  124. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +3 -1
  125. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +5 -1
  126. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +5 -0
  127. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  128. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -0
  129. data/lib/datadog/tracing/contrib/excon/middleware.rb +3 -0
  130. data/lib/datadog/tracing/contrib/faraday/middleware.rb +12 -0
  131. data/lib/datadog/tracing/contrib/grape/endpoint.rb +24 -2
  132. data/lib/datadog/tracing/contrib/graphql/patcher.rb +9 -12
  133. data/lib/datadog/tracing/contrib/graphql/trace_patcher.rb +3 -3
  134. data/lib/datadog/tracing/contrib/graphql/tracing_patcher.rb +3 -3
  135. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +13 -9
  136. data/lib/datadog/tracing/contrib/graphql/unified_trace_patcher.rb +6 -3
  137. data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +9 -0
  138. data/lib/datadog/tracing/contrib/http/instrumentation.rb +22 -15
  139. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +10 -5
  140. data/lib/datadog/tracing/contrib/httpclient/patcher.rb +1 -14
  141. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +9 -0
  142. data/lib/datadog/tracing/contrib/httprb/patcher.rb +1 -14
  143. data/lib/datadog/tracing/contrib/lograge/patcher.rb +1 -2
  144. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
  145. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +13 -6
  146. data/lib/datadog/tracing/contrib/patcher.rb +2 -1
  147. data/lib/datadog/tracing/contrib/presto/patcher.rb +1 -13
  148. data/lib/datadog/tracing/contrib/rack/middlewares.rb +27 -0
  149. data/lib/datadog/tracing/contrib/rails/runner.rb +1 -1
  150. data/lib/datadog/tracing/contrib/redis/tags.rb +4 -0
  151. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +3 -0
  152. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +4 -0
  153. data/lib/datadog/tracing/contrib/stripe/request.rb +3 -2
  154. data/lib/datadog/tracing/distributed/propagation.rb +7 -0
  155. data/lib/datadog/tracing/metadata/ext.rb +2 -0
  156. data/lib/datadog/tracing/remote.rb +5 -2
  157. data/lib/datadog/tracing/sampling/matcher.rb +6 -1
  158. data/lib/datadog/tracing/sampling/rate_sampler.rb +1 -1
  159. data/lib/datadog/tracing/sampling/rule.rb +2 -0
  160. data/lib/datadog/tracing/sampling/rule_sampler.rb +15 -9
  161. data/lib/datadog/tracing/sampling/span/ext.rb +1 -1
  162. data/lib/datadog/tracing/sampling/span/rule.rb +2 -2
  163. data/lib/datadog/tracing/trace_operation.rb +26 -2
  164. data/lib/datadog/tracing/tracer.rb +29 -22
  165. data/lib/datadog/tracing/transport/http/client.rb +1 -0
  166. data/lib/datadog/tracing/transport/http.rb +4 -0
  167. data/lib/datadog/tracing/transport/io/client.rb +1 -0
  168. data/lib/datadog/tracing/workers/trace_writer.rb +1 -1
  169. data/lib/datadog/tracing/workers.rb +2 -2
  170. data/lib/datadog/tracing/writer.rb +26 -28
  171. data/lib/datadog/version.rb +1 -1
  172. metadata +40 -15
  173. 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',
@@ -197,6 +197,14 @@ module Datadog
197
197
  o.type :bool, nilable: true
198
198
  o.env 'DD_APPSEC_SCA_ENABLED'
199
199
  end
200
+
201
+ settings :standalone do
202
+ option :enabled do |o|
203
+ o.type :bool
204
+ o.env 'DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED'
205
+ o.default false
206
+ end
207
+ end
200
208
  end
201
209
  end
202
210
  end
@@ -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)
@@ -28,20 +28,6 @@ module Datadog
28
28
 
29
29
  multiplex_return
30
30
  end
31
-
32
- private
33
-
34
- def active_trace
35
- return unless defined?(Datadog::Tracing)
36
-
37
- Datadog::Tracing.active_trace
38
- end
39
-
40
- def active_span
41
- return unless defined?(Datadog::Tracing)
42
-
43
- Datadog::Tracing.active_span
44
- end
45
31
  end
46
32
  end
47
33
  end
@@ -19,7 +19,7 @@ module Datadog
19
19
  end
20
20
 
21
21
  def arguments
22
- @arguments ||= create_arguments_hash
22
+ @arguments ||= build_arguments_hash
23
23
  end
24
24
 
25
25
  def queries
@@ -28,41 +28,77 @@ module Datadog
28
28
 
29
29
  private
30
30
 
31
- def create_arguments_hash
32
- args = {}
33
- @multiplex.queries.each_with_index do |query, index|
34
- resolver_args = {}
35
- resolver_dirs = {}
36
- selections = (query.selected_operation.selections.dup if query.selected_operation) || []
37
- # Iterative tree traversal
38
- while selections.any?
39
- selection = selections.shift
40
- set_hash_with_variables(resolver_args, selection.arguments, query.provided_variables)
41
- selection.directives.each do |dir|
42
- resolver_dirs[dir.name] ||= {}
43
- set_hash_with_variables(resolver_dirs[dir.name], dir.arguments, query.provided_variables)
44
- end
45
- selections.concat(selection.selections)
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
+ )
46
70
  end
47
- next if resolver_args.empty? && resolver_dirs.empty?
48
71
 
49
- args_resolver = (args[query.operation_name || "query#{index + 1}"] ||= [])
50
- # We don't want to add empty hashes so we check again if the arguments and directives are empty
51
- args_resolver << resolver_args unless resolver_args.empty?
52
- args_resolver << resolver_dirs unless resolver_dirs.empty?
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)
53
89
  end
54
- args
55
90
  end
56
91
 
57
- # Set the resolver hash (resolver_args and resolver_dirs) with the arguments and provided variables
58
- def set_hash_with_variables(resolver_hash, arguments, provided_variables)
59
- arguments.each do |arg|
60
- resolver_hash[arg.name] =
61
- if arg.value.is_a?(::GraphQL::Language::Nodes::VariableIdentifier)
62
- provided_variables[arg.value.name]
63
- else
64
- arg.value
65
- end
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
66
102
  end
67
103
  end
68
104
  end
@@ -24,27 +24,26 @@ module Datadog
24
24
  gateway.watch('graphql.multiplex', :appsec) do |stack, gateway_multiplex|
25
25
  block = false
26
26
  event = nil
27
+
27
28
  scope = AppSec::Scope.active_scope
28
29
 
29
- AppSec::Reactive::Operation.new('graphql.multiplex') do |op|
30
- GraphQL::Reactive::Multiplex.subscribe(op, scope.processor_context) do |result|
31
- event = {
32
- waf_result: result,
33
- trace: scope.trace,
34
- span: scope.service_entry_span,
35
- multiplex: gateway_multiplex,
36
- actions: result.actions
37
- }
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
+ }
38
40
 
39
- if scope.service_entry_span
40
- scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
41
- scope.service_entry_span.set_tag('appsec.event', 'true')
41
+ Datadog::AppSec::Event.tag_and_keep!(scope, result)
42
+ scope.processor_context.events << event
42
43
  end
43
44
 
44
- scope.processor_context.events << event
45
+ block = GraphQL::Reactive::Multiplex.publish(op, gateway_multiplex)
45
46
  end
46
-
47
- block = GraphQL::Reactive::Multiplex.publish(op, gateway_multiplex)
48
47
  end
49
48
 
50
49
  next [nil, [[:block, event]]] if block
@@ -13,6 +13,13 @@ module Datadog
13
13
 
14
14
  MINIMUM_VERSION = Gem::Version.new('2.0.19')
15
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
+
16
23
  register_as :graphql, auto_patch: false
17
24
 
18
25
  def self.version
@@ -24,13 +31,19 @@ module Datadog
24
31
  end
25
32
 
26
33
  def self.compatible?
27
- super && version >= MINIMUM_VERSION
34
+ super && version >= MINIMUM_VERSION && ast_node_classes_defined?
28
35
  end
29
36
 
30
37
  def self.auto_instrument?
31
38
  true
32
39
  end
33
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
+
34
47
  def patcher
35
48
  Patcher
36
49
  end
@@ -25,30 +25,17 @@ module Datadog
25
25
  Datadog.logger.debug { "reacted to #{ADDRESSES.inspect}: #{values.inspect}" }
26
26
  arguments = values[0]
27
27
 
28
- waf_args = {
28
+ persistent_data = {
29
29
  'graphql.server.all_resolvers' => arguments
30
30
  }
31
31
 
32
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
33
+ result = waf_context.run(persistent_data, {}, waf_timeout)
34
+
35
+ next if result.status != :match
36
+
37
+ yield result
38
+ throw(:block, true) unless result.actions.empty?
52
39
  end
53
40
  end
54
41
  end
@@ -45,14 +45,11 @@ module Datadog
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
@@ -41,11 +41,9 @@ module Datadog
41
41
  actions: result.actions
42
42
  }
43
43
 
44
- if scope.service_entry_span
45
- scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
46
- scope.service_entry_span.set_tag('appsec.event', 'true')
47
- end
48
-
44
+ # We want to keep the trace in case of security event
45
+ scope.trace.keep! if scope.trace
46
+ Datadog::AppSec::Event.tag_and_keep!(scope, result)
49
47
  scope.processor_context.events << event
50
48
  end
51
49
  end
@@ -85,11 +83,9 @@ module Datadog
85
83
  actions: result.actions
86
84
  }
87
85
 
88
- if scope.service_entry_span
89
- scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
90
- scope.service_entry_span.set_tag('appsec.event', 'true')
91
- end
92
-
86
+ # We want to keep the trace in case of security event
87
+ scope.trace.keep! if scope.trace
88
+ Datadog::AppSec::Event.tag_and_keep!(scope, result)
93
89
  scope.processor_context.events << event
94
90
  end
95
91
  end
@@ -129,11 +125,9 @@ module Datadog
129
125
  actions: result.actions
130
126
  }
131
127
 
132
- if scope.service_entry_span
133
- scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
134
- scope.service_entry_span.set_tag('appsec.event', 'true')
135
- end
136
-
128
+ # We want to keep the trace in case of security event
129
+ scope.trace.keep! if scope.trace
130
+ Datadog::AppSec::Event.tag_and_keep!(scope, result)
137
131
  scope.processor_context.events << event
138
132
  end
139
133
  end
@@ -33,6 +33,7 @@ module Datadog
33
33
  def self.subscribe(op, waf_context)
34
34
  op.subscribe(*ADDRESSES) do |*values|
35
35
  Datadog.logger.debug { "reacted to #{ADDRESSES.inspect}: #{values.inspect}" }
36
+
36
37
  headers = values[0]
37
38
  headers_no_cookies = headers.dup.tap { |h| h.delete('cookie') }
38
39
  uri_raw = values[1]
@@ -41,7 +42,7 @@ module Datadog
41
42
  client_ip = values[4]
42
43
  request_method = values[5]
43
44
 
44
- waf_args = {
45
+ persistent_data = {
45
46
  'server.request.cookies' => cookies,
46
47
  'server.request.query' => query,
47
48
  'server.request.uri.raw' => uri_raw,
@@ -52,25 +53,12 @@ module Datadog
52
53
  }
53
54
 
54
55
  waf_timeout = Datadog.configuration.appsec.waf_timeout
55
- result = waf_context.run(waf_args, waf_timeout)
56
-
57
- Datadog.logger.debug { "WAF TIMEOUT: #{result.inspect}" } if result.timeout
56
+ result = waf_context.run(persistent_data, {}, waf_timeout)
58
57
 
59
- case result.status
60
- when :match
61
- Datadog.logger.debug { "WAF: #{result.inspect}" }
58
+ next if result.status != :match
62
59
 
63
- yield result
64
- throw(:block, true) unless result.actions.empty?
65
- when :ok
66
- Datadog.logger.debug { "WAF OK: #{result.inspect}" }
67
- when :invalid_call
68
- Datadog.logger.debug { "WAF CALL ERROR: #{result.inspect}" }
69
- when :invalid_rule, :invalid_flow, :no_rule
70
- Datadog.logger.debug { "WAF RULE ERROR: #{result.inspect}" }
71
- else
72
- Datadog.logger.debug { "WAF UNKNOWN: #{result.status.inspect} #{result.inspect}" }
73
- end
60
+ yield result
61
+ throw(:block, true) unless result.actions.empty?
74
62
  end
75
63
  end
76
64
  end
@@ -26,30 +26,17 @@ module Datadog
26
26
  Datadog.logger.debug { "reacted to #{ADDRESSES.inspect}: #{values.inspect}" }
27
27
  body = values[0]
28
28
 
29
- waf_args = {
29
+ persistent_data = {
30
30
  'server.request.body' => body,
31
31
  }
32
32
 
33
33
  waf_timeout = Datadog.configuration.appsec.waf_timeout
34
- result = waf_context.run(waf_args, waf_timeout)
35
-
36
- Datadog.logger.debug { "WAF TIMEOUT: #{result.inspect}" } if result.timeout
37
-
38
- case result.status
39
- when :match
40
- Datadog.logger.debug { "WAF: #{result.inspect}" }
41
-
42
- yield result
43
- throw(:block, true) unless result.actions.empty?
44
- when :ok
45
- Datadog.logger.debug { "WAF OK: #{result.inspect}" }
46
- when :invalid_call
47
- Datadog.logger.debug { "WAF CALL ERROR: #{result.inspect}" }
48
- when :invalid_rule, :invalid_flow, :no_rule
49
- Datadog.logger.debug { "WAF RULE ERROR: #{result.inspect}" }
50
- else
51
- Datadog.logger.debug { "WAF UNKNOWN: #{result.status.inspect} #{result.inspect}" }
52
- end
34
+ result = waf_context.run(persistent_data, {}, waf_timeout)
35
+
36
+ next if result.status != :match
37
+
38
+ yield result
39
+ throw(:block, true) unless result.actions.empty?
53
40
  end
54
41
  end
55
42
  end
@@ -30,32 +30,19 @@ module Datadog
30
30
  response_headers = values[1]
31
31
  response_headers_no_cookies = response_headers.dup.tap { |h| h.delete('set-cookie') }
32
32
 
33
- waf_args = {
33
+ persistent_data = {
34
34
  'server.response.status' => response_status.to_s,
35
35
  'server.response.headers' => response_headers,
36
36
  'server.response.headers.no_cookies' => response_headers_no_cookies,
37
37
  }
38
38
 
39
39
  waf_timeout = Datadog.configuration.appsec.waf_timeout
40
- result = waf_context.run(waf_args, waf_timeout)
40
+ result = waf_context.run(persistent_data, {}, waf_timeout)
41
41
 
42
- Datadog.logger.debug { "WAF TIMEOUT: #{result.inspect}" } if result.timeout
42
+ next if result.status != :match
43
43
 
44
- case result.status
45
- when :match
46
- Datadog.logger.debug { "WAF: #{result.inspect}" }
47
-
48
- yield result
49
- throw(:block, true) unless result.actions.empty?
50
- when :ok
51
- Datadog.logger.debug { "WAF OK: #{result.inspect}" }
52
- when :invalid_call
53
- Datadog.logger.debug { "WAF CALL ERROR: #{result.inspect}" }
54
- when :invalid_rule, :invalid_flow, :no_rule
55
- Datadog.logger.debug { "WAF RULE ERROR: #{result.inspect}" }
56
- else
57
- Datadog.logger.debug { "WAF UNKNOWN: #{result.status.inspect} #{result.inspect}" }
58
- end
44
+ yield result
45
+ throw(:block, true) unless result.actions.empty?
59
46
  end
60
47
  end
61
48
  end
@@ -150,7 +150,9 @@ module Datadog
150
150
 
151
151
  return unless trace && span
152
152
 
153
- span.set_tag('_dd.appsec.enabled', 1)
153
+ span.set_metric(Datadog::AppSec::Ext::TAG_APPSEC_ENABLED, 1)
154
+ # We add this tag when ASM standalone is enabled to make sure we don't bill APM
155
+ span.set_metric(Datadog::AppSec::Ext::TAG_APM_ENABLED, 0) if Datadog.configuration.appsec.standalone.enabled
154
156
  span.set_tag('_dd.runtime_family', 'ruby')
155
157
  span.set_tag('_dd.appsec.waf.version', Datadog::AppSec::WAF::VERSION::BASE_STRING)
156
158
 
@@ -38,11 +38,9 @@ module Datadog
38
38
  actions: result.actions
39
39
  }
40
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
-
41
+ # We want to keep the trace in case of security event
42
+ scope.trace.keep! if scope.trace
43
+ Datadog::AppSec::Event.tag_and_keep!(scope, result)
46
44
  scope.processor_context.events << event
47
45
  end
48
46
  end