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.
- checksums.yaml +4 -4
- data/CODE_OF_CONDUCT.md +22 -0
- data/README.md +77 -0
- data/Rakefile +40 -0
- data/lib/sqreen.rb +67 -0
- data/lib/sqreen/binding_accessor.rb +184 -0
- data/lib/sqreen/ca.crt +72 -0
- data/lib/sqreen/callback_tree.rb +78 -0
- data/lib/sqreen/callbacks.rb +120 -0
- data/lib/sqreen/capped_queue.rb +23 -0
- data/lib/sqreen/condition_evaluator.rb +169 -0
- data/lib/sqreen/conditionable.rb +50 -0
- data/lib/sqreen/configuration.rb +151 -0
- data/lib/sqreen/context.rb +22 -0
- data/lib/sqreen/deliveries/batch.rb +80 -0
- data/lib/sqreen/deliveries/simple.rb +36 -0
- data/lib/sqreen/detect.rb +14 -0
- data/lib/sqreen/detect/shell_injection.rb +61 -0
- data/lib/sqreen/detect/sql_injection.rb +115 -0
- data/lib/sqreen/event.rb +16 -0
- data/lib/sqreen/events/attack.rb +60 -0
- data/lib/sqreen/events/remote_exception.rb +53 -0
- data/lib/sqreen/exception.rb +31 -0
- data/lib/sqreen/frameworks.rb +40 -0
- data/lib/sqreen/frameworks/generic.rb +243 -0
- data/lib/sqreen/frameworks/rails.rb +155 -0
- data/lib/sqreen/frameworks/rails3.rb +36 -0
- data/lib/sqreen/frameworks/sinatra.rb +34 -0
- data/lib/sqreen/frameworks/sqreen_test.rb +26 -0
- data/lib/sqreen/instrumentation.rb +504 -0
- data/lib/sqreen/log.rb +116 -0
- data/lib/sqreen/metrics.rb +6 -0
- data/lib/sqreen/metrics/average.rb +39 -0
- data/lib/sqreen/metrics/base.rb +41 -0
- data/lib/sqreen/metrics/collect.rb +22 -0
- data/lib/sqreen/metrics/sum.rb +20 -0
- data/lib/sqreen/metrics_store.rb +94 -0
- data/lib/sqreen/parsers/sql.rb +98 -0
- data/lib/sqreen/parsers/sql_tokenizer.rb +266 -0
- data/lib/sqreen/parsers/unix.rb +110 -0
- data/lib/sqreen/payload_creator.rb +132 -0
- data/lib/sqreen/performance_notifications.rb +86 -0
- data/lib/sqreen/performance_notifications/log.rb +36 -0
- data/lib/sqreen/performance_notifications/metrics.rb +36 -0
- data/lib/sqreen/performance_notifications/newrelic.rb +36 -0
- data/lib/sqreen/remote_command.rb +82 -0
- data/lib/sqreen/rule_attributes.rb +25 -0
- data/lib/sqreen/rule_callback.rb +97 -0
- data/lib/sqreen/rules.rb +116 -0
- data/lib/sqreen/rules_callbacks.rb +29 -0
- data/lib/sqreen/rules_callbacks/binding_accessor_metrics.rb +79 -0
- data/lib/sqreen/rules_callbacks/count_http_codes.rb +18 -0
- data/lib/sqreen/rules_callbacks/crawler_user_agent_matches.rb +24 -0
- data/lib/sqreen/rules_callbacks/crawler_user_agent_matches_metrics.rb +25 -0
- data/lib/sqreen/rules_callbacks/execjs.rb +136 -0
- data/lib/sqreen/rules_callbacks/headers_insert.rb +20 -0
- data/lib/sqreen/rules_callbacks/inspect_rule.rb +20 -0
- data/lib/sqreen/rules_callbacks/matcher_rule.rb +103 -0
- data/lib/sqreen/rules_callbacks/rails_parameters.rb +14 -0
- data/lib/sqreen/rules_callbacks/record_request_context.rb +23 -0
- data/lib/sqreen/rules_callbacks/reflected_xss.rb +40 -0
- data/lib/sqreen/rules_callbacks/regexp_rule.rb +36 -0
- data/lib/sqreen/rules_callbacks/shell.rb +33 -0
- data/lib/sqreen/rules_callbacks/shell_env.rb +32 -0
- data/lib/sqreen/rules_callbacks/sql.rb +41 -0
- data/lib/sqreen/rules_callbacks/system_shell.rb +25 -0
- data/lib/sqreen/rules_callbacks/url_matches.rb +25 -0
- data/lib/sqreen/rules_callbacks/user_agent_matches.rb +22 -0
- data/lib/sqreen/rules_signature.rb +142 -0
- data/lib/sqreen/runner.rb +312 -0
- data/lib/sqreen/runtime_infos.rb +127 -0
- data/lib/sqreen/session.rb +340 -0
- data/lib/sqreen/stats.rb +18 -0
- data/lib/sqreen/version.rb +6 -0
- 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
|