threatstack-agent-ruby 0.2.1 → 0.2.2
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 +4 -4
- data/ci/requirements.txt +4 -0
- data/ci/trigger.py +408 -0
- data/ext/libinjection/libinjection.h +9 -0
- data/ext/libinjection/libinjection_pathtraversal.c +43 -0
- data/ext/libinjection/libinjection_pathtraversal_data.h +240 -0
- data/ext/libinjection/libinjection_wrap.c +36 -0
- data/lib/constants.rb +29 -5
- data/lib/control.rb +19 -6
- data/lib/events/event_accumulator.rb +16 -7
- data/lib/events/models/dependency_event.rb +3 -16
- data/lib/instrumentation/common.rb +31 -5
- data/lib/instrumentation/frameworks/kernel.rb +39 -0
- data/lib/instrumentation/frameworks/rails.rb +95 -0
- data/lib/instrumentation/frameworks/random.rb +37 -0
- data/lib/instrumentation/instrumenter.rb +111 -30
- data/lib/jobs/delayed_job.rb +2 -2
- data/lib/jobs/event_submitter.rb +22 -37
- data/lib/jobs/job_queue.rb +14 -5
- data/lib/jobs/recurrent_job.rb +2 -2
- data/lib/utils/capped_queue.rb +39 -0
- data/lib/utils/logger.rb +5 -3
- data/threatstack-agent-ruby.gemspec +3 -2
- metadata +13 -6
- data/lib/instrumentation/kernel.rb +0 -45
- data/lib/instrumentation/rails.rb +0 -61
@@ -24,11 +24,9 @@ module Threatstack
|
|
24
24
|
logger.debug 'Creating dependency event...'
|
25
25
|
args[:event_type] = DEPENDENCIES
|
26
26
|
begin
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
logger.debug "Root Dir: #{root_dir}"
|
31
|
-
@name = File.basename root_dir
|
27
|
+
gemfile_path = File.join(ROOT_DIR, 'Gemfile')
|
28
|
+
lockfile_path = File.join(ROOT_DIR, 'Gemfile.lock')
|
29
|
+
@name = File.basename ROOT_DIR
|
32
30
|
# build dependency list
|
33
31
|
@dependencies = Bundler::Definition.build(gemfile_path, lockfile_path, nil).
|
34
32
|
dependencies.each_with_object({}) do |dep, obj|
|
@@ -66,17 +64,6 @@ module Threatstack
|
|
66
64
|
super args
|
67
65
|
end
|
68
66
|
|
69
|
-
# Returns the root directory of the currently running app
|
70
|
-
def app_root_dir
|
71
|
-
return Bundler.root if defined?(Bundler)
|
72
|
-
|
73
|
-
return ENV['RAILS_ROOT'] if defined?(ENV['RAILS_ROOT']) && ENV['RAILS_ROOT'].to_s.strip.length != 0
|
74
|
-
|
75
|
-
return Rails.root if defined?(Rails) && Rails.root.to_s.strip.length != 0
|
76
|
-
|
77
|
-
Dir.pwd
|
78
|
-
end
|
79
|
-
|
80
67
|
def to_hash
|
81
68
|
hash = to_core_hash
|
82
69
|
hash[:module_name] = AGENT_NAME
|
@@ -28,7 +28,7 @@ module Threatstack
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def self.create_attack_event(payload, type, location, request, headers, backtrace)
|
31
|
-
is_blocked = (type == SQLI && BLOCK_SQLI) || (type == XSS && BLOCK_XSS)
|
31
|
+
is_blocked = (type == SQLI && BLOCK_SQLI) || (type == XSS && BLOCK_XSS) || (type == PATH_TRAVERSAL && BLOCK_PATH_TRAVERSAL)
|
32
32
|
data = {
|
33
33
|
:timestamp => Time.now.utc.strftime('%FT%T.%3NZ'),
|
34
34
|
:module_name => AGENT_NAME,
|
@@ -92,6 +92,16 @@ module Threatstack
|
|
92
92
|
end
|
93
93
|
end
|
94
94
|
|
95
|
+
def self.extract_instrumentation_params(params)
|
96
|
+
module_name = params[:target_class].to_s.downcase
|
97
|
+
method_name = params[:method_name].downcase
|
98
|
+
called_by = params[:caller_loc] ? params[:caller_loc].first : nil
|
99
|
+
file_path = called_by ? called_by.absolute_path : nil
|
100
|
+
line_num = called_by ? called_by.lineno : nil
|
101
|
+
args = params[:args] ? params[:args] : []
|
102
|
+
return module_name, method_name, file_path, line_num, args
|
103
|
+
end
|
104
|
+
|
95
105
|
def self.drop_sensitive_fields(obj)
|
96
106
|
return obj if DROP_FIELDS.nil?
|
97
107
|
|
@@ -127,7 +137,7 @@ module Threatstack
|
|
127
137
|
return false
|
128
138
|
end
|
129
139
|
match = (Libinjection.libinjection_sqli(param, param.length, '') === 1 ? true : false)
|
130
|
-
@@logger.send(match ? :warn : :debug, "SQLI Check #{match ? '
|
140
|
+
@@logger.send(match ? :warn : :debug, "SQLI Check #{match ? 'POSITIVE' : 'negative'} for: #{name}=#{param}") unless name.nil?
|
131
141
|
match
|
132
142
|
end
|
133
143
|
|
@@ -138,7 +148,21 @@ module Threatstack
|
|
138
148
|
end
|
139
149
|
|
140
150
|
match = (Libinjection.libinjection_xss(param, param.length) === 1 ? true : false)
|
141
|
-
@@logger.send(match ? :warn : :debug, "XSS Check #{match ? '
|
151
|
+
@@logger.send(match ? :warn : :debug, "XSS Check #{match ? 'POSITIVE' : 'negative'} for: #{name}=#{param}") unless name.nil?
|
152
|
+
match
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.check_pathtraversal_payload(param, name = nil)
|
156
|
+
# exit early if path traversal checking is disabled
|
157
|
+
return false unless DETECT_PATH_TRAVERSAL
|
158
|
+
|
159
|
+
if param.nil? || !param.kind_of?(String)
|
160
|
+
@@logger.debug "Path Traversal Check skipped for: #{name}" unless name.nil?
|
161
|
+
return false
|
162
|
+
end
|
163
|
+
|
164
|
+
match = (Libinjection.libinjection_pathtraversal(param, param.length) === 1 ? true : false)
|
165
|
+
@@logger.send(match ? :warn : :debug, "Path Traversal Check #{match ? 'POSITIVE' : 'negative'} for: #{name}=#{param}") unless name.nil?
|
142
166
|
match
|
143
167
|
end
|
144
168
|
|
@@ -152,10 +176,11 @@ module Threatstack
|
|
152
176
|
flattened = flatten params
|
153
177
|
|
154
178
|
# check each parameter value for dangerous payloads
|
155
|
-
sqli_found, xss_found = false
|
179
|
+
sqli_found, xss_found, pathtraversal_found = false
|
156
180
|
flattened.each do |key, val|
|
157
181
|
sqli_found = check_sqli_payload(val, key) unless sqli_found
|
158
182
|
xss_found = check_xss_payload(val, key) unless xss_found
|
183
|
+
pathtraversal_found = check_pathtraversal_payload(val, key) unless pathtraversal_found
|
159
184
|
end
|
160
185
|
|
161
186
|
# whether or not to include the payload in the event
|
@@ -164,9 +189,10 @@ module Threatstack
|
|
164
189
|
# create the according attack event if the checks above returned positive
|
165
190
|
create_attack_event(payload, SQLI, location, request, headers, backtrace) if sqli_found
|
166
191
|
create_attack_event(payload, XSS, location, request, headers, backtrace) if xss_found
|
192
|
+
create_attack_event(payload, PATH_TRAVERSAL, location, request, headers, backtrace) if pathtraversal_found
|
167
193
|
|
168
194
|
# return results
|
169
|
-
{ :sqli => sqli_found, :xss => xss_found }
|
195
|
+
{ :sqli => sqli_found, :xss => xss_found, :path_traversal => pathtraversal_found }
|
170
196
|
end
|
171
197
|
end
|
172
198
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../common.rb'
|
4
|
+
require_relative '../instrumenter.rb'
|
5
|
+
require_relative '../../utils/logger'
|
6
|
+
|
7
|
+
module Threatstack
|
8
|
+
module Instrumentation
|
9
|
+
module Frameworks
|
10
|
+
module TSKernel
|
11
|
+
@@logger = Threatstack::Utils::TSLogger.create 'KernelINST'
|
12
|
+
|
13
|
+
# methods to wrap
|
14
|
+
METHOD_NAMES = ['exec', 'system', '`'].freeze
|
15
|
+
|
16
|
+
def self.wrap_methods
|
17
|
+
# executed every time a wrapped method is called
|
18
|
+
on_method_call = Proc.new do |params|
|
19
|
+
module_name, method_name, file_path, line_num, args = Threatstack::Instrumentation.extract_instrumentation_params params
|
20
|
+
# special case for ` method emulation
|
21
|
+
if method_name == '`' && !file_path.nil? && file_path =~ /.*\/kernel\/agnostics\.rb$/
|
22
|
+
called_by = params[:caller_loc][1]
|
23
|
+
file_path = called_by ? called_by.absolute_path : nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# create and queue the event
|
27
|
+
Threatstack::Instrumentation.create_instrumentation_event(module_name, method_name, file_path, line_num, args)
|
28
|
+
end
|
29
|
+
@@logger.info "Instrumenting Kernel methods: #{METHOD_NAMES}"
|
30
|
+
instrumenter = Threatstack::Instrumentation::Instrumenter.instance
|
31
|
+
METHOD_NAMES.each do |method_name|
|
32
|
+
instrumenter.wrap_class_method(Kernel, method_name, &on_method_call)
|
33
|
+
instrumenter.wrap_instance_method(Kernel, method_name, &on_method_call)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require_relative '../../constants'
|
6
|
+
require_relative '../../exceptions/request_blocked_error'
|
7
|
+
require_relative '../../utils/logger'
|
8
|
+
require_relative '../common'
|
9
|
+
|
10
|
+
module Threatstack
|
11
|
+
module Instrumentation
|
12
|
+
module Frameworks
|
13
|
+
module TSRails
|
14
|
+
@@logger = Threatstack::Utils::TSLogger.create 'RailsINST'
|
15
|
+
|
16
|
+
def self.patch_action_controller
|
17
|
+
@@logger.info 'Looking for Rails gem'
|
18
|
+
return unless defined?(::Rails) && defined?(::Rails::VERSION)
|
19
|
+
return unless defined?(ActionController) && defined?(ActionController::Base)
|
20
|
+
|
21
|
+
@@logger.info "Rails #{Rails::VERSION::MAJOR.to_s} gem found, instrumenting ActionController"
|
22
|
+
ActionController::Base.class_eval do
|
23
|
+
include Threatstack::Instrumentation::Frameworks::TSRails::TSActionController
|
24
|
+
end
|
25
|
+
@@logger.info 'Rails instrumentation done'
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.load_application_config
|
29
|
+
return nil unless Gem.loaded_specs.key?('rails') && defined?(Rails)
|
30
|
+
return Rails.configuration if Rails.respond_to?(:configuration)
|
31
|
+
return Rails.application.config if Rails.respond_to?(:application) && Rails.application.respond_to?(:config)
|
32
|
+
return nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.report_application_config
|
36
|
+
@@logger.info 'Checking Rails application config'
|
37
|
+
config = self.load_application_config
|
38
|
+
if config.nil?
|
39
|
+
@@logger.warn 'Rails config not found, skipping config check'
|
40
|
+
return
|
41
|
+
end
|
42
|
+
@@logger.info 'Rails config loaded, relaying relevant values to server'
|
43
|
+
# only send relevant props to the server
|
44
|
+
filtered = { :force_ssl => config.force_ssl }
|
45
|
+
Threatstack::Instrumentation.create_instrumentation_event('rails', 'configuration',
|
46
|
+
Threatstack::Constants::ROOT_DIR, 0, [filtered])
|
47
|
+
end
|
48
|
+
|
49
|
+
module TSActionController
|
50
|
+
include Threatstack::Constants
|
51
|
+
@@logger = Threatstack::Utils::TSLogger.create 'RailsINST'
|
52
|
+
|
53
|
+
def process_action(*args, &block)
|
54
|
+
# we need the headers hack below because Rails adds a lot of internal headers
|
55
|
+
headers = request.headers.each_with_object({}) do |(k, v), obj|
|
56
|
+
obj[k] = v if CGI_VARIABLES.include?(k.to_s) || k =~ /^HTTP_/
|
57
|
+
end
|
58
|
+
@@logger.debug("Incoming request: #{{ :headers => headers, :path => request.path_parameters,
|
59
|
+
:query => Threatstack::Instrumentation.drop_sensitive_fields(request.query_parameters),
|
60
|
+
:body => Threatstack::Instrumentation.drop_sensitive_fields(request.request_parameters) }}")
|
61
|
+
backtrace = caller.join("\n")
|
62
|
+
|
63
|
+
# check path/query/body parameters
|
64
|
+
path_res = Threatstack::Instrumentation.check_parameters(request.path_parameters, 'path', request, headers, backtrace)
|
65
|
+
query_res = Threatstack::Instrumentation.check_parameters(request.query_parameters, 'query', request, headers, backtrace)
|
66
|
+
body_res = Threatstack::Instrumentation.check_parameters(request.request_parameters, 'body', request, headers, backtrace)
|
67
|
+
|
68
|
+
@@logger.debug "RequestStats -- Path: #{path_res}, Query: #{query_res}, Body: #{body_res}"
|
69
|
+
sqli_found = (path_res[:sqli] || query_res[:sqli] || body_res[:sqli])
|
70
|
+
xss_found = (path_res[:xss] || query_res[:xss] || body_res[:xss])
|
71
|
+
pathtraversal_found = (path_res[:path_traversal] || query_res[:path_traversal] || body_res[:path_traversal])
|
72
|
+
# raise an exception if any attack payloads were detected and blocking is enabled
|
73
|
+
if (BLOCK_SQLI && sqli_found) || (BLOCK_XSS && xss_found) || (BLOCK_PATH_TRAVERSAL && pathtraversal_found)
|
74
|
+
raise Threatstack::Exceptions::RequestBlockedError, REQUEST_BLOCKED
|
75
|
+
end
|
76
|
+
|
77
|
+
# continue processing the request normally if no issues were found
|
78
|
+
super(*args, &block)
|
79
|
+
end
|
80
|
+
|
81
|
+
def render(*args, &block)
|
82
|
+
caller_loc = caller_locations(1, 10) ? caller_locations(1, 10).first : nil
|
83
|
+
file_path = caller_loc ? caller_loc.absolute_path : nil
|
84
|
+
line_num = caller_loc ? caller_loc.lineno : nil
|
85
|
+
# report back all parameters
|
86
|
+
Threatstack::Instrumentation.create_instrumentation_event('rails', 'render', file_path, line_num, args)
|
87
|
+
# continue processing the request
|
88
|
+
super(*args, &block)
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../common.rb'
|
4
|
+
require_relative '../instrumenter.rb'
|
5
|
+
require_relative '../../utils/logger'
|
6
|
+
|
7
|
+
module Threatstack
|
8
|
+
module Instrumentation
|
9
|
+
module Frameworks
|
10
|
+
module TSRandom
|
11
|
+
@@logger = Threatstack::Utils::TSLogger.create 'RandomINST'
|
12
|
+
|
13
|
+
# methods to wrap
|
14
|
+
METHOD_NAMES = ['rand'].freeze
|
15
|
+
|
16
|
+
def self.wrap_methods
|
17
|
+
# executed every time a wrapped method is called
|
18
|
+
on_method_call = Proc.new do |params|
|
19
|
+
module_name, method_name, file_path, line_num, args = Threatstack::Instrumentation.extract_instrumentation_params params
|
20
|
+
# create and queue the event
|
21
|
+
Threatstack::Instrumentation.create_instrumentation_event(module_name, method_name, file_path, line_num, args)
|
22
|
+
end
|
23
|
+
@@logger.info "Instrumenting Random methods: #{METHOD_NAMES}"
|
24
|
+
instrumenter = Threatstack::Instrumentation::Instrumenter.instance
|
25
|
+
METHOD_NAMES.each do |method_name|
|
26
|
+
# Random lib
|
27
|
+
instrumenter.wrap_class_method(Random, method_name, &on_method_call)
|
28
|
+
instrumenter.wrap_instance_method(Random, method_name, &on_method_call)
|
29
|
+
# Kernel lib
|
30
|
+
instrumenter.wrap_class_method(Kernel, method_name, &on_method_call)
|
31
|
+
instrumenter.wrap_instance_method(Kernel, method_name, &on_method_call)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -9,6 +9,7 @@ module Threatstack
|
|
9
9
|
include Singleton
|
10
10
|
|
11
11
|
@@logger = Threatstack::Utils::TSLogger.create 'Instrumenter'
|
12
|
+
CLASS_SUFFIX = 'class'
|
12
13
|
|
13
14
|
if RUBY_VERSION < '1.9'
|
14
15
|
def normalize_method_name(method)
|
@@ -20,8 +21,8 @@ module Threatstack
|
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
23
|
-
def self.define_callback(klass, method, &block)
|
24
|
-
backup_name = get_backup_name
|
24
|
+
def self.define_callback(klass, method, suffix = nil, &block)
|
25
|
+
backup_name = get_backup_name(method, suffix)
|
25
26
|
outer_block = block
|
26
27
|
Proc.new do |*args, &block|
|
27
28
|
@@logger.debug "Wrapped method called: #{klass}.#{method}"
|
@@ -38,76 +39,144 @@ module Threatstack
|
|
38
39
|
if is_class_method?(klass, method)
|
39
40
|
wrap_class_method(klass, method, &block)
|
40
41
|
elsif is_instance_method?(klass, method)
|
41
|
-
@@logger.debug "Wrapping instance method: #{klass}.#{method}"
|
42
42
|
wrap_instance_method(klass, method, &block)
|
43
|
+
elsif klass.respond_to?(method, true)
|
44
|
+
wrap_class_method(klass, method, &block)
|
43
45
|
else
|
44
46
|
raise "#{klass}.#{method} is not a class nor instance method"
|
45
47
|
end
|
46
48
|
end
|
47
49
|
|
48
50
|
def wrap_class_method(klass, method, &block)
|
49
|
-
@@logger.debug "
|
51
|
+
@@logger.debug "Attempting wrap of class method: #{klass}.#{method}"
|
50
52
|
original_name = method.to_sym
|
51
|
-
backup_name = get_backup_name
|
52
|
-
wrapped_name = get_wrapped_name
|
53
|
+
backup_name = get_backup_name(original_name, CLASS_SUFFIX)
|
54
|
+
wrapped_name = get_wrapped_name(original_name, CLASS_SUFFIX)
|
55
|
+
|
56
|
+
if method_exists?(klass, backup_name)
|
57
|
+
msg = "#{klass}.#{method} already instrumented"
|
58
|
+
@@logger.error msg
|
59
|
+
raise msg
|
60
|
+
end
|
53
61
|
|
62
|
+
@@logger.debug "Wrapping class method: #{klass}.#{method}"
|
54
63
|
klass.singleton_class.instance_eval do
|
55
64
|
alias_method backup_name, original_name
|
56
65
|
|
57
|
-
p = Instrumenter.define_callback(klass,
|
66
|
+
p = Instrumenter.define_callback(klass, original_name, CLASS_SUFFIX, &block)
|
58
67
|
define_method(wrapped_name, p)
|
59
68
|
|
60
69
|
private wrapped_name
|
61
70
|
|
62
|
-
|
71
|
+
visibility = nil
|
63
72
|
if public_method_defined? original_name
|
64
|
-
|
73
|
+
visibility = :public
|
65
74
|
elsif protected_method_defined? original_name
|
66
|
-
|
75
|
+
visibility = :protected
|
67
76
|
elsif private_method_defined? original_name
|
68
|
-
|
77
|
+
visibility = :private
|
69
78
|
end
|
70
79
|
|
71
80
|
alias_method original_name, wrapped_name
|
72
|
-
__send__(
|
81
|
+
__send__(visibility, original_name)
|
73
82
|
private backup_name
|
74
83
|
end
|
75
84
|
end
|
76
85
|
|
77
86
|
def wrap_instance_method(klass, method, &block)
|
78
|
-
@@logger.debug "
|
79
|
-
|
80
|
-
|
87
|
+
@@logger.debug "Attempting wrap of instance method: #{klass}.#{method}"
|
88
|
+
original_name = method.to_sym
|
89
|
+
backup_name = get_backup_name original_name
|
90
|
+
wrapped_name = get_wrapped_name original_name
|
81
91
|
|
82
|
-
|
83
|
-
|
84
|
-
@@logger.
|
85
|
-
|
92
|
+
if method_exists?(klass, backup_name)
|
93
|
+
msg = "#{klass}.#{method} already instrumented"
|
94
|
+
@@logger.error msg
|
95
|
+
raise msg
|
86
96
|
end
|
87
97
|
|
88
|
-
|
98
|
+
@@logger.debug "Wrapping instance method: #{klass}.#{method}"
|
99
|
+
p = Instrumenter.define_callback(klass, original_name, nil, &block)
|
89
100
|
visibility = nil
|
90
101
|
klass.class_eval do
|
91
|
-
alias_method backup_name,
|
102
|
+
alias_method backup_name, original_name
|
92
103
|
|
93
104
|
define_method(wrapped_name, p)
|
94
105
|
|
95
|
-
if public_method_defined?(
|
106
|
+
if public_method_defined?(original_name)
|
96
107
|
visibility = :public
|
97
|
-
elsif protected_method_defined?(
|
108
|
+
elsif protected_method_defined?(original_name)
|
98
109
|
visibility = :protected
|
99
|
-
elsif private_method_defined?(
|
110
|
+
elsif private_method_defined?(original_name)
|
100
111
|
visibility = :private
|
101
112
|
end
|
102
113
|
|
103
|
-
alias_method
|
114
|
+
alias_method original_name, wrapped_name
|
104
115
|
private backup_name
|
105
116
|
private wrapped_name
|
106
|
-
__send__(visibility,
|
117
|
+
__send__(visibility, original_name)
|
107
118
|
end
|
108
119
|
backup_name
|
109
120
|
end
|
110
121
|
|
122
|
+
def unwrap_method(klass, method)
|
123
|
+
original_name = method.to_sym
|
124
|
+
inst_backup_name = get_backup_name(original_name)
|
125
|
+
class_backup_name = get_backup_name(original_name, CLASS_SUFFIX)
|
126
|
+
# unwrap instance methods of that name
|
127
|
+
if is_instance_method?(klass, inst_backup_name)
|
128
|
+
unwrap_instance_method(klass, original_name)
|
129
|
+
end
|
130
|
+
# unwrap class methods of that name
|
131
|
+
if is_class_method?(klass, class_backup_name)
|
132
|
+
unwrap_class_method(klass, original_name)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def unwrap_class_method(klass, method)
|
137
|
+
@@logger.debug "Unwrapping class method: #{klass}.#{method}"
|
138
|
+
original_name = method.to_sym
|
139
|
+
backup_name = get_backup_name(original_name, CLASS_SUFFIX)
|
140
|
+
visibility = nil
|
141
|
+
|
142
|
+
klass.singleton_class.instance_eval do
|
143
|
+
if public_method_defined?(original_name)
|
144
|
+
visibility = :public
|
145
|
+
elsif protected_method_defined?(original_name)
|
146
|
+
visibility = :protected
|
147
|
+
elsif private_method_defined?(original_name)
|
148
|
+
visibility = :private
|
149
|
+
end
|
150
|
+
unless visibility.nil?
|
151
|
+
alias_method original_name, backup_name
|
152
|
+
__send__(visibility, original_name)
|
153
|
+
remove_method backup_name
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def unwrap_instance_method(klass, method)
|
159
|
+
@@logger.debug "Unwrapping instance method: #{klass}.#{method}"
|
160
|
+
original_name = method.to_sym
|
161
|
+
backup_name = get_backup_name original_name
|
162
|
+
visibility = nil
|
163
|
+
|
164
|
+
klass.class_eval do
|
165
|
+
if public_method_defined?(original_name)
|
166
|
+
visibility = :public
|
167
|
+
elsif protected_method_defined?(original_name)
|
168
|
+
visibility = :protected
|
169
|
+
elsif private_method_defined?(original_name)
|
170
|
+
visibility = :private
|
171
|
+
end
|
172
|
+
unless visibility.nil?
|
173
|
+
alias_method original_name, backup_name
|
174
|
+
__send__(visibility, original_name)
|
175
|
+
remove_method backup_name
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
111
180
|
def self.get_backup_name(method, suffix = nil)
|
112
181
|
"ts_#{method}_backup#{suffix ? "_#{suffix}" : ''}".to_sym
|
113
182
|
end
|
@@ -125,19 +194,31 @@ module Threatstack
|
|
125
194
|
end
|
126
195
|
|
127
196
|
def is_instance_method?(klass, method)
|
197
|
+
return false unless klass.respond_to?(:instance_methods)
|
128
198
|
method = normalize_method_name(method)
|
129
199
|
klass.instance_methods.include?(method) || klass.private_instance_methods.include?(method)
|
130
200
|
end
|
131
201
|
|
132
202
|
def is_class_method?(klass, method)
|
133
203
|
method = normalize_method_name(method)
|
134
|
-
klass.singleton_methods.include? method
|
204
|
+
klass.singleton_methods.include?(method) || klass.respond_to?(method, true)
|
135
205
|
end
|
136
206
|
|
137
207
|
def method_exists?(obj, method)
|
138
|
-
|
139
|
-
|
140
|
-
|
208
|
+
msg = "Method exists? #{obj}.#{method} =>"
|
209
|
+
if obj.nil? || method.nil?
|
210
|
+
@@logger.debug "#{msg} nil"
|
211
|
+
return false
|
212
|
+
end
|
213
|
+
if is_class_method?(obj, method)
|
214
|
+
@@logger.debug "#{msg} class method"
|
215
|
+
return true
|
216
|
+
end
|
217
|
+
if is_instance_method?(obj, method)
|
218
|
+
@@logger.debug "#{msg} instance method"
|
219
|
+
return true
|
220
|
+
end
|
221
|
+
false
|
141
222
|
end
|
142
223
|
end
|
143
224
|
end
|