Skip to content

Commit 7aa25ec

Browse files
committed
Add stack trace collection to meta_struct and actions_handler
1 parent b9e8d5d commit 7aa25ec

File tree

21 files changed

+727
-1
lines changed

21 files changed

+727
-1
lines changed

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

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

3+
require_relative '../tracing/stack_trace/collector'
4+
require_relative '../tracing/stack_trace/representor'
5+
36
module Datadog
47
module AppSec
58
# this module encapsulates functions for handling actions that libddawf returns
@@ -19,7 +22,41 @@ def interrupt_execution(action_params)
1922
throw(Datadog::AppSec::Ext::INTERRUPT, action_params)
2023
end
2124

22-
def generate_stack(_action_params); end
25+
def generate_stack(action_params)
26+
if Datadog.configuration.appsec.stack_trace.enabled
27+
context = AppSec::Context.active
28+
# We use methods defined in Tracing::Metadata::Tagging,
29+
# which means we can use both the trace and the service entry span
30+
service_entry_operation = (context.trace || context.span) if context
31+
32+
if service_entry_operation.nil?
33+
Datadog.logger.debug { 'Cannot find trace or service entry span to add stack trace' }
34+
return
35+
end
36+
37+
# caller_locations without params always returns an array but steep still thinks it can be nil
38+
# So we add || [] but it will never run the second part anyway (either this or steep:ignore)
39+
stack_frames = caller_locations || []
40+
# Steep thinks that path can still be nil and that include? is not a method of nil
41+
# We must add a variable assignment to avoid this
42+
stack_frames.reject! do |loc|
43+
path = loc.path
44+
next true if path.nil?
45+
46+
path.include?('lib/datadog')
47+
end
48+
49+
collected_stack_frames = Datadog::Tracing::StackTrace::Collector.collect(stack_frames)
50+
utf8_stack_id = action_params['stack_id'].encode('UTF-8') if action_params['stack_id']
51+
stack_trace = Datadog::Tracing::StackTrace::Representor.new(
52+
id: utf8_stack_id,
53+
message: nil,
54+
frames: collected_stack_frames
55+
)
56+
57+
service_entry_operation.set_stack_trace(stack_trace, group: AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY)
58+
end
59+
end
2360

2461
def generate_schema(_action_params); end
2562
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/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: lib/datadog/tracing/metadata/ext.rb

+4
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ module SpanKind
193193
TAG_INTERNAL = 'internal'
194194
end
195195

196+
module MetaStruct
197+
TAG_STACK_TRACE = '_dd.stack'
198+
end
199+
196200
# @public_api
197201
end
198202
end

Diff for: lib/datadog/tracing/metadata/meta_struct.rb

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

3+
require_relative 'stack_trace'
4+
35
module Datadog
46
module Tracing
57
module Metadata
68
# Adds complex structures tagging behavior through meta_struct
79
# @public_api
810
module MetaStruct
11+
include StackTrace
12+
913
def set_meta_struct(meta_struct)
1014
self.meta_struct.merge!(meta_struct)
1115
end

Diff for: lib/datadog/tracing/metadata/stack_trace.rb

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
module Datadog
4+
module Tracing
5+
module Metadata
6+
# Adds stack traces to meta_struct
7+
# @public_api
8+
module StackTrace
9+
def set_stack_trace(stack_trace, group:)
10+
meta_struct[Metadata::Ext::MetaStruct::TAG_STACK_TRACE] ||= {}
11+
meta_struct[Metadata::Ext::MetaStruct::TAG_STACK_TRACE][group] ||= []
12+
13+
stack_trace_group = meta_struct[Metadata::Ext::MetaStruct::TAG_STACK_TRACE][group]
14+
max_collect = Datadog.configuration.appsec.stack_trace.max_collect
15+
return if max_collect > 0 && stack_trace_group.size >= max_collect
16+
17+
stack_trace_group << stack_trace
18+
rescue StandardError => e
19+
Datadog.logger.debug("Unable to add stack_trace #{stack_trace.id} in meta_struct, ignoring it. Caused by: #{e}")
20+
end
21+
end
22+
end
23+
end
24+
end

Diff for: lib/datadog/tracing/stack_trace/collector.rb

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'frame'
4+
5+
module Datadog
6+
module Tracing
7+
module StackTrace
8+
# Represent a stack trace with its id and message in message pack
9+
module Collector
10+
class << self
11+
def collect(locations)
12+
return [] if locations.nil? || locations.empty?
13+
14+
skip_frames = skip_frames(locations.size)
15+
frames = []
16+
17+
locations.each_with_index do |location, index|
18+
next if skip_frames.include?(index)
19+
20+
frames << StackTrace::Frame.new(
21+
id: index,
22+
text: location.to_s.encode('UTF-8'),
23+
file: file_path(location),
24+
line: location.lineno,
25+
function: function_label(location)
26+
)
27+
end
28+
frames
29+
end
30+
31+
private
32+
33+
def skip_frames(locations_size)
34+
max_depth = Datadog.configuration.appsec.stack_trace.max_depth
35+
return [] if max_depth == 0 || locations_size <= max_depth
36+
37+
top_frames_limit = (max_depth * Datadog.configuration.appsec.stack_trace.max_depth_top_percent / 100.0).round
38+
bottom_frames_limit = locations_size - (max_depth - top_frames_limit)
39+
(top_frames_limit...bottom_frames_limit)
40+
end
41+
42+
def file_path(location)
43+
path = location.absolute_path || location.path
44+
return if path.nil?
45+
46+
path.encode('UTF-8')
47+
end
48+
49+
def function_label(location)
50+
label = location.label
51+
return if label.nil?
52+
53+
label.encode('UTF-8')
54+
end
55+
end
56+
end
57+
end
58+
end
59+
end

Diff for: lib/datadog/tracing/stack_trace/frame.rb

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
module Datadog
4+
module Tracing
5+
module StackTrace
6+
# Formatted stack frame.
7+
# This class extends a Struct as it's required by Steep to be able to add a method to it.
8+
class Frame < Struct.new(:id, :text, :file, :line, :function, keyword_init: true) # rubocop:disable Style/StructInheritance
9+
def to_msgpack(packer = nil)
10+
packer ||= MessagePack::Packer.new
11+
12+
packer.write_map_header(5)
13+
packer.write('id')
14+
packer.write(id)
15+
packer.write('text')
16+
packer.write(text)
17+
packer.write('file')
18+
packer.write(file)
19+
packer.write('line')
20+
packer.write(line)
21+
packer.write('function')
22+
packer.write(function)
23+
packer
24+
end
25+
end
26+
end
27+
end
28+
end

Diff for: lib/datadog/tracing/stack_trace/representor.rb

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
module Datadog
4+
module Tracing
5+
module StackTrace
6+
# Represent a stack trace with its id and message in message pack
7+
class Representor < Struct.new(:id, :message, :frames, keyword_init: true) # rubocop:disable Style/StructInheritance
8+
def to_msgpack(packer = nil)
9+
packer ||= MessagePack::Packer.new
10+
11+
packer.write_map_header(4)
12+
packer.write('language')
13+
packer.write('ruby')
14+
packer.write('id')
15+
packer.write(id)
16+
packer.write('message')
17+
packer.write(message)
18+
packer.write('frames')
19+
packer.write(frames)
20+
packer
21+
end
22+
end
23+
end
24+
end
25+
end

Diff for: sig/datadog/appsec/ext.rbs

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module Datadog
1414
CONTEXT_KEY: ::String
1515

1616
ACTIVE_CONTEXT_KEY: ::Symbol
17+
EXPLOIT_PREVENTION_EVENT_CATEGORY: ::String
1718

1819
TAG_APPSEC_ENABLED: ::String
1920

@@ -22,6 +23,7 @@ module Datadog
2223
TAG_DISTRIBUTED_APPSEC_EVENT: ::String
2324

2425
TELEMETRY_METRICS_NAMESPACE: ::String
26+
TAG_STACK_TRACE: ::String
2527
end
2628
end
2729
end

Diff for: sig/datadog/tracing/metadata/ext.rbs

+4
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ module Datadog
9999
TAG_CONSUMER: ::String
100100
TAG_INTERNAL: ::String
101101
end
102+
103+
module MetaStruct
104+
TAG_STACK_TRACE: ::String
105+
end
102106
end
103107
end
104108
end

Diff for: sig/datadog/tracing/metadata/meta_struct.rbs

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module Datadog
22
module Tracing
33
module Metadata
44
module MetaStruct
5+
include StackTrace
6+
57
def set_meta_struct: (Hash[String, untyped] meta_struct) -> void
68

79
def meta_struct: () -> Hash[String, untyped]

Diff for: sig/datadog/tracing/metadata/stack_trace.rbs

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Datadog
2+
module Tracing
3+
module Metadata
4+
module StackTrace
5+
def set_stack_trace: (Datadog::Tracing::StackTrace::Representor stack_trace, group: ::String) -> void
6+
7+
private
8+
9+
# Class/Module that includes this module should implement this method
10+
def meta_struct: () -> Hash[String, untyped]
11+
end
12+
end
13+
end
14+
end

Diff for: sig/datadog/tracing/stack_trace/collector.rbs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module Datadog
2+
module Tracing
3+
module StackTrace
4+
module Collector
5+
def self.collect: (Array[Thread::Backtrace::Location] locations) -> Array[Datadog::Tracing::StackTrace::Frame]
6+
7+
private
8+
9+
def self.skip_frames: (Integer locations_size) -> (Range[Integer] | Array[untyped])
10+
11+
def self.file_path: (Thread::Backtrace::Location location) -> String?
12+
13+
def self.function_label: (Thread::Backtrace::Location location) -> String?
14+
end
15+
end
16+
end
17+
end

Diff for: sig/datadog/tracing/stack_trace/frame.rbs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module Datadog
2+
module Tracing
3+
module StackTrace
4+
class Frame
5+
attr_reader id: Integer
6+
attr_reader text: String?
7+
attr_reader file: String?
8+
attr_reader line: Integer?
9+
attr_reader function: String?
10+
11+
def initialize: (?id: Integer, ?text: String?, ?file: String?, ?line: Integer?, ?function: String?) -> void
12+
13+
def to_msgpack: ((::MessagePack::Packer | nil) packer) -> ::MessagePack::Packer
14+
end
15+
end
16+
end
17+
end

Diff for: sig/datadog/tracing/stack_trace/representor.rbs

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module Datadog
2+
module Tracing
3+
module StackTrace
4+
class Representor
5+
attr_reader id: String?
6+
attr_reader message: String?
7+
attr_reader frames: Array[StackTrace::Frame]?
8+
9+
def initialize: (?id: String?, ?message: String?, ?frames: Array[StackTrace::Frame]?) -> void
10+
11+
def to_msgpack: ((::MessagePack::Packer | nil) packer) -> ::MessagePack::Packer
12+
end
13+
end
14+
end
15+
end

0 commit comments

Comments
 (0)