-
Notifications
You must be signed in to change notification settings - Fork 386
Add stack trace collection support #4269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ed939c9
ad56711
698a84c
6168e5d
9df943a
1915530
61a2970
c45f184
a40b791
630c10d
a3ce0bb
20d026a
110e1eb
094cc5e
1cbf501
7cfb7b8
c60706a
d68d25b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# frozen_string_literal: true | ||
|
||
module Datadog | ||
module AppSec | ||
module ActionsHandler | ||
# Module that collects the stack trace into formatted hash | ||
module StackTraceCollection | ||
module_function | ||
|
||
def collect(max_depth:, top_percent:) | ||
locations = StackTraceCollection.filter_map_datadog_locations(caller_locations || []) | ||
|
||
return [] if locations.empty? | ||
return StackTraceCollection.convert(locations) if max_depth.zero? || locations.size <= max_depth | ||
|
||
top_limit = (max_depth * top_percent / 100.0).round | ||
bottom_limit = locations.size - (max_depth - top_limit) | ||
|
||
locations.slice!(top_limit...bottom_limit) | ||
Comment on lines
+16
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is What are we trying to do here? Get the top n items from the array? Do we really need to calculate both limits, or can we just slice n items from one end? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For example, if max_depth is 32 and top_percent is 75%, and we have more than 32 stack frames, we want to keep the 24th first stack traces, the 8th last, and drop everything in the middle. So what this method do is calculate the limits of the frames that we should drop, so we can just do a slice on the locations with that range There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. according to RFC, we have to take 2 slices - one from the top of the stack trace, and one from the bottom:
I think this is not what we are doing here? |
||
StackTraceCollection.convert(locations) | ||
end | ||
|
||
def filter_map_datadog_locations(locations) | ||
locations.each_with_object([]) do |location, result| | ||
text = location.to_s | ||
next if text.include?('lib/datadog') | ||
|
||
result << { | ||
text: text, | ||
file: location.absolute_path || location.path, | ||
line: location.lineno, | ||
function: location.label | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RFC specifies separate |
||
} | ||
end | ||
end | ||
|
||
def convert(locations) | ||
locations.each_with_index do |location, index| | ||
location[:id] = index | ||
# Strings can be frozen so we need to copy them | ||
location[:text] = location[:text].encode('UTF-8') | ||
location[:file] = location[:file]&.encode('UTF-8') | ||
location[:function] = location[:function]&.encode('UTF-8') | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# frozen_string_literal: true | ||
|
||
module Datadog | ||
module AppSec | ||
module ActionsHandler | ||
# Object that holds a metastruct, and modify the exploit group stack traces | ||
class StackTraceInMetastruct | ||
# Implementation with empty metastruct | ||
class Noop | ||
def count | ||
0 | ||
end | ||
|
||
def push(_) | ||
nil | ||
end | ||
end | ||
|
||
def self.create(metastruct) | ||
metastruct.nil? ? Noop.new : new(metastruct) | ||
end | ||
|
||
def initialize(metastruct) | ||
@metastruct = metastruct | ||
end | ||
|
||
def count | ||
@metastruct.dig(AppSec::Ext::TAG_STACK_TRACE, AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY)&.size || 0 | ||
end | ||
|
||
def push(stack_trace) | ||
@metastruct[AppSec::Ext::TAG_STACK_TRACE] ||= {} | ||
@metastruct[AppSec::Ext::TAG_STACK_TRACE][AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY] ||= [] | ||
@metastruct[AppSec::Ext::TAG_STACK_TRACE][AppSec::Ext::EXPLOIT_PREVENTION_EVENT_CATEGORY] << stack_trace | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'forwardable' | ||
|
||
module Datadog | ||
module Tracing | ||
module Metadata | ||
# Adds complex structures tagging behavior through metastruct | ||
class Metastruct | ||
extend Forwardable | ||
|
||
MERGER = proc do |_, v1, v2| | ||
if v1.is_a?(Hash) && v2.is_a?(Hash) | ||
v1.merge(v2, &MERGER) | ||
elsif v1.is_a?(Array) && v2.is_a?(Array) | ||
v1.concat(v2) | ||
elsif v2.nil? | ||
v1 | ||
else | ||
v2 | ||
end | ||
end | ||
|
||
def self.empty | ||
new({}) | ||
end | ||
|
||
def initialize(metastruct) | ||
@metastruct = metastruct | ||
end | ||
|
||
# Deep merge two metastructs | ||
# If the types are not both Arrays or Hashes, the second one will overwrite the first one | ||
# | ||
# Example with same types: | ||
# metastruct = { a: { b: [1, 2] } } | ||
# second = { a: { b: [3, 4], c: 5 } } | ||
# result = { a: { b: [1, 2, 3, 4], c: 5 } } | ||
# | ||
# Example with different types: | ||
# metastruct = { a: { b: 1 } } | ||
# second = { a: { b: [2, 3] } } | ||
# result = { a: { b: [2, 3] } } | ||
def deep_merge!(second) | ||
@metastruct.merge!(second.to_h, &MERGER) | ||
end | ||
|
||
def_delegators :@metastruct, :[], :[]=, :dig, :to_h | ||
|
||
def pretty_print(q) | ||
q.seplist @metastruct.each do |key, value| | ||
q.text "#{key} => #{value}\n" | ||
end | ||
end | ||
|
||
def to_msgpack(packer = nil) | ||
packer ||= MessagePack::Packer.new | ||
|
||
packer.write(@metastruct.transform_values(&:to_msgpack)) | ||
end | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to avoid stack trace collection if the stack is longer than
max_collect
, or do we want to truncate the stack?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
max_collect is the maximum number of stack traces that we want in our trace, not the maximum number of frames in each stack trace (which is defined by max_depth). So if we already have the maximum number of stack traces in metastruct, we want to avoid collection.