sqreen 0.7.01461158029-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- 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 +143 -0
@@ -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
|