Skip to content

Commit c49806e

Browse files
committed
Add stack trace collection to meta_struct and actions_handler
1 parent a6579d2 commit c49806e

File tree

20 files changed

+963
-10
lines changed

20 files changed

+963
-10
lines changed

Diff for: lib/datadog/appsec/actions_handler.rb

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require_relative 'actions_handler/stack_trace'
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+
if Datadog.configuration.appsec.stack_trace.enabled
26+
context = AppSec::Context.active
27+
return if context.nil? ||
28+
ActionsHandler::StackTrace.skip_stack_trace?(context, group: AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY)
29+
30+
collected_stack_frames = ActionsHandler::StackTrace.collect_stack_frames
31+
utf8_stack_id = action_params['stack_id'].encode('UTF-8') if action_params['stack_id']
32+
stack_trace = ActionsHandler::StackTrace::Representor.new(
33+
id: utf8_stack_id,
34+
frames: collected_stack_frames
35+
)
36+
37+
ActionsHandler::StackTrace.add_stack_trace_to_context(
38+
stack_trace,
39+
context,
40+
group: AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY
41+
)
42+
end
43+
end
2344

2445
def generate_schema(_action_params); end
2546
end

Diff for: lib/datadog/appsec/actions_handler/stack_trace.rb

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'stack_trace/representor'
4+
require_relative 'stack_trace/collector'
5+
6+
require_relative '../../tracing/metadata/metastruct'
7+
8+
module Datadog
9+
module AppSec
10+
module ActionsHandler
11+
# Adds stack traces to meta_struct
12+
module StackTrace
13+
module_function
14+
15+
def skip_stack_trace?(context, group:)
16+
if context.trace.nil? && context.span.nil?
17+
Datadog.logger.debug { 'Cannot find trace or service entry span to add stack trace' }
18+
return true
19+
end
20+
21+
max_collect = Datadog.configuration.appsec.stack_trace.max_collect
22+
return false if max_collect == 0
23+
24+
stack_traces_count = 0
25+
26+
unless context.trace.nil?
27+
trace_dd_stack = context.trace.metastruct[AppSec::Ext::TAG_STACK_TRACE]
28+
stack_traces_count += trace_dd_stack[group].size unless trace_dd_stack.nil? || trace_dd_stack[group].nil?
29+
end
30+
31+
unless context.span.nil?
32+
span_dd_stack = context.span.metastruct[AppSec::Ext::TAG_STACK_TRACE]
33+
stack_traces_count += span_dd_stack[group].size unless span_dd_stack.nil? || span_dd_stack[group].nil?
34+
end
35+
36+
stack_traces_count >= max_collect
37+
end
38+
39+
def collect_stack_frames
40+
# caller_locations without params always returns an array but steep still thinks it can be nil
41+
# So we add || [] but it will never run the second part anyway (either this or steep:ignore)
42+
stack_frames = caller_locations || []
43+
# Steep thinks that path can still be nil and that include? is not a method of nil
44+
# We must add a variable assignment to avoid this
45+
stack_frames.reject! do |loc|
46+
path = loc.path
47+
next true if path.nil?
48+
49+
path.include?('lib/datadog')
50+
end
51+
52+
StackTrace::Collector.collect(stack_frames)
53+
end
54+
55+
def add_stack_trace_to_context(stack_trace, context, group:)
56+
# We use methods defined in Tracing::Metadata::Tagging,
57+
# which means we can use both the trace and the service entry span
58+
service_entry_op = (context.trace || context.span)
59+
60+
dd_stack = service_entry_op.metastruct[AppSec::Ext::TAG_STACK_TRACE]
61+
if dd_stack.nil?
62+
service_entry_op.metastruct[AppSec::Ext::TAG_STACK_TRACE] = {}
63+
dd_stack = service_entry_op.metastruct[AppSec::Ext::TAG_STACK_TRACE]
64+
end
65+
66+
dd_stack[AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY] ||= []
67+
stack_group = dd_stack[AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY]
68+
69+
stack_group << stack_trace
70+
rescue StandardError => e
71+
Datadog.logger.debug("Unable to add stack_trace #{stack_trace.id} in metastruct, ignoring it. Caused by: #{e}")
72+
end
73+
end
74+
end
75+
end
76+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'frame'
4+
5+
module Datadog
6+
module AppSec
7+
module ActionsHandler
8+
module StackTrace
9+
# Represent a stack trace with its id and message in message pack
10+
module Collector
11+
class << self
12+
def collect(locations)
13+
return [] if locations.nil? || locations.empty?
14+
15+
skip_frames = skip_frames(locations.size)
16+
frames = []
17+
18+
locations.each_with_index do |location, index|
19+
next if skip_frames.include?(index)
20+
21+
frames << StackTrace::Frame.new(
22+
id: index,
23+
text: location.to_s.encode('UTF-8'),
24+
file: file_path(location),
25+
line: location.lineno,
26+
function: function_label(location)
27+
)
28+
end
29+
frames
30+
end
31+
32+
private
33+
34+
def skip_frames(locations_size)
35+
max_depth = Datadog.configuration.appsec.stack_trace.max_depth
36+
return [] if max_depth == 0 || locations_size <= max_depth
37+
38+
top_frames_limit = (max_depth * Datadog.configuration.appsec.stack_trace.max_depth_top_percent / 100.0).round
39+
bottom_frames_limit = locations_size - (max_depth - top_frames_limit)
40+
(top_frames_limit...bottom_frames_limit)
41+
end
42+
43+
def file_path(location)
44+
path = location.absolute_path || location.path
45+
return if path.nil?
46+
47+
path.encode('UTF-8')
48+
end
49+
50+
def function_label(location)
51+
label = location.label
52+
return if label.nil?
53+
54+
label.encode('UTF-8')
55+
end
56+
end
57+
end
58+
end
59+
end
60+
end
61+
end
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
module Datadog
4+
module AppSec
5+
module ActionsHandler
6+
module StackTrace
7+
# Formatted stack frame.
8+
# This class extends a Struct as it's required by Steep to be able to add a method to it.
9+
class Frame < Struct.new(:id, :text, :file, :line, :function, keyword_init: true) # rubocop:disable Style/StructInheritance
10+
def to_msgpack(packer = nil)
11+
packer ||= MessagePack::Packer.new
12+
13+
packer.write_map_header(5)
14+
packer.write('id')
15+
packer.write(id)
16+
packer.write('text')
17+
packer.write(text)
18+
packer.write('file')
19+
packer.write(file)
20+
packer.write('line')
21+
packer.write(line)
22+
packer.write('function')
23+
packer.write(function)
24+
packer
25+
end
26+
end
27+
end
28+
end
29+
end
30+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
module Datadog
4+
module AppSec
5+
module ActionsHandler
6+
module StackTrace
7+
# Represent a stack trace with its id and message in message pack
8+
class Representor < Struct.new(:id, :message, :frames, keyword_init: true) # rubocop:disable Style/StructInheritance
9+
def to_msgpack(packer = nil)
10+
packer ||= MessagePack::Packer.new
11+
12+
packer.write_map_header(4)
13+
packer.write('language')
14+
packer.write('ruby')
15+
packer.write('id')
16+
packer.write(id)
17+
packer.write('message')
18+
packer.write(message)
19+
packer.write('frames')
20+
packer.write(frames)
21+
packer
22+
end
23+
end
24+
end
25+
end
26+
end
27+
end

Diff for: lib/datadog/appsec/configuration/settings.rb

+49
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,55 @@ def self.add_settings!(base)
214214
o.default false
215215
end
216216
end
217+
218+
settings :stack_trace do
219+
option :enabled do |o|
220+
o.type :bool
221+
o.env 'DD_APPSEC_STACK_TRACE_ENABLED'
222+
o.default true
223+
end
224+
225+
# The maximum number of stack frames to collect for each stack trace.
226+
# If the number of frames in a stack trace exceeds this value,
227+
# max_depth / 4 frames will be collected from the top, and max_depth * 3 / 4 from the bottom.
228+
option :max_depth do |o|
229+
o.type :int
230+
o.env 'DD_APPSEC_MAX_STACK_TRACE_DEPTH'
231+
o.default 32
232+
# 0 means no limit
233+
o.setter do |value|
234+
value = 0 if value.negative?
235+
value
236+
end
237+
end
238+
239+
# The percentage that decides the number of top stack frame to collect
240+
# for each stack trace if there is more stack frames than max_depth.
241+
# number_of_top_frames = max_depth * max_depth_top_percent / 100
242+
# Default is 75
243+
option :max_depth_top_percent do |o|
244+
o.type :float
245+
o.env 'DD_APPSEC_MAX_STACK_TRACE_DEPTH_TOP_PERCENT'
246+
o.default 75
247+
o.setter do |value|
248+
value = 100 if value > 100
249+
value = 0 if value < 0
250+
value
251+
end
252+
end
253+
254+
# The maximum number of stack traces to collect for each exploit prevention event.
255+
option :max_collect do |o|
256+
o.type :int
257+
o.env 'DD_APPSEC_MAX_STACK_TRACES'
258+
o.default 2
259+
# 0 means no limit
260+
o.setter do |value|
261+
value = 0 if value < 0
262+
value
263+
end
264+
end
265+
end
217266
end
218267
end
219268
end

Diff for: lib/datadog/appsec/context.rb

+6-3
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,13 @@ def extract_schema
6060
end
6161

6262
def export_metrics
63-
return if @span.nil?
63+
# Required to satisfy steep, as @span is defined as nilable.
64+
# According to soutaro, this is because instance variable can be changed by other threads.
65+
span = @span
66+
return if span.nil?
6467

65-
Metrics::Exporter.export_waf_metrics(@metrics.waf, @span)
66-
Metrics::Exporter.export_rasp_metrics(@metrics.rasp, @span)
68+
Metrics::Exporter.export_waf_metrics(@metrics.waf, span)
69+
Metrics::Exporter.export_rasp_metrics(@metrics.rasp, span)
6770
end
6871

6972
def finalize

Diff for: lib/datadog/appsec/ext.rb

+2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ 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'
1718

1819
TELEMETRY_METRICS_NAMESPACE = 'appsec'
20+
TAG_STACK_TRACE = '_dd.stack'
1921
end
2022
end
2123
end

Diff for: sig/datadog/appsec/actions_handler/stack_trace.rbs

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Datadog
2+
module AppSec
3+
module ActionsHandler
4+
module StackTrace
5+
def self.skip_stack_trace?: (Datadog::AppSec::Context context, group: String) -> bool
6+
7+
def self.collect_stack_frames: () -> Array[StackTrace::Frame]?
8+
9+
def self.add_stack_trace_to_context: (Datadog::AppSec::ActionsHandler::StackTrace::Representor stack_trace, Datadog::AppSec::Context context, group: String) -> void
10+
end
11+
end
12+
end
13+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module Datadog
2+
module AppSec
3+
module ActionsHandler
4+
module StackTrace
5+
module Collector
6+
def self.collect: (Array[Thread::Backtrace::Location] locations) -> Array[Datadog::AppSec::ActionsHandler::StackTrace::Frame]
7+
8+
private
9+
10+
def self.skip_frames: (Integer locations_size) -> (Range[Integer] | Array[untyped])
11+
12+
def self.file_path: (Thread::Backtrace::Location location) -> String?
13+
14+
def self.function_label: (Thread::Backtrace::Location location) -> String?
15+
end
16+
end
17+
end
18+
end
19+
end
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module Datadog
2+
module AppSec
3+
module ActionsHandler
4+
module StackTrace
5+
class Frame
6+
attr_reader id: Integer
7+
attr_reader text: String?
8+
attr_reader file: String?
9+
attr_reader line: Integer?
10+
attr_reader function: String?
11+
12+
def initialize: (?id: Integer, ?text: String?, ?file: String?, ?line: Integer?, ?function: String?) -> void
13+
14+
def to_msgpack: ((::MessagePack::Packer | nil) packer) -> ::MessagePack::Packer
15+
end
16+
end
17+
end
18+
end
19+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module Datadog
2+
module AppSec
3+
module ActionsHandler
4+
module StackTrace
5+
class Representor
6+
attr_reader id: String?
7+
attr_reader message: String?
8+
attr_reader frames: Array[StackTrace::Frame]?
9+
10+
def initialize: (?id: String?, ?message: String?, ?frames: Array[StackTrace::Frame]?) -> void
11+
12+
def to_msgpack: ((::MessagePack::Packer | nil) packer) -> ::MessagePack::Packer
13+
end
14+
end
15+
end
16+
end
17+
end

0 commit comments

Comments
 (0)