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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -2
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +64 -3
  4. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +23 -4
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -1
  6. data/ext/datadog_profiling_native_extension/extconf.rb +5 -0
  7. data/ext/datadog_profiling_native_extension/heap_recorder.c +183 -51
  8. data/ext/datadog_profiling_native_extension/heap_recorder.h +12 -1
  9. data/ext/datadog_profiling_native_extension/stack_recorder.c +34 -5
  10. data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -1
  11. data/ext/libdatadog_api/crashtracker.c +5 -0
  12. data/ext/libdatadog_api/crashtracker_report_exception.c +236 -0
  13. data/lib/datadog/ai_guard/configuration/settings.rb +13 -1
  14. data/lib/datadog/ai_guard/contrib/integration.rb +37 -0
  15. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +42 -0
  16. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +41 -0
  17. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +30 -0
  18. data/lib/datadog/ai_guard.rb +2 -0
  19. data/lib/datadog/appsec/assets/blocked.html +2 -1
  20. data/lib/datadog/appsec/configuration/settings.rb +14 -0
  21. data/lib/datadog/appsec/context.rb +44 -9
  22. data/lib/datadog/appsec/contrib/active_record/integration.rb +1 -1
  23. data/lib/datadog/appsec/contrib/active_record/patcher.rb +1 -1
  24. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +54 -5
  25. data/lib/datadog/appsec/contrib/faraday/integration.rb +1 -1
  26. data/lib/datadog/appsec/contrib/faraday/patcher.rb +1 -1
  27. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +60 -7
  28. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +11 -6
  29. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -1
  30. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +6 -10
  31. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +1 -3
  32. data/lib/datadog/appsec/contrib/rails/patcher.rb +10 -2
  33. data/lib/datadog/appsec/contrib/rails/patches/process_action_patch.rb +2 -0
  34. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +72 -7
  35. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +7 -4
  36. data/lib/datadog/appsec/contrib/sinatra/integration.rb +1 -1
  37. data/lib/datadog/appsec/contrib/sinatra/patcher.rb +4 -4
  38. data/lib/datadog/appsec/contrib/sinatra/patches/json_patch.rb +1 -1
  39. data/lib/datadog/appsec/counter_sampler.rb +25 -0
  40. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +18 -0
  41. data/lib/datadog/appsec/security_engine/engine.rb +23 -2
  42. data/lib/datadog/appsec/utils/http/body.rb +38 -0
  43. data/lib/datadog/appsec/utils/http/media_range.rb +2 -1
  44. data/lib/datadog/appsec/utils/http/media_type.rb +33 -26
  45. data/lib/datadog/appsec/utils/http/url_encoded.rb +52 -0
  46. data/lib/datadog/core/configuration/components.rb +29 -4
  47. data/lib/datadog/core/configuration/supported_configurations.rb +4 -0
  48. data/lib/datadog/core/configuration.rb +2 -2
  49. data/lib/datadog/core/crashtracking/component.rb +79 -19
  50. data/lib/datadog/core/crashtracking/tag_builder.rb +6 -0
  51. data/lib/datadog/core/environment/agent_info.rb +65 -1
  52. data/lib/datadog/core/knuth_sampler.rb +57 -0
  53. data/lib/datadog/core/logger.rb +1 -1
  54. data/lib/datadog/core/metrics/logging.rb +1 -1
  55. data/lib/datadog/core/process_discovery.rb +15 -19
  56. data/lib/datadog/core/rate_limiter.rb +2 -0
  57. data/lib/datadog/core/remote/component.rb +16 -5
  58. data/lib/datadog/core/remote/transport/config.rb +5 -11
  59. data/lib/datadog/core/telemetry/component.rb +0 -13
  60. data/lib/datadog/core/telemetry/transport/telemetry.rb +5 -6
  61. data/lib/datadog/core/transport/ext.rb +1 -0
  62. data/lib/datadog/core/transport/http/response.rb +4 -0
  63. data/lib/datadog/core/transport/parcel.rb +61 -9
  64. data/lib/datadog/core/utils/fnv.rb +26 -0
  65. data/lib/datadog/core.rb +6 -1
  66. data/lib/datadog/data_streams/processor.rb +34 -33
  67. data/lib/datadog/data_streams/transport/http/stats.rb +6 -0
  68. data/lib/datadog/data_streams/transport/http.rb +0 -4
  69. data/lib/datadog/data_streams/transport/stats.rb +5 -12
  70. data/lib/datadog/di/component.rb +1 -1
  71. data/lib/datadog/di/configuration/settings.rb +31 -0
  72. data/lib/datadog/di/context.rb +6 -0
  73. data/lib/datadog/di/instrumenter.rb +178 -133
  74. data/lib/datadog/di/probe.rb +10 -1
  75. data/lib/datadog/di/probe_file_loader.rb +2 -2
  76. data/lib/datadog/di/probe_manager.rb +7 -2
  77. data/lib/datadog/di/probe_notification_builder.rb +29 -8
  78. data/lib/datadog/di/probe_notifier_worker.rb +13 -3
  79. data/lib/datadog/di/proc_responder.rb +4 -0
  80. data/lib/datadog/di/redactor.rb +8 -1
  81. data/lib/datadog/di/remote.rb +2 -2
  82. data/lib/datadog/di/transport/diagnostics.rb +5 -7
  83. data/lib/datadog/di/transport/http/diagnostics.rb +3 -1
  84. data/lib/datadog/di/transport/http/input.rb +1 -1
  85. data/lib/datadog/di/transport/input.rb +5 -6
  86. data/lib/datadog/kit/tracing/method_tracer.rb +132 -0
  87. data/lib/datadog/open_feature/transport.rb +8 -11
  88. data/lib/datadog/profiling/component.rb +0 -6
  89. data/lib/datadog/tracing/contrib/http/integration.rb +0 -2
  90. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +6 -0
  91. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +2 -1
  92. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +6 -0
  93. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +2 -1
  94. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +10 -0
  95. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +5 -1
  96. data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +24 -0
  97. data/lib/datadog/tracing/contrib/rack/route_inference.rb +18 -6
  98. data/lib/datadog/tracing/contrib/registerable.rb +11 -0
  99. data/lib/datadog/tracing/contrib/sneakers/integration.rb +15 -4
  100. data/lib/datadog/tracing/contrib/trilogy/configuration/settings.rb +6 -0
  101. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +3 -1
  102. data/lib/datadog/tracing/sampling/rate_sampler.rb +8 -19
  103. data/lib/datadog/tracing/transport/io/client.rb +5 -8
  104. data/lib/datadog/tracing/transport/io/traces.rb +28 -34
  105. data/lib/datadog/tracing/transport/traces.rb +4 -10
  106. data/lib/datadog/version.rb +1 -1
  107. metadata +17 -7
  108. 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 >= Gem::Version.new('1.0.0')
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
- timeout = Datadog.configuration.appsec.waf_timeout
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' => env.url.to_s,
26
+ 'server.io.net.url' => url,
20
27
  'server.io.net.request.method' => env.method.to_s.upcase,
21
- 'server.io.net.request.headers' => env.request_headers.transform_keys(&:downcase)
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
- timeout = Datadog.configuration.appsec.waf_timeout
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' => response_headers.transform_keys(&:downcase)
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
- next unless query.selected_operation
50
+ selected_operation = query.selected_operation
51
+ next unless selected_operation
51
52
 
52
- arguments_from_selections(query.selected_operation.selections, query.variables, args_hash)
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
- case argument.value.class.name
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[argument.value.name]
101
+ query_variables[value.name]
98
102
  when Integration::AST_NODE_CLASS_NAMES[:input_object]
99
- arguments_hash(argument.value.arguments, query_variables)
103
+ # @type var value: GraphQL::Language::Nodes::InputObject
104
+ arguments_hash(value.arguments, query_variables)
100
105
  else
101
- argument.value
106
+ value
102
107
  end
103
108
  end
104
109
  end
@@ -31,7 +31,7 @@ module Datadog
31
31
  end
32
32
 
33
33
  def self.compatible?
34
- super && version >= MINIMUM_VERSION && ast_node_classes_defined?
34
+ !!(super && version&.>=(MINIMUM_VERSION) && ast_node_classes_defined?)
35
35
  end
36
36
 
37
37
  def self.auto_instrument?
@@ -23,16 +23,12 @@ module Datadog
23
23
  end
24
24
 
25
25
  def query
26
- # Downstream libddwaf expects keys and values to be extractable
27
- # separately so we can't use [[k, v], ...]. We also want to allow
28
- # duplicate keys, so we use {k => [v, ...], ...} instead, taking into
29
- # account that {k => [v1, v2, ...], ...} is possible for duplicate keys.
30
- request.query_string.split('&').each.with_object({}) do |e, hash|
31
- k, v = e.split('=').map { |s| CGI.unescape(s) }
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.events.push(
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 |app|
144
- Datadog::AppSec::Contrib::Rails::Patcher.report_routes_via_telemetry(app.routes.routes)
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
- 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
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' => 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:)
@@ -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, (gateway_request, gateway_route_params)|
53
- context = gateway_request.env[AppSec::Ext::CONTEXT_KEY]
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
@@ -26,7 +26,7 @@ module Datadog
26
26
  end
27
27
 
28
28
  def self.compatible?
29
- super && version >= MINIMUM_VERSION
29
+ !!(super && (version&.>= MINIMUM_VERSION))
30
30
  end
31
31
 
32
32
  def self.auto_instrument?
@@ -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 |*args|
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(*args)
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 = "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) ||