datadog 2.14.0 → 2.16.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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -1
  3. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +7 -6
  4. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +1 -4
  5. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +10 -0
  6. data/ext/datadog_profiling_native_extension/encoded_profile.c +69 -0
  7. data/ext/datadog_profiling_native_extension/encoded_profile.h +7 -0
  8. data/ext/datadog_profiling_native_extension/extconf.rb +3 -0
  9. data/ext/datadog_profiling_native_extension/heap_recorder.c +8 -1
  10. data/ext/datadog_profiling_native_extension/http_transport.c +25 -32
  11. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  12. data/ext/datadog_profiling_native_extension/stack_recorder.c +22 -21
  13. data/ext/libdatadog_api/crashtracker.c +1 -9
  14. data/ext/libdatadog_api/crashtracker.h +5 -0
  15. data/ext/libdatadog_api/datadog_ruby_common.c +1 -4
  16. data/ext/libdatadog_api/datadog_ruby_common.h +10 -0
  17. data/ext/libdatadog_api/init.c +15 -0
  18. data/ext/libdatadog_api/library_config.c +122 -0
  19. data/ext/libdatadog_api/library_config.h +19 -0
  20. data/ext/libdatadog_api/process_discovery.c +117 -0
  21. data/ext/libdatadog_api/process_discovery.h +5 -0
  22. data/lib/datadog/appsec/actions_handler.rb +3 -2
  23. data/lib/datadog/appsec/assets/waf_rules/README.md +50 -5
  24. data/lib/datadog/appsec/assets/waf_rules/processors.json +239 -10
  25. data/lib/datadog/appsec/assets/waf_rules/scanners.json +926 -17
  26. data/lib/datadog/appsec/autoload.rb +1 -1
  27. data/lib/datadog/appsec/component.rb +29 -20
  28. data/lib/datadog/appsec/compressed_json.rb +40 -0
  29. data/lib/datadog/appsec/configuration/settings.rb +31 -18
  30. data/lib/datadog/appsec/context.rb +1 -1
  31. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +10 -12
  32. data/lib/datadog/appsec/contrib/active_record/integration.rb +2 -2
  33. data/lib/datadog/appsec/contrib/active_record/patcher.rb +22 -22
  34. data/lib/datadog/appsec/contrib/devise/data_extractor.rb +2 -3
  35. data/lib/datadog/appsec/contrib/devise/ext.rb +1 -0
  36. data/lib/datadog/appsec/contrib/devise/integration.rb +1 -1
  37. data/lib/datadog/appsec/contrib/devise/patcher.rb +3 -5
  38. data/lib/datadog/appsec/contrib/devise/tracking_middleware.rb +17 -4
  39. data/lib/datadog/appsec/contrib/excon/integration.rb +1 -1
  40. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +9 -10
  41. data/lib/datadog/appsec/contrib/faraday/integration.rb +1 -1
  42. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +8 -9
  43. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +8 -9
  44. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -1
  45. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +22 -32
  46. data/lib/datadog/appsec/contrib/rack/integration.rb +1 -1
  47. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +16 -16
  48. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +11 -13
  49. data/lib/datadog/appsec/contrib/rails/integration.rb +1 -1
  50. data/lib/datadog/appsec/contrib/rails/patcher.rb +21 -21
  51. data/lib/datadog/appsec/contrib/rest_client/integration.rb +1 -1
  52. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +10 -11
  53. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +17 -23
  54. data/lib/datadog/appsec/contrib/sinatra/integration.rb +1 -1
  55. data/lib/datadog/appsec/event.rb +95 -134
  56. data/lib/datadog/appsec/instrumentation/gateway/argument.rb +5 -2
  57. data/lib/datadog/appsec/metrics/telemetry.rb +1 -1
  58. data/lib/datadog/appsec/monitor/gateway/watcher.rb +42 -12
  59. data/lib/datadog/appsec/processor/rule_loader.rb +26 -28
  60. data/lib/datadog/appsec/processor/rule_merger.rb +5 -5
  61. data/lib/datadog/appsec/processor.rb +1 -1
  62. data/lib/datadog/appsec/remote.rb +16 -11
  63. data/lib/datadog/appsec/response.rb +6 -6
  64. data/lib/datadog/appsec/security_engine/runner.rb +1 -1
  65. data/lib/datadog/appsec/security_event.rb +39 -0
  66. data/lib/datadog/appsec.rb +1 -1
  67. data/lib/datadog/core/configuration/agentless_settings_resolver.rb +176 -0
  68. data/lib/datadog/core/configuration/components.rb +19 -10
  69. data/lib/datadog/core/configuration/option.rb +61 -25
  70. data/lib/datadog/core/configuration/settings.rb +10 -0
  71. data/lib/datadog/core/configuration/stable_config.rb +23 -0
  72. data/lib/datadog/core/configuration.rb +24 -0
  73. data/lib/datadog/core/crashtracking/component.rb +1 -9
  74. data/lib/datadog/core/diagnostics/environment_logger.rb +1 -1
  75. data/lib/datadog/core/environment/git.rb +1 -0
  76. data/lib/datadog/core/environment/variable_helpers.rb +1 -1
  77. data/lib/datadog/core/metrics/client.rb +8 -7
  78. data/lib/datadog/core/process_discovery.rb +32 -0
  79. data/lib/datadog/core/remote/client.rb +7 -0
  80. data/lib/datadog/core/runtime/metrics.rb +1 -1
  81. data/lib/datadog/core/telemetry/component.rb +60 -50
  82. data/lib/datadog/core/telemetry/emitter.rb +17 -11
  83. data/lib/datadog/core/telemetry/event.rb +7 -4
  84. data/lib/datadog/core/telemetry/http/adapters/net.rb +12 -97
  85. data/lib/datadog/core/telemetry/metric.rb +5 -5
  86. data/lib/datadog/core/telemetry/request.rb +4 -4
  87. data/lib/datadog/core/telemetry/transport/http/api.rb +43 -0
  88. data/lib/datadog/core/telemetry/transport/http/client.rb +49 -0
  89. data/lib/datadog/core/telemetry/transport/http/telemetry.rb +92 -0
  90. data/lib/datadog/core/telemetry/transport/http.rb +63 -0
  91. data/lib/datadog/core/telemetry/transport/telemetry.rb +52 -0
  92. data/lib/datadog/core/telemetry/worker.rb +45 -0
  93. data/lib/datadog/core/utils/time.rb +12 -0
  94. data/lib/datadog/core/workers/async.rb +20 -2
  95. data/lib/datadog/core/workers/interval_loop.rb +12 -1
  96. data/lib/datadog/core/workers/runtime_metrics.rb +2 -2
  97. data/lib/datadog/core.rb +8 -0
  98. data/lib/datadog/di/boot.rb +34 -0
  99. data/lib/datadog/di/probe_notification_builder.rb +1 -1
  100. data/lib/datadog/di/remote.rb +2 -0
  101. data/lib/datadog/di/transport/http/diagnostics.rb +0 -1
  102. data/lib/datadog/di/transport/http/input.rb +0 -1
  103. data/lib/datadog/di/transport/http.rb +0 -6
  104. data/lib/datadog/di.rb +5 -32
  105. data/lib/datadog/error_tracking/collector.rb +87 -0
  106. data/lib/datadog/error_tracking/component.rb +167 -0
  107. data/lib/datadog/error_tracking/configuration/settings.rb +63 -0
  108. data/lib/datadog/error_tracking/configuration.rb +11 -0
  109. data/lib/datadog/error_tracking/ext.rb +18 -0
  110. data/lib/datadog/error_tracking/extensions.rb +16 -0
  111. data/lib/datadog/error_tracking/filters.rb +77 -0
  112. data/lib/datadog/error_tracking.rb +18 -0
  113. data/lib/datadog/kit/identity.rb +1 -1
  114. data/lib/datadog/profiling/collectors/info.rb +3 -0
  115. data/lib/datadog/profiling/encoded_profile.rb +11 -0
  116. data/lib/datadog/profiling/exporter.rb +3 -4
  117. data/lib/datadog/profiling/ext.rb +0 -1
  118. data/lib/datadog/profiling/flush.rb +4 -7
  119. data/lib/datadog/profiling/http_transport.rb +10 -59
  120. data/lib/datadog/profiling/stack_recorder.rb +4 -4
  121. data/lib/datadog/profiling.rb +1 -0
  122. data/lib/datadog/tracing/analytics.rb +1 -1
  123. data/lib/datadog/tracing/contrib/active_record/integration.rb +1 -1
  124. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +2 -0
  125. data/lib/datadog/tracing/contrib/karafka/monitor.rb +1 -1
  126. data/lib/datadog/tracing/contrib/mongodb/configuration/settings.rb +8 -0
  127. data/lib/datadog/tracing/contrib/mongodb/ext.rb +1 -0
  128. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +18 -1
  129. data/lib/datadog/tracing/contrib/opensearch/configuration/settings.rb +17 -0
  130. data/lib/datadog/tracing/contrib/opensearch/ext.rb +9 -0
  131. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +5 -1
  132. data/lib/datadog/tracing/contrib/rack/request_queue.rb +1 -1
  133. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +1 -1
  134. data/lib/datadog/tracing/distributed/b3_multi.rb +1 -1
  135. data/lib/datadog/tracing/distributed/b3_single.rb +1 -1
  136. data/lib/datadog/tracing/distributed/datadog.rb +2 -2
  137. data/lib/datadog/tracing/sampling/rate_sampler.rb +2 -1
  138. data/lib/datadog/tracing/span_event.rb +1 -1
  139. data/lib/datadog/tracing/span_operation.rb +38 -14
  140. data/lib/datadog/tracing/trace_operation.rb +15 -7
  141. data/lib/datadog/tracing/tracer.rb +7 -3
  142. data/lib/datadog/tracing/utils.rb +1 -1
  143. data/lib/datadog/version.rb +1 -1
  144. data/lib/datadog.rb +2 -3
  145. metadata +40 -10
  146. data/lib/datadog/core/telemetry/http/env.rb +0 -20
  147. data/lib/datadog/core/telemetry/http/ext.rb +0 -28
  148. data/lib/datadog/core/telemetry/http/response.rb +0 -70
  149. data/lib/datadog/core/telemetry/http/transport.rb +0 -90
@@ -1,181 +1,142 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require 'zlib'
5
-
6
4
  require_relative 'rate_limiter'
7
- require_relative '../core/utils/base64'
5
+ require_relative 'compressed_json'
8
6
 
9
7
  module Datadog
10
8
  module AppSec
11
9
  # AppSec event
12
10
  module Event
11
+ DERIVATIVE_SCHEMA_KEY_PREFIX = '_dd.appsec.s.'
12
+ DERIVATIVE_SCHEMA_MAX_COMPRESSED_SIZE = 25000
13
13
  ALLOWED_REQUEST_HEADERS = %w[
14
- X-Forwarded-For
15
- X-Client-IP
16
- X-Real-IP
17
- X-Forwarded
18
- X-Cluster-Client-IP
19
- Forwarded-For
20
- Forwarded
21
- Via
22
- True-Client-IP
23
- Content-Length
24
- Content-Type
25
- Content-Encoding
26
- Content-Language
27
- Host
28
- User-Agent
29
- Accept
30
- Accept-Encoding
31
- Accept-Language
32
- ].map!(&:downcase).freeze
14
+ x-forwarded-for
15
+ x-client-ip
16
+ x-real-ip
17
+ x-forwarded
18
+ x-cluster-client-ip
19
+ forwarded-for
20
+ forwarded
21
+ via
22
+ true-client-ip
23
+ content-length
24
+ content-type
25
+ content-encoding
26
+ content-language
27
+ host
28
+ user-agent
29
+ accept
30
+ accept-encoding
31
+ accept-language
32
+ ].freeze
33
33
 
34
34
  ALLOWED_RESPONSE_HEADERS = %w[
35
- Content-Length
36
- Content-Type
37
- Content-Encoding
38
- Content-Language
39
- ].map!(&:downcase).freeze
40
-
41
- MAX_ENCODED_SCHEMA_SIZE = 25000
42
- # For more information about this number
43
- # please check https://github.com/DataDog/dd-trace-rb/pull/3177#issuecomment-1747221082
44
- MIN_SCHEMA_SIZE_FOR_COMPRESSION = 260
45
-
46
- # Record events for a trace
47
- #
48
- # This is expected to be called only once per trace for the rate limiter
49
- # to properly apply
50
- class << self
51
- def record(span, *events)
52
- # ensure rate limiter is called only when there are events to record
53
- return if events.empty? || span.nil?
35
+ content-length
36
+ content-type
37
+ content-encoding
38
+ content-language
39
+ ].freeze
54
40
 
55
- Datadog::AppSec::RateLimiter.thread_local.limit do
56
- record_via_span(span, *events)
57
- end
58
- end
41
+ class << self
42
+ def tag_and_keep!(context, waf_result)
43
+ # We want to keep the trace in case of security event
44
+ context.trace&.keep!
59
45
 
60
- def record_via_span(span, *events)
61
- events.group_by { |e| e[:trace] }.each do |trace, event_group|
62
- unless trace
63
- Datadog.logger.debug { "{ error: 'no trace: cannot record', event_group: #{event_group.inspect}}" }
64
- next
46
+ if context.span
47
+ if waf_result.actions.key?('block_request') || waf_result.actions.key?('redirect_request')
48
+ context.span.set_tag('appsec.blocked', 'true')
65
49
  end
66
50
 
67
- trace.keep!
68
- trace.set_tag(
69
- Datadog::Tracing::Metadata::Ext::Distributed::TAG_DECISION_MAKER,
70
- Datadog::Tracing::Sampling::Ext::Decision::ASM
71
- )
72
-
73
- # prepare and gather tags to apply
74
- service_entry_tags = build_service_entry_tags(event_group)
75
-
76
- # apply tags to service entry span
77
- service_entry_tags.each do |key, value|
78
- span.set_tag(key, value)
79
- end
51
+ context.span.set_tag('appsec.event', 'true')
80
52
  end
53
+
54
+ add_distributed_tags(context.trace)
81
55
  end
82
56
 
83
- # rubocop:disable Metrics/MethodLength
84
- def build_service_entry_tags(event_group)
85
- waf_events = []
86
- entry_tags = event_group.each_with_object({ '_dd.origin' => 'appsec' }) do |event, tags|
87
- # TODO: assume HTTP request context for now
88
- if (request = event[:request])
89
- request.headers.each do |header, value|
90
- tags["http.request.headers.#{header}"] = value if ALLOWED_REQUEST_HEADERS.include?(header.downcase)
57
+ def record(context, request: nil, response: nil)
58
+ return if context.events.empty? || context.span.nil?
59
+
60
+ Datadog::AppSec::RateLimiter.thread_local.limit do
61
+ context.events.group_by(&:trace).each do |trace, event_group|
62
+ unless trace
63
+ next Datadog.logger.debug do
64
+ "AppSec: Cannot record event group with #{event_group.count} events because it has no trace"
65
+ end
91
66
  end
92
67
 
93
- tags['http.host'] = request.host
94
- tags['http.useragent'] = request.user_agent
95
- tags['network.client.ip'] = request.remote_addr
96
- end
68
+ if event_group.any? { |event| event.attack? || event.schema? }
69
+ trace.keep!
70
+ trace[Tracing::Metadata::Ext::Distributed::TAG_DECISION_MAKER] = Tracing::Sampling::Ext::Decision::ASM
97
71
 
98
- if (response = event[:response])
99
- response.headers.each do |header, value|
100
- tags["http.response.headers.#{header}"] = value if ALLOWED_RESPONSE_HEADERS.include?(header.downcase)
72
+ context.span['_dd.origin'] = 'appsec'
73
+ context.span.set_tags(request_tags(request)) if request
74
+ context.span.set_tags(response_tags(response)) if response
101
75
  end
76
+
77
+ context.span.set_tags(waf_tags(event_group))
102
78
  end
79
+ end
80
+ end
103
81
 
104
- waf_result = event[:waf_result]
105
- # accumulate triggers
106
- waf_events += waf_result.events
82
+ private
107
83
 
108
- waf_result.derivatives.each do |key, value|
109
- parsed_value = json_parse(value)
110
- next unless parsed_value
84
+ def request_tags(request)
85
+ tags = {}
111
86
 
112
- parsed_value_size = parsed_value.size
87
+ tags['http.host'] = request.host if request.host
88
+ tags['http.useragent'] = request.user_agent if request.user_agent
89
+ tags['network.client.ip'] = request.remote_addr if request.remote_addr
113
90
 
114
- schema_value = if parsed_value_size >= MIN_SCHEMA_SIZE_FOR_COMPRESSION
115
- compressed_and_base64_encoded(parsed_value)
116
- else
117
- parsed_value
118
- end
119
- next unless schema_value
91
+ request.headers.each_with_object(tags) do |(name, value), memo|
92
+ next unless ALLOWED_REQUEST_HEADERS.include?(name)
120
93
 
121
- if schema_value.size >= MAX_ENCODED_SCHEMA_SIZE
122
- Datadog.logger.debug do
123
- "Schema key: #{key} exceeds the max size value. It will not be included as part of the span tags"
124
- end
125
- next
126
- end
94
+ memo["http.request.headers.#{name}"] = value
95
+ end
96
+ end
127
97
 
128
- tags[key] = schema_value
129
- end
98
+ def response_tags(response)
99
+ response.headers.each_with_object({}) do |(name, value), memo|
100
+ next unless ALLOWED_RESPONSE_HEADERS.include?(name)
130
101
 
131
- tags
102
+ memo["http.response.headers.#{name}"] = value
132
103
  end
133
-
134
- appsec_events = json_parse({ triggers: waf_events })
135
- entry_tags['_dd.appsec.json'] = appsec_events if appsec_events
136
- entry_tags
137
104
  end
138
- # rubocop:enable Metrics/MethodLength
139
105
 
140
- def tag_and_keep!(context, waf_result)
141
- # We want to keep the trace in case of security event
142
- context.trace.keep! if context.trace
106
+ def waf_tags(security_events)
107
+ triggers = []
143
108
 
144
- if context.span
145
- context.span.set_tag('appsec.blocked', 'true') if waf_result.actions.key?('block_request')
146
- context.span.set_tag('appsec.event', 'true')
147
- end
109
+ tags = security_events.each_with_object({}) do |security_event, memo|
110
+ triggers.concat(security_event.waf_result.events)
148
111
 
149
- add_distributed_tags(context.trace)
150
- end
112
+ security_event.waf_result.derivatives.each do |key, value|
113
+ next memo[key] = value unless key.start_with?(DERIVATIVE_SCHEMA_KEY_PREFIX)
151
114
 
152
- private
115
+ value = CompressedJson.dump(value)
116
+ next if value.nil?
117
+
118
+ if value.size >= DERIVATIVE_SCHEMA_MAX_COMPRESSED_SIZE
119
+ Datadog.logger.debug { "AppSec: Schema key '#{key}' will not be included into span tags due to it's size" }
120
+ next
121
+ end
153
122
 
154
- def compressed_and_base64_encoded(value)
155
- Datadog::Core::Utils::Base64.strict_encode64(gzip(value))
156
- rescue TypeError => e
157
- Datadog.logger.debug do
158
- "Failed to compress and encode value when populating AppSec::Event. Error: #{e.message}"
123
+ memo[key] = value
124
+ end
159
125
  end
160
- nil
126
+
127
+ tags['_dd.appsec.json'] = json_parse({triggers: triggers}) unless triggers.empty?
128
+ tags
161
129
  end
162
130
 
131
+ # NOTE: Handling of Encoding::UndefinedConversionError is added as a quick fix to
132
+ # the issue between Ruby encoded strings and libddwaf produced events and now
133
+ # is under investigation.
163
134
  def json_parse(value)
164
135
  JSON.dump(value)
165
- rescue ArgumentError => e
166
- Datadog.logger.debug do
167
- "Failed to parse value to JSON when populating AppSec::Event. Error: #{e.message}"
168
- end
169
- nil
170
- end
136
+ rescue ArgumentError, Encoding::UndefinedConversionError, JSON::JSONError => e
137
+ AppSec.telemetry.report(e, description: 'AppSec: Failed to convert value into JSON')
171
138
 
172
- def gzip(value)
173
- sio = StringIO.new
174
- # For an in depth comparison of Zlib options check https://github.com/DataDog/dd-trace-rb/pull/3177#issuecomment-1747215473
175
- gz = Zlib::GzipWriter.new(sio, Zlib::BEST_SPEED, Zlib::DEFAULT_STRATEGY)
176
- gz.write(value)
177
- gz.close
178
- sio.string
139
+ nil
179
140
  end
180
141
 
181
142
  # Propagate to downstream services the information that the current distributed trace is
@@ -8,14 +8,17 @@ module Datadog
8
8
  class Argument; end # rubocop:disable Lint/EmptyClass
9
9
 
10
10
  # Gateway User argument
11
+ # NOTE: This class is a subject of elimination and will be removed when
12
+ # the event system is refactored.
11
13
  class User < Argument
12
- attr_reader :id, :login
14
+ attr_reader :id, :login, :session_id
13
15
 
14
- def initialize(id, login)
16
+ def initialize(id, login = nil, session_id = nil)
15
17
  super()
16
18
 
17
19
  @id = id
18
20
  @login = login
21
+ @session_id = session_id
19
22
  end
20
23
  end
21
24
  end
@@ -10,7 +10,7 @@ module Datadog
10
10
  def report_rasp(type, result)
11
11
  return if result.is_a?(SecurityEngine::Result::Error)
12
12
 
13
- tags = { rule_type: type, waf_version: Datadog::AppSec::WAF::VERSION::BASE_STRING }
13
+ tags = {rule_type: type, waf_version: Datadog::AppSec::WAF::VERSION::BASE_STRING}
14
14
  namespace = Ext::TELEMETRY_METRICS_NAMESPACE
15
15
 
16
16
  AppSec.telemetry.inc(namespace, 'rasp.rule.eval', 1, tags: tags)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../event'
4
+ require_relative '../../security_event'
3
5
  require_relative '../../instrumentation/gateway'
4
6
 
5
7
  module Datadog
@@ -8,18 +10,24 @@ module Datadog
8
10
  module Gateway
9
11
  # Watcher for Apssec internal events
10
12
  module Watcher
13
+ ARBITRARY_VALUE = 'invalid'
14
+ EVENT_LOGIN_SUCCESS = 'users.login.success'
15
+ EVENT_LOGIN_FAILURE = 'users.login.failure'
16
+ WATCHED_LOGIN_EVENTS = [EVENT_LOGIN_SUCCESS, EVENT_LOGIN_FAILURE].freeze
17
+
11
18
  class << self
12
19
  def watch
13
20
  gateway = Instrumentation.gateway
14
21
 
15
22
  watch_user_id(gateway)
23
+ watch_user_login(gateway)
16
24
  end
17
25
 
18
26
  def watch_user_id(gateway = Instrumentation.gateway)
19
27
  gateway.watch('identity.set_user', :appsec) do |stack, user|
20
- context = Datadog::AppSec.active_context
28
+ context = AppSec.active_context
21
29
 
22
- if user.id.nil? && user.login.nil?
30
+ if user.id.nil? && user.login.nil? && user.session_id.nil?
23
31
  Datadog.logger.debug { 'AppSec: skipping WAF check because no user information was provided' }
24
32
  next stack.call(user)
25
33
  end
@@ -27,24 +35,46 @@ module Datadog
27
35
  persistent_data = {}
28
36
  persistent_data['usr.id'] = user.id if user.id
29
37
  persistent_data['usr.login'] = user.login if user.login
38
+ persistent_data['usr.session_id'] = user.session_id if user.session_id
30
39
 
31
40
  result = context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
32
41
 
42
+ if result.match? || result.derivatives.any?
43
+ context.events.push(
44
+ AppSec::SecurityEvent.new(result, trace: context.trace, span: context.span)
45
+ )
46
+ end
47
+
33
48
  if result.match?
34
- Datadog::AppSec::Event.tag_and_keep!(context, result)
49
+ AppSec::Event.tag_and_keep!(context, result)
50
+ AppSec::ActionsHandler.handle(result.actions)
51
+ end
52
+
53
+ stack.call(user)
54
+ end
55
+ end
35
56
 
36
- context.events << {
37
- waf_result: result,
38
- trace: context.trace,
39
- span: context.span,
40
- user: user,
41
- actions: result.actions
42
- }
57
+ def watch_user_login(gateway = Instrumentation.gateway)
58
+ gateway.watch('appsec.events.user_lifecycle', :appsec) do |stack, kind|
59
+ context = AppSec.active_context
43
60
 
44
- Datadog::AppSec::ActionsHandler.handle(result.actions)
61
+ next stack.call(kind) unless WATCHED_LOGIN_EVENTS.include?(kind)
62
+
63
+ persistent_data = {"server.business_logic.#{kind}" => ARBITRARY_VALUE}
64
+ result = context.run_waf(persistent_data, {}, Datadog.configuration.appsec.waf_timeout)
65
+
66
+ if result.match? || result.derivatives.any?
67
+ context.events.push(
68
+ AppSec::SecurityEvent.new(result, trace: context.trace, span: context.span)
69
+ )
45
70
  end
46
71
 
47
- stack.call(user)
72
+ if result.match?
73
+ AppSec::Event.tag_and_keep!(context, result)
74
+ AppSec::ActionsHandler.handle(result.actions)
75
+ end
76
+
77
+ stack.call(kind)
48
78
  end
49
79
  end
50
80
  end
@@ -10,35 +10,33 @@ module Datadog
10
10
  module RuleLoader
11
11
  class << self
12
12
  def load_rules(ruleset:, telemetry:)
13
- begin
14
- case ruleset
15
- when :recommended, :strict
16
- JSON.parse(Datadog::AppSec::Assets.waf_rules(ruleset))
17
- when :risky
18
- Datadog.logger.warn(
19
- 'The :risky Application Security Management ruleset has been deprecated and no longer available.'\
20
- 'The `:recommended` ruleset will be used instead.'\
21
- 'Please remove the `appsec.ruleset = :risky` setting from your Datadog.configure block.'
22
- )
23
- JSON.parse(Datadog::AppSec::Assets.waf_rules(:recommended))
24
- when String
25
- JSON.parse(File.read(File.expand_path(ruleset)))
26
- when File, StringIO
27
- JSON.parse(ruleset.read || '').tap { ruleset.rewind }
28
- when Hash
29
- ruleset
30
- else
31
- raise ArgumentError, "unsupported value for ruleset setting: #{ruleset.inspect}"
32
- end
33
- rescue StandardError => e
34
- Datadog.logger.error do
35
- "libddwaf ruleset failed to load, ruleset: #{ruleset.inspect} error: #{e.inspect}"
36
- end
13
+ case ruleset
14
+ when :recommended, :strict
15
+ JSON.parse(Datadog::AppSec::Assets.waf_rules(ruleset))
16
+ when :risky
17
+ Datadog.logger.warn(
18
+ 'The :risky Application Security Management ruleset has been deprecated and no longer available.' \
19
+ 'The `:recommended` ruleset will be used instead.' \
20
+ 'Please remove the `appsec.ruleset = :risky` setting from your Datadog.configure block.'
21
+ )
22
+ JSON.parse(Datadog::AppSec::Assets.waf_rules(:recommended))
23
+ when String
24
+ JSON.parse(File.read(File.expand_path(ruleset)))
25
+ when File, StringIO
26
+ JSON.parse(ruleset.read || '').tap { ruleset.rewind }
27
+ when Hash
28
+ ruleset
29
+ else
30
+ raise ArgumentError, "unsupported value for ruleset setting: #{ruleset.inspect}"
31
+ end
32
+ rescue => e
33
+ Datadog.logger.error do
34
+ "libddwaf ruleset failed to load, ruleset: #{ruleset.inspect} error: #{e.inspect}"
35
+ end
37
36
 
38
- telemetry.report(e, description: 'libddwaf ruleset failed to load')
37
+ telemetry.report(e, description: 'libddwaf ruleset failed to load')
39
38
 
40
- nil
41
- end
39
+ nil
42
40
  end
43
41
 
44
42
  def load_data(ip_denylist: [], user_id_denylist: [])
@@ -62,7 +60,7 @@ module Datadog
62
60
  {
63
61
  'id' => id,
64
62
  'type' => 'data_with_expiration',
65
- 'data' => denylist.map { |v| { 'value' => v.to_s, 'expiration' => 2**63 } }
63
+ 'data' => denylist.map { |v| {'value' => v.to_s, 'expiration' => 2**63} }
66
64
  }
67
65
  end
68
66
 
@@ -11,8 +11,8 @@ module Datadog
11
11
  # RuleVersionMismatchError
12
12
  class RuleVersionMismatchError < StandardError
13
13
  def initialize(version1, version2)
14
- msg = 'Merging rule files with different version could lead to unkown behaviour. '\
15
- "We have receieve two rule files with versions: #{version1}, #{version2}. "\
14
+ msg = 'Merging rule files with different version could lead to unkown behaviour. ' \
15
+ "We have receieve two rule files with versions: #{version1}, #{version2}. " \
16
16
  'Please validate the configuration is correct and try again.'
17
17
  super(msg)
18
18
  end
@@ -27,7 +27,7 @@ module Datadog
27
27
  )
28
28
  processors ||= begin
29
29
  default_waf_processors
30
- rescue StandardError => e
30
+ rescue => e
31
31
  Datadog.logger.error("libddwaf rulemerger failed to parse default waf processors. Error: #{e.inspect}")
32
32
  telemetry.report(
33
33
  e,
@@ -38,7 +38,7 @@ module Datadog
38
38
 
39
39
  scanners ||= begin
40
40
  default_waf_scanners
41
- rescue StandardError => e
41
+ rescue => e
42
42
  Datadog.logger.error("libddwaf rulemerger failed to parse default waf scanners. Error: #{e.inspect}")
43
43
  telemetry.report(
44
44
  e,
@@ -146,7 +146,7 @@ module Datadog
146
146
  end
147
147
 
148
148
  result.each_with_object([]) do |entry, acc|
149
- value = { 'value' => entry[0] }
149
+ value = {'value' => entry[0]}
150
150
  value['expiration'] = entry[1] if entry[1]
151
151
 
152
152
  acc << value
@@ -82,7 +82,7 @@ module Datadog
82
82
  @diagnostics = e.diagnostics if e.diagnostics
83
83
 
84
84
  false
85
- rescue StandardError => e
85
+ rescue => e
86
86
  Datadog.logger.error do
87
87
  "libddwaf failed to initialize, error: #{e.inspect}"
88
88
  end
@@ -9,20 +9,23 @@ module Datadog
9
9
  # Remote
10
10
  module Remote
11
11
  class ReadError < StandardError; end
12
+
12
13
  class NoRulesError < StandardError; end
13
14
 
14
15
  class << self
15
- CAP_ASM_RESERVED_1 = 1 << 0 # RESERVED
16
- CAP_ASM_ACTIVATION = 1 << 1 # Remote activation via ASM_FEATURES product
17
- CAP_ASM_IP_BLOCKING = 1 << 2 # accept IP blocking data from ASM_DATA product
18
- CAP_ASM_DD_RULES = 1 << 3 # read ASM rules from ASM_DD product
19
- CAP_ASM_EXCLUSIONS = 1 << 4 # exclusion filters (passlist) via ASM product
20
- CAP_ASM_REQUEST_BLOCKING = 1 << 5 # can block on request info
21
- CAP_ASM_RESPONSE_BLOCKING = 1 << 6 # can block on response info
22
- CAP_ASM_USER_BLOCKING = 1 << 7 # accept user blocking data from ASM_DATA product
23
- CAP_ASM_CUSTOM_RULES = 1 << 8 # accept custom rules
24
- CAP_ASM_CUSTOM_BLOCKING_RESPONSE = 1 << 9 # supports custom http code or redirect sa blocking response
25
- CAP_ASM_TRUSTED_IPS = 1 << 10 # supports trusted ip
16
+ CAP_ASM_RESERVED_1 = 1 << 0 # RESERVED
17
+ CAP_ASM_ACTIVATION = 1 << 1 # Remote activation via ASM_FEATURES product
18
+ CAP_ASM_IP_BLOCKING = 1 << 2 # accept IP blocking data from ASM_DATA product
19
+ CAP_ASM_DD_RULES = 1 << 3 # read ASM rules from ASM_DD product
20
+ CAP_ASM_EXCLUSIONS = 1 << 4 # exclusion filters (passlist) via ASM product
21
+ CAP_ASM_REQUEST_BLOCKING = 1 << 5 # can block on request info
22
+ CAP_ASM_RESPONSE_BLOCKING = 1 << 6 # can block on response info
23
+ CAP_ASM_USER_BLOCKING = 1 << 7 # accept user blocking data from ASM_DATA product
24
+ CAP_ASM_CUSTOM_RULES = 1 << 8 # accept custom rules
25
+ CAP_ASM_CUSTOM_BLOCKING_RESPONSE = 1 << 9 # supports custom http code or redirect sa blocking response
26
+ CAP_ASM_TRUSTED_IPS = 1 << 10 # supports trusted ip
27
+ CAP_ASM_RASP_SSRF = 1 << 23 # support for server-side request forgery exploit prevention rules
28
+ CAP_ASM_RASP_SQLI = 1 << 21 # support for SQL injection exploit prevention rules
26
29
 
27
30
  # TODO: we need to dynamically add CAP_ASM_ACTIVATION once we support it
28
31
  ASM_CAPABILITIES = [
@@ -35,6 +38,8 @@ module Datadog
35
38
  CAP_ASM_CUSTOM_RULES,
36
39
  CAP_ASM_CUSTOM_BLOCKING_RESPONSE,
37
40
  CAP_ASM_TRUSTED_IPS,
41
+ CAP_ASM_RASP_SSRF,
42
+ CAP_ASM_RASP_SQLI,
38
43
  ].freeze
39
44
 
40
45
  ASM_PRODUCTS = [
@@ -30,13 +30,13 @@ module Datadog
30
30
 
31
31
  def block_response(interrupt_params, http_accept_header)
32
32
  content_type = case interrupt_params['type']
33
- when nil, 'auto' then content_type(http_accept_header)
34
- else FORMAT_TO_CONTENT_TYPE.fetch(interrupt_params['type'], DEFAULT_CONTENT_TYPE)
35
- end
33
+ when nil, 'auto' then content_type(http_accept_header)
34
+ else FORMAT_TO_CONTENT_TYPE.fetch(interrupt_params['type'], DEFAULT_CONTENT_TYPE)
35
+ end
36
36
 
37
37
  Response.new(
38
38
  status: interrupt_params['status_code']&.to_i || 403,
39
- headers: { 'Content-Type' => content_type },
39
+ headers: {'Content-Type' => content_type},
40
40
  body: [content(content_type)],
41
41
  )
42
42
  end
@@ -45,8 +45,8 @@ module Datadog
45
45
  status_code = interrupt_params['status_code'].to_i
46
46
 
47
47
  Response.new(
48
- status: (status_code >= 300 && status_code < 400 ? status_code : 303),
49
- headers: { 'Location' => interrupt_params.fetch('location') },
48
+ status: ((status_code >= 300 && status_code < 400) ? status_code : 303),
49
+ headers: {'Location' => interrupt_params.fetch('location')},
50
50
  body: [],
51
51
  )
52
52
  end
@@ -42,7 +42,7 @@ module Datadog
42
42
  return Result::Error.new(duration_ext_ns: stop_ns - start_ns)
43
43
  end
44
44
 
45
- klass = result.status == :match ? Result::Match : Result::Ok
45
+ klass = (result.status == :match) ? Result::Match : Result::Ok
46
46
  klass.new(
47
47
  events: result.events,
48
48
  actions: result.actions,
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AppSec
5
+ # A class that represents a security event of any kind. It could be an event
6
+ # representing an attack or fingerprinting results as derivatives or an API
7
+ # security check with extracted schema.
8
+ class SecurityEvent
9
+ SCHEMA_KEY_PREFIX = '_dd.appsec.s.'
10
+ FINGERPRINT_KEY_PREFIX = '_dd.appsec.fp.'
11
+
12
+ attr_reader :waf_result, :trace, :span
13
+
14
+ def initialize(waf_result, trace:, span:)
15
+ @waf_result = waf_result
16
+ @trace = trace
17
+ @span = span
18
+ end
19
+
20
+ def attack?
21
+ return @is_attack if defined?(@is_attack)
22
+
23
+ @is_attack = @waf_result.is_a?(SecurityEngine::Result::Match)
24
+ end
25
+
26
+ def schema?
27
+ return @has_schema if defined?(@has_schema)
28
+
29
+ @has_schema = @waf_result.derivatives.any? { |name, _| name.start_with?(SCHEMA_KEY_PREFIX) }
30
+ end
31
+
32
+ def fingerprint?
33
+ return @has_fingerprint if defined?(@has_fingerprint)
34
+
35
+ @has_fingerprint = @waf_result.derivatives.any? { |name, _| name.start_with?(FINGERPRINT_KEY_PREFIX) }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -44,7 +44,7 @@ module Datadog
44
44
  appsec_component.reconfigure_lock(&block)
45
45
  end
46
46
 
47
- def api_security_enabled?
47
+ def perform_api_security_check?
48
48
  Datadog.configuration.appsec.api_security.enabled &&
49
49
  Datadog.configuration.appsec.api_security.sample_rate.sample?
50
50
  end