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,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
|