44
55module Datadog
66 module AppSec
7- # Stack trace
8- class StackTrace
9- attr_reader :id , :message , :frames
10-
11- def initialize ( id , stack_trace , message : nil )
12- # ASCII-8BIT is encoded as byte array and backend cannot decode it properly
13- # so we enforce UTF-8 on strings
14- # We must copy them as they can be frozen
15- @id = id && id . encode ( 'UTF-8' )
16- @message = message && message . encode ( 'UTF-8' )
17- @frames = if stack_trace
18- top_frames = if Datadog . configuration . appsec . stack_trace . max_depth == 0
19- stack_trace . size
20- else
21- ( Datadog . configuration . appsec . stack_trace . max_depth *
22- Datadog . configuration . appsec . stack_trace . max_depth_top_percent / 100.0 ) . round
23- end
24- bottom_frames = Datadog . configuration . appsec . stack_trace . max_depth - top_frames
25- cropped_stack_trace = [ ]
26- stack_trace . each_with_index do |frame , index |
27- next if index >= top_frames && index < stack_trace . size - bottom_frames
28-
29- cropped_stack_trace << Frame . new (
30- # We must keep the original stack trace index, to show that it was cropped
31- index ,
32- text : frame . to_s ,
33- # path will show 'main' instead of the actual file
34- # but absolute_path does not work for e.g. <internal:kernel>
35- file : frame . absolute_path || frame . path ,
36- line : frame . lineno ,
37- # label will show 'block in function' instead of just 'function'
38- function : frame . label
39- )
40- end
41- cropped_stack_trace
42- else
43- [ ]
44- end
45- end
46-
47- def to_h
48- @to_h ||= {
49- language : 'ruby' ,
50- id : @id ,
51- message : @message ,
52- frames : @frames . map ( &:to_h )
53- }
54- end
55-
56- def to_msgpack ( packer = nil )
57- packer ||= MessagePack ::Packer . new
58-
59- packer . write_map_header ( 4 )
60- packer . write ( 'language' )
61- packer . write ( 'ruby' )
62- packer . write ( 'id' )
63- packer . write ( @id )
64- packer . write ( 'message' )
65- packer . write ( @message )
66- packer . write ( 'frames' )
67- packer . write ( @frames )
68- packer
69- end
70-
71- def pretty_print ( q )
72- q . text to_s
73- end
74-
75- class << self
76- # Add the stack trace to the trace
77- def add_stack_trace ( action_params )
78- # As ActionsHandler is only called when there is a context, we shouldn't need to check if it exists
79- context = AppSec ::Context . active
80- # We use methods defined in Tracing::Metadata::Tagging,
81- # which means we can work on both the trace and the service entry span
82- service_entry_operation = context . trace || context . span
83-
84- unless service_entry_operation
85- Datadog . logger . debug { 'Cannot find trace or service entry span to add stack trace' }
86- return
7+ module StackTrace
8+ # Stack trace collection class, including Enumerable (iterates on cropped frames array)
9+ class Collection
10+ include Enumerable
11+
12+ def initialize ( frames )
13+ max_depth = Datadog . configuration . appsec . stack_trace . max_depth
14+ # 0 means no limit, setting the top_frames limit to the size of the stack trace
15+ # will ensure that we keep all the frames.
16+ # If not set to 0, "max_depth_top_percent" % of the max_depth value
17+ # corresponds to the number of top frames we want to keep.
18+ top_frames = if max_depth == 0
19+ frames . size
20+ else
21+ ( max_depth * Datadog . configuration . appsec . stack_trace . max_depth_top_percent / 100.0 ) . round
22+ end
23+ bottom_frames = max_depth - top_frames
24+ cropped_frames = [ ]
25+ frames . each_with_index do |frame , index |
26+ next if index >= top_frames && index < frames . size - bottom_frames
27+
28+ # ASCII-8BIT is encoded as byte array and backend cannot decode it properly
29+ # so we enforce UTF-8 on strings.
30+ # We must copy them as they can be frozen
31+ cropped_frames << Frame . new (
32+ # We must keep the original stack trace index, to show that it was cropped
33+ index ,
34+ text : frame . to_s && frame . to_s . encode ( 'UTF-8' ) ,
35+ # path will show 'main' instead of the actual file
36+ # but absolute_path does not work for e.g. <internal:kernel>
37+ file : ( frame . absolute_path || frame . path ) && ( frame . absolute_path || frame . path ) . encode ( 'UTF-8' ) ,
38+ line : frame . lineno ,
39+ # label will show 'block in function' instead of just 'function'
40+ function : frame . label && frame . label . encode ( 'UTF-8' )
41+ )
8742 end
43+ @frames = cropped_frames
44+ end
8845
89- stack_trace = from_waf_result ( action_params )
90-
91- service_entry_operation . modify_meta_struct_tag ( AppSec ::Ext ::TAG_STACK_TRACE ) do |stack_traces |
92- # _dd.stack is a hash of stack traces (Hash<String, Array<StackTrace>>)
93- stack_traces ||= { }
94- exploit_stack_traces = ( stack_traces [ AppSec ::Ext ::EXPLOIT_PREVENTION_EVENT_CATEGORY ] ||= [ ] )
95- # We only keep the first 'max_collect' stack traces
96- # In practice we should also check the vulnerability category size, but we don't have IAST in Ruby
97- if Datadog . configuration . appsec . stack_trace . max_collect == 0 ||
98- exploit_stack_traces . size < Datadog . configuration . appsec . stack_trace . max_collect
99- exploit_stack_traces << stack_trace
100- end
101- stack_traces
102- end
46+ def each ( &block )
47+ @frames . each ( &block )
10348 end
10449
105- private
50+ def to_msgpack ( packer = nil )
51+ packer ||= MessagePack ::Packer . new
10652
107- # Create a stack trace from the result of WAF execution
108- def from_waf_result ( action_params )
109- stack_frames = caller_locations || [ ]
110- # caller_locations without params always returns an array but rbs still thinks it can be nil
111- stack_frames . reject! { |loc | loc . path && loc . path . include? ( 'lib/datadog' ) }
112- stack_trace_id = action_params [ 'stack_id' ]
113- AppSec ::StackTrace . new ( stack_trace_id , stack_frames )
53+ packer . write_array_header ( @frames . size )
54+ @frames . each do |frame |
55+ packer . write ( frame )
56+ end
57+ packer
11458 end
11559 end
11660
117- # Stack frame
61+ # Stack frame.
62+ # We cannot use Struct as we need to encode it to MessagePack, and Struct does not provide a 'to_msgpack' method.
11863 class Frame
11964 attr_reader :id , :text , :file , :line , :function
12065
@@ -123,10 +68,10 @@ class Frame
12368 # We must copy them as they can be frozen
12469 def initialize ( id , text : nil , file : nil , line : nil , function : nil )
12570 @id = id
126- @text = text && text . encode ( 'UTF-8' )
127- @file = file && file . encode ( 'UTF-8' )
71+ @text = text
72+ @file = file
12873 @line = line
129- @function = function && function . encode ( 'UTF-8' )
74+ @function = function
13075 end
13176
13277 def to_h
@@ -156,6 +101,45 @@ def to_msgpack(packer = nil)
156101 packer
157102 end
158103 end
104+
105+ # Stack trace representation.
106+ class Representation
107+ attr_reader :id , :message , :stack_trace
108+
109+ def initialize ( id , stack_trace , message : nil )
110+ @id = id
111+ @message = message
112+ @stack_trace = stack_trace
113+ end
114+
115+ def to_h
116+ @to_h ||= {
117+ language : 'ruby' ,
118+ id : @id ,
119+ message : @message ,
120+ frames : @stack_trace . map ( &:to_h )
121+ }
122+ end
123+
124+ def to_msgpack ( packer = nil )
125+ packer ||= MessagePack ::Packer . new
126+
127+ packer . write_map_header ( 4 )
128+ packer . write ( 'language' )
129+ packer . write ( 'ruby' )
130+ packer . write ( 'id' )
131+ packer . write ( @id )
132+ packer . write ( 'message' )
133+ packer . write ( @message )
134+ packer . write ( 'frames' )
135+ packer . write ( @stack_trace )
136+ packer
137+ end
138+
139+ def pretty_print ( q )
140+ q . text to_s
141+ end
142+ end
159143 end
160144 end
161145end
0 commit comments