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.
@@ -24,11 +24,9 @@ module Threatstack
24
24
  logger.debug 'Creating dependency event...'
25
25
  args[:event_type] = DEPENDENCIES
26
26
  begin
27
- root_dir = self.app_root_dir
28
- gemfile_path = File.join(root_dir, 'Gemfile')
29
- lockfile_path = File.join(root_dir, 'Gemfile.lock')
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 ? 'positive' : 'negative'} for: #{name}") unless name.nil?
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 ? 'positive' : 'negative'} for: #{name}") unless name.nil?
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, 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 method
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 "Wrapping class method: #{klass}.#{method}"
51
+ @@logger.debug "Attempting wrap of class method: #{klass}.#{method}"
50
52
  original_name = method.to_sym
51
- backup_name = get_backup_name method
52
- wrapped_name = get_wrapped_name method
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, method, &block)
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
- method_kind = nil
71
+ visibility = nil
63
72
  if public_method_defined? original_name
64
- method_kind = :public
73
+ visibility = :public
65
74
  elsif protected_method_defined? original_name
66
- method_kind = :protected
75
+ visibility = :protected
67
76
  elsif private_method_defined? original_name
68
- method_kind = :private
77
+ visibility = :private
69
78
  end
70
79
 
71
80
  alias_method original_name, wrapped_name
72
- __send__(method_kind, original_name)
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 "Wrapping instance method: #{klass}.#{method}"
79
- backup_name = get_backup_name method
80
- wrapped_name = get_wrapped_name method
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
- private_methods = klass.private_instance_methods(false)
83
- if private_methods.include?(backup_name)
84
- @@logger.debug "#{klass}.#{method} already instrumented"
85
- return backup_name
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
- p = Instrumenter.define_callback(klass, method, &block)
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, method
102
+ alias_method backup_name, original_name
92
103
 
93
104
  define_method(wrapped_name, p)
94
105
 
95
- if public_method_defined?(method)
106
+ if public_method_defined?(original_name)
96
107
  visibility = :public
97
- elsif protected_method_defined?(method)
108
+ elsif protected_method_defined?(original_name)
98
109
  visibility = :protected
99
- elsif private_method_defined?(method)
110
+ elsif private_method_defined?(original_name)
100
111
  visibility = :private
101
112
  end
102
113
 
103
- alias_method method, wrapped_name
114
+ alias_method original_name, wrapped_name
104
115
  private backup_name
105
116
  private wrapped_name
106
- __send__(visibility, method)
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
- return true if is_class_method?(obj, method)
139
- return false unless obj.respond_to?(:instance_methods)
140
- is_instance_method?(obj, method)
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