sqreen 1.15.8 → 1.16.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c3b1883dab046bead45a9f4918b23fd8c645c63d9e8526f2659a4864148f6f6
4
- data.tar.gz: be1317fecaa1476cabb5994ed25d8ba6852de00251004d43ca856aaed25dd225
3
+ metadata.gz: 88700ab65a0ac3080eb109fff2ab757f69da29f2a875bfa054a6ac89f6068ad4
4
+ data.tar.gz: 01f4d1e68fce364d10c4e54f37ef32e1b8490d36118d9cbe74488743ec3f6a31
5
5
  SHA512:
6
- metadata.gz: 40e08b9bdfe5fb86820ff3dae712c180637fdfe488233505dcfcd4b45b2ea465ba3658d5c9b63a2214c45309b8a975a9a4f66dc47e6a560f01d96d8b855693f8
7
- data.tar.gz: 04d801be1033efcd75e3b353ff7a9ffa44210fe0dd38fe04870bc7b7aa58509a6f09fc6ef23e2edb2967ed3f7e933bc8cd5948c5dc83259ffdef79ac55522759
6
+ metadata.gz: dcba32ae629498dce1051f3d9878ce974244622f967a7dd51fe9a5208c770fb4aef27f3aa502fe2033cc0ba1e8e94a2d28550777a1293184f7c86bc1bb0e266e
7
+ data.tar.gz: e8753a65ad853c63f6a0d18de2f453b7840db8a2b2fb24add9616cadf4e9e495d3d97dc33bc6c7b8d0d2d0460e2f48a98ef4216e984fd91c4c0b8d5db5fa7e63
data/Rakefile CHANGED
@@ -13,6 +13,7 @@ end
13
13
 
14
14
  Rake::TestTask.new do |t|
15
15
  t.pattern = 'test/**/*.rb'
16
+ t.warning = false
16
17
  t.libs << 'test'
17
18
  end
18
19
 
data/lib/sqreen.rb CHANGED
@@ -17,7 +17,7 @@ require 'thread'
17
17
  # Auto start the instrumentation.
18
18
 
19
19
  Sqreen.framework.on_start do |framework|
20
- if RUBY_VERSION =~ /\A2\.5\.[01]\z/ && Sqreen.framework.on_pre_fork_preload?
20
+ if Sqreen.framework.on_pre_fork_preload?
21
21
  STDERR.puts("Avoiding launching sqreen thread pre-fork") # sqreen log unavailable
22
22
  next
23
23
  end
@@ -265,45 +265,45 @@ module Sqreen
265
265
  end
266
266
  end
267
267
 
268
- # Blocks a user at the point Sqreen::identify()
269
- # or Sqreen::auth_track() are called
270
- class BlockUser < Base
271
- self.type_name = 'block_user'
272
-
273
- class << self
274
- def actions_matching(identity_params)
275
- return [] unless @idx
276
- key = stringify_keys(identity_params)
277
- actions = @idx[key]
278
- actions || []
279
- end
280
-
281
- def index(params, action)
282
- @idx ||= {}
283
- users = params['users']
284
- raise ::Sqreen::Exception, 'nil "users" param for block_user action' if users.nil?
285
- raise ::Sqreen::Exception, '"users" param must be an array' unless users.is_a? Array
268
+ module UserActionClass
269
+ def actions_matching(identity_params)
270
+ return [] unless @idx
271
+ key = stringify_keys(identity_params)
272
+ actions = @idx[key]
273
+ actions || []
274
+ end
286
275
 
287
- users.each do |u|
288
- @idx[u] ||= []
289
- @idx[u] << action
290
- end
276
+ def index(params, action)
277
+ @idx ||= {}
278
+ users = params['users']
279
+ raise ::Sqreen::Exception, 'nil "users" param for block_user action' if users.nil?
280
+ raise ::Sqreen::Exception, '"users" param must be an array' unless users.is_a? Array
281
+
282
+ users.each do |u|
283
+ @idx[u] ||= []
284
+ @idx[u] << action
291
285
  end
286
+ end
292
287
 
293
- def clear
294
- @idx = {}
295
- end
288
+ def clear
289
+ @idx = {}
290
+ end
296
291
 
297
- private
292
+ private
298
293
 
299
- def stringify_keys(hash)
300
- Hash[
301
- hash.map { |k, v| [k.to_s, v] }
302
- ]
303
- end
294
+ def stringify_keys(hash)
295
+ Hash[
296
+ hash.map { |k, v| [k.to_s, v] }
297
+ ]
304
298
  end
299
+ end
305
300
 
306
- # BlockUser proper definition continues
301
+ # Blocks a user at the point Sqreen::identify()
302
+ # or Sqreen::auth_track() are called
303
+ class BlockUser < Base
304
+ extend UserActionClass
305
+
306
+ self.type_name = 'block_user'
307
307
 
308
308
  def initialize(id, opts, params = {})
309
309
  super(id, opts)
@@ -330,5 +330,39 @@ module Sqreen
330
330
  { 'user' => identity_params }
331
331
  end
332
332
  end
333
+
334
+ # Redirects a user at the point Sqreen::identify()
335
+ # or Sqreen::auth_track() are called
336
+ class RedirectUser < Base
337
+ extend UserActionClass
338
+
339
+ self.type_name = 'redirect_user'
340
+
341
+ def initialize(id, opts, params = {})
342
+ super(id, opts)
343
+ @redirect_url = params['url']
344
+ raise "no url provided for action #{id}" unless @redirect_url
345
+ end
346
+
347
+ def do_run(identity_params)
348
+ Sqreen.log.info 'Will request redirect for user with identity ' \
349
+ "#{identity_params} (action: #{id})."
350
+
351
+ e = Sqreen::AttackBlocked.new(
352
+ "Redirected user with identity #{identity_params} " \
353
+ 'due to automatic security response. No action is required'
354
+ )
355
+ e.redirect_url = @redirect_url
356
+
357
+ {
358
+ :status => :raise,
359
+ :exception => e,
360
+ }
361
+ end
362
+
363
+ def event_properties(identity_params)
364
+ { 'user' => identity_params }
365
+ end
366
+ end
333
367
  end
334
368
  end
@@ -35,6 +35,8 @@ module Sqreen
35
35
  :default => 'https://back.sqreen.io' },
36
36
  { :env => :SQREEN_TOKEN, :name => :token,
37
37
  :default => nil },
38
+ { :env => :SQREEN_APP_NAME, :name => :app_name,
39
+ :default => nil },
38
40
  { :env => :SQREEN_RULES, :name => :local_rules,
39
41
  :default => nil },
40
42
  { :env => :SQREEN_RULES_SIGNATURE, :name => :rules_verify_signature,
@@ -28,6 +28,8 @@ module Sqreen
28
28
  # Sqreen users when watching their logs. It should not raise any concern to
29
29
  # them.
30
30
  class AttackBlocked < Exception
31
+ attr_accessor :redirect_url
32
+
31
33
  def log_message(msg)
32
34
  Sqreen.log.warn(msg)
33
35
  end
@@ -178,6 +178,10 @@ module Sqreen
178
178
  nil
179
179
  end
180
180
 
181
+ def application_name
182
+ nil
183
+ end
184
+
181
185
  # Main entry point for sqreen.
182
186
  # launch whenever we are ready
183
187
  def on_start
@@ -70,8 +70,11 @@ module Sqreen
70
70
  end
71
71
 
72
72
  def root
73
- return nil unless @application
74
- @application.root
73
+ Rails.application.root
74
+ end
75
+
76
+ def application_name
77
+ Rails.application.class.name.deconstantize.underscore
75
78
  end
76
79
 
77
80
  # Register a new initializer in rails to ba called when we are starting up
@@ -11,7 +11,7 @@ require 'sqreen/rules_signature'
11
11
  require 'sqreen/shared_storage'
12
12
  require 'sqreen/rules_callbacks/record_request_context'
13
13
  require 'sqreen/rules_callbacks/run_req_start_actions'
14
- require 'sqreen/rules_callbacks/run_block_user_actions'
14
+ require 'sqreen/rules_callbacks/run_user_actions'
15
15
  require 'sqreen/mono_time'
16
16
  require 'set'
17
17
 
@@ -706,8 +706,8 @@ module Sqreen
706
706
  def hardcoded_callbacks(framework)
707
707
  [
708
708
  Sqreen::Rules::RunReqStartActions.new(framework),
709
- Sqreen::Rules::RunBlockUserActions.new(Sqreen, :identify, 0),
710
- Sqreen::Rules::RunBlockUserActions.new(Sqreen, :auth_track, 1),
709
+ Sqreen::Rules::RunUserActions.new(Sqreen, :identify, 0),
710
+ Sqreen::Rules::RunUserActions.new(Sqreen, :auth_track, 1),
711
711
  ]
712
712
  end
713
713
 
@@ -81,89 +81,6 @@ module Sqreen
81
81
  end
82
82
  end
83
83
 
84
- class JsonConversion
85
- class << self
86
- if defined?(::JSON::Ext::Generator::State)
87
- def convert_arguments(_args)
88
- args = purge_dangerous_objs(_args)
89
- new_state.generate(args)
90
- rescue ::JSON::GeneratorError, ::Encoding::UndefinedConversionError
91
- fixed_args = fixup_bad_encoding(args)
92
- new_state.generate(fixed_args)
93
- end
94
- else
95
- def convert_arguments(_args)
96
- args = purge_dangerous_objs(_args)
97
- args.to_json(new_state)
98
- rescue ::EncodingError
99
- fixed_args = fixup_bad_encoding(args)
100
- fixed_args.to_json(new_state)
101
- end
102
- end
103
-
104
- private
105
-
106
- def new_state
107
- ::JSON::SAFE_STATE_PROTOTYPE.dup
108
- end
109
-
110
- def fixup_bad_encoding(arg, max_depth = 100)
111
- return nil if max_depth <= 0
112
-
113
- if arg.is_a?(::Array)
114
- return arg.map { |it| fixup_bad_encoding(it, max_depth - 1) }
115
- end
116
-
117
- if arg.is_a?(::Hash)
118
- return ::Hash[
119
- arg.map do |k, v|
120
- [fixup_bad_encoding(k, max_depth - 1),
121
- fixup_bad_encoding(v, max_depth - 1)]
122
- end
123
- ]
124
- end
125
-
126
- return arg unless arg.is_a?(::String)
127
-
128
- unless arg.valid_encoding?
129
- return arg.dup.force_encoding(::Encoding::ISO_8859_1)
130
- end
131
-
132
- # encoding is valid if it reaches this point
133
- return arg if arg.encoding == ::Encoding::UTF_8
134
-
135
- begin
136
- arg.encode(::Encoding::UTF_8)
137
- rescue ::Encoding::UndefinedConversionError
138
- arg.dup.force_encoding(::Encoding::ISO_8859_1)
139
- end
140
- end
141
-
142
- def purge_dangerous_objs(arg, max_depth = 100)
143
- return nil if max_depth <= 0
144
-
145
- if arg.is_a?(::Array)
146
- arg.map { |it| purge_dangerous_objs(it, max_depth - 1) }
147
- elsif arg.is_a?(::Hash)
148
- Hash[
149
- arg.map do |k, v|
150
- [purge_dangerous_objs(k, max_depth - 1),
151
- purge_dangerous_objs(v, max_depth - 1)]
152
- end
153
- ]
154
- elsif arg.is_a?(::String) || arg.nil? || arg == false ||
155
- arg == true || arg.is_a?(::Fixnum) || arg.is_a?(::Bignum) ||
156
- arg.is_a?(::Float)
157
- arg
158
- elsif arg.respond_to?(:to_json)
159
- arg.to_s
160
- else
161
- arg
162
- end
163
- end
164
- end
165
- end
166
-
167
84
  class MiniRacerExecutableJs < ExecutableJs
168
85
  @@ctx_defined = false
169
86
 
@@ -194,9 +111,7 @@ module Sqreen
194
111
  ctx.add_code(@code_id, @code) unless ctx.has_code?(@code_id)
195
112
 
196
113
  begin
197
- json_args = JsonConversion.convert_arguments(arguments)
198
- ctx.eval_unsafe(
199
- "sqreen_data['#{@code_id}']['#{cb_name}'].apply(this, #{json_args})", nil, budget)
114
+ ctx.call("sqreen_#{@code_id}_#{cb_name}", *arguments)
200
115
  rescue @module::ScriptTerminatedError
201
116
  Sqreen.log.debug "ScriptTerminatedError/#{cb_name}"
202
117
  nil
@@ -205,7 +120,7 @@ module Sqreen
205
120
  end
206
121
 
207
122
  def self.code_id(code)
208
- Digest::MD5.base64digest(code)
123
+ Digest::MD5.hexdigest(code)
209
124
  end
210
125
 
211
126
  private
@@ -231,6 +146,9 @@ module Sqreen
231
146
  end
232
147
 
233
148
  def add_code(code_id, code)
149
+ # It's important that the definition is run in its own scope (by executing it inside an anonymous function)
150
+ # Otherwise some auxiliary functions that the backend server sends will collide the name
151
+ # Because they're defined with `var`, running the definitions inside a function is enough
234
152
  eval_unsafe "(function() { #{code} })()"
235
153
  transf_global_funcs code_id
236
154
  @code_ids ||= Set.new
@@ -271,20 +189,14 @@ module Sqreen
271
189
  private
272
190
 
273
191
  def transf_global_funcs(code_id)
192
+ # Multiple callbacks may share the same name. In order to avoid collisions, we rename them here.
274
193
  eval_unsafe <<EOD
275
- if (typeof sqreen_data === 'undefined') {
276
- sqreen_data = {};
277
- }
278
-
279
- group = {};
280
194
  Object.keys(this).forEach(name => {
281
- if (typeof this[name] === "function") {
282
- group[name] = this[name];
195
+ if (typeof this[name] === "function" && !name.startsWith("sqreen_")) {
196
+ this['sqreen_#{code_id}_' + name] = this[name];
283
197
  this[name] = undefined;
284
198
  }
285
199
  });
286
- sqreen_data['#{code_id}'] = group
287
- delete group;
288
200
  EOD
289
201
 
290
202
  end
@@ -19,6 +19,20 @@ module Sqreen
19
19
 
20
20
  def call(env)
21
21
  @app.call(env)
22
+ rescue => e
23
+ sqreen_attack = nil
24
+ if e.is_a?(Sqreen::AttackBlocked)
25
+ sqreen_attack = e
26
+ elsif e.respond_to?(:original_exception) &&
27
+ e.original_exception.is_a?(Sqreen::AttackBlocked)
28
+ sqreen_attack = e.original_exception
29
+ end
30
+
31
+ if sqreen_attack && sqreen_attack.redirect_url
32
+ return [303, { 'Location' => sqreen_attack.redirect_url }, ['']]
33
+ else
34
+ raise
35
+ end
22
36
  end
23
37
  end
24
38
 
@@ -6,6 +6,19 @@ module Sqreen
6
6
  false
7
7
  end
8
8
 
9
+ @has_thread_cpu_time = begin
10
+ Process.clock_gettime Process::CLOCK_THREAD_CPUTIME_ID
11
+ true
12
+ rescue StandardError
13
+ false
14
+ end
15
+
16
+ class << self
17
+ def thread_cpu_time?
18
+ @has_thread_cpu_time
19
+ end
20
+ end
21
+
9
22
  if has_mono_time
10
23
  def self.time
11
24
  Process.clock_gettime Process::CLOCK_MONOTONIC
@@ -15,4 +28,14 @@ module Sqreen
15
28
  Time.now.to_f
16
29
  end
17
30
  end
31
+
32
+ if @has_thread_cpu_time
33
+ def self.thread_cpu_time
34
+ Process.clock_gettime Process::CLOCK_THREAD_CPUTIME_ID
35
+ end
36
+ else
37
+ def self.thread_cpu_time
38
+ 0
39
+ end
40
+ end
18
41
  end
@@ -15,6 +15,7 @@ module Sqreen
15
15
  EVENT_REQ = 'req'.freeze # request total time
16
16
  EVENT_TOTAL_TIME = 'sq'.freeze # sqreen total overhead callback time
17
17
  EVENT_PERCENT = 'pct'.freeze # sqreen total overhead percent of time
18
+ EVENT_SQ_THREAD_CPU_PCT = 'sq_thread_cpu_pct'.freeze # sqreen thread cpu time
18
19
 
19
20
  # @param metrics_store [Sqreen::MetricsStore]
20
21
  def initialize(metrics_store, period, perf_metric_opts, perf_metric_percent_opts)
@@ -23,6 +24,8 @@ module Sqreen
23
24
  @subid = nil
24
25
  @perf_metric_opts = perf_metric_opts
25
26
  @perf_metric_percent_opts = perf_metric_percent_opts
27
+ @clock_time = Sqreen.time
28
+ @watcher_cpu_time = 0
26
29
  end
27
30
 
28
31
  def enable
@@ -38,6 +41,12 @@ module Sqreen
38
41
  'name' => EVENT_PERCENT, 'period' => period, 'kind' => 'Binning', 'options' => @perf_metric_percent_opts
39
42
  )
40
43
 
44
+ if Sqreen.thread_cpu_time?
45
+ metrics_store.create_metric('name' => EVENT_SQ_THREAD_CPU_PCT,
46
+ 'period' => period, 'kind' => 'Binning',
47
+ 'options' => @perf_metric_percent_opts)
48
+ end
49
+
41
50
  @subid = Sqreen::PerformanceNotifications.subscribe(&method(:log))
42
51
  end
43
52
 
@@ -85,6 +94,26 @@ module Sqreen
85
94
  )
86
95
  end
87
96
 
97
+ def finish_watcher_run
98
+ return unless Sqreen.thread_cpu_time?
99
+ new_clock_time = Sqreen.time
100
+ # collect observation at min 30 second intervals so it's nicely averaged
101
+ return if new_clock_time - @clock_time < 30.0
102
+
103
+ clock_time_before = @clock_time
104
+ watcher_cpu_time_before = @watcher_cpu_time
105
+ @clock_time = new_clock_time
106
+ @watcher_cpu_time = Sqreen.thread_cpu_time
107
+
108
+ clock_time_diff = @clock_time - clock_time_before
109
+ watcher_cpu_diff = @watcher_cpu_time - watcher_cpu_time_before
110
+
111
+ Sqreen.observations_queue.push(
112
+ [EVENT_SQ_THREAD_CPU_PCT, nil,
113
+ (watcher_cpu_diff * 100.0) / clock_time_diff, Time.now.utc]
114
+ )
115
+ end
116
+
88
117
  private
89
118
 
90
119
  # @return [Sqreen::MetricsStore]
@@ -111,7 +140,7 @@ module Sqreen
111
140
  @instance = new(metrics_store, period,
112
141
  {'base'=> base, 'factor' => factor},
113
142
  {'base' => base_pct, 'factor' => factor_pct}
114
- ).tap(&:enable)
143
+ ).tap(&:enable)
115
144
  end
116
145
 
117
146
  def disable
@@ -129,6 +158,11 @@ module Sqreen
129
158
  return unless instance
130
159
  instance.finish_request
131
160
  end
161
+
162
+ def finish_watcher_run
163
+ return unless instance
164
+ instance.finish_watcher_run
165
+ end
132
166
  end
133
167
  end
134
168
  end
@@ -7,14 +7,15 @@ require 'sqreen/actions'
7
7
  module Sqreen
8
8
  module Rules
9
9
  # Runs the block_user actions (for hooking Sqreen.{identify,auth_user})
10
- class RunBlockUserActions < CB
10
+ class RunUserActions < CB
11
11
  def initialize(klass, method, auth_keys_idx)
12
12
  super(klass, method)
13
13
  @auth_keys_idx = auth_keys_idx
14
14
  end
15
15
 
16
16
  def post(_retval, _inst, args, _budget = nil)
17
- actions = actions_repo.get('block_user', args[@auth_keys_idx])
17
+ actions = actions_repo.get('block_user', args[@auth_keys_idx]) +
18
+ actions_repo.get('redirect_user', args[@auth_keys_idx])
18
19
 
19
20
  actions.each do |action|
20
21
  res = action.run args[@auth_keys_idx]
data/lib/sqreen/runner.rb CHANGED
@@ -109,6 +109,7 @@ module Sqreen
109
109
  @running = true
110
110
 
111
111
  @token = @configuration.get(:token)
112
+ @app_name = @configuration.get(:app_name)
112
113
  @url = @configuration.get(:url)
113
114
  Sqreen.update_whitelisted_paths([])
114
115
  Sqreen.update_whitelisted_ips({})
@@ -121,7 +122,7 @@ module Sqreen
121
122
  self.metrics_engine = MetricsStore.new
122
123
  @instrumenter = Instrumentation.new(metrics_engine)
123
124
 
124
- Sqreen.log.warn "using token #{@token}"
125
+ Sqreen.log.warn "Using token #{@token}"
125
126
  response = create_session(session_class)
126
127
  wanted_features = response.fetch('features', {})
127
128
  conf_initial_features = configuration.get(:initial_features)
@@ -151,7 +152,7 @@ module Sqreen
151
152
  end
152
153
 
153
154
  def create_session(session_class)
154
- @session = session_class.new(@url, @token)
155
+ @session = session_class.new(@url, @token, @app_name)
155
156
  session.login(@framework)
156
157
  end
157
158
 
@@ -378,6 +379,8 @@ module Sqreen
378
379
  Sqreen.log.debug 'Forced an heartbeat'
379
380
  periodic_cleanup # will trigger do_heartbeat since it's time
380
381
  end
382
+ ensure
383
+ PerformanceNotifications::BinnedMetrics.finish_watcher_run
381
384
  end
382
385
 
383
386
  def periodic_cleanup
@@ -44,8 +44,9 @@ module Sqreen
44
44
 
45
45
  attr_accessor :request_compression
46
46
 
47
- def initialize(server_url, token)
47
+ def initialize(server_url, token, app_name = nil)
48
48
  @token = token
49
+ @app_name = app_name
49
50
  @session_id = nil
50
51
  @server_url = server_url
51
52
  @request_compression = false
@@ -221,7 +222,12 @@ module Sqreen
221
222
  end
222
223
 
223
224
  def login(framework)
224
- headers = { 'x-api-key' => @token }
225
+ headers = {
226
+ 'x-api-key' => @token,
227
+ 'x-app-name' => @app_name || framework.application_name,
228
+ }.reject { |k, v| v == nil }
229
+
230
+ Sqreen.log.warn "Using app name: #{headers['x-app-name']}"
225
231
 
226
232
  res = resilient_post('app-login', RuntimeInfos.all(framework), headers)
227
233
 
@@ -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.15.8'.freeze
4
+ VERSION = '1.16.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.15.8
4
+ version: 1.16.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-01-07 00:00:00.000000000 Z
11
+ date: 2019-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sq_mini_racer
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.2.4.sqreen1
19
+ version: 0.2.4.sqreen2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.2.4.sqreen1
26
+ version: 0.2.4.sqreen2
27
27
  description: Sqreen is a SaaS based Application protection and monitoring platform
28
28
  that integrates directly into your Ruby applications. Learn more at https://sqreen.io.
29
29
  email: contact@sqreen.io
@@ -104,8 +104,8 @@ files:
104
104
  - lib/sqreen/rules_callbacks/record_request_context.rb
105
105
  - lib/sqreen/rules_callbacks/reflected_xss.rb
106
106
  - lib/sqreen/rules_callbacks/regexp_rule.rb
107
- - lib/sqreen/rules_callbacks/run_block_user_actions.rb
108
107
  - lib/sqreen/rules_callbacks/run_req_start_actions.rb
108
+ - lib/sqreen/rules_callbacks/run_user_actions.rb
109
109
  - lib/sqreen/rules_callbacks/shell_env.rb
110
110
  - lib/sqreen/rules_callbacks/url_matches.rb
111
111
  - lib/sqreen/rules_callbacks/user_agent_matches.rb