Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8bae22c

Browse files
committedFeb 19, 2025··
Add stack trace collection to meta_struct and actions_handler
1 parent 630c10d commit 8bae22c

17 files changed

+768
-9
lines changed
 

‎lib/datadog/appsec/actions_handler.rb

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

3+
require_relative 'actions_handler/rasp_stack_trace'
4+
require_relative 'actions_handler/stack_trace_collection'
5+
36
module Datadog
47
module AppSec
58
# this module encapsulates functions for handling actions that libddawf returns
@@ -19,7 +22,31 @@ 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+
return unless Datadog.configuration.appsec.stack_trace.enabled
27+
28+
context = AppSec.active_context
29+
if context.nil? || context.trace.nil? && context.span.nil?
30+
Datadog.logger.debug { 'Cannot find trace or service entry span to add stack trace' }
31+
return
32+
end
33+
34+
config = Datadog.configuration.appsec.stack_trace
35+
36+
# Check that the sum of stack_trace count in trace and entry_span does not exceed configuration
37+
span_stack = ActionsHandler::RaspStackTrace.new(context.span&.metastruct)
38+
trace_stack = ActionsHandler::RaspStackTrace.new(context.trace&.metastruct)
39+
return if config.max_collect != 0 && span_stack.count + trace_stack.count >= config.max_collect
40+
41+
# Generate stacktrace
42+
utf8_stack_id = action_params['stack_id'].encode('UTF-8') if action_params['stack_id']
43+
stack_frames = ActionsHandler::StackTraceCollection.collect(config.max_depth, config.max_depth_top_percent)
44+
stack_trace = { language: 'ruby', id: utf8_stack_id, frames: stack_frames }
45+
46+
# Add newly created stacktrace to metastruct
47+
stack = context.trace.nil? ? span_stack : trace_stack
48+
stack.push(stack_trace)
49+
end
2350

2451
def generate_schema(_action_params); end
2552
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
module Datadog
4+
module AppSec
5+
module ActionsHandler
6+
# Object that holds a metastruct, and modify the exploit group stack traces
7+
class RaspStackTrace
8+
def initialize(metastruct)
9+
@metastruct = metastruct
10+
end
11+
12+
def count
13+
@metastruct&.dig(AppSec::Ext::TAG_STACK_TRACE, AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY)&.size || 0
14+
end
15+
16+
def push(stack_trace)
17+
return if @metastruct.nil?
18+
19+
# steep:ignore:start
20+
@metastruct[AppSec::Ext::TAG_STACK_TRACE] ||= {}
21+
@metastruct[AppSec::Ext::TAG_STACK_TRACE][AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY] ||= []
22+
@metastruct[AppSec::Ext::TAG_STACK_TRACE][AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY] << stack_trace
23+
# steep:ignore:end
24+
end
25+
end
26+
end
27+
end
28+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
module Datadog
4+
module AppSec
5+
module ActionsHandler
6+
# Module that collects the stack trace into formatted hash
7+
module StackTraceCollection
8+
module_function
9+
10+
def collect(max_depth, top_percent)
11+
filtered_locations = caller_locations&.reject { |location| location.to_s.include?('lib/datadog') } || []
12+
13+
skip_locations =
14+
if max_depth == 0 || filtered_locations.size <= max_depth
15+
(0...0)
16+
else
17+
top_limit = (max_depth * top_percent / 100.0).round
18+
bottom_limit = filtered_locations.size - (max_depth - top_limit)
19+
(top_limit...bottom_limit)
20+
end
21+
filtered_locations.slice!(skip_locations)
22+
23+
filtered_locations.map.with_index do |location, index|
24+
{
25+
id: index,
26+
text: location.to_s.encode('UTF-8'),
27+
file: (location.absolute_path || location.path)&.encode('UTF-8'),
28+
line: location.lineno,
29+
function: location.label&.encode('UTF-8')
30+
}
31+
end
32+
end
33+
end
34+
end
35+
end
36+
end

‎lib/datadog/appsec/configuration/settings.rb

+49
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,55 @@ def self.add_settings!(base)
267267
o.default false
268268
end
269269
end
270+
271+
settings :stack_trace do
272+
option :enabled do |o|
273+
o.type :bool
274+
o.env 'DD_APPSEC_STACK_TRACE_ENABLED'
275+
o.default true
276+
end
277+
278+
# The maximum number of stack frames to collect for each stack trace.
279+
# If the number of frames in a stack trace exceeds this value,
280+
# max_depth / 4 frames will be collected from the top, and max_depth * 3 / 4 from the bottom.
281+
option :max_depth do |o|
282+
o.type :int
283+
o.env 'DD_APPSEC_MAX_STACK_TRACE_DEPTH'
284+
o.default 32
285+
# 0 means no limit
286+
o.setter do |value|
287+
value = 0 if value.negative?
288+
value
289+
end
290+
end
291+
292+
# The percentage that decides the number of top stack frame to collect
293+
# for each stack trace if there is more stack frames than max_depth.
294+
# number_of_top_frames = max_depth * max_depth_top_percent / 100
295+
# Default is 75
296+
option :max_depth_top_percent do |o|
297+
o.type :float
298+
o.env 'DD_APPSEC_MAX_STACK_TRACE_DEPTH_TOP_PERCENT'
299+
o.default 75
300+
o.setter do |value|
301+
value = 100 if value > 100
302+
value = 0 if value < 0
303+
value
304+
end
305+
end
306+
307+
# The maximum number of stack traces to collect for each exploit prevention event.
308+
option :max_collect do |o|
309+
o.type :int
310+
o.env 'DD_APPSEC_MAX_STACK_TRACES'
311+
o.default 2
312+
# 0 means no limit
313+
o.setter do |value|
314+
value = 0 if value < 0
315+
value
316+
end
317+
end
318+
end
270319
end
271320
end
272321
end

‎lib/datadog/appsec/context.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ def extract_schema
6262
def export_metrics
6363
return if @span.nil?
6464

65-
Metrics::Exporter.export_waf_metrics(@metrics.waf, @span)
66-
Metrics::Exporter.export_rasp_metrics(@metrics.rasp, @span)
65+
Metrics::Exporter.export_waf_metrics(@metrics.waf, @span) # steep:ignore ArgumentTypeMismatch
66+
Metrics::Exporter.export_rasp_metrics(@metrics.rasp, @span) # steep:ignore ArgumentTypeMismatch
6767
end
6868

6969
def finalize

‎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

‎sig/datadog/appsec/actions_handler.rbs

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ module Datadog
88
def generate_stack: (Datadog::AppSec::SecurityEngine::Result::action action_params) -> void
99

1010
def generate_schema: (Datadog::AppSec::SecurityEngine::Result::action action_params) -> void
11+
12+
private
13+
14+
def generate_stack_trace: (Integer max_depth, Float top_percent) -> (Hash[Symbol, String | Integer | Array[Hash[Symbol, String | Integer | nil]] | nil ])
15+
16+
1117
end
1218
end
1319
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module Datadog
2+
module AppSec
3+
module ActionsHandler
4+
class RaspStackTrace
5+
type stack_trace = Hash[Symbol, String | nil | Array[Datadog::AppSec::ActionsHandler::StackTraceCollection::stack_frame]]
6+
7+
@metastruct: Datadog::Tracing::Metadata::Metastruct?
8+
9+
def initialize: (Datadog::Tracing::Metadata::Metastruct? metastruct) -> void
10+
11+
def count: () -> Integer
12+
13+
def push: (stack_trace stack_trace) -> void
14+
end
15+
end
16+
end
17+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module Datadog
2+
module AppSec
3+
module ActionsHandler
4+
module StackTraceCollection
5+
type stack_frame = Hash[Symbol, String | Integer | nil]
6+
7+
def self.collect: (Integer max_depth, Float top_percent) -> Array[stack_frame]
8+
end
9+
end
10+
end
11+
end

‎sig/datadog/appsec/context.rbs

+5-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ module Datadog
33
class Context
44
type input_data = SecurityEngine::Runner::input_data
55

6-
@trace: Tracing::TraceOperation
6+
@trace: Tracing::TraceOperation?
77

8-
@span: Tracing::SpanOperation
8+
@span: Tracing::SpanOperation?
99

1010
@events: ::Array[untyped]
1111

@@ -17,17 +17,17 @@ module Datadog
1717

1818
ActiveContextError: ::StandardError
1919

20-
attr_reader trace: Tracing::TraceOperation
20+
attr_reader trace: Tracing::TraceOperation?
2121

22-
attr_reader span: Tracing::SpanOperation
22+
attr_reader span: Tracing::SpanOperation?
2323

2424
attr_reader events: ::Array[untyped]
2525

2626
def self.activate: (Context context) -> Context
2727

2828
def self.deactivate: () -> void
2929

30-
def self.active: () -> Context
30+
def self.active: () -> Context?
3131

3232
def initialize: (Tracing::TraceOperation trace, Tracing::SpanOperation span, AppSec::Processor security_engine) -> void
3333

‎sig/datadog/appsec/event.rbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ module Datadog
2424

2525
def self.gzip: (untyped value) -> untyped
2626

27-
def self.add_distributed_tags: (Tracing::TraceOperation trace) -> void
27+
def self.add_distributed_tags: (Tracing::TraceOperation? trace) -> void
2828
end
2929
end
3030
end

‎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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# frozen_string_literal: true
2+
3+
require 'datadog/appsec/actions_handler/rasp_stack_trace'
4+
require 'datadog/tracing/metadata/metastruct'
5+
6+
RSpec.describe Datadog::AppSec::ActionsHandler::RaspStackTrace do
7+
subject(:rasp_stack_trace) { described_class.new(metastruct) }
8+
let(:metastruct) { Datadog::Tracing::Metadata::Metastruct.new(metastruct_hash) }
9+
let(:metastruct_hash) { {} }
10+
11+
describe '.count' do
12+
subject(:count) { rasp_stack_trace.count }
13+
14+
context 'with nil as metastruct' do
15+
let(:metastruct) { nil }
16+
17+
it { is_expected.to eq 0 }
18+
end
19+
20+
context 'with empty metastruct' do
21+
it { is_expected.to eq 0 }
22+
end
23+
24+
context 'with metastruct containing non-exploit stack traces' do
25+
let(:metastruct_hash) do
26+
{
27+
'_dd.stack' => {
28+
'vulnerabilities' => [1, 2]
29+
}
30+
}
31+
end
32+
33+
it { is_expected.to eq 0 }
34+
end
35+
36+
context 'with metastruct containing exploit stack traces' do
37+
let(:metastruct_hash) do
38+
{
39+
'_dd.stack' => {
40+
'exploit' => [1, 2]
41+
}
42+
}
43+
end
44+
45+
it { is_expected.to eq 2 }
46+
end
47+
end
48+
49+
describe '.push' do
50+
before do
51+
rasp_stack_trace.push({ language: 'ruby', stack_id: 'foo', frames: [] })
52+
end
53+
54+
context 'with empty metastruct' do
55+
it 'adds a new stack trace to the metastruct' do
56+
expect(metastruct.to_h).to eq(
57+
'_dd.stack' => {
58+
'exploit' => [
59+
{
60+
language: 'ruby',
61+
stack_id: 'foo',
62+
frames: []
63+
}
64+
]
65+
}
66+
)
67+
end
68+
end
69+
70+
context 'with existing exploit stack traces in different group' do
71+
let(:metastruct_hash) do
72+
{
73+
'_dd.stack' => {
74+
'vulnerabilities' => [1, 2]
75+
}
76+
}
77+
end
78+
79+
it 'adds a new stack trace to the metastruct' do
80+
expect(metastruct.to_h).to eq(
81+
'_dd.stack' => {
82+
'vulnerabilities' => [1, 2],
83+
'exploit' => [
84+
{
85+
language: 'ruby',
86+
stack_id: 'foo',
87+
frames: []
88+
}
89+
]
90+
}
91+
)
92+
end
93+
end
94+
95+
context 'with existing exploit stack traces in the same group' do
96+
let(:metastruct_hash) do
97+
{
98+
'_dd.stack' => {
99+
'exploit' => [1, 2]
100+
}
101+
}
102+
end
103+
104+
it 'adds a new stack trace to the metastruct' do
105+
expect(metastruct.to_h).to eq(
106+
'_dd.stack' => {
107+
'exploit' => [
108+
1,
109+
2,
110+
{
111+
language: 'ruby',
112+
stack_id: 'foo',
113+
frames: []
114+
}
115+
]
116+
}
117+
)
118+
end
119+
end
120+
end
121+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
require 'datadog/appsec/actions_handler/stack_trace_collection'
4+
require 'support/thread_backtrace_helpers'
5+
6+
RSpec.describe Datadog::AppSec::ActionsHandler::StackTraceCollection do
7+
describe '.collect' do
8+
subject(:collection) { described_class.collect(max_depth, top_percent) }
9+
10+
# Default values in config
11+
let(:max_depth) { 32 }
12+
let(:top_percent) { 75 }
13+
14+
# "/app/spec/support/thread_backtrace_helpers.rb:12:in `block in locations_inside_nested_blocks'",
15+
# "/app/spec/support/thread_backtrace_helpers.rb:14:in `block (2 levels) in locations_inside_nested_blocks'",
16+
# "/app/spec/support/thread_backtrace_helpers.rb:16:in `block (3 levels) in locations_inside_nested_blocks'",
17+
# "/app/spec/support/thread_backtrace_helpers.rb:16:in `block (4 levels) in locations_inside_nested_blocks'",
18+
# "/app/spec/support/thread_backtrace_helpers.rb:16:in `block (5 levels) in locations_inside_nested_blocks'"
19+
let(:frames) { ThreadBacktraceHelper.locations_inside_nested_blocks }
20+
21+
before do
22+
# Hack to get caller_locations to return a known set of frames
23+
allow_any_instance_of(Array).to receive(:reject).and_return(frames.clone)
24+
end
25+
26+
it 'returns stack frames excluding those from datadog' do
27+
expect(collection.any? { |loc| loc[:text].include?('lib/datadog') }).to be false
28+
end
29+
30+
it 'returns the correct number of stack frames' do
31+
expect(collection.size).to eq(5)
32+
end
33+
34+
context 'with max_depth set to 4' do
35+
let(:max_depth) { 4 }
36+
37+
it 'creates a stack trace with 4 frames, 3 top' do
38+
expect(collection.count).to eq(4)
39+
expect(collection[2][:text]).to eq(frames[2].to_s)
40+
expect(collection[3][:text]).to eq(frames[4].to_s)
41+
end
42+
43+
context 'with max_depth_top_percent set to 25' do
44+
let(:top_percent) { 25 }
45+
46+
it 'creates a stack trace with 4 frames, 1 top' do
47+
expect(collection.count).to eq(4)
48+
expect(collection[0][:text]).to eq(frames[0].to_s)
49+
expect(collection[1][:text]).to eq(frames[2].to_s)
50+
end
51+
end
52+
53+
context 'with max_depth_top_percent set to 100' do
54+
let(:top_percent) { 100 }
55+
56+
it 'creates a stack trace with 4 top frames' do
57+
expect(collection.count).to eq(4)
58+
expect(collection[0][:text]).to eq(frames[0].to_s)
59+
expect(collection[3][:text]).to eq(frames[3].to_s)
60+
end
61+
end
62+
63+
context 'with max_depth_top_percent set to 0' do
64+
let(:top_percent) { 0 }
65+
66+
it 'creates a stack trace with 4 bottom frames' do
67+
expect(collection.count).to eq(4)
68+
expect(collection[0][:text]).to eq(frames[1].to_s)
69+
expect(collection[3][:text]).to eq(frames[4].to_s)
70+
end
71+
end
72+
end
73+
74+
context 'with max_depth set to 3' do
75+
let(:max_depth) { 3 }
76+
77+
context 'with max_depth_top_percent set to 66.67' do
78+
let(:top_percent) { 200 / 3.0 }
79+
80+
it 'creates a stack trace with 3 frames, 2 top' do
81+
expect(collection.count).to eq(3)
82+
expect(collection[1][:text]).to eq(frames[1].to_s)
83+
expect(collection[2][:text]).to eq(frames[4].to_s)
84+
end
85+
end
86+
end
87+
end
88+
end

‎spec/datadog/appsec/actions_handler_spec.rb

+124
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# frozen_string_literal: true
22

3+
require 'ostruct'
4+
5+
require 'datadog/appsec/ext'
36
require 'datadog/appsec/spec_helper'
7+
require 'support/thread_backtrace_helpers'
48

59
RSpec.describe Datadog::AppSec::ActionsHandler do
610
describe '.handle' do
@@ -97,4 +101,124 @@
97101
end
98102
end
99103
end
104+
105+
describe '.generate_stack' do
106+
let(:generate_stack_action) { { 'stack_id' => 'foo' } }
107+
let(:trace_op) { Datadog::Tracing::TraceOperation.new }
108+
let(:trace_op_metastruct) { nil }
109+
let(:span_op) { Datadog::Tracing::SpanOperation.new('span_test') }
110+
let(:span_op_metastruct) { nil }
111+
let(:context) { OpenStruct.new(trace: trace_op, span: span_op) }
112+
let(:stack_trace_enabled) { true }
113+
let(:max_collect) { 0 }
114+
115+
let(:stack_key) { Datadog::AppSec::Ext::TAG_STACK_TRACE }
116+
let(:exploit_category) { Datadog::AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY }
117+
118+
before do
119+
Datadog.configure do |c|
120+
c.appsec.stack_trace.max_depth = 0
121+
c.appsec.stack_trace.max_depth_top_percent = 0
122+
end
123+
allow(Datadog.configuration.appsec.stack_trace).to receive(:enabled).and_return(stack_trace_enabled)
124+
allow(Datadog.configuration.appsec.stack_trace).to receive(:max_collect).and_return(max_collect)
125+
allow(Datadog::AppSec).to receive(:active_context).and_return(context)
126+
allow(Datadog::AppSec::ActionsHandler::StackTraceCollection).to receive(:collect).and_return(
127+
ThreadBacktraceHelper.locations_inside_nested_blocks.map.with_index do |location, index|
128+
{
129+
id: index,
130+
text: location.to_s.encode('UTF-8'),
131+
file: (location.absolute_path || location.path)&.encode('UTF-8'),
132+
line: location.lineno,
133+
function: location.label&.encode('UTF-8')
134+
}
135+
end
136+
)
137+
trace_op.metastruct[Datadog::AppSec::Ext::TAG_STACK_TRACE] = trace_op_metastruct
138+
span_op.metastruct[Datadog::AppSec::Ext::TAG_STACK_TRACE] = span_op_metastruct
139+
described_class.generate_stack(generate_stack_action)
140+
end
141+
142+
context 'when stack trace is enabled and context contains trace and span' do
143+
it 'adds stack trace to the trace' do
144+
trace_op_result = trace_op.metastruct[stack_key][exploit_category]
145+
expect(trace_op_result.size).to eq(1)
146+
expect(trace_op_result.first[:id]).to eq('foo')
147+
expect(trace_op_result.first[:frames].size).to eq(5)
148+
end
149+
150+
context 'when max_collect is 2' do
151+
let(:max_collect) { 2 }
152+
153+
context 'with two elements contained in same group in trace' do
154+
let(:trace_op_metastruct) { { exploit_category => [1, 2] } }
155+
156+
it 'does not add stack trace to the trace nor the span' do
157+
trace_op_result = trace_op.metastruct.dig(stack_key, exploit_category)
158+
expect(trace_op_result.size).to eq(2)
159+
expect(trace_op_result[0]).to eq(1)
160+
expect(trace_op_result[1]).to eq(2)
161+
span_op_result = span_op.metastruct.dig(stack_key, exploit_category)
162+
expect(span_op_result).to be_nil
163+
end
164+
end
165+
166+
context 'with two elements contained in same group in span' do
167+
let(:span_op_metastruct) { { exploit_category => [1, 2] } }
168+
169+
it 'does not add stack trace to the trace nor the span' do
170+
span_op_result = span_op.metastruct.dig(stack_key, exploit_category)
171+
expect(span_op_result.size).to eq(2)
172+
expect(span_op_result[0]).to eq(1)
173+
expect(span_op_result[1]).to eq(2)
174+
trace_op_result = trace_op.metastruct.dig(stack_key, exploit_category)
175+
expect(trace_op_result).to be_nil
176+
end
177+
end
178+
179+
context 'with one element contained in same group in span and trace' do
180+
let(:trace_op_metastruct) { { exploit_category => [1] } }
181+
let(:span_op_metastruct) { { exploit_category => [2] } }
182+
183+
it 'does not add stack trace to the trace nor the span' do
184+
trace_op_result = trace_op.metastruct.dig(stack_key, exploit_category)
185+
expect(trace_op_result.size).to eq(1)
186+
expect(trace_op_result.first).to eq(1)
187+
span_op_result = span_op.metastruct.dig(stack_key, exploit_category)
188+
expect(span_op_result.size).to eq(1)
189+
expect(span_op_result.first).to eq(2)
190+
end
191+
end
192+
193+
context 'with two elements contained in different group in trace' do
194+
let(:trace_op_metastruct) { { 'other_group' => [1, 2] } }
195+
196+
it 'does add stack trace to the trace' do
197+
trace_op_result = trace_op.metastruct.dig(stack_key, exploit_category)
198+
expect(trace_op_result.size).to eq(1)
199+
expect(trace_op_result.first[:id]).to eq('foo')
200+
end
201+
end
202+
end
203+
end
204+
205+
context 'when stack trace is enabled and context contains only span' do
206+
let(:context) { OpenStruct.new(span: span_op) }
207+
208+
it 'adds stack trace to the span' do
209+
test_result = span_op.metastruct[stack_key][exploit_category]
210+
expect(test_result.size).to eq(1)
211+
expect(test_result.first[:id]).to eq('foo')
212+
expect(test_result.first[:frames].size).to eq(5)
213+
end
214+
end
215+
216+
context 'when stack trace is disabled' do
217+
let(:stack_trace_enabled) { false }
218+
219+
it 'does not add stack trace to the trace' do
220+
expect(trace_op.metastruct[stack_key]).to be_nil
221+
end
222+
end
223+
end
100224
end

‎spec/datadog/appsec/configuration/settings_spec.rb

+184
Original file line numberDiff line numberDiff line change
@@ -930,5 +930,189 @@ def patcher
930930
end
931931
end
932932
end
933+
934+
describe 'stack_trace' do
935+
describe '#enabled' do
936+
subject(:enabled) { settings.appsec.stack_trace.enabled }
937+
938+
context 'when DD_APPSEC_STACK_TRACE_ENABLED' do
939+
around do |example|
940+
ClimateControl.modify('DD_APPSEC_STACK_TRACE_ENABLED' => stack_trace_enabled) do
941+
example.run
942+
end
943+
end
944+
945+
context 'is not defined' do
946+
let(:stack_trace_enabled) { nil }
947+
948+
it { is_expected.to eq true }
949+
end
950+
951+
[true, false].each do |value|
952+
context "is defined as #{value}" do
953+
let(:stack_trace_enabled) { value.to_s }
954+
955+
it { is_expected.to eq value }
956+
end
957+
end
958+
end
959+
end
960+
961+
describe '#enabled=' do
962+
subject(:set_stack_trace_enabled) { settings.appsec.stack_trace.enabled = stack_trace_enabled }
963+
964+
[true, false].each do |value|
965+
context "when given #{value}" do
966+
let(:stack_trace_enabled) { value }
967+
968+
before { set_stack_trace_enabled }
969+
970+
it { expect(settings.appsec.stack_trace.enabled).to eq(value) }
971+
end
972+
end
973+
end
974+
975+
describe '#max_depth' do
976+
subject(:max_depth) { settings.appsec.stack_trace.max_depth }
977+
978+
context 'when DD_APPSEC_MAX_STACK_TRACE_DEPTH' do
979+
around do |example|
980+
ClimateControl.modify('DD_APPSEC_MAX_STACK_TRACE_DEPTH' => stack_trace_max_depth) do
981+
example.run
982+
end
983+
end
984+
985+
context 'is not defined' do
986+
let(:stack_trace_max_depth) { nil }
987+
988+
it { is_expected.to eq 32 }
989+
end
990+
991+
context 'is defined' do
992+
let(:stack_trace_max_depth) { '64' }
993+
994+
it { is_expected.to eq(64) }
995+
end
996+
end
997+
end
998+
999+
describe '#max_depth=' do
1000+
subject(:set_stack_trace_max_depth) { settings.appsec.stack_trace.max_depth = stack_trace_max_depth }
1001+
1002+
context 'when given a value' do
1003+
let(:stack_trace_max_depth) { 64 }
1004+
1005+
before { set_stack_trace_max_depth }
1006+
1007+
it { expect(settings.appsec.stack_trace.max_depth).to eq(64) }
1008+
end
1009+
1010+
context 'when given a negative value' do
1011+
let(:stack_trace_max_depth) { -1 }
1012+
1013+
before { set_stack_trace_max_depth }
1014+
1015+
it { expect(settings.appsec.stack_trace.max_depth).to eq(0) }
1016+
end
1017+
end
1018+
1019+
describe '#max_depth_top_percent' do
1020+
subject(:max_depth_top_percent) { settings.appsec.stack_trace.max_depth_top_percent }
1021+
1022+
context 'when DD_APPSEC_MAX_STACK_TRACE_DEPTH_TOP_PERCENT' do
1023+
around do |example|
1024+
ClimateControl.modify('DD_APPSEC_MAX_STACK_TRACE_DEPTH_TOP_PERCENT' => stack_trace_max_depth_top_percent) do
1025+
example.run
1026+
end
1027+
end
1028+
1029+
context 'is not defined' do
1030+
let(:stack_trace_max_depth_top_percent) { nil }
1031+
1032+
it { is_expected.to eq 75 }
1033+
end
1034+
1035+
context 'is defined' do
1036+
let(:stack_trace_max_depth_top_percent) { '50' }
1037+
1038+
it { is_expected.to eq(50) }
1039+
end
1040+
end
1041+
end
1042+
1043+
describe '#max_depth_top_percent=' do
1044+
subject(:set_stack_trace_max_depth_top_percent) do
1045+
settings.appsec.stack_trace.max_depth_top_percent = stack_trace_max_depth_top_percent
1046+
end
1047+
1048+
context 'when given a value' do
1049+
let(:stack_trace_max_depth_top_percent) { 50 }
1050+
1051+
before { set_stack_trace_max_depth_top_percent }
1052+
1053+
it { expect(settings.appsec.stack_trace.max_depth_top_percent).to eq(50) }
1054+
end
1055+
1056+
context 'when given a negative value' do
1057+
let(:stack_trace_max_depth_top_percent) { -1 }
1058+
1059+
before { set_stack_trace_max_depth_top_percent }
1060+
1061+
it { expect(settings.appsec.stack_trace.max_depth_top_percent).to eq(0) }
1062+
end
1063+
1064+
context 'when given a value higher than 100' do
1065+
let(:stack_trace_max_depth_top_percent) { 101 }
1066+
1067+
before { set_stack_trace_max_depth_top_percent }
1068+
1069+
it { expect(settings.appsec.stack_trace.max_depth_top_percent).to eq(100) }
1070+
end
1071+
end
1072+
1073+
describe '#max_collect' do
1074+
subject(:max_collect) { settings.appsec.stack_trace.max_collect }
1075+
1076+
context 'when DD_APPSEC_MAX_STACK_TRACES' do
1077+
around do |example|
1078+
ClimateControl.modify('DD_APPSEC_MAX_STACK_TRACES' => stack_trace_max_collect) do
1079+
example.run
1080+
end
1081+
end
1082+
1083+
context 'is not defined' do
1084+
let(:stack_trace_max_collect) { nil }
1085+
1086+
it { is_expected.to eq 2 }
1087+
end
1088+
1089+
context 'is defined' do
1090+
let(:stack_trace_max_collect) { '4' }
1091+
1092+
it { is_expected.to eq(4) }
1093+
end
1094+
end
1095+
end
1096+
1097+
describe '#max_collect=' do
1098+
subject(:set_stack_trace_max_collect) { settings.appsec.stack_trace.max_collect = stack_trace_max_collect }
1099+
1100+
context 'when given a value' do
1101+
let(:stack_trace_max_collect) { 4 }
1102+
1103+
before { set_stack_trace_max_collect }
1104+
1105+
it { expect(settings.appsec.stack_trace.max_collect).to eq(4) }
1106+
end
1107+
1108+
context 'when given a negative value' do
1109+
let(:stack_trace_max_collect) { -1 }
1110+
1111+
before { set_stack_trace_max_collect }
1112+
1113+
it { expect(settings.appsec.stack_trace.max_collect).to eq(0) }
1114+
end
1115+
end
1116+
end
9331117
end
9341118
end
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# frozen_string_literal: true
2+
3+
# Module to test stack trace generation. Inspired by:
4+
# https://github.com/ruby/ruby/blob/master/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb
5+
module ThreadBacktraceHelper
6+
def self.locations
7+
caller_locations
8+
end
9+
10+
# Deeply nested blocks to test max_depth and max_depth_top_percentage variables
11+
def self.locations_inside_nested_blocks
12+
first_level_location = nil
13+
second_level_location = nil
14+
third_level_location = nil
15+
fourth_level_location = nil
16+
fifth_level_location = nil
17+
18+
# rubocop:disable Lint/UselessTimes
19+
1.times do
20+
first_level_location = locations.first
21+
1.times do
22+
second_level_location = locations.first
23+
1.times do
24+
third_level_location = locations.first
25+
1.times do
26+
fourth_level_location = locations.first
27+
1.times do
28+
fifth_level_location = locations.first
29+
end
30+
end
31+
end
32+
end
33+
end
34+
# rubocop:enable Lint/UselessTimes
35+
36+
[first_level_location, second_level_location, third_level_location, fourth_level_location, fifth_level_location]
37+
end
38+
39+
def self.thousand_locations
40+
locations = []
41+
1000.times do
42+
locations << self.locations.first
43+
end
44+
locations
45+
end
46+
47+
LocationASCII8Bit = Struct.new(:text, :path, :lineno, :label, keyword_init: true) do
48+
def to_s
49+
text
50+
end
51+
end
52+
53+
def self.location_ascii_8bit
54+
location = locations.first
55+
LocationASCII8Bit.new(
56+
text: location.to_s.encode('ASCII-8BIT'),
57+
path: (location.absolute_path || location.path).encode('ASCII-8BIT'),
58+
lineno: location.lineno,
59+
label: location.label.encode('ASCII-8BIT')
60+
)
61+
62+
[location]
63+
end
64+
end

0 commit comments

Comments
 (0)
Please sign in to comment.