datadog 2.2.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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)