sqreen 0.7.01461158029-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/README.md +77 -0
- data/Rakefile +40 -0
- data/lib/sqreen.rb +67 -0
- data/lib/sqreen/binding_accessor.rb +184 -0
- data/lib/sqreen/ca.crt +72 -0
- data/lib/sqreen/callback_tree.rb +78 -0
- data/lib/sqreen/callbacks.rb +120 -0
- data/lib/sqreen/capped_queue.rb +23 -0
- data/lib/sqreen/condition_evaluator.rb +169 -0
- data/lib/sqreen/conditionable.rb +50 -0
- data/lib/sqreen/configuration.rb +151 -0
- data/lib/sqreen/context.rb +22 -0
- data/lib/sqreen/deliveries/batch.rb +80 -0
- data/lib/sqreen/deliveries/simple.rb +36 -0
- data/lib/sqreen/detect.rb +14 -0
- data/lib/sqreen/detect/shell_injection.rb +61 -0
- data/lib/sqreen/detect/sql_injection.rb +115 -0
- data/lib/sqreen/event.rb +16 -0
- data/lib/sqreen/events/attack.rb +60 -0
- data/lib/sqreen/events/remote_exception.rb +53 -0
- data/lib/sqreen/exception.rb +31 -0
- data/lib/sqreen/frameworks.rb +40 -0
- data/lib/sqreen/frameworks/generic.rb +243 -0
- data/lib/sqreen/frameworks/rails.rb +155 -0
- data/lib/sqreen/frameworks/rails3.rb +36 -0
- data/lib/sqreen/frameworks/sinatra.rb +34 -0
- data/lib/sqreen/frameworks/sqreen_test.rb +26 -0
- data/lib/sqreen/instrumentation.rb +504 -0
- data/lib/sqreen/log.rb +116 -0
- data/lib/sqreen/metrics.rb +6 -0
- data/lib/sqreen/metrics/average.rb +39 -0
- data/lib/sqreen/metrics/base.rb +41 -0
- data/lib/sqreen/metrics/collect.rb +22 -0
- data/lib/sqreen/metrics/sum.rb +20 -0
- data/lib/sqreen/metrics_store.rb +94 -0
- data/lib/sqreen/parsers/sql.rb +98 -0
- data/lib/sqreen/parsers/sql_tokenizer.rb +266 -0
- data/lib/sqreen/parsers/unix.rb +110 -0
- data/lib/sqreen/payload_creator.rb +132 -0
- data/lib/sqreen/performance_notifications.rb +86 -0
- data/lib/sqreen/performance_notifications/log.rb +36 -0
- data/lib/sqreen/performance_notifications/metrics.rb +36 -0
- data/lib/sqreen/performance_notifications/newrelic.rb +36 -0
- data/lib/sqreen/remote_command.rb +82 -0
- data/lib/sqreen/rule_attributes.rb +25 -0
- data/lib/sqreen/rule_callback.rb +97 -0
- data/lib/sqreen/rules.rb +116 -0
- data/lib/sqreen/rules_callbacks.rb +29 -0
- data/lib/sqreen/rules_callbacks/binding_accessor_metrics.rb +79 -0
- data/lib/sqreen/rules_callbacks/count_http_codes.rb +18 -0
- data/lib/sqreen/rules_callbacks/crawler_user_agent_matches.rb +24 -0
- data/lib/sqreen/rules_callbacks/crawler_user_agent_matches_metrics.rb +25 -0
- data/lib/sqreen/rules_callbacks/execjs.rb +136 -0
- data/lib/sqreen/rules_callbacks/headers_insert.rb +20 -0
- data/lib/sqreen/rules_callbacks/inspect_rule.rb +20 -0
- data/lib/sqreen/rules_callbacks/matcher_rule.rb +103 -0
- data/lib/sqreen/rules_callbacks/rails_parameters.rb +14 -0
- data/lib/sqreen/rules_callbacks/record_request_context.rb +23 -0
- data/lib/sqreen/rules_callbacks/reflected_xss.rb +40 -0
- data/lib/sqreen/rules_callbacks/regexp_rule.rb +36 -0
- data/lib/sqreen/rules_callbacks/shell.rb +33 -0
- data/lib/sqreen/rules_callbacks/shell_env.rb +32 -0
- data/lib/sqreen/rules_callbacks/sql.rb +41 -0
- data/lib/sqreen/rules_callbacks/system_shell.rb +25 -0
- data/lib/sqreen/rules_callbacks/url_matches.rb +25 -0
- data/lib/sqreen/rules_callbacks/user_agent_matches.rb +22 -0
- data/lib/sqreen/rules_signature.rb +142 -0
- data/lib/sqreen/runner.rb +312 -0
- data/lib/sqreen/runtime_infos.rb +127 -0
- data/lib/sqreen/session.rb +340 -0
- data/lib/sqreen/stats.rb +18 -0
- data/lib/sqreen/version.rb +6 -0
- metadata +143 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
module Sqreen
|
5
|
+
# Context
|
6
|
+
class Context
|
7
|
+
attr_accessor :bt
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@bt = get_current_backtrace
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_current_backtrace
|
14
|
+
# Force caller to be resolved now
|
15
|
+
caller.map(&:to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
other.bt == @bt
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'sqreen/deliveries/simple'
|
5
|
+
require 'sqreen/events/remote_exception'
|
6
|
+
module Sqreen
|
7
|
+
module Deliveries
|
8
|
+
# Simple delivery method that batch event already seen in a batch
|
9
|
+
class Batch < Simple
|
10
|
+
attr_accessor :max_batch, :max_staleness
|
11
|
+
attr_accessor :current_batch
|
12
|
+
|
13
|
+
def initialize(session,
|
14
|
+
max_batch,
|
15
|
+
max_staleness,
|
16
|
+
randomize_staleness = true)
|
17
|
+
super(session)
|
18
|
+
self.max_batch = max_batch
|
19
|
+
self.max_staleness = max_staleness
|
20
|
+
@original_max_staleness = max_staleness
|
21
|
+
self.current_batch = []
|
22
|
+
@first_seen = {}
|
23
|
+
@randomize_staleness = randomize_staleness
|
24
|
+
end
|
25
|
+
|
26
|
+
def post_event(event)
|
27
|
+
current_batch.push(event)
|
28
|
+
post_batch if post_batch_needed?(event)
|
29
|
+
end
|
30
|
+
|
31
|
+
def drain
|
32
|
+
post_batch
|
33
|
+
end
|
34
|
+
|
35
|
+
def tick
|
36
|
+
post_batch if current_batch.size > 0 && stale?
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def stale?
|
42
|
+
min = @first_seen.values.min
|
43
|
+
(min + max_staleness) < Time.now
|
44
|
+
end
|
45
|
+
|
46
|
+
def post_batch_needed?(event)
|
47
|
+
key = event_key(event)
|
48
|
+
was = @first_seen[key]
|
49
|
+
now = Time.now
|
50
|
+
@first_seen[key] ||= now
|
51
|
+
return true if was.nil?
|
52
|
+
return true if current_batch.size > max_batch
|
53
|
+
return true if (was + max_staleness) < now
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
def post_batch
|
58
|
+
session.post_batch(current_batch)
|
59
|
+
current_batch.clear
|
60
|
+
now = Time.now
|
61
|
+
@first_seen.each_key do |key|
|
62
|
+
@first_seen[key] = now
|
63
|
+
end
|
64
|
+
return unless @randomize_staleness
|
65
|
+
self.max_staleness = @original_max_staleness
|
66
|
+
# Adds up to 10% of lateness
|
67
|
+
self.max_staleness += rand(@original_max_staleness / 10)
|
68
|
+
end
|
69
|
+
|
70
|
+
def event_key(event)
|
71
|
+
case event
|
72
|
+
when Sqreen::Attack
|
73
|
+
return "att-#{event.type}"
|
74
|
+
when Sqreen::RemoteException
|
75
|
+
return "rex-#{event.klass}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'sqreen/events/remote_exception'
|
5
|
+
|
6
|
+
module Sqreen
|
7
|
+
module Deliveries
|
8
|
+
# Simple delivery method that directly call session on event
|
9
|
+
class Simple
|
10
|
+
attr_accessor :session
|
11
|
+
|
12
|
+
def initialize(session)
|
13
|
+
self.session = session
|
14
|
+
end
|
15
|
+
|
16
|
+
def post_event(event)
|
17
|
+
case event
|
18
|
+
when Sqreen::Attack
|
19
|
+
session.post_attack(event)
|
20
|
+
when Sqreen::RemoteException
|
21
|
+
session.post_sqreen_exception(event)
|
22
|
+
else
|
23
|
+
session.post_event(event)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def drain
|
28
|
+
# Since everything is posted at once nothing needs to be done here
|
29
|
+
end
|
30
|
+
|
31
|
+
def tick
|
32
|
+
# Since everything is posted at once nothing needs to be done here
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'sqreen/detect/sql_injection'
|
5
|
+
require 'sqreen/detect/shell_injection'
|
6
|
+
|
7
|
+
module Sqreen
|
8
|
+
module Detect
|
9
|
+
def sql_injection?(request, params, db_type, db_infos = {})
|
10
|
+
inj = SQLInjection.new(db_type, db_infos)
|
11
|
+
inj.user_escape?(request, params)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'sqreen/parsers/unix'
|
5
|
+
|
6
|
+
module Sqreen
|
7
|
+
module Detect
|
8
|
+
# Detector class for shell injections
|
9
|
+
# Find instance of user parameters injections into executable commands
|
10
|
+
# It work by:
|
11
|
+
# 1 - Highlighting the cmd for executable sections
|
12
|
+
# 2 - Highlighting the cmd for traces of user parameters
|
13
|
+
# 3 - Comparing if there is any intersection
|
14
|
+
class ShellInjection
|
15
|
+
def initialize
|
16
|
+
@parser = Sqreen::Parsers::Unix.new
|
17
|
+
end
|
18
|
+
|
19
|
+
# Is there a user injection in cmd
|
20
|
+
# @param cmd [String] command to analyze
|
21
|
+
# @param params [Hash] Hash of user parameters
|
22
|
+
def user_escape?(cmd, params)
|
23
|
+
Sqreen.log.info format('escape? %s', [cmd, params].inspect)
|
24
|
+
|
25
|
+
# We found the user query inside the cmd. A risk exists.
|
26
|
+
@parser.parse(cmd)
|
27
|
+
execs = @parser.atoms.select(&:executable?)
|
28
|
+
|
29
|
+
each_param_scalar(params) do |v|
|
30
|
+
next unless v
|
31
|
+
value = v.to_s
|
32
|
+
next unless value.size > 0
|
33
|
+
offset = 0
|
34
|
+
loop do
|
35
|
+
match_start = cmd.index(value, offset)
|
36
|
+
break if match_start.nil?
|
37
|
+
match_end = match_start + value.size
|
38
|
+
offset = match_end
|
39
|
+
covered = execs.any? do |exec|
|
40
|
+
match_end >= exec.start && match_start < exec.end
|
41
|
+
end
|
42
|
+
next unless covered
|
43
|
+
Sqreen.log.info format('injection for parameter %s', value.inspect)
|
44
|
+
return true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
# FIXME: deduplicate code
|
51
|
+
def each_param_scalar(params, &block)
|
52
|
+
case params
|
53
|
+
when Hash then params.each { |_k, v| each_param_scalar(v, &block) }
|
54
|
+
when Array then params.each { |v| each_param_scalar(v, &block) }
|
55
|
+
else
|
56
|
+
yield params
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'sqreen/parsers/sql'
|
5
|
+
require 'strscan'
|
6
|
+
|
7
|
+
module Sqreen
|
8
|
+
module Detect
|
9
|
+
class SQLInjection
|
10
|
+
PARAM_SIZE_LIMIT = 0
|
11
|
+
|
12
|
+
def self.parser(db_type, db_infos)
|
13
|
+
@parsers ||= {}
|
14
|
+
res = nil
|
15
|
+
results = @parsers[db_type]
|
16
|
+
res = results.find { |infos, _| infos == db_infos } unless results.nil?
|
17
|
+
return res.last unless res.nil?
|
18
|
+
@parsers[db_type] ||= []
|
19
|
+
parser = Sqreen::Parsers::SQL.new(db_type, db_infos)
|
20
|
+
@parsers[db_type] << [db_infos, parser]
|
21
|
+
parser
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_accessor :db_type, :db_infos
|
25
|
+
def initialize(db_type, db_infos)
|
26
|
+
@db_type = db_type
|
27
|
+
@db_infos = db_infos
|
28
|
+
@parser = SQLInjection.parser(db_type, db_infos)
|
29
|
+
end
|
30
|
+
|
31
|
+
# FIXME: we are likely to find false postive
|
32
|
+
# As is, high risk of false positive with extremely short parameters.
|
33
|
+
# E.g. if parameter value is 'e', it will be found in request, (e in
|
34
|
+
# select) but not in literals.
|
35
|
+
#
|
36
|
+
# We may want to skip:
|
37
|
+
# - too short parameters (e.g. < 10 letters?)
|
38
|
+
# - non suspicious parameters (e.g. without blanks or comments?)
|
39
|
+
def user_escape?(request, params)
|
40
|
+
included = count_user_params_in_request(request, params)
|
41
|
+
return false if included == {}
|
42
|
+
|
43
|
+
escape_found = false
|
44
|
+
|
45
|
+
# We found the user query inside the request. A risk exists.
|
46
|
+
@parser.parse(request)
|
47
|
+
literals = @parser.atoms.select(&:is_literal?)
|
48
|
+
included.each do |param, expected_count|
|
49
|
+
param_count = 0
|
50
|
+
literals.each do |literal|
|
51
|
+
# Count number of literals that fully include the user query
|
52
|
+
param_count += count_substring_nb(literal.val, param)
|
53
|
+
end
|
54
|
+
|
55
|
+
# puts "%s in raw request: %d, in atoms: %d" % [param, expected_count, param_count]
|
56
|
+
next unless param_count != expected_count
|
57
|
+
Sqreen.log.info format('injection for parameter %s', param.inspect)
|
58
|
+
# require request aborption
|
59
|
+
# log attack
|
60
|
+
escape_found = true
|
61
|
+
end
|
62
|
+
escape_found
|
63
|
+
end
|
64
|
+
|
65
|
+
# What if a string can be prefixed itself?
|
66
|
+
# E.g. substr = 'a b c a b c'
|
67
|
+
# If str = 'a b c a b c a b c' we will return 2:
|
68
|
+
# \----1----/
|
69
|
+
# \----2----/
|
70
|
+
def count_substring_nb(str, substr)
|
71
|
+
s = StringScanner.new(str)
|
72
|
+
nb = 0
|
73
|
+
quote = Regexp.quote(substr)
|
74
|
+
re = Regexp.new(quote)
|
75
|
+
nb += 1 while s.search_full(re, true, false)
|
76
|
+
nb
|
77
|
+
end
|
78
|
+
|
79
|
+
def each_param_scalar(params, &block)
|
80
|
+
case params
|
81
|
+
when Hash then params.each { |_k, v| each_param_scalar(v, &block) }
|
82
|
+
when Array then params.each { |v| each_param_scalar(v, &block) }
|
83
|
+
else
|
84
|
+
yield params
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# FIXME: this work on params values. We might wnat to work on parameters
|
89
|
+
# names? High risk of false positive since a parameter name is often the
|
90
|
+
# database column name.
|
91
|
+
def count_user_params_in_request(request, params_hash)
|
92
|
+
res = {}
|
93
|
+
params_hash.each do |_type, params|
|
94
|
+
next if params.nil?
|
95
|
+
each_param_scalar(params) do |value|
|
96
|
+
next unless value
|
97
|
+
v = value.to_s
|
98
|
+
next if v.size <= PARAM_SIZE_LIMIT
|
99
|
+
next if v =~ /\A\.+\z/
|
100
|
+
next if v =~ /\A\s+\z/
|
101
|
+
next if v =~ /\A(\w+|\w[\w\.]+\w)\z/i
|
102
|
+
|
103
|
+
# We need to overwrite the count of equal parameters that
|
104
|
+
# came from different ways (e.g. Cookie and query).
|
105
|
+
next if res.key? v
|
106
|
+
nb = count_substring_nb(request, v)
|
107
|
+
|
108
|
+
res[v] = nb if nb > 0
|
109
|
+
end
|
110
|
+
end
|
111
|
+
res
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/sqreen/event.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
module Sqreen
|
5
|
+
# Master interface for point in time events (e.g. Attack, RemoteException)
|
6
|
+
class Event
|
7
|
+
attr_reader :payload
|
8
|
+
def initialize(payload)
|
9
|
+
@payload = payload
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_hash
|
13
|
+
payload.to_hash
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'sqreen/event'
|
5
|
+
|
6
|
+
module Sqreen
|
7
|
+
# Attack
|
8
|
+
# When creating a new attack, it gets automatically pushed to the event's
|
9
|
+
# queue.
|
10
|
+
class Attack < Event
|
11
|
+
def self.record(payload)
|
12
|
+
attack = Attack.new(payload)
|
13
|
+
attack.enqueue
|
14
|
+
end
|
15
|
+
|
16
|
+
def infos
|
17
|
+
payload['infos']
|
18
|
+
end
|
19
|
+
|
20
|
+
def rulespack_id
|
21
|
+
return nil unless payload['rule']
|
22
|
+
payload['rule']['rulespack_id']
|
23
|
+
end
|
24
|
+
|
25
|
+
def type
|
26
|
+
return nil unless payload['rule']
|
27
|
+
payload['rule']['name']
|
28
|
+
end
|
29
|
+
|
30
|
+
def time
|
31
|
+
return nil unless payload['local']
|
32
|
+
payload['local']['time'].to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def backtrace
|
36
|
+
return nil unless payload['context']
|
37
|
+
payload['context']['backtrace']
|
38
|
+
end
|
39
|
+
|
40
|
+
def enqueue
|
41
|
+
Sqreen.queue.push(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_hash
|
45
|
+
res = {}
|
46
|
+
rule_p = payload['rule']
|
47
|
+
request_p = payload['request']
|
48
|
+
res[:rule_name] = rule_p['name'] if rule_p && rule_p['name']
|
49
|
+
res[:rulespack_id] = rule_p['rulespack_id'] if rule_p && rule_p['rulespack_id']
|
50
|
+
res[:test] = rule_p['test'] if rule_p && rule_p['test']
|
51
|
+
res[:infos] = payload['infos'] if payload['infos']
|
52
|
+
res[:time] = time if time
|
53
|
+
res[:client_ip] = request_p[:addr] if request_p && request_p[:addr]
|
54
|
+
res[:request] = request_p if request_p
|
55
|
+
res[:params] = payload['params'] if payload['params']
|
56
|
+
res[:context] = payload['context'] if payload['context']
|
57
|
+
res
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'sqreen/event'
|
6
|
+
|
7
|
+
module Sqreen
|
8
|
+
# When an exception arise it is automatically pushed to the event queue
|
9
|
+
class RemoteException < Sqreen::Event
|
10
|
+
def self.record(payload_or_exception)
|
11
|
+
exception = RemoteException.new(payload_or_exception)
|
12
|
+
exception.enqueue
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(payload_or_exception)
|
16
|
+
payload = if payload_or_exception.is_a?(Hash)
|
17
|
+
payload_or_exception
|
18
|
+
else
|
19
|
+
{ 'exception' => payload_or_exception }
|
20
|
+
end
|
21
|
+
super(payload)
|
22
|
+
end
|
23
|
+
|
24
|
+
def enqueue
|
25
|
+
Sqreen.queue.push(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
def klass
|
29
|
+
payload['exception'].class.name
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_hash
|
33
|
+
exception = payload['exception']
|
34
|
+
ev = {
|
35
|
+
:klass => exception.class.name,
|
36
|
+
:message => exception.message,
|
37
|
+
:params => payload['request_params'],
|
38
|
+
:time => payload['time'],
|
39
|
+
:infos => {
|
40
|
+
:client_ip => payload['client_ip'],
|
41
|
+
},
|
42
|
+
:request => payload['request_infos'],
|
43
|
+
:rule_name => payload['rule_name'],
|
44
|
+
:rulespack_id => payload['rulespack_id'],
|
45
|
+
}
|
46
|
+
|
47
|
+
ev[:infos].merge!(payload['infos']) if payload['infos']
|
48
|
+
return ev unless exception.backtrace
|
49
|
+
ev[:context] = { :backtrace => exception.backtrace.map(&:to_s) }
|
50
|
+
ev
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|