datadog 2.3.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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -1
  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/collectors_cpu_and_wall_time_worker.c +148 -30
  6. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +4 -2
  7. data/ext/datadog_profiling_native_extension/collectors_stack.c +89 -46
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +580 -29
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +9 -1
  10. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +0 -27
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -4
  12. data/ext/datadog_profiling_native_extension/extconf.rb +38 -21
  13. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +50 -0
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +75 -0
  15. data/ext/datadog_profiling_native_extension/heap_recorder.c +20 -6
  16. data/ext/datadog_profiling_native_extension/http_transport.c +38 -6
  17. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +52 -1
  18. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +3 -0
  19. data/ext/datadog_profiling_native_extension/profiling.c +1 -1
  20. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  21. data/ext/libdatadog_api/crashtracker.c +20 -18
  22. data/ext/libdatadog_api/datadog_ruby_common.c +0 -27
  23. data/ext/libdatadog_api/datadog_ruby_common.h +0 -4
  24. data/ext/libdatadog_extconf_helpers.rb +1 -1
  25. data/lib/datadog/appsec/assets/waf_rules/recommended.json +2184 -108
  26. data/lib/datadog/appsec/assets/waf_rules/strict.json +1430 -2
  27. data/lib/datadog/appsec/component.rb +29 -8
  28. data/lib/datadog/appsec/configuration/settings.rb +2 -2
  29. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +1 -0
  30. data/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb +21 -0
  31. data/lib/datadog/appsec/contrib/devise/patcher.rb +12 -2
  32. data/lib/datadog/appsec/contrib/graphql/appsec_trace.rb +0 -14
  33. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +67 -31
  34. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +18 -15
  35. data/lib/datadog/appsec/contrib/graphql/integration.rb +14 -1
  36. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +2 -5
  37. data/lib/datadog/appsec/event.rb +1 -1
  38. data/lib/datadog/appsec/processor/rule_loader.rb +3 -1
  39. data/lib/datadog/appsec/processor/rule_merger.rb +33 -15
  40. data/lib/datadog/appsec/processor.rb +36 -37
  41. data/lib/datadog/appsec/rate_limiter.rb +25 -40
  42. data/lib/datadog/appsec/remote.rb +7 -3
  43. data/lib/datadog/appsec.rb +2 -2
  44. data/lib/datadog/core/configuration/components.rb +4 -3
  45. data/lib/datadog/core/configuration/settings.rb +84 -5
  46. data/lib/datadog/core/crashtracking/component.rb +1 -1
  47. data/lib/datadog/core/environment/execution.rb +5 -5
  48. data/lib/datadog/core/metrics/client.rb +7 -0
  49. data/lib/datadog/core/rate_limiter.rb +183 -0
  50. data/lib/datadog/core/remote/client/capabilities.rb +4 -3
  51. data/lib/datadog/core/remote/component.rb +4 -2
  52. data/lib/datadog/core/remote/negotiation.rb +4 -4
  53. data/lib/datadog/core/remote/tie.rb +2 -0
  54. data/lib/datadog/core/runtime/metrics.rb +1 -1
  55. data/lib/datadog/core/telemetry/component.rb +2 -0
  56. data/lib/datadog/core/telemetry/event.rb +12 -7
  57. data/lib/datadog/core/telemetry/logger.rb +51 -0
  58. data/lib/datadog/core/telemetry/logging.rb +50 -14
  59. data/lib/datadog/core/telemetry/request.rb +13 -1
  60. data/lib/datadog/core/utils/time.rb +12 -0
  61. data/lib/datadog/di/code_tracker.rb +168 -0
  62. data/lib/datadog/di/configuration/settings.rb +163 -0
  63. data/lib/datadog/di/configuration.rb +11 -0
  64. data/lib/datadog/di/error.rb +31 -0
  65. data/lib/datadog/di/extensions.rb +16 -0
  66. data/lib/datadog/di/probe.rb +133 -0
  67. data/lib/datadog/di/probe_builder.rb +41 -0
  68. data/lib/datadog/di/redactor.rb +188 -0
  69. data/lib/datadog/di/serializer.rb +193 -0
  70. data/lib/datadog/di.rb +14 -0
  71. data/lib/datadog/opentelemetry/sdk/propagator.rb +2 -0
  72. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +12 -10
  73. data/lib/datadog/profiling/collectors/info.rb +12 -3
  74. data/lib/datadog/profiling/collectors/thread_context.rb +26 -0
  75. data/lib/datadog/profiling/component.rb +20 -4
  76. data/lib/datadog/profiling/http_transport.rb +6 -1
  77. data/lib/datadog/profiling/scheduler.rb +2 -0
  78. data/lib/datadog/profiling/stack_recorder.rb +3 -0
  79. data/lib/datadog/single_step_instrument.rb +12 -0
  80. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +8 -12
  81. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +5 -0
  82. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +78 -0
  83. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/patcher.rb +33 -0
  84. data/lib/datadog/tracing/contrib/action_pack/patcher.rb +2 -0
  85. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +4 -0
  86. data/lib/datadog/tracing/contrib/active_record/events/instantiation.rb +3 -1
  87. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +3 -1
  88. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +5 -1
  89. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +5 -0
  90. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  91. data/lib/datadog/tracing/contrib/faraday/middleware.rb +9 -0
  92. data/lib/datadog/tracing/contrib/grape/endpoint.rb +19 -0
  93. data/lib/datadog/tracing/contrib/graphql/patcher.rb +9 -12
  94. data/lib/datadog/tracing/contrib/graphql/trace_patcher.rb +3 -3
  95. data/lib/datadog/tracing/contrib/graphql/tracing_patcher.rb +3 -3
  96. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +13 -9
  97. data/lib/datadog/tracing/contrib/graphql/unified_trace_patcher.rb +6 -3
  98. data/lib/datadog/tracing/contrib/http/instrumentation.rb +18 -15
  99. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +6 -5
  100. data/lib/datadog/tracing/contrib/httpclient/patcher.rb +1 -14
  101. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +5 -0
  102. data/lib/datadog/tracing/contrib/httprb/patcher.rb +1 -14
  103. data/lib/datadog/tracing/contrib/lograge/patcher.rb +1 -2
  104. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
  105. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +13 -6
  106. data/lib/datadog/tracing/contrib/patcher.rb +2 -1
  107. data/lib/datadog/tracing/contrib/presto/patcher.rb +1 -13
  108. data/lib/datadog/tracing/contrib/rack/middlewares.rb +27 -0
  109. data/lib/datadog/tracing/contrib/redis/tags.rb +4 -0
  110. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +4 -0
  111. data/lib/datadog/tracing/contrib/stripe/request.rb +3 -2
  112. data/lib/datadog/tracing/distributed/propagation.rb +7 -0
  113. data/lib/datadog/tracing/metadata/ext.rb +2 -0
  114. data/lib/datadog/tracing/remote.rb +5 -2
  115. data/lib/datadog/tracing/sampling/matcher.rb +6 -1
  116. data/lib/datadog/tracing/sampling/rate_sampler.rb +1 -1
  117. data/lib/datadog/tracing/sampling/rule.rb +2 -0
  118. data/lib/datadog/tracing/sampling/rule_sampler.rb +9 -5
  119. data/lib/datadog/tracing/sampling/span/ext.rb +1 -1
  120. data/lib/datadog/tracing/sampling/span/rule.rb +2 -2
  121. data/lib/datadog/tracing/trace_operation.rb +26 -2
  122. data/lib/datadog/tracing/tracer.rb +14 -12
  123. data/lib/datadog/tracing/transport/http/client.rb +1 -0
  124. data/lib/datadog/tracing/transport/io/client.rb +1 -0
  125. data/lib/datadog/tracing/workers/trace_writer.rb +1 -1
  126. data/lib/datadog/tracing/workers.rb +1 -1
  127. data/lib/datadog/version.rb +1 -1
  128. metadata +25 -8
  129. 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)
@@ -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,30 @@ 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
+ }
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
38
45
 
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')
46
+ scope.processor_context.events << event
42
47
  end
43
48
 
44
- scope.processor_context.events << event
49
+ block = GraphQL::Reactive::Multiplex.publish(op, gateway_multiplex)
45
50
  end
46
-
47
- block = GraphQL::Reactive::Multiplex.publish(op, gateway_multiplex)
48
51
  end
49
52
 
50
53
  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
@@ -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
@@ -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
@@ -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)
@@ -73,13 +73,15 @@ module Datadog
73
73
 
74
74
  attr_reader :diagnostics, :addresses
75
75
 
76
- def initialize(ruleset:)
76
+ def initialize(ruleset:, telemetry:)
77
77
  @diagnostics = nil
78
78
  @addresses = []
79
79
  settings = Datadog.configuration.appsec
80
+ @telemetry = telemetry
80
81
 
81
- unless load_libddwaf && create_waf_handle(settings, ruleset)
82
- Datadog.logger.warn { 'AppSec is disabled, see logged errors above' }
82
+ # TODO: Refactor to make it easier to test
83
+ unless require_libddwaf && libddwaf_provides_waf? && create_waf_handle(settings, ruleset)
84
+ Datadog.logger.warn('AppSec is disabled, see logged errors above')
83
85
  end
84
86
  end
85
87
 
@@ -97,8 +99,27 @@ module Datadog
97
99
 
98
100
  private
99
101
 
100
- def load_libddwaf
101
- Processor.require_libddwaf && Processor.libddwaf_provides_waf?
102
+ # libddwaf raises a LoadError on unsupported platforms; it may at some
103
+ # point succeed in being required yet not provide a specific needed feature.
104
+ def require_libddwaf
105
+ Datadog.logger.debug { "libddwaf platform: #{libddwaf_platform}" }
106
+
107
+ require 'libddwaf'
108
+
109
+ true
110
+ rescue LoadError => e
111
+ Datadog.logger.error do
112
+ 'libddwaf failed to load,' \
113
+ "installed platform: #{libddwaf_platform} ruby platforms: #{ruby_platforms} error: #{e.inspect}"
114
+ end
115
+ @telemetry.report(e, description: 'libddwaf failed to load')
116
+
117
+ false
118
+ end
119
+
120
+ # check whether libddwaf is required *and* able to provide the needed feature
121
+ def libddwaf_provides_waf?
122
+ defined?(Datadog::AppSec::WAF) ? true : false
102
123
  end
103
124
 
104
125
  def create_waf_handle(settings, ruleset)
@@ -119,6 +140,7 @@ module Datadog
119
140
  Datadog.logger.error do
120
141
  "libddwaf failed to initialize, error: #{e.inspect}"
121
142
  end
143
+ @telemetry.report(e, description: 'libddwaf failed to initialize')
122
144
 
123
145
  @diagnostics = e.diagnostics if e.diagnostics
124
146
 
@@ -127,44 +149,21 @@ module Datadog
127
149
  Datadog.logger.error do
128
150
  "libddwaf failed to initialize, error: #{e.inspect}"
129
151
  end
152
+ @telemetry.report(e, description: 'libddwaf failed to initialize')
130
153
 
131
154
  false
132
155
  end
133
156
 
134
- class << self
135
- # check whether libddwaf is required *and* able to provide the needed feature
136
- def libddwaf_provides_waf?
137
- defined?(Datadog::AppSec::WAF) ? true : false
138
- end
139
-
140
- # libddwaf raises a LoadError on unsupported platforms; it may at some
141
- # point succeed in being required yet not provide a specific needed feature.
142
- def require_libddwaf
143
- Datadog.logger.debug { "libddwaf platform: #{libddwaf_platform}" }
144
-
145
- require 'libddwaf'
146
-
147
- true
148
- rescue LoadError => e
149
- Datadog.logger.error do
150
- 'libddwaf failed to load,' \
151
- "installed platform: #{libddwaf_platform} ruby platforms: #{ruby_platforms} error: #{e.inspect}"
152
- end
153
-
154
- false
155
- end
156
-
157
- def libddwaf_spec
158
- Gem.loaded_specs['libddwaf']
159
- end
160
-
161
- def libddwaf_platform
162
- libddwaf_spec ? libddwaf_spec.platform.to_s : 'unknown'
157
+ def libddwaf_platform
158
+ if Gem.loaded_specs['libddwaf']
159
+ Gem.loaded_specs['libddwaf'].platform.to_s
160
+ else
161
+ 'unknown'
163
162
  end
163
+ end
164
164
 
165
- def ruby_platforms
166
- Gem.platforms.map(&:to_s)
167
- end
165
+ def ruby_platforms
166
+ Gem.platforms.map(&:to_s)
168
167
  end
169
168
  end
170
169
  end