sqreen 0.1.0.pre → 0.7.01461158029

Sign up to get free protection for your applications and to get access to all the features.
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