datadog 2.35.0 → 2.37.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 +85 -1
- data/ext/datadog_profiling_native_extension/clock_id_from_mach.c +5 -12
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +71 -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 +18 -4
- 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 +596 -341
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +11 -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 +13 -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 +40 -34
- data/ext/datadog_profiling_native_extension/stack_recorder.c +24 -3
- 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_api/di.c +76 -0
- data/ext/libdatadog_api/extconf.rb +6 -0
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/ai_guard/api_client.rb +6 -4
- data/lib/datadog/appsec/api_security/route_extractor.rb +13 -4
- data/lib/datadog/appsec/component.rb +1 -1
- data/lib/datadog/appsec/configuration.rb +13 -0
- data/lib/datadog/appsec/context.rb +4 -2
- data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +37 -4
- data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +59 -4
- data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +59 -5
- 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 +24 -7
- data/lib/datadog/appsec/contrib/rack/input_peeker.rb +79 -0
- data/lib/datadog/appsec/contrib/rack/request_middleware.rb +21 -0
- data/lib/datadog/appsec/contrib/rails/gateway/request.rb +35 -1
- data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +17 -1
- data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +60 -7
- data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +24 -3
- data/lib/datadog/appsec/default_header_tags.rb +10 -6
- data/lib/datadog/appsec/ext.rb +1 -0
- data/lib/datadog/appsec/metrics/collector.rb +18 -1
- data/lib/datadog/appsec/metrics/exporter.rb +9 -3
- data/lib/datadog/appsec/remote.rb +9 -3
- data/lib/datadog/appsec/route_normalizer/rails_route_pattern.rb +176 -0
- data/lib/datadog/appsec/route_normalizer/route_pattern.rb +378 -0
- data/lib/datadog/appsec/route_normalizer/route_text.rb +57 -0
- data/lib/datadog/appsec/route_normalizer.rb +80 -0
- data/lib/datadog/appsec/utils/http/body_reader.rb +61 -0
- data/lib/datadog/core/configuration/components.rb +83 -2
- data/lib/datadog/core/configuration/components_state.rb +6 -1
- data/lib/datadog/core/configuration/settings.rb +1 -5
- data/lib/datadog/core/configuration/supported_configurations.rb +4 -0
- data/lib/datadog/core/evp.rb +11 -0
- data/lib/datadog/core/remote/client/capabilities.rb +34 -7
- data/lib/datadog/core/remote/component.rb +1 -1
- data/lib/datadog/core/telemetry/event/app_started.rb +8 -22
- data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
- 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 +7 -2
- data/lib/datadog/di/code_tracker.rb +5 -2
- data/lib/datadog/di/component.rb +110 -36
- data/lib/datadog/di/error.rb +10 -0
- data/lib/datadog/di/fatal_exceptions.rb +26 -0
- data/lib/datadog/di/instrumenter.rb +381 -165
- data/lib/datadog/di/probe.rb +14 -0
- data/lib/datadog/di/probe_file_loader.rb +8 -3
- data/lib/datadog/di/probe_manager.rb +67 -9
- data/lib/datadog/di/probe_notification_builder.rb +4 -1
- data/lib/datadog/di/probe_notifier_worker.rb +52 -32
- data/lib/datadog/di/redactor.rb +16 -1
- data/lib/datadog/di/remote.rb +175 -14
- data/lib/datadog/di/serializer.rb +12 -5
- data/lib/datadog/di/transport/input.rb +8 -4
- data/lib/datadog/di.rb +63 -0
- data/lib/datadog/error_tracking/collector.rb +2 -1
- data/lib/datadog/error_tracking/component.rb +2 -2
- data/lib/datadog/kit/appsec/events/v2.rb +60 -2
- data/lib/datadog/kit/tracing/method_tracer.rb +4 -1
- data/lib/datadog/open_feature/component.rb +29 -6
- data/lib/datadog/open_feature/configuration.rb +8 -0
- data/lib/datadog/open_feature/ext.rb +2 -0
- data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +286 -0
- data/lib/datadog/open_feature/flag_evaluation/writer.rb +433 -0
- data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +101 -0
- data/lib/datadog/open_feature/hooks/{flag_eval_hook.rb → flag_eval_metrics_hook.rb} +1 -1
- data/lib/datadog/open_feature/native_evaluator.rb +22 -0
- data/lib/datadog/open_feature/provider.rb +59 -27
- data/lib/datadog/open_feature/transport.rb +55 -1
- data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +5 -12
- data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +4 -11
- data/lib/datadog/opentelemetry/sdk/propagator.rb +9 -3
- data/lib/datadog/opentelemetry/sdk/span_processor.rb +4 -1
- data/lib/datadog/opentelemetry/sdk.rb +19 -0
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -3
- data/lib/datadog/profiling/collectors/thread_context.rb +1 -0
- data/lib/datadog/profiling/component.rb +13 -15
- data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -3
- data/lib/datadog/ruby_version.rb +25 -0
- data/lib/datadog/symbol_database/component.rb +453 -128
- data/lib/datadog/symbol_database/configuration.rb +12 -4
- data/lib/datadog/symbol_database/extensions.rb +19 -0
- data/lib/datadog/symbol_database/extractor.rb +326 -150
- data/lib/datadog/symbol_database/file_hash.rb +3 -1
- data/lib/datadog/symbol_database/remote.rb +8 -3
- data/lib/datadog/symbol_database/scope_batcher.rb +7 -3
- data/lib/datadog/symbol_database/uploader.rb +9 -2
- data/lib/datadog/symbol_database.rb +22 -0
- data/lib/datadog/tracing/configuration/ext.rb +13 -0
- data/lib/datadog/tracing/configuration/settings.rb +17 -0
- data/lib/datadog/tracing/contrib/configuration/resolver.rb +7 -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/http/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/http.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/rack/middlewares.rb +3 -1
- data/lib/datadog/tracing/contrib/rack/route_inference.rb +3 -1
- data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +2 -0
- data/lib/datadog/tracing/contrib/sidekiq.rb +1 -0
- 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/baggage.rb +2 -1
- data/lib/datadog/tracing/distributed/propagation.rb +33 -1
- data/lib/datadog/tracing/distributed/trace_context.rb +11 -2
- data/lib/datadog/tracing/ext.rb +9 -0
- data/lib/datadog/tracing/remote.rb +34 -3
- data/lib/datadog/tracing/trace_digest.rb +7 -0
- data/lib/datadog/tracing/trace_operation.rb +4 -1
- data/lib/datadog/tracing/tracer.rb +1 -0
- data/lib/datadog/version.rb +1 -1
- data/lib/datadog.rb +5 -1
- metadata +22 -6
|
@@ -7,6 +7,8 @@ require_relative '../../trace_keeper'
|
|
|
7
7
|
require_relative '../../security_event'
|
|
8
8
|
require_relative '../../utils/http/url_encoded'
|
|
9
9
|
require_relative '../../utils/http/body'
|
|
10
|
+
require_relative '../../utils/http/body_reader'
|
|
11
|
+
require_relative '../../utils/http/media_type'
|
|
10
12
|
|
|
11
13
|
module Datadog
|
|
12
14
|
module AppSec
|
|
@@ -39,7 +41,7 @@ module Datadog
|
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
if !is_redirect && data[SAMPLE_BODY_KEY]
|
|
42
|
-
body =
|
|
44
|
+
body = parse_request_body(data[:body], content_type: headers['content-type'])
|
|
43
45
|
ephemeral_data['server.io.net.request.body'] = body if body
|
|
44
46
|
end
|
|
45
47
|
|
|
@@ -67,7 +69,7 @@ module Datadog
|
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
if !is_redirect && data[SAMPLE_BODY_KEY]
|
|
70
|
-
body =
|
|
72
|
+
body = parse_response_body(data.dig(:response, :body), headers: headers, context: context)
|
|
71
73
|
ephemeral_data['server.io.net.response.body'] = body if body
|
|
72
74
|
end
|
|
73
75
|
|
|
@@ -89,11 +91,64 @@ module Datadog
|
|
|
89
91
|
data[SAMPLE_BODY_KEY] = true
|
|
90
92
|
end
|
|
91
93
|
|
|
92
|
-
def
|
|
94
|
+
def parse_request_body(body, content_type:)
|
|
93
95
|
media_type = Utils::HTTP::MediaType.parse(content_type)
|
|
94
96
|
return unless media_type
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
limit = Datadog.configuration.appsec.api_security.downstream_body_analysis.max_downstream_body_bytes
|
|
99
|
+
content = read_body(body, limit: limit)
|
|
100
|
+
return if content.nil? || content.bytesize > limit
|
|
101
|
+
|
|
102
|
+
Utils::HTTP::Body.parse(content, media_type: media_type)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_response_body(body, headers:, context:)
|
|
106
|
+
return unless readable_body?(body)
|
|
107
|
+
|
|
108
|
+
media_type = Utils::HTTP::MediaType.parse(headers['content-type'])
|
|
109
|
+
if !media_type || media_type.type != 'application'
|
|
110
|
+
context.metrics.record_ignored_downstream_response_body(:content_type_invalid)
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
subtype = media_type.subtype
|
|
115
|
+
if subtype != 'json' && !subtype.end_with?('+json') && subtype != 'x-www-form-urlencoded'
|
|
116
|
+
context.metrics.record_ignored_downstream_response_body(:content_type_invalid)
|
|
117
|
+
return
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
content_length_value = headers['content-length']
|
|
121
|
+
if !content_length_value.is_a?(String) || !content_length_value.match?(/\A[1-9][0-9]*\z/)
|
|
122
|
+
context.metrics.record_ignored_downstream_response_body(:content_length_missing)
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
content_length = content_length_value.to_i
|
|
127
|
+
max = Datadog.configuration.appsec.api_security.downstream_body_analysis.max_downstream_body_bytes
|
|
128
|
+
if content_length > max
|
|
129
|
+
context.metrics.record_ignored_downstream_response_body(:content_length_too_big)
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
content = read_body(body, limit: content_length)
|
|
134
|
+
return if content.nil?
|
|
135
|
+
|
|
136
|
+
if content.bytesize > content_length
|
|
137
|
+
context.metrics.record_ignored_downstream_response_body(:content_exceed_content_length)
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
Utils::HTTP::Body.parse(content, media_type: media_type)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def readable_body?(body)
|
|
145
|
+
body.is_a?(String) || (body.respond_to?(:read) && body.respond_to?(:rewind))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def read_body(body, limit:)
|
|
149
|
+
Utils::HTTP::BodyReader.read(body, limit: limit, rewind_before_read: true)
|
|
150
|
+
rescue
|
|
151
|
+
nil
|
|
97
152
|
end
|
|
98
153
|
|
|
99
154
|
def request_url(data)
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
require_relative '../../event'
|
|
4
4
|
require_relative '../../trace_keeper'
|
|
5
5
|
require_relative '../../security_event'
|
|
6
|
-
require_relative '../../utils/http/media_type'
|
|
7
6
|
require_relative '../../utils/http/body'
|
|
7
|
+
require_relative '../../utils/http/body_reader'
|
|
8
|
+
require_relative '../../utils/http/media_type'
|
|
8
9
|
|
|
9
10
|
module Datadog
|
|
10
11
|
module AppSec
|
|
@@ -37,7 +38,7 @@ module Datadog
|
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
if !is_redirect && env[SAMPLE_BODY_KEY]
|
|
40
|
-
body =
|
|
41
|
+
body = parse_request_body(env.body, content_type: headers['content-type'])
|
|
41
42
|
ephemeral_data['server.io.net.request.body'] = body if body
|
|
42
43
|
end
|
|
43
44
|
|
|
@@ -64,7 +65,7 @@ module Datadog
|
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
if !is_redirect && env[SAMPLE_BODY_KEY]
|
|
67
|
-
body =
|
|
68
|
+
body = parse_response_body(env.body, headers: headers, context: context)
|
|
68
69
|
ephemeral_data['server.io.net.response.body'] = body if body
|
|
69
70
|
end
|
|
70
71
|
|
|
@@ -82,11 +83,64 @@ module Datadog
|
|
|
82
83
|
env[SAMPLE_BODY_KEY] = true
|
|
83
84
|
end
|
|
84
85
|
|
|
85
|
-
def
|
|
86
|
+
def parse_request_body(body, content_type:)
|
|
86
87
|
media_type = Utils::HTTP::MediaType.parse(content_type)
|
|
87
88
|
return unless media_type
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
limit = Datadog.configuration.appsec.api_security.downstream_body_analysis.max_downstream_body_bytes
|
|
91
|
+
content = read_body(body, limit: limit)
|
|
92
|
+
return if content.nil? || content.bytesize > limit
|
|
93
|
+
|
|
94
|
+
Utils::HTTP::Body.parse(content, media_type: media_type)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_response_body(body, headers:, context:)
|
|
98
|
+
return unless readable_body?(body)
|
|
99
|
+
|
|
100
|
+
media_type = Utils::HTTP::MediaType.parse(headers['content-type'])
|
|
101
|
+
if !media_type || media_type.type != 'application'
|
|
102
|
+
context.metrics.record_ignored_downstream_response_body(:content_type_invalid)
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
subtype = media_type.subtype
|
|
107
|
+
if subtype != 'json' && !subtype.end_with?('+json') && subtype != 'x-www-form-urlencoded'
|
|
108
|
+
context.metrics.record_ignored_downstream_response_body(:content_type_invalid)
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
content_length_value = headers['content-length']
|
|
113
|
+
if !content_length_value.is_a?(String) || !content_length_value.match?(/\A[1-9][0-9]*\z/)
|
|
114
|
+
context.metrics.record_ignored_downstream_response_body(:content_length_missing)
|
|
115
|
+
return
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
content_length = content_length_value.to_i
|
|
119
|
+
max = Datadog.configuration.appsec.api_security.downstream_body_analysis.max_downstream_body_bytes
|
|
120
|
+
if content_length > max
|
|
121
|
+
context.metrics.record_ignored_downstream_response_body(:content_length_too_big)
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
content = read_body(body, limit: content_length)
|
|
126
|
+
return if content.nil?
|
|
127
|
+
|
|
128
|
+
if content.bytesize > content_length
|
|
129
|
+
context.metrics.record_ignored_downstream_response_body(:content_exceed_content_length)
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
Utils::HTTP::Body.parse(content, media_type: media_type)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def readable_body?(body)
|
|
137
|
+
body.is_a?(String) || (body.respond_to?(:read) && body.respond_to?(:rewind))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def read_body(body, limit:)
|
|
141
|
+
Utils::HTTP::BodyReader.read(body, limit: limit, rewind_before_read: true)
|
|
142
|
+
rescue
|
|
143
|
+
nil
|
|
90
144
|
end
|
|
91
145
|
|
|
92
146
|
def normalize_headers(headers)
|
|
@@ -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
|
+
# re-used fragments are not expanded
|
|
76
|
+
visited_fragments[fragment_name] = true
|
|
77
|
+
arguments_from_selections(
|
|
78
|
+
fragment.selections, query_variables, args_hash, fragments, visited_fragments
|
|
79
|
+
)
|
|
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,30 @@ 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
|
+
# NOTE: Body was parsed before measurement, keep byte_length unset
|
|
108
|
+
elsif gateway_request.env.key?('rack.request.form_hash')
|
|
109
|
+
body = gateway_request.env['rack.request.form_hash']
|
|
110
|
+
persistent_data['server.request.body'] = body if body
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
next stack.call(request) if persistent_data.empty?
|
|
93
114
|
|
|
94
115
|
result = context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
|
|
95
116
|
|
|
@@ -119,10 +140,6 @@ module Datadog
|
|
|
119
140
|
next stack.call(gateway_request.request) if context.span.nil?
|
|
120
141
|
|
|
121
142
|
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
143
|
if context.state[:has_identity_event] && Ext::IDENTITY_COLLECTABLE_REQUEST_HEADERS.include?(name)
|
|
127
144
|
context.span["http.request.headers.#{name}"] ||= value
|
|
128
145
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
require_relative 'buffered_input'
|
|
5
|
+
require_relative '../../utils/http/body_reader'
|
|
6
|
+
|
|
7
|
+
module Datadog
|
|
8
|
+
module AppSec
|
|
9
|
+
module Contrib
|
|
10
|
+
module Rack
|
|
11
|
+
# Peeks at `rack.input` without changing what downstream Rack code can read
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module InputPeeker
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Peeks at `env['rack.input']` and returns the body bytesize when it
|
|
18
|
+
# can be measured within the given limit
|
|
19
|
+
#
|
|
20
|
+
# NOTE: Over-limit bodies cannot be fully measured, so returning `nil`
|
|
21
|
+
# is the tradeoff we accept
|
|
22
|
+
#
|
|
23
|
+
# WARNING: For forward-only input, replaces `env['rack.input']` with a
|
|
24
|
+
# replay stream over the peeked bytes, preserving the rest
|
|
25
|
+
# of the original stream
|
|
26
|
+
#
|
|
27
|
+
# @param env [Hash] Rack environment
|
|
28
|
+
# @param limit [Integer] Maximum body bytesize to measure
|
|
29
|
+
# @return [Integer, nil] `Integer` bytesize when measured within
|
|
30
|
+
# `limit`, including `0`; `nil` when input is missing, over the
|
|
31
|
+
# limit, or cannot be preserved for downstream reads
|
|
32
|
+
def peek_bytesize(env, limit:)
|
|
33
|
+
rack_input = env['rack.input']
|
|
34
|
+
return unless rack_input
|
|
35
|
+
|
|
36
|
+
rewindable = rewind? && rack_input.respond_to?(:rewind)
|
|
37
|
+
|
|
38
|
+
# NOTE: Rack 2 requires `rack.input` to be rewindable. Rewind before peeking
|
|
39
|
+
# in case an upstream framework already consumed part of the stream
|
|
40
|
+
return if rewindable && !rewind(rack_input)
|
|
41
|
+
|
|
42
|
+
# NOTE: Read one byte past the limit to distinguish an exact-limit body
|
|
43
|
+
# from an over-limit body without reading the whole stream.
|
|
44
|
+
buffer = Utils::HTTP::BodyReader.read_stream(rack_input, limit: limit)
|
|
45
|
+
over_limit = buffer.bytesize > limit
|
|
46
|
+
|
|
47
|
+
if rewindable
|
|
48
|
+
# NOTE: If we cannot rewind after peeking, downstream code would observe
|
|
49
|
+
# a partially consumed body. Treat it as not safely collectable
|
|
50
|
+
return unless rewind(rack_input)
|
|
51
|
+
else
|
|
52
|
+
env['rack.input'] = if over_limit
|
|
53
|
+
BufferedInput.new(rack_input, buffer: StringIO.new(buffer))
|
|
54
|
+
else
|
|
55
|
+
StringIO.new(buffer)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
over_limit ? nil : buffer.bytesize
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private_class_method def rewind?
|
|
63
|
+
return @rewind if defined?(@rewind)
|
|
64
|
+
|
|
65
|
+
@rewind = ::Gem::Version.new(::Rack.release) < ::Gem::Version.new('3')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private_class_method def rewind(io)
|
|
69
|
+
io.rewind
|
|
70
|
+
true
|
|
71
|
+
rescue => e
|
|
72
|
+
Datadog.logger.debug { "AppSec: Failed to rewind `rack.input`: #{e.class}: #{e.message}" }
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -10,6 +10,7 @@ require_relative '../../event'
|
|
|
10
10
|
require_relative '../../response'
|
|
11
11
|
require_relative '../../api_security'
|
|
12
12
|
require_relative '../../default_header_tags'
|
|
13
|
+
require_relative '../../route_normalizer'
|
|
13
14
|
require_relative '../../security_event'
|
|
14
15
|
require_relative '../../instrumentation/gateway'
|
|
15
16
|
|
|
@@ -76,6 +77,8 @@ module Datadog
|
|
|
76
77
|
nil
|
|
77
78
|
end
|
|
78
79
|
|
|
80
|
+
add_normalized_route_tag(ctx, env)
|
|
81
|
+
|
|
79
82
|
if interrupt_params
|
|
80
83
|
ctx.mark_as_interrupted!
|
|
81
84
|
http_response = AppSec::Response.from_interrupt_params(interrupt_params, env['HTTP_ACCEPT']).to_rack
|
|
@@ -198,6 +201,24 @@ module Datadog
|
|
|
198
201
|
end
|
|
199
202
|
end
|
|
200
203
|
|
|
204
|
+
def add_normalized_route_tag(context, env)
|
|
205
|
+
return unless AppSec::APISecurity.enabled?
|
|
206
|
+
|
|
207
|
+
span = context.span
|
|
208
|
+
return unless span
|
|
209
|
+
|
|
210
|
+
pattern = context.trace&.get_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE)
|
|
211
|
+
return unless pattern
|
|
212
|
+
|
|
213
|
+
# NOTE: To build full path that covers mounted engines we need to add
|
|
214
|
+
# pre-computed by Tracer route path tag to the normalized route
|
|
215
|
+
prefix = context.trace&.get_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE_PATH) || env['SCRIPT_NAME']
|
|
216
|
+
normalized_route = RouteNormalizer.extract_normalized_route(env, prefix: prefix, pattern: pattern)
|
|
217
|
+
return unless normalized_route
|
|
218
|
+
|
|
219
|
+
span.set_tag(AppSec::Ext::TAG_NORMALIZED_ROUTE, "#{prefix}#{normalized_route}")
|
|
220
|
+
end
|
|
221
|
+
|
|
201
222
|
def oneshot_tags_sent?
|
|
202
223
|
@oneshot_tags_sent
|
|
203
224
|
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
|
|
@@ -46,6 +47,7 @@ module Datadog
|
|
|
46
47
|
body = request.env['action_dispatch.request.request_parameters']
|
|
47
48
|
|
|
48
49
|
return if body.nil?
|
|
50
|
+
return body unless request.env['action_dispatch.request.path_parameters']
|
|
49
51
|
|
|
50
52
|
body.reject do |k, _v|
|
|
51
53
|
request.env['action_dispatch.request.path_parameters'].key?(k)
|
|
@@ -55,10 +57,42 @@ module Datadog
|
|
|
55
57
|
def route_params
|
|
56
58
|
excluded = [:controller, :action]
|
|
57
59
|
|
|
58
|
-
request.env
|
|
60
|
+
request.env.fetch('action_dispatch.request.path_parameters', {}).reject do |k, _v|
|
|
59
61
|
excluded.include?(k)
|
|
60
62
|
end
|
|
61
63
|
end
|
|
64
|
+
|
|
65
|
+
# Returns the request body size in bytes using all available methods,
|
|
66
|
+
# or nil when the size cannot be measured within the limit
|
|
67
|
+
#
|
|
68
|
+
# NOTE: The priority of the measurement is the following:
|
|
69
|
+
# raw posted data, raw form vars, size if known, raw
|
|
70
|
+
# Content-Length, then buffering to the limit if unknown-length
|
|
71
|
+
def body_bytesize(limit)
|
|
72
|
+
raw_body = env['RAW_POST_DATA']
|
|
73
|
+
return raw_body.bytesize if raw_body
|
|
74
|
+
|
|
75
|
+
form_vars = env['rack.request.form_vars']
|
|
76
|
+
return form_vars.bytesize if form_vars
|
|
77
|
+
|
|
78
|
+
io = request.body
|
|
79
|
+
return 0 unless io
|
|
80
|
+
return io.size if io.respond_to?(:size)
|
|
81
|
+
|
|
82
|
+
# NOTE: Read raw `CONTENT_LENGTH` as {ActionDispatch::Request#content_length}
|
|
83
|
+
# drains `rack.input` into `RAW_POST_DATA` on chunked Transfer-Encoding
|
|
84
|
+
content_length = env['CONTENT_LENGTH']
|
|
85
|
+
return content_length.to_i if content_length
|
|
86
|
+
|
|
87
|
+
# NOTE: An already-read body (e.g. late-parsed multipart on Rack 3+) peeks
|
|
88
|
+
# as 0, so we skip byte_length but still collect the parsed body
|
|
89
|
+
begin
|
|
90
|
+
Rack::InputPeeker.peek_bytesize(env, limit: limit)
|
|
91
|
+
rescue => e
|
|
92
|
+
Datadog.logger.debug { "AppSec: Failed to measure Rails request body: #{e.class}: #{e.message}" }
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
62
96
|
end
|
|
63
97
|
end
|
|
64
98
|
end
|