sqreen 1.19.1 → 1.21.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/lib/sqreen/agent_message.rb +20 -0
  4. data/lib/sqreen/aggregated_metric.rb +25 -0
  5. data/lib/sqreen/attack_detected.html +1 -2
  6. data/lib/sqreen/ca.crt +24 -0
  7. data/lib/sqreen/configuration.rb +10 -4
  8. data/lib/sqreen/deliveries/batch.rb +12 -2
  9. data/lib/sqreen/deliveries/simple.rb +4 -0
  10. data/lib/sqreen/ecosystem.rb +80 -0
  11. data/lib/sqreen/ecosystem/dispatch_table.rb +43 -0
  12. data/lib/sqreen/ecosystem/http/net_http.rb +51 -0
  13. data/lib/sqreen/ecosystem/http/rack_request.rb +38 -0
  14. data/lib/sqreen/ecosystem/loggable.rb +13 -0
  15. data/lib/sqreen/ecosystem/module_api.rb +30 -0
  16. data/lib/sqreen/ecosystem/module_api/event_listener.rb +18 -0
  17. data/lib/sqreen/ecosystem/module_api/instrumentation.rb +23 -0
  18. data/lib/sqreen/ecosystem/module_api/signal_producer.rb +26 -0
  19. data/lib/sqreen/ecosystem/module_api/tracing_push_down.rb +34 -0
  20. data/lib/sqreen/ecosystem/module_api/transaction_storage.rb +71 -0
  21. data/lib/sqreen/ecosystem/module_registry.rb +39 -0
  22. data/lib/sqreen/ecosystem/redis/redis_connection.rb +35 -0
  23. data/lib/sqreen/ecosystem/tracing/sampler.rb +160 -0
  24. data/lib/sqreen/ecosystem/tracing/sampling_configuration.rb +150 -0
  25. data/lib/sqreen/ecosystem/tracing/signals/tracing_client.rb +53 -0
  26. data/lib/sqreen/ecosystem/tracing/signals/tracing_server.rb +53 -0
  27. data/lib/sqreen/ecosystem/tracing_id_setup.rb +34 -0
  28. data/lib/sqreen/ecosystem/transaction_storage.rb +64 -0
  29. data/lib/sqreen/ecosystem_integration.rb +70 -0
  30. data/lib/sqreen/ecosystem_integration/around_callbacks.rb +89 -0
  31. data/lib/sqreen/ecosystem_integration/instrumentation_service.rb +38 -0
  32. data/lib/sqreen/ecosystem_integration/request_lifecycle_tracking.rb +56 -0
  33. data/lib/sqreen/ecosystem_integration/signal_consumption.rb +35 -0
  34. data/lib/sqreen/endpoint_testing.rb +184 -0
  35. data/lib/sqreen/event.rb +7 -5
  36. data/lib/sqreen/events/attack.rb +23 -18
  37. data/lib/sqreen/events/remote_exception.rb +0 -22
  38. data/lib/sqreen/events/request_record.rb +15 -70
  39. data/lib/sqreen/frameworks/generic.rb +15 -1
  40. data/lib/sqreen/frameworks/request_recorder.rb +13 -2
  41. data/lib/sqreen/graft/call.rb +9 -0
  42. data/lib/sqreen/kit/signals/specialized/aggregated_metric.rb +72 -0
  43. data/lib/sqreen/kit/signals/specialized/attack.rb +57 -0
  44. data/lib/sqreen/kit/signals/specialized/binning_metric.rb +76 -0
  45. data/lib/sqreen/kit/signals/specialized/http_trace.rb +26 -0
  46. data/lib/sqreen/kit/signals/specialized/sdk_track_call.rb +50 -0
  47. data/lib/sqreen/kit/signals/specialized/sqreen_exception.rb +57 -0
  48. data/lib/sqreen/legacy/old_event_submission_strategy.rb +227 -0
  49. data/lib/sqreen/legacy/waf_redactions.rb +49 -0
  50. data/lib/sqreen/log/loggable.rb +1 -1
  51. data/lib/sqreen/metrics/base.rb +3 -0
  52. data/lib/sqreen/metrics_store.rb +22 -12
  53. data/lib/sqreen/performance_notifications/binned_metrics.rb +8 -2
  54. data/lib/sqreen/remote_command.rb +3 -0
  55. data/lib/sqreen/rules.rb +4 -2
  56. data/lib/sqreen/rules/not_found_cb.rb +2 -0
  57. data/lib/sqreen/rules/rule_cb.rb +2 -0
  58. data/lib/sqreen/rules/waf_cb.rb +13 -10
  59. data/lib/sqreen/runner.rb +94 -13
  60. data/lib/sqreen/sensitive_data_redactor.rb +19 -31
  61. data/lib/sqreen/session.rb +53 -43
  62. data/lib/sqreen/signals/conversions.rb +288 -0
  63. data/lib/sqreen/signals/http_trace_redaction.rb +111 -0
  64. data/lib/sqreen/signals/signals_submission_strategy.rb +78 -0
  65. data/lib/sqreen/version.rb +1 -1
  66. data/lib/sqreen/weave/legacy/instrumentation.rb +4 -4
  67. metadata +74 -10
  68. data/lib/sqreen/backport.rb +0 -9
  69. data/lib/sqreen/backport/clock_gettime.rb +0 -74
  70. data/lib/sqreen/backport/original_name.rb +0 -88
@@ -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,56 @@
1
+ require 'sqreen/log/loggable'
2
+
3
+ module Sqreen
4
+ class EcosystemIntegration
5
+ # This class gets notified of request start/end and
6
+ # 1) distributes such events to listeners (typically ecosystem modules;
7
+ # the method add_start_observer is exposed to ecosystem modules through
8
+ # +Sqreen::Ecosystem::ModuleApi::EventListener+ and the dispatch table).
9
+ # 2) keeps track of whether a request is active on this thread. This is
10
+ # so that users of this class can have this information without needing
11
+ # to subscribe to request start/events and keeping thread local state
12
+ # themselves.
13
+ # XXX: Since the Ecosystem is also notified of request start/end, it could
14
+ # notify its modules of request start without going through the dispatch
15
+ # table and call add_start_observer. We need to think if we want to keep
16
+ # the transaction / request distinction or if they should just be
17
+ # assumed to be the same, though.
18
+ class RequestLifecycleTracking
19
+ include Sqreen::Log::Loggable
20
+
21
+ def initialize
22
+ @start_observers = []
23
+ @tl_key = "#{object_id}_req_in_flight"
24
+ end
25
+
26
+ # API for classes needing to know the request state
27
+
28
+ # @param cb A callback taking a Rack::Request
29
+ def add_start_observer(cb)
30
+ @start_observers << cb
31
+ end
32
+
33
+ def in_request?
34
+ Thread.current[@tl_key] ? true : false
35
+ end
36
+
37
+ # API for classes notifying this one of request events
38
+
39
+ def notify_request_start(rack_req)
40
+ Thread.current[@tl_key] = true
41
+ return if @start_observers.empty?
42
+ @start_observers.each do |cb|
43
+ begin
44
+ cb.call(rack_req)
45
+ rescue ::Exception => e # rubocop:disable Lint/RescueException
46
+ logger.warn { "Error calling #{cb} on request start: #{e.message}" }
47
+ end
48
+ end
49
+ end
50
+
51
+ def notify_request_end
52
+ Thread.current[@tl_key] = false
53
+ end
54
+ end
55
+ end
56
+ 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
@@ -0,0 +1,184 @@
1
+ require 'net/https'
2
+ require 'sqreen/agent_message'
3
+ require 'sqreen/log/loggable'
4
+
5
+ module Sqreen
6
+ class EndpointTesting
7
+ Endpoint = Struct.new(:url, :ca_store)
8
+ class ChosenEndpoints
9
+ def initialize
10
+ @messages = []
11
+ end
12
+
13
+ # @return [Sqreen::EndpointTesting::Endpoint]
14
+ attr_accessor :control
15
+
16
+ # @return [Sqreen::EndpointTesting::Endpoint]
17
+ attr_accessor :ingestion
18
+
19
+ # @return [Array<Sqreen::AgentMessage>]
20
+ attr_reader :messages
21
+
22
+ # @param [Sqreen::AgentMessage] message
23
+ def add_message(message)
24
+ @messages << message
25
+ end
26
+ end
27
+
28
+ MAIN_CONTROL_HOST = 'back.sqreen.com'.freeze
29
+ MAIN_INJECTION_HOST = 'ingestion.sqreen.com'.freeze
30
+ FALLBACK_ENDPOINT_URL = 'https://back.sqreen.io/'.freeze
31
+ GLOBAL_TIMEOUT = 30
32
+
33
+ CONTROL_ERROR_KIND = 'back_sqreen_com_unavailable'.freeze
34
+ INGESTION_ERROR_KIND = 'ingestion_sqreen_com_unavailable'.freeze
35
+
36
+ class << self
37
+ include Log::Loggable
38
+
39
+ # reproduces behaviour before endpoint testing was introduced
40
+ def no_test_endpoints(config_url, config_ingestion_url)
41
+ endpoints = ChosenEndpoints.new
42
+
43
+ endpoints.control = Endpoint.new(
44
+ config_url || "https://#{MAIN_CONTROL_HOST}/", cert_store
45
+ )
46
+ endpoints.ingestion = Endpoint.new(
47
+ config_ingestion_url || "https://#{MAIN_INJECTION_HOST}/", nil
48
+ )
49
+
50
+ endpoints
51
+ end
52
+
53
+ def test_endpoints(proxy_url, config_url, config_ingestion_url)
54
+ proxy_params = create_proxy_params(proxy_url)
55
+
56
+ # execute the tests in separate threads and wait for them
57
+ thread_control = Thread.new do
58
+ thread_main(config_url, proxy_params, MAIN_CONTROL_HOST)
59
+ end
60
+ thread_injection = Thread.new do
61
+ thread_main(config_ingestion_url, proxy_params, MAIN_INJECTION_HOST)
62
+ end
63
+
64
+ wait_for_threads(thread_control, thread_injection)
65
+
66
+ # build and return result
67
+ fallback = Endpoint.new(FALLBACK_ENDPOINT_URL, cert_store)
68
+ endpoints = ChosenEndpoints.new
69
+ endpoints.control = thread_control[:endpoint] || fallback
70
+ endpoints.ingestion = thread_injection[:endpoint] || fallback
71
+
72
+ if thread_control[:endpoint_error]
73
+ msg = AgentMessage.new(CONTROL_ERROR_KIND, thread_control[:endpoint_error])
74
+ endpoints.add_message msg
75
+ end
76
+ if thread_injection[:endpoint_error]
77
+ msg = AgentMessage.new(INGESTION_ERROR_KIND, thread_injection[:endpoint_error])
78
+ endpoints.add_message msg
79
+ end
80
+
81
+ endpoints
82
+ end
83
+
84
+ private
85
+
86
+ def thread_main(configured_url, proxy_params, host)
87
+ res = if configured_url
88
+ Endpoint.new(configured_url, nil)
89
+ else
90
+ EndpointTesting.send(:test_with_store_variants, proxy_params, host)
91
+ end
92
+
93
+ Thread.current[:endpoint] = res
94
+ rescue StandardError => e
95
+ Thread.current[:endpoint_error] = e.message
96
+ end
97
+
98
+ def create_proxy_params(proxy_url)
99
+ return [] unless proxy_url
100
+
101
+ proxy = URI.parse(proxy_url)
102
+
103
+ return [] unless proxy.scheme == 'http'
104
+
105
+ [proxy.host, proxy.port, proxy.user, proxy.password]
106
+ end
107
+
108
+ def test_with_store_variants(proxy_params, server_name)
109
+ # first without custom store
110
+ do_test(proxy_params, server_name, false)
111
+ rescue StandardError => _e
112
+ do_test(proxy_params, server_name, true)
113
+ end
114
+
115
+ # @param [Array] proxy_params
116
+ # @param [String] server_name
117
+ # @param [Boolean] custom_store
118
+ def do_test(proxy_params, server_name, custom_store)
119
+ http = Net::HTTP.new(server_name, 443, *proxy_params)
120
+ http.use_ssl = true
121
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if ENV['SQREEN_SSL_NO_VERIFY']
122
+ http.verify_callback = lambda do |preverify_ok, ctx|
123
+ unless preverify_ok
124
+ logger.warn do
125
+ "Certificate validation failure for certificate issued to " \
126
+ "#{ctx.chain[0].subject}: #{ctx.error_string}"
127
+ end
128
+ end
129
+
130
+ preverify_ok
131
+ end
132
+
133
+ http.open_timeout = 13
134
+ http.ssl_timeout = 7
135
+ http.read_timeout = 7
136
+ http.close_on_empty_response = true
137
+
138
+ http.cert_store = cert_store if custom_store
139
+
140
+ resp = http.get('/ping')
141
+
142
+ logger.info do
143
+ "Got response from #{server_name}'s ping endpoint. " \
144
+ "Status code is #{resp.code} (custom CA store: #{custom_store})"
145
+ end
146
+
147
+ unless resp.code == '200'
148
+ raise "Response code for /ping is #{resp.code}, not 200"
149
+ end
150
+
151
+ Endpoint.new("https://#{server_name}/", http.cert_store)
152
+ rescue StandardError => e
153
+ logger.info do
154
+ "Error in request to #{server_name} " \
155
+ "(custom store: #{custom_store}): #{e.message}"
156
+ end
157
+
158
+ raise "Error in request to #{server_name}: #{e.message}"
159
+ end
160
+
161
+ def wait_for_threads(thread_control, thread_injection)
162
+ deadline = Time.now + GLOBAL_TIMEOUT
163
+ [thread_control, thread_injection].each do |thread|
164
+ rem = deadline - Time.now
165
+ rem = 0.1 if rem < 0.1
166
+ next if thread.join(rem)
167
+ logger.debug { "Timeout for thread #{thread}" }
168
+ thread.kill
169
+ thread[:endpoint_error] = "Timeout doing endpoint testing"
170
+ end
171
+ end
172
+
173
+ def cert_store
174
+ @cert_store ||= begin
175
+ cert_file = File.join(File.dirname(__FILE__), 'ca.crt')
176
+ cert_store = OpenSSL::X509::Store.new
177
+ cert_store.add_file cert_file
178
+
179
+ cert_store
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -8,17 +8,19 @@
8
8
  module Sqreen
9
9
  # Master interface for point in time events (e.g. Attack, RemoteException)
10
10
  class Event
11
+ # @return [Hash]
11
12
  attr_reader :payload
13
+
14
+ # @return [Time]
15
+ attr_accessor :time # writer used only in tests
16
+
12
17
  def initialize(payload)
13
18
  @payload = payload
14
- end
15
-
16
- def to_hash
17
- payload.to_hash
19
+ @time = Time.now.utc
18
20
  end
19
21
 
20
22
  def to_s
21
- "<#{self.class.name}: #{to_hash}>"
23
+ "<#{self.class.name}: #{payload.to_hash}>"
22
24
  end
23
25
  end
24
26
  end
@@ -11,6 +11,8 @@ module Sqreen
11
11
  # Attack
12
12
  # When creating a new attack, it gets automatically pushed to the event's
13
13
  # queue.
14
+ # XXX: TURNS OUT THIS CLASS IS ACTUALLY NOT USED ANYMORE
15
+ # Framework.observe is used instead with unstructured attack details
14
16
  class Attack < Event
15
17
  def self.record(payload)
16
18
  attack = Attack.new(payload)
@@ -26,11 +28,31 @@ module Sqreen
26
28
  payload['rule']['rulespack_id']
27
29
  end
28
30
 
29
- def type
31
+ def rule_name
30
32
  return nil unless payload['rule']
31
33
  payload['rule']['name']
32
34
  end
33
35
 
36
+ def test?
37
+ return nil unless payload['rule']
38
+ payload['rule']['test'] ? true : false
39
+ end
40
+
41
+ def beta?
42
+ return nil unless payload['rule']
43
+ payload['rule']['beta'] ? true : false
44
+ end
45
+
46
+ def block?
47
+ return nil unless payload['rule']
48
+ payload['rule']['block'] ? true : false
49
+ end
50
+
51
+ def attack_type
52
+ return nil unless payload['rule']
53
+ payload['rule']['attack_type']
54
+ end
55
+
34
56
  def time
35
57
  return nil unless payload['local']
36
58
  payload['local']['time']
@@ -44,22 +66,5 @@ module Sqreen
44
66
  def enqueue
45
67
  Sqreen.queue.push(self)
46
68
  end
47
-
48
- def to_hash
49
- res = {}
50
- rule_p = payload['rule']
51
- request_p = payload['request']
52
- res[:rule_name] = rule_p['name'] if rule_p && rule_p['name']
53
- res[:rulespack_id] = rule_p['rulespack_id'] if rule_p && rule_p['rulespack_id']
54
- res[:test] = rule_p['test'] if rule_p && rule_p['test']
55
- res[:infos] = payload['infos'] if payload['infos']
56
- res[:time] = time if time
57
- res[:client_ip] = request_p[:addr] if request_p && request_p[:addr]
58
- res[:request] = request_p if request_p
59
- res[:params] = payload['params'] if payload['params']
60
- res[:context] = payload['context'] if payload['context']
61
- res[:headers] = payload['headers'] if payload['headers']
62
- res
63
- end
64
69
  end
65
70
  end
@@ -30,27 +30,5 @@ module Sqreen
30
30
  def klass
31
31
  payload['exception'].class.name
32
32
  end
33
-
34
- def to_hash
35
- exception = payload['exception']
36
- ev = {
37
- :klass => exception.class.name,
38
- :message => exception.message,
39
- :params => payload['request_params'],
40
- :time => payload['time'],
41
- :infos => {
42
- :client_ip => payload['client_ip'],
43
- },
44
- :request => payload['request_infos'],
45
- :headers => payload['headers'],
46
- :rule_name => payload['rule_name'],
47
- :rulespack_id => payload['rulespack_id'],
48
- }
49
-
50
- ev[:infos].merge!(payload['infos']) if payload['infos']
51
- return ev unless exception.backtrace
52
- ev[:context] = { :backtrace => exception.backtrace.map(&:to_s) }
53
- ev
54
- end
55
33
  end
56
34
  end
@@ -14,6 +14,10 @@ require 'sqreen/sensitive_data_redactor'
14
14
  module Sqreen
15
15
  # When a request is deeemed worthy of being sent to the backend
16
16
  class RequestRecord < Sqreen::Event
17
+ attr_reader :redactor
18
+
19
+ # @param [Hash] payload
20
+ # @param [Sqreen::SensitiveDataRedactor] redactor
17
21
  def initialize(payload, redactor = nil)
18
22
  @redactor = redactor
19
23
  super(payload)
@@ -23,74 +27,18 @@ module Sqreen
23
27
  (payload && payload[:observed]) || {}
24
28
  end
25
29
 
26
- def to_hash
27
- res = { :version => '20171208' }
28
- if payload[:observed]
29
- res[:observed] = payload[:observed].dup
30
- rulespack = nil
31
- if observed[:attacks]
32
- res[:observed][:attacks] = observed[:attacks].map do |att|
33
- natt = att.dup
34
- rulespack = natt.delete(:rulespack_id) || rulespack
35
- natt
36
- end
37
- end
38
- if observed[:sqreen_exceptions]
39
- res[:observed][:sqreen_exceptions] = observed[:sqreen_exceptions].map do |exc|
40
- nex = exc.dup
41
- excp = nex.delete(:exception)
42
- if excp
43
- nex[:message] = excp.message
44
- nex[:klass] = excp.class.name
45
- end
46
- rulespack = nex.delete(:rulespack_id) || rulespack
47
- nex
48
- end
49
- end
50
- res[:rulespack_id] = rulespack unless rulespack.nil?
51
- if observed[:observations]
52
- res[:observed][:observations] = observed[:observations].map do |cat, key, value, time|
53
- { :category => cat, :key => key, :value => value, :time => time }
54
- end
55
- end
56
- if observed[:sdk]
57
- res[:observed][:sdk] = processed_sdk_calls
58
- end
59
- end
60
- res[:local] = payload['local'] if payload['local']
61
- if payload['request']
62
- res[:request] = payload['request'].dup
63
- res[:client_ip] = res[:request].delete(:client_ip) if res[:request][:client_ip]
64
- else
65
- res[:request] = {}
66
- end
67
- if payload['response']
68
- res[:response] = payload['response'].dup
69
- else
70
- res[:response] = {}
71
- end
72
-
73
- res[:request][:parameters] = payload['params'] if payload['params']
74
- res[:request][:headers] = payload['headers'] if payload['headers']
75
-
76
- res = Sqreen::EncodingSanitizer.sanitize(res)
30
+ def last_identify_args
31
+ return nil unless observed[:sdk]
77
32
 
78
- if @redactor
79
- res[:request], redacted = @redactor.redact(res[:request])
80
- if redacted.any? && res[:observed] && res[:observed][:attacks]
81
- res[:observed][:attacks] = @redactor.redact_attacks!(res[:observed][:attacks], redacted)
82
- end
83
- if redacted.any? && res[:observed] && res[:observed][:sqreen_exceptions]
84
- res[:observed][:sqreen_exceptions] = @redactor.redact_exceptions!(res[:observed][:sqreen_exceptions], redacted)
85
- end
33
+ observed[:sdk].reverse_each do |meth, _time, *args|
34
+ next unless meth == :identify
35
+ return args
86
36
  end
87
-
88
- res
37
+ nil
89
38
  end
90
39
 
91
- private
92
-
93
40
  def processed_sdk_calls
41
+ return [] unless observed[:sdk]
94
42
  auth_keys = last_identify_id
95
43
 
96
44
  observed[:sdk].map do |meth, time, *args|
@@ -102,6 +50,8 @@ module Sqreen
102
50
  end
103
51
  end
104
52
 
53
+ private
54
+
105
55
  def inject_identifiers(args, meth, auth_keys)
106
56
  return args unless meth == :track && auth_keys
107
57
 
@@ -118,13 +68,8 @@ module Sqreen
118
68
  end
119
69
 
120
70
  def last_identify_id
121
- return nil unless observed[:sdk]
122
-
123
- observed[:sdk].reverse_each do |meth, _time, *args|
124
- next unless meth == :identify
125
- return args.first if args.respond_to? :first
126
- end
127
- nil
71
+ args = last_identify_args
72
+ args.first if args.respond_to? :first
128
73
  end
129
74
  end
130
75
  end