sqreen 1.20.4.beta1 → 1.21.0

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/lib/sqreen/condition_evaluator.rb +8 -2
  4. data/lib/sqreen/deliveries/batch.rb +8 -1
  5. data/lib/sqreen/ecosystem.rb +123 -0
  6. data/lib/sqreen/ecosystem/databases/database_connection_data.rb +23 -0
  7. data/lib/sqreen/ecosystem/databases/mongo.rb +39 -0
  8. data/lib/sqreen/ecosystem/databases/mysql.rb +54 -0
  9. data/lib/sqreen/ecosystem/databases/postgres.rb +51 -0
  10. data/lib/sqreen/ecosystem/databases/redis.rb +36 -0
  11. data/lib/sqreen/ecosystem/dispatch_table.rb +43 -0
  12. data/lib/sqreen/ecosystem/exception_reporting.rb +28 -0
  13. data/lib/sqreen/ecosystem/http/net_http.rb +50 -0
  14. data/lib/sqreen/ecosystem/http/rack_request.rb +39 -0
  15. data/lib/sqreen/ecosystem/loggable.rb +13 -0
  16. data/lib/sqreen/ecosystem/messaging/bunny.rb +61 -0
  17. data/lib/sqreen/ecosystem/messaging/kafka.rb +70 -0
  18. data/lib/sqreen/ecosystem/messaging/kinesis.rb +66 -0
  19. data/lib/sqreen/ecosystem/messaging/sqs.rb +68 -0
  20. data/lib/sqreen/ecosystem/module_api.rb +30 -0
  21. data/lib/sqreen/ecosystem/module_api/event_listener.rb +18 -0
  22. data/lib/sqreen/ecosystem/module_api/instrumentation.rb +23 -0
  23. data/lib/sqreen/ecosystem/module_api/message_producer.rb +57 -0
  24. data/lib/sqreen/ecosystem/module_api/signal_producer.rb +24 -0
  25. data/lib/sqreen/ecosystem/module_api/tracing.rb +45 -0
  26. data/lib/sqreen/ecosystem/module_api/tracing/client_data.rb +31 -0
  27. data/lib/sqreen/ecosystem/module_api/tracing/consumer_data.rb +13 -0
  28. data/lib/sqreen/ecosystem/module_api/tracing/messaging_data.rb +35 -0
  29. data/lib/sqreen/ecosystem/module_api/tracing/producer_data.rb +13 -0
  30. data/lib/sqreen/ecosystem/module_api/tracing/server_data.rb +27 -0
  31. data/lib/sqreen/ecosystem/module_api/tracing_id_generation.rb +16 -0
  32. data/lib/sqreen/ecosystem/module_api/transaction_storage.rb +71 -0
  33. data/lib/sqreen/ecosystem/module_registry.rb +48 -0
  34. data/lib/sqreen/ecosystem/tracing/modules/client.rb +35 -0
  35. data/lib/sqreen/ecosystem/tracing/modules/consumer.rb +35 -0
  36. data/lib/sqreen/ecosystem/tracing/modules/determine_ip.rb +28 -0
  37. data/lib/sqreen/ecosystem/tracing/modules/producer.rb +35 -0
  38. data/lib/sqreen/ecosystem/tracing/modules/server.rb +30 -0
  39. data/lib/sqreen/ecosystem/tracing/sampler.rb +160 -0
  40. data/lib/sqreen/ecosystem/tracing/sampling_configuration.rb +150 -0
  41. data/lib/sqreen/ecosystem/tracing/signals/tracing_client.rb +53 -0
  42. data/lib/sqreen/ecosystem/tracing/signals/tracing_consumer.rb +56 -0
  43. data/lib/sqreen/ecosystem/tracing/signals/tracing_producer.rb +56 -0
  44. data/lib/sqreen/ecosystem/tracing/signals/tracing_server.rb +53 -0
  45. data/lib/sqreen/ecosystem/tracing_broker.rb +101 -0
  46. data/lib/sqreen/ecosystem/tracing_id_setup.rb +34 -0
  47. data/lib/sqreen/ecosystem/transaction_storage.rb +64 -0
  48. data/lib/sqreen/ecosystem/util/call_writers_from_init.rb +13 -0
  49. data/lib/sqreen/ecosystem_integration.rb +81 -0
  50. data/lib/sqreen/ecosystem_integration/around_callbacks.rb +89 -0
  51. data/lib/sqreen/ecosystem_integration/instrumentation_service.rb +38 -0
  52. data/lib/sqreen/ecosystem_integration/request_lifecycle_tracking.rb +58 -0
  53. data/lib/sqreen/ecosystem_integration/signal_consumption.rb +35 -0
  54. data/lib/sqreen/events/request_record.rb +0 -1
  55. data/lib/sqreen/frameworks/generic.rb +15 -1
  56. data/lib/sqreen/frameworks/request_recorder.rb +2 -0
  57. data/lib/sqreen/graft/call.rb +9 -0
  58. data/lib/sqreen/graft/hook.rb +5 -3
  59. data/lib/sqreen/graft/hook_point.rb +17 -10
  60. data/lib/sqreen/kit/signals/specialized/sqreen_exception.rb +2 -0
  61. data/lib/sqreen/legacy/old_event_submission_strategy.rb +9 -2
  62. data/lib/sqreen/remote_command.rb +3 -0
  63. data/lib/sqreen/rules.rb +7 -3
  64. data/lib/sqreen/rules/custom_error_cb.rb +3 -3
  65. data/lib/sqreen/rules/waf_cb.rb +3 -3
  66. data/lib/sqreen/runner.rb +19 -5
  67. data/lib/sqreen/session.rb +2 -0
  68. data/lib/sqreen/signals/conversions.rb +6 -1
  69. data/lib/sqreen/version.rb +1 -1
  70. data/lib/sqreen/weave/legacy/instrumentation.rb +38 -19
  71. metadata +57 -11
  72. data/lib/sqreen/encoding_sanitizer.rb +0 -27
@@ -0,0 +1,89 @@
1
+ require 'sqreen/log/loggable'
2
+ require 'sqreen/events/remote_exception'
3
+ require 'sqreen/mono_time'
4
+
5
+ module Sqreen
6
+ class EcosystemIntegration
7
+ module AroundCallbacks
8
+ class << self
9
+ include Log::Loggable::ClassMethods
10
+
11
+ # for instrumentation hooks
12
+ # instrumentation hooks already handle budgets, so nothing
13
+ # to do in that respect
14
+ def wrap_instrumentation_hook(module_name, action, callable)
15
+ perf_notif_name = "ecosystem_#{module_name}"
16
+
17
+ Proc.new do |*args|
18
+ begin
19
+ start = Sqreen.time
20
+ callable.call(*args)
21
+ rescue ::Exception => e # rubocop:disable Lint/RescueException
22
+ # 2) rescue exceptions
23
+ logger.warn { "Error in #{module_name}:#{action}: #{e.message}" }
24
+ logger.debug { e.backtrace.map { |x| " #{x}" }.join("\n") }
25
+ Sqreen::RemoteException.record(e)
26
+ ensure
27
+ # 3) contribute to performance metrics
28
+ stop = Sqreen.time
29
+ Sqreen::PerformanceNotifications.notify(perf_notif_name, action, start, stop)
30
+ end # end proc
31
+ end # end begin
32
+ end
33
+
34
+ # XXX: not used yet
35
+ def wrap_generic_callback(module_name, action, callable)
36
+ timer_name = "ecosystem:#{module_name}@#{action}"
37
+ perf_notif_name = "ecosystem_#{module_name}"
38
+
39
+ Proc.new do |*args|
40
+ begin
41
+ req_storage = Thread.current[:sqreen_http_request]
42
+
43
+ timer = Graft::Timer.new(timer_name) do |t|
44
+ # this is an epilogue to measure()
45
+ req_storage && req_storage[:timed_hooks] << t
46
+ end
47
+
48
+ req_timer = nil
49
+ timer.measure do
50
+ # not in a request, no budget; call cb
51
+ next callable.call(*args) unless req_storage
52
+
53
+ # 1) budget enforcement
54
+ # skip callback if budget already expended
55
+ next if req_storage[:time_budget_expended]
56
+
57
+ budget = req_storage[:time_budget]
58
+ if budget
59
+ req_timer = req_storage[:timer]
60
+ remaining = budget - req_timer.elapsed
61
+ unless remaining > 0
62
+ req_storage[:time_budget_expended] = true
63
+ next # skip callback
64
+ end
65
+ end
66
+
67
+ callable.call(*args)
68
+ end
69
+ rescue ::Exception => e # rubocop:disable Lint/RescueException
70
+ # 2) rescue exceptions
71
+ logger.warn { "Error in #{module_name}:#{action}: #{e.message}" }
72
+ logger.debug { e.backtrace.map { |x| " #{x}" }.join("\n") }
73
+ Sqreen::RemoteException.record(e)
74
+ ensure
75
+ # 3) contribute to performance metrics
76
+ if timer
77
+ req_timer.include_measurements(timer) if req_timer
78
+
79
+ Sqreen::PerformanceNotifications.notify(
80
+ perf_notif_name, action, *timer.start_and_end
81
+ )
82
+ end
83
+ end # end begin
84
+ end # end proc
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,38 @@
1
+ require 'sqreen/graft/hook'
2
+ require 'sqreen/ecosystem_integration/around_callbacks'
3
+
4
+ module Sqreen
5
+ class EcosystemIntegration
6
+ module InstrumentationService
7
+ class << self
8
+ # @param [String] module_name
9
+ # @param [String] method in form A::B#c or A::B.c
10
+ # @param [Hash{Symbol=>Proc}] spec
11
+ def instrument(module_name, method, spec)
12
+ hook = Sqreen::Graft::Hook[method].add do
13
+ if spec[:before]
14
+ cb = AroundCallbacks.wrap_instrumentation_hook(module_name, 'pre', spec[:before])
15
+ before(nil, flow: true, &cb)
16
+ end
17
+
18
+ if spec[:after]
19
+ cb = AroundCallbacks.wrap_instrumentation_hook(module_name, 'post', spec[:after])
20
+ after(nil, flow: true, &cb)
21
+ end
22
+
23
+ if spec[:raised]
24
+ cb = AroundCallbacks.wrap_instrumentation_hook(module_name, 'failing', spec[:raised])
25
+ raised(nil, flow: true, &cb)
26
+ end
27
+
28
+ if spec[:ensured]
29
+ cb = AroundCallbacks.wrap_instrumentation_hook(module_name, 'finally', spec[:ensured])
30
+ ensured(nil, flow: true, &cb)
31
+ end
32
+ end
33
+ hook.install
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,58 @@
1
+ require 'sqreen/events/remote_exception'
2
+ require 'sqreen/log/loggable'
3
+
4
+ module Sqreen
5
+ class EcosystemIntegration
6
+ # This class gets notified of request start/end and
7
+ # 1) distributes such events to listeners (typically ecosystem modules;
8
+ # the method add_start_observer is exposed to ecosystem modules through
9
+ # +Sqreen::Ecosystem::ModuleApi::EventListener+ and the dispatch table).
10
+ # 2) keeps track of whether a request is active on this thread. This is
11
+ # so that users of this class can have this information without needing
12
+ # to subscribe to request start/events and keeping thread local state
13
+ # themselves.
14
+ # XXX: Since the Ecosystem is also notified of request start/end, it could
15
+ # notify its modules of request start without going through the dispatch
16
+ # table and call add_start_observer. We need to think if we want to keep
17
+ # the transaction / request distinction or if they should just be
18
+ # assumed to be the same, though.
19
+ class RequestLifecycleTracking
20
+ include Sqreen::Log::Loggable
21
+
22
+ def initialize
23
+ @start_observers = []
24
+ @tl_key = "#{object_id}_req_in_flight"
25
+ end
26
+
27
+ # API for classes needing to know the request state
28
+
29
+ # @param cb A callback taking a Rack::Request
30
+ def add_start_observer(cb)
31
+ @start_observers << cb
32
+ end
33
+
34
+ def in_request?
35
+ Thread.current[@tl_key] ? true : false
36
+ end
37
+
38
+ # API for classes notifying this one of request events
39
+
40
+ def notify_request_start(rack_req)
41
+ Thread.current[@tl_key] = true
42
+ return if @start_observers.empty?
43
+ @start_observers.each do |cb|
44
+ begin
45
+ cb.call(rack_req)
46
+ rescue ::Exception => e # rubocop:disable Lint/RescueException
47
+ logger.warn { "Error calling #{cb} on request start: #{e.message}" }
48
+ Sqreen::RemoteException.record(e)
49
+ end
50
+ end
51
+ end
52
+
53
+ def notify_request_end
54
+ Thread.current[@tl_key] = false
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,35 @@
1
+ require 'sqreen/log/loggable'
2
+
3
+ module Sqreen
4
+ class EcosystemIntegration
5
+ class SignalConsumption
6
+ include Sqreen::Log::Loggable
7
+
8
+ PAYLOAD_CREATOR_SECTIONS = %w[request response params headers].freeze
9
+
10
+ # @param [Sqreen::Frameworks::GenericFramework] framework
11
+ # @param [Sqreen::EcosystemIntegration::RequestLifecycleTracking]
12
+ # @param [Sqreen::CappedQueue]
13
+ def initialize(framework, req_lifecycle, queue)
14
+ @framework = framework
15
+ @req_lifecycle = req_lifecycle
16
+ @queue = queue
17
+ end
18
+
19
+ def consume_signal(signal)
20
+ # transitional
21
+ unless Sqreen.features.fetch('use_signals', DEFAULT_USE_SIGNALS)
22
+ logger.debug { "Discarding signal #{signal} (signals disabled)" }
23
+ return
24
+ end
25
+
26
+ if @req_lifecycle.in_request?
27
+ # add it to the request record
28
+ @framework.observe(:signals, signal, PAYLOAD_CREATOR_SECTIONS, true)
29
+ else
30
+ @queue.push signal
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -8,7 +8,6 @@
8
8
  require 'json'
9
9
  require 'sqreen/log'
10
10
  require 'sqreen/event'
11
- require 'sqreen/encoding_sanitizer'
12
11
  require 'sqreen/sensitive_data_redactor'
13
12
 
14
13
  module Sqreen
@@ -22,8 +22,17 @@ module Sqreen
22
22
  include RequestRecorder
23
23
  attr_accessor :sqreen_configuration
24
24
 
25
+ attr_writer :req_start_cb, :req_end_cb
26
+
25
27
  def initialize
26
28
  clean_request_record
29
+
30
+ # for notifying the ecosystem of request boundaries
31
+ # XXX: this should be refactored. It shouldn't be
32
+ # the framework doing these notifications to the ecosystem
33
+ # Probably the rule callback should do it itself
34
+ @req_start_cb = Proc.new {}
35
+ @req_end_cb = Proc.new {}
27
36
  end
28
37
 
29
38
  # What kind of database is this
@@ -260,8 +269,12 @@ module Sqreen
260
269
  # Nota: cleanup should be performed at end of request (see clean_request)
261
270
  def store_request(object)
262
271
  return unless ensure_rack_loaded
272
+
273
+ rack_req = Rack::Request.new(object)
274
+ @req_start_cb.call(rack_req)
275
+
263
276
  self.remaining_perf_budget = Sqreen.performance_budget
264
- SharedStorage.set(:request, Rack::Request.new(object))
277
+ SharedStorage.set(:request, rack_req)
265
278
  SharedStorage.set(:xss_params, nil)
266
279
  SharedStorage.set(:whitelisted, nil)
267
280
  SharedStorage.set(:request_overtime, nil)
@@ -290,6 +303,7 @@ module Sqreen
290
303
  SharedStorage.set(:xss_params, nil)
291
304
  SharedStorage.set(:whitelisted, nil)
292
305
  SharedStorage.set(:request_overtime, nil)
306
+ @req_end_cb.call
293
307
  end
294
308
 
295
309
  def remaining_perf_budget
@@ -69,6 +69,8 @@ module Sqreen
69
69
 
70
70
  # signals require request section to be present
71
71
  payload_requests << 'request'
72
+ # for signals, response is optional, but the backend team wants them
73
+ payload_requests << 'response'
72
74
  payload = payload_creator.payload(payload_requests)
73
75
  payload[:observed] = observed_items
74
76
 
@@ -195,6 +195,15 @@ module Sqreen
195
195
  @size.even?
196
196
  end
197
197
 
198
+ def include_measurements(another_timer)
199
+ @blips += another_timer.instance_variable_get(:@blips)
200
+ end
201
+
202
+ def start_and_end
203
+ raise 'Not exactly two measurements recorded' unless size == 2
204
+ @blips
205
+ end
206
+
198
207
  def to_s
199
208
  "#{@tag}: time=%.03fus" % (duration * 1_000_000)
200
209
  end
@@ -11,13 +11,15 @@ require 'sqreen/graft/hook_point'
11
11
  module Sqreen
12
12
  module Graft
13
13
  class Hook
14
+ DEFAULT_STRATEGY = Sqreen::Graft::HookPoint::DEFAULT_STRATEGY
15
+
14
16
  @hooks = {}
15
17
 
16
- def self.[](hook_point, strategy = :chain)
18
+ def self.[](hook_point, strategy = DEFAULT_STRATEGY)
17
19
  @hooks[hook_point] ||= new(hook_point, nil, strategy)
18
20
  end
19
21
 
20
- def self.add(hook_point, strategy = :chain, &block)
22
+ def self.add(hook_point, strategy = DEFAULT_STRATEGY, &block)
21
23
  self[hook_point, strategy].add(&block)
22
24
  end
23
25
 
@@ -30,7 +32,7 @@ module Sqreen
30
32
 
31
33
  attr_reader :point
32
34
 
33
- def initialize(hook_point, dependency_test = nil, strategy = :chain)
35
+ def initialize(hook_point, dependency_test = nil, strategy = DEFAULT_STRATEGY)
34
36
  @disabled = false
35
37
  @point = hook_point.is_a?(HookPoint) ? hook_point : HookPoint.new(hook_point, strategy)
36
38
  @before = []
@@ -23,6 +23,8 @@ module Sqreen
23
23
  end
24
24
 
25
25
  class HookPoint
26
+ DEFAULT_STRATEGY = Module.respond_to?(:prepend) ? :prepend : :chain
27
+
26
28
  def self.parse(hook_point)
27
29
  klass_name, separator, method_name = hook_point.split(/(\#|\.)/, 2)
28
30
 
@@ -36,7 +38,7 @@ module Sqreen
36
38
 
37
39
  attr_reader :klass_name, :method_kind, :method_name
38
40
 
39
- def initialize(hook_point, strategy = :chain)
41
+ def initialize(hook_point, strategy = DEFAULT_STRATEGY)
40
42
  @klass_name, @method_kind, @method_name = Sqreen::Graft::HookPoint.parse(hook_point)
41
43
  @strategy = strategy
42
44
  end
@@ -177,23 +179,30 @@ module Sqreen
177
179
 
178
180
  private
179
181
 
180
- def prepend(key)
182
+ def hook_spot(key)
181
183
  target = klass_method? ? klass.singleton_class : klass
182
184
  mod = target.ancestors.each { |e| break if e == target; break(e) if e.class == HookSpot && e.key == key }
185
+ raise "Inconsistency detected: #{target} missing from its own ancestors" if mod.is_a?(Array)
186
+
187
+ [target, mod]
188
+ end
189
+
190
+ def prepend(key)
191
+ target, mod = hook_spot(key)
192
+
183
193
  mod ||= HookSpot.new(key)
194
+
184
195
  target.instance_eval { prepend(mod) }
185
196
  end
186
197
 
187
198
  def prepended?(key)
188
- target = klass_method? ? klass.singleton_class : klass
189
- mod = target.ancestors.each { |e| break if e == target; break(e) if e.class == HookSpot && e.key == key }
199
+ _, mod = hook_spot(key)
190
200
 
191
201
  mod != nil
192
202
  end
193
203
 
194
204
  def overridden?(key)
195
- target = klass_method? ? klass.singleton_class : klass
196
- mod = target.ancestors.each { |e| break if e == target; break(e) if e.class == HookSpot && e.key == key }
205
+ _, mod = hook_spot(key)
197
206
 
198
207
  (mod.instance_methods(false) + mod.protected_instance_methods(false) + mod.private_instance_methods(false)).include?(method_name)
199
208
  end
@@ -202,8 +211,7 @@ module Sqreen
202
211
  hook_point = self
203
212
  method_name = @method_name
204
213
 
205
- target = klass_method? ? klass.singleton_class : klass
206
- mod = target.ancestors.each { |e| break if e == target; break(e) if e.class == HookSpot && e.key == key }
214
+ _, mod = hook_spot(key)
207
215
 
208
216
  mod.instance_eval do
209
217
  if hook_point.private_method?
@@ -221,8 +229,7 @@ module Sqreen
221
229
  def unoverride(key)
222
230
  method_name = @method_name
223
231
 
224
- target = klass_method? ? klass.singleton_class : klass
225
- mod = target.ancestors.each { |e| break if e == target; break(e) if e.class == HookSpot && e.key == key }
232
+ _, mod = hook_spot(key)
226
233
 
227
234
  mod.instance_eval { remove_method(method_name) }
228
235
  end
@@ -3,6 +3,7 @@
3
3
  # Copyright (c) 2015 Sqreen. All Rights Reserved.
4
4
  # Please refer to our terms for more information: https://www.sqreen.com/terms.html
5
5
 
6
+ require 'sqreen/kit/configuration'
6
7
  require 'sqreen/kit/signals/point'
7
8
  require 'sqreen/kit/signals/dto_helper'
8
9
 
@@ -38,6 +39,7 @@ class Sqreen::Kit::Signals::Specialized::SqreenException < Sqreen::Kit::Signals:
38
39
  self.payload_schema = PAYLOAD_SCHEMA_VERSION
39
40
  self.signal_name = 'sq.agent.exception'
40
41
  self.time = values[:time] || Time.now
42
+ self.source = values[:source] || Sqreen::Kit::Configuration.default_source
41
43
  super
42
44
  end
43
45
 
@@ -6,6 +6,7 @@
6
6
  require 'sqreen/aggregated_metric'
7
7
  require 'sqreen/log/loggable'
8
8
  require 'sqreen/legacy/waf_redactions'
9
+ require 'sqreen/kit/string_sanitizer'
9
10
 
10
11
  module Sqreen
11
12
  module Legacy
@@ -55,6 +56,12 @@ module Sqreen
55
56
  when AggregatedMetric
56
57
  logger.warn "Aggregated metric event in non-signal mode. Signals disabled at runtime?"
57
58
  next
59
+ when Sqreen::Kit::Signals::Signal
60
+ logger.warn "Signal event in non-signal mode"
61
+ next
62
+ when Sqreen::Kit::Signals::Trace
63
+ logger.warn "Trace event in non-signal mode"
64
+ next
58
65
  when Attack # in practice only found inside req rec
59
66
  EventToHash.convert_attack event
60
67
  when RemoteException
@@ -72,7 +79,7 @@ module Sqreen
72
79
  tally = Hash[events.group_by(&:class).map { |k, v| [k, v.count] }]
73
80
  "Doing batch with the following tally of event types: #{tally}"
74
81
  end
75
- post('batch', { batch: batch }, {}, RETRY_MANY)
82
+ post('batch', { batch: batch.compact }, {}, RETRY_MANY)
76
83
  end
77
84
 
78
85
  private
@@ -166,7 +173,7 @@ module Sqreen
166
173
  res[:request][:parameters] = payload['params'] if payload['params']
167
174
  res[:request][:headers] = payload['headers'] if payload['headers']
168
175
 
169
- res = Sqreen::EncodingSanitizer.sanitize(res)
176
+ res = Sqreen::Kit::StringSanitizer.sanitize(res)
170
177
 
171
178
  if rr.redactor
172
179
  res[:request], redacted = rr.redactor.redact(res[:request])