sqreen-alt 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +22 -0
  3. data/README.md +77 -0
  4. data/Rakefile +20 -0
  5. data/lib/sqreen-alt.rb +1 -0
  6. data/lib/sqreen.rb +68 -0
  7. data/lib/sqreen/attack_detected.html +2 -0
  8. data/lib/sqreen/binding_accessor.rb +288 -0
  9. data/lib/sqreen/ca.crt +72 -0
  10. data/lib/sqreen/call_countable.rb +67 -0
  11. data/lib/sqreen/callback_tree.rb +78 -0
  12. data/lib/sqreen/callbacks.rb +100 -0
  13. data/lib/sqreen/capped_queue.rb +23 -0
  14. data/lib/sqreen/condition_evaluator.rb +235 -0
  15. data/lib/sqreen/conditionable.rb +50 -0
  16. data/lib/sqreen/configuration.rb +168 -0
  17. data/lib/sqreen/context.rb +26 -0
  18. data/lib/sqreen/deliveries/batch.rb +84 -0
  19. data/lib/sqreen/deliveries/simple.rb +39 -0
  20. data/lib/sqreen/event.rb +16 -0
  21. data/lib/sqreen/events/attack.rb +61 -0
  22. data/lib/sqreen/events/remote_exception.rb +54 -0
  23. data/lib/sqreen/events/request_record.rb +62 -0
  24. data/lib/sqreen/exception.rb +34 -0
  25. data/lib/sqreen/frameworks.rb +40 -0
  26. data/lib/sqreen/frameworks/generic.rb +446 -0
  27. data/lib/sqreen/frameworks/rails.rb +148 -0
  28. data/lib/sqreen/frameworks/rails3.rb +36 -0
  29. data/lib/sqreen/frameworks/request_recorder.rb +69 -0
  30. data/lib/sqreen/frameworks/sinatra.rb +57 -0
  31. data/lib/sqreen/frameworks/sqreen_test.rb +26 -0
  32. data/lib/sqreen/instrumentation.rb +542 -0
  33. data/lib/sqreen/log.rb +119 -0
  34. data/lib/sqreen/metrics.rb +6 -0
  35. data/lib/sqreen/metrics/average.rb +39 -0
  36. data/lib/sqreen/metrics/base.rb +45 -0
  37. data/lib/sqreen/metrics/collect.rb +22 -0
  38. data/lib/sqreen/metrics/sum.rb +20 -0
  39. data/lib/sqreen/metrics_store.rb +96 -0
  40. data/lib/sqreen/middleware.rb +34 -0
  41. data/lib/sqreen/payload_creator.rb +137 -0
  42. data/lib/sqreen/performance_notifications.rb +86 -0
  43. data/lib/sqreen/performance_notifications/log.rb +36 -0
  44. data/lib/sqreen/performance_notifications/metrics.rb +36 -0
  45. data/lib/sqreen/performance_notifications/newrelic.rb +36 -0
  46. data/lib/sqreen/remote_command.rb +93 -0
  47. data/lib/sqreen/rule_attributes.rb +26 -0
  48. data/lib/sqreen/rule_callback.rb +108 -0
  49. data/lib/sqreen/rules.rb +126 -0
  50. data/lib/sqreen/rules_callbacks.rb +29 -0
  51. data/lib/sqreen/rules_callbacks/binding_accessor_matcher.rb +77 -0
  52. data/lib/sqreen/rules_callbacks/binding_accessor_metrics.rb +79 -0
  53. data/lib/sqreen/rules_callbacks/blacklist_ips.rb +44 -0
  54. data/lib/sqreen/rules_callbacks/count_http_codes.rb +40 -0
  55. data/lib/sqreen/rules_callbacks/crawler_user_agent_matches.rb +24 -0
  56. data/lib/sqreen/rules_callbacks/crawler_user_agent_matches_metrics.rb +24 -0
  57. data/lib/sqreen/rules_callbacks/custom_error.rb +64 -0
  58. data/lib/sqreen/rules_callbacks/execjs.rb +241 -0
  59. data/lib/sqreen/rules_callbacks/headers_insert.rb +22 -0
  60. data/lib/sqreen/rules_callbacks/inspect_rule.rb +25 -0
  61. data/lib/sqreen/rules_callbacks/matcher_rule.rb +138 -0
  62. data/lib/sqreen/rules_callbacks/rails_parameters.rb +14 -0
  63. data/lib/sqreen/rules_callbacks/record_request_context.rb +39 -0
  64. data/lib/sqreen/rules_callbacks/reflected_xss.rb +254 -0
  65. data/lib/sqreen/rules_callbacks/regexp_rule.rb +36 -0
  66. data/lib/sqreen/rules_callbacks/shell_env.rb +32 -0
  67. data/lib/sqreen/rules_callbacks/url_matches.rb +25 -0
  68. data/lib/sqreen/rules_callbacks/user_agent_matches.rb +22 -0
  69. data/lib/sqreen/rules_signature.rb +151 -0
  70. data/lib/sqreen/runner.rb +365 -0
  71. data/lib/sqreen/runtime_infos.rb +138 -0
  72. data/lib/sqreen/safe_json.rb +60 -0
  73. data/lib/sqreen/sdk.rb +22 -0
  74. data/lib/sqreen/serializer.rb +46 -0
  75. data/lib/sqreen/session.rb +317 -0
  76. data/lib/sqreen/shared_storage.rb +31 -0
  77. data/lib/sqreen/stats.rb +18 -0
  78. data/lib/sqreen/version.rb +5 -0
  79. metadata +148 -0
@@ -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
+ require 'sqreen/rule_callback'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # Generic regexp based matching
9
+ class RegexpRuleCB < RuleCB
10
+ def initialize(*args)
11
+ super(*args)
12
+ prepare
13
+ end
14
+
15
+ def prepare
16
+ @patterns = []
17
+ raw_patterns = @data['values']
18
+ if raw_patterns.nil?
19
+ msg = "no key 'values' in data (had #{@data.keys})"
20
+ raise Sqreen::Exception, msg
21
+ end
22
+
23
+ @patterns = raw_patterns.map do |pattern|
24
+ Regexp.compile(pattern, Regexp::IGNORECASE)
25
+ end
26
+ end
27
+
28
+ def match_regexp(str)
29
+ @patterns.each do |pattern|
30
+ return pattern if pattern.match(str)
31
+ end
32
+ nil
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,32 @@
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
+ require 'sqreen/rules_callbacks/regexp_rule'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # Callback that detect nifty env in system calls
9
+ class ShellEnvCB < RegexpRuleCB
10
+ def pre(_inst, *args, &_block)
11
+ return if args.size == 0
12
+ env = args.first
13
+ return unless env.is_a?(Hash)
14
+ return if env.size == 0
15
+ found = nil
16
+ var, value = env.find do |_, val|
17
+ next unless val.is_a?(String)
18
+ found = match_regexp(val)
19
+ end
20
+ return unless var
21
+ infos = {
22
+ :variable_name => var,
23
+ :variable_value => value,
24
+ :found => found,
25
+ }
26
+ Sqreen.log.warn "presence of a shell env tampering: #{infos.inspect}"
27
+ record_event(infos)
28
+ advise_action(:raise)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
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
+ require 'sqreen/rules_callbacks/regexp_rule'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # FIXME: Tune this as Rack capable callback?
9
+ # If:
10
+ # - we have a 404
11
+ # - the path is a typical bot scanning request
12
+ # Then we deny the ressource and record the attack.
13
+ class URLMatchesCB < RegexpRuleCB
14
+ def post(rv, _inst, *args, &_block)
15
+ return unless rv.is_a?(Array) && rv.size > 0 && rv[0] == 404
16
+ env = args[0]
17
+ path = env['SCRIPT_NAME'].to_s + env['PATH_INFO'].to_s
18
+ found = match_regexp(path)
19
+ infos = { :path => path, :found => found }
20
+ record_event(infos) if found
21
+ advise_action(nil)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
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
+ require 'sqreen/rules_callbacks/regexp_rule'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # Look for badly behaved clients
9
+ class UserAgentMatchesCB < RegexpRuleCB
10
+ def pre(_inst, *_args, &_block)
11
+ ua = framework.client_user_agent
12
+ return unless ua
13
+ found = match_regexp(ua)
14
+ return unless found
15
+ Sqreen.log.debug { "Found UA #{ua} - found: #{found}" }
16
+ infos = { :found => found }
17
+ record_event(infos)
18
+ advise_action(:raise, :data => found)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,151 @@
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
+ require 'sqreen/exception'
5
+
6
+ require 'set'
7
+ require 'openssl'
8
+ require 'base64'
9
+ require 'json'
10
+
11
+ ## Rules signature
12
+ module Sqreen
13
+ # Perform an EC + digest verification of a message.
14
+ class SignatureVerifier
15
+ def initialize(key, digest)
16
+ @pub_key = OpenSSL::PKey.read(key)
17
+ @digest = digest
18
+ end
19
+
20
+ def verify(sig, val)
21
+ hashed_val = @digest.digest(val)
22
+ @pub_key.dsa_verify_asn1(hashed_val, sig)
23
+ end
24
+ end
25
+
26
+ # Normalize and verify a rule
27
+ class SqreenSignedVerifier
28
+ REQUIRED_SIGNED_KEYS = %w(hookpoint name callbacks conditions).freeze
29
+ SIGNATURE_KEY = 'signature'.freeze
30
+ SIGNATURE_VALUE_KEY = 'value'.freeze
31
+ SIGNED_KEYS_KEY = 'keys'.freeze
32
+ SIGNATURE_VERSION = 'v0_9'.freeze
33
+ PUBLIC_KEY = <<-END.gsub(/^ */, '').freeze
34
+ -----BEGIN PUBLIC KEY-----
35
+ MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA39oWMHR8sxb9LRaM5evZ7mw03iwJ
36
+ WNHuDeGqgPo1HmvuMfLnAyVLwaMXpGPuvbqhC1U65PG90bTJLpvNokQf0VMA5Tpi
37
+ m+NXwl7bjqa03vO/HErLbq3zBRysrZnC4OhJOF1jazkAg0psQOea2r5HcMcPHgMK
38
+ fnWXiKWnZX+uOWPuerE=
39
+ -----END PUBLIC KEY-----
40
+ END
41
+
42
+ attr_accessor :pub_key
43
+ attr_accessor :required_signed_keys
44
+ attr_accessor :digest
45
+
46
+ def initialize(required_keys = REQUIRED_SIGNED_KEYS,
47
+ public_key = PUBLIC_KEY,
48
+ digest = OpenSSL::Digest::SHA512.new)
49
+ @required_signed_keys = required_keys
50
+ @signature_verifier = SignatureVerifier.new(public_key, digest)
51
+ end
52
+
53
+ def normalize_val(val, level)
54
+ raise Sqreen::Exception, 'recursion level too deep' if level == 0
55
+
56
+ case val
57
+ when Hash
58
+ normalize(val, nil, level - 1)
59
+ when Array
60
+ ary = val.map do |i|
61
+ normalize_val(i, level - 1)
62
+ end
63
+ "[#{ary.join(',')}]"
64
+ when String, Integer
65
+ begin
66
+ JSON.dump(val)
67
+ rescue JSON::GeneratorError
68
+ JSON.generate(val, :quirks_mode => true)
69
+ end
70
+ else
71
+ msg = "JSON hash parsing error (wrong value type: #{val.class})"
72
+ raise Sqreen::Exception.new, msg
73
+ end
74
+ end
75
+
76
+ def normalize_key(key)
77
+ case key
78
+ when String, Integer
79
+ begin
80
+ JSON.dump(key)
81
+ rescue JSON::GeneratorError
82
+ JSON.generate(key, :quirks_mode => true)
83
+ end
84
+ else
85
+ msg = "JSON hash parsing error (wrong key type: #{key.class})"
86
+ raise Sqreen::Exception, msg
87
+ end
88
+ end
89
+
90
+ def normalize(hash_rule, signed_keys = nil, level = 20)
91
+ # Normalize the provided hash to a string:
92
+ # - sort keys lexicographically, recursively
93
+ # - convert each scalar to its JSON representation
94
+ # - convert hash to '{key:value}'
95
+ # - convert array [v1,v2] to '[v1,v2]' and [] to '[]'
96
+ # Two hash with different key ordering should have the same normalized
97
+ # value.
98
+
99
+ raise Sqreen::Exception, 'recursion level too deep' if level == 0
100
+ unless hash_rule.is_a?(Hash)
101
+ raise Sqreen::Exception, "wrong hash type #{hash_rule.class}"
102
+ end
103
+
104
+ res = []
105
+ hash_rule.sort.each do |k, v|
106
+ # Only keep signed keys
107
+ next if signed_keys && !signed_keys.include?(k)
108
+
109
+ k = normalize_key(k)
110
+ v = normalize_val(v, level - 1)
111
+
112
+ res << "#{k}:#{v}"
113
+ end
114
+ "{#{res.join(',')}}"
115
+ end
116
+
117
+ def get_sig_infos_or_fail(hash_rule)
118
+ raise Sqreen::Exception, 'non hash argument' unless hash_rule.is_a?(Hash)
119
+
120
+ sigs = hash_rule[SIGNATURE_KEY]
121
+ raise Sqreen::Exception, 'no signature found' unless sigs
122
+
123
+ sig = sigs[SIGNATURE_VERSION]
124
+ msg = "signature #{SIGNATURE_VERSION} not found (#{sigs})"
125
+ raise Sqreen::Exception, msg unless sig
126
+
127
+ sig_value = sig[SIGNATURE_VALUE_KEY]
128
+ raise Sqreen::Exception, 'no signature value found' unless sig_value
129
+
130
+ signed_keys = sig[SIGNED_KEYS_KEY]
131
+ raise Sqreen::Exception, "no signed keys found (#{sig})" unless signed_keys
132
+
133
+ inc = Set.new(signed_keys).superset?(Set.new(@required_signed_keys))
134
+ raise Sqreen::Exception, 'signed keys miss equired keys' unless inc
135
+
136
+ [signed_keys, sig_value]
137
+ end
138
+
139
+ def verify(hash_rule)
140
+ # Return true if rule signature is correct, else false
141
+
142
+ signed_keys, sig_value = get_sig_infos_or_fail(hash_rule)
143
+
144
+ norm_str = normalize(hash_rule, signed_keys)
145
+ bin_sig = Base64.decode64(sig_value)
146
+ @signature_verifier.verify(bin_sig, norm_str)
147
+ rescue OpenSSL::PKey::ECError
148
+ false
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,365 @@
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
+ require 'ipaddr'
5
+ require 'timeout'
6
+ require 'json'
7
+
8
+ require 'sqreen/events/attack'
9
+
10
+ require 'sqreen/log'
11
+
12
+ require 'sqreen/rules'
13
+ require 'sqreen/session'
14
+ require 'sqreen/remote_command'
15
+ require 'sqreen/capped_queue'
16
+ require 'sqreen/metrics_store'
17
+ require 'sqreen/deliveries/simple'
18
+ require 'sqreen/deliveries/batch'
19
+ require 'sqreen/performance_notifications/metrics'
20
+ require 'sqreen/instrumentation'
21
+ require 'sqreen/call_countable'
22
+
23
+ module Sqreen
24
+ @features = {}
25
+ @queue = nil
26
+
27
+ # Event Queue that enable communication between threads and the reporter
28
+ MAX_QUEUE_LENGTH = 100
29
+ MAX_OBS_QUEUE_LENGTH = 1000
30
+
31
+ METRICS_EVENT = 'metrics'.freeze
32
+
33
+ class << self
34
+ attr_reader :features
35
+ def update_features(features)
36
+ @features = features
37
+ end
38
+
39
+ def queue
40
+ @queue ||= CappedQueue.new(MAX_QUEUE_LENGTH)
41
+ end
42
+
43
+ def observations_queue
44
+ @observations_queue ||= CappedQueue.new(MAX_OBS_QUEUE_LENGTH)
45
+ end
46
+
47
+ attr_accessor :instrumentation_ready
48
+ alias instrumentation_ready? instrumentation_ready
49
+
50
+ attr_accessor :logged_in
51
+ alias logged_in? logged_in
52
+
53
+ attr_reader :whitelisted_paths
54
+ def update_whitelisted_paths(paths)
55
+ @whitelisted_paths = paths.freeze
56
+ end
57
+
58
+ attr_reader :whitelisted_ips
59
+ def update_whitelisted_ips(paths)
60
+ @whitelisted_ips = Hash[paths.map { |v| [v, IPAddr.new(v)] }].freeze
61
+ end
62
+ end
63
+
64
+ # Main running job class for the agent
65
+ class Runner
66
+ # During one hour
67
+ HEARTBEAT_WARMUP = 60 * 60
68
+ # Initail delay is 5 minutes
69
+ HEARTBEAT_MAX_DELAY = 5 * 60
70
+
71
+ attr_accessor :heartbeat_delay
72
+ attr_accessor :metrics_engine
73
+ attr_reader :deliverer
74
+ attr_reader :session
75
+ attr_reader :instrumenter
76
+ attr_accessor :running
77
+ attr_accessor :next_command_results
78
+ attr_accessor :next_metrics
79
+
80
+ # we may want to do that in a thread in order to prevent delaying app
81
+ # startup
82
+ # set_at_exit do not place a global at_exit (used for testing)
83
+ def initialize(configuration, framework, set_at_exit = true, session_class = Sqreen::Session)
84
+ @logged_out_tried = false
85
+ @configuration = configuration
86
+ @framework = framework
87
+ @heartbeat_delay = HEARTBEAT_MAX_DELAY
88
+ @last_heartbeat_request = Time.now
89
+ @next_command_results = {}
90
+ @next_metrics = []
91
+ @running = true
92
+
93
+ @token = @configuration.get(:token)
94
+ @url = @configuration.get(:url)
95
+ Sqreen.update_whitelisted_paths([])
96
+ Sqreen.update_whitelisted_ips({})
97
+ raise(Sqreen::Exception, 'no url found') unless @url
98
+ raise(Sqreen::TokenNotFoundException, 'no token found') unless @token
99
+
100
+ register_exit_cb if set_at_exit
101
+
102
+ self.metrics_engine = MetricsStore.new
103
+ @instrumenter = Instrumentation.new(metrics_engine)
104
+
105
+ Sqreen.log.warn "using token #{@token}"
106
+ response = create_session(session_class)
107
+ wanted_features = response.fetch('features', {})
108
+ conf_initial_features = configuration.get(:initial_features)
109
+ unless conf_initial_features.nil?
110
+ begin
111
+ conf_features = JSON.parse(conf_initial_features)
112
+ raise 'Invalid Type' unless conf_features.is_a?(Hash)
113
+ Sqreen.log.debug do
114
+ "Override initial features with #{conf_features.inspect}"
115
+ end
116
+ wanted_features = conf_features
117
+ rescue
118
+ Sqreen.log.warn do
119
+ "NOT using invalid inital features #{conf_initial_features}"
120
+ end
121
+ end
122
+ end
123
+ self.features = wanted_features
124
+
125
+ # Ensure a deliverer is there unless features have set it first
126
+ self.deliverer ||= Deliveries::Simple.new(session)
127
+ context_infos = {}
128
+ %w[rules pack_id].each do |p|
129
+ context_infos[p] = response[p] unless response[p].nil?
130
+ end
131
+ process_commands(response.fetch('commands', []), context_infos)
132
+ end
133
+
134
+ def create_session(session_class)
135
+ @session = session_class.new(@url, @token)
136
+ session.login(@framework)
137
+ end
138
+
139
+ def deliverer=(new_deliverer)
140
+ deliverer.drain if deliverer
141
+ @deliverer = new_deliverer
142
+ end
143
+
144
+ def batch_events(batch_size, max_staleness = nil)
145
+ size = batch_size.to_i
146
+ self.deliverer = if size < 1
147
+ Deliveries::Simple.new(session)
148
+ else
149
+ staleness = max_staleness.to_i
150
+ Deliveries::Batch.new(session, size, staleness)
151
+ end
152
+ end
153
+
154
+ def load_rules(context_infos = {})
155
+ rules_pack = context_infos['rules']
156
+ rulespack_id = context_infos['pack_id']
157
+ if rules_pack.nil? || rulespack_id.nil?
158
+ session_rules = session.rules
159
+ rules_pack = session_rules['rules']
160
+ rulespack_id = session_rules['pack_id']
161
+ end
162
+ rules = rules_pack.each { |r| r['rulespack_id'] = rulespack_id }
163
+ Sqreen.log.info { format('retrieved rulespack id: %s', rulespack_id) }
164
+ Sqreen.log.debug { format('retrieved %d rules', rules.size) }
165
+ local_rules = Sqreen::Rules.local(@configuration) || []
166
+ rules += local_rules.
167
+ select { |rule| rule['enabled'] }.
168
+ each { |r| r['rulespack_id'] = 'local' }
169
+ Sqreen.log.debug do
170
+ format('rules: %s', rules.
171
+ sort_by { |r| r['name'] }.
172
+ map { |r| format('(%s, %s)', r['name'], r.to_json.size) }.
173
+ join(', '))
174
+ end
175
+ [rulespack_id, rules]
176
+ end
177
+
178
+ def call_counts_metrics_period=(value)
179
+ value = value.to_i
180
+ return unless value > 0 # else disable collection?
181
+ metrics_engine.create_metric('name' => CallCountable::COUNT_CALLS,
182
+ 'period' => value,
183
+ 'kind' => 'Sum')
184
+ end
185
+
186
+ def performance_metrics_period=(value)
187
+ value = value.to_i
188
+ if value > 0
189
+ PerformanceNotifications::Metrics.enable(metrics_engine, value)
190
+ else
191
+ PerformanceNotifications::Metrics.disable
192
+ end
193
+ end
194
+
195
+ def setup_instrumentation(context_infos = {})
196
+ Sqreen.log.info 'setup instrumentation'
197
+ rulespack_id, rules = load_rules(context_infos)
198
+ @framework.instrument_when_ready!(instrumenter, rules)
199
+ rulespack_id.to_s
200
+ end
201
+
202
+ def remove_instrumentation(_context_infos = {})
203
+ Sqreen.log.debug 'removing instrumentation'
204
+ instrumenter.remove_all_callbacks
205
+ true
206
+ end
207
+
208
+ def reload_rules(_context_infos = {})
209
+ Sqreen.log.debug 'Reloading rules'
210
+ rulespack_id, rules = load_rules
211
+ instrumenter.remove_all_callbacks
212
+
213
+ @framework.instrument_when_ready!(instrumenter, rules)
214
+ Sqreen.log.debug 'Rules reloaded'
215
+ rulespack_id.to_s
216
+ end
217
+
218
+ def process_commands(commands, context_infos = {})
219
+ return if commands.nil? || commands.empty?
220
+ res = RemoteCommand.process_list(self, commands, context_infos)
221
+ @next_command_results = res
222
+ end
223
+
224
+ def do_heartbeat
225
+ @last_heartbeat_request = Time.now
226
+ @next_metrics.concat(metrics_engine.publish(false)) if metrics_engine
227
+ res = session.heartbeat(next_command_results, next_metrics)
228
+ next_command_results.clear
229
+ next_metrics.clear
230
+ process_commands(res['commands'])
231
+ end
232
+
233
+ def features(_context_infos = {})
234
+ Sqreen.features
235
+ end
236
+
237
+ def features=(features)
238
+ Sqreen.update_features(features)
239
+ session.request_compression = features['request_compression'] if session
240
+ self.performance_metrics_period = features['performance_metrics_period']
241
+ self.call_counts_metrics_period = features['call_counts_metrics_period']
242
+ hd = features['heartbeat_delay'].to_i
243
+ self.heartbeat_delay = hd if hd > 0
244
+ return if features['batch_size'].nil?
245
+ batch_events(features['batch_size'], features['max_staleness'])
246
+ end
247
+
248
+ def change_whitelisted_paths(paths, _context_infos = {})
249
+ return false unless paths.respond_to?(:each)
250
+ Sqreen.update_whitelisted_paths(paths)
251
+ true
252
+ end
253
+
254
+ def upload_bundle(_context_infos = {})
255
+ t = Time.now
256
+ session.post_bundle(RuntimeInfos.dependencies_signature, RuntimeInfos.dependencies)
257
+ Time.now - t
258
+ end
259
+
260
+ def change_whitelisted_ips(ips, _context_infos = {})
261
+ return false unless ips.respond_to?(:each)
262
+ Sqreen.update_whitelisted_ips(ips)
263
+ true
264
+ end
265
+
266
+ def change_features(new_features, _context_infos = {})
267
+ old = features
268
+ self.features = new_features
269
+ {
270
+ 'was' => old,
271
+ 'now' => new_features,
272
+ }
273
+ end
274
+
275
+ def aggregate_observations
276
+ q = Sqreen.observations_queue
277
+ q.size.times do
278
+ cat, key, obs, t = q.pop
279
+ metrics_engine.update(cat, t, key, obs)
280
+ end
281
+ end
282
+
283
+ def heartbeat_needed?
284
+ (@last_heartbeat_request + heartbeat_delay) < Time.now
285
+ end
286
+
287
+ def run_watcher_once
288
+ event = Timeout.timeout(heartbeat_delay) do
289
+ Sqreen.queue.pop
290
+ end
291
+ rescue Timeout::Error
292
+ periodic_cleanup
293
+ else
294
+ handle_event(event)
295
+ if heartbeat_needed?
296
+ # Also aggregate/post metrics when cleanup has
297
+ # not been done for a long time
298
+ Sqreen.log.debug 'Forced an heartbeat'
299
+ periodic_cleanup # will trigger do_heartbeat since it's time
300
+ end
301
+ end
302
+
303
+ def periodic_cleanup
304
+ # Nothing occured:
305
+ # tick delivery, aggregates_metrics
306
+ # issue a simple heartbeat if it's time (which may return commands)
307
+ @deliverer.tick
308
+ aggregate_observations
309
+ do_heartbeat if heartbeat_needed?
310
+ end
311
+
312
+ def handle_event(event)
313
+ if event == METRICS_EVENT
314
+ aggregate_observations
315
+ else
316
+ @deliverer.post_event(event)
317
+ end
318
+ end
319
+
320
+ def run_watcher
321
+ run_watcher_once while running
322
+ end
323
+
324
+ # Sinatra is using at_exit to run the application, see:
325
+ # https://github.com/sinatra/sinatra/blob/cd503e6c590cd48c2c9bb7869522494bfc62cb14/lib/sinatra/main.rb#L25
326
+ def exit_from_sinatra_startup?
327
+ defined?(Sinatra::Application) &&
328
+ Sinatra::Application.respond_to?(:run?) &&
329
+ !Sinatra::Application.run?
330
+ end
331
+
332
+ def shutdown(_context_infos = {})
333
+ remove_instrumentation
334
+ logout
335
+ end
336
+
337
+ def logout(retrying = true)
338
+ return unless session
339
+ if @framework.development?
340
+ @running = false
341
+ return
342
+ end
343
+ if @logged_out_tried
344
+ Sqreen.log.debug('Not running logout twice')
345
+ return
346
+ end
347
+ @logged_out_tried = true
348
+ @deliverer.drain if @deliverer
349
+ aggregate_observations
350
+ session.post_metrics(metrics_engine.publish) if metrics_engine
351
+ session.logout(retrying)
352
+ @running = false
353
+ end
354
+
355
+ def register_exit_cb(try_again = true)
356
+ at_exit do
357
+ if exit_from_sinatra_startup? && try_again
358
+ register_exit_cb(false)
359
+ else
360
+ logout
361
+ end
362
+ end
363
+ end
364
+ end
365
+ end