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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a76d5247720780e68021b3bf5b252d5ce95bb141bec396eb4dfbd32858c4cbc3
4
- data.tar.gz: 84a2aa50afcff20f86a4232bf62a9a90c468d71c5eff8caf8028554721bcd965
3
+ metadata.gz: a588437667070c3e175082755b4f63a1bcc2a1c1f599142fc56ac2348d05a1bf
4
+ data.tar.gz: 1dd5d9a28a0d4285c807587d1a0740342561950c85c4fc84e273ced99b90a6c7
5
5
  SHA512:
6
- metadata.gz: 0de1fbb01aadba0da7fe66a12aba82b5e388fd0d1a2a1c66da27d5ed4a09e1f211218a65c84718d0406215a70a8448dd1cf26e524d3920593e911e6a9417faef
7
- data.tar.gz: 8fe62157f44cd084bcaf090782f1e28f70b3d274af2498d2625e25eba9c82b56986be087ab600be70b22730247dc15c964a6a784d16117d51a3aaf76464aa87f
6
+ metadata.gz: 4e2d876f635ceadf46df13484f50ec6dfd418ebece431916330d2319be79f6a0f38c27a73f2a4261809af30fc3d32e6024a68c415b372ad7214b245f2e2443f6
7
+ data.tar.gz: 38a0ee125e1ded9a6bbddd440f30a3011b7c36051ac7393f5cd8294a128952fd9f7a93735b20496394e77a042a14c95199817fad6598625d5583030f8c50aace
@@ -1,3 +1,7 @@
1
+ ## 1.18.0
2
+
3
+ * Support In-App WAF
4
+
1
5
  ## 1.17.2
2
6
 
3
7
  * Support Rails 6.0 (single database)
@@ -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
@@ -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
@@ -31,3 +31,4 @@ require 'sqreen/rules_callbacks/devise_auth_track'
31
31
  require 'sqreen/rules_callbacks/devise_signup_track'
32
32
 
33
33
  require 'sqreen/rules_callbacks/custom_error'
34
+ require 'sqreen/rules_callbacks/waf'
@@ -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
@@ -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
@@ -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(format('Cannot %s %s. Parsed response body was: %s', method, path, payload.inspect))
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
@@ -1,5 +1,5 @@
1
1
  # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
2
  # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
3
  module Sqreen
4
- VERSION = '1.17.2'.freeze
4
+ VERSION = '1.18.0'.freeze
5
5
  end
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.17.2
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-08-30 00:00:00.000000000 Z
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