datadog 2.3.0 → 2.4.0

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