sqreen 0.8.11465220943 → 1.0.0.pre1480953244

Sign up to get free protection for your applications and to get access to all the features.
@@ -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