sqreen 0.8.11465220943 → 1.0.0.pre1480953244

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.
@@ -159,15 +159,13 @@ module Sqreen
159
159
  end
160
160
 
161
161
  def self.guard_call(method, retval)
162
- if @sqreen_in_instr && @sqreen_in_instr.member?(method)
163
- return retval
164
- else
165
- @sqreen_in_instr ||= Set.new
166
- @sqreen_in_instr.add(method)
167
- r = yield
168
- @sqreen_in_instr.delete(method)
169
- return r
170
- end
162
+ @sqreen_in_instr ||= nil
163
+ return retval if @sqreen_in_instr && @sqreen_in_instr.member?(method)
164
+ @sqreen_in_instr ||= Set.new
165
+ @sqreen_in_instr.add(method)
166
+ r = yield
167
+ @sqreen_in_instr.delete(method)
168
+ return r
171
169
  rescue Exception => e
172
170
  @sqreen_in_instr.delete(method)
173
171
  raise e
@@ -338,7 +336,6 @@ module Sqreen
338
336
  saved_meth_name = get_saved_method_name(meth)
339
337
 
340
338
  eval "method_kind = nil; class << #{klass}
341
- new_method = '#{meth}_modified'.to_sym
342
339
  case
343
340
  when public_method_defined?(#{meth.to_sym.inspect})
344
341
  method_kind = :public
@@ -8,6 +8,7 @@ require 'sqreen/configuration'
8
8
 
9
9
  module Sqreen
10
10
  def self::log
11
+ @logger ||= nil
11
12
  return @logger unless @logger.nil?
12
13
  @logger = Logger.new(
13
14
  Sqreen.config_get(:log_level).to_s.upcase,
@@ -10,6 +10,10 @@ module Sqreen
10
10
  FINISH_KEY = 'finish'.freeze
11
11
  # Base interface for a metric
12
12
  class Base
13
+ def initialize
14
+ @sample = nil
15
+ end
16
+
13
17
  # Update the current metric with a new observation
14
18
  # @param _at [Time] when was the observation made
15
19
  # @param _key [String] which aggregation key was it made for
@@ -49,15 +49,15 @@ module Sqreen
49
49
  at = at.utc
50
50
  metric, period, start = @metrics[name]
51
51
  raise UnregisteredMetric, "Unknown metric #{name}" unless metric
52
- next_sample(name, at) if start.nil? || start + period < at
52
+ next_sample(name, at) if start.nil? || (start + period) < at
53
53
  metric.update(at, key, value)
54
54
  end
55
55
 
56
56
  # Drains every metrics and returns the store content
57
57
  # @params at [Time] when is the store emptied
58
- def publish(at = Time.now.utc)
59
- @metrics.each_key do |name|
60
- next_sample(name, at)
58
+ def publish(flush = true, at = Time.now.utc)
59
+ @metrics.each do |name, (_, period, start)|
60
+ next_sample(name, at) if flush || !start.nil? && (start + period) < at
61
61
  end
62
62
  out = @store
63
63
  @store = []
@@ -1,5 +1,6 @@
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
+ require "sqreen/log"
3
4
 
4
5
  module Sqreen
5
6
  # Execute and sanitize remote commands
@@ -20,15 +21,15 @@ module Sqreen
20
21
  @uuid = json_desc['uuid']
21
22
  end
22
23
 
23
- def process(runner)
24
+ def process(runner, context_infos = {})
24
25
  failing = validate_command(runner)
25
26
  return failing if failing
26
27
  Sqreen.log.debug format('processing command %s', @name)
27
- output = runner.send(KNOWN_COMMANDS[@name], *@params)
28
+ output = runner.send(KNOWN_COMMANDS[@name], *@params, context_infos)
28
29
  format_output(output)
29
30
  end
30
31
 
31
- def self.process_list(runner, commands)
32
+ def self.process_list(runner, commands, context_infos = {})
32
33
  res_list = {}
33
34
 
34
35
  return res_list unless commands
@@ -43,7 +44,7 @@ module Sqreen
43
44
  cmd = RemoteCommand.new(cmd_json)
44
45
  Sqreen.log.debug cmd.inspect
45
46
  uuid = cmd.uuid
46
- res_list[uuid] = cmd.process(runner)
47
+ res_list[uuid] = cmd.process(runner, context_infos)
47
48
  end
48
49
  res_list
49
50
  end
@@ -11,12 +11,8 @@ require 'sqreen/rules_callbacks/headers_insert'
11
11
 
12
12
  require 'sqreen/rules_callbacks/inspect_rule'
13
13
 
14
- require 'sqreen/rules_callbacks/shell'
15
- require 'sqreen/rules_callbacks/system_shell'
16
14
  require 'sqreen/rules_callbacks/shell_env'
17
15
 
18
- require 'sqreen/rules_callbacks/sql'
19
-
20
16
  require 'sqreen/rules_callbacks/url_matches'
21
17
  require 'sqreen/rules_callbacks/user_agent_matches'
22
18
  require 'sqreen/rules_callbacks/crawler_user_agent_matches'
@@ -19,6 +19,7 @@ module Sqreen
19
19
  Regexp.compile(value, res)
20
20
  end
21
21
 
22
+ ANYWHERE_OPT = 'anywhere'.freeze
22
23
  def prepare
23
24
  @string = {}
24
25
  @regex_patterns = []
@@ -30,10 +31,10 @@ module Sqreen
30
31
  end
31
32
 
32
33
  @funs = {
33
- 'anywhere' => lambda { |value, str| str.include?(value) },
34
- 'starts_with' => lambda { |value, str| str.start_with?(value) },
35
- 'ends_with' => lambda { |value, str| str.end_with?(value) },
36
- 'equals' => lambda { |value, str| str == value },
34
+ ANYWHERE_OPT => lambda { |value, str| str.include?(value) },
35
+ 'starts_with'.freeze => lambda { |value, str| str.start_with?(value) },
36
+ 'ends_with'.freeze => lambda { |value, str| str.end_with?(value) },
37
+ 'equals'.freeze => lambda { |value, str| str == value },
37
38
  }
38
39
 
39
40
  patterns.each do |entry|
@@ -41,7 +42,8 @@ module Sqreen
41
42
  type = entry['type']
42
43
  val = entry['value']
43
44
  opts = entry['options']
44
- opt = (opts && opts.first && opts.first != '') ? opts.first : 'anywhere'
45
+ opt = ANYWHERE_OPT
46
+ opt = opts.first.freeze if opts && opts.first && opts.first != ''
45
47
  case_sensitive = entry['case_sensitive'] || false
46
48
  case type
47
49
  when 'string'
@@ -49,7 +51,7 @@ module Sqreen
49
51
  case_type = :cs
50
52
  else
51
53
  case_type = :ci
52
- val = val.downcase
54
+ val.downcase!
53
55
  end
54
56
 
55
57
  unless @funs.keys.include?(opt)
@@ -7,35 +7,79 @@ require 'sqreen/rules_callbacks/regexp_rule'
7
7
 
8
8
  module Sqreen
9
9
  module Rules
10
- # look for reflected XSS
10
+ # look for reflected XSS with erb template engine
11
11
  class ReflectedXSSCB < RegexpRuleCB
12
12
  def pre(_inst, *args, &_block)
13
13
  value = args[0]
14
- return if value.nil?
14
+
15
+ return unless value.is_a?(String)
16
+
15
17
  # If the value is not marked as html_safe, it will be escaped later
16
18
  return unless value.html_safe?
17
19
 
18
20
  # Sqreen::log.debug value
19
- # Sqreen::log.debug params
20
21
 
21
22
  return unless framework.params_include?(value)
22
23
 
23
24
  Sqreen.log.debug { format('Found unescaped user param: %s', value) }
24
25
 
25
- return unless value.is_a?(String)
26
-
27
26
  saved_value = value.dup
28
27
  # potential XSS! let's escape
29
28
  args[0].replace(CGI.escape_html(value)) if block
30
- # The remaining code is only to find out if user entry was an attack,
31
- # and record it. Since we don't rely on it to respond to user, it would
32
- # be better to do it in background.
33
- found = match_regexp(saved_value)
29
+
30
+ report_dangerous_xss(saved_value)
31
+
32
+ nil
33
+ end
34
+
35
+ # The remaining code is only to find out if user entry was an attack,
36
+ # and record it. Since we don't rely on it to respond to user, it would
37
+ # be better to do it in background.
38
+ def report_dangerous_xss(value)
39
+ found = match_regexp(value)
34
40
 
35
41
  return unless found
36
- infos = { :found => found }
42
+ infos = {
43
+ :found => found,
44
+ :payload => value
45
+ }
37
46
  record_event(infos)
38
- nil
47
+ end
48
+ end
49
+ # look for reflected XSS with haml template engine
50
+ # hook function arguments of
51
+ # Haml::Buffer.format_script(result, preserve_script, in_tag, preserve_tag,
52
+ # escape_html, nuke_inner_whitespace,
53
+ # interpolated, ugly)
54
+ class ReflectedXSSHamlCB < ReflectedXSSCB
55
+ def pre(inst, *args, &_block)
56
+ value = args[0]
57
+
58
+ return unless value.is_a?(String)
59
+
60
+ # if escape_html is already true, it will be escaped later
61
+ return if args[4]
62
+
63
+ return unless framework.params_include?(value)
64
+
65
+ Sqreen.log.debug { format('Found unescaped user param: %s', value) }
66
+
67
+ # potential XSS ! Call the same method with the argument
68
+ # escape_html true
69
+ if block
70
+ nargs = args.dup
71
+ nargs[4] = true
72
+ return_value = {
73
+ :status => :skip,
74
+ # 'method' is an attribut reader in class CB, it's the name of the
75
+ # hooked method
76
+ :new_return_value => inst.send(method, *nargs),
77
+ }
78
+ end
79
+
80
+ report_dangerous_xss(value)
81
+
82
+ return_value
39
83
  end
40
84
  end
41
85
  end
@@ -2,18 +2,21 @@
2
2
  # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
3
 
4
4
  require 'timeout'
5
+ require 'json'
5
6
 
6
7
  require 'sqreen/events/attack'
7
8
 
8
9
  require 'sqreen/log'
9
10
 
10
11
  require 'sqreen/rules'
12
+ require 'sqreen/session'
11
13
  require 'sqreen/remote_command'
12
14
  require 'sqreen/capped_queue'
13
15
  require 'sqreen/metrics_store'
14
16
  require 'sqreen/deliveries/simple'
15
17
  require 'sqreen/deliveries/batch'
16
18
  require 'sqreen/performance_notifications/metrics'
19
+ require 'sqreen/instrumentation'
17
20
 
18
21
  module Sqreen
19
22
  @features = {}
@@ -48,19 +51,18 @@ module Sqreen
48
51
 
49
52
  # Main running job class for the agent
50
53
  class Runner
51
- # At start, heartbeat is every 15 seconds
52
- HEARTBEAT_INITIAL_DELAY = 15
53
54
  # During one hour
54
55
  HEARTBEAT_WARMUP = 60 * 60
55
- # Then delay raises to 5 minutes
56
+ # Initail delay is 5 minutes
56
57
  HEARTBEAT_MAX_DELAY = 5 * 60
57
58
 
58
- attr_reader :heartbeat_delay
59
+ attr_accessor :heartbeat_delay
59
60
  attr_accessor :metrics_engine
60
- attr_reader :publish_metrics_delay
61
61
  attr_reader :deliverer
62
62
  attr_reader :session
63
63
  attr_reader :instrumenter
64
+ attr_accessor :next_command_results
65
+ attr_accessor :next_metrics
64
66
 
65
67
  # we may want to do that in a thread in order to prevent delaying app
66
68
  # startup
@@ -69,12 +71,10 @@ module Sqreen
69
71
  @logged_out_tried = false
70
72
  @configuration = configuration
71
73
  @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
74
+ @heartbeat_delay = HEARTBEAT_MAX_DELAY
75
+ @last_heartbeat_request = Time.now
76
+ @next_command_results = {}
77
+ @next_metrics = []
78
78
 
79
79
  @token = @configuration.get(:token)
80
80
  @url = @configuration.get(:url)
@@ -83,13 +83,37 @@ module Sqreen
83
83
 
84
84
  register_exit_cb if set_at_exit
85
85
 
86
+ self.metrics_engine = MetricsStore.new
87
+ @instrumenter = Instrumentation.new(metrics_engine)
88
+
86
89
  Sqreen.log.warn "using token #{@token}"
87
- self.features = create_session(session_class)
90
+ response = create_session(session_class)
91
+ wanted_features = response.fetch('features', {})
92
+ conf_initial_features = configuration.get(:initial_features)
93
+ unless conf_initial_features.nil?
94
+ begin
95
+ conf_features = JSON.parse(conf_initial_features)
96
+ raise 'Invalid Type' unless conf_features.is_a?(Hash)
97
+ Sqreen.log.debug do
98
+ "Override initial features with #{conf_features.inspect}"
99
+ end
100
+ wanted_features = conf_features
101
+ rescue
102
+ Sqreen.log.error do
103
+ "NOT using Invalid inital features #{conf_initial_features}"
104
+ end
105
+ end
106
+ end
107
+ self.features = wanted_features
108
+
88
109
  # Ensure a deliverer is there unless features have set it first
89
110
  self.deliverer ||= Deliveries::Simple.new(session)
90
111
 
91
- self.metrics_engine = MetricsStore.new
92
- @instrumenter = Instrumentation.new(metrics_engine)
112
+ context_infos = {}
113
+ %w(rules pack_id).each do |p|
114
+ context_infos[p] = response[p] unless response[p].nil?
115
+ end
116
+ process_commands(response.fetch('commands', []), context_infos)
93
117
  end
94
118
 
95
119
  def create_session(session_class)
@@ -112,20 +136,27 @@ module Sqreen
112
136
  end
113
137
  end
114
138
 
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)
139
+ def load_rules(context_infos = {})
140
+ rules_pack = context_infos['rules']
141
+ rulespack_id = context_infos['pack_id']
142
+ if rules_pack.nil? || rulespack_id.nil?
143
+ session_rules = session.rules
144
+ rules_pack = session_rules['rules']
145
+ rulespack_id = session_rules['pack_id']
146
+ end
147
+ rules = rules_pack.each { |r| r['rulespack_id'] = rulespack_id }
148
+ Sqreen.log.info { format('retrieved rulespack id: %s', rulespack_id) }
149
+ Sqreen.log.debug { format('retrieved %d rules', rules.size) }
121
150
  local_rules = Sqreen::Rules.local(@configuration) || []
122
151
  rules += local_rules.
123
152
  select { |rule| rule['enabled'] }.
124
153
  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(', '))
154
+ Sqreen.log.debug do
155
+ format('rules: %s', rules.
156
+ sort_by { |r| r['name'] }.
157
+ map { |r| format('(%s, %s)', r['name'], r.to_json.size) }.
158
+ join(', '))
159
+ end
129
160
  [rulespack_id, rules]
130
161
  end
131
162
 
@@ -138,20 +169,20 @@ module Sqreen
138
169
  end
139
170
  end
140
171
 
141
- def setup_instrumentation
172
+ def setup_instrumentation(context_infos = {})
142
173
  Sqreen.log.info 'setup instrumentation'
143
- rulespack_id, rules = load_rules
174
+ rulespack_id, rules = load_rules(context_infos)
144
175
  @framework.instrument_when_ready!(instrumenter, rules)
145
176
  rulespack_id.to_s
146
177
  end
147
178
 
148
- def remove_instrumentation
179
+ def remove_instrumentation(_context_infos = {})
149
180
  Sqreen.log.debug 'removing instrumentation'
150
181
  instrumenter.remove_all_callbacks
151
182
  true
152
183
  end
153
184
 
154
- def reload_rules
185
+ def reload_rules(_context_infos = {})
155
186
  Sqreen.log.debug 'Reloading rules'
156
187
  rulespack_id, rules = load_rules
157
188
  instrumenter.remove_all_callbacks
@@ -161,22 +192,21 @@ module Sqreen
161
192
  rulespack_id.to_s
162
193
  end
163
194
 
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
195
+ def process_commands(commands, context_infos = {})
196
+ return if commands.nil? || commands.empty?
197
+ res = RemoteCommand.process_list(self, commands, context_infos)
198
+ @next_command_results = res
170
199
  end
171
200
 
172
201
  def do_heartbeat
173
202
  @last_heartbeat_request = Time.now
174
- res = session.heartbeat
175
- update_heartbeat_delay
203
+ res = session.heartbeat(next_command_results, next_metrics)
204
+ next_command_results.clear
205
+ next_metrics.clear
176
206
  process_commands(res['commands'])
177
207
  end
178
208
 
179
- def features
209
+ def features(_context_infos = {})
180
210
  Sqreen.features
181
211
  end
182
212
 
@@ -184,13 +214,13 @@ module Sqreen
184
214
  Sqreen.update_features(features)
185
215
  session.request_compression = features['request_compression'] if session
186
216
  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
217
+ hd = features['heartbeat_delay'].to_i
218
+ self.heartbeat_delay = hd if hd > 0
189
219
  return if features['batch_size'].nil?
190
220
  batch_events(features['batch_size'], features['max_staleness'])
191
221
  end
192
222
 
193
- def change_features(new_features)
223
+ def change_features(new_features, _context_infos = {})
194
224
  old = features
195
225
  self.features = new_features
196
226
  {
@@ -208,7 +238,7 @@ module Sqreen
208
238
  end
209
239
 
210
240
  def run_watcher_once
211
- event = Timeout.timeout(@sleep_delay) do
241
+ event = Timeout.timeout(heartbeat_delay) do
212
242
  Sqreen.queue.pop
213
243
  end
214
244
  rescue Timeout::Error
@@ -231,8 +261,10 @@ module Sqreen
231
261
  @deliverer.tick
232
262
  aggregate_observations
233
263
  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
264
+ if (@last_heartbeat_request + heartbeat_delay) < t
265
+ @next_metrics.concat(metrics_engine.publish(false)) if metrics_engine
266
+ do_heartbeat
267
+ end
236
268
  end
237
269
 
238
270
  def handle_event(event)
@@ -249,35 +281,6 @@ module Sqreen
249
281
  end
250
282
  end
251
283
 
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
284
  # Sinatra is using at_exit to run the application, see:
282
285
  # https://github.com/sinatra/sinatra/blob/cd503e6c590cd48c2c9bb7869522494bfc62cb14/lib/sinatra/main.rb#L25
283
286
  def exit_from_sinatra_startup?
@@ -295,7 +298,7 @@ module Sqreen
295
298
  @logged_out_tried = true
296
299
  @deliverer.drain if @deliverer
297
300
  aggregate_observations
298
- post_metrics
301
+ session.post_metrics(metrics_engine.publish) if metrics_engine
299
302
  session.logout(retrying)
300
303
  end
301
304