sqreen 1.17.2 → 1.18.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 +4 -4
- data/CHANGELOG.md +4 -0
- data/lib/sqreen/encoding_sanitizer.rb +22 -0
- data/lib/sqreen/events/request_record.rb +1 -21
- data/lib/sqreen/log.rb +20 -0
- data/lib/sqreen/rules_callbacks.rb +1 -0
- data/lib/sqreen/rules_callbacks/waf.rb +102 -0
- data/lib/sqreen/runtime_infos.rb +26 -1
- data/lib/sqreen/session.rb +10 -10
- data/lib/sqreen/version.rb +1 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a588437667070c3e175082755b4f63a1bcc2a1c1f599142fc56ac2348d05a1bf
|
4
|
+
data.tar.gz: 1dd5d9a28a0d4285c807587d1a0740342561950c85c4fc84e273ced99b90a6c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e2d876f635ceadf46df13484f50ec6dfd418ebece431916330d2319be79f6a0f38c27a73f2a4261809af30fc3d32e6024a68c415b372ad7214b245f2e2443f6
|
7
|
+
data.tar.gz: 38a0ee125e1ded9a6bbddd440f30a3011b7c36051ac7393f5cd8294a128952fd9f7a93735b20496394e77a042a14c95199817fad6598625d5583030f8c50aace
|
data/CHANGELOG.md
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Sqreen
|
2
|
+
class EncodingSanitizer
|
3
|
+
def self.sanitize(obj)
|
4
|
+
case obj
|
5
|
+
when String
|
6
|
+
sanitize_string(obj)
|
7
|
+
when Array
|
8
|
+
obj.map { |e| sanitize(e) }
|
9
|
+
when Hash
|
10
|
+
obj.each_with_object({}) { |(k, v), h| h[k] = sanitize(v) }
|
11
|
+
else
|
12
|
+
obj
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.sanitize_string(s)
|
17
|
+
return s if s.encoding.name == 'UTF-8' && s.valid_encoding?
|
18
|
+
|
19
|
+
s.encode('UTF-16', :invalid => :replace, :undef => :replace).encode('UTF-8')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
require 'json'
|
5
5
|
require 'sqreen/event'
|
6
|
+
require 'sqreen/encoding_sanitizer'
|
6
7
|
|
7
8
|
module Sqreen
|
8
9
|
# When a request is deeemed worthy of being sent to the backend
|
@@ -115,27 +116,6 @@ module Sqreen
|
|
115
116
|
end
|
116
117
|
end
|
117
118
|
|
118
|
-
class EncodingSanitizer
|
119
|
-
def self.sanitize(obj)
|
120
|
-
case obj
|
121
|
-
when String
|
122
|
-
sanitize_string(obj)
|
123
|
-
when Array
|
124
|
-
obj.map { |e| sanitize(e) }
|
125
|
-
when Hash
|
126
|
-
obj.each_with_object({}) { |(k, v), h| h[k] = sanitize(v) }
|
127
|
-
else
|
128
|
-
obj
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
def self.sanitize_string(s)
|
133
|
-
return s if s.encoding.name == 'UTF-8' && s.valid_encoding?
|
134
|
-
|
135
|
-
s.encode('UTF-16', :invalid => :replace, :undef => :replace).encode('UTF-8')
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
119
|
# For redacting sensitive data and avoid having it sent to our servers
|
140
120
|
class SensitiveDataRedactor
|
141
121
|
DEFAULT_SENSITIVE_KEYS = Set.new(%w[password secret passwd authorization api_key apikey access_token]).freeze
|
data/lib/sqreen/log.rb
CHANGED
@@ -62,6 +62,10 @@ module Sqreen
|
|
62
62
|
|
63
63
|
# Wrapper class for sqreen logging
|
64
64
|
class Logger
|
65
|
+
SEVERITY_TO_METHOD = ::Logger::Severity.constants.each_with_object({}) do |s, h|
|
66
|
+
h[::Logger::Severity.const_get(s)] = s.downcase
|
67
|
+
end
|
68
|
+
|
65
69
|
def initialize(desired_level, log_location, force_logger = nil)
|
66
70
|
if force_logger
|
67
71
|
@logger = force_logger
|
@@ -90,6 +94,10 @@ module Sqreen
|
|
90
94
|
@logger.error(msg, &block)
|
91
95
|
end
|
92
96
|
|
97
|
+
def add(severity, msg = nil, &block)
|
98
|
+
send(SEVERITY_TO_METHOD[severity], msg, &block)
|
99
|
+
end
|
100
|
+
|
93
101
|
protected
|
94
102
|
|
95
103
|
def init_logger_output(path)
|
@@ -135,6 +143,10 @@ module Sqreen
|
|
135
143
|
|
136
144
|
def error(_msg = nil); end
|
137
145
|
|
146
|
+
def fatal(_msg = nil); end
|
147
|
+
|
148
|
+
def add(_severity, _msg = nil); end
|
149
|
+
|
138
150
|
def formatter=(_); end
|
139
151
|
end
|
140
152
|
|
@@ -162,6 +174,14 @@ module Sqreen
|
|
162
174
|
@logger.error(msg, &block)
|
163
175
|
end
|
164
176
|
|
177
|
+
def fatal(msg = nil, &block)
|
178
|
+
@logger.error(msg, &block)
|
179
|
+
end
|
180
|
+
|
181
|
+
def add(severity, msg = nil, &block)
|
182
|
+
send(Sqreen::Logger::SEVERITY_TO_METHOD[severity], msg, &block)
|
183
|
+
end
|
184
|
+
|
165
185
|
def formatter=(value)
|
166
186
|
@logger.formatter = value
|
167
187
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'sqreen/rule_attributes'
|
3
|
+
require 'sqreen/binding_accessor'
|
4
|
+
require 'sqreen/rule_callback'
|
5
|
+
require 'sqreen/safe_json'
|
6
|
+
|
7
|
+
module Sqreen
|
8
|
+
module Rules
|
9
|
+
class WAFCB < RuleCB
|
10
|
+
BUDGET_MAX = 5000
|
11
|
+
|
12
|
+
# TODO: move to Dependency
|
13
|
+
begin
|
14
|
+
require 'libsqreen'
|
15
|
+
@libsqreen = true
|
16
|
+
rescue LoadError
|
17
|
+
Sqreen.log.warn('libsqreen gem not found')
|
18
|
+
@libsqreen = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.libsqreen?
|
22
|
+
@libsqreen
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :binding_accessors, :budget, :waf_rule_name
|
26
|
+
|
27
|
+
def initialize(*args)
|
28
|
+
super(*args)
|
29
|
+
@overtimeable = false
|
30
|
+
|
31
|
+
unless WAFCB.libsqreen?
|
32
|
+
Sqreen.log.warn('libsqreen gem not found')
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
36
|
+
unless @data['values']
|
37
|
+
Sqreen.log.warn('no values in data')
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
::LibSqreen::WAF.logger = Sqreen.log
|
42
|
+
|
43
|
+
name = format("%s_%s", SecureRandom.uuid, rule_name)
|
44
|
+
unless @data['values']['waf_rules'] && (::LibSqreen::WAF[name] = @data['values']['waf_rules'])
|
45
|
+
Sqreen.log.error("WAF rule #{name} failed to be set, from #<#{self.class.name}:0x#{object_id.to_s(16).rjust(16, '0')}>")
|
46
|
+
return
|
47
|
+
end
|
48
|
+
@waf_rule_name = name
|
49
|
+
Sqreen.log.debug("WAF rule #{name} set, from #<#{self.class.name}:0x#{object_id.to_s(16).rjust(16, '0')}>")
|
50
|
+
|
51
|
+
@binding_accessors = @data['values'].fetch('binding_accessors', []).each_with_object({}) do |e, h|
|
52
|
+
h[e] = BindingAccessor.new(e)
|
53
|
+
end
|
54
|
+
@budget = @data['values'].fetch('budget', BUDGET_MAX)
|
55
|
+
|
56
|
+
ObjectSpace.define_finalizer(self, WAFCB.finalizer(@waf_rule_name.dup))
|
57
|
+
end
|
58
|
+
|
59
|
+
def pre(instance, args, _budget)
|
60
|
+
unless WAFCB.libsqreen?
|
61
|
+
Sqreen.log.warn('libsqreen not required')
|
62
|
+
return
|
63
|
+
end
|
64
|
+
|
65
|
+
request = framework.request
|
66
|
+
return if !waf_rule_name || !request
|
67
|
+
|
68
|
+
env = [binding, framework, instance, args]
|
69
|
+
|
70
|
+
waf_args = Hash[binding_accessors.map { |e, b| [e, b.resolve(*env)] }]
|
71
|
+
waf_args = Sqreen::EncodingSanitizer.sanitize(waf_args)
|
72
|
+
action, data = ::LibSqreen::WAF.run(waf_rule_name, waf_args, budget)
|
73
|
+
|
74
|
+
case action
|
75
|
+
when :monitor
|
76
|
+
record_event({ 'waf_data' => data })
|
77
|
+
advise_action(nil)
|
78
|
+
when :block
|
79
|
+
record_event({ 'waf_data' => data })
|
80
|
+
advise_action(:raise)
|
81
|
+
when :good
|
82
|
+
advise_action(nil)
|
83
|
+
when :timeout, :invalid_call, :invalid_rule, :invalid_flow, :no_rule
|
84
|
+
Sqreen.log.warn("error from waf: #{action}")
|
85
|
+
advise_action(nil)
|
86
|
+
else
|
87
|
+
Sqreen.log.warn("unexpected action returned from waf")
|
88
|
+
advise_action(nil)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.finalizer(rule_name)
|
93
|
+
lambda do |object_id|
|
94
|
+
return unless WAFCB.libsqreen?
|
95
|
+
|
96
|
+
::LibSqreen::WAF.delete(waf_rule_name, waf_args, budget)
|
97
|
+
Sqreen.log.debug("WAF rule #{rule_name} deleted, from #<#{name}:0x#{object_id.to_s(16).rjust(16, '0')}>")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/sqreen/runtime_infos.rb
CHANGED
@@ -65,7 +65,32 @@ module Sqreen
|
|
65
65
|
{
|
66
66
|
:agent_type => :ruby,
|
67
67
|
:agent_version => ::Sqreen::VERSION,
|
68
|
-
}
|
68
|
+
}.tap do |h|
|
69
|
+
h[:libsqreen_version] = libsqreen_version if libsqreen?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def libsqreen?
|
74
|
+
libsqreen_loaded? && !libsqreen_stub?
|
75
|
+
end
|
76
|
+
|
77
|
+
def libsqreen_loaded?
|
78
|
+
Kernel.const_defined?('LibSqreen')
|
79
|
+
end
|
80
|
+
|
81
|
+
def libsqreen_stub?
|
82
|
+
!::LibSqreen.respond_to?(:version)
|
83
|
+
end
|
84
|
+
|
85
|
+
def libsqreen_version
|
86
|
+
return unless libsqreen?
|
87
|
+
|
88
|
+
@libsqreen_version ||= case (version = ::LibSqreen.version)
|
89
|
+
when Array
|
90
|
+
version.map(&:to_s).join('.')
|
91
|
+
else
|
92
|
+
version
|
93
|
+
end
|
69
94
|
end
|
70
95
|
|
71
96
|
def os
|
data/lib/sqreen/session.rb
CHANGED
@@ -100,7 +100,7 @@ module Sqreen
|
|
100
100
|
rescue StandardError => e
|
101
101
|
Sqreen.log.debug { "Caught exception during request: #{e.inspect}" }
|
102
102
|
Sqreen.log.debug { e.backtrace }
|
103
|
-
Sqreen.log.debug "Cannot connect, retry in #{RETRY_CONNECT_SECONDS} seconds"
|
103
|
+
Sqreen.log.debug { "Cannot connect, retry in #{RETRY_CONNECT_SECONDS} seconds" }
|
104
104
|
sleep RETRY_CONNECT_SECONDS
|
105
105
|
@conn_retry += 1
|
106
106
|
retry
|
@@ -129,7 +129,7 @@ module Sqreen
|
|
129
129
|
raise e if max_retry != RETRY_FOREVER && current_retry >= max_retry || e.is_a?(Sqreen::NotImplementedYet) || e.is_a?(Sqreen::Unauthorized)
|
130
130
|
|
131
131
|
sleep_delay = [MAX_DELAY, retry_request_seconds * current_retry].min
|
132
|
-
Sqreen.log.debug format("Sleeping %ds before retry #{current_retry}/#{max_retry}", sleep_delay)
|
132
|
+
Sqreen.log.debug { format("Sleeping %ds before retry #{current_retry}/#{max_retry}", sleep_delay) }
|
133
133
|
sleep(sleep_delay)
|
134
134
|
|
135
135
|
retry
|
@@ -164,10 +164,10 @@ module Sqreen
|
|
164
164
|
@req_nb += 1
|
165
165
|
|
166
166
|
path = prefix_path(path)
|
167
|
-
Sqreen.log.debug format('%s %s (%s)', method, path, @token)
|
168
167
|
|
169
168
|
payload = {}
|
170
169
|
resiliently(RETRY_REQUEST_SECONDS, max_retry) do
|
170
|
+
Sqreen.log.debug { format('%s %s %s %s (%s)', method, path, JSON.dump(data), headers.inspect, @token) }
|
171
171
|
res = nil
|
172
172
|
MUTEX.synchronize do
|
173
173
|
res = case method.upcase
|
@@ -181,7 +181,7 @@ module Sqreen
|
|
181
181
|
end
|
182
182
|
@con.post(path, json_data, headers)
|
183
183
|
else
|
184
|
-
Sqreen.log.debug format('unknown method %s', method)
|
184
|
+
Sqreen.log.debug { format('unknown method %s', method) }
|
185
185
|
raise Sqreen::NotImplementedYet
|
186
186
|
end
|
187
187
|
end
|
@@ -192,17 +192,17 @@ module Sqreen
|
|
192
192
|
if res['Content-Type'] && res['Content-Type'].start_with?('application/json')
|
193
193
|
payload = JSON.parse(res.body)
|
194
194
|
unless payload['status']
|
195
|
-
Sqreen.log.debug
|
195
|
+
Sqreen.log.debug { format('Cannot %s %s. Parsed response body was: %s', method, path, payload.inspect) }
|
196
196
|
end
|
197
197
|
else
|
198
|
-
Sqreen.log.debug "Unexpected response Content-Type: #{res['Content-Type']}"
|
199
|
-
Sqreen.log.debug "Unexpected response body: #{res.body.inspect}"
|
198
|
+
Sqreen.log.debug { "Unexpected response Content-Type: #{res['Content-Type']}" }
|
199
|
+
Sqreen.log.debug { "Unexpected response body: #{res.body.inspect}" }
|
200
200
|
end
|
201
201
|
else
|
202
|
-
Sqreen.log.debug 'warning: empty return value'
|
202
|
+
Sqreen.log.debug { 'warning: empty return value' }
|
203
203
|
end
|
204
|
+
Sqreen.log.debug { format('%s %s (DONE: %s in %f ms)', method, path, res && res.code, (Time.now.utc - now) * 1000) }
|
204
205
|
end
|
205
|
-
Sqreen.log.debug format('%s %s (DONE in %f ms)', method, path, (Time.now.utc - now) * 1000)
|
206
206
|
payload
|
207
207
|
end
|
208
208
|
|
@@ -233,7 +233,7 @@ module Sqreen
|
|
233
233
|
end
|
234
234
|
Sqreen.log.info 'Login success.'
|
235
235
|
@session_id = res['session_id']
|
236
|
-
Sqreen.log.debug "received session_id #{@session_id}"
|
236
|
+
Sqreen.log.debug { "received session_id #{@session_id}" }
|
237
237
|
Sqreen.logged_in = true
|
238
238
|
res
|
239
239
|
end
|
data/lib/sqreen/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sqreen
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.18.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sqreen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sq_mini_racer
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 0.2.4.sqreen2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: libsqreen
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.3.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.3.0.0
|
27
41
|
description: Sqreen is a SaaS based Application protection and monitoring platform
|
28
42
|
that integrates directly into your Ruby applications. Learn more at https://sqreen.io.
|
29
43
|
email: contact@sqreen.io
|
@@ -64,6 +78,7 @@ files:
|
|
64
78
|
- lib/sqreen/dependency/rails.rb
|
65
79
|
- lib/sqreen/dependency/sentry.rb
|
66
80
|
- lib/sqreen/dependency/sinatra.rb
|
81
|
+
- lib/sqreen/encoding_sanitizer.rb
|
67
82
|
- lib/sqreen/event.rb
|
68
83
|
- lib/sqreen/events/attack.rb
|
69
84
|
- lib/sqreen/events/remote_exception.rb
|
@@ -126,6 +141,7 @@ files:
|
|
126
141
|
- lib/sqreen/rules_callbacks/shell_env.rb
|
127
142
|
- lib/sqreen/rules_callbacks/url_matches.rb
|
128
143
|
- lib/sqreen/rules_callbacks/user_agent_matches.rb
|
144
|
+
- lib/sqreen/rules_callbacks/waf.rb
|
129
145
|
- lib/sqreen/rules_signature.rb
|
130
146
|
- lib/sqreen/runner.rb
|
131
147
|
- lib/sqreen/runtime_infos.rb
|