sqreen 0.1.0.pre → 0.7.01461158029

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CODE_OF_CONDUCT.md +22 -0
  3. data/README.md +77 -0
  4. data/Rakefile +40 -0
  5. data/lib/sqreen.rb +67 -0
  6. data/lib/sqreen/binding_accessor.rb +184 -0
  7. data/lib/sqreen/ca.crt +72 -0
  8. data/lib/sqreen/callback_tree.rb +78 -0
  9. data/lib/sqreen/callbacks.rb +120 -0
  10. data/lib/sqreen/capped_queue.rb +23 -0
  11. data/lib/sqreen/condition_evaluator.rb +169 -0
  12. data/lib/sqreen/conditionable.rb +50 -0
  13. data/lib/sqreen/configuration.rb +151 -0
  14. data/lib/sqreen/context.rb +22 -0
  15. data/lib/sqreen/deliveries/batch.rb +80 -0
  16. data/lib/sqreen/deliveries/simple.rb +36 -0
  17. data/lib/sqreen/detect.rb +14 -0
  18. data/lib/sqreen/detect/shell_injection.rb +61 -0
  19. data/lib/sqreen/detect/sql_injection.rb +115 -0
  20. data/lib/sqreen/event.rb +16 -0
  21. data/lib/sqreen/events/attack.rb +60 -0
  22. data/lib/sqreen/events/remote_exception.rb +53 -0
  23. data/lib/sqreen/exception.rb +31 -0
  24. data/lib/sqreen/frameworks.rb +40 -0
  25. data/lib/sqreen/frameworks/generic.rb +243 -0
  26. data/lib/sqreen/frameworks/rails.rb +155 -0
  27. data/lib/sqreen/frameworks/rails3.rb +36 -0
  28. data/lib/sqreen/frameworks/sinatra.rb +34 -0
  29. data/lib/sqreen/frameworks/sqreen_test.rb +26 -0
  30. data/lib/sqreen/instrumentation.rb +504 -0
  31. data/lib/sqreen/log.rb +116 -0
  32. data/lib/sqreen/metrics.rb +6 -0
  33. data/lib/sqreen/metrics/average.rb +39 -0
  34. data/lib/sqreen/metrics/base.rb +41 -0
  35. data/lib/sqreen/metrics/collect.rb +22 -0
  36. data/lib/sqreen/metrics/sum.rb +20 -0
  37. data/lib/sqreen/metrics_store.rb +94 -0
  38. data/lib/sqreen/parsers/sql.rb +98 -0
  39. data/lib/sqreen/parsers/sql_tokenizer.rb +266 -0
  40. data/lib/sqreen/parsers/unix.rb +110 -0
  41. data/lib/sqreen/payload_creator.rb +132 -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 +82 -0
  47. data/lib/sqreen/rule_attributes.rb +25 -0
  48. data/lib/sqreen/rule_callback.rb +97 -0
  49. data/lib/sqreen/rules.rb +116 -0
  50. data/lib/sqreen/rules_callbacks.rb +29 -0
  51. data/lib/sqreen/rules_callbacks/binding_accessor_metrics.rb +79 -0
  52. data/lib/sqreen/rules_callbacks/count_http_codes.rb +18 -0
  53. data/lib/sqreen/rules_callbacks/crawler_user_agent_matches.rb +24 -0
  54. data/lib/sqreen/rules_callbacks/crawler_user_agent_matches_metrics.rb +25 -0
  55. data/lib/sqreen/rules_callbacks/execjs.rb +136 -0
  56. data/lib/sqreen/rules_callbacks/headers_insert.rb +20 -0
  57. data/lib/sqreen/rules_callbacks/inspect_rule.rb +20 -0
  58. data/lib/sqreen/rules_callbacks/matcher_rule.rb +103 -0
  59. data/lib/sqreen/rules_callbacks/rails_parameters.rb +14 -0
  60. data/lib/sqreen/rules_callbacks/record_request_context.rb +23 -0
  61. data/lib/sqreen/rules_callbacks/reflected_xss.rb +40 -0
  62. data/lib/sqreen/rules_callbacks/regexp_rule.rb +36 -0
  63. data/lib/sqreen/rules_callbacks/shell.rb +33 -0
  64. data/lib/sqreen/rules_callbacks/shell_env.rb +32 -0
  65. data/lib/sqreen/rules_callbacks/sql.rb +41 -0
  66. data/lib/sqreen/rules_callbacks/system_shell.rb +25 -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 +142 -0
  70. data/lib/sqreen/runner.rb +312 -0
  71. data/lib/sqreen/runtime_infos.rb +127 -0
  72. data/lib/sqreen/session.rb +340 -0
  73. data/lib/sqreen/stats.rb +18 -0
  74. data/lib/sqreen/version.rb +6 -0
  75. metadata +95 -34
@@ -0,0 +1,312 @@
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 'timeout'
5
+
6
+ require 'sqreen/events/attack'
7
+
8
+ require 'sqreen/log'
9
+
10
+ require 'sqreen/rules'
11
+ require 'sqreen/remote_command'
12
+ require 'sqreen/capped_queue'
13
+ require 'sqreen/metrics_store'
14
+ require 'sqreen/deliveries/simple'
15
+ require 'sqreen/deliveries/batch'
16
+ require 'sqreen/performance_notifications/metrics'
17
+
18
+ module Sqreen
19
+ @features = {}
20
+ @queue = nil
21
+
22
+ # Event Queue that enable communication between threads and the reporter
23
+ MAX_QUEUE_LENGTH = 100
24
+ MAX_OBS_QUEUE_LENGTH = 1000
25
+
26
+ METRICS_EVENT = 'metrics'.freeze
27
+
28
+ class << self
29
+ attr_reader :features
30
+ def update_features(features)
31
+ @features = features
32
+ end
33
+
34
+ def queue
35
+ @queue ||= CappedQueue.new(MAX_QUEUE_LENGTH)
36
+ end
37
+
38
+ def observations_queue
39
+ @observations_queue ||= CappedQueue.new(MAX_OBS_QUEUE_LENGTH)
40
+ end
41
+
42
+ attr_accessor :instrumentation_ready
43
+ alias instrumentation_ready? instrumentation_ready
44
+
45
+ attr_accessor :logged_in
46
+ alias logged_in? logged_in
47
+ end
48
+
49
+ # Main running job class for the agent
50
+ class Runner
51
+ # At start, heartbeat is every 15 seconds
52
+ HEARTBEAT_INITIAL_DELAY = 15
53
+ # During one hour
54
+ HEARTBEAT_WARMUP = 60 * 60
55
+ # Then delay raises to 5 minutes
56
+ HEARTBEAT_MAX_DELAY = 5 * 60
57
+
58
+ attr_reader :heartbeat_delay
59
+ attr_accessor :metrics_engine
60
+ attr_reader :publish_metrics_delay
61
+ attr_reader :deliverer
62
+ attr_reader :session
63
+ attr_reader :instrumenter
64
+
65
+ # we may want to do that in a thread in order to prevent delaying app
66
+ # startup
67
+ # set_at_exit do not place a global at_exit (used for testing)
68
+ def initialize(configuration, framework, set_at_exit = true, session_class = Sqreen::Session)
69
+ @logged_out_tried = false
70
+ @configuration = configuration
71
+ @framework = framework
72
+ @heartbeat_delay = HEARTBEAT_INITIAL_DELAY
73
+ @publish_metrics_delay = HEARTBEAT_MAX_DELAY
74
+ @sleep_delay = HEARTBEAT_INITIAL_DELAY
75
+ @last_heartbeat_request = Time.at(0)
76
+ @last_post_metrics_request = Time.now
77
+ @started = Time.now
78
+
79
+ @token = @configuration.get(:token)
80
+ @url = @configuration.get(:url)
81
+ raise(Sqreen::Exception, 'no url found') unless @url
82
+ raise(Sqreen::TokenNotFoundException, 'no token found') unless @token
83
+
84
+ register_exit_cb if set_at_exit
85
+
86
+ Sqreen.log.warn "using token #{@token}"
87
+ self.features = create_session(session_class)
88
+ # Ensure a deliverer is there unless features have set it first
89
+ self.deliverer ||= Deliveries::Simple.new(session)
90
+
91
+ self.metrics_engine = MetricsStore.new
92
+ @instrumenter = Instrumentation.new(metrics_engine)
93
+ end
94
+
95
+ def create_session(session_class)
96
+ @session = session_class.new(@url, @token)
97
+ session.login(@framework)
98
+ end
99
+
100
+ def deliverer=(new_deliverer)
101
+ deliverer.drain if deliverer
102
+ @deliverer = new_deliverer
103
+ end
104
+
105
+ def batch_events(batch_size, max_staleness = nil)
106
+ size = batch_size.to_i
107
+ self.deliverer = if size < 1
108
+ Deliveries::Simple.new(session)
109
+ else
110
+ staleness = max_staleness.to_i
111
+ Deliveries::Batch.new(session, size, staleness)
112
+ end
113
+ end
114
+
115
+ def load_rules
116
+ rules_pack = session.rules
117
+ rulespack_id = rules_pack['pack_id']
118
+ rules = rules_pack['rules'].each { |r| r['rulespack_id'] = rulespack_id }
119
+ Sqreen.log.info format('retrieved rulespack id: %s', rulespack_id)
120
+ Sqreen.log.debug format('retrieved %d rules', rules.size)
121
+ local_rules = Sqreen::Rules.local(@configuration) || []
122
+ rules += local_rules.
123
+ select { |rule| rule['enabled'] }.
124
+ each { |r| r['rulespack_id'] = 'local' }
125
+ Sqreen.log.debug format('rules: %s', rules.
126
+ sort_by { |r| r['name'] }.
127
+ map { |r| format('(%s, %s)', r['name'], r.to_json.size) }.
128
+ join(', '))
129
+ [rulespack_id, rules]
130
+ end
131
+
132
+ def performance_metrics_period=(value)
133
+ value = value.to_i
134
+ if value > 0
135
+ PerformanceNotifications::Metrics.enable(metrics_engine, value)
136
+ else
137
+ PerformanceNotifications::Metrics.disable
138
+ end
139
+ end
140
+
141
+ def setup_instrumentation
142
+ Sqreen.log.info 'setup instrumentation'
143
+ rulespack_id, rules = load_rules
144
+ @framework.instrument_when_ready!(instrumenter, rules)
145
+ rulespack_id.to_s
146
+ end
147
+
148
+ def remove_instrumentation
149
+ Sqreen.log.debug 'removing instrumentation'
150
+ instrumenter.remove_all_callbacks
151
+ true
152
+ end
153
+
154
+ def reload_rules
155
+ Sqreen.log.debug 'Reloading rules'
156
+ rulespack_id, rules = load_rules
157
+ instrumenter.remove_all_callbacks
158
+
159
+ @framework.instrument_when_ready!(instrumenter, rules)
160
+ Sqreen.log.debug 'Rules reloaded'
161
+ rulespack_id.to_s
162
+ end
163
+
164
+ def process_commands(commands)
165
+ while commands && !commands.empty?
166
+ res = RemoteCommand.process_list(self, commands)
167
+ res = session.post_commands_result(res)
168
+ commands = res['commands']
169
+ end
170
+ end
171
+
172
+ def do_heartbeat
173
+ @last_heartbeat_request = Time.now
174
+ res = session.heartbeat
175
+ update_heartbeat_delay
176
+ process_commands(res['commands'])
177
+ end
178
+
179
+ def features
180
+ Sqreen.features
181
+ end
182
+
183
+ def features=(features)
184
+ Sqreen.update_features(features)
185
+ session.request_compression = features['request_compression'] if session
186
+ self.performance_metrics_period = features['performance_metrics_period']
187
+ md = features['publish_metrics_delay'].to_i
188
+ self.publish_metrics_delay = md if md > 0
189
+ return if features['batch_size'].nil?
190
+ batch_events(features['batch_size'], features['max_staleness'])
191
+ end
192
+
193
+ def change_features(new_features)
194
+ old = features
195
+ self.features = new_features
196
+ {
197
+ 'was' => old,
198
+ 'now' => new_features,
199
+ }
200
+ end
201
+
202
+ def aggregate_observations
203
+ q = Sqreen.observations_queue
204
+ q.size.times do
205
+ cat, key, obs, t = q.pop
206
+ metrics_engine.update(cat, t, key, obs)
207
+ end
208
+ end
209
+
210
+ def run_watcher_once
211
+ event = Timeout.timeout(@sleep_delay) do
212
+ Sqreen.queue.pop
213
+ end
214
+ rescue Timeout::Error
215
+ periodic_cleanup
216
+ else
217
+ handle_event(event)
218
+ if (@last_heartbeat_request + HEARTBEAT_MAX_DELAY) < Time.now
219
+ Sqreen.log.debug 'Forced an heartbeat'
220
+ do_heartbeat
221
+ # Also aggregate/post metrics when cleanup has
222
+ # not been done for a long time
223
+ periodic_cleanup
224
+ end
225
+ end
226
+
227
+ def periodic_cleanup
228
+ # Nothing occured:
229
+ # tick delivery, aggregates_metrics
230
+ # issue a simple heartbeat if it's time (which may return commands)
231
+ @deliverer.tick
232
+ aggregate_observations
233
+ t = Time.now
234
+ do_heartbeat if (@last_heartbeat_request + heartbeat_delay) < t
235
+ post_metrics if (@last_post_metrics_request + publish_metrics_delay) < t
236
+ end
237
+
238
+ def handle_event(event)
239
+ if event == METRICS_EVENT
240
+ aggregate_observations
241
+ else
242
+ @deliverer.post_event(event)
243
+ end
244
+ end
245
+
246
+ def run_watcher
247
+ loop do
248
+ run_watcher_once
249
+ end
250
+ end
251
+
252
+ def update_heartbeat_delay
253
+ return unless Time.now - @started > HEARTBEAT_WARMUP
254
+ return if heartbeat_delay == HEARTBEAT_MAX_DELAY
255
+ self.heartbeat_delay = HEARTBEAT_MAX_DELAY
256
+ end
257
+
258
+ def update_sleep_delay
259
+ @sleep_delay = [heartbeat_delay, publish_metrics_delay].min
260
+ Sqreen.log.debug { format('sleep delay %f', @sleep_delay) }
261
+ end
262
+
263
+ def heartbeat_delay=(x)
264
+ @heartbeat_delay = x
265
+ update_sleep_delay
266
+ x
267
+ end
268
+
269
+ def publish_metrics_delay=(x)
270
+ @publish_metrics_delay = x
271
+ update_sleep_delay
272
+ x
273
+ end
274
+
275
+ def post_metrics
276
+ return unless metrics_engine
277
+ @last_post_metrics_request = Time.now
278
+ session.post_metrics(metrics_engine.publish)
279
+ end
280
+
281
+ # Sinatra is using at_exit to run the application, see:
282
+ # https://github.com/sinatra/sinatra/blob/cd503e6c590cd48c2c9bb7869522494bfc62cb14/lib/sinatra/main.rb#L25
283
+ def exit_from_sinatra_startup?
284
+ defined?(Sinatra::Application) &&
285
+ Sinatra::Application.respond_to?(:run?) &&
286
+ !Sinatra::Application.run?
287
+ end
288
+
289
+ def logout(retrying = true)
290
+ return unless session
291
+ if @logged_out_tried
292
+ Sqreen.log.debug('Not running logout twice')
293
+ return
294
+ end
295
+ @logged_out_tried = true
296
+ @deliverer.drain if @deliverer
297
+ aggregate_observations
298
+ post_metrics
299
+ session.logout(retrying)
300
+ end
301
+
302
+ def register_exit_cb(try_again = true)
303
+ at_exit do
304
+ if exit_from_sinatra_startup? && try_again
305
+ register_exit_cb(false)
306
+ else
307
+ logout
308
+ end
309
+ end
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,127 @@
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/version'
5
+ require 'sqreen/frameworks'
6
+
7
+ require 'socket'
8
+
9
+ module Sqreen
10
+ module RuntimeInfos
11
+ module_function
12
+
13
+ def all(framework)
14
+ res = { :various_infos => {} }
15
+ res.merge! agent
16
+ res.merge! os
17
+ res.merge! runtime
18
+ res.merge! framework.framework_infos
19
+ res[:various_infos].merge! time
20
+ res[:various_infos].merge! dependencies
21
+ res[:various_infos].merge! process
22
+ res
23
+ end
24
+
25
+ def local_infos
26
+ {
27
+ 'time' => Time.now.utc,
28
+ 'name' => hostname,
29
+ }
30
+ end
31
+
32
+ def dependencies
33
+ gem_info = Gem.loaded_specs
34
+ gem_info = gem_info.map do |name, spec|
35
+ {
36
+ :name => name,
37
+ :version => spec.version.to_s,
38
+ :homepage => spec.homepage,
39
+ :source => (extract_source(spec.source) if spec.respond_to?(:source)),
40
+ }
41
+ end
42
+ {
43
+ :dependencies => gem_info,
44
+ }
45
+ end
46
+
47
+ def time
48
+ { :time => Time.now.to_s }
49
+ end
50
+
51
+ def ssl
52
+ type = nil
53
+ version = nil
54
+ if defined? OpenSSL
55
+ type = 'OpenSSL'
56
+ version = OpenSSL::OPENSSL_VERSION if defined? OpenSSL::OPENSSL_VERSION
57
+ end
58
+ { :ssl =>
59
+ {
60
+ :type => type,
61
+ :version => version,
62
+ },
63
+ }
64
+ end
65
+
66
+ def agent
67
+ {
68
+ :agent_type => :ruby,
69
+ :agent_version => ::Sqreen::VERSION,
70
+ }
71
+ end
72
+
73
+ def os
74
+ plat = if defined? ::RUBY_PLATFORM
75
+ ::RUBY_PLATFORM
76
+ elsif defined? ::PLATFORM
77
+ ::PLATFORM
78
+ else
79
+ ''
80
+ end
81
+ {
82
+ :os_type => plat,
83
+ :hostname => hostname,
84
+ }
85
+ end
86
+
87
+ def hostname
88
+ Socket.gethostname
89
+ end
90
+
91
+ def process
92
+ {
93
+ :pid => Process.pid,
94
+ :ppid => Process.ppid,
95
+ :euid => Process.euid,
96
+ :egid => Process.egid,
97
+ :uid => Process.uid,
98
+ :gid => Process.gid,
99
+ :name => $0,
100
+ }
101
+ end
102
+
103
+ def runtime
104
+ engine = if defined? ::RUBY_ENGINE
105
+ ::RUBY_ENGINE
106
+ else
107
+ 'ruby'
108
+ end
109
+ {
110
+ :runtime_type => engine,
111
+ :runtime_version => ::RUBY_DESCRIPTION,
112
+ }
113
+ end
114
+
115
+ def extract_source(source)
116
+ return nil unless source
117
+ ret = { 'name' => source.class.name.split(':')[-1] }
118
+ opts = {}
119
+ opts = source.options if source.respond_to?(:options)
120
+ ret['remotes'] = opts['remotes'] if opts['remotes']
121
+ ret['uri'] = opts['uri'] if opts['uri']
122
+ # FIXME: scrub any auth data in uris
123
+ ret['path'] = opts['path'] if opts['path']
124
+ ret
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,340 @@
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/log'
5
+ require 'sqreen/runtime_infos'
6
+ require 'sqreen/events/remote_exception'
7
+
8
+ require 'net/https'
9
+ require 'json'
10
+ require 'uri'
11
+ require 'openssl'
12
+ require 'zlib'
13
+
14
+ # $ curl -H"x-api-key: ${KEY}" http://127.0.0.1:5000/sqreen/v0/app-login
15
+ # {
16
+ # "session_id": "c9171007c27d4da8906312ff343ed41307f65b2f6fdf4a05a445bb7016186657",
17
+ # "status": true
18
+ # }
19
+ #
20
+ # $ curl -H"x-session-key: ${SESS}" http://127.0.0.1:5000/sqreen/v0/get-rulespack
21
+
22
+ #
23
+ # FIXME: we should be proxy capable
24
+ # FIXME: we should be multithread aware (when callbacks perform server requests?)
25
+ #
26
+
27
+ module Sqreen
28
+ class Session
29
+ RETRY_CONNECT_SECONDS = 10
30
+ RETRY_REQUEST_SECONDS = 10
31
+
32
+ MAX_DELAY = 60 * 30
33
+
34
+ RETRY_LONG = 128
35
+
36
+ MUTEX = Mutex.new
37
+ METRICS_KEY = 'metrics'.freeze
38
+
39
+ @@path_prefix = '/sqreen/v0/'
40
+
41
+ attr_accessor :request_compression
42
+
43
+ def initialize(server_url, token)
44
+ @token = token
45
+ @session_id = nil
46
+ @server_url = server_url
47
+ @request_compression = false
48
+ @connected = nil
49
+
50
+ uri = parse_uri(server_url)
51
+ use_ssl = (uri.scheme == 'https')
52
+
53
+ @req_nb = 0
54
+
55
+ @http = Net::HTTP.new(uri.host, uri.port)
56
+ @http.use_ssl = use_ssl
57
+ if use_ssl
58
+ cert_file = File.join(File.dirname(__FILE__), 'ca.crt')
59
+ cert_store = OpenSSL::X509::Store.new
60
+ cert_store.add_file cert_file
61
+ @http.cert_store = cert_store
62
+ end
63
+ end
64
+
65
+ def parse_uri(uri)
66
+ # This regexp is the Ruby constant URI::PATTERN::HOSTNAME augmented
67
+ # with the _ character that is frequent in Docker linked containers.
68
+ re = '(?:(?:[a-zA-Z\\d](?:[-_a-zA-Z\\d]*[a-zA-Z\\d])?)\\.)*(?:[a-zA-Z](?:[-_a-zA-Z\\d]*[a-zA-Z\\d])?)\\.?'
69
+ parser = URI::Parser.new :HOSTNAME => re
70
+ parser.parse(uri)
71
+ end
72
+
73
+ def prefix_path(path)
74
+ @@path_prefix + path
75
+ end
76
+
77
+ def connected?
78
+ @con && @con.started?
79
+ end
80
+
81
+ def disconnect
82
+ @http.finish if connected?
83
+ end
84
+
85
+ NET_ERRORS = [Timeout::Error,
86
+ Errno::EINVAL,
87
+ Errno::ECONNRESET,
88
+ Errno::ECONNREFUSED,
89
+ EOFError,
90
+ Net::HTTPBadResponse,
91
+ Net::HTTPHeaderSyntaxError,
92
+ SocketError,
93
+ Net::ProtocolError].freeze
94
+
95
+ def connect
96
+ return if connected?
97
+ Sqreen.log.warn "connection to #{@server_url}..."
98
+ @session_id = nil
99
+ @conn_retry = 0
100
+ begin
101
+ @con = @http.start
102
+ rescue *NET_ERRORS
103
+ Sqreen.log.debug "Cannot connect, retry in #{RETRY_CONNECT_SECONDS} seconds"
104
+ sleep RETRY_CONNECT_SECONDS
105
+ @conn_retry += 1
106
+ retry
107
+ else
108
+ Sqreen.log.warn 'connection success.'
109
+ end
110
+ end
111
+
112
+ def resilient_post(path, data, headers = {})
113
+ post(path, data, headers, RETRY_LONG)
114
+ end
115
+
116
+ def resilient_get(path, headers = {})
117
+ get(path, headers, RETRY_LONG)
118
+ end
119
+
120
+ def post(path, data, headers = {}, max_retry = 2)
121
+ do_http_request(:POST, path, data, headers, max_retry)
122
+ end
123
+
124
+ def get(path, headers = {}, max_retry = 2)
125
+ do_http_request(:GET, path, nil, headers, max_retry)
126
+ end
127
+
128
+ def resiliently(retry_request_seconds, max_retry, current_retry = 0)
129
+ return yield
130
+ rescue => e
131
+
132
+ Sqreen.log.error(e.inspect)
133
+
134
+ current_retry += 1
135
+
136
+ raise e if current_retry >= max_retry || e.is_a?(Sqreen::NotImplementedYet)
137
+
138
+ sleep_delay = [MAX_DELAY, retry_request_seconds * current_retry].min
139
+ Sqreen.log.debug format('Sleeping %ds', sleep_delay)
140
+ sleep(sleep_delay)
141
+
142
+ retry
143
+ end
144
+
145
+ def thread_id
146
+ th = Thread.current
147
+ return '' unless th
148
+ re = th.to_s.scan(/:(0x.*)>/)
149
+ return '' unless re && re.size > 0
150
+ res = re[0]
151
+ return '' unless res && res.size > 0
152
+ res[0]
153
+ end
154
+
155
+ def do_http_request(method, path, data, headers = {}, max_retry = 2)
156
+ connect unless connected?
157
+ headers['X-Session-Key'] = @session_id if @session_id
158
+ headers['X-Sqreen-Time'] = Time.now.utc.to_f.to_s
159
+ headers['X-Sqreen-Agent'] = "Ruby/#{Sqreen::VERSION}"
160
+ headers['X-Sqreen-Beta'] = format('pid=%d;tid=%s;nb=%d;t=%f',
161
+ Process.pid,
162
+ thread_id,
163
+ @req_nb,
164
+ Time.now.utc.to_f)
165
+ headers['Content-Type'] = 'application/json'
166
+ if request_compression && !method.casecmp(:GET).zero?
167
+ headers['Content-Encoding'] = 'gzip'
168
+ end
169
+
170
+ @req_nb += 1
171
+
172
+ path = prefix_path(path)
173
+ Sqreen.log.debug format('%s %s (%s)', method, path, @token)
174
+
175
+ res = {}
176
+ resiliently(RETRY_REQUEST_SECONDS, max_retry) do
177
+ json = nil
178
+ MUTEX.synchronize do
179
+ json = case method.upcase
180
+ when :GET
181
+ @con.get(path, headers)
182
+ when :POST
183
+ json_data = compress(self.class.encode_payload(data))
184
+ @con.post(path, json_data, headers)
185
+ else
186
+ Sqreen.log.debug format('unknown method %s', method)
187
+ raise Sqreen::NotImplementedYet
188
+ end
189
+ end
190
+
191
+ if json && json.body
192
+ res = JSON.parse(json.body)
193
+ unless res['status']
194
+ Sqreen.log.debug(format('Cannot %s %s.', method, path))
195
+ end
196
+ else
197
+ Sqreen.log.debug 'warning: empty return value'
198
+ end
199
+ end
200
+ Sqreen.log.debug format('%s %s (DONE)', method, path)
201
+ res
202
+ end
203
+
204
+ def self.encode_payload(data)
205
+ JSON.generate(data)
206
+ rescue JSON::GeneratorError
207
+ Sqreen.log.debug('Payload could not be encoded enforcing recode')
208
+ JSON.generate(rencode_payload(data))
209
+ end
210
+
211
+ def compress(data)
212
+ return data unless request_compression
213
+ out = StringIO.new
214
+ w = Zlib::GzipWriter.new(out)
215
+ w.write(data)
216
+ w.close
217
+ out.string
218
+ end
219
+
220
+ def self.rencode_payload(obj, max_depth = 20)
221
+ max_depth -= 1
222
+ return obj if max_depth < 0
223
+ return rencode_array(obj, max_depth) if obj.is_a?(Array)
224
+ return enforce_encoding(obj) unless obj.is_a?(Hash)
225
+ obj.each do |k, v|
226
+ case v
227
+ when Array
228
+ obj[k] = rencode_array(v, max_depth)
229
+ when Hash
230
+ obj[k] = rencode_payload(v, max_depth)
231
+ when String
232
+ obj[k] = enforce_encoding(v)
233
+ end
234
+ end
235
+ obj
236
+ end
237
+
238
+ def self.rencode_array(array, max_depth)
239
+ array.map! { |e| rencode_payload(e, max_depth - 1) }
240
+ array
241
+ end
242
+
243
+ def self.enforce_encoding(str)
244
+ return str unless str.is_a?(String)
245
+ return str if str.valid_encoding?
246
+ str.chars.map do |v|
247
+ if v.valid_encoding?
248
+ v
249
+ else
250
+ v.bytes.map { |c| "\\x#{c.to_s(16).upcase}" }.join
251
+ end
252
+ end.join
253
+ end
254
+
255
+ def login(framework)
256
+ headers = { 'x-api-key' => @token }
257
+
258
+ res = resilient_post('app-login', RuntimeInfos.all(framework), headers)
259
+
260
+ if !res || !res['status']
261
+ public_error = format('Cannot login. Token may be invalid: %s', @token)
262
+ Sqreen.log.error public_error
263
+ raise(Sqreen::TokenInvalidException,
264
+ format('invalid response: %s', res.inspect))
265
+ end
266
+ Sqreen.log.info 'Login success.'
267
+ @session_id = res['session_id']
268
+ Sqreen.log.debug "received session_id #{@session_id}"
269
+ Sqreen.logged_in = true
270
+ res.fetch('features', {})
271
+ end
272
+
273
+ def rules
274
+ resilient_get('rulespack')
275
+ end
276
+
277
+ def heartbeat
278
+ get('app-beat', {}, 5)
279
+ end
280
+
281
+ def post_commands_result(res)
282
+ resilient_post('commands', res)
283
+ end
284
+
285
+ def post_metrics(metrics)
286
+ return if metrics.nil? || metrics.empty?
287
+ payload = { METRICS_KEY => metrics }
288
+ resilient_post(METRICS_KEY, payload)
289
+ end
290
+
291
+ def post_attack(attack)
292
+ resilient_post('attack', attack.to_hash)
293
+ end
294
+
295
+ # Post an exception to Sqreen for analysis
296
+ # @param exception [RemoteException] Exception and context to be sent over
297
+ def post_sqreen_exception(exception)
298
+ post('sqreen_exception', exception.to_hash, {}, 5)
299
+ rescue *NET_ERRORS => e
300
+ Sqreen.log.error(format('Could not post exception (network down? %s) %s',
301
+ e.inspect,
302
+ exception.to_hash.inspect))
303
+ nil
304
+ end
305
+
306
+ BATCH_KEY = 'batch'.freeze
307
+ EVENT_TYPE_KEY = 'event_type'.freeze
308
+ def post_batch(events)
309
+ batch = events.map do |event|
310
+ h = event.to_hash
311
+ h[EVENT_TYPE_KEY] = event_kind(event)
312
+ h
313
+ end
314
+ resilient_post(BATCH_KEY, BATCH_KEY => batch)
315
+ end
316
+
317
+ # Perform agent logout
318
+ # @param retrying [Boolean] whether to try again on error
319
+ def logout(retrying = true)
320
+ # Do not try to connect if we are not connected
321
+ unless connected?
322
+ Sqreen.log.debug('Not connected: not trying to logout')
323
+ return
324
+ end
325
+ # Perform not very resilient logout not to slow down client app shutdown
326
+ get('app-logout', {}, retrying ? 2 : 1)
327
+ Sqreen.logged_in = false
328
+ disconnect
329
+ end
330
+
331
+ protected
332
+
333
+ def event_kind(event)
334
+ case event
335
+ when Sqreen::RemoteException then 'sqreen_exception'
336
+ when Sqreen::Attack then 'attack'
337
+ end
338
+ end
339
+ end
340
+ end