sqreen 1.19.0.beta1 → 1.20.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -2
  3. data/lib/sqreen/aggregated_metric.rb +25 -0
  4. data/lib/sqreen/configuration.rb +7 -3
  5. data/lib/sqreen/deliveries/batch.rb +4 -1
  6. data/lib/sqreen/deliveries/simple.rb +4 -0
  7. data/lib/sqreen/event.rb +7 -5
  8. data/lib/sqreen/events/attack.rb +23 -18
  9. data/lib/sqreen/events/remote_exception.rb +0 -22
  10. data/lib/sqreen/events/request_record.rb +15 -70
  11. data/lib/sqreen/frameworks/request_recorder.rb +13 -2
  12. data/lib/sqreen/kit/signals/specialized/aggregated_metric.rb +72 -0
  13. data/lib/sqreen/kit/signals/specialized/attack.rb +57 -0
  14. data/lib/sqreen/kit/signals/specialized/binning_metric.rb +76 -0
  15. data/lib/sqreen/kit/signals/specialized/http_trace.rb +26 -0
  16. data/lib/sqreen/kit/signals/specialized/sdk_track_call.rb +50 -0
  17. data/lib/sqreen/kit/signals/specialized/sqreen_exception.rb +57 -0
  18. data/lib/sqreen/legacy/old_event_submission_strategy.rb +221 -0
  19. data/lib/sqreen/legacy/waf_redactions.rb +49 -0
  20. data/lib/sqreen/metrics/base.rb +3 -0
  21. data/lib/sqreen/metrics_store.rb +22 -12
  22. data/lib/sqreen/performance_notifications/binned_metrics.rb +8 -2
  23. data/lib/sqreen/rules.rb +4 -2
  24. data/lib/sqreen/rules/not_found_cb.rb +2 -0
  25. data/lib/sqreen/rules/rule_cb.rb +2 -0
  26. data/lib/sqreen/rules/waf_cb.rb +39 -16
  27. data/lib/sqreen/runner.rb +48 -6
  28. data/lib/sqreen/sensitive_data_redactor.rb +19 -31
  29. data/lib/sqreen/session.rb +39 -37
  30. data/lib/sqreen/signals/conversions.rb +283 -0
  31. data/lib/sqreen/signals/http_trace_redaction.rb +111 -0
  32. data/lib/sqreen/signals/signals_submission_strategy.rb +78 -0
  33. data/lib/sqreen/version.rb +1 -1
  34. data/lib/sqreen/weave/legacy/instrumentation.rb +15 -7
  35. metadata +55 -14
  36. data/lib/sqreen/backport.rb +0 -9
  37. data/lib/sqreen/backport/clock_gettime.rb +0 -74
  38. data/lib/sqreen/backport/original_name.rb +0 -88
@@ -0,0 +1,49 @@
1
+ # typed: ignore
2
+
3
+ # Copyright (c) 2015 Sqreen. All Rights Reserved.
4
+ # Please refer to our terms for more information: https://www.sqreen.com/terms.html
5
+
6
+ module Sqreen
7
+ module Legacy
8
+ module WafRedactions
9
+ class << self
10
+ def redact_attacks!(attacks, values)
11
+ return attacks if values.empty?
12
+
13
+ values = values.map { |v| v.downcase if v.is_a?(String) }
14
+
15
+ attacks.each do |e|
16
+ next(e) unless e[:infos]
17
+ next(e) unless e[:infos][:waf_data]
18
+
19
+ parsed = JSON.parse(e[:infos][:waf_data])
20
+ redacted = parsed.each do |w|
21
+ next unless (filters = w['filter'])
22
+
23
+ filters.each do |f|
24
+ next unless (v = f['resolved_value'])
25
+ next unless values.include?(v.downcase)
26
+
27
+ f['match_status'] = SensitiveDataRedactor::MASK
28
+ f['resolved_value'] = SensitiveDataRedactor::MASK
29
+ end
30
+ end
31
+ e[:infos][:waf_data] = JSON.dump(redacted)
32
+ end
33
+ end
34
+
35
+ # see https://github.com/sqreen/TechDoc/blob/master/content/specs/spec000022-waf-data-sanitization.md#changes-to-the-agents
36
+ def redact_exceptions!(exceptions, values)
37
+ return exceptions if values.empty?
38
+
39
+ exceptions.each do |e|
40
+ next(e) unless e[:infos]
41
+ next(e) unless e[:infos][:waf]
42
+
43
+ e[:infos][:waf].delete(:args)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -12,6 +12,9 @@ module Sqreen
12
12
  FINISH_KEY = 'finish'.freeze
13
13
  # Base interface for a metric
14
14
  class Base
15
+ attr_accessor :name, :period # for signals serialization
16
+ attr_accessor :rule # optional
17
+
15
18
  def initialize(_opts={})
16
19
  @sample = nil
17
20
  end
@@ -3,6 +3,7 @@
3
3
  # Copyright (c) 2015 Sqreen. All Rights Reserved.
4
4
  # Please refer to our terms for more information: https://www.sqreen.com/terms.html
5
5
 
6
+ require 'sqreen/aggregated_metric'
6
7
  require 'sqreen/metrics'
7
8
  require 'sqreen/mono_time'
8
9
  require 'sqreen/metrics_store/unknown_metric'
@@ -30,8 +31,9 @@ module Sqreen
30
31
 
31
32
  # Definition contains a name,period and aggregate at least
32
33
  # @param definition [Hash] a metric definition
34
+ # @param rule [RuleCB] the rule associated with this metric, if any
33
35
  # @param mklass [Object] Override metric object (used in testing)
34
- def create_metric(definition, mklass = nil)
36
+ def create_metric(definition, rule = nil, mklass = nil)
35
37
  name = definition[NAME_KEY]
36
38
  kind = definition[KIND_KEY]
37
39
  klass = valid_metric(kind, name)
@@ -43,6 +45,9 @@ module Sqreen
43
45
  definition[PERIOD_KEY],
44
46
  nil # Start
45
47
  ]
48
+ metric.name = name
49
+ metric.rule = rule
50
+ metric.period = definition[PERIOD_KEY]
46
51
  metric
47
52
  end
48
53
 
@@ -50,7 +55,7 @@ module Sqreen
50
55
  @metrics.key?(name)
51
56
  end
52
57
 
53
- # @params at [Time] when is the store emptied
58
+ # @param at [Time] when is the store emptied
54
59
  def update(name, at, key, value)
55
60
  metric, period, start = @metrics[name]
56
61
  raise UnregisteredMetric, "Unknown metric #{name}" unless metric
@@ -59,7 +64,7 @@ module Sqreen
59
64
  end
60
65
 
61
66
  # Drains every metrics and returns the store content
62
- # @params at [Time] when is the store emptied
67
+ # @param at [Time] when is the store emptied
63
68
  def publish(flush = true, at = Sqreen.time)
64
69
  @metrics.each do |name, (_, period, start)|
65
70
  next_sample(name, at) if flush || !start.nil? && (start + period) < at
@@ -75,15 +80,20 @@ module Sqreen
75
80
  metric = @metrics[name][0]
76
81
  r = metric.next_sample(at)
77
82
  @metrics[name][2] = at # new start
78
- if r
79
- r[NAME_KEY] = name
80
- obs = r[Metric::OBSERVATION_KEY]
81
- start_of_mono = Time.now.utc - Sqreen.time
82
- r[Metric::START_KEY] = start_of_mono + r[Metric::START_KEY]
83
- r[Metric::FINISH_KEY] = start_of_mono + r[Metric::FINISH_KEY]
84
- @store << r if obs && (!obs.respond_to?(:empty?) || !obs.empty?)
85
- end
86
- r
83
+ return unless r
84
+
85
+ r[NAME_KEY] = name
86
+ obs = r[Metric::OBSERVATION_KEY]
87
+ return unless obs && (!obs.respond_to?(:empty?) || !obs.empty?)
88
+ start_of_mono = Time.now.utc - Sqreen.time
89
+
90
+ agg = AggregatedMetric.new
91
+ agg.metric = metric
92
+ agg.rule = agg.metric.rule
93
+ agg.start = start_of_mono + r[Metric::START_KEY]
94
+ agg.finish = start_of_mono + r[Metric::FINISH_KEY]
95
+ agg.data = obs
96
+ @store << agg
87
97
  end
88
98
 
89
99
  def valid_metric(kind, name)
@@ -122,10 +122,16 @@ module Sqreen
122
122
  attr_reader :metrics_store
123
123
  attr_reader :period
124
124
 
125
- def ensure_metric(metric_name)
125
+ def ensure_metric(metric_name, rule = nil)
126
126
  return if metrics_store.metric?(metric_name)
127
127
  metrics_store.create_metric(
128
- 'name' => metric_name, 'period' => period, 'kind' => 'Binning', 'options' => @perf_metric_opts
128
+ {
129
+ 'name' => metric_name,
130
+ 'period' => period,
131
+ 'kind' => 'Binning',
132
+ 'options' => @perf_metric_opts,
133
+ },
134
+ rule
129
135
  )
130
136
  end
131
137
 
@@ -135,13 +135,15 @@ module Sqreen
135
135
  return nil
136
136
  end
137
137
 
138
+ rule_cb = cb_class.new(instr_class, instr_method, hash_rule)
139
+
138
140
  if metrics_store
139
141
  (hash_rule[Attrs::METRICS] || []).each do |metric|
140
- metrics_store.create_metric(metric)
142
+ metrics_store.create_metric(metric, rule_cb)
141
143
  end
142
144
  end
143
145
 
144
- cb_class.new(instr_class, instr_method, hash_rule)
146
+ rule_cb
145
147
  rescue => e
146
148
  rule_name = nil
147
149
  rulespack_id = nil
@@ -24,6 +24,8 @@ module Sqreen
24
24
  exception = env['action_dispatch.exception']
25
25
 
26
26
  record_from_env(ua, script_name, path_info, verb, override, host, exception)
27
+
28
+ nil
27
29
  end
28
30
 
29
31
  def record_from_env(ua, script_name, path_info, verb, override, host, exception)
@@ -61,7 +61,9 @@ module Sqreen
61
61
  :infos => infos,
62
62
  :rulespack_id => rulespack_id,
63
63
  :rule_name => rule_name,
64
+ :attack_type => @rule['attack_type'], # for signal
64
65
  :test => test,
66
+ :block => @rule['block'], # for signal
65
67
  :time => at,
66
68
  }
67
69
  if payload_tpl.include?('context')
@@ -11,11 +11,15 @@ require 'sqreen/safe_json'
11
11
  require 'sqreen/exception'
12
12
  require 'sqreen/util/capper'
13
13
  require 'sqreen/dependency/libsqreen'
14
+ require 'sqreen/encoding_sanitizer'
14
15
 
15
16
  module Sqreen
16
17
  module Rules
17
18
  class WAFCB < RuleCB
18
- BUDGET_MAX = 5
19
+ # 2^30 -1 or 2^62 -1
20
+ MAX_FIXNUM = 1.size == 4 ? 1_073_741_823 : 4_611_686_018_427_387_903
21
+ # will be converted to a long, so better not to overflow
22
+ INFINITE_BUDGET_US = MAX_FIXNUM
19
23
 
20
24
  def self.libsqreen?
21
25
  Sqreen::Dependency::LibSqreen.required?
@@ -25,7 +29,7 @@ module Sqreen
25
29
  Sqreen::Dependency.const_exist?('LibSqreen::WAF')
26
30
  end
27
31
 
28
- attr_reader :binding_accessors, :budget, :waf_rule_name
32
+ attr_reader :binding_accessors, :max_run_budget_us, :waf_rule_name
29
33
 
30
34
  def initialize(*args)
31
35
  super(*args)
@@ -54,8 +58,12 @@ module Sqreen
54
58
  @binding_accessors = @data['values'].fetch('binding_accessors', []).each_with_object({}) do |e, h|
55
59
  h[e] = BindingAccessor.new(e)
56
60
  end
57
- @budget = (@data['values'].fetch('budget_in_ms', nil) || BUDGET_MAX) * 1000
58
- Sqreen.log.debug("WAF budget for #{@waf_rule_name} set to #{@budget}us")
61
+
62
+ # 0 for using defaults (PW_RUN_TIMEOUT)
63
+ @max_run_budget_us = (@data['values'].fetch('budget_in_ms', 0) * 1000).to_i
64
+ @max_run_budget_us = INFINITE_BUDGET_US if @max_run_budget_us >= INFINITE_BUDGET_US
65
+
66
+ Sqreen.log.debug { "Max WAF run budget for #{@waf_rule_name} set to #{@max_run_budget_us} us" }
59
67
 
60
68
  ObjectSpace.define_finalizer(self, WAFCB.finalizer(@waf_rule_name.dup))
61
69
  end
@@ -68,20 +76,32 @@ module Sqreen
68
76
 
69
77
  env = [binding, framework, instance, args]
70
78
 
79
+ start = Sqreen.time if budget
80
+
71
81
  capper = Sqreen::Util::Capper.new(string_size_cap: 4096, size_cap: 150, depth_cap: 10)
72
82
  waf_args = binding_accessors.each_with_object({}) do |(e, b), h|
73
83
  h[e] = capper.call(b.resolve(*env))
74
84
  end
75
85
  waf_args = Sqreen::EncodingSanitizer.sanitize(waf_args)
76
- waf_budget = [self.budget, budget && budget * 1_000_000].compact.min.to_i
77
- action, data = ::LibSqreen::WAF.run(waf_rule_name, waf_args, waf_budget)
86
+
87
+ if budget
88
+ rem_budget_s = budget - (Sqreen.time - start)
89
+ return advise_action(nil) if rem_budget_s <= 0.0
90
+
91
+ waf_gen_budget_us = [(rem_budget_s * 1_000_000).to_i, MAX_FIXNUM].min
92
+ else # no budget
93
+ waf_gen_budget_us = INFINITE_BUDGET_US
94
+ end
95
+
96
+ action, data = ::LibSqreen::WAF.run(waf_rule_name, waf_args,
97
+ waf_gen_budget_us, @max_run_budget_us)
78
98
 
79
99
  case action
80
100
  when :monitor
81
- record_event({ 'waf_data' => data })
101
+ record_event({ waf_data: data })
82
102
  advise_action(nil)
83
103
  when :block
84
- record_event({ 'waf_data' => data })
104
+ record_event({ waf_data: data })
85
105
  advise_action(:raise)
86
106
  when :good
87
107
  advise_action(nil)
@@ -112,20 +132,23 @@ module Sqreen
112
132
  end
113
133
 
114
134
  def record_exception(exception, infos = {}, at = Time.now.utc)
115
- infos.merge!(exception_to_infos(exception)) if exception.is_a?(Sqreen::WAFError)
135
+ infos.merge!(waf_infos(exception)) if exception.is_a?(Sqreen::WAFError)
116
136
  super(exception, infos, at)
117
137
  end
118
138
 
119
139
  private
120
140
 
121
- def exception_to_infos(e)
141
+ # see https://github.com/sqreen/TechDoc/blob/master/content/specs/spec000016-waf-integration.md#error-management
142
+ def waf_infos(e)
122
143
  {
123
- waf_rule: e.rule_name,
124
- error_code: ERROR_CODES[e.error],
125
- }.tap do |r|
126
- r[:error_data] = e.data if e.data
127
- r[:args] = e.args if e.args
128
- end
144
+ waf: {
145
+ waf_rule: e.rule_name,
146
+ error_code: ERROR_CODES[e.error],
147
+ }.tap do |r|
148
+ r[:error_data] = e.data if e.data
149
+ r[:args] = e.args if e.arg
150
+ end,
151
+ }
129
152
  end
130
153
 
131
154
  ERROR_CODES = {
@@ -23,6 +23,7 @@ require 'sqreen/performance_notifications/binned_metrics'
23
23
  require 'sqreen/legacy/instrumentation'
24
24
  require 'sqreen/call_countable'
25
25
  require 'sqreen/weave/legacy/instrumentation'
26
+ require 'sqreen/kit/configuration'
26
27
 
27
28
  module Sqreen
28
29
  @features = {}
@@ -37,6 +38,8 @@ module Sqreen
37
38
  PERF_METRICS_PERIOD = 60 # 1 min
38
39
  DEFAULT_PERF_LEVEL = 0 # disabled
39
40
 
41
+ DEFAULT_USE_SIGNALS = false
42
+
40
43
  class << self
41
44
  attr_reader :features
42
45
  def update_features(features)
@@ -87,7 +90,9 @@ module Sqreen
87
90
 
88
91
  attr_accessor :heartbeat_delay
89
92
  attr_accessor :metrics_engine
93
+ # @return [Sqreen::Deliveries::Simple]
90
94
  attr_reader :deliverer
95
+ # @return [Sqreen::Session]
91
96
  attr_reader :session
92
97
  attr_reader :instrumenter
93
98
  attr_accessor :running
@@ -111,17 +116,26 @@ module Sqreen
111
116
  @token = @configuration.get(:token)
112
117
  @app_name = @configuration.get(:app_name)
113
118
  @url = @configuration.get(:url)
119
+ @proxy_url = @configuration.get(:proxy_url)
114
120
  Sqreen.update_whitelisted_paths([])
115
121
  Sqreen.update_whitelisted_ips({})
116
122
  Sqreen.update_performance_budget(nil)
117
123
  raise(Sqreen::Exception, 'no url found') unless @url
118
124
  raise(Sqreen::TokenNotFoundException, 'no token found') unless @token
119
125
 
126
+ Sqreen::Kit::Configuration.logger = Sqreen.log
127
+ Sqreen::Kit::Configuration.ingestion_url = @configuration.get(:ingestion_url)
128
+ Sqreen::Kit::Configuration.proxy_url = @configuration.get(:proxy_url)
129
+
120
130
  register_exit_cb if set_at_exit
121
131
 
122
132
  self.metrics_engine = MetricsStore.new
123
133
 
124
- if @configuration.get(:weave)
134
+ needs_weave = proc do
135
+ Gem::Specification.select { |s| s.name == 'scout_apm' && Gem::Requirement.new('>= 2.5.2').satisfied_by?(Gem::Version.new(s.version)) }.any?
136
+ end
137
+
138
+ if @configuration.get(:weave) || needs_weave.call
125
139
  @instrumenter = Sqreen::Weave::Legacy::Instrumentation.new(metrics_engine)
126
140
  else
127
141
  @instrumenter = Sqreen::Legacy::Instrumentation.new(metrics_engine)
@@ -138,7 +152,7 @@ module Sqreen
138
152
  Sqreen.log.debug do
139
153
  "Override initial features with #{conf_features.inspect}"
140
154
  end
141
- wanted_features = conf_features
155
+ wanted_features = wanted_features.merge(conf_features)
142
156
  rescue
143
157
  Sqreen.log.warn do
144
158
  "NOT using invalid inital features #{conf_initial_features}"
@@ -157,7 +171,7 @@ module Sqreen
157
171
  end
158
172
 
159
173
  def create_session(session_class)
160
- @session = session_class.new(@url, @token, @app_name)
174
+ @session = session_class.new(@url, @token, @app_name, @proxy_url)
161
175
  session.login(@framework)
162
176
  end
163
177
 
@@ -166,8 +180,18 @@ module Sqreen
166
180
  @deliverer = new_deliverer
167
181
  end
168
182
 
169
- def batch_events(batch_size, max_staleness = nil)
183
+ def batch_events(batch_size, max_staleness = nil, use_signals = false)
170
184
  size = batch_size.to_i
185
+
186
+ if size <= 1 && use_signals
187
+ Sqreen.log.warn do
188
+ "Using signals with no delivery batching is unsupported. " \
189
+ "Using instead batching with batch size = 30, max_staleness = 60"
190
+ end
191
+ size = 30
192
+ max_staleness = 60
193
+ end
194
+
171
195
  self.deliverer = if size < 1
172
196
  Deliveries::Simple.new(session)
173
197
  else
@@ -297,19 +321,37 @@ module Sqreen
297
321
  def do_heartbeat
298
322
  @last_heartbeat_request = Time.now
299
323
  @next_metrics.concat(metrics_engine.publish(false)) if metrics_engine
300
- res = session.heartbeat(next_command_results, next_metrics)
324
+ metrics_in_hb = use_signals? ? nil : next_metrics
325
+
326
+ res = session.heartbeat(next_command_results, metrics_in_hb)
301
327
  next_command_results.clear
328
+
329
+ deliver_metrics_as_event if use_signals?
302
330
  next_metrics.clear
331
+
303
332
  process_commands(res['commands'])
304
333
  end
305
334
 
335
+ def deliver_metrics_as_event
336
+ # this is disastrous withe simple delivery strategy,
337
+ # as each aggregated metric would trigger an http request
338
+ # Sending of metrics is therefore not supported with simple delivery strategy
339
+ # TODO: Confirm that only batch is used in production
340
+ next_metrics.each { |x| deliverer.post_event(x) }
341
+ end
342
+
306
343
  def features(_context_infos = {})
307
344
  Sqreen.features
308
345
  end
309
346
 
347
+ def use_signals?
348
+ features.fetch('use_signals', DEFAULT_USE_SIGNALS)
349
+ end
350
+
310
351
  def features=(features)
311
352
  Sqreen.update_features(features)
312
353
  session.request_compression = features['request_compression'] if session
354
+ session.use_signals = use_signals?
313
355
  self.performance_metrics_period = features['performance_metrics_period']
314
356
 
315
357
  unless @configuration.get(:weave)
@@ -327,7 +369,7 @@ module Sqreen
327
369
  hd = features['heartbeat_delay'].to_i
328
370
  self.heartbeat_delay = hd if hd > 0
329
371
  return if features['batch_size'].nil?
330
- batch_events(features['batch_size'], features['max_staleness'])
372
+ batch_events(features['batch_size'], features['max_staleness'], use_signals?)
331
373
  end
332
374
 
333
375
  def change_whitelisted_paths(paths, _context_infos = {})
@@ -61,7 +61,7 @@ module Sqreen
61
61
  obj.each do |k, v|
62
62
  ck = k.is_a?(String) ? k.downcase : k
63
63
  if @keys.include?(ck)
64
- redacted << v
64
+ redacted += SensitiveDataRedactor.all_strings(v)
65
65
  v = MASK
66
66
  else
67
67
  v, r = redact(v)
@@ -74,39 +74,27 @@ module Sqreen
74
74
  [result, redacted]
75
75
  end
76
76
 
77
- def redact_attacks!(attacks, values)
78
- return attacks if values.empty?
79
-
80
- values = values.map { |v| v.downcase if v.is_a?(String) }
81
-
82
- attacks.each do |e|
83
- next(e) unless e[:infos]
84
- next(e) unless e[:infos][:waf_data]
85
-
86
- parsed = JSON.parse(e[:infos][:waf_data])
87
- redacted = parsed.each do |w|
88
- next unless (filters = w['filter'])
89
-
90
- filters.each do |f|
91
- next unless (v = f['resolved_value'])
92
- next unless values.include?(v.downcase)
77
+ class << self
78
+ def all_strings(v)
79
+ accum = []
80
+ all_strings_impl(v, accum)
81
+ accum
82
+ end
93
83
 
94
- f['match_status'] = MASK
95
- f['resolved_value'] = MASK
84
+ private
85
+
86
+ def all_strings_impl(obj, accum)
87
+ case obj
88
+ when String
89
+ accum << obj
90
+ when Array
91
+ obj.each { |el| all_strings_impl(el, accum) }
92
+ when Hash
93
+ obj.each do |k, v|
94
+ all_strings_impl(k, accum)
95
+ all_strings_impl(v, accum)
96
96
  end
97
97
  end
98
- e[:infos][:waf_data] = JSON.dump(redacted)
99
- end
100
- end
101
-
102
- def redact_exceptions!(exceptions, values)
103
- return exceptions if values.empty?
104
-
105
- exceptions.each do |e|
106
- next(e) unless e[:infos]
107
- next(e) unless e[:infos][:waf]
108
-
109
- e[:infos][:waf].delete(:args)
110
98
  end
111
99
  end
112
100
  end