sqreen-alt 1.10.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 (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