datadog 2.34.0 → 2.36.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 (168) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +114 -6
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +68 -31
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +1 -1
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +37 -18
  7. data/ext/datadog_profiling_native_extension/collectors_stack.h +8 -2
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +435 -314
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +9 -7
  10. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +7 -8
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -12
  12. data/ext/datadog_profiling_native_extension/extconf.rb +2 -2
  13. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +4 -43
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +15 -47
  15. data/ext/datadog_profiling_native_extension/heap_recorder.c +44 -26
  16. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +14 -35
  17. data/ext/datadog_profiling_native_extension/profiling.c +41 -4
  18. data/ext/datadog_profiling_native_extension/ruby_helpers.c +33 -34
  19. data/ext/datadog_profiling_native_extension/stack_recorder.c +30 -14
  20. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  21. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +4 -2
  22. data/ext/libdatadog_api/datadog_ruby_common.c +7 -8
  23. data/ext/libdatadog_api/datadog_ruby_common.h +0 -12
  24. data/ext/libdatadog_extconf_helpers.rb +1 -1
  25. data/lib/datadog/ai_guard/configuration.rb +1 -0
  26. data/lib/datadog/ai_guard/contrib/rack/request_middleware.rb +53 -39
  27. data/lib/datadog/ai_guard/evaluation.rb +6 -1
  28. data/lib/datadog/ai_guard/ext.rb +12 -1
  29. data/lib/datadog/appsec/api_security/route_extractor.rb +9 -0
  30. data/lib/datadog/appsec/component.rb +1 -1
  31. data/lib/datadog/appsec/configuration.rb +7 -0
  32. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +37 -4
  33. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +64 -19
  34. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -0
  35. data/lib/datadog/appsec/contrib/rack/buffered_input.rb +83 -0
  36. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +41 -3
  37. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +20 -7
  38. data/lib/datadog/appsec/contrib/rack/input_peeker.rb +92 -0
  39. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +9 -40
  40. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +33 -0
  41. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +17 -1
  42. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +20 -3
  43. data/lib/datadog/appsec/default_header_tags.rb +52 -0
  44. data/lib/datadog/core/configuration/components.rb +1 -0
  45. data/lib/datadog/core/configuration/settings.rb +11 -8
  46. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  47. data/lib/datadog/core/environment/process.rb +4 -2
  48. data/lib/datadog/core/remote/component.rb +1 -1
  49. data/lib/datadog/core/telemetry/event/app_started.rb +0 -21
  50. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  51. data/lib/datadog/core/utils/{array.rb → enumerable_compat.rb} +2 -2
  52. data/lib/datadog/core/utils/forking.rb +3 -1
  53. data/lib/datadog/core/utils/spawn_monkey_patch.rb +3 -1
  54. data/lib/datadog/core.rb +3 -0
  55. data/lib/datadog/di/base.rb +4 -1
  56. data/lib/datadog/di/component.rb +1 -1
  57. data/lib/datadog/error_tracking/collector.rb +2 -1
  58. data/lib/datadog/error_tracking/component.rb +2 -2
  59. data/lib/datadog/kit/tracing/method_tracer.rb +4 -1
  60. data/lib/datadog/opentelemetry/sdk/propagator.rb +9 -3
  61. data/lib/datadog/opentelemetry/sdk/span_processor.rb +4 -1
  62. data/lib/datadog/profiling/collectors/thread_context.rb +1 -4
  63. data/lib/datadog/profiling/component.rb +15 -25
  64. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +6 -5
  65. data/lib/datadog/profiling/stack_recorder.rb +0 -4
  66. data/lib/datadog/ruby_version.rb +25 -0
  67. data/lib/datadog/symbol_database/component.rb +306 -98
  68. data/lib/datadog/symbol_database/extractor.rb +226 -87
  69. data/lib/datadog/tracing/configuration/ext.rb +13 -0
  70. data/lib/datadog/tracing/configuration/settings.rb +17 -0
  71. data/lib/datadog/tracing/contrib/action_cable/events/broadcast.rb +4 -1
  72. data/lib/datadog/tracing/contrib/action_cable/events/perform_action.rb +4 -1
  73. data/lib/datadog/tracing/contrib/action_cable/events/transmit.rb +4 -1
  74. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +4 -1
  75. data/lib/datadog/tracing/contrib/action_mailer/event.rb +4 -1
  76. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +1 -0
  77. data/lib/datadog/tracing/contrib/action_view/events/render_partial.rb +4 -1
  78. data/lib/datadog/tracing/contrib/action_view/events/render_template.rb +4 -1
  79. data/lib/datadog/tracing/contrib/active_job/events/discard.rb +4 -1
  80. data/lib/datadog/tracing/contrib/active_job/events/enqueue.rb +4 -1
  81. data/lib/datadog/tracing/contrib/active_job/events/enqueue_at.rb +4 -1
  82. data/lib/datadog/tracing/contrib/active_job/events/enqueue_retry.rb +4 -1
  83. data/lib/datadog/tracing/contrib/active_job/events/perform.rb +4 -1
  84. data/lib/datadog/tracing/contrib/active_job/events/retry_stopped.rb +4 -1
  85. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  86. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +1 -0
  87. data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +1 -0
  88. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +1 -0
  89. data/lib/datadog/tracing/contrib/configuration/resolver.rb +7 -0
  90. data/lib/datadog/tracing/contrib/dalli/instrumentation.rb +1 -0
  91. data/lib/datadog/tracing/contrib/delayed_job/plugin.rb +2 -0
  92. data/lib/datadog/tracing/contrib/delayed_job/server_internal_tracer/worker.rb +1 -0
  93. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +1 -0
  94. data/lib/datadog/tracing/contrib/elasticsearch/quantize.rb +2 -2
  95. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +1 -0
  96. data/lib/datadog/tracing/contrib/ethon/multi_patch.rb +1 -0
  97. data/lib/datadog/tracing/contrib/excon/middleware.rb +1 -0
  98. data/lib/datadog/tracing/contrib/ext.rb +3 -0
  99. data/lib/datadog/tracing/contrib/faraday/middleware.rb +1 -0
  100. data/lib/datadog/tracing/contrib/grape/endpoint.rb +3 -0
  101. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +1 -0
  102. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +1 -0
  103. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +1 -0
  104. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +2 -0
  105. data/lib/datadog/tracing/contrib/grpc.rb +1 -0
  106. data/lib/datadog/tracing/contrib/hanami/action_tracer.rb +1 -0
  107. data/lib/datadog/tracing/contrib/hanami/renderer_policy_tracing.rb +1 -0
  108. data/lib/datadog/tracing/contrib/hanami/router_tracing.rb +1 -0
  109. data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +2 -0
  110. data/lib/datadog/tracing/contrib/http/instrumentation.rb +1 -0
  111. data/lib/datadog/tracing/contrib/http.rb +1 -0
  112. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +1 -0
  113. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +1 -0
  114. data/lib/datadog/tracing/contrib/kafka/event.rb +1 -0
  115. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +2 -0
  116. data/lib/datadog/tracing/contrib/karafka.rb +1 -0
  117. data/lib/datadog/tracing/contrib/mongodb/parsers.rb +5 -5
  118. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
  119. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +1 -0
  120. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +1 -0
  121. data/lib/datadog/tracing/contrib/opensearch/quantize.rb +2 -2
  122. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +1 -0
  123. data/lib/datadog/tracing/contrib/presto/instrumentation.rb +3 -0
  124. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +3 -0
  125. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +2 -2
  126. data/lib/datadog/tracing/contrib/que/tracer.rb +1 -0
  127. data/lib/datadog/tracing/contrib/racecar/event.rb +1 -0
  128. data/lib/datadog/tracing/contrib/rack/header_tagging.rb +23 -0
  129. data/lib/datadog/tracing/contrib/rack/middlewares.rb +4 -1
  130. data/lib/datadog/tracing/contrib/rack/route_inference.rb +3 -1
  131. data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +2 -0
  132. data/lib/datadog/tracing/contrib/rails/runner.rb +2 -0
  133. data/lib/datadog/tracing/contrib/rake/instrumentation.rb +4 -2
  134. data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +2 -0
  135. data/lib/datadog/tracing/contrib/resque/resque_job.rb +1 -0
  136. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +1 -0
  137. data/lib/datadog/tracing/contrib/roda/ext.rb +1 -0
  138. data/lib/datadog/tracing/contrib/roda/instrumentation.rb +4 -1
  139. data/lib/datadog/tracing/contrib/sequel/database.rb +1 -0
  140. data/lib/datadog/tracing/contrib/sequel/dataset.rb +1 -0
  141. data/lib/datadog/tracing/contrib/shoryuken/tracer.rb +1 -0
  142. data/lib/datadog/tracing/contrib/sidekiq/client_tracer.rb +1 -0
  143. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +2 -0
  144. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/heartbeat.rb +2 -0
  145. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/job_fetch.rb +1 -0
  146. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/redis_info.rb +1 -0
  147. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/scheduled_poller.rb +2 -0
  148. data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/stop.rb +1 -0
  149. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +3 -2
  150. data/lib/datadog/tracing/contrib/sidekiq.rb +1 -0
  151. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +1 -0
  152. data/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +1 -0
  153. data/lib/datadog/tracing/contrib/sneakers/tracer.rb +1 -0
  154. data/lib/datadog/tracing/contrib/sucker_punch/instrumentation.rb +1 -0
  155. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +1 -0
  156. data/lib/datadog/tracing/contrib/utils/quantization/{hash.rb → hash_formatter.rb} +1 -1
  157. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +2 -0
  158. data/lib/datadog/tracing/contrib/waterdrop.rb +1 -0
  159. data/lib/datadog/tracing/distributed/propagation.rb +33 -1
  160. data/lib/datadog/tracing/distributed/trace_context.rb +11 -2
  161. data/lib/datadog/tracing/metadata/ext.rb +7 -0
  162. data/lib/datadog/tracing/trace_digest.rb +7 -0
  163. data/lib/datadog/tracing/trace_operation.rb +4 -1
  164. data/lib/datadog/tracing/tracer.rb +4 -0
  165. data/lib/datadog/tracing/transport/traces.rb +2 -2
  166. data/lib/datadog/version.rb +1 -1
  167. data/lib/datadog.rb +4 -1
  168. metadata +13 -11
@@ -50,38 +50,83 @@ module Datadog
50
50
  selected_operation = query.selected_operation
51
51
  next unless selected_operation
52
52
 
53
- arguments_from_selections(selected_operation.selections, query.variables, args_hash)
53
+ arguments_from_selections(selected_operation.selections, query.variables, args_hash, query.fragments)
54
54
  end
55
55
  end
56
56
 
57
- def arguments_from_selections(selections, query_variables, args_hash)
57
+ def arguments_from_selections(selections, query_variables, args_hash, fragments, visited_fragments = {})
58
58
  selections.each do |selection|
59
- # rubocop:disable Style/ClassEqualityComparison
60
- next unless selection.class.name == Integration::AST_NODE_CLASS_NAMES[:field]
61
- # rubocop:enable Style/ClassEqualityComparison
62
-
63
- selection_name = selection.alias || selection.name
64
-
65
- if !selection.arguments.empty? || !selection.directives.empty?
66
- args_hash[selection_name] ||= []
67
- args_hash[selection_name] <<
68
- arguments_hash(selection.arguments, query_variables).merge!(
69
- arguments_from_directives(selection.directives, query_variables)
70
- )
59
+ case selection
60
+ when ::GraphQL::Language::Nodes::FragmentSpread
61
+ fragment_name = selection.name
62
+ append_arguments(
63
+ args_hash, fragment_name, nil, arguments_from_directives(selection.directives, query_variables)
64
+ )
65
+
66
+ next if visited_fragments[fragment_name]
67
+
68
+ fragment = fragments[fragment_name]
69
+ next unless fragment
70
+
71
+ append_arguments(
72
+ args_hash, fragment_name, nil, arguments_from_directives(fragment.directives, query_variables)
73
+ )
74
+
75
+ visited_fragments[fragment_name] = true
76
+ arguments_from_selections(
77
+ fragment.selections, query_variables, args_hash, fragments, visited_fragments
78
+ )
79
+ visited_fragments.delete(fragment_name)
80
+ when ::GraphQL::Language::Nodes::Field
81
+ selection_name = selection.alias || selection.name
82
+ field_arguments = arguments_hash(selection.arguments, query_variables) unless selection.arguments.empty?
83
+ append_arguments(
84
+ args_hash,
85
+ selection_name,
86
+ field_arguments,
87
+ arguments_from_directives(selection.directives, query_variables)
88
+ )
89
+
90
+ arguments_from_selections(
91
+ selection.selections, query_variables, args_hash, fragments, visited_fragments
92
+ )
93
+ when ::GraphQL::Language::Nodes::InlineFragment
94
+ append_arguments(
95
+ args_hash, selection.type.name, nil, arguments_from_directives(selection.directives, query_variables)
96
+ )
97
+
98
+ arguments_from_selections(
99
+ selection.selections, query_variables, args_hash, fragments, visited_fragments
100
+ )
71
101
  end
102
+ end
103
+ end
72
104
 
73
- arguments_from_selections(selection.selections, query_variables, args_hash)
105
+ def append_arguments(args_hash, selection_name, arguments, directive_arguments)
106
+ combined_arguments = if arguments
107
+ arguments.merge!(directive_arguments) if directive_arguments
108
+ arguments
109
+ else
110
+ directive_arguments
74
111
  end
112
+ return unless combined_arguments
113
+
114
+ args_hash[selection_name] ||= []
115
+ args_hash[selection_name] << combined_arguments
75
116
  end
76
117
 
77
118
  def arguments_from_directives(directives, query_variables)
78
- directives.each_with_object({}) do |directive, args_hash|
79
- # rubocop:disable Style/ClassEqualityComparison
80
- next unless directive.class.name == Integration::AST_NODE_CLASS_NAMES[:directive]
81
- # rubocop:enable Style/ClassEqualityComparison
119
+ return if directives.empty?
120
+
121
+ directive_arguments = directives.each_with_object({}) do |directive, args_hash|
122
+ next unless directive.is_a?(::GraphQL::Language::Nodes::Directive)
82
123
 
83
124
  args_hash[directive.name] = arguments_hash(directive.arguments, query_variables)
84
125
  end
126
+
127
+ return if directive_arguments.empty?
128
+
129
+ directive_arguments
85
130
  end
86
131
 
87
132
  def arguments_hash(arguments, query_variables)
@@ -16,6 +16,7 @@ module Datadog
16
16
  AST_NODE_CLASS_NAMES = {
17
17
  field: 'GraphQL::Language::Nodes::Field',
18
18
  directive: 'GraphQL::Language::Nodes::Directive',
19
+ fragment_spread: 'GraphQL::Language::Nodes::FragmentSpread',
19
20
  variable_identifier: 'GraphQL::Language::Nodes::VariableIdentifier',
20
21
  input_object: 'GraphQL::Language::Nodes::InputObject',
21
22
  }.freeze
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AppSec
5
+ module Contrib
6
+ module Rack
7
+ # Wraps a `rack.input` stream with a buffer placed in front of it.
8
+ # Every read drains the buffer first, then continues from the stream
9
+ #
10
+ # NOTE: Forward-only: no rewind, no seek
11
+ #
12
+ # NOTE: Rack 3 dropped the rewind requirement from the input stream contract
13
+ # @see https://github.com/rack/rack/blob/v3.2.6/SPEC.rdoc
14
+ class BufferedInput
15
+ # NOTE: Rack's multipart parser reads in 1 MiB chunks, used to bound
16
+ # {#each} the same way
17
+ # @see https://github.com/rack/rack/blob/v3.2.6/lib/rack/multipart/parser.rb#L54
18
+ READ_BUFSIZE_BYTES = 1_048_576
19
+
20
+ def initialize(stream, buffer:)
21
+ @stream = stream
22
+ @buffer = buffer
23
+ end
24
+
25
+ def read(length = nil, outbuf = nil)
26
+ if length.nil?
27
+ data = @buffer.read(nil, outbuf) || +''
28
+ more = @stream.read
29
+
30
+ data << more if more
31
+
32
+ return data
33
+ end
34
+
35
+ data = @buffer.read(length, outbuf)
36
+
37
+ if data.nil?
38
+ more = @stream.read(length, outbuf)
39
+ return more if more && !more.empty?
40
+
41
+ # NOTE: Match `IO#read(length, outbuf)` at EOF. Return nil and clear
42
+ # the caller's buffer so stale bytes are not mistaken for data
43
+ outbuf&.clear
44
+ return
45
+ end
46
+
47
+ remaining = length - data.bytesize
48
+ return data if remaining <= 0
49
+
50
+ more = @stream.read(remaining)
51
+ data << more if more
52
+
53
+ data
54
+ end
55
+
56
+ def gets
57
+ line = @buffer.gets
58
+
59
+ return @stream.gets if line.nil?
60
+ return line if line.end_with?("\n")
61
+
62
+ more = @stream.gets
63
+ more ? (line << more) : line
64
+ end
65
+
66
+ def each
67
+ while (chunk = read(READ_BUFSIZE_BYTES))
68
+ yield chunk
69
+ end
70
+
71
+ self
72
+ end
73
+
74
+ def close
75
+ @buffer.close
76
+ ensure
77
+ @stream.close
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../input_peeker'
3
4
  require_relative '../../../instrumentation/gateway/argument'
4
5
  require_relative '../../../../core/header_collection'
5
6
  require_relative '../../../../tracing/client_ip'
@@ -75,14 +76,51 @@ module Datadog
75
76
  end
76
77
 
77
78
  def form_hash
78
- # force form data processing
79
+ # NOTE: Rack populates `rack.request.form_hash` only as a side effect
80
+ # of {::Rack::Request#POST}, which reads and parses the body.
79
81
  request.POST if request.form_data?
80
82
 
81
- # usually Hash<String,String> but can be a more complex
82
- # Hash<String,String||Array||Hash> when e.g coming from JSON
83
+ # usually Hash[String, String] but can be a more complex
84
+ # Hash[String, (String|Array|Hash)] when e.g coming from JSON
83
85
  env['rack.request.form_hash']
84
86
  end
85
87
 
88
+ # Returns the request body size in bytes using all available methods,
89
+ # or nil when the size cannot be measured within the limit
90
+ #
91
+ # NOTE: The priority of the measurement is the following:
92
+ # size if it's known, content-length if provided, and buffering
93
+ # to the limit if unknown-length
94
+ #
95
+ # WARNING: The buffering path adds overhead for streaming web-servers
96
+ # (Rack 3+) when the body length is unknown
97
+ def body_bytesize(limit)
98
+ io = request.body
99
+
100
+ return 0 unless io
101
+ return io.size if io.respond_to?(:size)
102
+
103
+ # NOTE: {::Rack::Request#content_length} is a plain `CONTENT_LENGTH`
104
+ # getter forces no body read
105
+ content_length = request.content_length
106
+ return content_length.to_i if content_length
107
+
108
+ # NOTE: A parsed-body cache without a raw byte count means the input
109
+ # stream was consumed before measurement, consider it unknown-length
110
+ return if env.key?('rack.request.form_hash')
111
+
112
+ InputPeeker.peek_bytesize(env, limit: limit)
113
+ end
114
+
115
+ # Whether a request body can be collected without forcing a parse:
116
+ # either form data parseable on demand, or a body already parsed upstream
117
+ #
118
+ # NOTE: Rack does not parse JSON itself, a body parser middleware such as
119
+ # {Rack::JSONBodyParser} populates the form hash read by {#form_hash}
120
+ def collectable_body?
121
+ request.form_data? || request.parseable_data? || env.key?('rack.request.form_hash')
122
+ end
123
+
86
124
  def client_ip
87
125
  remote_ip = remote_addr
88
126
  header_collection = Datadog::Core::HeaderCollection.from_hash(headers)
@@ -87,9 +87,26 @@ module Datadog
87
87
  gateway.watch('rack.request.body') do |stack, gateway_request|
88
88
  context = gateway_request.env[AppSec::Ext::CONTEXT_KEY]
89
89
 
90
- persistent_data = {
91
- 'server.request.body' => gateway_request.form_hash
92
- }
90
+ request = gateway_request.request
91
+ next stack.call(request) unless gateway_request.collectable_body?
92
+
93
+ # NOTE: A limit of 0 disables request body collection entirely.
94
+ limit = Datadog.configuration.appsec.body_parsing_size_limit
95
+ next stack.call(request) if limit.zero?
96
+
97
+ persistent_data = {}
98
+ byte_length = gateway_request.body_bytesize(limit)
99
+
100
+ if byte_length
101
+ persistent_data['server.request.body.byte_length'] = byte_length
102
+
103
+ if byte_length <= limit
104
+ body = gateway_request.form_hash
105
+ persistent_data['server.request.body'] = body if body
106
+ end
107
+ end
108
+
109
+ next stack.call(request) if persistent_data.empty?
93
110
 
94
111
  result = context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
95
112
 
@@ -119,10 +136,6 @@ module Datadog
119
136
  next stack.call(gateway_request.request) if context.span.nil?
120
137
 
121
138
  gateway_request.headers.each do |name, value|
122
- if Ext::COLLECTABLE_REQUEST_HEADERS.include?(name)
123
- context.span["http.request.headers.#{name}"] ||= value
124
- end
125
-
126
139
  if context.state[:has_identity_event] && Ext::IDENTITY_COLLECTABLE_REQUEST_HEADERS.include?(name)
127
140
  context.span["http.request.headers.#{name}"] ||= value
128
141
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require_relative 'buffered_input'
5
+
6
+ module Datadog
7
+ module AppSec
8
+ module Contrib
9
+ module Rack
10
+ # Peeks at `rack.input` without changing what downstream Rack code can read
11
+ #
12
+ # @api private
13
+ module InputPeeker
14
+ module_function
15
+
16
+ # Peeks at `env['rack.input']` and returns the body bytesize when it
17
+ # can be measured within the given limit
18
+ #
19
+ # NOTE: Over-limit bodies cannot be fully measured, so returning `nil`
20
+ # is the tradeoff we accept
21
+ #
22
+ # WARNING: For forward-only input, replaces `env['rack.input']` with a
23
+ # replay stream over the peeked bytes, preserving the rest
24
+ # of the original stream
25
+ #
26
+ # @param env [Hash] Rack environment
27
+ # @param limit [Integer] Maximum body bytesize to measure
28
+ # @return [Integer, nil] `Integer` bytesize when measured within
29
+ # `limit`, including `0`; `nil` when input is missing, over the
30
+ # limit, or cannot be preserved for downstream reads
31
+ def peek_bytesize(env, limit:)
32
+ rack_input = env['rack.input']
33
+ return unless rack_input
34
+
35
+ rewindable = rewind? && rack_input.respond_to?(:rewind)
36
+
37
+ # NOTE: Rack 2 requires `rack.input` to be rewindable. Rewind before peeking
38
+ # in case an upstream framework already consumed part of the stream
39
+ return if rewindable && !rewind(rack_input)
40
+
41
+ buffer = peek(rack_input, limit)
42
+ over_limit = buffer.bytesize > limit
43
+
44
+ if rewindable
45
+ # NOTE: If we cannot rewind after peeking, downstream code would observe
46
+ # a partially consumed body. Treat it as not safely collectable
47
+ return unless rewind(rack_input)
48
+ else
49
+ env['rack.input'] = if over_limit
50
+ BufferedInput.new(rack_input, buffer: StringIO.new(buffer))
51
+ else
52
+ StringIO.new(buffer)
53
+ end
54
+ end
55
+
56
+ over_limit ? nil : buffer.bytesize
57
+ end
58
+
59
+ private_class_method def rewind?
60
+ return @rewind if defined?(@rewind)
61
+
62
+ @rewind = ::Gem::Version.new(::Rack.release) < ::Gem::Version.new('3')
63
+ end
64
+
65
+ private_class_method def rewind(io)
66
+ io.rewind
67
+ true
68
+ rescue => e
69
+ Datadog.logger.debug { "AppSec: Failed to rewind `rack.input`: #{e.class}: #{e.message}" }
70
+ false
71
+ end
72
+
73
+ private_class_method def peek(io, limit)
74
+ # NOTE: Read one byte past the limit to distinguish an exact-limit body
75
+ # from an over-limit body without reading the whole stream.
76
+ max = limit + 1
77
+ buffer = +''.b
78
+
79
+ while buffer.bytesize <= limit
80
+ chunk = io.read(max - buffer.bytesize)
81
+ break if chunk.nil? || chunk.empty?
82
+
83
+ buffer << chunk
84
+ end
85
+
86
+ buffer
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -9,9 +9,11 @@ require_relative 'gateway/response'
9
9
  require_relative '../../event'
10
10
  require_relative '../../response'
11
11
  require_relative '../../api_security'
12
+ require_relative '../../default_header_tags'
12
13
  require_relative '../../security_event'
13
14
  require_relative '../../instrumentation/gateway'
14
15
 
16
+ require_relative '../../../core/header_collection'
15
17
  require_relative '../../../tracing/client_ip'
16
18
  require_relative '../../../tracing/contrib/rack/header_collection'
17
19
 
@@ -19,24 +21,6 @@ module Datadog
19
21
  module AppSec
20
22
  module Contrib
21
23
  module Rack
22
- RESPONSE_HEADERS_TAGS = %w[
23
- content-length
24
- content-type
25
- content-encoding
26
- content-language
27
- ].freeze
28
-
29
- WAF_VENDOR_HEADERS_TAGS = %w[
30
- X-Amzn-Trace-Id
31
- Cloudfront-Viewer-Ja3-Fingerprint
32
- Cf-Ray
33
- X-Cloud-Trace-Context
34
- X-Appgw-Trace-id
35
- X-SigSci-RequestID
36
- X-SigSci-Tags
37
- Akamai-User-Risk
38
- ].map(&:downcase).freeze
39
-
40
24
  # Topmost Rack middleware for AppSec
41
25
  # This should be inserted just below Datadog::Tracing::Contrib::Rack::TraceMiddleware
42
26
  class RequestMiddleware
@@ -44,7 +28,6 @@ module Datadog
44
28
  @app = app
45
29
 
46
30
  @oneshot_tags_sent = false
47
- @rack_headers = {}
48
31
  end
49
32
 
50
33
  # rubocop:disable Metrics/MethodLength
@@ -186,38 +169,28 @@ module Datadog
186
169
  end
187
170
  # standard:enable Metrics/MethodLength
188
171
 
189
- # standard:disable Metrics/MethodLength
190
172
  def add_request_tags(context, env)
191
173
  span = context.span
192
174
  return unless span
193
175
 
194
- # Always add WAF vendors headers
195
- WAF_VENDOR_HEADERS_TAGS.each do |lowercase_header|
196
- rack_header = to_rack_header(lowercase_header)
197
- span.set_tag("http.request.headers.#{lowercase_header}", env[rack_header]) if env[rack_header]
198
- end
199
-
200
- if span && span.get_tag(Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP).nil?
201
- request_header_collection = Datadog::Tracing::Contrib::Rack::Header::RequestHeaderCollection.new(env)
176
+ headers = Tracing::Contrib::Rack::Header::RequestHeaderCollection.new(env)
177
+ AppSec::DefaultHeaderTags.tag_request(span, headers)
202
178
 
179
+ if span.get_tag(Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP).nil?
203
180
  # always collect client ip, as this is part of AppSec provided functionality
204
181
  Datadog::Tracing::ClientIp.set_client_ip_tag!(
205
- span,
206
- headers: request_header_collection,
207
- remote_ip: env['REMOTE_ADDR']
182
+ span, headers: headers, remote_ip: env['REMOTE_ADDR']
208
183
  )
209
184
  end
210
185
  end
211
- # standard:enable Metrics/MethodLength
212
186
 
213
187
  def add_response_tags(context, response)
214
188
  span = context.span
215
189
  return unless span
216
190
 
217
- RESPONSE_HEADERS_TAGS.each do |name|
218
- value = response.headers[name]
219
- span.set_tag("http.response.headers.#{name}", value.to_s) if value
220
- end
191
+ AppSec::DefaultHeaderTags.tag_response(
192
+ span, Datadog::Core::HeaderCollection.from_hash(response.headers)
193
+ )
221
194
 
222
195
  unless response.headers.key?('content-length')
223
196
  length = ResponseBody.content_length(response.body)
@@ -228,10 +201,6 @@ module Datadog
228
201
  def oneshot_tags_sent?
229
202
  @oneshot_tags_sent
230
203
  end
231
-
232
- def to_rack_header(header)
233
- @rack_headers[header] ||= Datadog::Tracing::Contrib::Rack::Header.to_rack_header(header)
234
- end
235
204
  end
236
205
  end
237
206
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../rack/input_peeker'
3
4
  require_relative '../../../instrumentation/gateway/argument'
4
5
 
5
6
  module Datadog
@@ -59,6 +60,38 @@ module Datadog
59
60
  excluded.include?(k)
60
61
  end
61
62
  end
63
+
64
+ # Returns the request body size in bytes using all available methods,
65
+ # or nil when the size cannot be measured within the limit
66
+ #
67
+ # NOTE: The priority of the measurement is the following:
68
+ # raw posted data, raw form vars, size if known, raw
69
+ # Content-Length, then buffering to the limit if unknown-length
70
+ def body_bytesize(limit)
71
+ raw_body = env['RAW_POST_DATA']
72
+ return raw_body.bytesize if raw_body
73
+
74
+ form_vars = env['rack.request.form_vars']
75
+ return form_vars.bytesize if form_vars
76
+
77
+ io = request.body
78
+ return 0 unless io
79
+ return io.size if io.respond_to?(:size)
80
+
81
+ # NOTE: Read raw `CONTENT_LENGTH` as {ActionDispatch::Request#content_length}
82
+ # drains `rack.input` into `RAW_POST_DATA` on chunked Transfer-Encoding
83
+ content_length = env['CONTENT_LENGTH']
84
+ return content_length.to_i if content_length
85
+
86
+ # NOTE: An already-read body (e.g. late-parsed multipart on Rack 3+) peeks
87
+ # as 0, so we skip byte_length but still collect the parsed body
88
+ begin
89
+ Rack::InputPeeker.peek_bytesize(env, limit: limit)
90
+ rescue => e
91
+ Datadog.logger.debug { "AppSec: Failed to measure Rails request body: #{e.class}: #{e.message}" }
92
+ nil
93
+ end
94
+ end
62
95
  end
63
96
  end
64
97
  end
@@ -23,12 +23,28 @@ module Datadog
23
23
  def watch_request_action(gateway = Instrumentation.gateway)
24
24
  gateway.watch('rails.request.action') do |stack, gateway_request|
25
25
  context = gateway_request.env[AppSec::Ext::CONTEXT_KEY]
26
+ limit = Datadog.configuration.appsec.body_parsing_size_limit
26
27
 
27
28
  persistent_data = {
28
- 'server.request.body' => gateway_request.parsed_body,
29
29
  'server.request.path_params' => gateway_request.route_params
30
30
  }
31
31
 
32
+ unless limit.zero?
33
+ byte_length = gateway_request.body_bytesize(limit)
34
+
35
+ if byte_length
36
+ # NOTE: Params may be parsed before this hook, leaving the
37
+ # body stream at EOF. Keep byte_length unset for that
38
+ # case, but still inspect cached params
39
+ persistent_data['server.request.body.byte_length'] = byte_length if byte_length.positive?
40
+
41
+ if byte_length <= limit
42
+ body = gateway_request.parsed_body
43
+ persistent_data['server.request.body'] = body unless body.nil? || body.empty?
44
+ end
45
+ end
46
+ end
47
+
32
48
  result = context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
33
49
 
34
50
  if result.match?
@@ -27,9 +27,26 @@ module Datadog
27
27
 
28
28
  context.state[:web_framework] = 'sinatra'
29
29
 
30
- persistent_data = {
31
- 'server.request.body' => gateway_request.form_hash
32
- }
30
+ request = gateway_request.request
31
+ next stack.call(request) unless gateway_request.collectable_body?
32
+
33
+ # NOTE: A limit of 0 disables request body collection entirely.
34
+ limit = Datadog.configuration.appsec.body_parsing_size_limit
35
+ next stack.call(request) if limit.zero?
36
+
37
+ persistent_data = {}
38
+ byte_length = gateway_request.body_bytesize(limit)
39
+
40
+ if byte_length
41
+ persistent_data['server.request.body.byte_length'] = byte_length
42
+
43
+ if byte_length <= limit
44
+ body = gateway_request.form_hash
45
+ persistent_data['server.request.body'] = body if body
46
+ end
47
+ end
48
+
49
+ next stack.call(request) if persistent_data.empty?
33
50
 
34
51
  result = context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
35
52
 
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AppSec
5
+ # Sets the always-on span tags built from a fixed allowlist of request and
6
+ # response headers.
7
+ #
8
+ # NOTE: Unlike {Event#record}, which reports request headers only when
9
+ # a security event fires, these are tagged on every request/response
10
+ #
11
+ # @api private
12
+ module DefaultHeaderTags
13
+ # NOTE: Contains additional WAF vendor headers
14
+ REQUEST_HEADERS_TAGS = %w[
15
+ accept
16
+ content-type
17
+ user-agent
18
+ akamai-user-risk
19
+ x-amzn-trace-id
20
+ x-cloud-trace-context
21
+ x-appgw-trace-id
22
+ x-sigsci-requestid
23
+ x-sigsci-tags
24
+ cf-ray
25
+ cloudfront-viewer-ja3-fingerprint
26
+ ].freeze
27
+
28
+ RESPONSE_HEADERS_TAGS = %w[
29
+ content-type
30
+ content-length
31
+ content-encoding
32
+ content-language
33
+ ].freeze
34
+
35
+ module_function
36
+
37
+ def tag_request(span, headers)
38
+ REQUEST_HEADERS_TAGS.each do |name|
39
+ value = headers.get(name)
40
+ span.set_tag("http.request.headers.#{name}", value) if value
41
+ end
42
+ end
43
+
44
+ def tag_response(span, headers)
45
+ RESPONSE_HEADERS_TAGS.each do |name|
46
+ value = headers.get(name)
47
+ span.set_tag("http.response.headers.#{name}", value.to_s) if value
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -194,6 +194,7 @@ module Datadog
194
194
  remote&.after_fork
195
195
  crashtracker&.update_on_fork
196
196
  ProcessDiscovery.after_fork
197
+ symbol_database&.after_fork!
197
198
  end
198
199
 
199
200
  # Hot-swaps with a new sampler.