sqreen 1.11.3 → 1.12.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: 3e8eff78019936af7f34aba50cc99778250b703abf40045b13d3656dcb905804
4
- data.tar.gz: 39ad12dfbe8dc9429ed66b2015e2e8aaa6383ecd5eb90dbe9221a35c38fcf0b5
3
+ metadata.gz: 867b430d86c473c22fcad2e5531c5ee70078ffa45201e8e4e906127e854eaf84
4
+ data.tar.gz: e8ba868e3b21824963da46dde7abbf171f17cb83732af65599fb84eaa4181245
5
5
  SHA512:
6
- metadata.gz: 9737911ca8cfa2fb17bc88454c47d28134e7f923a5490abab57be7d63d852842f5ca457b83a90d28a38b34baac09a099d42dd66d8cbd116fe1127511d3b7b6d6
7
- data.tar.gz: 95e83d374c1e68d8085ae09538669b6a6a42ab84e461faf7620090154127bd1dd69bf80ed7a25a45d1b9aa338816b8e9e7425752346b7e25771c06de099c71e1
6
+ metadata.gz: 98d5d2879397613f82ead509e0dc352ab7c139c8e045f0e64d4f2f8938b3c992264e4053e6b1d001af7f2131593bf319d45882235388b417d48c9cf88a337093
7
+ data.tar.gz: c0ee0912af909dd7fa94db71a5480215dc36e4e2c3cd6471dae61b0ee888763c63e10fa385027957d0d7c17e34c47aa96968225be3a4ebe8f04fbe044b2ad0ab
@@ -0,0 +1,203 @@
1
+ # Copyright (c) 2018 Sqreen. All Rights Reserved.
2
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+
4
+ require 'ipaddr'
5
+ require 'sqreen/log'
6
+ require 'sqreen/exception'
7
+ require 'sqreen/sdk'
8
+ require 'sqreen/frameworks'
9
+
10
+ module Sqreen
11
+ # Implements actions (behavior taken in response to agent signals)
12
+ module Actions
13
+ # Exception for when an unknown action type is gotten from the server
14
+ class UnknownActionType < ::Sqreen::Exception
15
+ attr_reader :action_type
16
+ def initialize(action_type)
17
+ super("no such action type: #{action_type}. Must be one of #{Base.known_types}")
18
+ @action_type = action_type
19
+ end
20
+ end
21
+
22
+ # Where the currently loaded actions are stored. Singleton
23
+ class Repository
24
+ include Singleton
25
+
26
+ def initialize
27
+ @actions = {} # indexed by subclass
28
+ @actions.default_proc = proc { |h, k| h[k] = [] }
29
+ end
30
+
31
+ def <<(action)
32
+ @actions[action.class] << action
33
+ end
34
+
35
+ def [](action_class)
36
+ @actions[action_class]
37
+ end
38
+
39
+ def clear
40
+ @actions.clear
41
+ end
42
+ end
43
+
44
+ # @return [Sqreen::Actions::Base]
45
+ def self.deserialize_action(hash)
46
+ action_type = hash['action']
47
+ raise 'no action type available' unless action_type
48
+
49
+ subclass = Base.get_type_class(action_type)
50
+ raise UnknownActionType, action_type unless subclass
51
+
52
+ id = hash['action_id']
53
+ raise 'no action id available' unless id
54
+
55
+ duration = hash['duration']
56
+ if !duration.nil? && duration <= 0
57
+ Sqreen.log.debug "Action #{id} is already expired"
58
+ return nil
59
+ end
60
+
61
+ subclass.new(id, duration, hash['parameters'] || {})
62
+ end
63
+
64
+ class Base
65
+ attr_reader :id, :expiry
66
+
67
+ def initialize(id, duration)
68
+ @id = id
69
+ @expiry = Time.new + duration unless duration.nil?
70
+ end
71
+
72
+ # See Sqreen::CB for return values
73
+ def run(*args)
74
+ return if expiry && Time.new > expiry
75
+ ret = do_run *args
76
+ unless ret.nil?
77
+ Sqreen.internal_track(event_name,
78
+ 'properties' => event_properties(*args).
79
+ merge('action_id' => id))
80
+ end
81
+ ret
82
+ end
83
+
84
+ protected
85
+
86
+ def do_run(*_args)
87
+ raise ::Sqreen::NotImplementedYet, "do_run not implemented in #{self.class}"
88
+ # implement in subclasses
89
+ end
90
+
91
+ def event_properties(*_run_args)
92
+ raise ::Sqreen::NotImplementedYet, "event_properties not implemented in #{self.class}"
93
+ # implement in subclasses
94
+ end
95
+
96
+ private
97
+
98
+ def event_name
99
+ "sq.action.#{self.class.type_name}"
100
+ end
101
+
102
+ @@subclasses = {}
103
+ class << self
104
+ private :new
105
+
106
+ attr_reader :type_name
107
+
108
+ def get_type_class(name)
109
+ @@subclasses[name]
110
+ end
111
+
112
+ def known_types
113
+ @@subclasses.keys
114
+ end
115
+
116
+ def inherited(subclass)
117
+ class << subclass
118
+ public :new
119
+ end
120
+ end
121
+
122
+ protected
123
+
124
+ def type_name=(name)
125
+ @type_name = name
126
+ @@subclasses[name] = self
127
+ end
128
+ end
129
+ end
130
+
131
+ module IpRanges
132
+ attr_reader :ranges
133
+
134
+ def parse_ip_ranges(params)
135
+ ranges = params['ip_cidr']
136
+ unless ranges && ranges.is_a?(Array) && !ranges.empty?
137
+ raise 'no non-empty ip_cidr array present'
138
+ end
139
+
140
+ @ranges = ranges.map &IPAddr.method(:new)
141
+ end
142
+
143
+ def matches_ip?(client_ip)
144
+ parsed_ip = IPAddr.new client_ip
145
+ found = ranges.find { |r| r.include? parsed_ip }
146
+ return false unless found
147
+
148
+ Sqreen.log.debug("Client ip #{client_ip} matches #{found.inspect}")
149
+ true
150
+ end
151
+ end
152
+
153
+ # Block a list of IP address ranges. Standard "raise" behavior.
154
+ class BlockIp < Base
155
+ include IpRanges
156
+ self.type_name = 'block_ip'
157
+
158
+ def initialize(id, duration, params = {})
159
+ super(id, duration)
160
+ parse_ip_ranges params
161
+ end
162
+
163
+ def do_run(client_ip)
164
+ return nil unless matches_ip? client_ip
165
+ e = Sqreen::AttackBlocked.new("Blocked client's IP (action: #{id}). No action is required")
166
+ { :status => :raise, :exception => e }
167
+ end
168
+
169
+ def event_properties(client_ip)
170
+ { 'ip_address' => client_ip }
171
+ end
172
+ end
173
+
174
+ # Block a list of IP address ranges by forcefully redirecting the user
175
+ # to a specific URL.
176
+ class RedirectIp < Base
177
+ include IpRanges
178
+ self.type_name = 'redirect_ip'
179
+
180
+ attr_reader :redirect_url
181
+
182
+ def initialize(id, duration, params = {})
183
+ super(id, duration)
184
+ @redirect_url = params['url']
185
+ raise "no url provided for action #{id}" unless @redirect_url
186
+ parse_ip_ranges params
187
+ end
188
+
189
+ def do_run(client_ip)
190
+ return nil unless matches_ip? client_ip
191
+ Sqreen.log.info "Will request redirect for client with IP #{client_ip} (action: #{id}). "
192
+ {
193
+ :status => :skip,
194
+ :new_return_value => [303, { 'Location' => @redirect_url }, ['']],
195
+ }
196
+ end
197
+
198
+ def event_properties(client_ip)
199
+ { 'ip_address' => client_ip, 'url' => @redirect_url }
200
+ end
201
+ end
202
+ end
203
+ end
@@ -9,13 +9,14 @@ module Sqreen
9
9
  # the value located at the given binding
10
10
  class BindingAccessor
11
11
  PathElem = Struct.new(:kind, :value)
12
- attr_reader :path, :expression, :final_transform
12
+ attr_reader :path, :expression, :final_transform, :transform_args
13
13
 
14
14
  # Expression to be accessed
15
15
  # @param expression [String] expression to read
16
16
  # @param convert [Boolean] wheter to convert objects to
17
17
  # simpler types (Array, Hash, String...)
18
18
  def initialize(expression, convert = false)
19
+ @transform_args = []
19
20
  @final_transform = nil
20
21
  @expression = expression
21
22
  @path = []
@@ -41,6 +42,19 @@ module Sqreen
41
42
  value
42
43
  end
43
44
 
45
+ # implement eql? and hash for uniq
46
+ def hash
47
+ expression.hash ^ @convert.hash
48
+ end
49
+
50
+ def eql?(other)
51
+ self.class == other.class &&
52
+ expression == other.expression &&
53
+ @convert == other.instance_variable_get('@convert')
54
+ end
55
+
56
+ alias == eql?
57
+
44
58
  protected
45
59
 
46
60
  STRING_KIND = 'string'.freeze
@@ -138,8 +152,17 @@ module Sqreen
138
152
  parts.join('|').rstrip
139
153
  end
140
154
 
155
+ TRANSFORM_ARGS_REGEXP = /\(([^)]*)\)\z/
141
156
  def final_transform=(transform)
142
157
  transform.strip!
158
+
159
+ transform.sub!(TRANSFORM_ARGS_REGEXP, '')
160
+ @transform_args = if Regexp.last_match
161
+ Regexp.last_match(1).split(/,\s*/)
162
+ else
163
+ []
164
+ end
165
+
143
166
  unless KNOWN_TRANSFORMS.include?(transform)
144
167
  raise Sqreen::Exception, "Invalid transform #{transform}"
145
168
  end
@@ -277,12 +300,58 @@ module Sqreen
277
300
  end
278
301
  values
279
302
  end
280
- end
303
+
304
+ def concat_keys_and_values(value, max_size = nil)
305
+ return nil if value.nil?
306
+ values = Set.new
307
+ max_size = max_size.to_i if max_size
308
+ res = ''
309
+ descend(value) do |x|
310
+ next unless values.add?(x)
311
+ x = x.to_s
312
+ return res if max_size && res.size + x.size + 1 > max_size
313
+ res << "\n" unless res.empty?
314
+ res << x
315
+ end
316
+ res
317
+ end
318
+
319
+ private
320
+
321
+ def descend(value, max_iter = 1000)
322
+ seen = Set.new
323
+ look_into = [value]
324
+ idx = 0
325
+ until look_into.empty? || max_iter <= idx
326
+ idx += 1
327
+ val = look_into.pop
328
+
329
+ case val
330
+ when Hash
331
+ next unless seen.add?(val.object_id)
332
+ look_into.concat(val.keys)
333
+ look_into.concat(val.values)
334
+ when Array
335
+ next unless seen.add?(val.object_id)
336
+ look_into.concat(val)
337
+ else
338
+ next if val.respond_to?(:seek)
339
+ if val.respond_to?(:each)
340
+ next unless seen.add?(val.object_id)
341
+ val.each { |v| look_into << v }
342
+ else
343
+ yield val
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end # end module Transforms
349
+
281
350
  include Transforms
282
351
  KNOWN_TRANSFORMS = Transforms.public_instance_methods.map(&:to_s)
283
352
 
284
353
  def transform(value)
285
- send(@final_transform, value) if @final_transform
354
+ send(@final_transform, value, *@transform_args) if @final_transform
286
355
  end
287
- end
356
+ end # end class BindingAccessor
288
357
  end
@@ -109,4 +109,59 @@ module Sqreen
109
109
  @block.call
110
110
  end
111
111
  end
112
+
113
+ # Framework-aware callback
114
+ class FrameworkCB < CB
115
+ attr_accessor :framework
116
+
117
+ def whitelisted?
118
+ whitelisted = SharedStorage.get(:whitelisted)
119
+ return whitelisted unless whitelisted.nil?
120
+ framework && !framework.whitelisted_match.nil?
121
+ end
122
+
123
+ # Record an attack event into Sqreen system
124
+ # @param infos [Hash] Additional information about request
125
+ def record_event(infos, at = Time.now.utc)
126
+ return unless framework
127
+ payload = {
128
+ :infos => infos,
129
+ :rulespack_id => rulespack_id,
130
+ :rule_name => rule_name,
131
+ :test => test,
132
+ :time => at,
133
+ }
134
+ if payload_tpl.include?('context')
135
+ payload[:backtrace] = Sqreen::Context.new.bt
136
+ end
137
+ framework.observe(:attacks, payload, payload_tpl)
138
+ end
139
+
140
+ # Record a metric observation
141
+ # @param category [String] Name of the metric observed
142
+ # @param key [String] aggregation key
143
+ # @param observation [Object] data observed
144
+ # @param at [Time] time when observation was made
145
+ def record_observation(category, key, observation, at = Time.now.utc)
146
+ return unless framework
147
+ framework.observe(:observations, [category, key, observation, at], [], false)
148
+ end
149
+
150
+ # Record an exception that just occurred
151
+ # @param exception [Exception] Exception to send over
152
+ # @param infos [Hash] Additional contextual information
153
+ def record_exception(exception, infos = {}, at = Time.now.utc)
154
+ return unless framework
155
+ payload = {
156
+ :exception => exception,
157
+ :infos => infos,
158
+ :rulespack_id => rulespack_id,
159
+ :rule_name => rule_name,
160
+ :test => test,
161
+ :time => at,
162
+ :backtrace => exception.backtrace || Sqreen::Context.bt,
163
+ }
164
+ framework.observe(:sqreen_exceptions, payload)
165
+ end
166
+ end
112
167
  end
@@ -68,7 +68,13 @@ module Sqreen
68
68
 
69
69
  def event_keys(event)
70
70
  return [event_key(event)] unless event.is_a?(Sqreen::RequestRecord)
71
- event.observed.fetch(:attacks, []).map { |e| "att-#{e[:rule_name]}" } + event.observed.fetch(:sqreen_exceptions, []).map { |e| "rex-#{e[:exception].class}" }
71
+ res = []
72
+ res += event.observed.fetch(:attacks, []).map { |e| "att-#{e[:rule_name]}" }
73
+ res += event.observed.fetch(:sqreen_exceptions, []).map { |e| "rex-#{e[:exception].class}" }
74
+ res += event.observed.fetch(:sdk, []).select { |e|
75
+ e[0] == :track
76
+ }.map { |e| "sdk-track".freeze }
77
+ return res
72
78
  end
73
79
 
74
80
  def event_key(event)
@@ -10,6 +10,7 @@ require 'sqreen/events/remote_exception'
10
10
  require 'sqreen/rules_signature'
11
11
  require 'sqreen/shared_storage'
12
12
  require 'sqreen/rules_callbacks/record_request_context'
13
+ require 'sqreen/rules_callbacks/run_req_start_actions'
13
14
  require 'set'
14
15
 
15
16
  # How to override a class method:
@@ -310,6 +311,7 @@ module Sqreen
310
311
  args = ret[:args]
311
312
  when :raise, 'raise'
312
313
  Thread.current[:sqreen_in_use] = false
314
+ raise ret[:exception] if ret.key?(:exception)
313
315
  raise Sqreen::AttackBlocked, "Sqreen blocked a security threat (type: #{ret[:rule_name]}). No action is required."
314
316
  end
315
317
  end
@@ -659,9 +661,16 @@ module Sqreen
659
661
 
660
662
  attr_accessor :metrics_engine
661
663
 
664
+ # @return [Array<Sqreen::CB>]
665
+ def hardcoded_callbacks(framework)
666
+ [
667
+ Sqreen::Rules::RunReqStartActions.new(framework)
668
+ ]
669
+ end
670
+
662
671
  # Instrument the application code using the rules
663
672
  # @param rules [Array<Hash>] Rules to instrument
664
- # @param metrics_engine [MetricsStore] Metric storage facility
673
+ # @param framework [Sqreen::Frameworks::GenericFramework]
665
674
  def instrument!(rules, framework)
666
675
  verifier = nil
667
676
  if Sqreen.features['rules_signature'] &&
@@ -671,13 +680,18 @@ module Sqreen
671
680
  else
672
681
  Sqreen.log.debug('Rules signature is not enabled')
673
682
  end
683
+
674
684
  remove_all_callbacks # Force cb tree to be empty before instrumenting
685
+
675
686
  rules.each do |rule|
676
687
  rcb = Sqreen::Rules.cb_from_rule(rule, self, metrics_engine, verifier)
677
688
  next unless rcb
678
689
  rcb.framework = framework
679
690
  add_callback(rcb)
680
691
  end
692
+
693
+ hardcoded_callbacks(framework).each { |cb| add_callback(cb) }
694
+
681
695
  Sqreen.instrumentation_ready = true
682
696
  end
683
697
 
@@ -10,6 +10,7 @@ module Sqreen
10
10
  :instrumentation_enable => :setup_instrumentation,
11
11
  :instrumentation_remove => :remove_instrumentation,
12
12
  :rules_reload => :reload_rules,
13
+ :actions_reload => :reload_actions,
13
14
  :features_get => :features,
14
15
  :features_change => :change_features,
15
16
  :force_logout => :shutdown,
@@ -19,6 +20,14 @@ module Sqreen
19
20
  :performance_budget => :change_performance_budget,
20
21
  }.freeze
21
22
 
23
+ # wraps output returned by a command that should also result in status: false
24
+ class FailureOutput
25
+ attr_reader :wrapped_output
26
+ def initialize(output)
27
+ @wrapped_output = output
28
+ end
29
+ end
30
+
22
31
  attr_reader :uuid
23
32
 
24
33
  def initialize(json_desc)
@@ -83,11 +92,13 @@ module Sqreen
83
92
  def format_output(output)
84
93
  case output
85
94
  when NilClass
86
- return { :status => false, :reason => 'nil returned' }
95
+ { :status => false, :reason => 'nil returned' }
87
96
  when TrueClass
88
- return { :status => true }
97
+ { :status => true }
98
+ when FailureOutput
99
+ { :status => false, :output => output.wrapped_output }
89
100
  else
90
- return { :status => true, :output => output }
101
+ { :status => true, :output => output }
91
102
  end
92
103
  end
93
104
  end
@@ -14,7 +14,7 @@ require 'sqreen/payload_creator'
14
14
  module Sqreen
15
15
  module Rules
16
16
  # Base class for callback that are initialized by rules from Sqreen
17
- class RuleCB < CB
17
+ class RuleCB < FrameworkCB
18
18
  include Conditionable
19
19
  include CallCountable
20
20
  # If nothing was asked by the rule we will ask for all sections available
@@ -23,7 +23,6 @@ module Sqreen
23
23
  attr_reader :test
24
24
  attr_reader :payload_tpl
25
25
  attr_reader :block
26
- attr_accessor :framework
27
26
 
28
27
  # @params klass [String] class instrumented
29
28
  # @params method [String] method that was instrumented
@@ -48,12 +47,6 @@ module Sqreen
48
47
  @rule[Attrs::RULESPACK_ID]
49
48
  end
50
49
 
51
- def whitelisted?
52
- whitelisted = SharedStorage.get(:whitelisted)
53
- return whitelisted unless whitelisted.nil?
54
- framework && !framework.whitelisted_match.nil?
55
- end
56
-
57
50
  # Recommend taking an action (optionnally adding more data/context)
58
51
  #
59
52
  # This will format the requested action and optionnally
@@ -63,50 +56,6 @@ module Sqreen
63
56
  additional_data.merge(:status => action)
64
57
  end
65
58
 
66
- # Record an attack event into Sqreen system
67
- # @param infos [Hash] Additional information about request
68
- def record_event(infos, at = Time.now.utc)
69
- return unless framework
70
- payload = {
71
- :infos => infos,
72
- :rulespack_id => rulespack_id,
73
- :rule_name => rule_name,
74
- :test => test,
75
- :time => at,
76
- }
77
- if payload_tpl.include?('context')
78
- payload[:backtrace] = Sqreen::Context.new.bt
79
- end
80
- framework.observe(:attacks, payload, payload_tpl)
81
- end
82
-
83
- # Record a metric observation
84
- # @param category [String] Name of the metric observed
85
- # @param key [String] aggregation key
86
- # @param observation [Object] data observed
87
- # @param at [Time] time when observation was made
88
- def record_observation(category, key, observation, at = Time.now.utc)
89
- return unless framework
90
- framework.observe(:observations, [category, key, observation, at], [], false)
91
- end
92
-
93
- # Record an exception that just occurred
94
- # @param exception [Exception] Exception to send over
95
- # @param infos [Hash] Additional contextual information
96
- def record_exception(exception, infos = {}, at = Time.now.utc)
97
- return unless framework
98
- payload = {
99
- :exception => exception,
100
- :infos => infos,
101
- :rulespack_id => rulespack_id,
102
- :rule_name => rule_name,
103
- :test => test,
104
- :time => at,
105
- :backtrace => exception.backtrace || Sqreen::Context.bt,
106
- }
107
- framework.observe(:sqreen_exceptions, payload)
108
- end
109
-
110
59
  def overtime!
111
60
  return false unless @overtimeable
112
61
  Sqreen.log.debug { "rulecb #{self} is overtime!" }
@@ -0,0 +1,61 @@
1
+ # Copyright (c) 2018 Sqreen. All Rights Reserved.
2
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+
4
+ require 'sqreen/rule_callback'
5
+ require 'sqreen/actions'
6
+ require 'sqreen/middleware'
7
+
8
+ module Sqreen
9
+ module Rules
10
+ # Runs actions concerned with whether the request ought to be served
11
+ class RunReqStartActions < FrameworkCB
12
+ def initialize(framework)
13
+ if defined?(Sqreen::Frameworks::SinatraFramework) &&
14
+ framework.is_a?(Sqreen::Frameworks::SinatraFramework)
15
+ super(Sinatra::ExtendedRack, :call)
16
+ elsif defined?(Sqreen::Frameworks::RailsFramework) &&
17
+ framework.is_a?(Sqreen::Frameworks::RailsFramework)
18
+ super(Sqreen::RailsMiddleware, :call)
19
+ else
20
+ # last resort; we won't get nice errors
21
+ super(Sqreen::Middleware, :call)
22
+ end
23
+
24
+ self.framework = framework
25
+ end
26
+
27
+ def whitelisted?
28
+ whitelisted = SharedStorage.get(:whitelisted)
29
+ return whitelisted unless whitelisted.nil?
30
+ framework && !framework.whitelisted_match.nil?
31
+ end
32
+
33
+ def pre(_inst, _args, _budget = nil, &_block)
34
+ return unless framework
35
+ ip = framework.client_ip
36
+ return unless ip
37
+
38
+ actions = actions_repo[Sqreen::Actions::BlockIp] +
39
+ actions_repo[Sqreen::Actions::RedirectIp]
40
+
41
+ actions.each do |act|
42
+ res = run_client_ip_action(act, ip)
43
+ return res unless res.nil?
44
+ end
45
+ nil
46
+ end
47
+
48
+ private
49
+
50
+ # @param action [Sqreen::Actions::Base]
51
+ def run_client_ip_action(action, client_ip)
52
+ action.run client_ip
53
+ end
54
+
55
+ # @return [Sqreen::Actions::Repository]
56
+ def actions_repo
57
+ Sqreen::Actions::Repository.instance
58
+ end
59
+ end
60
+ end
61
+ end
@@ -214,6 +214,7 @@ module Sqreen
214
214
  def remove_instrumentation(_context_infos = {})
215
215
  Sqreen.log.debug 'removing instrumentation'
216
216
  instrumenter.remove_all_callbacks
217
+ Sqreen::Actions::Repository.instance.clear
217
218
  true
218
219
  end
219
220
 
@@ -221,12 +222,43 @@ module Sqreen
221
222
  Sqreen.log.debug 'Reloading rules'
222
223
  rulespack_id, rules = load_rules
223
224
  instrumenter.remove_all_callbacks
225
+ Sqreen::Actions::Repository.instance.clear
224
226
 
225
227
  @framework.instrument_when_ready!(instrumenter, rules)
226
228
  Sqreen.log.debug 'Rules reloaded'
227
229
  rulespack_id.to_s
228
230
  end
229
231
 
232
+ def reload_actions(_context_infos = {})
233
+ Sqreen.log.debug 'Reloading actions'
234
+
235
+ data = session.get_actionspack
236
+
237
+ unless data.respond_to?(:[]) && data['status']
238
+ Sqreen.log.warn('Could not load actions')
239
+ return RemoteCommand::FailureOutput.new(
240
+ :error => 'Could not load actions from /actionspack'
241
+ )
242
+ end
243
+
244
+ action_hashes = data['actions']
245
+ unless action_hashes.respond_to? :each
246
+ Sqreen.log.warn('No action definitions in response')
247
+ return RemoteCommand::FailureOutput.new(
248
+ :error => 'No action definitions in response'
249
+ )
250
+ end
251
+ Sqreen.log.debug("Loading actions from hashes #{action_hashes}")
252
+
253
+ unsupported = load_actions(action_hashes)
254
+
255
+ if unsupported.empty?
256
+ true
257
+ else
258
+ RemoteCommand::FailureOutput.new(:unsupported_actions => unsupported.to_a)
259
+ end
260
+ end
261
+
230
262
  def process_commands(commands, context_infos = {})
231
263
  return if commands.nil? || commands.empty?
232
264
  res = RemoteCommand.process_list(self, commands, context_infos)
@@ -380,5 +412,32 @@ module Sqreen
380
412
  end
381
413
  end
382
414
  end
415
+
416
+ private
417
+
418
+ def load_actions(hashes)
419
+ unsupported = Set.new
420
+
421
+ actions = hashes.map do |h|
422
+ begin
423
+ Sqreen::Actions.deserialize_action(h)
424
+ rescue Sqreen::Actions::UnknownActionType => e
425
+ Sqreen.log.warn("Unsupported action type: #{e.action_type}")
426
+ unsupported << e.action_type
427
+ nil
428
+ rescue => e
429
+ raise Sqreen::Exception, "Invalid action hash: #{h}: #{e.message}"
430
+ end
431
+ end
432
+
433
+ actions = actions.reject(&:nil?)
434
+ Sqreen.log.debug("Will add #{actions.size} valid actions")
435
+
436
+ repos = Sqreen::Actions::Repository.instance
437
+ repos.clear
438
+ actions.each { |action| repos << action }
439
+
440
+ unsupported
441
+ end
383
442
  end
384
443
  end
@@ -3,6 +3,10 @@
3
3
 
4
4
  # Sqreen Namespace
5
5
  module Sqreen
6
+
7
+ SDK_RESERVED_PREFIX = 'sq.'.freeze
8
+ TRACK_PAYLOAD_DATA = ['request'.freeze, 'params'.freeze, 'headers'.freeze].freeze
9
+
6
10
  # Sqreen SDK
7
11
  class << self
8
12
  # Authentication tracking method
@@ -18,5 +22,35 @@ module Sqreen
18
22
  [], false
19
23
  )
20
24
  end
25
+
26
+ def track(event_name, options = {})
27
+ return unless Sqreen.framework
28
+ if event_name.start_with? SDK_RESERVED_PREFIX
29
+ Sqreen.log.warn("Event names starting with '#{SDK_RESERVED_PREFIX}' " \
30
+ 'are reserved. Event ignored.')
31
+ return false
32
+ end
33
+ internal_track(event_name, options)
34
+ end
35
+
36
+ # For internal usage. Users are to call track() instead.
37
+ def internal_track(event_name, options = {})
38
+ properties = options[:properties]
39
+ authentication_keys = options[:user_identifiers]
40
+ timestamp = options[:timestamp] || Time.now.utc
41
+ # Not in SDK v0
42
+ # request = options[:request]
43
+
44
+ args = {}
45
+ args[:authentication_keys] = authentication_keys if authentication_keys
46
+ args[:properties] = properties if properties
47
+
48
+ Sqreen.framework.observe(
49
+ :sdk,
50
+ [:track, timestamp, event_name, :args => args],
51
+ TRACK_PAYLOAD_DATA, true
52
+ )
53
+ true
54
+ end
21
55
  end
22
56
  end
@@ -159,8 +159,9 @@ module Sqreen
159
159
 
160
160
  def do_http_request(method, path, data, headers = {}, max_retry = 2)
161
161
  connect unless connected?
162
+ now = Time.now.utc
162
163
  headers['X-Session-Key'] = @session_id if @session_id
163
- headers['X-Sqreen-Time'] = Time.now.utc.to_f.to_s
164
+ headers['X-Sqreen-Time'] = now.to_f.to_s
164
165
  headers['User-Agent'] = "sqreen-ruby/#{Sqreen::VERSION}"
165
166
  headers['X-Sqreen-Beta'] = format('pid=%d;tid=%s;nb=%d;t=%f',
166
167
  Process.pid,
@@ -206,7 +207,7 @@ module Sqreen
206
207
  Sqreen.log.debug 'warning: empty return value'
207
208
  end
208
209
  end
209
- Sqreen.log.debug format('%s %s (DONE)', method, path)
210
+ Sqreen.log.debug format('%s %s (DONE in %f ms)', method, path, (Time.now.utc - now) * 1000)
210
211
  res
211
212
  end
212
213
 
@@ -264,6 +265,10 @@ module Sqreen
264
265
  'dependencies' => dependencies)
265
266
  end
266
267
 
268
+ def get_actionspack
269
+ resilient_get('actionspack')
270
+ end
271
+
267
272
  def post_request_record(request_record)
268
273
  resilient_post('request_record', request_record.to_hash)
269
274
  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.11.3'.freeze
4
+ VERSION = '1.12.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.11.3
4
+ version: 1.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sqreen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-26 00:00:00.000000000 Z
11
+ date: 2018-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: execjs
@@ -50,6 +50,7 @@ files:
50
50
  - Rakefile
51
51
  - lib/sqreen-alt.rb
52
52
  - lib/sqreen.rb
53
+ - lib/sqreen/actions.rb
53
54
  - lib/sqreen/attack_detected.html
54
55
  - lib/sqreen/binding_accessor.rb
55
56
  - lib/sqreen/ca.crt
@@ -110,6 +111,7 @@ files:
110
111
  - lib/sqreen/rules_callbacks/record_request_context.rb
111
112
  - lib/sqreen/rules_callbacks/reflected_xss.rb
112
113
  - lib/sqreen/rules_callbacks/regexp_rule.rb
114
+ - lib/sqreen/rules_callbacks/run_req_start_actions.rb
113
115
  - lib/sqreen/rules_callbacks/shell_env.rb
114
116
  - lib/sqreen/rules_callbacks/url_matches.rb
115
117
  - lib/sqreen/rules_callbacks/user_agent_matches.rb