Skip to content

Commit 97354cd

Browse files
authored
Merge pull request #4526 from DataDog/appsec-stack-trace-reporting
Add Stack Trace reporting for AppSec actions
2 parents a04dfda + bf5323c commit 97354cd

File tree

9 files changed

+676
-1
lines changed

9 files changed

+676
-1
lines changed

lib/datadog/appsec/actions_handler.rb

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require_relative 'actions_handler/serializable_backtrace'
4+
35
module Datadog
46
module AppSec
57
# this module encapsulates functions for handling actions that libddawf returns
@@ -19,7 +21,26 @@ def interrupt_execution(action_params)
1921
throw(Datadog::AppSec::Ext::INTERRUPT, action_params)
2022
end
2123

22-
def generate_stack(_action_params); end
24+
def generate_stack(action_params)
25+
return unless Datadog.configuration.appsec.stack_trace.enabled
26+
27+
stack_id = action_params['stack_id']
28+
return unless stack_id
29+
30+
active_span = AppSec.active_context&.span
31+
return unless active_span
32+
33+
event_category = Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY
34+
tag_key = Ext::TAG_METASTRUCT_STACK_TRACE
35+
36+
existing_stack_data = active_span.get_metastruct_tag(tag_key).dup || { event_category => [] }
37+
max_stack_traces = Datadog.configuration.appsec.stack_trace.max_stack_traces
38+
return if max_stack_traces != 0 && existing_stack_data[event_category].count >= max_stack_traces
39+
40+
backtrace = SerializableBacktrace.new(locations: Array(caller_locations), stack_id: stack_id)
41+
existing_stack_data[event_category] << backtrace
42+
active_span.set_metastruct_tag(tag_key, existing_stack_data)
43+
end
2344

2445
def generate_schema(_action_params); end
2546
end
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# frozen_string_literal: true
2+
3+
module Datadog
4+
module AppSec
5+
module ActionsHandler
6+
# This module serves encapsulates MessagePack serialization for caller locations.
7+
#
8+
# It serializes part of the stack:
9+
# up to 32 frames (configurable)
10+
# keeping frames from top and bottom of the stack (75% to 25%, configurable).
11+
#
12+
# It represents the stack trace that is added to span metastruct field.
13+
class SerializableBacktrace
14+
CLASS_AND_FUNCTION_NAME_REGEX = /\b([\w+:{2}]*\w+)?[#|.]?\b(\w+)\z/.freeze
15+
16+
def initialize(locations:, stack_id:)
17+
@stack_id = stack_id
18+
@locations = locations
19+
end
20+
21+
def to_msgpack(packer = nil)
22+
# JRuby doesn't pass the packer
23+
packer ||= MessagePack::Packer.new
24+
25+
packer.write_map_header(3)
26+
27+
packer.write('id')
28+
packer.write(@stack_id.encode('UTF-8'))
29+
30+
packer.write('language')
31+
packer.write('ruby'.encode('UTF-8'))
32+
33+
serializable_locations_map = build_serializable_locations_map
34+
35+
packer.write('frames')
36+
packer.write_array_header(serializable_locations_map.size)
37+
38+
serializable_locations_map.each do |frame_id, location|
39+
packer.write_map_header(6)
40+
41+
packer.write('id')
42+
packer.write(frame_id)
43+
44+
packer.write('text')
45+
packer.write(location.to_s.encode('UTF-8'))
46+
47+
packer.write('file')
48+
packer.write(location.path&.encode('UTF-8'))
49+
50+
packer.write('line')
51+
packer.write(location.lineno)
52+
53+
class_name, function_name = location.label&.match(CLASS_AND_FUNCTION_NAME_REGEX)&.captures
54+
55+
packer.write('class_name')
56+
packer.write(class_name&.encode('UTF-8'))
57+
58+
packer.write('function')
59+
packer.write(function_name&.encode('UTF-8'))
60+
end
61+
62+
packer
63+
end
64+
65+
private
66+
67+
def build_serializable_locations_map
68+
max_depth = Datadog.configuration.appsec.stack_trace.max_depth
69+
top_percent = Datadog.configuration.appsec.stack_trace.top_percentage
70+
71+
drop_from_idx = max_depth * top_percent / 100
72+
drop_until_idx = @locations.size - (max_depth - drop_from_idx)
73+
74+
frame_idx = -1
75+
@locations.each_with_object({}) do |location, map|
76+
# we are dropping frames from library code without increasing frame index
77+
next if location.path&.include?('lib/datadog')
78+
79+
frame_idx += 1
80+
81+
next if max_depth != 0 && frame_idx >= drop_from_idx && frame_idx < drop_until_idx
82+
83+
map[frame_idx] = location
84+
end
85+
end
86+
end
87+
end
88+
end
89+
end

lib/datadog/appsec/configuration/settings.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,66 @@ def self.add_settings!(base)
164164
end
165165
end
166166

167+
settings :stack_trace do
168+
option :enabled do |o|
169+
o.type :bool
170+
o.env 'DD_APPSEC_STACK_TRACE_ENABLED'
171+
o.default true
172+
end
173+
174+
# The maximum number of stack trace frames to collect for each stack trace.
175+
#
176+
# If the stack trace exceeds this limit, the frames are dropped from the middle of the stack trace:
177+
# 75% of the frames are kept from the top of the stack trace and 25% from the bottom
178+
# (this percentage is also configurable).
179+
#
180+
# Minimum value is 10.
181+
# Set to zero if you don't want any frames to be dropped.
182+
#
183+
# Default value is 32
184+
option :max_depth do |o|
185+
o.type :int
186+
o.env 'DD_APPSEC_MAX_STACK_TRACE_DEPTH'
187+
o.default 32
188+
189+
o.setter do |value|
190+
value = 0 if value < 0
191+
value
192+
end
193+
end
194+
195+
# The percentage of frames to keep from the top of the stack trace.
196+
#
197+
# Default value is 75
198+
option :top_percentage do |o|
199+
o.type :int
200+
o.env 'DD_APPSEC_MAX_STACK_TRACE_DEPTH_TOP_PERCENT'
201+
o.default 75
202+
203+
o.setter do |value|
204+
value = 100 if value > 100
205+
value = 0 if value.negative?
206+
value
207+
end
208+
end
209+
210+
# Maximum number of stack traces to collect per span.
211+
#
212+
# Set to zero if you want to collect all stack traces.
213+
#
214+
# Default value is 2
215+
option :max_stack_traces do |o|
216+
o.type :int
217+
o.env 'DD_APPSEC_MAX_STACK_TRACES'
218+
o.default 2
219+
220+
o.setter do |value|
221+
value = 0 if value < 0
222+
value
223+
end
224+
end
225+
end
226+
167227
settings :auto_user_instrumentation do
168228
define_method(:enabled?) { get_option(:mode) != DISABLED_AUTO_USER_INSTRUMENTATION_MODE }
169229

lib/datadog/appsec/ext.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ module Ext
1010
INTERRUPT = :datadog_appsec_interrupt
1111
CONTEXT_KEY = 'datadog.appsec.context'
1212
ACTIVE_CONTEXT_KEY = :datadog_appsec_active_context
13+
EXPLOIT_PREVENTION_EVENT_CATEGORY = 'exploit'
1314

1415
TAG_APPSEC_ENABLED = '_dd.appsec.enabled'
1516
TAG_APM_ENABLED = '_dd.apm.enabled'
1617
TAG_DISTRIBUTED_APPSEC_EVENT = '_dd.p.appsec'
18+
TAG_METASTRUCT_STACK_TRACE = '_dd.stack'
1719

1820
TELEMETRY_METRICS_NAMESPACE = 'appsec'
1921
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Datadog
2+
module AppSec
3+
module ActionsHandler
4+
class SerializableBacktrace
5+
CLASS_AND_FUNCTION_NAME_REGEX: ::Regexp
6+
7+
def initialize: (locations: ::Array[::Thread::Backtrace::Location], stack_id: String) -> void
8+
def to_msgpack: (?untyped? packer) -> void
9+
10+
private def build_serializable_locations_map: -> ::Hash[Integer, ::Thread::Backtrace::Location]
11+
end
12+
end
13+
end
14+
end

sig/datadog/appsec/ext.rbs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ module Datadog
1515

1616
ACTIVE_CONTEXT_KEY: ::Symbol
1717

18+
EXPLOIT_PREVENTION_EVENT_CATEGORY: ::String
19+
1820
TAG_APPSEC_ENABLED: ::String
1921

2022
TAG_APM_ENABLED: ::String
2123

2224
TAG_DISTRIBUTED_APPSEC_EVENT: ::String
2325

26+
TAG_METASTRUCT_STACK_TRACE: ::String
27+
2428
TELEMETRY_METRICS_NAMESPACE: ::String
2529
end
2630
end

0 commit comments

Comments
 (0)