datadog 2.27.0 → 2.29.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 +64 -2
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +64 -3
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +23 -4
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -1
- data/ext/datadog_profiling_native_extension/extconf.rb +5 -0
- data/ext/datadog_profiling_native_extension/heap_recorder.c +183 -51
- data/ext/datadog_profiling_native_extension/heap_recorder.h +12 -1
- data/ext/datadog_profiling_native_extension/stack_recorder.c +34 -5
- data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -1
- data/ext/libdatadog_api/crashtracker.c +5 -0
- data/ext/libdatadog_api/crashtracker_report_exception.c +236 -0
- data/lib/datadog/ai_guard/configuration/settings.rb +13 -1
- data/lib/datadog/ai_guard/contrib/integration.rb +37 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +42 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +41 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +30 -0
- data/lib/datadog/ai_guard.rb +2 -0
- data/lib/datadog/appsec/assets/blocked.html +2 -1
- data/lib/datadog/appsec/configuration/settings.rb +14 -0
- data/lib/datadog/appsec/context.rb +44 -9
- data/lib/datadog/appsec/contrib/active_record/integration.rb +1 -1
- data/lib/datadog/appsec/contrib/active_record/patcher.rb +1 -1
- data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +54 -5
- data/lib/datadog/appsec/contrib/faraday/integration.rb +1 -1
- data/lib/datadog/appsec/contrib/faraday/patcher.rb +1 -1
- data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +60 -7
- data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +11 -6
- data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -1
- data/lib/datadog/appsec/contrib/rack/gateway/request.rb +6 -10
- data/lib/datadog/appsec/contrib/rack/request_middleware.rb +1 -3
- data/lib/datadog/appsec/contrib/rails/patcher.rb +10 -2
- data/lib/datadog/appsec/contrib/rails/patches/process_action_patch.rb +2 -0
- data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +72 -7
- data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +7 -4
- data/lib/datadog/appsec/contrib/sinatra/integration.rb +1 -1
- data/lib/datadog/appsec/contrib/sinatra/patcher.rb +4 -4
- data/lib/datadog/appsec/contrib/sinatra/patches/json_patch.rb +1 -1
- data/lib/datadog/appsec/counter_sampler.rb +25 -0
- data/lib/datadog/appsec/metrics/telemetry_exporter.rb +18 -0
- data/lib/datadog/appsec/security_engine/engine.rb +23 -2
- data/lib/datadog/appsec/utils/http/body.rb +38 -0
- data/lib/datadog/appsec/utils/http/media_range.rb +2 -1
- data/lib/datadog/appsec/utils/http/media_type.rb +33 -26
- data/lib/datadog/appsec/utils/http/url_encoded.rb +52 -0
- data/lib/datadog/core/configuration/components.rb +29 -4
- data/lib/datadog/core/configuration/supported_configurations.rb +4 -0
- data/lib/datadog/core/configuration.rb +2 -2
- data/lib/datadog/core/crashtracking/component.rb +79 -19
- data/lib/datadog/core/crashtracking/tag_builder.rb +6 -0
- data/lib/datadog/core/environment/agent_info.rb +65 -1
- data/lib/datadog/core/knuth_sampler.rb +57 -0
- data/lib/datadog/core/logger.rb +1 -1
- data/lib/datadog/core/metrics/logging.rb +1 -1
- data/lib/datadog/core/process_discovery.rb +15 -19
- data/lib/datadog/core/rate_limiter.rb +2 -0
- data/lib/datadog/core/remote/component.rb +16 -5
- data/lib/datadog/core/remote/transport/config.rb +5 -11
- data/lib/datadog/core/telemetry/component.rb +0 -13
- data/lib/datadog/core/telemetry/transport/telemetry.rb +5 -6
- data/lib/datadog/core/transport/ext.rb +1 -0
- data/lib/datadog/core/transport/http/response.rb +4 -0
- data/lib/datadog/core/transport/parcel.rb +61 -9
- data/lib/datadog/core/utils/fnv.rb +26 -0
- data/lib/datadog/core.rb +6 -1
- data/lib/datadog/data_streams/processor.rb +34 -33
- data/lib/datadog/data_streams/transport/http/stats.rb +6 -0
- data/lib/datadog/data_streams/transport/http.rb +0 -4
- data/lib/datadog/data_streams/transport/stats.rb +5 -12
- data/lib/datadog/di/component.rb +1 -1
- data/lib/datadog/di/configuration/settings.rb +31 -0
- data/lib/datadog/di/context.rb +6 -0
- data/lib/datadog/di/instrumenter.rb +178 -133
- data/lib/datadog/di/probe.rb +10 -1
- data/lib/datadog/di/probe_file_loader.rb +2 -2
- data/lib/datadog/di/probe_manager.rb +7 -2
- data/lib/datadog/di/probe_notification_builder.rb +29 -8
- data/lib/datadog/di/probe_notifier_worker.rb +13 -3
- data/lib/datadog/di/proc_responder.rb +4 -0
- data/lib/datadog/di/redactor.rb +8 -1
- data/lib/datadog/di/remote.rb +2 -2
- data/lib/datadog/di/transport/diagnostics.rb +5 -7
- data/lib/datadog/di/transport/http/diagnostics.rb +3 -1
- data/lib/datadog/di/transport/http/input.rb +1 -1
- data/lib/datadog/di/transport/input.rb +5 -6
- data/lib/datadog/kit/tracing/method_tracer.rb +132 -0
- data/lib/datadog/open_feature/transport.rb +8 -11
- data/lib/datadog/profiling/component.rb +0 -6
- data/lib/datadog/tracing/contrib/http/integration.rb +0 -2
- data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +6 -0
- data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +2 -1
- data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +6 -0
- data/lib/datadog/tracing/contrib/pg/instrumentation.rb +2 -1
- data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +10 -0
- data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +5 -1
- data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +24 -0
- data/lib/datadog/tracing/contrib/rack/route_inference.rb +18 -6
- data/lib/datadog/tracing/contrib/registerable.rb +11 -0
- data/lib/datadog/tracing/contrib/sneakers/integration.rb +15 -4
- data/lib/datadog/tracing/contrib/trilogy/configuration/settings.rb +6 -0
- data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +3 -1
- data/lib/datadog/tracing/sampling/rate_sampler.rb +8 -19
- data/lib/datadog/tracing/transport/io/client.rb +5 -8
- data/lib/datadog/tracing/transport/io/traces.rb +28 -34
- data/lib/datadog/tracing/transport/traces.rb +4 -10
- data/lib/datadog/version.rb +1 -1
- metadata +17 -7
- data/lib/datadog/appsec/contrib/rails/ext.rb +0 -13
|
@@ -28,7 +28,7 @@ module Datadog
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def configure_default_faraday_connection
|
|
31
|
-
if target_version
|
|
31
|
+
if target_version&.>= Gem::Version.new('1.0.0')
|
|
32
32
|
# Patch the default connection (e.g. +Faraday.get+)
|
|
33
33
|
::Faraday.default_connection.use(:datadog_appsec)
|
|
34
34
|
|
|
@@ -3,6 +3,8 @@
|
|
|
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
|
+
require_relative '../../utils/http/body'
|
|
6
8
|
|
|
7
9
|
module Datadog
|
|
8
10
|
module AppSec
|
|
@@ -10,17 +12,36 @@ module Datadog
|
|
|
10
12
|
module Faraday
|
|
11
13
|
# AppSec SSRF detection Middleware for Faraday
|
|
12
14
|
class SSRFDetectionMiddleware < ::Faraday::Middleware
|
|
15
|
+
REDIRECT_STATUS_CODES = (300..399).freeze
|
|
16
|
+
SAMPLE_BODY_KEY = :__datadog_appsec_sample_downstream_body
|
|
17
|
+
|
|
13
18
|
def call(env)
|
|
14
19
|
context = AppSec.active_context
|
|
15
20
|
return @app.call(env) unless context && AppSec.rasp_enabled?
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
url = env.url.to_s
|
|
23
|
+
headers = normalize_headers(env.request_headers)
|
|
24
|
+
# @type var ephemeral_data: ::Datadog::AppSec::Context::input_data
|
|
18
25
|
ephemeral_data = {
|
|
19
|
-
'server.io.net.url' =>
|
|
26
|
+
'server.io.net.url' => url,
|
|
20
27
|
'server.io.net.request.method' => env.method.to_s.upcase,
|
|
21
|
-
'server.io.net.request.headers' =>
|
|
28
|
+
'server.io.net.request.headers' => headers
|
|
22
29
|
}
|
|
23
30
|
|
|
31
|
+
is_redirect = context.state[:downstream_redirect_url] == url
|
|
32
|
+
if is_redirect
|
|
33
|
+
context.state.delete(:downstream_redirect_url)
|
|
34
|
+
env[SAMPLE_BODY_KEY] = true
|
|
35
|
+
else
|
|
36
|
+
mark_body_sampling!(env, context: context)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if !is_redirect && env[SAMPLE_BODY_KEY]
|
|
40
|
+
body = parse_body(env.body, content_type: headers['content-type'])
|
|
41
|
+
ephemeral_data['server.io.net.request.body'] = body if body
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
timeout = Datadog.configuration.appsec.waf_timeout
|
|
24
45
|
result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, timeout, phase: Ext::RASP_REQUEST_PHASE)
|
|
25
46
|
handle(result, context: context) if result.match?
|
|
26
47
|
|
|
@@ -30,18 +51,50 @@ module Datadog
|
|
|
30
51
|
private
|
|
31
52
|
|
|
32
53
|
def on_complete(env, context:)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
response_headers = env.response_headers || {}
|
|
54
|
+
headers = normalize_headers(env.response_headers)
|
|
55
|
+
# @type var ephemeral_data: ::Datadog::AppSec::Context::input_data
|
|
36
56
|
ephemeral_data = {
|
|
37
57
|
'server.io.net.response.status' => env.status.to_s,
|
|
38
|
-
'server.io.net.response.headers' =>
|
|
58
|
+
'server.io.net.response.headers' => headers
|
|
39
59
|
}
|
|
40
60
|
|
|
61
|
+
is_redirect = REDIRECT_STATUS_CODES.cover?(env.status) && headers.key?('location')
|
|
62
|
+
if is_redirect && env[SAMPLE_BODY_KEY]
|
|
63
|
+
context.state[:downstream_redirect_url] = URI.join(env.url.to_s, headers['location']).to_s
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if !is_redirect && env[SAMPLE_BODY_KEY]
|
|
67
|
+
body = parse_body(env.body, content_type: headers['content-type'])
|
|
68
|
+
ephemeral_data['server.io.net.response.body'] = body if body
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
timeout = Datadog.configuration.appsec.waf_timeout
|
|
41
72
|
result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, timeout, phase: Ext::RASP_RESPONSE_PHASE)
|
|
42
73
|
handle(result, context: context) if result.match?
|
|
43
74
|
end
|
|
44
75
|
|
|
76
|
+
def mark_body_sampling!(env, context:)
|
|
77
|
+
max = Datadog.configuration.appsec.api_security.downstream_body_analysis.max_requests
|
|
78
|
+
return if context.state[:downstream_body_analyzed_count] >= max
|
|
79
|
+
return unless context.downstream_body_sampler.sample?
|
|
80
|
+
|
|
81
|
+
context.state[:downstream_body_analyzed_count] += 1
|
|
82
|
+
env[SAMPLE_BODY_KEY] = true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def parse_body(body, content_type:)
|
|
86
|
+
media_type = Utils::HTTP::MediaType.parse(content_type)
|
|
87
|
+
return unless media_type
|
|
88
|
+
|
|
89
|
+
Utils::HTTP::Body.parse(body, media_type: media_type)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def normalize_headers(headers)
|
|
93
|
+
return {} if headers.nil? || headers.empty?
|
|
94
|
+
|
|
95
|
+
headers.transform_keys(&:downcase)
|
|
96
|
+
end
|
|
97
|
+
|
|
45
98
|
def handle(result, context:)
|
|
46
99
|
AppSec::Event.tag(context, result)
|
|
47
100
|
TraceKeeper.keep!(context.trace) if result.keep?
|
|
@@ -47,9 +47,10 @@ module Datadog
|
|
|
47
47
|
# Note that the `comments` "include" directive is included in the arguments list
|
|
48
48
|
def build_arguments_hash
|
|
49
49
|
queries.each_with_object({}) do |query, args_hash|
|
|
50
|
-
|
|
50
|
+
selected_operation = query.selected_operation
|
|
51
|
+
next unless selected_operation
|
|
51
52
|
|
|
52
|
-
arguments_from_selections(
|
|
53
|
+
arguments_from_selections(selected_operation.selections, query.variables, args_hash)
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -90,15 +91,19 @@ module Datadog
|
|
|
90
91
|
end
|
|
91
92
|
|
|
92
93
|
def argument_value(argument, query_variables)
|
|
93
|
-
|
|
94
|
+
value = argument.value
|
|
95
|
+
|
|
96
|
+
case value.class.name
|
|
94
97
|
when Integration::AST_NODE_CLASS_NAMES[:variable_identifier]
|
|
98
|
+
# @type var value: GraphQL::Language::Nodes::VariableIdentifier
|
|
95
99
|
# we need to pass query.variables here instead of query.provided_variables,
|
|
96
100
|
# since #provided_variables don't know anything about variable default value
|
|
97
|
-
query_variables[
|
|
101
|
+
query_variables[value.name]
|
|
98
102
|
when Integration::AST_NODE_CLASS_NAMES[:input_object]
|
|
99
|
-
|
|
103
|
+
# @type var value: GraphQL::Language::Nodes::InputObject
|
|
104
|
+
arguments_hash(value.arguments, query_variables)
|
|
100
105
|
else
|
|
101
|
-
|
|
106
|
+
value
|
|
102
107
|
end
|
|
103
108
|
end
|
|
104
109
|
end
|
|
@@ -23,16 +23,12 @@ module Datadog
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def query
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
hash[k] ||= []
|
|
33
|
-
|
|
34
|
-
hash[k] << v
|
|
35
|
-
end
|
|
26
|
+
::Rack::Utils.parse_query(request.query_string)
|
|
27
|
+
rescue => e
|
|
28
|
+
Datadog.logger.debug { "AppSec: Failed to parse request query string: #{e.class}: #{e.message}" }
|
|
29
|
+
AppSec.telemetry.report(e, description: 'AppSec: Failed to parse request query string')
|
|
30
|
+
|
|
31
|
+
{}
|
|
36
32
|
end
|
|
37
33
|
|
|
38
34
|
def method
|
|
@@ -107,9 +107,7 @@ module Datadog
|
|
|
107
107
|
|
|
108
108
|
if AppSec::APISecurity.enabled? && AppSec::APISecurity.sample_trace?(ctx.trace) &&
|
|
109
109
|
AppSec::APISecurity.sample?(gateway_request.request, tmp_response.response)
|
|
110
|
-
ctx.
|
|
111
|
-
AppSec::SecurityEvent.new(ctx.extract_schema, trace: ctx.trace, span: ctx.span)
|
|
112
|
-
)
|
|
110
|
+
ctx.extract_schema!
|
|
113
111
|
end
|
|
114
112
|
|
|
115
113
|
AppSec::Event.record(ctx, request: gateway_request, response: gateway_response)
|
|
@@ -136,12 +136,20 @@ module Datadog
|
|
|
136
136
|
if Datadog::AppSec::Contrib::Rails::Patcher.target_version < Gem::Version.new('7.1')
|
|
137
137
|
Datadog::AppSec::Contrib::Rails::Patcher.report_routes_via_telemetry(::Rails.application.routes.routes)
|
|
138
138
|
end
|
|
139
|
+
rescue => e
|
|
140
|
+
error_message = 'Failed to get application routes'
|
|
141
|
+
Datadog.logger.error("#{error_message}, #{e.class}: #{e.message}")
|
|
142
|
+
AppSec.telemetry.report(e, description: error_message)
|
|
139
143
|
end
|
|
140
144
|
end
|
|
141
145
|
|
|
142
146
|
def subscribe_to_routes_loaded
|
|
143
|
-
::ActiveSupport.on_load(:after_routes_loaded) do
|
|
144
|
-
Datadog::AppSec::Contrib::Rails::Patcher.report_routes_via_telemetry(
|
|
147
|
+
::ActiveSupport.on_load(:after_routes_loaded) do
|
|
148
|
+
Datadog::AppSec::Contrib::Rails::Patcher.report_routes_via_telemetry(::Rails.application.routes.routes)
|
|
149
|
+
rescue => e
|
|
150
|
+
error_message = 'Failed to get application routes'
|
|
151
|
+
Datadog.logger.error("#{error_message}, #{e.class}: #{e.message}")
|
|
152
|
+
AppSec.telemetry.report(e, description: error_message)
|
|
145
153
|
end
|
|
146
154
|
end
|
|
147
155
|
|
|
@@ -11,6 +11,8 @@ module Datadog
|
|
|
11
11
|
context = request.env[Datadog::AppSec::Ext::CONTEXT_KEY]
|
|
12
12
|
return super unless context
|
|
13
13
|
|
|
14
|
+
context.state[:web_framework] = 'rails'
|
|
15
|
+
|
|
14
16
|
# TODO: handle exceptions, except for super
|
|
15
17
|
gateway_request = Gateway::Request.new(request)
|
|
16
18
|
http_response, _gateway_request = Instrumentation.gateway.push('rails.request.action', gateway_request) do
|
|
@@ -3,6 +3,8 @@
|
|
|
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
|
+
require_relative '../../utils/http/body'
|
|
6
8
|
|
|
7
9
|
module Datadog
|
|
8
10
|
module AppSec
|
|
@@ -10,35 +12,97 @@ module Datadog
|
|
|
10
12
|
module RestClient
|
|
11
13
|
# Module that adds SSRF detection to RestClient::Request#execute
|
|
12
14
|
module RequestSSRFDetectionPatch
|
|
15
|
+
REDIRECT_STATUS_CODES = (300..399).freeze
|
|
16
|
+
|
|
13
17
|
def execute(&block)
|
|
14
18
|
context = AppSec.active_context
|
|
15
19
|
return super unless context && AppSec.rasp_enabled?
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
headers = normalize_request_headers
|
|
22
|
+
# @type var ephemeral_data: ::Datadog::AppSec::Context::input_data
|
|
18
23
|
ephemeral_data = {
|
|
19
24
|
'server.io.net.url' => url,
|
|
20
25
|
'server.io.net.request.method' => method.to_s.upcase,
|
|
21
|
-
'server.io.net.request.headers' =>
|
|
26
|
+
'server.io.net.request.headers' => headers
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
is_redirect = context.state[:downstream_redirect_url] == url
|
|
30
|
+
if is_redirect
|
|
31
|
+
context.state.delete(:downstream_redirect_url)
|
|
32
|
+
sample_body = true
|
|
33
|
+
else
|
|
34
|
+
sample_body = mark_body_sampling!(context)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if !is_redirect && sample_body
|
|
38
|
+
body = parse_body(payload.to_s, content_type: headers['content-type'])
|
|
39
|
+
ephemeral_data['server.io.net.request.body'] = body if body
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
timeout = Datadog.configuration.appsec.waf_timeout
|
|
24
43
|
result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, timeout, phase: Ext::RASP_REQUEST_PHASE)
|
|
25
44
|
handle(result, context: context) if result.match?
|
|
26
45
|
|
|
27
|
-
|
|
46
|
+
# NOTE: RestClient raises exceptions for non-2xx responses. For POST/PUT/PATCH
|
|
47
|
+
# requests with 3xx redirects, RestClient raises instead of auto-following.
|
|
48
|
+
# We rescue to process the response before re-raising.
|
|
49
|
+
begin
|
|
50
|
+
response = super
|
|
51
|
+
rescue ::RestClient::Exception => e
|
|
52
|
+
response = e.response
|
|
53
|
+
process_response(response, sample_body: sample_body) if response
|
|
54
|
+
|
|
55
|
+
raise
|
|
56
|
+
end
|
|
28
57
|
|
|
58
|
+
process_response(response, sample_body: sample_body)
|
|
59
|
+
response
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def process_response(response, sample_body:)
|
|
63
|
+
context = AppSec.active_context
|
|
64
|
+
return unless context
|
|
65
|
+
|
|
66
|
+
headers = normalize_response_headers(response)
|
|
67
|
+
# @type var ephemeral_data: ::Datadog::AppSec::Context::input_data
|
|
29
68
|
ephemeral_data = {
|
|
30
69
|
'server.io.net.response.status' => response.code.to_s,
|
|
31
|
-
'server.io.net.response.headers' =>
|
|
70
|
+
'server.io.net.response.headers' => headers
|
|
32
71
|
}
|
|
33
72
|
|
|
73
|
+
is_redirect = REDIRECT_STATUS_CODES.cover?(response.code.to_i) && headers.key?('location')
|
|
74
|
+
context.state[:downstream_redirect_url] = URI.join(url, headers['location']).to_s if is_redirect && sample_body
|
|
75
|
+
|
|
76
|
+
if sample_body && !is_redirect
|
|
77
|
+
body = parse_body(response.body, content_type: headers['content-type'])
|
|
78
|
+
ephemeral_data['server.io.net.response.body'] = body if body
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
timeout = Datadog.configuration.appsec.waf_timeout
|
|
34
82
|
result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, timeout, phase: Ext::RASP_RESPONSE_PHASE)
|
|
35
83
|
handle(result, context: context) if result.match?
|
|
36
|
-
|
|
37
|
-
response
|
|
38
84
|
end
|
|
39
85
|
|
|
40
86
|
private
|
|
41
87
|
|
|
88
|
+
def mark_body_sampling!(context)
|
|
89
|
+
max = Datadog.configuration.appsec.api_security.downstream_body_analysis.max_requests
|
|
90
|
+
return false if context.state[:downstream_body_analyzed_count] >= max
|
|
91
|
+
return false unless context.downstream_body_sampler.sample?
|
|
92
|
+
|
|
93
|
+
context.state[:downstream_body_analyzed_count] += 1
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_body(body, content_type:)
|
|
98
|
+
return if body.empty?
|
|
99
|
+
|
|
100
|
+
media_type = Utils::HTTP::MediaType.parse(content_type)
|
|
101
|
+
return unless media_type
|
|
102
|
+
|
|
103
|
+
Utils::HTTP::Body.parse(body, media_type: media_type)
|
|
104
|
+
end
|
|
105
|
+
|
|
42
106
|
# NOTE: Starting version 2.1.0 headers are already normalized via internal
|
|
43
107
|
# variable `@processed_headers_lowercase`. In case it's available,
|
|
44
108
|
# we use it to avoid unnecessary transformation.
|
|
@@ -55,7 +119,8 @@ module Datadog
|
|
|
55
119
|
# FIXME: Steep has issues with `transform_values!` modifying the original
|
|
56
120
|
# type and it failed with "Cannot allow block body" error
|
|
57
121
|
def normalize_response_headers(response) # steep:ignore MethodBodyTypeMismatch
|
|
58
|
-
response.net_http_res.to_hash
|
|
122
|
+
response.net_http_res.to_hash
|
|
123
|
+
.transform_values! { |value| Array(value).join(', ') } # steep:ignore BlockBodyTypeMismatch
|
|
59
124
|
end
|
|
60
125
|
|
|
61
126
|
def handle(result, context:)
|
|
@@ -23,7 +23,9 @@ module Datadog
|
|
|
23
23
|
|
|
24
24
|
def watch_request_dispatch(gateway = Instrumentation.gateway)
|
|
25
25
|
gateway.watch('sinatra.request.dispatch', :appsec) do |stack, gateway_request|
|
|
26
|
-
context = gateway_request.env[AppSec::Ext::CONTEXT_KEY]
|
|
26
|
+
context = gateway_request.env[AppSec::Ext::CONTEXT_KEY] # : Context
|
|
27
|
+
|
|
28
|
+
context.state[:web_framework] = 'sinatra'
|
|
27
29
|
|
|
28
30
|
persistent_data = {
|
|
29
31
|
'server.request.body' => gateway_request.form_hash
|
|
@@ -49,8 +51,9 @@ module Datadog
|
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def watch_request_routed(gateway = Instrumentation.gateway)
|
|
52
|
-
gateway.watch('sinatra.request.routed', :appsec) do |stack,
|
|
53
|
-
|
|
54
|
+
gateway.watch('sinatra.request.routed', :appsec) do |stack, args|
|
|
55
|
+
gateway_request, gateway_route_params = args # : [Gateway::Request, Gateway::RouteParams]
|
|
56
|
+
context = gateway_request.env[AppSec::Ext::CONTEXT_KEY] # : Context
|
|
54
57
|
|
|
55
58
|
persistent_data = {
|
|
56
59
|
'server.request.path_params' => gateway_route_params.params
|
|
@@ -75,7 +78,7 @@ module Datadog
|
|
|
75
78
|
|
|
76
79
|
def watch_response_body_json(gateway = Instrumentation.gateway)
|
|
77
80
|
gateway.watch('sinatra.response.body.json', :appsec) do |stack, container|
|
|
78
|
-
context = container.context
|
|
81
|
+
context = container.context # : Context
|
|
79
82
|
|
|
80
83
|
persistent_data = {
|
|
81
84
|
'server.response.body' => container.data
|
|
@@ -71,7 +71,7 @@ module Datadog
|
|
|
71
71
|
# path params are returned by pattern.params in process_route, then
|
|
72
72
|
# merged with normal params, so we get both
|
|
73
73
|
module RoutePatch
|
|
74
|
-
def process_route(*)
|
|
74
|
+
def process_route(*args)
|
|
75
75
|
env = @request.env
|
|
76
76
|
|
|
77
77
|
context = env[Datadog::AppSec::Ext::CONTEXT_KEY]
|
|
@@ -83,7 +83,7 @@ module Datadog
|
|
|
83
83
|
# Capture normal params.
|
|
84
84
|
base_params = params
|
|
85
85
|
|
|
86
|
-
super do |*
|
|
86
|
+
super do |*super_args|
|
|
87
87
|
# This block is called only once the route is found.
|
|
88
88
|
# At this point params has both route params and normal params.
|
|
89
89
|
route_params = params.each.with_object({}) { |(k, v), h| h[k] = v unless base_params.key?(k) }
|
|
@@ -93,7 +93,7 @@ module Datadog
|
|
|
93
93
|
|
|
94
94
|
Instrumentation.gateway.push('sinatra.request.routed', [gateway_request, gateway_route_params])
|
|
95
95
|
|
|
96
|
-
yield(*
|
|
96
|
+
yield(*super_args)
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
end
|
|
@@ -123,7 +123,7 @@ module Datadog
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
def patch_json?
|
|
126
|
-
defined?(::Sinatra::JSON) && ::Sinatra::Base < ::Sinatra::JSON
|
|
126
|
+
!!(defined?(::Sinatra::JSON) && ::Sinatra::Base < ::Sinatra::JSON)
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
end
|
|
@@ -12,7 +12,7 @@ module Datadog
|
|
|
12
12
|
# body right before it is serialized.
|
|
13
13
|
module JsonPatch
|
|
14
14
|
def json(object, options = {})
|
|
15
|
-
context = @request.env[Datadog::AppSec::Ext::CONTEXT_KEY]
|
|
15
|
+
context = @request.env[Datadog::AppSec::Ext::CONTEXT_KEY] # : Context?
|
|
16
16
|
return super unless context
|
|
17
17
|
|
|
18
18
|
data = Utils::HashCoercion.coerce(object)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../core/knuth_sampler'
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module AppSec
|
|
7
|
+
# Sampler that uses an internal counter to make deterministic sampling decisions.
|
|
8
|
+
#
|
|
9
|
+
# Each call to {#sample?} increments the counter and uses it as input to
|
|
10
|
+
# the underlying Knuth multiplicative hash algorithm.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
class CounterSampler
|
|
14
|
+
def initialize(rate = 1.0)
|
|
15
|
+
@sampler = Core::KnuthSampler.new(rate)
|
|
16
|
+
@counter = 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def sample?
|
|
20
|
+
@counter += 1
|
|
21
|
+
@sampler.sample?(@counter)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -23,6 +23,24 @@ module Datadog
|
|
|
23
23
|
}
|
|
24
24
|
)
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
def export_api_security_metrics(context)
|
|
28
|
+
web_framework = context.state[:web_framework]
|
|
29
|
+
return unless web_framework
|
|
30
|
+
|
|
31
|
+
if context.span&.get_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE).nil?
|
|
32
|
+
AppSec.telemetry.inc(
|
|
33
|
+
AppSec::Ext::TELEMETRY_METRICS_NAMESPACE, 'api_security.missing_route', 1,
|
|
34
|
+
tags: {framework: web_framework}
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
metric_name = context.state[:schema_extracted] ? 'schema' : 'no_schema'
|
|
39
|
+
AppSec.telemetry.inc(
|
|
40
|
+
AppSec::Ext::TELEMETRY_METRICS_NAMESPACE, "api_security.request.#{metric_name}", 1,
|
|
41
|
+
tags: {framework: web_framework}
|
|
42
|
+
)
|
|
43
|
+
end
|
|
26
44
|
end
|
|
27
45
|
end
|
|
28
46
|
end
|
|
@@ -40,12 +40,16 @@ module Datadog
|
|
|
40
40
|
@ruleset_version = diagnostics['ruleset_version']
|
|
41
41
|
|
|
42
42
|
@handle_ref = ThreadSafeRef.new(@waf_builder.build_handle)
|
|
43
|
+
|
|
44
|
+
metric('init', success: true, ruleset_version: @ruleset_version, telemetry: telemetry)
|
|
43
45
|
rescue WAF::Error => e
|
|
44
|
-
error_message =
|
|
46
|
+
error_message = 'AppSec security engine failed to initialize'
|
|
45
47
|
|
|
46
48
|
Datadog.logger.error("#{error_message}, error #{e.inspect}")
|
|
47
49
|
telemetry.report(e, description: error_message)
|
|
48
50
|
|
|
51
|
+
metric('init', success: false, ruleset_version: @ruleset_version, telemetry: telemetry)
|
|
52
|
+
|
|
49
53
|
raise e
|
|
50
54
|
end
|
|
51
55
|
|
|
@@ -103,17 +107,34 @@ module Datadog
|
|
|
103
107
|
@ruleset_version = @reconfigured_ruleset_version
|
|
104
108
|
|
|
105
109
|
@handle_ref.current = new_waf_handle
|
|
110
|
+
|
|
111
|
+
metric('updates', success: true, ruleset_version: @ruleset_version, telemetry: AppSec.telemetry)
|
|
106
112
|
rescue WAF::Error => e
|
|
107
113
|
# WAF::Error can only be raised during new WAF handle creation or when reading known addresses.
|
|
108
114
|
# This means that the current WAF handle was not yet substituted.
|
|
109
|
-
error_message =
|
|
115
|
+
error_message = 'AppSec security engine failed to reconfigure, reverting to the previous configuration'
|
|
110
116
|
|
|
111
117
|
Datadog.logger.error("#{error_message}, error #{e.inspect}")
|
|
112
118
|
AppSec.telemetry.report(e, description: error_message)
|
|
119
|
+
|
|
120
|
+
metric('updates', success: false, ruleset_version: @reconfigured_ruleset_version, telemetry: AppSec.telemetry)
|
|
113
121
|
end
|
|
114
122
|
|
|
115
123
|
private
|
|
116
124
|
|
|
125
|
+
def metric(metric_name, success:, ruleset_version:, telemetry:)
|
|
126
|
+
telemetry.inc(
|
|
127
|
+
Ext::TELEMETRY_METRICS_NAMESPACE,
|
|
128
|
+
"waf.#{metric_name}",
|
|
129
|
+
1,
|
|
130
|
+
tags: {
|
|
131
|
+
waf_version: Datadog::AppSec::WAF::VERSION::BASE_STRING,
|
|
132
|
+
event_rules_version: ruleset_version.to_s,
|
|
133
|
+
success: success.to_s
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
117
138
|
def load_default_config(telemetry:)
|
|
118
139
|
config = AppSec::Processor::RuleLoader.load_rules(telemetry: telemetry, ruleset: @default_ruleset)
|
|
119
140
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'cgi'
|
|
5
|
+
|
|
6
|
+
require_relative 'url_encoded'
|
|
7
|
+
|
|
8
|
+
module Datadog
|
|
9
|
+
module AppSec
|
|
10
|
+
module Utils
|
|
11
|
+
module HTTP
|
|
12
|
+
# Module for handling HTTP body parsing
|
|
13
|
+
module Body
|
|
14
|
+
def self.parse(body, media_type:)
|
|
15
|
+
return if body.nil?
|
|
16
|
+
|
|
17
|
+
body.rewind if body.respond_to?(:rewind) # steep:ignore NoMethod
|
|
18
|
+
# @type var content: ::String?
|
|
19
|
+
content = body.respond_to?(:read) ? body.read : body # steep:ignore NoMethod, IncompatibleAssignment
|
|
20
|
+
body.rewind if body.respond_to?(:rewind) # steep:ignore NoMethod
|
|
21
|
+
|
|
22
|
+
return if content.nil? || content.empty?
|
|
23
|
+
|
|
24
|
+
if media_type.subtype == 'json' || media_type.subtype.end_with?('+json')
|
|
25
|
+
JSON.parse(content)
|
|
26
|
+
elsif media_type.subtype == 'x-www-form-urlencoded'
|
|
27
|
+
URLEncoded.parse(content)
|
|
28
|
+
end
|
|
29
|
+
rescue => e
|
|
30
|
+
AppSec.telemetry.report(e, description: 'AppSec: Failed to parse body')
|
|
31
|
+
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -168,7 +168,8 @@ module Datadog
|
|
|
168
168
|
#
|
|
169
169
|
# returns true if the MediaType is accepted by this MediaRange
|
|
170
170
|
def ===(other)
|
|
171
|
-
return
|
|
171
|
+
return false if other.nil?
|
|
172
|
+
return self === MediaType.parse(other) if other.is_a?(::String)
|
|
172
173
|
|
|
173
174
|
type == other.type && subtype == other.subtype && other.parameters.all? { |k, v| parameters[k] == v } ||
|
|
174
175
|
type == other.type && wildcard?(:subtype) ||
|