sqreen-alt 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/README.md +77 -0
- data/Rakefile +20 -0
- data/lib/sqreen-alt.rb +1 -0
- data/lib/sqreen.rb +68 -0
- data/lib/sqreen/attack_detected.html +2 -0
- data/lib/sqreen/binding_accessor.rb +288 -0
- data/lib/sqreen/ca.crt +72 -0
- data/lib/sqreen/call_countable.rb +67 -0
- data/lib/sqreen/callback_tree.rb +78 -0
- data/lib/sqreen/callbacks.rb +100 -0
- data/lib/sqreen/capped_queue.rb +23 -0
- data/lib/sqreen/condition_evaluator.rb +235 -0
- data/lib/sqreen/conditionable.rb +50 -0
- data/lib/sqreen/configuration.rb +168 -0
- data/lib/sqreen/context.rb +26 -0
- data/lib/sqreen/deliveries/batch.rb +84 -0
- data/lib/sqreen/deliveries/simple.rb +39 -0
- data/lib/sqreen/event.rb +16 -0
- data/lib/sqreen/events/attack.rb +61 -0
- data/lib/sqreen/events/remote_exception.rb +54 -0
- data/lib/sqreen/events/request_record.rb +62 -0
- data/lib/sqreen/exception.rb +34 -0
- data/lib/sqreen/frameworks.rb +40 -0
- data/lib/sqreen/frameworks/generic.rb +446 -0
- data/lib/sqreen/frameworks/rails.rb +148 -0
- data/lib/sqreen/frameworks/rails3.rb +36 -0
- data/lib/sqreen/frameworks/request_recorder.rb +69 -0
- data/lib/sqreen/frameworks/sinatra.rb +57 -0
- data/lib/sqreen/frameworks/sqreen_test.rb +26 -0
- data/lib/sqreen/instrumentation.rb +542 -0
- data/lib/sqreen/log.rb +119 -0
- data/lib/sqreen/metrics.rb +6 -0
- data/lib/sqreen/metrics/average.rb +39 -0
- data/lib/sqreen/metrics/base.rb +45 -0
- data/lib/sqreen/metrics/collect.rb +22 -0
- data/lib/sqreen/metrics/sum.rb +20 -0
- data/lib/sqreen/metrics_store.rb +96 -0
- data/lib/sqreen/middleware.rb +34 -0
- data/lib/sqreen/payload_creator.rb +137 -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 +93 -0
- data/lib/sqreen/rule_attributes.rb +26 -0
- data/lib/sqreen/rule_callback.rb +108 -0
- data/lib/sqreen/rules.rb +126 -0
- data/lib/sqreen/rules_callbacks.rb +29 -0
- data/lib/sqreen/rules_callbacks/binding_accessor_matcher.rb +77 -0
- data/lib/sqreen/rules_callbacks/binding_accessor_metrics.rb +79 -0
- data/lib/sqreen/rules_callbacks/blacklist_ips.rb +44 -0
- data/lib/sqreen/rules_callbacks/count_http_codes.rb +40 -0
- data/lib/sqreen/rules_callbacks/crawler_user_agent_matches.rb +24 -0
- data/lib/sqreen/rules_callbacks/crawler_user_agent_matches_metrics.rb +24 -0
- data/lib/sqreen/rules_callbacks/custom_error.rb +64 -0
- data/lib/sqreen/rules_callbacks/execjs.rb +241 -0
- data/lib/sqreen/rules_callbacks/headers_insert.rb +22 -0
- data/lib/sqreen/rules_callbacks/inspect_rule.rb +25 -0
- data/lib/sqreen/rules_callbacks/matcher_rule.rb +138 -0
- data/lib/sqreen/rules_callbacks/rails_parameters.rb +14 -0
- data/lib/sqreen/rules_callbacks/record_request_context.rb +39 -0
- data/lib/sqreen/rules_callbacks/reflected_xss.rb +254 -0
- data/lib/sqreen/rules_callbacks/regexp_rule.rb +36 -0
- data/lib/sqreen/rules_callbacks/shell_env.rb +32 -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 +151 -0
- data/lib/sqreen/runner.rb +365 -0
- data/lib/sqreen/runtime_infos.rb +138 -0
- data/lib/sqreen/safe_json.rb +60 -0
- data/lib/sqreen/sdk.rb +22 -0
- data/lib/sqreen/serializer.rb +46 -0
- data/lib/sqreen/session.rb +317 -0
- data/lib/sqreen/shared_storage.rb +31 -0
- data/lib/sqreen/stats.rb +18 -0
- data/lib/sqreen/version.rb +5 -0
- metadata +148 -0
@@ -0,0 +1,138 @@
|
|
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
|
+
require 'digest/sha1'
|
9
|
+
|
10
|
+
module Sqreen
|
11
|
+
module RuntimeInfos
|
12
|
+
module_function
|
13
|
+
|
14
|
+
def all(framework)
|
15
|
+
res = { :various_infos => {} }
|
16
|
+
res.merge! agent
|
17
|
+
res.merge! os
|
18
|
+
res.merge! runtime
|
19
|
+
res.merge! framework.framework_infos
|
20
|
+
res[:bundle_signature] = dependencies_signature
|
21
|
+
res[:various_infos].merge! time
|
22
|
+
res[:various_infos].merge! process
|
23
|
+
res
|
24
|
+
end
|
25
|
+
|
26
|
+
def local_infos
|
27
|
+
{
|
28
|
+
'time' => Time.now.utc,
|
29
|
+
'name' => hostname,
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def dependencies
|
34
|
+
gem_info = Gem.loaded_specs
|
35
|
+
gem_info.map do |name, spec|
|
36
|
+
{
|
37
|
+
:name => name,
|
38
|
+
:version => spec.version.to_s,
|
39
|
+
:homepage => spec.homepage,
|
40
|
+
:source => (extract_source(spec.source) if spec.respond_to?(:source)),
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def time
|
46
|
+
# FIXME: That should maybe be called local-time
|
47
|
+
{ :time => Time.now }
|
48
|
+
end
|
49
|
+
|
50
|
+
def ssl
|
51
|
+
type = nil
|
52
|
+
version = nil
|
53
|
+
if defined? OpenSSL
|
54
|
+
type = 'OpenSSL'
|
55
|
+
version = OpenSSL::OPENSSL_VERSION if defined? OpenSSL::OPENSSL_VERSION
|
56
|
+
end
|
57
|
+
{ :ssl =>
|
58
|
+
{
|
59
|
+
:type => type,
|
60
|
+
:version => version,
|
61
|
+
} }
|
62
|
+
end
|
63
|
+
|
64
|
+
def agent
|
65
|
+
{
|
66
|
+
:agent_type => :ruby,
|
67
|
+
:agent_version => ::Sqreen::VERSION,
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def os
|
72
|
+
plat = if defined? ::RUBY_PLATFORM
|
73
|
+
::RUBY_PLATFORM
|
74
|
+
elsif defined? ::PLATFORM
|
75
|
+
::PLATFORM
|
76
|
+
else
|
77
|
+
''
|
78
|
+
end
|
79
|
+
{
|
80
|
+
:os_type => plat,
|
81
|
+
:hostname => hostname,
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def hostname
|
86
|
+
Socket.gethostname
|
87
|
+
end
|
88
|
+
|
89
|
+
def process
|
90
|
+
{
|
91
|
+
:pid => Process.pid,
|
92
|
+
:ppid => Process.ppid,
|
93
|
+
:euid => Process.euid,
|
94
|
+
:egid => Process.egid,
|
95
|
+
:uid => Process.uid,
|
96
|
+
:gid => Process.gid,
|
97
|
+
:name => $0,
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def runtime
|
102
|
+
engine = if defined? ::RUBY_ENGINE
|
103
|
+
::RUBY_ENGINE
|
104
|
+
else
|
105
|
+
'ruby'
|
106
|
+
end
|
107
|
+
{
|
108
|
+
:runtime_type => engine,
|
109
|
+
:runtime_version => ::RUBY_DESCRIPTION,
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def dependencies_signature
|
114
|
+
calculate_dependencies_signature(dependencies)
|
115
|
+
end
|
116
|
+
|
117
|
+
def calculate_dependencies_signature(pkgs)
|
118
|
+
return nil if pkgs.nil? || pkgs.empty?
|
119
|
+
sha1 = Digest::SHA1.new
|
120
|
+
pkgs.map { |pkg| [pkg[:name], pkg[:version]] }.sort.each_with_index do |p, i|
|
121
|
+
sha1 << format(i.zero? ? '%s-%s' : '|%s-%s', *p)
|
122
|
+
end
|
123
|
+
sha1.hexdigest
|
124
|
+
end
|
125
|
+
|
126
|
+
def extract_source(source)
|
127
|
+
return nil unless source
|
128
|
+
ret = { 'name' => source.class.name.split(':')[-1] }
|
129
|
+
opts = {}
|
130
|
+
opts = source.options if source.respond_to?(:options)
|
131
|
+
ret['remotes'] = opts['remotes'] if opts['remotes']
|
132
|
+
ret['uri'] = opts['uri'] if opts['uri']
|
133
|
+
# FIXME: scrub any auth data in uris
|
134
|
+
ret['path'] = opts['path'].to_s if opts['path']
|
135
|
+
ret
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,60 @@
|
|
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 'json'
|
5
|
+
|
6
|
+
require 'sqreen/log'
|
7
|
+
|
8
|
+
module Sqreen
|
9
|
+
# Safely dump datastructure in json (more resilient to encoding errors)
|
10
|
+
class SafeJSON
|
11
|
+
def self.dump(data)
|
12
|
+
JSON.generate(data)
|
13
|
+
rescue JSON::GeneratorError, Encoding::UndefinedConversionError
|
14
|
+
Sqreen.log.debug('Payload could not be encoded enforcing recode')
|
15
|
+
JSON.generate(rencode_payload(data))
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.rencode_payload(obj, max_depth = 20)
|
19
|
+
max_depth -= 1
|
20
|
+
return obj if max_depth < 0
|
21
|
+
return rencode_array(obj, max_depth) if obj.is_a?(Array)
|
22
|
+
return enforce_encoding(obj) unless obj.is_a?(Hash)
|
23
|
+
nobj = {}
|
24
|
+
obj.each do |k, v|
|
25
|
+
safe_k = rencode_payload(k, max_depth)
|
26
|
+
nobj[safe_k] = case v
|
27
|
+
when Array
|
28
|
+
rencode_array(v, max_depth)
|
29
|
+
when Hash
|
30
|
+
rencode_payload(v, max_depth)
|
31
|
+
when String
|
32
|
+
enforce_encoding(v)
|
33
|
+
else # for example integers
|
34
|
+
v
|
35
|
+
end
|
36
|
+
end
|
37
|
+
nobj
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.rencode_array(array, max_depth)
|
41
|
+
array.map! { |e| rencode_payload(e, max_depth - 1) }
|
42
|
+
array
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.enforce_encoding(str)
|
46
|
+
return str unless str.is_a?(String)
|
47
|
+
return str if str.ascii_only?
|
48
|
+
encoded8bit = str.encoding.name == 'ASCII-8BIT'
|
49
|
+
return str if !encoded8bit && str.valid_encoding?
|
50
|
+
r = str.chars.map do |v|
|
51
|
+
if !v.valid_encoding? || (encoded8bit && !v.ascii_only?)
|
52
|
+
v.bytes.map { |c| "\\x#{c.to_s(16).upcase}" }.join
|
53
|
+
else
|
54
|
+
v
|
55
|
+
end
|
56
|
+
end.join
|
57
|
+
"SqBytes[#{r}]"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/sqreen/sdk.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
# Sqreen Namespace
|
5
|
+
module Sqreen
|
6
|
+
# Sqreen SDK
|
7
|
+
class << self
|
8
|
+
# Authentication tracking method
|
9
|
+
def auth_track(is_logged_in, authentication_keys); end
|
10
|
+
|
11
|
+
def signup_track(authentication_keys); end
|
12
|
+
|
13
|
+
def identify(authentication_keys, traits = {})
|
14
|
+
return unless Sqreen.framework
|
15
|
+
Sqreen.framework.observe(
|
16
|
+
:sdk,
|
17
|
+
[:identify, Time.now, authentication_keys, traits],
|
18
|
+
[], false
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,46 @@
|
|
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
|
+
module Sqreen
|
5
|
+
# Serialization functions: convert Hash -> simple ruby types
|
6
|
+
module Serializer
|
7
|
+
# Serialize a deep hash/array to more simple types
|
8
|
+
def self.serialize(obj, max_depth = 10)
|
9
|
+
if obj.is_a?(Array)
|
10
|
+
new_obj = []
|
11
|
+
i = -1
|
12
|
+
to_do = obj.map { |v| [new_obj, i += 1, v, 0] }
|
13
|
+
else
|
14
|
+
new_obj = {}
|
15
|
+
to_do = obj.map { |k, v| [new_obj, k, v, 0] }
|
16
|
+
end
|
17
|
+
until to_do.empty?
|
18
|
+
where, key, value, deepness = to_do.pop
|
19
|
+
safe_key = key.kind_of?(Integer) ? key : key.to_s
|
20
|
+
if value.is_a?(Hash) && deepness < max_depth
|
21
|
+
where[safe_key] = {}
|
22
|
+
to_do += value.map { |k, v| [where[safe_key], k, v, deepness + 1] }
|
23
|
+
elsif value.is_a?(Array) && deepness < max_depth
|
24
|
+
where[safe_key] = []
|
25
|
+
i = -1
|
26
|
+
to_do += value.map { |v| [where[safe_key], i += 1, v, deepness + 1] }
|
27
|
+
else
|
28
|
+
case value
|
29
|
+
when Symbol
|
30
|
+
where[safe_key] = value.to_s
|
31
|
+
when Rational
|
32
|
+
where[safe_key] = value.to_f
|
33
|
+
when Time
|
34
|
+
where[safe_key] = value.iso8601
|
35
|
+
when Numeric, String, TrueClass, FalseClass, NilClass
|
36
|
+
where[safe_key] = value
|
37
|
+
else
|
38
|
+
where[safe_key] = "#{value.class.name}:#{value.inspect}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
new_obj
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,317 @@
|
|
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/serializer'
|
6
|
+
require 'sqreen/runtime_infos'
|
7
|
+
require 'sqreen/events/remote_exception'
|
8
|
+
require 'sqreen/events/attack'
|
9
|
+
require 'sqreen/events/request_record'
|
10
|
+
require 'sqreen/exception'
|
11
|
+
require 'sqreen/safe_json'
|
12
|
+
|
13
|
+
require 'net/https'
|
14
|
+
require 'uri'
|
15
|
+
require 'openssl'
|
16
|
+
require 'zlib'
|
17
|
+
|
18
|
+
# $ curl -H"x-api-key: ${KEY}" http://127.0.0.1:5000/sqreen/v0/app-login
|
19
|
+
# {
|
20
|
+
# "session_id": "c9171007c27d4da8906312ff343ed41307f65b2f6fdf4a05a445bb7016186657",
|
21
|
+
# "status": true
|
22
|
+
# }
|
23
|
+
#
|
24
|
+
# $ curl -H"x-session-key: ${SESS}" http://127.0.0.1:5000/sqreen/v0/get-rulespack
|
25
|
+
|
26
|
+
#
|
27
|
+
# FIXME: we should be proxy capable
|
28
|
+
# FIXME: we should be multithread aware (when callbacks perform server requests?)
|
29
|
+
#
|
30
|
+
|
31
|
+
module Sqreen
|
32
|
+
class Session
|
33
|
+
RETRY_CONNECT_SECONDS = 10
|
34
|
+
RETRY_REQUEST_SECONDS = 10
|
35
|
+
|
36
|
+
MAX_DELAY = 60 * 30
|
37
|
+
|
38
|
+
RETRY_LONG = 128
|
39
|
+
|
40
|
+
MUTEX = Mutex.new
|
41
|
+
METRICS_KEY = 'metrics'.freeze
|
42
|
+
|
43
|
+
@@path_prefix = '/sqreen/v0/'
|
44
|
+
|
45
|
+
attr_accessor :request_compression
|
46
|
+
|
47
|
+
def initialize(server_url, token)
|
48
|
+
@token = token
|
49
|
+
@session_id = nil
|
50
|
+
@server_url = server_url
|
51
|
+
@request_compression = false
|
52
|
+
@connected = nil
|
53
|
+
@con = nil
|
54
|
+
|
55
|
+
uri = parse_uri(server_url)
|
56
|
+
use_ssl = (uri.scheme == 'https')
|
57
|
+
|
58
|
+
@req_nb = 0
|
59
|
+
|
60
|
+
@http = Net::HTTP.new(uri.host, uri.port)
|
61
|
+
@http.use_ssl = use_ssl
|
62
|
+
if use_ssl
|
63
|
+
cert_file = File.join(File.dirname(__FILE__), 'ca.crt')
|
64
|
+
cert_store = OpenSSL::X509::Store.new
|
65
|
+
cert_store.add_file cert_file
|
66
|
+
@http.cert_store = cert_store
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_uri(uri)
|
71
|
+
# This regexp is the Ruby constant URI::PATTERN::HOSTNAME augmented
|
72
|
+
# with the _ character that is frequent in Docker linked containers.
|
73
|
+
re = '(?:(?:[a-zA-Z\\d](?:[-_a-zA-Z\\d]*[a-zA-Z\\d])?)\\.)*(?:[a-zA-Z](?:[-_a-zA-Z\\d]*[a-zA-Z\\d])?)\\.?'
|
74
|
+
parser = URI::Parser.new :HOSTNAME => re
|
75
|
+
parser.parse(uri)
|
76
|
+
end
|
77
|
+
|
78
|
+
def prefix_path(path)
|
79
|
+
return '/sqreen/v1/' + path if path == 'app-login' || path == 'app-beat'
|
80
|
+
@@path_prefix + path
|
81
|
+
end
|
82
|
+
|
83
|
+
def connected?
|
84
|
+
@con && @con.started?
|
85
|
+
end
|
86
|
+
|
87
|
+
def disconnect
|
88
|
+
@http.finish if connected?
|
89
|
+
end
|
90
|
+
|
91
|
+
NET_ERRORS = [Timeout::Error,
|
92
|
+
Errno::EINVAL,
|
93
|
+
Errno::ECONNRESET,
|
94
|
+
Errno::ECONNREFUSED,
|
95
|
+
EOFError,
|
96
|
+
Net::HTTPBadResponse,
|
97
|
+
Net::HTTPHeaderSyntaxError,
|
98
|
+
SocketError,
|
99
|
+
Net::ProtocolError].freeze
|
100
|
+
|
101
|
+
def connect
|
102
|
+
return if connected?
|
103
|
+
Sqreen.log.warn "connection to #{@server_url}..."
|
104
|
+
@session_id = nil
|
105
|
+
@conn_retry = 0
|
106
|
+
begin
|
107
|
+
@con = @http.start
|
108
|
+
rescue *NET_ERRORS
|
109
|
+
Sqreen.log.debug "Cannot connect, retry in #{RETRY_CONNECT_SECONDS} seconds"
|
110
|
+
sleep RETRY_CONNECT_SECONDS
|
111
|
+
@conn_retry += 1
|
112
|
+
retry
|
113
|
+
else
|
114
|
+
Sqreen.log.warn 'connection success.'
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def resilient_post(path, data, headers = {})
|
119
|
+
post(path, data, headers, RETRY_LONG)
|
120
|
+
end
|
121
|
+
|
122
|
+
def resilient_get(path, headers = {})
|
123
|
+
get(path, headers, RETRY_LONG)
|
124
|
+
end
|
125
|
+
|
126
|
+
def post(path, data, headers = {}, max_retry = 2)
|
127
|
+
do_http_request(:POST, path, data, headers, max_retry)
|
128
|
+
end
|
129
|
+
|
130
|
+
def get(path, headers = {}, max_retry = 2)
|
131
|
+
do_http_request(:GET, path, nil, headers, max_retry)
|
132
|
+
end
|
133
|
+
|
134
|
+
def resiliently(retry_request_seconds, max_retry, current_retry = 0)
|
135
|
+
return yield
|
136
|
+
rescue => e
|
137
|
+
Sqreen.log.debug(e.inspect)
|
138
|
+
|
139
|
+
current_retry += 1
|
140
|
+
|
141
|
+
raise e if current_retry >= max_retry || e.is_a?(Sqreen::NotImplementedYet)
|
142
|
+
|
143
|
+
sleep_delay = [MAX_DELAY, retry_request_seconds * current_retry].min
|
144
|
+
Sqreen.log.debug format('Sleeping %ds', sleep_delay)
|
145
|
+
sleep(sleep_delay)
|
146
|
+
|
147
|
+
retry
|
148
|
+
end
|
149
|
+
|
150
|
+
def thread_id
|
151
|
+
th = Thread.current
|
152
|
+
return '' unless th
|
153
|
+
re = th.to_s.scan(/:(0x.*)>/)
|
154
|
+
return '' unless re && !re.empty?
|
155
|
+
res = re[0]
|
156
|
+
return '' unless res && !res.empty?
|
157
|
+
res[0]
|
158
|
+
end
|
159
|
+
|
160
|
+
def do_http_request(method, path, data, headers = {}, max_retry = 2)
|
161
|
+
connect unless connected?
|
162
|
+
headers['X-Session-Key'] = @session_id if @session_id
|
163
|
+
headers['X-Sqreen-Time'] = Time.now.utc.to_f.to_s
|
164
|
+
headers['User-Agent'] = "sqreen-ruby/#{Sqreen::VERSION}"
|
165
|
+
headers['X-Sqreen-Beta'] = format('pid=%d;tid=%s;nb=%d;t=%f',
|
166
|
+
Process.pid,
|
167
|
+
thread_id,
|
168
|
+
@req_nb,
|
169
|
+
Time.now.utc.to_f)
|
170
|
+
headers['Content-Type'] = 'application/json'
|
171
|
+
if request_compression && !method.casecmp(:GET).zero?
|
172
|
+
headers['Content-Encoding'] = 'gzip'
|
173
|
+
end
|
174
|
+
|
175
|
+
@req_nb += 1
|
176
|
+
|
177
|
+
path = prefix_path(path)
|
178
|
+
Sqreen.log.debug format('%s %s (%s)', method, path, @token)
|
179
|
+
|
180
|
+
res = {}
|
181
|
+
resiliently(RETRY_REQUEST_SECONDS, max_retry) do
|
182
|
+
json = nil
|
183
|
+
MUTEX.synchronize do
|
184
|
+
json = case method.upcase
|
185
|
+
when :GET
|
186
|
+
@con.get(path, headers)
|
187
|
+
when :POST
|
188
|
+
json_data = nil
|
189
|
+
unless data.nil?
|
190
|
+
serialized = Serializer.serialize(data)
|
191
|
+
json_data = compress(SafeJSON.dump(serialized))
|
192
|
+
end
|
193
|
+
@con.post(path, json_data, headers)
|
194
|
+
else
|
195
|
+
Sqreen.log.debug format('unknown method %s', method)
|
196
|
+
raise Sqreen::NotImplementedYet
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
if json && json.body
|
201
|
+
res = JSON.parse(json.body)
|
202
|
+
unless res['status']
|
203
|
+
Sqreen.log.debug(format('Cannot %s %s.', method, path))
|
204
|
+
end
|
205
|
+
else
|
206
|
+
Sqreen.log.debug 'warning: empty return value'
|
207
|
+
end
|
208
|
+
end
|
209
|
+
Sqreen.log.debug format('%s %s (DONE)', method, path)
|
210
|
+
res
|
211
|
+
end
|
212
|
+
|
213
|
+
def compress(data)
|
214
|
+
return data unless request_compression
|
215
|
+
out = StringIO.new
|
216
|
+
w = Zlib::GzipWriter.new(out)
|
217
|
+
w.write(data)
|
218
|
+
w.close
|
219
|
+
out.string
|
220
|
+
end
|
221
|
+
|
222
|
+
def login(framework)
|
223
|
+
headers = { 'x-api-key' => @token }
|
224
|
+
|
225
|
+
res = resilient_post('app-login', RuntimeInfos.all(framework), headers)
|
226
|
+
|
227
|
+
if !res || !res['status']
|
228
|
+
public_error = format('Cannot login. Token may be invalid: %s', @token)
|
229
|
+
Sqreen.log.error public_error
|
230
|
+
raise(Sqreen::TokenInvalidException,
|
231
|
+
format('invalid response: %s', res.inspect))
|
232
|
+
end
|
233
|
+
Sqreen.log.info 'Login success.'
|
234
|
+
@session_id = res['session_id']
|
235
|
+
Sqreen.log.debug "received session_id #{@session_id}"
|
236
|
+
Sqreen.logged_in = true
|
237
|
+
res
|
238
|
+
end
|
239
|
+
|
240
|
+
def rules
|
241
|
+
resilient_get('rulespack')
|
242
|
+
end
|
243
|
+
|
244
|
+
def heartbeat(cmd_res = {}, metrics = [])
|
245
|
+
payload = {}
|
246
|
+
payload['metrics'] = metrics unless metrics.nil? || metrics.empty?
|
247
|
+
payload['command_results'] = cmd_res unless cmd_res.nil? || cmd_res.empty?
|
248
|
+
|
249
|
+
post('app-beat', payload.empty? ? nil : payload, {}, 5)
|
250
|
+
end
|
251
|
+
|
252
|
+
def post_metrics(metrics)
|
253
|
+
return if metrics.nil? || metrics.empty?
|
254
|
+
payload = { METRICS_KEY => metrics }
|
255
|
+
resilient_post(METRICS_KEY, payload)
|
256
|
+
end
|
257
|
+
|
258
|
+
def post_attack(attack)
|
259
|
+
resilient_post('attack', attack.to_hash)
|
260
|
+
end
|
261
|
+
|
262
|
+
def post_bundle(bundle_sig, dependencies)
|
263
|
+
resilient_post('bundle', 'bundle_signature' => bundle_sig,
|
264
|
+
'dependencies' => dependencies)
|
265
|
+
end
|
266
|
+
|
267
|
+
def post_request_record(request_record)
|
268
|
+
resilient_post('request_record', request_record.to_hash)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Post an exception to Sqreen for analysis
|
272
|
+
# @param exception [RemoteException] Exception and context to be sent over
|
273
|
+
def post_sqreen_exception(exception)
|
274
|
+
post('sqreen_exception', exception.to_hash, {}, 5)
|
275
|
+
rescue *NET_ERRORS => e
|
276
|
+
Sqreen.log.warn(format('Could not post exception (network down? %s) %s',
|
277
|
+
e.inspect,
|
278
|
+
exception.to_hash.inspect))
|
279
|
+
nil
|
280
|
+
end
|
281
|
+
|
282
|
+
BATCH_KEY = 'batch'.freeze
|
283
|
+
EVENT_TYPE_KEY = 'event_type'.freeze
|
284
|
+
def post_batch(events)
|
285
|
+
batch = events.map do |event|
|
286
|
+
h = event.to_hash
|
287
|
+
h[EVENT_TYPE_KEY] = event_kind(event)
|
288
|
+
h
|
289
|
+
end
|
290
|
+
resilient_post(BATCH_KEY, BATCH_KEY => batch)
|
291
|
+
end
|
292
|
+
|
293
|
+
# Perform agent logout
|
294
|
+
# @param retrying [Boolean] whether to try again on error
|
295
|
+
def logout(retrying = true)
|
296
|
+
# Do not try to connect if we are not connected
|
297
|
+
unless connected?
|
298
|
+
Sqreen.log.debug('Not connected: not trying to logout')
|
299
|
+
return
|
300
|
+
end
|
301
|
+
# Perform not very resilient logout not to slow down client app shutdown
|
302
|
+
get('app-logout', {}, retrying ? 2 : 1)
|
303
|
+
Sqreen.logged_in = false
|
304
|
+
disconnect
|
305
|
+
end
|
306
|
+
|
307
|
+
protected
|
308
|
+
|
309
|
+
def event_kind(event)
|
310
|
+
case event
|
311
|
+
when Sqreen::RemoteException then 'sqreen_exception'
|
312
|
+
when Sqreen::Attack then 'attack'
|
313
|
+
when Sqreen::RequestRecord then 'request_record'
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|