sqreen-alt 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/README.md +77 -0
- data/Rakefile +20 -0
- data/lib/sqreen-alt.rb +1 -0
- data/lib/sqreen.rb +68 -0
- data/lib/sqreen/attack_detected.html +2 -0
- data/lib/sqreen/binding_accessor.rb +288 -0
- data/lib/sqreen/ca.crt +72 -0
- data/lib/sqreen/call_countable.rb +67 -0
- data/lib/sqreen/callback_tree.rb +78 -0
- data/lib/sqreen/callbacks.rb +100 -0
- data/lib/sqreen/capped_queue.rb +23 -0
- data/lib/sqreen/condition_evaluator.rb +235 -0
- data/lib/sqreen/conditionable.rb +50 -0
- data/lib/sqreen/configuration.rb +168 -0
- data/lib/sqreen/context.rb +26 -0
- data/lib/sqreen/deliveries/batch.rb +84 -0
- data/lib/sqreen/deliveries/simple.rb +39 -0
- data/lib/sqreen/event.rb +16 -0
- data/lib/sqreen/events/attack.rb +61 -0
- data/lib/sqreen/events/remote_exception.rb +54 -0
- data/lib/sqreen/events/request_record.rb +62 -0
- data/lib/sqreen/exception.rb +34 -0
- data/lib/sqreen/frameworks.rb +40 -0
- data/lib/sqreen/frameworks/generic.rb +446 -0
- data/lib/sqreen/frameworks/rails.rb +148 -0
- data/lib/sqreen/frameworks/rails3.rb +36 -0
- data/lib/sqreen/frameworks/request_recorder.rb +69 -0
- data/lib/sqreen/frameworks/sinatra.rb +57 -0
- data/lib/sqreen/frameworks/sqreen_test.rb +26 -0
- data/lib/sqreen/instrumentation.rb +542 -0
- data/lib/sqreen/log.rb +119 -0
- data/lib/sqreen/metrics.rb +6 -0
- data/lib/sqreen/metrics/average.rb +39 -0
- data/lib/sqreen/metrics/base.rb +45 -0
- data/lib/sqreen/metrics/collect.rb +22 -0
- data/lib/sqreen/metrics/sum.rb +20 -0
- data/lib/sqreen/metrics_store.rb +96 -0
- data/lib/sqreen/middleware.rb +34 -0
- data/lib/sqreen/payload_creator.rb +137 -0
- data/lib/sqreen/performance_notifications.rb +86 -0
- data/lib/sqreen/performance_notifications/log.rb +36 -0
- data/lib/sqreen/performance_notifications/metrics.rb +36 -0
- data/lib/sqreen/performance_notifications/newrelic.rb +36 -0
- data/lib/sqreen/remote_command.rb +93 -0
- data/lib/sqreen/rule_attributes.rb +26 -0
- data/lib/sqreen/rule_callback.rb +108 -0
- data/lib/sqreen/rules.rb +126 -0
- data/lib/sqreen/rules_callbacks.rb +29 -0
- data/lib/sqreen/rules_callbacks/binding_accessor_matcher.rb +77 -0
- data/lib/sqreen/rules_callbacks/binding_accessor_metrics.rb +79 -0
- data/lib/sqreen/rules_callbacks/blacklist_ips.rb +44 -0
- data/lib/sqreen/rules_callbacks/count_http_codes.rb +40 -0
- data/lib/sqreen/rules_callbacks/crawler_user_agent_matches.rb +24 -0
- data/lib/sqreen/rules_callbacks/crawler_user_agent_matches_metrics.rb +24 -0
- data/lib/sqreen/rules_callbacks/custom_error.rb +64 -0
- data/lib/sqreen/rules_callbacks/execjs.rb +241 -0
- data/lib/sqreen/rules_callbacks/headers_insert.rb +22 -0
- data/lib/sqreen/rules_callbacks/inspect_rule.rb +25 -0
- data/lib/sqreen/rules_callbacks/matcher_rule.rb +138 -0
- data/lib/sqreen/rules_callbacks/rails_parameters.rb +14 -0
- data/lib/sqreen/rules_callbacks/record_request_context.rb +39 -0
- data/lib/sqreen/rules_callbacks/reflected_xss.rb +254 -0
- data/lib/sqreen/rules_callbacks/regexp_rule.rb +36 -0
- data/lib/sqreen/rules_callbacks/shell_env.rb +32 -0
- data/lib/sqreen/rules_callbacks/url_matches.rb +25 -0
- data/lib/sqreen/rules_callbacks/user_agent_matches.rb +22 -0
- data/lib/sqreen/rules_signature.rb +151 -0
- data/lib/sqreen/runner.rb +365 -0
- data/lib/sqreen/runtime_infos.rb +138 -0
- data/lib/sqreen/safe_json.rb +60 -0
- data/lib/sqreen/sdk.rb +22 -0
- data/lib/sqreen/serializer.rb +46 -0
- data/lib/sqreen/session.rb +317 -0
- data/lib/sqreen/shared_storage.rb +31 -0
- data/lib/sqreen/stats.rb +18 -0
- data/lib/sqreen/version.rb +5 -0
- metadata +148 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright (c) 2015 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
|
+
|
6
|
+
module Sqreen
|
7
|
+
module Rules
|
8
|
+
# Generic regexp based matching
|
9
|
+
class RegexpRuleCB < RuleCB
|
10
|
+
def initialize(*args)
|
11
|
+
super(*args)
|
12
|
+
prepare
|
13
|
+
end
|
14
|
+
|
15
|
+
def prepare
|
16
|
+
@patterns = []
|
17
|
+
raw_patterns = @data['values']
|
18
|
+
if raw_patterns.nil?
|
19
|
+
msg = "no key 'values' in data (had #{@data.keys})"
|
20
|
+
raise Sqreen::Exception, msg
|
21
|
+
end
|
22
|
+
|
23
|
+
@patterns = raw_patterns.map do |pattern|
|
24
|
+
Regexp.compile(pattern, Regexp::IGNORECASE)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def match_regexp(str)
|
29
|
+
@patterns.each do |pattern|
|
30
|
+
return pattern if pattern.match(str)
|
31
|
+
end
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'sqreen/rules_callbacks/regexp_rule'
|
5
|
+
|
6
|
+
module Sqreen
|
7
|
+
module Rules
|
8
|
+
# Callback that detect nifty env in system calls
|
9
|
+
class ShellEnvCB < RegexpRuleCB
|
10
|
+
def pre(_inst, *args, &_block)
|
11
|
+
return if args.size == 0
|
12
|
+
env = args.first
|
13
|
+
return unless env.is_a?(Hash)
|
14
|
+
return if env.size == 0
|
15
|
+
found = nil
|
16
|
+
var, value = env.find do |_, val|
|
17
|
+
next unless val.is_a?(String)
|
18
|
+
found = match_regexp(val)
|
19
|
+
end
|
20
|
+
return unless var
|
21
|
+
infos = {
|
22
|
+
:variable_name => var,
|
23
|
+
:variable_value => value,
|
24
|
+
:found => found,
|
25
|
+
}
|
26
|
+
Sqreen.log.warn "presence of a shell env tampering: #{infos.inspect}"
|
27
|
+
record_event(infos)
|
28
|
+
advise_action(:raise)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'sqreen/rules_callbacks/regexp_rule'
|
5
|
+
|
6
|
+
module Sqreen
|
7
|
+
module Rules
|
8
|
+
# FIXME: Tune this as Rack capable callback?
|
9
|
+
# If:
|
10
|
+
# - we have a 404
|
11
|
+
# - the path is a typical bot scanning request
|
12
|
+
# Then we deny the ressource and record the attack.
|
13
|
+
class URLMatchesCB < RegexpRuleCB
|
14
|
+
def post(rv, _inst, *args, &_block)
|
15
|
+
return unless rv.is_a?(Array) && rv.size > 0 && rv[0] == 404
|
16
|
+
env = args[0]
|
17
|
+
path = env['SCRIPT_NAME'].to_s + env['PATH_INFO'].to_s
|
18
|
+
found = match_regexp(path)
|
19
|
+
infos = { :path => path, :found => found }
|
20
|
+
record_event(infos) if found
|
21
|
+
advise_action(nil)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'sqreen/rules_callbacks/regexp_rule'
|
5
|
+
|
6
|
+
module Sqreen
|
7
|
+
module Rules
|
8
|
+
# Look for badly behaved clients
|
9
|
+
class UserAgentMatchesCB < RegexpRuleCB
|
10
|
+
def pre(_inst, *_args, &_block)
|
11
|
+
ua = framework.client_user_agent
|
12
|
+
return unless ua
|
13
|
+
found = match_regexp(ua)
|
14
|
+
return unless found
|
15
|
+
Sqreen.log.debug { "Found UA #{ua} - found: #{found}" }
|
16
|
+
infos = { :found => found }
|
17
|
+
record_event(infos)
|
18
|
+
advise_action(:raise, :data => found)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# Copyright (c) 2015 Sqreen. All Rights Reserved.
|
2
|
+
# Please refer to our terms for more information: https://www.sqreen.io/terms.html
|
3
|
+
|
4
|
+
require 'sqreen/exception'
|
5
|
+
|
6
|
+
require 'set'
|
7
|
+
require 'openssl'
|
8
|
+
require 'base64'
|
9
|
+
require 'json'
|
10
|
+
|
11
|
+
## Rules signature
|
12
|
+
module Sqreen
|
13
|
+
# Perform an EC + digest verification of a message.
|
14
|
+
class SignatureVerifier
|
15
|
+
def initialize(key, digest)
|
16
|
+
@pub_key = OpenSSL::PKey.read(key)
|
17
|
+
@digest = digest
|
18
|
+
end
|
19
|
+
|
20
|
+
def verify(sig, val)
|
21
|
+
hashed_val = @digest.digest(val)
|
22
|
+
@pub_key.dsa_verify_asn1(hashed_val, sig)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Normalize and verify a rule
|
27
|
+
class SqreenSignedVerifier
|
28
|
+
REQUIRED_SIGNED_KEYS = %w(hookpoint name callbacks conditions).freeze
|
29
|
+
SIGNATURE_KEY = 'signature'.freeze
|
30
|
+
SIGNATURE_VALUE_KEY = 'value'.freeze
|
31
|
+
SIGNED_KEYS_KEY = 'keys'.freeze
|
32
|
+
SIGNATURE_VERSION = 'v0_9'.freeze
|
33
|
+
PUBLIC_KEY = <<-END.gsub(/^ */, '').freeze
|
34
|
+
-----BEGIN PUBLIC KEY-----
|
35
|
+
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA39oWMHR8sxb9LRaM5evZ7mw03iwJ
|
36
|
+
WNHuDeGqgPo1HmvuMfLnAyVLwaMXpGPuvbqhC1U65PG90bTJLpvNokQf0VMA5Tpi
|
37
|
+
m+NXwl7bjqa03vO/HErLbq3zBRysrZnC4OhJOF1jazkAg0psQOea2r5HcMcPHgMK
|
38
|
+
fnWXiKWnZX+uOWPuerE=
|
39
|
+
-----END PUBLIC KEY-----
|
40
|
+
END
|
41
|
+
|
42
|
+
attr_accessor :pub_key
|
43
|
+
attr_accessor :required_signed_keys
|
44
|
+
attr_accessor :digest
|
45
|
+
|
46
|
+
def initialize(required_keys = REQUIRED_SIGNED_KEYS,
|
47
|
+
public_key = PUBLIC_KEY,
|
48
|
+
digest = OpenSSL::Digest::SHA512.new)
|
49
|
+
@required_signed_keys = required_keys
|
50
|
+
@signature_verifier = SignatureVerifier.new(public_key, digest)
|
51
|
+
end
|
52
|
+
|
53
|
+
def normalize_val(val, level)
|
54
|
+
raise Sqreen::Exception, 'recursion level too deep' if level == 0
|
55
|
+
|
56
|
+
case val
|
57
|
+
when Hash
|
58
|
+
normalize(val, nil, level - 1)
|
59
|
+
when Array
|
60
|
+
ary = val.map do |i|
|
61
|
+
normalize_val(i, level - 1)
|
62
|
+
end
|
63
|
+
"[#{ary.join(',')}]"
|
64
|
+
when String, Integer
|
65
|
+
begin
|
66
|
+
JSON.dump(val)
|
67
|
+
rescue JSON::GeneratorError
|
68
|
+
JSON.generate(val, :quirks_mode => true)
|
69
|
+
end
|
70
|
+
else
|
71
|
+
msg = "JSON hash parsing error (wrong value type: #{val.class})"
|
72
|
+
raise Sqreen::Exception.new, msg
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def normalize_key(key)
|
77
|
+
case key
|
78
|
+
when String, Integer
|
79
|
+
begin
|
80
|
+
JSON.dump(key)
|
81
|
+
rescue JSON::GeneratorError
|
82
|
+
JSON.generate(key, :quirks_mode => true)
|
83
|
+
end
|
84
|
+
else
|
85
|
+
msg = "JSON hash parsing error (wrong key type: #{key.class})"
|
86
|
+
raise Sqreen::Exception, msg
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def normalize(hash_rule, signed_keys = nil, level = 20)
|
91
|
+
# Normalize the provided hash to a string:
|
92
|
+
# - sort keys lexicographically, recursively
|
93
|
+
# - convert each scalar to its JSON representation
|
94
|
+
# - convert hash to '{key:value}'
|
95
|
+
# - convert array [v1,v2] to '[v1,v2]' and [] to '[]'
|
96
|
+
# Two hash with different key ordering should have the same normalized
|
97
|
+
# value.
|
98
|
+
|
99
|
+
raise Sqreen::Exception, 'recursion level too deep' if level == 0
|
100
|
+
unless hash_rule.is_a?(Hash)
|
101
|
+
raise Sqreen::Exception, "wrong hash type #{hash_rule.class}"
|
102
|
+
end
|
103
|
+
|
104
|
+
res = []
|
105
|
+
hash_rule.sort.each do |k, v|
|
106
|
+
# Only keep signed keys
|
107
|
+
next if signed_keys && !signed_keys.include?(k)
|
108
|
+
|
109
|
+
k = normalize_key(k)
|
110
|
+
v = normalize_val(v, level - 1)
|
111
|
+
|
112
|
+
res << "#{k}:#{v}"
|
113
|
+
end
|
114
|
+
"{#{res.join(',')}}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_sig_infos_or_fail(hash_rule)
|
118
|
+
raise Sqreen::Exception, 'non hash argument' unless hash_rule.is_a?(Hash)
|
119
|
+
|
120
|
+
sigs = hash_rule[SIGNATURE_KEY]
|
121
|
+
raise Sqreen::Exception, 'no signature found' unless sigs
|
122
|
+
|
123
|
+
sig = sigs[SIGNATURE_VERSION]
|
124
|
+
msg = "signature #{SIGNATURE_VERSION} not found (#{sigs})"
|
125
|
+
raise Sqreen::Exception, msg unless sig
|
126
|
+
|
127
|
+
sig_value = sig[SIGNATURE_VALUE_KEY]
|
128
|
+
raise Sqreen::Exception, 'no signature value found' unless sig_value
|
129
|
+
|
130
|
+
signed_keys = sig[SIGNED_KEYS_KEY]
|
131
|
+
raise Sqreen::Exception, "no signed keys found (#{sig})" unless signed_keys
|
132
|
+
|
133
|
+
inc = Set.new(signed_keys).superset?(Set.new(@required_signed_keys))
|
134
|
+
raise Sqreen::Exception, 'signed keys miss equired keys' unless inc
|
135
|
+
|
136
|
+
[signed_keys, sig_value]
|
137
|
+
end
|
138
|
+
|
139
|
+
def verify(hash_rule)
|
140
|
+
# Return true if rule signature is correct, else false
|
141
|
+
|
142
|
+
signed_keys, sig_value = get_sig_infos_or_fail(hash_rule)
|
143
|
+
|
144
|
+
norm_str = normalize(hash_rule, signed_keys)
|
145
|
+
bin_sig = Base64.decode64(sig_value)
|
146
|
+
@signature_verifier.verify(bin_sig, norm_str)
|
147
|
+
rescue OpenSSL::PKey::ECError
|
148
|
+
false
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,365 @@
|
|
1
|
+
# Copyright (c) 2015 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 'timeout'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
require 'sqreen/events/attack'
|
9
|
+
|
10
|
+
require 'sqreen/log'
|
11
|
+
|
12
|
+
require 'sqreen/rules'
|
13
|
+
require 'sqreen/session'
|
14
|
+
require 'sqreen/remote_command'
|
15
|
+
require 'sqreen/capped_queue'
|
16
|
+
require 'sqreen/metrics_store'
|
17
|
+
require 'sqreen/deliveries/simple'
|
18
|
+
require 'sqreen/deliveries/batch'
|
19
|
+
require 'sqreen/performance_notifications/metrics'
|
20
|
+
require 'sqreen/instrumentation'
|
21
|
+
require 'sqreen/call_countable'
|
22
|
+
|
23
|
+
module Sqreen
|
24
|
+
@features = {}
|
25
|
+
@queue = nil
|
26
|
+
|
27
|
+
# Event Queue that enable communication between threads and the reporter
|
28
|
+
MAX_QUEUE_LENGTH = 100
|
29
|
+
MAX_OBS_QUEUE_LENGTH = 1000
|
30
|
+
|
31
|
+
METRICS_EVENT = 'metrics'.freeze
|
32
|
+
|
33
|
+
class << self
|
34
|
+
attr_reader :features
|
35
|
+
def update_features(features)
|
36
|
+
@features = features
|
37
|
+
end
|
38
|
+
|
39
|
+
def queue
|
40
|
+
@queue ||= CappedQueue.new(MAX_QUEUE_LENGTH)
|
41
|
+
end
|
42
|
+
|
43
|
+
def observations_queue
|
44
|
+
@observations_queue ||= CappedQueue.new(MAX_OBS_QUEUE_LENGTH)
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_accessor :instrumentation_ready
|
48
|
+
alias instrumentation_ready? instrumentation_ready
|
49
|
+
|
50
|
+
attr_accessor :logged_in
|
51
|
+
alias logged_in? logged_in
|
52
|
+
|
53
|
+
attr_reader :whitelisted_paths
|
54
|
+
def update_whitelisted_paths(paths)
|
55
|
+
@whitelisted_paths = paths.freeze
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :whitelisted_ips
|
59
|
+
def update_whitelisted_ips(paths)
|
60
|
+
@whitelisted_ips = Hash[paths.map { |v| [v, IPAddr.new(v)] }].freeze
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Main running job class for the agent
|
65
|
+
class Runner
|
66
|
+
# During one hour
|
67
|
+
HEARTBEAT_WARMUP = 60 * 60
|
68
|
+
# Initail delay is 5 minutes
|
69
|
+
HEARTBEAT_MAX_DELAY = 5 * 60
|
70
|
+
|
71
|
+
attr_accessor :heartbeat_delay
|
72
|
+
attr_accessor :metrics_engine
|
73
|
+
attr_reader :deliverer
|
74
|
+
attr_reader :session
|
75
|
+
attr_reader :instrumenter
|
76
|
+
attr_accessor :running
|
77
|
+
attr_accessor :next_command_results
|
78
|
+
attr_accessor :next_metrics
|
79
|
+
|
80
|
+
# we may want to do that in a thread in order to prevent delaying app
|
81
|
+
# startup
|
82
|
+
# set_at_exit do not place a global at_exit (used for testing)
|
83
|
+
def initialize(configuration, framework, set_at_exit = true, session_class = Sqreen::Session)
|
84
|
+
@logged_out_tried = false
|
85
|
+
@configuration = configuration
|
86
|
+
@framework = framework
|
87
|
+
@heartbeat_delay = HEARTBEAT_MAX_DELAY
|
88
|
+
@last_heartbeat_request = Time.now
|
89
|
+
@next_command_results = {}
|
90
|
+
@next_metrics = []
|
91
|
+
@running = true
|
92
|
+
|
93
|
+
@token = @configuration.get(:token)
|
94
|
+
@url = @configuration.get(:url)
|
95
|
+
Sqreen.update_whitelisted_paths([])
|
96
|
+
Sqreen.update_whitelisted_ips({})
|
97
|
+
raise(Sqreen::Exception, 'no url found') unless @url
|
98
|
+
raise(Sqreen::TokenNotFoundException, 'no token found') unless @token
|
99
|
+
|
100
|
+
register_exit_cb if set_at_exit
|
101
|
+
|
102
|
+
self.metrics_engine = MetricsStore.new
|
103
|
+
@instrumenter = Instrumentation.new(metrics_engine)
|
104
|
+
|
105
|
+
Sqreen.log.warn "using token #{@token}"
|
106
|
+
response = create_session(session_class)
|
107
|
+
wanted_features = response.fetch('features', {})
|
108
|
+
conf_initial_features = configuration.get(:initial_features)
|
109
|
+
unless conf_initial_features.nil?
|
110
|
+
begin
|
111
|
+
conf_features = JSON.parse(conf_initial_features)
|
112
|
+
raise 'Invalid Type' unless conf_features.is_a?(Hash)
|
113
|
+
Sqreen.log.debug do
|
114
|
+
"Override initial features with #{conf_features.inspect}"
|
115
|
+
end
|
116
|
+
wanted_features = conf_features
|
117
|
+
rescue
|
118
|
+
Sqreen.log.warn do
|
119
|
+
"NOT using invalid inital features #{conf_initial_features}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
self.features = wanted_features
|
124
|
+
|
125
|
+
# Ensure a deliverer is there unless features have set it first
|
126
|
+
self.deliverer ||= Deliveries::Simple.new(session)
|
127
|
+
context_infos = {}
|
128
|
+
%w[rules pack_id].each do |p|
|
129
|
+
context_infos[p] = response[p] unless response[p].nil?
|
130
|
+
end
|
131
|
+
process_commands(response.fetch('commands', []), context_infos)
|
132
|
+
end
|
133
|
+
|
134
|
+
def create_session(session_class)
|
135
|
+
@session = session_class.new(@url, @token)
|
136
|
+
session.login(@framework)
|
137
|
+
end
|
138
|
+
|
139
|
+
def deliverer=(new_deliverer)
|
140
|
+
deliverer.drain if deliverer
|
141
|
+
@deliverer = new_deliverer
|
142
|
+
end
|
143
|
+
|
144
|
+
def batch_events(batch_size, max_staleness = nil)
|
145
|
+
size = batch_size.to_i
|
146
|
+
self.deliverer = if size < 1
|
147
|
+
Deliveries::Simple.new(session)
|
148
|
+
else
|
149
|
+
staleness = max_staleness.to_i
|
150
|
+
Deliveries::Batch.new(session, size, staleness)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def load_rules(context_infos = {})
|
155
|
+
rules_pack = context_infos['rules']
|
156
|
+
rulespack_id = context_infos['pack_id']
|
157
|
+
if rules_pack.nil? || rulespack_id.nil?
|
158
|
+
session_rules = session.rules
|
159
|
+
rules_pack = session_rules['rules']
|
160
|
+
rulespack_id = session_rules['pack_id']
|
161
|
+
end
|
162
|
+
rules = rules_pack.each { |r| r['rulespack_id'] = rulespack_id }
|
163
|
+
Sqreen.log.info { format('retrieved rulespack id: %s', rulespack_id) }
|
164
|
+
Sqreen.log.debug { format('retrieved %d rules', rules.size) }
|
165
|
+
local_rules = Sqreen::Rules.local(@configuration) || []
|
166
|
+
rules += local_rules.
|
167
|
+
select { |rule| rule['enabled'] }.
|
168
|
+
each { |r| r['rulespack_id'] = 'local' }
|
169
|
+
Sqreen.log.debug do
|
170
|
+
format('rules: %s', rules.
|
171
|
+
sort_by { |r| r['name'] }.
|
172
|
+
map { |r| format('(%s, %s)', r['name'], r.to_json.size) }.
|
173
|
+
join(', '))
|
174
|
+
end
|
175
|
+
[rulespack_id, rules]
|
176
|
+
end
|
177
|
+
|
178
|
+
def call_counts_metrics_period=(value)
|
179
|
+
value = value.to_i
|
180
|
+
return unless value > 0 # else disable collection?
|
181
|
+
metrics_engine.create_metric('name' => CallCountable::COUNT_CALLS,
|
182
|
+
'period' => value,
|
183
|
+
'kind' => 'Sum')
|
184
|
+
end
|
185
|
+
|
186
|
+
def performance_metrics_period=(value)
|
187
|
+
value = value.to_i
|
188
|
+
if value > 0
|
189
|
+
PerformanceNotifications::Metrics.enable(metrics_engine, value)
|
190
|
+
else
|
191
|
+
PerformanceNotifications::Metrics.disable
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def setup_instrumentation(context_infos = {})
|
196
|
+
Sqreen.log.info 'setup instrumentation'
|
197
|
+
rulespack_id, rules = load_rules(context_infos)
|
198
|
+
@framework.instrument_when_ready!(instrumenter, rules)
|
199
|
+
rulespack_id.to_s
|
200
|
+
end
|
201
|
+
|
202
|
+
def remove_instrumentation(_context_infos = {})
|
203
|
+
Sqreen.log.debug 'removing instrumentation'
|
204
|
+
instrumenter.remove_all_callbacks
|
205
|
+
true
|
206
|
+
end
|
207
|
+
|
208
|
+
def reload_rules(_context_infos = {})
|
209
|
+
Sqreen.log.debug 'Reloading rules'
|
210
|
+
rulespack_id, rules = load_rules
|
211
|
+
instrumenter.remove_all_callbacks
|
212
|
+
|
213
|
+
@framework.instrument_when_ready!(instrumenter, rules)
|
214
|
+
Sqreen.log.debug 'Rules reloaded'
|
215
|
+
rulespack_id.to_s
|
216
|
+
end
|
217
|
+
|
218
|
+
def process_commands(commands, context_infos = {})
|
219
|
+
return if commands.nil? || commands.empty?
|
220
|
+
res = RemoteCommand.process_list(self, commands, context_infos)
|
221
|
+
@next_command_results = res
|
222
|
+
end
|
223
|
+
|
224
|
+
def do_heartbeat
|
225
|
+
@last_heartbeat_request = Time.now
|
226
|
+
@next_metrics.concat(metrics_engine.publish(false)) if metrics_engine
|
227
|
+
res = session.heartbeat(next_command_results, next_metrics)
|
228
|
+
next_command_results.clear
|
229
|
+
next_metrics.clear
|
230
|
+
process_commands(res['commands'])
|
231
|
+
end
|
232
|
+
|
233
|
+
def features(_context_infos = {})
|
234
|
+
Sqreen.features
|
235
|
+
end
|
236
|
+
|
237
|
+
def features=(features)
|
238
|
+
Sqreen.update_features(features)
|
239
|
+
session.request_compression = features['request_compression'] if session
|
240
|
+
self.performance_metrics_period = features['performance_metrics_period']
|
241
|
+
self.call_counts_metrics_period = features['call_counts_metrics_period']
|
242
|
+
hd = features['heartbeat_delay'].to_i
|
243
|
+
self.heartbeat_delay = hd if hd > 0
|
244
|
+
return if features['batch_size'].nil?
|
245
|
+
batch_events(features['batch_size'], features['max_staleness'])
|
246
|
+
end
|
247
|
+
|
248
|
+
def change_whitelisted_paths(paths, _context_infos = {})
|
249
|
+
return false unless paths.respond_to?(:each)
|
250
|
+
Sqreen.update_whitelisted_paths(paths)
|
251
|
+
true
|
252
|
+
end
|
253
|
+
|
254
|
+
def upload_bundle(_context_infos = {})
|
255
|
+
t = Time.now
|
256
|
+
session.post_bundle(RuntimeInfos.dependencies_signature, RuntimeInfos.dependencies)
|
257
|
+
Time.now - t
|
258
|
+
end
|
259
|
+
|
260
|
+
def change_whitelisted_ips(ips, _context_infos = {})
|
261
|
+
return false unless ips.respond_to?(:each)
|
262
|
+
Sqreen.update_whitelisted_ips(ips)
|
263
|
+
true
|
264
|
+
end
|
265
|
+
|
266
|
+
def change_features(new_features, _context_infos = {})
|
267
|
+
old = features
|
268
|
+
self.features = new_features
|
269
|
+
{
|
270
|
+
'was' => old,
|
271
|
+
'now' => new_features,
|
272
|
+
}
|
273
|
+
end
|
274
|
+
|
275
|
+
def aggregate_observations
|
276
|
+
q = Sqreen.observations_queue
|
277
|
+
q.size.times do
|
278
|
+
cat, key, obs, t = q.pop
|
279
|
+
metrics_engine.update(cat, t, key, obs)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def heartbeat_needed?
|
284
|
+
(@last_heartbeat_request + heartbeat_delay) < Time.now
|
285
|
+
end
|
286
|
+
|
287
|
+
def run_watcher_once
|
288
|
+
event = Timeout.timeout(heartbeat_delay) do
|
289
|
+
Sqreen.queue.pop
|
290
|
+
end
|
291
|
+
rescue Timeout::Error
|
292
|
+
periodic_cleanup
|
293
|
+
else
|
294
|
+
handle_event(event)
|
295
|
+
if heartbeat_needed?
|
296
|
+
# Also aggregate/post metrics when cleanup has
|
297
|
+
# not been done for a long time
|
298
|
+
Sqreen.log.debug 'Forced an heartbeat'
|
299
|
+
periodic_cleanup # will trigger do_heartbeat since it's time
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def periodic_cleanup
|
304
|
+
# Nothing occured:
|
305
|
+
# tick delivery, aggregates_metrics
|
306
|
+
# issue a simple heartbeat if it's time (which may return commands)
|
307
|
+
@deliverer.tick
|
308
|
+
aggregate_observations
|
309
|
+
do_heartbeat if heartbeat_needed?
|
310
|
+
end
|
311
|
+
|
312
|
+
def handle_event(event)
|
313
|
+
if event == METRICS_EVENT
|
314
|
+
aggregate_observations
|
315
|
+
else
|
316
|
+
@deliverer.post_event(event)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def run_watcher
|
321
|
+
run_watcher_once while running
|
322
|
+
end
|
323
|
+
|
324
|
+
# Sinatra is using at_exit to run the application, see:
|
325
|
+
# https://github.com/sinatra/sinatra/blob/cd503e6c590cd48c2c9bb7869522494bfc62cb14/lib/sinatra/main.rb#L25
|
326
|
+
def exit_from_sinatra_startup?
|
327
|
+
defined?(Sinatra::Application) &&
|
328
|
+
Sinatra::Application.respond_to?(:run?) &&
|
329
|
+
!Sinatra::Application.run?
|
330
|
+
end
|
331
|
+
|
332
|
+
def shutdown(_context_infos = {})
|
333
|
+
remove_instrumentation
|
334
|
+
logout
|
335
|
+
end
|
336
|
+
|
337
|
+
def logout(retrying = true)
|
338
|
+
return unless session
|
339
|
+
if @framework.development?
|
340
|
+
@running = false
|
341
|
+
return
|
342
|
+
end
|
343
|
+
if @logged_out_tried
|
344
|
+
Sqreen.log.debug('Not running logout twice')
|
345
|
+
return
|
346
|
+
end
|
347
|
+
@logged_out_tried = true
|
348
|
+
@deliverer.drain if @deliverer
|
349
|
+
aggregate_observations
|
350
|
+
session.post_metrics(metrics_engine.publish) if metrics_engine
|
351
|
+
session.logout(retrying)
|
352
|
+
@running = false
|
353
|
+
end
|
354
|
+
|
355
|
+
def register_exit_cb(try_again = true)
|
356
|
+
at_exit do
|
357
|
+
if exit_from_sinatra_startup? && try_again
|
358
|
+
register_exit_cb(false)
|
359
|
+
else
|
360
|
+
logout
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|