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 +4 -4
- data/lib/sqreen/actions.rb +203 -0
- data/lib/sqreen/binding_accessor.rb +73 -4
- data/lib/sqreen/callbacks.rb +55 -0
- data/lib/sqreen/deliveries/batch.rb +7 -1
- data/lib/sqreen/instrumentation.rb +15 -1
- data/lib/sqreen/remote_command.rb +14 -3
- data/lib/sqreen/rule_callback.rb +1 -52
- data/lib/sqreen/rules_callbacks/run_req_start_actions.rb +61 -0
- data/lib/sqreen/runner.rb +59 -0
- data/lib/sqreen/sdk.rb +34 -0
- data/lib/sqreen/session.rb +7 -2
- data/lib/sqreen/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 867b430d86c473c22fcad2e5531c5ee70078ffa45201e8e4e906127e854eaf84
|
4
|
+
data.tar.gz: e8ba868e3b21824963da46dde7abbf171f17cb83732af65599fb84eaa4181245
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
data/lib/sqreen/callbacks.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
95
|
+
{ :status => false, :reason => 'nil returned' }
|
87
96
|
when TrueClass
|
88
|
-
|
97
|
+
{ :status => true }
|
98
|
+
when FailureOutput
|
99
|
+
{ :status => false, :output => output.wrapped_output }
|
89
100
|
else
|
90
|
-
|
101
|
+
{ :status => true, :output => output }
|
91
102
|
end
|
92
103
|
end
|
93
104
|
end
|
data/lib/sqreen/rule_callback.rb
CHANGED
@@ -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 <
|
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
|
data/lib/sqreen/runner.rb
CHANGED
@@ -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
|
data/lib/sqreen/sdk.rb
CHANGED
@@ -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
|
data/lib/sqreen/session.rb
CHANGED
@@ -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'] =
|
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
|
data/lib/sqreen/version.rb
CHANGED
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.
|
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-
|
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
|