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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +22 -0
  3. data/README.md +77 -0
  4. data/Rakefile +20 -0
  5. data/lib/sqreen-alt.rb +1 -0
  6. data/lib/sqreen.rb +68 -0
  7. data/lib/sqreen/attack_detected.html +2 -0
  8. data/lib/sqreen/binding_accessor.rb +288 -0
  9. data/lib/sqreen/ca.crt +72 -0
  10. data/lib/sqreen/call_countable.rb +67 -0
  11. data/lib/sqreen/callback_tree.rb +78 -0
  12. data/lib/sqreen/callbacks.rb +100 -0
  13. data/lib/sqreen/capped_queue.rb +23 -0
  14. data/lib/sqreen/condition_evaluator.rb +235 -0
  15. data/lib/sqreen/conditionable.rb +50 -0
  16. data/lib/sqreen/configuration.rb +168 -0
  17. data/lib/sqreen/context.rb +26 -0
  18. data/lib/sqreen/deliveries/batch.rb +84 -0
  19. data/lib/sqreen/deliveries/simple.rb +39 -0
  20. data/lib/sqreen/event.rb +16 -0
  21. data/lib/sqreen/events/attack.rb +61 -0
  22. data/lib/sqreen/events/remote_exception.rb +54 -0
  23. data/lib/sqreen/events/request_record.rb +62 -0
  24. data/lib/sqreen/exception.rb +34 -0
  25. data/lib/sqreen/frameworks.rb +40 -0
  26. data/lib/sqreen/frameworks/generic.rb +446 -0
  27. data/lib/sqreen/frameworks/rails.rb +148 -0
  28. data/lib/sqreen/frameworks/rails3.rb +36 -0
  29. data/lib/sqreen/frameworks/request_recorder.rb +69 -0
  30. data/lib/sqreen/frameworks/sinatra.rb +57 -0
  31. data/lib/sqreen/frameworks/sqreen_test.rb +26 -0
  32. data/lib/sqreen/instrumentation.rb +542 -0
  33. data/lib/sqreen/log.rb +119 -0
  34. data/lib/sqreen/metrics.rb +6 -0
  35. data/lib/sqreen/metrics/average.rb +39 -0
  36. data/lib/sqreen/metrics/base.rb +45 -0
  37. data/lib/sqreen/metrics/collect.rb +22 -0
  38. data/lib/sqreen/metrics/sum.rb +20 -0
  39. data/lib/sqreen/metrics_store.rb +96 -0
  40. data/lib/sqreen/middleware.rb +34 -0
  41. data/lib/sqreen/payload_creator.rb +137 -0
  42. data/lib/sqreen/performance_notifications.rb +86 -0
  43. data/lib/sqreen/performance_notifications/log.rb +36 -0
  44. data/lib/sqreen/performance_notifications/metrics.rb +36 -0
  45. data/lib/sqreen/performance_notifications/newrelic.rb +36 -0
  46. data/lib/sqreen/remote_command.rb +93 -0
  47. data/lib/sqreen/rule_attributes.rb +26 -0
  48. data/lib/sqreen/rule_callback.rb +108 -0
  49. data/lib/sqreen/rules.rb +126 -0
  50. data/lib/sqreen/rules_callbacks.rb +29 -0
  51. data/lib/sqreen/rules_callbacks/binding_accessor_matcher.rb +77 -0
  52. data/lib/sqreen/rules_callbacks/binding_accessor_metrics.rb +79 -0
  53. data/lib/sqreen/rules_callbacks/blacklist_ips.rb +44 -0
  54. data/lib/sqreen/rules_callbacks/count_http_codes.rb +40 -0
  55. data/lib/sqreen/rules_callbacks/crawler_user_agent_matches.rb +24 -0
  56. data/lib/sqreen/rules_callbacks/crawler_user_agent_matches_metrics.rb +24 -0
  57. data/lib/sqreen/rules_callbacks/custom_error.rb +64 -0
  58. data/lib/sqreen/rules_callbacks/execjs.rb +241 -0
  59. data/lib/sqreen/rules_callbacks/headers_insert.rb +22 -0
  60. data/lib/sqreen/rules_callbacks/inspect_rule.rb +25 -0
  61. data/lib/sqreen/rules_callbacks/matcher_rule.rb +138 -0
  62. data/lib/sqreen/rules_callbacks/rails_parameters.rb +14 -0
  63. data/lib/sqreen/rules_callbacks/record_request_context.rb +39 -0
  64. data/lib/sqreen/rules_callbacks/reflected_xss.rb +254 -0
  65. data/lib/sqreen/rules_callbacks/regexp_rule.rb +36 -0
  66. data/lib/sqreen/rules_callbacks/shell_env.rb +32 -0
  67. data/lib/sqreen/rules_callbacks/url_matches.rb +25 -0
  68. data/lib/sqreen/rules_callbacks/user_agent_matches.rb +22 -0
  69. data/lib/sqreen/rules_signature.rb +151 -0
  70. data/lib/sqreen/runner.rb +365 -0
  71. data/lib/sqreen/runtime_infos.rb +138 -0
  72. data/lib/sqreen/safe_json.rb +60 -0
  73. data/lib/sqreen/sdk.rb +22 -0
  74. data/lib/sqreen/serializer.rb +46 -0
  75. data/lib/sqreen/session.rb +317 -0
  76. data/lib/sqreen/shared_storage.rb +31 -0
  77. data/lib/sqreen/stats.rb +18 -0
  78. data/lib/sqreen/version.rb +5 -0
  79. metadata +148 -0
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (c) 2015 Sqreen. All Rights Reserved.
3
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
4
+
5
+ require 'sqreen/frameworks/generic'
6
+ require 'sqreen/middleware'
7
+
8
+ module Sqreen
9
+ module Frameworks
10
+ # Rails related framework code
11
+ class RailsFramework < GenericFramework
12
+ DB_MAPPING = {
13
+ 'SQLite' => :sqlite,
14
+ 'MySQL' => :mysql,
15
+ 'Mysql2' => :mysql,
16
+ }.freeze
17
+
18
+ def framework_infos
19
+ {
20
+ :framework_type => 'Rails',
21
+ :framework_version => Rails::VERSION::STRING,
22
+ :environment => Rails.env.to_s,
23
+ }
24
+ end
25
+
26
+ def development?
27
+ Rails.env.development?
28
+ end
29
+
30
+ def db_settings(options = {})
31
+ adapter = options[:connection_adapter]
32
+ return nil unless adapter
33
+
34
+ begin
35
+ adapter_name = adapter.adapter_name
36
+ rescue
37
+ # FIXME: we may want to log that
38
+ Sqreen.log.warn 'cannot find ADAPTER_NAME'
39
+ return nil
40
+ end
41
+ db_type = DB_MAPPING[adapter_name]
42
+ db_infos = { :name => adapter_name }
43
+ [db_type, db_infos]
44
+ end
45
+
46
+ def ip_headers
47
+ ret = super
48
+ remote_ip = rails_client_ip
49
+ ret << ['action_dispatch.remote_ip', remote_ip] unless remote_ip.nil?
50
+ ret
51
+ end
52
+
53
+ # What is the current client IP as seen by rails
54
+ def rails_client_ip
55
+ req = request
56
+ return unless req && req.env
57
+ remote_ip = req.env['action_dispatch.remote_ip']
58
+ return unless remote_ip
59
+ # FIXME: - this exist only since Rails 3.2.1
60
+ # http://apidock.com/rails/v3.2.1/ActionDispatch/RemoteIp/GetIp/calculate_ip
61
+ return remote_ip.calculate_ip if remote_ip.respond_to?(:calculate_ip)
62
+ # This might not return the same value as calculate IP
63
+ remote_ip.to_s
64
+ end
65
+
66
+ def request_id
67
+ req = request
68
+ return super unless req
69
+ req.env['action_dispatch.request_id'] || super
70
+ end
71
+
72
+ def root
73
+ return nil unless @application
74
+ @application.root
75
+ end
76
+
77
+ # Register a new initializer in rails to ba called when we are starting up
78
+ class Init < ::Rails::Railtie
79
+ def self.startup
80
+ initializer 'sqreen.startup' do |app|
81
+ app.middleware.insert_before(Rack::Runtime, Sqreen::Middleware)
82
+ app.middleware.insert_after(ActionDispatch::DebugExceptions, Sqreen::RailsMiddleware)
83
+ app.middleware.insert_after(ActionDispatch::DebugExceptions, Sqreen::ErrorHandlingMiddleware)
84
+ yield app
85
+ end
86
+ end
87
+ end
88
+
89
+ def on_start(&block)
90
+ @calling_pid = Process.pid
91
+ Init.startup do |app|
92
+ hook_rack_request(app.class, &block)
93
+ app.config.after_initialize do
94
+ yield self
95
+ end
96
+ end
97
+ end
98
+
99
+ def prevent_startup
100
+ res = super
101
+ return res if res
102
+ run_in_test = sqreen_configuration.get(:run_in_test)
103
+ return :rails_test if !run_in_test && (Rails.env.test? || Rails.env.cucumber?)
104
+
105
+ # SQREEN-880 - prevent Sqreen startup on Sidekiq workers
106
+ return :sidekiq_cli if defined?(Sidekiq::CLI)
107
+
108
+ # Prevent Sqreen startup on rake tasks - unless this is a Sqreen test
109
+ return :rake if !run_in_test && $0.end_with?('rake')
110
+
111
+ return nil unless defined?(Rails::CommandsTasks)
112
+ return nil if defined?(Rails::Server)
113
+ return :rails_console if defined?(Rails::Console)
114
+ return :rails_dbconsole if defined?(Rails::DBConsole)
115
+ return :rails_generators if defined?(Rails::Generators)
116
+ nil
117
+ end
118
+
119
+ def instrument_when_ready!(instrumentor, rules)
120
+ instrumentor.instrument!(rules, self)
121
+ end
122
+
123
+ def rails_params
124
+ self.class.rails_params(request)
125
+ end
126
+
127
+ def self.rails_params(request)
128
+ return nil unless request
129
+ other = request.env['action_dispatch.request.parameters']
130
+ return nil unless other
131
+ # Remove Rails created parameters:
132
+ other = other.dup
133
+ other.delete :action
134
+ other.delete :controller
135
+ other
136
+ end
137
+
138
+ P_OTHER = 'other'.freeze
139
+
140
+ def self.parameters_from_request(request)
141
+ return {} unless request
142
+ ret = super(request)
143
+ ret[P_OTHER] = rails_params(request)
144
+ ret
145
+ end
146
+ end
147
+ end
148
+ end
@@ -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/frameworks/rails'
5
+
6
+ module Sqreen
7
+ module Frameworks
8
+ # Handle Rails 3 specifics
9
+ class Rails3Framework < RailsFramework
10
+ def root
11
+ Rails.root
12
+ end
13
+
14
+ def prevent_startup
15
+ res = super
16
+ return res if res
17
+ return :rails_console if defined?(Rails::Console)
18
+ nil
19
+ end
20
+
21
+ def instrument_when_ready!(instrumentor, rules)
22
+ config = Rails.configuration
23
+ if config.cache_classes
24
+ instrumentor.instrument!(rules, self)
25
+ else
26
+ # FIXME: What needs to be done if no active_record?
27
+ # (probably related to SQREEN-219)
28
+ frm = self
29
+ ActiveSupport.on_load(:active_record) do
30
+ instrumentor.instrument!(rules, frm)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,69 @@
1
+ # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+ require 'set'
4
+ require 'sqreen/shared_storage'
5
+ require 'sqreen/events/request_record'
6
+
7
+ module Sqreen
8
+ # Store event/observations that happened in this request
9
+ module RequestRecorder
10
+ def observed_items
11
+ SharedStorage.get(:observed_items)
12
+ end
13
+
14
+ def observed_items=(value)
15
+ SharedStorage.set(:observed_items, value)
16
+ end
17
+
18
+ def payload_requests
19
+ SharedStorage.get(:payload_requests)
20
+ end
21
+
22
+ def payload_requests=(value)
23
+ SharedStorage.set(:payload_requests, value)
24
+ end
25
+
26
+ def only_metric_observation
27
+ SharedStorage.get(:only_metric_observation)
28
+ end
29
+
30
+ def only_metric_observation=(value)
31
+ SharedStorage.set(:only_metric_observation, value)
32
+ end
33
+
34
+ def clean_request_record
35
+ self.only_metric_observation = true
36
+ self.payload_requests = Set.new
37
+ self.observed_items = Hash.new { |hash, key| hash[key] = [] }
38
+ end
39
+
40
+ def observe(what, data, accessors = [], report = true)
41
+ clean_request_record if observed_items.nil?
42
+ self.only_metric_observation = false if report
43
+ observed_items[what] << data
44
+ payload_requests.merge(accessors)
45
+ end
46
+
47
+ def close_request_record(queue, observations_queue, payload_creator)
48
+ clean_request_record if observed_items.nil?
49
+ if only_metric_observation
50
+ push_metrics(observations_queue, queue)
51
+ return clean_request_record
52
+ end
53
+ payload = payload_creator.payload(payload_requests)
54
+ payload[:observed] = observed_items
55
+ queue.push RequestRecord.new(payload)
56
+ clean_request_record
57
+ end
58
+
59
+ protected
60
+
61
+ def push_metrics(observations_queue, event_queue)
62
+ observed_items[:observations].each do |obs|
63
+ observations_queue.push obs
64
+ end
65
+ return unless observations_queue.size > MAX_OBS_QUEUE_LENGTH / 2
66
+ event_queue.push Sqreen::METRICS_EVENT
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,57 @@
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/frameworks/generic'
5
+ require 'sqreen/middleware'
6
+
7
+ module Sqreen
8
+ module Frameworks
9
+ # Handle Sinatra specific functions
10
+ class SinatraFramework < GenericFramework
11
+ def framework_infos
12
+ h = super
13
+ h[:framework_type] = 'Sinatra'
14
+ h[:framework_version] = Sinatra::VERSION
15
+ h
16
+ end
17
+
18
+ def on_start(&block)
19
+ hook_app_build(Sinatra::Base)
20
+ hook_rack_request(Sinatra::Application, &block)
21
+ yield self
22
+ end
23
+
24
+ def db_settings(options = {})
25
+ adapter = options[:connection_adapter]
26
+ return nil unless adapter
27
+
28
+ begin
29
+ adapter_name = adapter.class.const_get 'ADAPTER_NAME'
30
+ rescue
31
+ # FIXME: we may want to log that
32
+ Sqreen.log.warn 'cannot find ADAPTER_NAME'
33
+ return nil
34
+ end
35
+ db_type = DB_MAPPING[adapter_name]
36
+ db_infos = { :name => adapter_name }
37
+ [db_type, db_infos]
38
+ end
39
+
40
+ def hook_app_build(klass)
41
+ klass.singleton_class.class_eval do
42
+ define_method(:setup_default_middleware_with_sqreen) do |builder|
43
+ ret = setup_default_middleware_without_sqreen(builder)
44
+ builder.instance_variable_get('@use').insert(2, proc do |app|
45
+ # Inject error middle just before sinatra one
46
+ Sqreen::ErrorHandlingMiddleware.new(app)
47
+ end)
48
+ ret
49
+ end
50
+
51
+ alias_method :setup_default_middleware_without_sqreen, :setup_default_middleware
52
+ alias_method :setup_default_middleware, :setup_default_middleware_with_sqreen
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,26 @@
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/frameworks/generic'
5
+
6
+ module Sqreen
7
+ module Frameworks
8
+ # Rails related framework code
9
+ class SqreenTestFramework < GenericFramework
10
+ def framework_infos
11
+ {
12
+ :framework_type => 'SqreenTest',
13
+ :framework_version => '0.1',
14
+ }
15
+ end
16
+
17
+ def client_ip
18
+ '127.0.0.1'
19
+ end
20
+
21
+ def request_infos
22
+ {}
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,542 @@
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/callback_tree'
5
+ require 'sqreen/log'
6
+ require 'sqreen/stats'
7
+ require 'sqreen/exception'
8
+ require 'sqreen/performance_notifications'
9
+ require 'sqreen/call_countable'
10
+ require 'sqreen/events/remote_exception'
11
+ require 'sqreen/rules_signature'
12
+ require 'set'
13
+
14
+ # How to override a class method:
15
+ #
16
+ # class Cache
17
+ #
18
+ # def self.get3
19
+ # puts "GET3"
20
+ # end
21
+ # def self.get
22
+ # puts "GET"
23
+ # end
24
+ # end
25
+ #
26
+ # class << Cache # Change context to metaclass of Cache
27
+ # def get_modified
28
+ # puts "GET MODIFI"
29
+ # end
30
+ # alias_method :get_not_modified, :get
31
+ # alias_method :get, :get_modified
32
+ # end
33
+
34
+ module Sqreen
35
+ class Instrumentation
36
+ WHITELISTED_METRIC='whitelisted'.freeze
37
+ @@override_semaphore = Mutex.new
38
+
39
+ ## Overriden methods and callbacks globals
40
+ @@overriden_methods = []
41
+ @@registered_callbacks = CBTree.new
42
+ @@instrumented_pid = nil
43
+
44
+ def self.semaphore
45
+ @@override_semaphore
46
+ end
47
+
48
+ def self.instrumented_pid
49
+ @@instrumented_pid
50
+ end
51
+
52
+ def self.callbacks
53
+ @@registered_callbacks
54
+ end
55
+
56
+ def self.overriden
57
+ @@overriden_methods
58
+ end
59
+
60
+ def self.callback_wrapper_pre(klass, method, instance, *args, &block)
61
+ Instrumentation.guard_call(method, []) do
62
+ callbacks = @@registered_callbacks.get(klass, method, :pre)
63
+ if callbacks.any?(&:whitelisted?)
64
+ callbacks = callbacks.reject(&:whitelisted?)
65
+ end
66
+
67
+ returns = []
68
+ callbacks.each do |cb|
69
+ # If record_request is part of callbacks we should filter after it ran
70
+ next if cb.whitelisted?
71
+ rule = cb.rule_name if cb.respond_to?(:rule_name)
72
+ Sqreen.log.debug { "running pre cb #{cb}" }
73
+ Sqreen::PerformanceNotifications.instrument("Callbacks/#{rule || cb.class.name}/pre") do
74
+ begin
75
+ res = cb.send(:pre, instance, *args, &block)
76
+ if !res.nil? && cb.respond_to?(:block) && (!cb.block && !Sqreen.config_get(:block_all_rules))
77
+ Sqreen.log.debug do
78
+ "#{cb} cannot block, overriding return value"
79
+ end
80
+ res = nil
81
+ elsif res.is_a?(Hash)
82
+ res[:rule_name] = rule
83
+ end
84
+ returns << res
85
+ rescue => e
86
+ Sqreen.log.warn "we catch an exception: #{e.inspect}"
87
+ Sqreen.log.debug e.backtrace
88
+ if cb.respond_to?(:record_exception)
89
+ cb.record_exception(e)
90
+ else
91
+ Sqreen::RemoteException.record(e)
92
+ end
93
+ next
94
+ end
95
+ end
96
+ end
97
+ returns
98
+ end
99
+ end
100
+
101
+ def self.callback_wrapper_post(klass, method, return_val, instance, *args, &block)
102
+ Instrumentation.guard_call(method, []) do
103
+ callbacks = @@registered_callbacks.get(klass, method, :post)
104
+ if callbacks.any?(&:whitelisted?)
105
+ callbacks = callbacks.reject(&:whitelisted?)
106
+ end
107
+
108
+ returns = []
109
+ callbacks.reverse_each do |cb|
110
+ rule = cb.rule_name if cb.respond_to?(:rule_name)
111
+ Sqreen.log.debug { "running post cb #{cb}" }
112
+ Sqreen::PerformanceNotifications.instrument("Callbacks/#{rule || cb.class.name}/post") do
113
+ begin
114
+ res = cb.send(:post, return_val, instance, *args, &block)
115
+ if !res.nil? && cb.respond_to?(:block) && (!cb.block && !Sqreen.config_get(:block_all_rules))
116
+ Sqreen.log.debug do
117
+ "#{cb} cannot block, overriding return value"
118
+ end
119
+ res = nil
120
+ elsif res.is_a?(Hash)
121
+ res[:rule_name] = rule
122
+ end
123
+ returns << res
124
+ rescue => e
125
+ Sqreen.log.warn "we catch an exception: #{e.inspect}"
126
+ Sqreen.log.debug e.backtrace
127
+ if cb.respond_to?(:record_exception)
128
+ cb.record_exception(e)
129
+ else
130
+ Sqreen::RemoteException.record(e)
131
+ end
132
+ next
133
+ end
134
+ end
135
+ end
136
+ returns
137
+ end
138
+ end
139
+
140
+ def self.callback_wrapper_failing(exception, klass, method, instance, *args, &block)
141
+ Instrumentation.guard_call(method, []) do
142
+ callbacks = @@registered_callbacks.get(klass, method, :failing)
143
+ if callbacks.any?(&:whitelisted?)
144
+ callbacks = callbacks.reject(&:whitelisted?)
145
+ end
146
+
147
+ returns = []
148
+ callbacks.each do |cb|
149
+ rule = cb.rule_name if cb.respond_to?(:rule_name)
150
+ Sqreen.log.debug { "running failing cb #{cb}" }
151
+ Sqreen::PerformanceNotifications.instrument("Callbacks/#{rule || cb.class.name}/failing") do
152
+ begin
153
+ res = cb.send(:failing, exception, instance, *args, &block)
154
+ if !res.nil? && cb.respond_to?(:block) && (!cb.block && !Sqreen.config_get(:block_all_rules))
155
+ Sqreen.log.debug do
156
+ "#{cb} cannot block, overriding return value"
157
+ end
158
+ res = nil
159
+ elsif res.is_a?(Hash)
160
+ res[:rule_name] = rule
161
+ end
162
+ returns << res
163
+ rescue => e
164
+ Sqreen.log.warn "we catch an exception: #{e.inspect}"
165
+ Sqreen.log.debug e.backtrace
166
+ if cb.respond_to?(:record_exception)
167
+ cb.record_exception(e)
168
+ else
169
+ Sqreen::RemoteException.record(e)
170
+ end
171
+ next
172
+ end
173
+ end
174
+ end
175
+ returns
176
+ end
177
+ end
178
+
179
+ def self.guard_call(method, retval)
180
+ @sqreen_in_instr ||= nil
181
+ return retval if @sqreen_in_instr && @sqreen_in_instr.member?(method)
182
+ @sqreen_in_instr ||= Set.new
183
+ @sqreen_in_instr.add(method)
184
+ r = yield
185
+ @sqreen_in_instr.delete(method)
186
+ return r
187
+ rescue Exception => e
188
+ @sqreen_in_instr.delete(method)
189
+ raise e
190
+ end
191
+
192
+ def self.define_callback_method(meth, original_meth, klass_name)
193
+ proc do |*args, &block|
194
+ if Process.pid != Instrumentation.instrumented_pid
195
+ Sqreen.log.debug do
196
+ "Instrumented #{Instrumentation.instrumented_pid} != PID #{Process.pid}"
197
+ end
198
+ return send(original_meth, *args, &block)
199
+ end
200
+ Sqreen.stats.callbacks_calls += 1
201
+
202
+ skip = false
203
+ result = nil
204
+
205
+ # pre callback
206
+ returns = Instrumentation.callback_wrapper_pre(klass_name,
207
+ meth,
208
+ self,
209
+ *args,
210
+ &block)
211
+ returns.each do |ret|
212
+ next unless ret.is_a? Hash
213
+ case ret[:status]
214
+ when :skip, 'skip'
215
+ skip = true
216
+ result = ret[:new_return_value] if ret.key? :new_return_value
217
+ next
218
+ when :modify_args, 'modify_args'
219
+ args = ret[:args]
220
+ when :raise, 'raise'
221
+ fail Sqreen::AttackBlocked, "Sqreen blocked a security threat (type: #{ret[:rule_name]}). No action is required."
222
+ end
223
+ end
224
+
225
+ return result if skip
226
+ begin
227
+ result = send(original_meth, *args, &block)
228
+ rescue => e
229
+ returns = Instrumentation.callback_wrapper_failing(e, klass_name,
230
+ meth,
231
+ self,
232
+ *args,
233
+ &block)
234
+ will_retry = false
235
+ will_raise = returns.empty?
236
+ returns.each do |ret|
237
+ will_raise = true if ret.nil?
238
+ next unless ret.is_a? Hash
239
+ case ret[:status]
240
+ when :override, 'override'
241
+ result = ret[:new_return_value] if ret.key? :new_return_value
242
+ when :retry, 'retry'
243
+ will_retry = true
244
+ else # :reraise, 'reraise'
245
+ will_raise = true
246
+ end
247
+ end
248
+ raise e if will_raise
249
+ retry if will_retry
250
+ result
251
+ else
252
+
253
+ # post callback
254
+ returns = Instrumentation.callback_wrapper_post(klass_name,
255
+ meth,
256
+ result,
257
+ self,
258
+ *args,
259
+ &block)
260
+ returns.each do |ret|
261
+ next unless ret.is_a? Hash
262
+ case ret[:status]
263
+ when :raise, 'raise'
264
+ fail Sqreen::AttackBlocked, "Sqreen blocked a security threat (type: #{ret[:rule_name]}). No action is required."
265
+ when :override, 'override'
266
+ result = ret[:new_return_value]
267
+ else
268
+ next
269
+ end
270
+ end
271
+ result
272
+ end
273
+ end
274
+ end
275
+
276
+ def override_class_method(klass, meth)
277
+ # FIXME: This is somehow ugly. We should reduce the amount of
278
+ # `evaled` code.
279
+ str = " class << #{klass}
280
+
281
+ original = '#{meth}'.to_sym
282
+ saved_meth_name = '#{get_saved_method_name(meth)}'.to_sym
283
+ new_method = '#{meth}_modified'.to_sym
284
+
285
+ alias_method saved_meth_name, original
286
+
287
+ p = Instrumentation.define_callback_method(original, saved_meth_name,
288
+ #{klass})
289
+ define_method(new_method, p)
290
+
291
+ private new_method
292
+
293
+ method_kind = nil
294
+ case
295
+ when public_method_defined?(original)
296
+ method_kind = :public
297
+ when protected_method_defined?(original)
298
+ method_kind = :protected
299
+ when private_method_defined?(original)
300
+ method_kind = :private
301
+ end
302
+ alias_method original, new_method
303
+ send(method_kind, original)
304
+ private saved_meth_name
305
+ end "
306
+ eval str
307
+ end
308
+
309
+ def unoverride_instance_method(obj, meth)
310
+ saved_meth_name = get_saved_method_name(meth)
311
+
312
+ method_kind = nil
313
+ obj.class_eval do
314
+ # Note: As a lambda the following will crash ruby 2.2.3p173
315
+ case
316
+ when public_method_defined?(meth)
317
+ method_kind = :public
318
+ when protected_method_defined?(meth)
319
+ method_kind = :protected
320
+ when private_method_defined?(meth)
321
+ method_kind = :private
322
+ end
323
+ alias_method meth, saved_meth_name
324
+ send(method_kind, meth)
325
+ end
326
+ end
327
+
328
+ def get_saved_method_name(meth)
329
+ "#{meth}_not_modified".to_sym
330
+ end
331
+
332
+ def override_instance_method(klass_name, meth)
333
+ saved_meth_name = get_saved_method_name(meth)
334
+ new_method = "#{meth}_modified".to_sym
335
+
336
+ p = Instrumentation.define_callback_method(meth, saved_meth_name,
337
+ klass_name)
338
+ method_kind = nil
339
+ klass_name.class_eval do
340
+ alias_method saved_meth_name, meth
341
+
342
+ define_method(new_method, p)
343
+
344
+ case
345
+ when public_method_defined?(meth)
346
+ method_kind = :public
347
+ when protected_method_defined?(meth)
348
+ method_kind = :protected
349
+ when private_method_defined?(meth)
350
+ method_kind = :private
351
+ end
352
+ alias_method meth, new_method
353
+ private saved_meth_name
354
+ private new_method
355
+ send(method_kind, meth)
356
+ end
357
+ saved_meth_name
358
+ end
359
+
360
+ # WARNING We do not actually remove `meth`
361
+ def unoverride_class_method(klass, meth)
362
+ saved_meth_name = get_saved_method_name(meth)
363
+
364
+ eval "method_kind = nil; class << #{klass}
365
+ case
366
+ when public_method_defined?(#{meth.to_sym.inspect})
367
+ method_kind = :public
368
+ when protected_method_defined?(original)
369
+ method_kind = :protected
370
+ when private_method_defined?(#{meth.to_sym.inspect})
371
+ method_kind = :private
372
+ end
373
+ alias_method #{meth.to_sym.inspect}, #{saved_meth_name.to_sym.inspect}
374
+ send(method_kind, #{meth.to_sym.inspect})
375
+ end "
376
+ end
377
+
378
+ if RUBY_VERSION < '1.9'
379
+ def adjust_method_name(method)
380
+ method.to_s
381
+ end
382
+ else
383
+ def adjust_method_name(method)
384
+ method
385
+ end
386
+ end
387
+
388
+ def is_instance_method?(klass, method)
389
+ method = adjust_method_name(method)
390
+ klass.instance_methods.include?(method) ||
391
+ klass.private_instance_methods.include?(method)
392
+ end
393
+
394
+ def is_class_method?(klass, method)
395
+ method = adjust_method_name(method)
396
+ klass.singleton_methods.include? method
397
+ end
398
+
399
+ # Does this object or an instance of it respond_to method?
400
+ def valid_method?(obj, method)
401
+ return true if is_class_method?(obj, method)
402
+ return false unless obj.respond_to?(:instance_methods)
403
+ is_instance_method?(obj, method)
404
+ end
405
+
406
+ def add_callback(cb)
407
+ @@override_semaphore.synchronize do
408
+ klass = cb.klass
409
+ method = cb.method
410
+ key = [klass, method]
411
+
412
+ already_overriden = @@overriden_methods.include? key
413
+
414
+ if !already_overriden
415
+ if is_class_method?(klass, method)
416
+ Sqreen.log.debug "overriding class method for #{cb}"
417
+ success = override_class_method(klass, method)
418
+ elsif is_instance_method?(klass, method)
419
+ Sqreen.log.debug "overriding instance method for #{cb}"
420
+ success = override_instance_method(klass, method)
421
+ else
422
+ # FIXME: Override define_method and other dynamic ways to
423
+ # The following should be monitored to make sure we
424
+ # don't forget dynamically added methods:
425
+ # - define_method
426
+ # - method_added
427
+ # - method_missing
428
+ # ...
429
+ #
430
+ msg = "#{cb} is neither singleton or instance"
431
+ raise Sqreen::NotImplementedYet, msg
432
+ end
433
+
434
+ @@overriden_methods += [key] if success
435
+ else
436
+ Sqreen.log.debug "#{key} was already overriden"
437
+ end
438
+
439
+ @@registered_callbacks.add(cb)
440
+ @@instrumented_pid = Process.pid
441
+ end
442
+ end
443
+
444
+ def remove_callback(cb)
445
+ @@override_semaphore.synchronize do
446
+ remove_callback_no_lock(cb)
447
+ end
448
+ end
449
+
450
+ def remove_callback_no_lock(cb)
451
+ klass = cb.klass
452
+ method = cb.method
453
+
454
+ key = [klass, method]
455
+
456
+ already_overriden = @@overriden_methods.include? key
457
+ unless already_overriden
458
+ Sqreen.log.debug "#{key} not overriden, returning"
459
+ return
460
+ end
461
+
462
+ defined_cbs = @@registered_callbacks.get(klass, method)
463
+
464
+ nb_removed = 0
465
+ defined_cbs.each do |found_cb|
466
+ if found_cb == cb
467
+ Sqreen.log.debug "Removing callback #{found_cb}"
468
+ @@registered_callbacks.remove(found_cb)
469
+ nb_removed += 1
470
+ else
471
+ Sqreen.log.debug "Not removing callback #{found_cb} (remains #{defined_cbs.size} cbs)"
472
+ end
473
+ end
474
+
475
+ return unless nb_removed == defined_cbs.size
476
+
477
+ Sqreen.log.debug "Removing overriden method #{key}"
478
+ @@overriden_methods.delete(key)
479
+
480
+ if is_class_method?(klass, method)
481
+ unoverride_class_method(klass, method)
482
+ elsif is_instance_method?(klass, method)
483
+ unoverride_instance_method(klass, method)
484
+ else
485
+ # FIXME: Override define_method and other dynamic ways to
486
+ # The following should be monitored to make sure we
487
+ # don't forget dynamically added methods:
488
+ # - define_method
489
+ # - method_added
490
+ # - method_missing
491
+ # ...
492
+ #
493
+ msg = "#{cb} is neither singleton or instance"
494
+ raise Sqreen::NotImplementedYet, msg
495
+ end
496
+ end
497
+
498
+ def remove_all_callbacks
499
+ @@override_semaphore.synchronize do
500
+ @@registered_callbacks.entries.each do |cb|
501
+ remove_callback_no_lock(cb)
502
+ end
503
+ Sqreen.instrumentation_ready = false
504
+ end
505
+ end
506
+
507
+ attr_accessor :metrics_engine
508
+
509
+ # Instrument the application code using the rules
510
+ # @param rules [Array<Hash>] Rules to instrument
511
+ # @param metrics_engine [MetricsStore] Metric storage facility
512
+ def instrument!(rules, framework)
513
+ verifier = nil
514
+ if Sqreen.features['rules_signature'] &&
515
+ Sqreen.config_get(:rules_verify_signature) == true &&
516
+ !defined?(::JRUBY_VERSION)
517
+ verifier = Sqreen::SqreenSignedVerifier.new
518
+ else
519
+ Sqreen.log.debug('Rules signature is not enabled')
520
+ end
521
+ remove_all_callbacks # Force cb tree to be empty before instrumenting
522
+ rules.each do |rule|
523
+ rcb = Sqreen::Rules.cb_from_rule(rule, self, metrics_engine, verifier)
524
+ next unless rcb
525
+ rcb.framework = framework
526
+ add_callback(rcb)
527
+ end
528
+ Sqreen.instrumentation_ready = true
529
+ end
530
+
531
+ def initialize(metrics_engine = nil)
532
+ self.metrics_engine = metrics_engine
533
+ return if metrics_engine.nil?
534
+ metrics_engine.create_metric('name' => CallCountable::COUNT_CALLS,
535
+ 'period' => 60,
536
+ 'kind' => 'Sum')
537
+ metrics_engine.create_metric('name' => WHITELISTED_METRIC,
538
+ 'period' => 60,
539
+ 'kind' => 'Sum')
540
+ end
541
+ end
542
+ end