datadog 2.28.0 → 2.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +82 -12
  4. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +32 -11
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -1
  6. data/ext/datadog_profiling_native_extension/extconf.rb +9 -24
  7. data/ext/datadog_profiling_native_extension/heap_recorder.c +186 -55
  8. data/ext/datadog_profiling_native_extension/heap_recorder.h +12 -1
  9. data/ext/datadog_profiling_native_extension/http_transport.c +51 -64
  10. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -13
  11. data/ext/datadog_profiling_native_extension/profiling.c +3 -1
  12. data/ext/datadog_profiling_native_extension/setup_signal_handler.c +24 -8
  13. data/ext/datadog_profiling_native_extension/setup_signal_handler.h +1 -3
  14. data/ext/datadog_profiling_native_extension/stack_recorder.c +63 -48
  15. data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -1
  16. data/ext/libdatadog_api/crashtracker.c +5 -0
  17. data/ext/libdatadog_api/crashtracker_report_exception.c +126 -0
  18. data/ext/libdatadog_api/extconf.rb +4 -21
  19. data/ext/libdatadog_extconf_helpers.rb +49 -11
  20. data/lib/datadog/ai_guard/configuration/settings.rb +3 -0
  21. data/lib/datadog/appsec/assets/blocked.html +2 -1
  22. data/lib/datadog/appsec/configuration/settings.rb +14 -0
  23. data/lib/datadog/appsec/context.rb +44 -9
  24. data/lib/datadog/appsec/contrib/active_record/patcher.rb +3 -0
  25. data/lib/datadog/appsec/contrib/devise/integration.rb +1 -1
  26. data/lib/datadog/appsec/contrib/excon/patcher.rb +2 -0
  27. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +55 -6
  28. data/lib/datadog/appsec/contrib/faraday/integration.rb +1 -1
  29. data/lib/datadog/appsec/contrib/faraday/patcher.rb +1 -1
  30. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +60 -7
  31. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +11 -6
  32. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +1 -1
  33. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -1
  34. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +6 -10
  35. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +4 -4
  36. data/lib/datadog/appsec/contrib/rack/integration.rb +1 -1
  37. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +26 -5
  38. data/lib/datadog/appsec/contrib/rack/response_body.rb +36 -0
  39. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +2 -2
  40. data/lib/datadog/appsec/contrib/rails/integration.rb +1 -1
  41. data/lib/datadog/appsec/contrib/rails/patches/process_action_patch.rb +2 -0
  42. data/lib/datadog/appsec/contrib/rest_client/patcher.rb +2 -0
  43. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +72 -7
  44. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +5 -3
  45. data/lib/datadog/appsec/counter_sampler.rb +25 -0
  46. data/lib/datadog/appsec/event.rb +1 -17
  47. data/lib/datadog/appsec/instrumentation/gateway/middleware.rb +2 -3
  48. data/lib/datadog/appsec/instrumentation/gateway.rb +2 -2
  49. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +18 -0
  50. data/lib/datadog/appsec/monitor/gateway/watcher.rb +2 -2
  51. data/lib/datadog/appsec/security_engine/engine.rb +23 -2
  52. data/lib/datadog/appsec/utils/http/body.rb +38 -0
  53. data/lib/datadog/appsec/utils/http/media_range.rb +2 -1
  54. data/lib/datadog/appsec/utils/http/media_type.rb +28 -35
  55. data/lib/datadog/appsec/utils/http/url_encoded.rb +52 -0
  56. data/lib/datadog/core/configuration/components.rb +29 -4
  57. data/lib/datadog/core/configuration/option.rb +2 -1
  58. data/lib/datadog/core/configuration/options.rb +1 -1
  59. data/lib/datadog/core/configuration/settings.rb +27 -3
  60. data/lib/datadog/core/configuration/supported_configurations.rb +19 -0
  61. data/lib/datadog/core/configuration.rb +2 -2
  62. data/lib/datadog/core/crashtracking/component.rb +71 -19
  63. data/lib/datadog/core/environment/agent_info.rb +65 -1
  64. data/lib/datadog/core/logger.rb +1 -1
  65. data/lib/datadog/core/metrics/logging.rb +1 -1
  66. data/lib/datadog/core/process_discovery.rb +20 -19
  67. data/lib/datadog/core/rate_limiter.rb +2 -0
  68. data/lib/datadog/core/remote/component.rb +16 -5
  69. data/lib/datadog/core/remote/transport/config.rb +5 -11
  70. data/lib/datadog/core/runtime/metrics.rb +1 -2
  71. data/lib/datadog/core/telemetry/component.rb +0 -13
  72. data/lib/datadog/core/telemetry/transport/telemetry.rb +5 -6
  73. data/lib/datadog/core/transport/ext.rb +1 -0
  74. data/lib/datadog/core/transport/http/response.rb +4 -0
  75. data/lib/datadog/core/transport/parcel.rb +61 -9
  76. data/lib/datadog/core/utils/base64.rb +1 -1
  77. data/lib/datadog/core/utils/fnv.rb +26 -0
  78. data/lib/datadog/core/workers/interval_loop.rb +13 -6
  79. data/lib/datadog/core/workers/queue.rb +0 -4
  80. data/lib/datadog/core/workers/runtime_metrics.rb +9 -1
  81. data/lib/datadog/core.rb +6 -1
  82. data/lib/datadog/data_streams/processor.rb +35 -33
  83. data/lib/datadog/data_streams/transport/http/stats.rb +6 -0
  84. data/lib/datadog/data_streams/transport/http.rb +0 -4
  85. data/lib/datadog/data_streams/transport/stats.rb +5 -12
  86. data/lib/datadog/di/boot.rb +1 -0
  87. data/lib/datadog/di/component.rb +17 -5
  88. data/lib/datadog/di/configuration/settings.rb +9 -0
  89. data/lib/datadog/di/context.rb +6 -0
  90. data/lib/datadog/di/instrumenter.rb +183 -134
  91. data/lib/datadog/di/probe.rb +10 -1
  92. data/lib/datadog/di/probe_file_loader.rb +2 -2
  93. data/lib/datadog/di/probe_manager.rb +86 -64
  94. data/lib/datadog/di/probe_notification_builder.rb +46 -18
  95. data/lib/datadog/di/probe_notifier_worker.rb +65 -9
  96. data/lib/datadog/di/probe_repository.rb +198 -0
  97. data/lib/datadog/di/proc_responder.rb +4 -0
  98. data/lib/datadog/di/remote.rb +6 -7
  99. data/lib/datadog/di/serializer.rb +127 -9
  100. data/lib/datadog/di/transport/diagnostics.rb +5 -7
  101. data/lib/datadog/di/transport/http/diagnostics.rb +3 -1
  102. data/lib/datadog/di/transport/http/input.rb +1 -1
  103. data/lib/datadog/di/transport/http.rb +12 -3
  104. data/lib/datadog/di/transport/input.rb +51 -14
  105. data/lib/datadog/kit/tracing/method_tracer.rb +132 -0
  106. data/lib/datadog/open_feature/configuration.rb +2 -0
  107. data/lib/datadog/open_feature/transport.rb +8 -11
  108. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +13 -0
  109. data/lib/datadog/profiling/component.rb +20 -6
  110. data/lib/datadog/profiling/http_transport.rb +5 -6
  111. data/lib/datadog/profiling/profiler.rb +15 -8
  112. data/lib/datadog/tracing/contrib/dalli/integration.rb +4 -1
  113. data/lib/datadog/tracing/contrib/ethon/configuration/settings.rb +5 -1
  114. data/lib/datadog/tracing/contrib/ethon/ext.rb +1 -0
  115. data/lib/datadog/tracing/contrib/excon/configuration/settings.rb +5 -2
  116. data/lib/datadog/tracing/contrib/excon/ext.rb +1 -0
  117. data/lib/datadog/tracing/contrib/faraday/configuration/settings.rb +5 -2
  118. data/lib/datadog/tracing/contrib/faraday/ext.rb +1 -0
  119. data/lib/datadog/tracing/contrib/grape/endpoint.rb +2 -2
  120. data/lib/datadog/tracing/contrib/grape/instrumentation.rb +13 -8
  121. data/lib/datadog/tracing/contrib/grape/patcher.rb +6 -1
  122. data/lib/datadog/tracing/contrib/grpc/configuration/settings.rb +5 -2
  123. data/lib/datadog/tracing/contrib/grpc/ext.rb +1 -0
  124. data/lib/datadog/tracing/contrib/http/configuration/settings.rb +5 -2
  125. data/lib/datadog/tracing/contrib/http/ext.rb +1 -0
  126. data/lib/datadog/tracing/contrib/http/integration.rb +0 -2
  127. data/lib/datadog/tracing/contrib/httpclient/configuration/settings.rb +5 -2
  128. data/lib/datadog/tracing/contrib/httpclient/ext.rb +1 -0
  129. data/lib/datadog/tracing/contrib/httprb/configuration/settings.rb +5 -2
  130. data/lib/datadog/tracing/contrib/httprb/ext.rb +1 -0
  131. data/lib/datadog/tracing/contrib/karafka/configuration/settings.rb +5 -1
  132. data/lib/datadog/tracing/contrib/karafka/ext.rb +1 -0
  133. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +6 -0
  134. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +2 -1
  135. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +6 -0
  136. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +2 -1
  137. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +10 -0
  138. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +5 -1
  139. data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +24 -0
  140. data/lib/datadog/tracing/contrib/que/configuration/settings.rb +5 -2
  141. data/lib/datadog/tracing/contrib/que/ext.rb +1 -0
  142. data/lib/datadog/tracing/contrib/rack/configuration/settings.rb +5 -1
  143. data/lib/datadog/tracing/contrib/rack/ext.rb +1 -0
  144. data/lib/datadog/tracing/contrib/rack/route_inference.rb +18 -6
  145. data/lib/datadog/tracing/contrib/rails/configuration/settings.rb +5 -2
  146. data/lib/datadog/tracing/contrib/rails/ext.rb +1 -0
  147. data/lib/datadog/tracing/contrib/registerable.rb +11 -0
  148. data/lib/datadog/tracing/contrib/rest_client/configuration/settings.rb +5 -2
  149. data/lib/datadog/tracing/contrib/rest_client/ext.rb +1 -0
  150. data/lib/datadog/tracing/contrib/sidekiq/configuration/settings.rb +5 -1
  151. data/lib/datadog/tracing/contrib/sidekiq/ext.rb +1 -0
  152. data/lib/datadog/tracing/contrib/sinatra/configuration/settings.rb +5 -1
  153. data/lib/datadog/tracing/contrib/sinatra/ext.rb +1 -0
  154. data/lib/datadog/tracing/contrib/sneakers/integration.rb +15 -4
  155. data/lib/datadog/tracing/contrib/trilogy/configuration/settings.rb +6 -0
  156. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +3 -1
  157. data/lib/datadog/tracing/contrib/waterdrop/configuration/settings.rb +5 -1
  158. data/lib/datadog/tracing/contrib/waterdrop/ext.rb +1 -0
  159. data/lib/datadog/tracing/metadata/ext.rb +4 -0
  160. data/lib/datadog/tracing/sync_writer.rb +0 -1
  161. data/lib/datadog/tracing/transport/io/client.rb +5 -8
  162. data/lib/datadog/tracing/transport/io/traces.rb +28 -34
  163. data/lib/datadog/tracing/transport/trace_formatter.rb +11 -0
  164. data/lib/datadog/tracing/transport/traces.rb +4 -10
  165. data/lib/datadog/tracing/writer.rb +0 -1
  166. data/lib/datadog/version.rb +1 -1
  167. metadata +14 -8
  168. data/lib/datadog/appsec/contrib/rails/ext.rb +0 -13
  169. data/lib/datadog/tracing/workers/trace_writer.rb +0 -204
@@ -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
@@ -20,6 +20,8 @@ module Datadog
20
20
  require_relative 'request_ssrf_detection_patch'
21
21
 
22
22
  ::RestClient::Request.prepend(RequestSSRFDetectionPatch)
23
+
24
+ Patcher.instance_variable_set(:@patched, true)
23
25
  end
24
26
  end
25
27
  end
@@ -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
- timeout = Datadog.configuration.appsec.waf_timeout
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' => normalize_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
- response = super
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.is_a?(::RestClient::AbstractResponse)
54
+
55
+ raise
56
+ end
28
57
 
58
+ process_response(response, sample_body: sample_body) if response.is_a?(::RestClient::AbstractResponse)
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' => normalize_response_headers(response)
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.transform_values! { |value| Array(value).join(', ') } # steep:ignore BlockBodyTypeMismatch
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:)
@@ -22,9 +22,11 @@ module Datadog
22
22
  end
23
23
 
24
24
  def watch_request_dispatch(gateway = Instrumentation.gateway)
25
- gateway.watch('sinatra.request.dispatch', :appsec) do |stack, gateway_request|
25
+ gateway.watch('sinatra.request.dispatch') do |stack, gateway_request|
26
26
  context = gateway_request.env[AppSec::Ext::CONTEXT_KEY] # : Context
27
27
 
28
+ context.state[:web_framework] = 'sinatra'
29
+
28
30
  persistent_data = {
29
31
  'server.request.body' => gateway_request.form_hash
30
32
  }
@@ -49,7 +51,7 @@ 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, args|
54
+ gateway.watch('sinatra.request.routed') do |stack, args|
53
55
  gateway_request, gateway_route_params = args # : [Gateway::Request, Gateway::RouteParams]
54
56
  context = gateway_request.env[AppSec::Ext::CONTEXT_KEY] # : Context
55
57
 
@@ -75,7 +77,7 @@ module Datadog
75
77
  end
76
78
 
77
79
  def watch_response_body_json(gateway = Instrumentation.gateway)
78
- gateway.watch('sinatra.response.body.json', :appsec) do |stack, container|
80
+ gateway.watch('sinatra.response.body.json') do |stack, container|
79
81
  context = container.context # : Context
80
82
 
81
83
  persistent_data = {
@@ -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
@@ -32,13 +32,6 @@ module Datadog
32
32
  accept-language
33
33
  ].freeze
34
34
 
35
- ALLOWED_RESPONSE_HEADERS = %w[
36
- content-length
37
- content-type
38
- content-encoding
39
- content-language
40
- ].freeze
41
-
42
35
  class << self
43
36
  def tag(context, waf_result)
44
37
  return if context.span.nil?
@@ -50,7 +43,7 @@ module Datadog
50
43
  context.span.set_tag('appsec.event', 'true')
51
44
  end
52
45
 
53
- def record(context, request: nil, response: nil)
46
+ def record(context, request: nil)
54
47
  return if context.events.empty? || context.span.nil?
55
48
 
56
49
  Datadog::AppSec::RateLimiter.thread_local.limit do
@@ -66,7 +59,6 @@ module Datadog
66
59
 
67
60
  context.span['_dd.origin'] = 'appsec'
68
61
  context.span.set_tags(request_tags(request)) if request
69
- context.span.set_tags(response_tags(response)) if response
70
62
  end
71
63
 
72
64
  context.span.set_tags(waf_tags(event_group))
@@ -90,14 +82,6 @@ module Datadog
90
82
  end
91
83
  end
92
84
 
93
- def response_tags(response)
94
- response.headers.each_with_object({}) do |(name, value), memo|
95
- next unless ALLOWED_RESPONSE_HEADERS.include?(name)
96
-
97
- memo["http.response.headers.#{name}"] = value
98
- end
99
- end
100
-
101
85
  def waf_tags(security_events)
102
86
  triggers = []
103
87
 
@@ -7,10 +7,9 @@ module Datadog
7
7
  # NOTE: This class extracted as-is and will be deprecated
8
8
  # Instrumentation gateway middleware
9
9
  class Middleware
10
- attr_reader :key, :block
10
+ attr_reader :block
11
11
 
12
- def initialize(key, &block)
13
- @key = key
12
+ def initialize(&block)
14
13
  @block = block
15
14
  end
16
15
 
@@ -41,8 +41,8 @@ module Datadog
41
41
  stack.call(env)
42
42
  end
43
43
 
44
- def watch(name, key, &block)
45
- @middlewares[name] << Middleware.new(key, &block) unless @middlewares[name].any? { |m| m.key == key }
44
+ def watch(name, &block)
45
+ @middlewares[name] << Middleware.new(&block)
46
46
  end
47
47
 
48
48
  def pushed?(name)
@@ -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
@@ -24,7 +24,7 @@ module Datadog
24
24
  end
25
25
 
26
26
  def watch_user_id(gateway = Instrumentation.gateway)
27
- gateway.watch('identity.set_user', :appsec) do |stack, user|
27
+ gateway.watch('identity.set_user') do |stack, user|
28
28
  context = AppSec.active_context
29
29
 
30
30
  if user.id.nil? && user.login.nil? && user.session_id.nil?
@@ -55,7 +55,7 @@ module Datadog
55
55
  end
56
56
 
57
57
  def watch_user_login(gateway = Instrumentation.gateway)
58
- gateway.watch('appsec.events.user_lifecycle', :appsec) do |stack, kind|
58
+ gateway.watch('appsec.events.user_lifecycle') do |stack, kind|
59
59
  context = AppSec.active_context
60
60
 
61
61
  next stack.call(kind) unless WATCHED_LOGIN_EVENTS.include?(kind)
@@ -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 = "AppSec security engine failed to initialize"
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 = "AppSec security engine failed to reconfigure, reverting to the previous configuration"
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 self === MediaType.new(other) if other.is_a?(::String)
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) ||
@@ -10,8 +10,6 @@ module Datadog
10
10
  # - https://www.rfc-editor.org/rfc/rfc7231#section-5.3.1
11
11
  # - https://www.rfc-editor.org/rfc/rfc7231#section-5.3.2
12
12
  class MediaType
13
- ParseError = Class.new(StandardError) # steep:ignore IncompatibleAssignment
14
-
15
13
  WILDCARD = '*'
16
14
 
17
15
  # See: https://www.rfc-editor.org/rfc/rfc7230#section-3.2.6
@@ -45,48 +43,43 @@ module Datadog
45
43
 
46
44
  attr_reader :type, :subtype, :parameters
47
45
 
48
- def self.json?(media_type)
49
- return false if media_type.nil? || media_type.empty?
50
-
51
- match = MEDIA_TYPE_RE.match(media_type)
52
- return false if match.nil?
46
+ def self.parse(media)
47
+ match = MEDIA_TYPE_RE.match(media)
48
+ return if match.nil?
53
49
 
54
- subtype = match['subtype']
55
- return false if subtype.nil? || subtype.empty?
50
+ type = match['type'] || WILDCARD
51
+ type.downcase!
56
52
 
53
+ subtype = match['subtype'] || WILDCARD
57
54
  subtype.downcase!
58
- subtype == 'json' || subtype.end_with?('+json')
59
- end
60
-
61
- def initialize(media_type)
62
- match = MEDIA_TYPE_RE.match(media_type)
63
- raise ParseError, media_type.inspect if match.nil?
64
55
 
65
- @type = match['type'] || WILDCARD
66
- @type.downcase!
56
+ parameters = {}
57
+ params = match['parameters']
67
58
 
68
- @subtype = match['subtype'] || WILDCARD
69
- @subtype.downcase!
59
+ unless params.nil? || params.empty?
60
+ params.scan(PARAMETER_RE) do |name, unquoted_value, quoted_value|
61
+ # NOTE: Order of unquoted_value and quoted_value does not matter,
62
+ # as they are mutually exclusive by the regex.
63
+ # @type var value: ::String?
64
+ value = unquoted_value || quoted_value
65
+ next if name.nil? || value.nil?
70
66
 
71
- @parameters = {}
67
+ # See https://github.com/soutaro/steep/issues/2051
68
+ name.downcase! # steep:ignore NoMethod
69
+ value.downcase!
72
70
 
73
- parameters = match['parameters']
74
- return if parameters.nil? || parameters.empty?
75
-
76
- parameters.scan(PARAMETER_RE) do |name, unquoted_value, quoted_value|
77
- # NOTE: Order of unquoted_value and quoted_value does not matter,
78
- # as they are mutually exclusive by the regex.
79
- # @type var value: ::String?
80
- value = unquoted_value || quoted_value
81
- next if name.nil? || value.nil?
71
+ # See https://github.com/soutaro/steep/issues/2051
72
+ parameters[name] = value # steep:ignore ArgumentTypeMismatch
73
+ end
74
+ end
82
75
 
83
- # See https://github.com/soutaro/steep/issues/2051
84
- name.downcase! # steep:ignore NoMethod
85
- value.downcase!
76
+ new(type: type, subtype: subtype, parameters: parameters)
77
+ end
86
78
 
87
- # See https://github.com/soutaro/steep/issues/2051
88
- @parameters[name] = value # steep:ignore ArgumentTypeMismatch
89
- end
79
+ def initialize(type:, subtype:, parameters: {})
80
+ @type = type
81
+ @subtype = subtype
82
+ @parameters = parameters
90
83
  end
91
84
 
92
85
  def to_s
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+
5
+ module Datadog
6
+ module AppSec
7
+ module Utils
8
+ module HTTP
9
+ # Module for parsing URL encoded payloads
10
+ module URLEncoded
11
+ # Parses a URL encoded payload (query string or form data) into a hash
12
+ # of keys and values, merging duplicate keys.
13
+ #
14
+ # Example:
15
+ #
16
+ # URLEncoded.parse("foo=bar&foo=baz&qux=quux") # => {"foo" => ["bar", "baz"], "qux" => "quux"}
17
+ #
18
+ # NOTE: Use it in the absence of `Rack::Utils.parse_query`
19
+ #
20
+ # WARNING: This method doesn't limit params byte size.
21
+ # See: https://github.com/rack/rack/blob/603b799de38b5eb9b2ff1657c8036a20f4c4db7b/lib/rack/query_parser.rb#L231-L233
22
+ def self.parse(payload)
23
+ return {} if payload.nil? || payload.empty?
24
+
25
+ payload.split('&').each_with_object({}) do |pair, memo|
26
+ next if pair.empty?
27
+
28
+ # NOTE: Steep has issues with mutation methods
29
+ # See https://github.com/ruby/rbs/issues/2819
30
+ #
31
+ # @type var key: ::String
32
+ # @type var value: ::String
33
+ key, value = pair.split('=', 2).map! do |value| #: ::String
34
+ CGI.unescape(value)
35
+ end
36
+
37
+ if (stored = memo[key])
38
+ if stored.is_a?(Array)
39
+ stored.push(value)
40
+ else
41
+ memo[key] = [stored, value]
42
+ end
43
+ else
44
+ memo[key] = value
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -11,6 +11,8 @@ require_relative '../runtime/metrics'
11
11
  require_relative '../telemetry/component'
12
12
  require_relative '../workers/runtime_metrics'
13
13
  require_relative '../remote/component'
14
+ require_relative '../utils/at_fork_monkey_patch'
15
+ require_relative '../utils/only_once'
14
16
  require_relative '../../tracing/component'
15
17
  require_relative '../../profiling/component'
16
18
  require_relative '../../appsec/component'
@@ -28,6 +30,9 @@ module Datadog
28
30
  module Configuration
29
31
  # Global components for the trace library.
30
32
  class Components
33
+ # Class-level constant to ensure fork patch is applied only once
34
+ AT_FORK_ONLY_ONCE = Utils::OnlyOnce.new
35
+
31
36
  class << self
32
37
  def build_health_metrics(settings, logger, telemetry)
33
38
  settings = settings.health_metrics
@@ -38,7 +43,7 @@ module Datadog
38
43
  end
39
44
 
40
45
  def build_logger(settings)
41
- logger = settings.logger.instance || Core::Logger.new($stdout)
46
+ logger = settings.logger.instance || Core::Logger.new($stderr)
42
47
  logger.level = settings.diagnostics.debug ? ::Logger::DEBUG : settings.logger.level
43
48
 
44
49
  logger
@@ -80,14 +85,15 @@ module Datadog
80
85
  Datadog::Core::Crashtracking::Component.build(settings, agent_settings, logger: logger)
81
86
  end
82
87
 
83
- def build_data_streams(settings, agent_settings, logger)
88
+ def build_data_streams(settings, agent_settings, logger, agent_info)
84
89
  return unless settings.data_streams.enabled
85
90
 
86
91
  Datadog::DataStreams::Processor.new(
87
92
  interval: settings.data_streams.interval,
88
93
  logger: logger,
89
94
  settings: settings,
90
- agent_settings: agent_settings
95
+ agent_settings: agent_settings,
96
+ agent_info: agent_info
91
97
  )
92
98
  rescue => e
93
99
  logger.warn("Failed to initialize Data Streams Monitoring: #{e.class}: #{e}")
@@ -121,6 +127,17 @@ module Datadog
121
127
  StableConfig.log_result(@logger)
122
128
  Deprecations.log_deprecations_from_all_sources(@logger)
123
129
 
130
+ # Register fork handling once globally
131
+ self.class::AT_FORK_ONLY_ONCE.run do
132
+ Utils::AtForkMonkeyPatch.apply!
133
+
134
+ # Register callback that calls Components.after_fork
135
+ Utils::AtForkMonkeyPatch.at_fork(:child) do
136
+ # Access via global to avoid capturing 'self'
137
+ Datadog.send(:components, allow_initialization: false)&.after_fork
138
+ end
139
+ end
140
+
124
141
  # This agent_settings is intended for use within Core. If you require
125
142
  # agent_settings within a product outside of core you should extend
126
143
  # the Core resolver from within your product/component's namespace.
@@ -150,13 +167,21 @@ module Datadog
150
167
  @open_feature = OpenFeature::Component.build(settings, agent_settings, logger: @logger, telemetry: telemetry)
151
168
  @dynamic_instrumentation = Datadog::DI::Component.build(settings, agent_settings, @logger, telemetry: telemetry)
152
169
  @error_tracking = Datadog::ErrorTracking::Component.build(settings, @tracer, @logger)
153
- @data_streams = self.class.build_data_streams(settings, agent_settings, @logger)
170
+ @data_streams = self.class.build_data_streams(settings, agent_settings, @logger, @agent_info)
154
171
  @environment_logger_extra[:dynamic_instrumentation_enabled] = !!@dynamic_instrumentation
155
172
 
156
173
  # Configure non-privileged components.
157
174
  Datadog::Tracing::Contrib::Component.configure(settings)
158
175
  end
159
176
 
177
+ # Called when a fork is detected
178
+ def after_fork
179
+ telemetry.after_fork
180
+ remote&.after_fork
181
+ crashtracker&.update_on_fork
182
+ ProcessDiscovery.after_fork
183
+ end
184
+
160
185
  # Hot-swaps with a new sampler.
161
186
  # This operation acquires the Components lock to ensure
162
187
  # there is no concurrent modification of the sampler.
@@ -167,7 +167,8 @@ module Datadog
167
167
 
168
168
  def default_value
169
169
  if definition.default.instance_of?(Proc)
170
- context_eval(&definition.default)
170
+ # Steep: https://github.com/soutaro/steep/issues/335
171
+ context_eval(&definition.default) # steep:ignore BlockTypeMismatch
171
172
  else
172
173
  definition.default_proc || Core::Utils::SafeDup.frozen_or_dup(definition.default)
173
174
  end
@@ -120,7 +120,7 @@ module Datadog
120
120
 
121
121
  assert_valid_option!(name)
122
122
  definition = self.class.options[name]
123
- # @type self: Configuration::Options::GenericSettingsClass
123
+ # @type self: Configuration::Options::_Settings
124
124
  options[name] = definition.build(self)
125
125
  end
126
126