sqreen 1.17.0 → 1.17.2.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
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
+ module Dependency
6
+ module NewRelic
7
+ module_function
8
+
9
+ def ignore_sqreen_exceptions
10
+ return unless defined?(NewRelic::Agent::Agent)
11
+ NewRelic::Agent::Agent.instance.error_collector.ignore(['Sqreen::AttackBlocked'])
12
+ rescue ::Exception => e # rubocop:disable Lint/RescueException
13
+ Sqreen.log.warn "Failed ignoring AttackBlocked on NewRelic: #{e.inspect}"
14
+ end
15
+ end
16
+ end
17
+ 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
+ module Sqreen
5
+ module Dependency
6
+ module Rack
7
+ module_function
8
+
9
+ def find_handler(&block)
10
+ Sqreen::Dependency::Hook.add('Rack::Server#server') do
11
+ after do |callback, _, server, _|
12
+ block.call(server)
13
+ callback.disable # do this once, :server is a lazy init accessor
14
+ end
15
+ end
16
+ Sqreen::Dependency::Hook['Rack::Server#server'].install
17
+ end
18
+
19
+ def on_run(handler, &block)
20
+ Sqreen.log.debug "[#{Process.pid}] #{handler.inspect}"
21
+ hookpoint_name = "#{handler.name}.run"
22
+
23
+ Sqreen::Dependency::Hook.add(hookpoint_name) do
24
+ before { block.call(handler) }
25
+ end
26
+ Sqreen::Dependency::Hook[hookpoint_name].install
27
+ end
28
+
29
+ def rackup?
30
+ return false if Sqreen::Dependency::Rails.server?
31
+
32
+ Sqreen::Dependency.const_exist?('Rack::Server') && ObjectSpace.each_object(::Rack::Server).count > 0
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
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
+ module Dependency
6
+ module Rails
7
+ module_function
8
+
9
+ def required?
10
+ Sqreen::Dependency.const_exist?('Rails::Application')
11
+ end
12
+
13
+ def server?
14
+ Sqreen::Dependency.const_exist?('Rails::Server') && ObjectSpace.each_object(::Rails::Server).count > 0
15
+ end
16
+
17
+ def inspect_middlewares
18
+ Sqreen.log.debug { "Middlewares: " << ::Rails.application.middleware.map(&:inspect).inspect }
19
+ end
20
+
21
+ def insert_sqreen_middlewares
22
+ Sqreen.log.debug { 'Inserting Sqreen middlewares for Rails' }
23
+ app = ::Rails.application
24
+ app.middleware.insert_after(::Rack::Runtime, Sqreen::Middleware)
25
+ app.middleware.insert_after(::ActionDispatch::DebugExceptions, Sqreen::RailsMiddleware)
26
+ app.middleware.insert_after(::ActionDispatch::DebugExceptions, Sqreen::ErrorHandlingMiddleware)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
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
+ module Dependency
6
+ module Sentry
7
+ module_function
8
+
9
+ def ignore_sqreen_exceptions
10
+ return unless defined?(Raven) && Raven.respond_to?(:configuration)
11
+ Raven.configuration.excluded_exceptions += ['Sqreen::AttackBlocked']
12
+ rescue ::Exception => e # rubocop:disable Lint/RescueException
13
+ Sqreen.log.warn "Failed setting Sentry's excluded_exceptions: #{e.inspect}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -40,4 +40,7 @@ module Sqreen
40
40
 
41
41
  class InvalidSignatureException < Exception
42
42
  end
43
+
44
+ class Unauthorized < Exception
45
+ end
43
46
  end
@@ -1,5 +1,6 @@
1
1
  # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
2
  # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+
3
4
  require 'ipaddr'
4
5
  require 'set'
5
6
 
@@ -17,11 +18,6 @@ module Sqreen
17
18
  attr_accessor :sqreen_configuration
18
19
 
19
20
  def initialize
20
- if defined?(Rack::Builder)
21
- hook_rack_builder
22
- else
23
- to_app_done(Process.pid)
24
- end
25
21
  clean_request_record
26
22
  end
27
23
 
@@ -205,12 +201,6 @@ module Sqreen
205
201
  nil
206
202
  end
207
203
 
208
- # Main entry point for sqreen.
209
- # launch whenever we are ready
210
- def on_start
211
- yield self
212
- end
213
-
214
204
  # Should the agent not be starting up?
215
205
  def prevent_startup
216
206
  return :irb if $0 == 'irb'
@@ -221,23 +211,7 @@ module Sqreen
221
211
 
222
212
  # Instrument with our rules when the framework as finished loading
223
213
  def instrument_when_ready!(instrumentor, rules)
224
- wait_for_to_app do
225
- instrumentor.instrument!(rules, self)
226
- end
227
- end
228
-
229
- def to_app_done(val)
230
- return if @to_app_done
231
- @to_app_done = val
232
- return unless @wait
233
- @wait.each(&:call)
234
- @wait.clear
235
- end
236
-
237
- def wait_for_to_app(&block)
238
- yield && return if @to_app_done
239
- @wait ||= []
240
- @wait << block
214
+ instrumentor.instrument!(rules, self)
241
215
  end
242
216
 
243
217
  # Does the parameters value include this value
@@ -456,15 +430,6 @@ module Sqreen
456
430
  end
457
431
  end
458
432
 
459
- def on_pre_fork_preload?
460
- # unlike with worker_fork_detection, we can't be instrument Puma::Cluster
461
- # in time for intercepting run
462
- stack = Kernel.caller_locations
463
-
464
- puma_preload?(stack) || unicorn_preload?(stack)
465
- end
466
-
467
-
468
433
  protected
469
434
 
470
435
  # Is this a whitelisted path?
@@ -485,22 +450,6 @@ module Sqreen
485
450
  ret.first
486
451
  end
487
452
 
488
- def hook_rack_request(klass)
489
- @calling_pid = Process.pid
490
- klass.class_eval do
491
- define_method(:call_with_sqreen) do |*args, &block|
492
- rv = call_without_sqreen(*args, &block)
493
- if Sqreen.framework.instance_variable_get('@calling_pid') != Process.pid
494
- Sqreen.framework.instance_variable_set('@calling_pid', Process.pid)
495
- yield Sqreen.framework
496
- end
497
- rv
498
- end
499
- alias_method :call_without_sqreen, :call
500
- alias_method :call, :call_with_sqreen
501
- end
502
- end
503
-
504
453
  def hook_rack_builder
505
454
  Rack::Builder.class_eval do
506
455
  define_method(:to_app_with_sqreen) do |*args, &block|
@@ -547,52 +496,8 @@ module Sqreen
547
496
  false
548
497
  end
549
498
 
550
- def sentry_ignore_exceptions
551
- return unless defined?(Raven) && Raven.respond_to?(:configuration)
552
- Raven.configuration.excluded_exceptions += ['Sqreen::AttackBlocked']
553
- rescue
554
- Sqreen.log.warn "Failed setting Sentry's excluded_exceptions: #{e.inspect}"
555
- end
556
-
557
- def newrelic_ignore_errors
558
- return unless defined?(NewRelic::Agent::Agent)
559
- NewRelic::Agent::Agent.instance.
560
- error_collector.ignore(['Sqreen::AttackBlocked'])
561
- rescue
562
- Sqreen.log.warn "Failed ignoring AttackBlocked on NewRelic: #{e.inspect}"
563
- end
564
-
565
- # if it doesn't detect the fork it's not a problem
566
- def worker_fork_detection
567
- # only Puma currently supported
568
- return unless defined?(Puma::Cluster) && Puma::Cluster.instance_methods.include?(:worker)
569
- cur_worker_meth = Puma::Cluster.instance_method(:worker)
570
- Puma::Cluster.class_eval do
571
- define_method(:worker) do |*args|
572
- Sqreen.on_forked_worker = true
573
- cur_worker_meth.bind(self)[*args]
574
- end
575
- end
576
- end
577
-
578
499
  private
579
500
 
580
- def puma_preload?(stack)
581
- cluster_run = stack.each_with_index.find { |b, _i| b.path =~ /puma\/cluster\.rb\z/ && b.label == 'run' }
582
- return false unless cluster_run
583
- frame_atop = stack[cluster_run[1] - 1]
584
- frame_atop.label == 'load_and_bind'
585
- end
586
-
587
- def unicorn_preload?(stack)
588
- build_app = stack.each_with_index.find do |b, _i|
589
- b.path =~ /unicorn\/http_server\.rb\z/ && b.label == 'build_app!'
590
- end
591
- return false unless build_app
592
- frame_below = stack[build_app[1] + 1]
593
- frame_below.label == 'start'
594
- end
595
-
596
501
  def split_ip_addresses(ip_addresses)
597
502
  return [] unless ip_addresses
598
503
 
@@ -15,6 +15,10 @@ module Sqreen
15
15
  'Mysql2' => :mysql,
16
16
  }.freeze
17
17
 
18
+ def initialize
19
+ super
20
+ end
21
+
18
22
  def framework_infos
19
23
  {
20
24
  :framework_type => 'Rails',
@@ -89,19 +93,6 @@ module Sqreen
89
93
  end
90
94
  end
91
95
 
92
- def on_start(&block)
93
- @calling_pid = Process.pid
94
- Init.startup do |app|
95
- worker_fork_detection
96
- sentry_ignore_exceptions
97
- newrelic_ignore_errors
98
- hook_rack_request(app.class, &block)
99
- app.config.after_initialize do
100
- yield self
101
- end
102
- end
103
- end
104
-
105
96
  def prevent_startup
106
97
  res = super
107
98
  return res if res
@@ -15,15 +15,6 @@ module Sqreen
15
15
  h
16
16
  end
17
17
 
18
- def on_start(&block)
19
- worker_fork_detection
20
- sentry_ignore_exceptions
21
- newrelic_ignore_errors
22
- hook_app_build(Sinatra::Base)
23
- hook_rack_request(Sinatra::Application, &block)
24
- yield self
25
- end
26
-
27
18
  def db_settings(options = {})
28
19
  adapter = options[:connection_adapter]
29
20
  return nil unless adapter
@@ -39,22 +30,6 @@ module Sqreen
39
30
  db_infos = { :name => adapter_name }
40
31
  [db_type, db_infos]
41
32
  end
42
-
43
- def hook_app_build(klass)
44
- klass.singleton_class.class_eval do
45
- define_method(:setup_default_middleware_with_sqreen) do |builder|
46
- ret = setup_default_middleware_without_sqreen(builder)
47
- builder.instance_variable_get('@use').insert(2, proc do |app|
48
- # Inject error middle just before sinatra one
49
- Sqreen::ErrorHandlingMiddleware.new(app)
50
- end)
51
- ret
52
- end
53
-
54
- alias_method :setup_default_middleware_without_sqreen, :setup_default_middleware
55
- alias_method :setup_default_middleware, :setup_default_middleware_with_sqreen
56
- end
57
- end
58
33
  end
59
34
  end
60
35
  end
@@ -43,7 +43,6 @@ module Sqreen
43
43
  POST_CB = 'post'.freeze
44
44
  FAILING_CB = 'failing'.freeze
45
45
 
46
- MGMT_COST = 0.000025
47
46
  @@override_semaphore = Mutex.new
48
47
  @@overriden_singleton_methods = false
49
48
 
@@ -115,7 +114,7 @@ module Sqreen
115
114
  Sqreen::PerformanceNotifications.notify(rule || cb.class.name, PRE_CB, start, stop)
116
115
  end
117
116
  all_stop = Sqreen.time
118
- framework.remaining_perf_budget = budget - (all_stop - all_start) - MGMT_COST * callbacks.size if framework && budget
117
+ framework.remaining_perf_budget = budget - (all_stop - all_start) if framework && budget
119
118
  Sqreen::PerformanceNotifications.notify('hooks_pre', PRE_CB, all_start, all_stop)
120
119
  returns
121
120
  #end
@@ -168,7 +167,7 @@ module Sqreen
168
167
  end
169
168
  all_stop = Sqreen.time
170
169
  if framework && budget && framework.remaining_perf_budget
171
- framework.remaining_perf_budget = budget - (all_stop - all_start) - MGMT_COST * callbacks.size
170
+ framework.remaining_perf_budget = budget - (all_stop - all_start)
172
171
  end
173
172
  Sqreen::PerformanceNotifications.notify('hooks_post', POST_CB, all_start, all_stop)
174
173
  returns
@@ -222,7 +221,7 @@ module Sqreen
222
221
  end
223
222
  all_stop = Sqreen.time
224
223
  if framework && budget && framework.remaining_perf_budget
225
- framework.remaining_perf_budget = budget - (all_stop - all_start) - MGMT_COST * callbacks.size
224
+ framework.remaining_perf_budget = budget - (all_stop - all_start)
226
225
  end
227
226
  Sqreen::PerformanceNotifications.notify('hooks_failing', FAILING_CB, all_start, all_stop)
228
227
  returns
@@ -254,6 +253,7 @@ module Sqreen
254
253
  @sqreen_multi_instr ||= nil
255
254
 
256
255
  proc do |*args, &block|
256
+ Sqreen.log.debug { "Calling instrumented #{klass_name} #{original_meth} => #{meth}" }
257
257
  budget = nil
258
258
  skip_call = Thread.current[:sqreen_in_use]
259
259
  begin
@@ -626,6 +626,7 @@ module Sqreen
626
626
  end
627
627
  end
628
628
 
629
+ Sqreen.log.debug "Adding callback #{cb} for #{klass} #{method}"
629
630
  @@registered_callbacks.add(cb)
630
631
  @@unovertimable_hookpoints << key unless cb.overtimeable
631
632
  @@instrumented_pid = Process.pid
@@ -50,18 +50,21 @@ module Sqreen
50
50
  end
51
51
 
52
52
  def pre(inst, args, budget = nil, &_block)
53
+ Sqreen.log.debug { "#{self.class} pre args: #{args.inspect}" }
53
54
  return unless pre?
54
55
 
55
56
  call_callback('pre', budget, inst, @cb_bas['pre'], args)
56
57
  end
57
58
 
58
59
  def post(rv, inst, args, budget = nil, &_block)
60
+ Sqreen.log.debug { "#{self.class} post args: #{args.inspect}" }
59
61
  return unless post?
60
62
 
61
63
  call_callback('post', budget, inst, @cb_bas['post'], args, rv)
62
64
  end
63
65
 
64
66
  def failing(rv, inst, args, budget = nil, &_block)
67
+ Sqreen.log.debug { "#{self.class} failing args: #{args.inspect}" }
65
68
  return unless failing?
66
69
 
67
70
  call_callback('failing', budget, inst, @cb_bas['failing'], args, rv)
@@ -18,6 +18,7 @@ module Sqreen
18
18
  end
19
19
 
20
20
  def pre(_inst, args, _budget = nil, &_block)
21
+ Sqreen.log.debug { "RecordRequestContext pre args: #{args.inspect}" }
21
22
  framework.store_request(args[0])
22
23
  wh = framework.whitelisted_match
23
24
  if wh
@@ -31,12 +32,14 @@ module Sqreen
31
32
  end
32
33
 
33
34
  def post(rv, _inst, args, _budget = nil, &_block)
35
+ Sqreen.log.debug { "RecordRequestContext post args: #{args.inspect}" }
34
36
  framework.store_response(rv, args[0])
35
37
  framework.clean_request
36
38
  advise_action(nil)
37
39
  end
38
40
 
39
- def failing(_exception, _inst, _args, _budget = nil, &_block)
41
+ def failing(_exception, _inst, args, _budget = nil, &_block)
42
+ Sqreen.log.debug { "RecordRequestContext failing args: #{args.inspect}" }
40
43
  framework.clean_request
41
44
  advise_action(nil)
42
45
  end
@@ -58,9 +58,6 @@ module Sqreen
58
58
  attr_accessor :logged_in
59
59
  alias logged_in? logged_in
60
60
 
61
- attr_accessor :on_forked_worker
62
- alias on_forked_worker? on_forked_worker
63
-
64
61
  attr_reader :whitelisted_paths
65
62
  def update_whitelisted_paths(paths)
66
63
  @whitelisted_paths = paths.freeze
@@ -33,9 +33,10 @@ module Sqreen
33
33
  RETRY_CONNECT_SECONDS = 10
34
34
  RETRY_REQUEST_SECONDS = 10
35
35
 
36
- MAX_DELAY = 60 * 30
36
+ MAX_DELAY = 300
37
37
 
38
- RETRY_LONG = 128
38
+ RETRY_FOREVER = :forever
39
+ RETRY_MANY = 301
39
40
 
40
41
  MUTEX = Mutex.new
41
42
  METRICS_KEY = 'metrics'.freeze
@@ -108,14 +109,6 @@ module Sqreen
108
109
  end
109
110
  end
110
111
 
111
- def resilient_post(path, data, headers = {})
112
- post(path, data, headers, RETRY_LONG)
113
- end
114
-
115
- def resilient_get(path, headers = {})
116
- get(path, headers, RETRY_LONG)
117
- end
118
-
119
112
  def post(path, data, headers = {}, max_retry = 2)
120
113
  do_http_request(:POST, path, data, headers, max_retry)
121
114
  end
@@ -133,7 +126,7 @@ module Sqreen
133
126
 
134
127
  current_retry += 1
135
128
 
136
- raise e if current_retry >= max_retry || e.is_a?(Sqreen::NotImplementedYet)
129
+ raise e if max_retry != RETRY_FOREVER && current_retry >= max_retry || e.is_a?(Sqreen::NotImplementedYet) || e.is_a?(Sqreen::Unauthorized)
137
130
 
138
131
  sleep_delay = [MAX_DELAY, retry_request_seconds * current_retry].min
139
132
  Sqreen.log.debug format("Sleeping %ds before retry #{current_retry}/#{max_retry}", sleep_delay)
@@ -173,41 +166,44 @@ module Sqreen
173
166
  path = prefix_path(path)
174
167
  Sqreen.log.debug format('%s %s (%s)', method, path, @token)
175
168
 
176
- res = {}
169
+ payload = {}
177
170
  resiliently(RETRY_REQUEST_SECONDS, max_retry) do
178
- json = nil
171
+ res = nil
179
172
  MUTEX.synchronize do
180
- json = case method.upcase
181
- when :GET
182
- @con.get(path, headers)
183
- when :POST
184
- json_data = nil
185
- unless data.nil?
186
- serialized = Serializer.serialize(data)
187
- json_data = compress(SafeJSON.dump(serialized))
188
- end
189
- @con.post(path, json_data, headers)
190
- else
191
- Sqreen.log.debug format('unknown method %s', method)
192
- raise Sqreen::NotImplementedYet
193
- end
173
+ res = case method.upcase
174
+ when :GET
175
+ @con.get(path, headers)
176
+ when :POST
177
+ json_data = nil
178
+ unless data.nil?
179
+ serialized = Serializer.serialize(data)
180
+ json_data = compress(SafeJSON.dump(serialized))
181
+ end
182
+ @con.post(path, json_data, headers)
183
+ else
184
+ Sqreen.log.debug format('unknown method %s', method)
185
+ raise Sqreen::NotImplementedYet
186
+ end
187
+ end
188
+ if res && res.code == '401'
189
+ raise Sqreen::Unauthorized, 'HTTP 401: shall relogin'
194
190
  end
195
- if json && json.body
196
- if json['Content-Type'] && json['Content-Type'].start_with?('application/json')
197
- res = JSON.parse(json.body)
198
- unless res['status']
199
- Sqreen.log.debug(format('Cannot %s %s. Parsed response body was: %s', method, path, res.inspect))
191
+ if res && res.body
192
+ if res['Content-Type'] && res['Content-Type'].start_with?('application/json')
193
+ payload = JSON.parse(res.body)
194
+ unless payload['status']
195
+ Sqreen.log.debug(format('Cannot %s %s. Parsed response body was: %s', method, path, payload.inspect))
200
196
  end
201
197
  else
202
- Sqreen.log.debug "Unexpected response Content-Type: #{json['Content-Type']}"
203
- Sqreen.log.debug "Unexpected response body: #{json.body.inspect}"
198
+ Sqreen.log.debug "Unexpected response Content-Type: #{res['Content-Type']}"
199
+ Sqreen.log.debug "Unexpected response body: #{res.body.inspect}"
204
200
  end
205
201
  else
206
202
  Sqreen.log.debug 'warning: empty return value'
207
203
  end
208
204
  end
209
205
  Sqreen.log.debug format('%s %s (DONE in %f ms)', method, path, (Time.now.utc - now) * 1000)
210
- res
206
+ payload
211
207
  end
212
208
 
213
209
  def compress(data)
@@ -227,7 +223,7 @@ module Sqreen
227
223
 
228
224
  Sqreen.log.warn "Using app name: #{headers['x-app-name']}"
229
225
 
230
- res = resilient_post('app-login', RuntimeInfos.all(framework), headers)
226
+ res = post('app-login', RuntimeInfos.all(framework), headers, RETRY_FOREVER)
231
227
 
232
228
  if !res || !res['status']
233
229
  public_error = format('Cannot login. Token may be invalid: %s', @token)
@@ -243,7 +239,7 @@ module Sqreen
243
239
  end
244
240
 
245
241
  def rules
246
- resilient_get('rulespack')
242
+ get('rulespack', {}, RETRY_MANY)
247
243
  end
248
244
 
249
245
  def heartbeat(cmd_res = {}, metrics = [])
@@ -251,30 +247,29 @@ module Sqreen
251
247
  payload['metrics'] = metrics unless metrics.nil? || metrics.empty?
252
248
  payload['command_results'] = cmd_res unless cmd_res.nil? || cmd_res.empty?
253
249
 
254
- post('app-beat', payload.empty? ? nil : payload, {}, 5)
250
+ post('app-beat', payload.empty? ? nil : payload, {}, RETRY_MANY)
255
251
  end
256
252
 
257
253
  def post_metrics(metrics)
258
254
  return if metrics.nil? || metrics.empty?
259
255
  payload = { METRICS_KEY => metrics }
260
- resilient_post(METRICS_KEY, payload)
256
+ post(METRICS_KEY, payload, {}, RETRY_MANY)
261
257
  end
262
258
 
263
259
  def post_attack(attack)
264
- resilient_post('attack', attack.to_hash)
260
+ post('attack', attack.to_hash, {}, RETRY_MANY)
265
261
  end
266
262
 
267
263
  def post_bundle(bundle_sig, dependencies)
268
- resilient_post('bundle', 'bundle_signature' => bundle_sig,
269
- 'dependencies' => dependencies)
264
+ post('bundle', { 'bundle_signature' => bundle_sig, 'dependencies' => dependencies }, {}, RETRY_MANY)
270
265
  end
271
266
 
272
267
  def get_actionspack
273
- resilient_get('actionspack')
268
+ get('actionspack', {}, RETRY_MANY)
274
269
  end
275
270
 
276
271
  def post_request_record(request_record)
277
- resilient_post('request_record', request_record.to_hash)
272
+ post('request_record', request_record.to_hash, {}, RETRY_MANY)
278
273
  end
279
274
 
280
275
  # Post an exception to Sqreen for analysis
@@ -300,7 +295,7 @@ module Sqreen
300
295
  tally = Hash[events.group_by(&:class).map{ |k,v| [k, v.count] }]
301
296
  "Doing batch with the following tally of event types: #{tally}"
302
297
  end
303
- resilient_post(BATCH_KEY, BATCH_KEY => batch)
298
+ post(BATCH_KEY, { BATCH_KEY => batch }, {}, RETRY_MANY)
304
299
  end
305
300
 
306
301
  # Perform agent logout