sqreen-alt 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
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