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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +114 -6
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +68 -31
- data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +1 -1
- data/ext/datadog_profiling_native_extension/collectors_stack.c +37 -18
- data/ext/datadog_profiling_native_extension/collectors_stack.h +8 -2
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +435 -314
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +9 -7
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +7 -8
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -12
- data/ext/datadog_profiling_native_extension/extconf.rb +2 -2
- data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +4 -43
- data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +15 -47
- data/ext/datadog_profiling_native_extension/heap_recorder.c +44 -26
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +14 -35
- data/ext/datadog_profiling_native_extension/profiling.c +41 -4
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +33 -34
- data/ext/datadog_profiling_native_extension/stack_recorder.c +30 -14
- data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
- data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +4 -2
- data/ext/libdatadog_api/datadog_ruby_common.c +7 -8
- data/ext/libdatadog_api/datadog_ruby_common.h +0 -12
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/ai_guard/configuration.rb +1 -0
- data/lib/datadog/ai_guard/contrib/rack/request_middleware.rb +53 -39
- data/lib/datadog/ai_guard/evaluation.rb +6 -1
- data/lib/datadog/ai_guard/ext.rb +12 -1
- data/lib/datadog/appsec/api_security/route_extractor.rb +9 -0
- data/lib/datadog/appsec/component.rb +1 -1
- data/lib/datadog/appsec/configuration.rb +7 -0
- data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +37 -4
- data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +64 -19
- data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -0
- data/lib/datadog/appsec/contrib/rack/buffered_input.rb +83 -0
- data/lib/datadog/appsec/contrib/rack/gateway/request.rb +41 -3
- data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +20 -7
- data/lib/datadog/appsec/contrib/rack/input_peeker.rb +92 -0
- data/lib/datadog/appsec/contrib/rack/request_middleware.rb +9 -40
- data/lib/datadog/appsec/contrib/rails/gateway/request.rb +33 -0
- data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +17 -1
- data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +20 -3
- data/lib/datadog/appsec/default_header_tags.rb +52 -0
- data/lib/datadog/core/configuration/components.rb +1 -0
- data/lib/datadog/core/configuration/settings.rb +11 -8
- data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
- data/lib/datadog/core/environment/process.rb +4 -2
- data/lib/datadog/core/remote/component.rb +1 -1
- data/lib/datadog/core/telemetry/event/app_started.rb +0 -21
- data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
- data/lib/datadog/core/utils/{array.rb → enumerable_compat.rb} +2 -2
- data/lib/datadog/core/utils/forking.rb +3 -1
- data/lib/datadog/core/utils/spawn_monkey_patch.rb +3 -1
- data/lib/datadog/core.rb +3 -0
- data/lib/datadog/di/base.rb +4 -1
- data/lib/datadog/di/component.rb +1 -1
- data/lib/datadog/error_tracking/collector.rb +2 -1
- data/lib/datadog/error_tracking/component.rb +2 -2
- data/lib/datadog/kit/tracing/method_tracer.rb +4 -1
- data/lib/datadog/opentelemetry/sdk/propagator.rb +9 -3
- data/lib/datadog/opentelemetry/sdk/span_processor.rb +4 -1
- data/lib/datadog/profiling/collectors/thread_context.rb +1 -4
- data/lib/datadog/profiling/component.rb +15 -25
- data/lib/datadog/profiling/ext/dir_monkey_patches.rb +6 -5
- data/lib/datadog/profiling/stack_recorder.rb +0 -4
- data/lib/datadog/ruby_version.rb +25 -0
- data/lib/datadog/symbol_database/component.rb +306 -98
- data/lib/datadog/symbol_database/extractor.rb +226 -87
- data/lib/datadog/tracing/configuration/ext.rb +13 -0
- data/lib/datadog/tracing/configuration/settings.rb +17 -0
- data/lib/datadog/tracing/contrib/action_cable/events/broadcast.rb +4 -1
- data/lib/datadog/tracing/contrib/action_cable/events/perform_action.rb +4 -1
- data/lib/datadog/tracing/contrib/action_cable/events/transmit.rb +4 -1
- data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +4 -1
- data/lib/datadog/tracing/contrib/action_mailer/event.rb +4 -1
- data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/action_view/events/render_partial.rb +4 -1
- data/lib/datadog/tracing/contrib/action_view/events/render_template.rb +4 -1
- data/lib/datadog/tracing/contrib/active_job/events/discard.rb +4 -1
- data/lib/datadog/tracing/contrib/active_job/events/enqueue.rb +4 -1
- data/lib/datadog/tracing/contrib/active_job/events/enqueue_at.rb +4 -1
- data/lib/datadog/tracing/contrib/active_job/events/enqueue_retry.rb +4 -1
- data/lib/datadog/tracing/contrib/active_job/events/perform.rb +4 -1
- data/lib/datadog/tracing/contrib/active_job/events/retry_stopped.rb +4 -1
- data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
- data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +1 -0
- data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/aws/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/configuration/resolver.rb +7 -0
- data/lib/datadog/tracing/contrib/dalli/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/delayed_job/plugin.rb +2 -0
- data/lib/datadog/tracing/contrib/delayed_job/server_internal_tracer/worker.rb +1 -0
- data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +1 -0
- data/lib/datadog/tracing/contrib/elasticsearch/quantize.rb +2 -2
- data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +1 -0
- data/lib/datadog/tracing/contrib/ethon/multi_patch.rb +1 -0
- data/lib/datadog/tracing/contrib/excon/middleware.rb +1 -0
- data/lib/datadog/tracing/contrib/ext.rb +3 -0
- data/lib/datadog/tracing/contrib/faraday/middleware.rb +1 -0
- data/lib/datadog/tracing/contrib/grape/endpoint.rb +3 -0
- data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +1 -0
- data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +1 -0
- data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +1 -0
- data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/grpc.rb +1 -0
- data/lib/datadog/tracing/contrib/hanami/action_tracer.rb +1 -0
- data/lib/datadog/tracing/contrib/hanami/renderer_policy_tracing.rb +1 -0
- data/lib/datadog/tracing/contrib/hanami/router_tracing.rb +1 -0
- data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/http/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/http.rb +1 -0
- data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/kafka/event.rb +1 -0
- data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/karafka.rb +1 -0
- data/lib/datadog/tracing/contrib/mongodb/parsers.rb +5 -5
- data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
- data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/opensearch/patcher.rb +1 -0
- data/lib/datadog/tracing/contrib/opensearch/quantize.rb +2 -2
- data/lib/datadog/tracing/contrib/pg/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/presto/instrumentation.rb +3 -0
- data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +3 -0
- data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +2 -2
- data/lib/datadog/tracing/contrib/que/tracer.rb +1 -0
- data/lib/datadog/tracing/contrib/racecar/event.rb +1 -0
- data/lib/datadog/tracing/contrib/rack/header_tagging.rb +23 -0
- data/lib/datadog/tracing/contrib/rack/middlewares.rb +4 -1
- data/lib/datadog/tracing/contrib/rack/route_inference.rb +3 -1
- data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +2 -0
- data/lib/datadog/tracing/contrib/rails/runner.rb +2 -0
- data/lib/datadog/tracing/contrib/rake/instrumentation.rb +4 -2
- data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +2 -0
- data/lib/datadog/tracing/contrib/resque/resque_job.rb +1 -0
- data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +1 -0
- data/lib/datadog/tracing/contrib/roda/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/roda/instrumentation.rb +4 -1
- data/lib/datadog/tracing/contrib/sequel/database.rb +1 -0
- data/lib/datadog/tracing/contrib/sequel/dataset.rb +1 -0
- data/lib/datadog/tracing/contrib/shoryuken/tracer.rb +1 -0
- data/lib/datadog/tracing/contrib/sidekiq/client_tracer.rb +1 -0
- data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/heartbeat.rb +2 -0
- data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/job_fetch.rb +1 -0
- data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/redis_info.rb +1 -0
- data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/scheduled_poller.rb +2 -0
- data/lib/datadog/tracing/contrib/sidekiq/server_internal_tracer/stop.rb +1 -0
- data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +3 -2
- data/lib/datadog/tracing/contrib/sidekiq.rb +1 -0
- data/lib/datadog/tracing/contrib/sinatra/tracer.rb +1 -0
- data/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +1 -0
- data/lib/datadog/tracing/contrib/sneakers/tracer.rb +1 -0
- data/lib/datadog/tracing/contrib/sucker_punch/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +1 -0
- data/lib/datadog/tracing/contrib/utils/quantization/{hash.rb → hash_formatter.rb} +1 -1
- data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/waterdrop.rb +1 -0
- data/lib/datadog/tracing/distributed/propagation.rb +33 -1
- data/lib/datadog/tracing/distributed/trace_context.rb +11 -2
- data/lib/datadog/tracing/metadata/ext.rb +7 -0
- data/lib/datadog/tracing/trace_digest.rb +7 -0
- data/lib/datadog/tracing/trace_operation.rb +4 -1
- data/lib/datadog/tracing/tracer.rb +4 -0
- data/lib/datadog/tracing/transport/traces.rb +2 -2
- data/lib/datadog/version.rb +1 -1
- data/lib/datadog.rb +4 -1
- 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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
#
|
|
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
|
|
82
|
-
# Hash
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|