sqreen 1.20.1-java → 1.21.0.beta3-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +16 -0
- data/lib/sqreen/actions/block_user.rb +1 -1
- data/lib/sqreen/actions/redirect_ip.rb +1 -1
- data/lib/sqreen/actions/redirect_user.rb +1 -1
- data/lib/sqreen/attack_detected.html +1 -2
- data/lib/sqreen/condition_evaluator.rb +9 -2
- data/lib/sqreen/conditionable.rb +24 -6
- data/lib/sqreen/configuration.rb +1 -1
- data/lib/sqreen/deferred_logger.rb +50 -14
- data/lib/sqreen/deliveries/batch.rb +8 -1
- data/lib/sqreen/deprecation.rb +38 -0
- data/lib/sqreen/ecosystem.rb +96 -0
- data/lib/sqreen/ecosystem/dispatch_table.rb +43 -0
- data/lib/sqreen/ecosystem/exception_reporting.rb +26 -0
- data/lib/sqreen/ecosystem/http/net_http.rb +50 -0
- data/lib/sqreen/ecosystem/http/rack_request.rb +39 -0
- data/lib/sqreen/ecosystem/loggable.rb +13 -0
- data/lib/sqreen/ecosystem/module_api.rb +30 -0
- data/lib/sqreen/ecosystem/module_api/event_listener.rb +18 -0
- data/lib/sqreen/ecosystem/module_api/instrumentation.rb +23 -0
- data/lib/sqreen/ecosystem/module_api/message_producer.rb +51 -0
- data/lib/sqreen/ecosystem/module_api/signal_producer.rb +24 -0
- data/lib/sqreen/ecosystem/module_api/tracing.rb +45 -0
- data/lib/sqreen/ecosystem/module_api/tracing/client_data.rb +31 -0
- data/lib/sqreen/ecosystem/module_api/tracing/server_data.rb +27 -0
- data/lib/sqreen/ecosystem/module_api/tracing_id_generation.rb +16 -0
- data/lib/sqreen/ecosystem/module_api/transaction_storage.rb +71 -0
- data/lib/sqreen/ecosystem/module_registry.rb +44 -0
- data/lib/sqreen/ecosystem/redis/redis_connection.rb +43 -0
- data/lib/sqreen/ecosystem/tracing/modules/client.rb +31 -0
- data/lib/sqreen/ecosystem/tracing/modules/server.rb +30 -0
- data/lib/sqreen/ecosystem/tracing/sampler.rb +160 -0
- data/lib/sqreen/ecosystem/tracing/sampling_configuration.rb +150 -0
- data/lib/sqreen/ecosystem/tracing/signals/tracing_client.rb +53 -0
- data/lib/sqreen/ecosystem/tracing/signals/tracing_server.rb +53 -0
- data/lib/sqreen/ecosystem/tracing_broker.rb +101 -0
- data/lib/sqreen/ecosystem/tracing_id_setup.rb +34 -0
- data/lib/sqreen/ecosystem/transaction_storage.rb +64 -0
- data/lib/sqreen/ecosystem/util/call_writers_from_init.rb +13 -0
- data/lib/sqreen/ecosystem_integration.rb +87 -0
- data/lib/sqreen/ecosystem_integration/around_callbacks.rb +99 -0
- data/lib/sqreen/ecosystem_integration/instrumentation_service.rb +42 -0
- data/lib/sqreen/ecosystem_integration/request_lifecycle_tracking.rb +58 -0
- data/lib/sqreen/ecosystem_integration/signal_consumption.rb +35 -0
- data/lib/sqreen/events/request_record.rb +0 -1
- data/lib/sqreen/frameworks/generic.rb +24 -1
- data/lib/sqreen/frameworks/rails.rb +0 -7
- data/lib/sqreen/frameworks/request_recorder.rb +2 -0
- data/lib/sqreen/graft/call.rb +106 -19
- data/lib/sqreen/graft/callback.rb +1 -1
- data/lib/sqreen/graft/hook.rb +212 -100
- data/lib/sqreen/graft/hook_point.rb +18 -11
- data/lib/sqreen/legacy/instrumentation.rb +22 -10
- data/lib/sqreen/legacy/old_event_submission_strategy.rb +9 -2
- data/lib/sqreen/log.rb +3 -2
- data/lib/sqreen/log/loggable.rb +1 -0
- data/lib/sqreen/logger.rb +24 -0
- data/lib/sqreen/metrics.rb +1 -0
- data/lib/sqreen/metrics/req_detailed.rb +41 -0
- data/lib/sqreen/metrics_store.rb +11 -0
- data/lib/sqreen/null_logger.rb +22 -0
- data/lib/sqreen/remote_command.rb +4 -0
- data/lib/sqreen/rules.rb +8 -4
- data/lib/sqreen/rules/blacklist_ips_cb.rb +2 -2
- data/lib/sqreen/rules/custom_error_cb.rb +3 -3
- data/lib/sqreen/rules/rule_cb.rb +4 -2
- data/lib/sqreen/rules/waf_cb.rb +3 -3
- data/lib/sqreen/runner.rb +63 -8
- data/lib/sqreen/session.rb +2 -0
- data/lib/sqreen/signals/conversions.rb +6 -1
- data/lib/sqreen/version.rb +1 -1
- data/lib/sqreen/weave/budget.rb +35 -0
- data/lib/sqreen/weave/legacy/instrumentation.rb +274 -132
- data/lib/sqreen/worker.rb +6 -2
- metadata +46 -9
- data/lib/sqreen/encoding_sanitizer.rb +0 -27
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'sqreen/ecosystem/loggable'
|
2
|
+
|
3
|
+
module Sqreen
|
4
|
+
module Ecosystem
|
5
|
+
class ModuleRegistry
|
6
|
+
include Sqreen::Ecosystem::Loggable
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@mods = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def register(mod)
|
13
|
+
@mods << mod
|
14
|
+
end
|
15
|
+
|
16
|
+
def init_all
|
17
|
+
logger.info { "Initializing #{@mods.size} ecosystem modules" }
|
18
|
+
each_module do |mod|
|
19
|
+
next unless mod.respond_to? :setup
|
20
|
+
logger.debug { "Initializing module with type #{mod.class}" }
|
21
|
+
mod.setup
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def destroy_all
|
26
|
+
# not implemented
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param [Class] type
|
30
|
+
def each_module(type = nil, &block)
|
31
|
+
selected_mods = type ? (@mods.select { |mod| mod.is_a?(type) }) : @mods
|
32
|
+
if block_given?
|
33
|
+
selected_mods.each(&block)
|
34
|
+
else
|
35
|
+
selected_mods.each
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def module_subset(type)
|
40
|
+
each_module(type).to_a
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'sqreen/ecosystem/module_api'
|
2
|
+
require 'sqreen/ecosystem/module_api/instrumentation'
|
3
|
+
require 'sqreen/ecosystem/module_api/message_producer'
|
4
|
+
require 'sqreen/ecosystem/module_api/tracing_id_generation'
|
5
|
+
require 'sqreen/ecosystem/module_api/tracing/client_data'
|
6
|
+
|
7
|
+
module Sqreen
|
8
|
+
module Ecosystem
|
9
|
+
module Redis
|
10
|
+
class RedisConnection
|
11
|
+
class RedisConnectionData
|
12
|
+
include ModuleApi::Tracing::ClientData
|
13
|
+
|
14
|
+
attr_accessor :port
|
15
|
+
end
|
16
|
+
|
17
|
+
include ModuleApi::Instrumentation
|
18
|
+
include ModuleApi::MessageProducer
|
19
|
+
include ModuleApi::TracingIdGeneration
|
20
|
+
|
21
|
+
def setup
|
22
|
+
advice = wrap_for_interest(ModuleApi::Tracing::ClientData, &method(:before_advice))
|
23
|
+
instrument 'Redis::Connection::TCPSocket#connect',
|
24
|
+
before: advice
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def before_advice
|
30
|
+
host = call.args[0]
|
31
|
+
port = call.args[1]
|
32
|
+
|
33
|
+
RedisConnectionData.new(
|
34
|
+
transport: 'redis',
|
35
|
+
host: host,
|
36
|
+
port: port,
|
37
|
+
tracing_identifier: create_tracing_id
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'sqreen/ecosystem/tracing/signals/tracing_client'
|
2
|
+
require 'sqreen/ecosystem/module_api/tracing'
|
3
|
+
require 'sqreen/ecosystem/module_api/tracing/client_data'
|
4
|
+
|
5
|
+
module Sqreen
|
6
|
+
module Ecosystem
|
7
|
+
module Tracing
|
8
|
+
module Modules
|
9
|
+
class Client
|
10
|
+
include ModuleApi::Tracing
|
11
|
+
|
12
|
+
consumes ModuleApi::Tracing::ClientData
|
13
|
+
fixed_scope 'client'
|
14
|
+
|
15
|
+
# @param [Sqreen::Ecosystem::ModuleApi::Tracing::ClientData] data
|
16
|
+
def receive(data)
|
17
|
+
signal = Tracing::Signals::TracingClient.new
|
18
|
+
signal.payload = Tracing::Signals::TracingClient::Payload.new(
|
19
|
+
transport: data.transport,
|
20
|
+
host: data.host,
|
21
|
+
ip: data.ip,
|
22
|
+
tracing_identifier: data.tracing_identifier
|
23
|
+
)
|
24
|
+
|
25
|
+
submit_signal signal
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'sqreen/ecosystem/tracing/signals/tracing_server'
|
2
|
+
require 'sqreen/ecosystem/module_api/tracing'
|
3
|
+
require 'sqreen/ecosystem/module_api/tracing/server_data'
|
4
|
+
|
5
|
+
module Sqreen
|
6
|
+
module Ecosystem
|
7
|
+
module Tracing
|
8
|
+
module Modules
|
9
|
+
class Server
|
10
|
+
include ModuleApi::Tracing
|
11
|
+
|
12
|
+
consumes ModuleApi::Tracing::ServerData
|
13
|
+
fixed_scope 'server'
|
14
|
+
|
15
|
+
# @param [Sqreen::Ecosystem::ModuleApi::Tracing::ServerData] data
|
16
|
+
def receive(data)
|
17
|
+
signal = Tracing::Signals::TracingServer.new
|
18
|
+
signal.payload = Tracing::Signals::TracingServer::Payload.new(
|
19
|
+
transport: data.transport,
|
20
|
+
client_ip: data.client_ip,
|
21
|
+
tracing_identifier: data.tracing_identifier
|
22
|
+
)
|
23
|
+
|
24
|
+
submit_signal signal
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'thread' # for Mutex
|
2
|
+
require 'singleton'
|
3
|
+
require 'sqreen/ecosystem/loggable'
|
4
|
+
|
5
|
+
# see https://github.com/sqreen/TechDoc/blob/master/content/specs/spec000024-sampling.md
|
6
|
+
|
7
|
+
module Sqreen
|
8
|
+
module Ecosystem
|
9
|
+
module Tracing
|
10
|
+
class Sampler
|
11
|
+
# @param [Array<Hash{String=>Object}>] definition
|
12
|
+
def initialize(definition)
|
13
|
+
@lines = definition.map { |h| Line.new(h) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def should_sample?
|
17
|
+
line = @lines.find(&:triggers?)
|
18
|
+
line ? line.saved_definition : false
|
19
|
+
end
|
20
|
+
|
21
|
+
class Line
|
22
|
+
include Loggable
|
23
|
+
|
24
|
+
attr_reader :saved_definition
|
25
|
+
|
26
|
+
# @param [Hash{String=>Object}] definition
|
27
|
+
def initialize(definition)
|
28
|
+
@saved_definition = definition
|
29
|
+
@primitives = []
|
30
|
+
|
31
|
+
unknown = definition.keys - PRIMITIVES_MAP.keys
|
32
|
+
unless unknown.empty?
|
33
|
+
logger.warn "Unknown primitives: #{unknown}"
|
34
|
+
@primitives << AlwaysFalsePrimitive.instance
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
PRIMITIVES_MAP.each do |key, prim_class|
|
39
|
+
next unless definition[key]
|
40
|
+
@primitives << prim_class.new(definition[key])
|
41
|
+
end
|
42
|
+
# if @primitives is empty the line will always
|
43
|
+
# return true: [].all?(&:triggers?) is true
|
44
|
+
end
|
45
|
+
|
46
|
+
def triggers?
|
47
|
+
@primitives.all?(&:triggers?)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class AlwaysFalsePrimitive
|
52
|
+
include Singleton
|
53
|
+
|
54
|
+
def triggers?
|
55
|
+
false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class CallsPrimitive
|
60
|
+
def initialize(calls_period)
|
61
|
+
@calls_period = calls_period
|
62
|
+
@count = 0
|
63
|
+
@mutex = Mutex.new
|
64
|
+
end
|
65
|
+
|
66
|
+
def triggers?
|
67
|
+
prev_count = nil
|
68
|
+
@mutex.synchronize do
|
69
|
+
prev_count = @count
|
70
|
+
@count += 1
|
71
|
+
end
|
72
|
+
|
73
|
+
(prev_count % @calls_period).zero?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class RandomPrimitive
|
78
|
+
def initialize(probability)
|
79
|
+
@probability = probability
|
80
|
+
end
|
81
|
+
|
82
|
+
def triggers?
|
83
|
+
@probability >= rand
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class MaxDurationMinutesPrimitive
|
88
|
+
def initialize(time_in_minutes)
|
89
|
+
@deadline = Sqreen.time + time_in_minutes * 60
|
90
|
+
@passed = false # no locking needed
|
91
|
+
end
|
92
|
+
|
93
|
+
def triggers?
|
94
|
+
return false if @passed
|
95
|
+
if Sqreen.time > @deadline
|
96
|
+
@passed = true
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
true
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class TargetPerMinutePrimitive
|
104
|
+
def initialize(max_calls)
|
105
|
+
@max_calls = max_calls
|
106
|
+
@minute_last_call = cur_minute
|
107
|
+
@calls_accumulated = 0
|
108
|
+
@mutex = Mutex.new
|
109
|
+
end
|
110
|
+
|
111
|
+
def triggers?
|
112
|
+
this_minute = cur_minute
|
113
|
+
calls_cur_minute = @mutex.synchronize do
|
114
|
+
if @minute_last_call == this_minute
|
115
|
+
@calls_accumulated += 1
|
116
|
+
else
|
117
|
+
@minute_last_call = this_minute
|
118
|
+
@calls_accumulated = 1
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
calls_cur_minute <= @max_calls
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def cur_minute
|
128
|
+
(Sqreen.time / 60).floor
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class MaxCallsPrimitive
|
133
|
+
def initialize(max_calls)
|
134
|
+
@max_calls = max_calls
|
135
|
+
@disabled = false # to avoid lock
|
136
|
+
@mutex = Mutex.new
|
137
|
+
@num_calls = 0
|
138
|
+
end
|
139
|
+
|
140
|
+
def triggers?
|
141
|
+
return false if @disabled
|
142
|
+
num_calls = @mutex.synchronize do
|
143
|
+
@num_calls += 1
|
144
|
+
end
|
145
|
+
|
146
|
+
num_calls <= @max_calls
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
PRIMITIVES_MAP = {
|
151
|
+
"calls" => CallsPrimitive,
|
152
|
+
"random" => RandomPrimitive,
|
153
|
+
"max_duration_minutes" => MaxDurationMinutesPrimitive,
|
154
|
+
"target_per_minute" => TargetPerMinutePrimitive,
|
155
|
+
"max_calls" => MaxCallsPrimitive,
|
156
|
+
}.freeze
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'sqreen/ecosystem/loggable'
|
3
|
+
require 'sqreen/ecosystem/tracing/sampler'
|
4
|
+
|
5
|
+
module Sqreen
|
6
|
+
module Ecosystem
|
7
|
+
module Tracing
|
8
|
+
# tracing sampling configuration, as specified by the 2nd argument of the
|
9
|
+
# tracing_enable command.
|
10
|
+
# See https://github.com/sqreen/TechDoc/blob/master/content/specs/spec000025-enabling-tracing.md
|
11
|
+
class SamplingConfiguration
|
12
|
+
include Loggable
|
13
|
+
|
14
|
+
DEFAULT_SCOPE = '*'.freeze
|
15
|
+
|
16
|
+
def initialize(sampling_config)
|
17
|
+
@sampling_config = sampling_config
|
18
|
+
@samplers = {}
|
19
|
+
@samplers_virtual = build_virtual_holders(sampling_config)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [String] scope either the scope or the part behind @ in a virtual scope
|
23
|
+
# @param [String] qualifier the part after @ in a virtual scope or nil
|
24
|
+
def should_sample?(scope, qualifier = nil)
|
25
|
+
if qualifier
|
26
|
+
fetch_sampler_virtual(scope, qualifier).should_sample?
|
27
|
+
else
|
28
|
+
fetch_sampler(scope).should_sample?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def forget_virtual_scope(scope, qualifier)
|
33
|
+
holder = @samplers_virtual[scope]
|
34
|
+
return unless holder.delete!(qualifier)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def fetch_sampler_virtual(scope, qualifier)
|
40
|
+
holder = @samplers_virtual[scope]
|
41
|
+
|
42
|
+
# no virtual scope configured, fallback to plain scope
|
43
|
+
return fetch_sampler(scope) unless holder
|
44
|
+
|
45
|
+
holder[qualifier]
|
46
|
+
end
|
47
|
+
|
48
|
+
def fetch_sampler(scope)
|
49
|
+
sampler = @samplers[scope]
|
50
|
+
return sampler if sampler
|
51
|
+
|
52
|
+
cfg = @sampling_config[scope] || @sampling_config[DEFAULT_SCOPE]
|
53
|
+
if cfg
|
54
|
+
@samplers[scope] = SamplingConfiguration.build_sampler(scope, cfg)
|
55
|
+
else
|
56
|
+
logger.info { "Disabling scope #{scope} due to its not being configured" }
|
57
|
+
@samplers[scope] = DisabledScopeSampler
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_virtual_holders(sampling_config)
|
62
|
+
Hash[
|
63
|
+
sampling_config
|
64
|
+
.select { |scope, _cfg| scope.end_with?('@*') }
|
65
|
+
.map do |scope, cfg|
|
66
|
+
parent = scope[0...-2] # remove trailing @*
|
67
|
+
[parent, VirtualScopesHolder.new(parent, cfg)]
|
68
|
+
end
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
class << self
|
73
|
+
include Loggable
|
74
|
+
|
75
|
+
def build_sampler(scope, cfg)
|
76
|
+
do_build_sampler(cfg['enabled'], cfg['sampling'] || [{}])
|
77
|
+
rescue StandardError => e
|
78
|
+
logger.warn "Invalid sampling configuration for #{scope}: #{e.inspect}"
|
79
|
+
logger.debug { e.backtrace }
|
80
|
+
DisabledScopeSampler
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def do_build_sampler(enabled, sampling)
|
86
|
+
if enabled
|
87
|
+
Sampler.new(sampling)
|
88
|
+
else
|
89
|
+
logger.debug { "Disabling scope #{scope}" }
|
90
|
+
DisabledScopeSampler
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class VirtualScopesHolder
|
97
|
+
include Loggable
|
98
|
+
|
99
|
+
MAX_VIRTUAL_SCOPES = 120
|
100
|
+
DISCARD_SIZE = 20
|
101
|
+
|
102
|
+
def initialize(parent, cfg)
|
103
|
+
@parent = parent
|
104
|
+
@cfg = cfg
|
105
|
+
@virtual_scopes = {}
|
106
|
+
@mutex = Mutex.new
|
107
|
+
end
|
108
|
+
|
109
|
+
def [](qualifier)
|
110
|
+
return false unless @cfg['enabled']
|
111
|
+
|
112
|
+
@mutex.synchronize do
|
113
|
+
sampler = @virtual_scopes[qualifier]
|
114
|
+
return sampler if sampler
|
115
|
+
|
116
|
+
@virtual_scopes[qualifier] = create_virtual(qualifier)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def delete!(qualifier)
|
121
|
+
@virtual_scopes.delete(qualifier)
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def create_virtual(qualifier)
|
127
|
+
discard if @virtual_scopes.size >= MAX_VIRTUAL_SCOPES
|
128
|
+
|
129
|
+
@virtual_scopes[qualifier] =
|
130
|
+
SamplingConfiguration.build_sampler("#{@parent}@#{qualifier}", @cfg)
|
131
|
+
end
|
132
|
+
|
133
|
+
def discard
|
134
|
+
logger.info { "Discarding excess virtual scopes for scope '#{@parent}'" }
|
135
|
+
discard_keys = @virtual_scopes.keys.sample(DISCARD_SIZE)
|
136
|
+
@virtual_scopes.delete_if { |k, _v| discard_keys.include? k }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# fake sampler that always returns false
|
141
|
+
class DisabledScopeSampler
|
142
|
+
class << self
|
143
|
+
def should_sample?
|
144
|
+
false
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|