immunio 0.15.2
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/LICENSE +234 -0
- data/README.md +147 -0
- data/bin/immunio +5 -0
- data/lib/immunio.rb +29 -0
- data/lib/immunio/agent.rb +260 -0
- data/lib/immunio/authentication.rb +96 -0
- data/lib/immunio/blocked_app.rb +38 -0
- data/lib/immunio/channel.rb +432 -0
- data/lib/immunio/cli.rb +39 -0
- data/lib/immunio/context.rb +114 -0
- data/lib/immunio/errors.rb +43 -0
- data/lib/immunio/immunio_ca.crt +45 -0
- data/lib/immunio/logger.rb +87 -0
- data/lib/immunio/plugins/action_dispatch.rb +45 -0
- data/lib/immunio/plugins/action_view.rb +431 -0
- data/lib/immunio/plugins/active_record.rb +707 -0
- data/lib/immunio/plugins/active_record_relation.rb +370 -0
- data/lib/immunio/plugins/authlogic.rb +80 -0
- data/lib/immunio/plugins/csrf.rb +24 -0
- data/lib/immunio/plugins/devise.rb +40 -0
- data/lib/immunio/plugins/environment_reporter.rb +69 -0
- data/lib/immunio/plugins/eval.rb +51 -0
- data/lib/immunio/plugins/exception_handler.rb +55 -0
- data/lib/immunio/plugins/gems_tracker.rb +5 -0
- data/lib/immunio/plugins/haml.rb +36 -0
- data/lib/immunio/plugins/http_finisher.rb +50 -0
- data/lib/immunio/plugins/http_tracker.rb +203 -0
- data/lib/immunio/plugins/io.rb +96 -0
- data/lib/immunio/plugins/redirect.rb +42 -0
- data/lib/immunio/plugins/warden.rb +66 -0
- data/lib/immunio/processor.rb +234 -0
- data/lib/immunio/rails.rb +26 -0
- data/lib/immunio/request.rb +139 -0
- data/lib/immunio/rufus_lua_ext/ref.rb +27 -0
- data/lib/immunio/rufus_lua_ext/state.rb +157 -0
- data/lib/immunio/rufus_lua_ext/table.rb +137 -0
- data/lib/immunio/rufus_lua_ext/utils.rb +13 -0
- data/lib/immunio/version.rb +5 -0
- data/lib/immunio/vm.rb +291 -0
- data/lua-hooks/ext/all.c +78 -0
- data/lua-hooks/ext/bitop/README +22 -0
- data/lua-hooks/ext/bitop/bit.c +189 -0
- data/lua-hooks/ext/extconf.rb +38 -0
- data/lua-hooks/ext/libinjection/COPYING +37 -0
- data/lua-hooks/ext/libinjection/libinjection.h +65 -0
- data/lua-hooks/ext/libinjection/libinjection_html5.c +847 -0
- data/lua-hooks/ext/libinjection/libinjection_html5.h +54 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli.c +2301 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli.h +295 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli_data.h +9349 -0
- data/lua-hooks/ext/libinjection/libinjection_xss.c +531 -0
- data/lua-hooks/ext/libinjection/libinjection_xss.h +21 -0
- data/lua-hooks/ext/libinjection/lualib.c +109 -0
- data/lua-hooks/ext/lpeg/HISTORY +90 -0
- data/lua-hooks/ext/lpeg/lpcap.c +537 -0
- data/lua-hooks/ext/lpeg/lpcap.h +43 -0
- data/lua-hooks/ext/lpeg/lpcode.c +986 -0
- data/lua-hooks/ext/lpeg/lpcode.h +34 -0
- data/lua-hooks/ext/lpeg/lpeg-128.gif +0 -0
- data/lua-hooks/ext/lpeg/lpeg.html +1429 -0
- data/lua-hooks/ext/lpeg/lpprint.c +244 -0
- data/lua-hooks/ext/lpeg/lpprint.h +35 -0
- data/lua-hooks/ext/lpeg/lptree.c +1238 -0
- data/lua-hooks/ext/lpeg/lptree.h +77 -0
- data/lua-hooks/ext/lpeg/lptypes.h +149 -0
- data/lua-hooks/ext/lpeg/lpvm.c +355 -0
- data/lua-hooks/ext/lpeg/lpvm.h +58 -0
- data/lua-hooks/ext/lpeg/makefile +55 -0
- data/lua-hooks/ext/lpeg/re.html +498 -0
- data/lua-hooks/ext/lpeg/test.lua +1409 -0
- data/lua-hooks/ext/lua-cmsgpack/CMakeLists.txt +45 -0
- data/lua-hooks/ext/lua-cmsgpack/README.md +115 -0
- data/lua-hooks/ext/lua-cmsgpack/lua_cmsgpack.c +957 -0
- data/lua-hooks/ext/lua-cmsgpack/test.lua +570 -0
- data/lua-hooks/ext/lua-snapshot/LICENSE +7 -0
- data/lua-hooks/ext/lua-snapshot/Makefile +12 -0
- data/lua-hooks/ext/lua-snapshot/README.md +18 -0
- data/lua-hooks/ext/lua-snapshot/dump.lua +15 -0
- data/lua-hooks/ext/lua-snapshot/snapshot.c +455 -0
- data/lua-hooks/ext/lua/COPYRIGHT +34 -0
- data/lua-hooks/ext/lua/lapi.c +1087 -0
- data/lua-hooks/ext/lua/lapi.h +16 -0
- data/lua-hooks/ext/lua/lauxlib.c +652 -0
- data/lua-hooks/ext/lua/lauxlib.h +174 -0
- data/lua-hooks/ext/lua/lbaselib.c +659 -0
- data/lua-hooks/ext/lua/lcode.c +831 -0
- data/lua-hooks/ext/lua/lcode.h +76 -0
- data/lua-hooks/ext/lua/ldblib.c +398 -0
- data/lua-hooks/ext/lua/ldebug.c +638 -0
- data/lua-hooks/ext/lua/ldebug.h +33 -0
- data/lua-hooks/ext/lua/ldo.c +519 -0
- data/lua-hooks/ext/lua/ldo.h +57 -0
- data/lua-hooks/ext/lua/ldump.c +164 -0
- data/lua-hooks/ext/lua/lfunc.c +174 -0
- data/lua-hooks/ext/lua/lfunc.h +34 -0
- data/lua-hooks/ext/lua/lgc.c +710 -0
- data/lua-hooks/ext/lua/lgc.h +110 -0
- data/lua-hooks/ext/lua/linit.c +38 -0
- data/lua-hooks/ext/lua/liolib.c +556 -0
- data/lua-hooks/ext/lua/llex.c +463 -0
- data/lua-hooks/ext/lua/llex.h +81 -0
- data/lua-hooks/ext/lua/llimits.h +128 -0
- data/lua-hooks/ext/lua/lmathlib.c +263 -0
- data/lua-hooks/ext/lua/lmem.c +86 -0
- data/lua-hooks/ext/lua/lmem.h +49 -0
- data/lua-hooks/ext/lua/loadlib.c +705 -0
- data/lua-hooks/ext/lua/loadlib_rel.c +760 -0
- data/lua-hooks/ext/lua/lobject.c +214 -0
- data/lua-hooks/ext/lua/lobject.h +381 -0
- data/lua-hooks/ext/lua/lopcodes.c +102 -0
- data/lua-hooks/ext/lua/lopcodes.h +268 -0
- data/lua-hooks/ext/lua/loslib.c +243 -0
- data/lua-hooks/ext/lua/lparser.c +1339 -0
- data/lua-hooks/ext/lua/lparser.h +82 -0
- data/lua-hooks/ext/lua/lstate.c +214 -0
- data/lua-hooks/ext/lua/lstate.h +169 -0
- data/lua-hooks/ext/lua/lstring.c +111 -0
- data/lua-hooks/ext/lua/lstring.h +31 -0
- data/lua-hooks/ext/lua/lstrlib.c +871 -0
- data/lua-hooks/ext/lua/ltable.c +588 -0
- data/lua-hooks/ext/lua/ltable.h +40 -0
- data/lua-hooks/ext/lua/ltablib.c +287 -0
- data/lua-hooks/ext/lua/ltm.c +75 -0
- data/lua-hooks/ext/lua/ltm.h +54 -0
- data/lua-hooks/ext/lua/lua.c +392 -0
- data/lua-hooks/ext/lua/lua.def +131 -0
- data/lua-hooks/ext/lua/lua.h +388 -0
- data/lua-hooks/ext/lua/lua.rc +28 -0
- data/lua-hooks/ext/lua/lua_dll.rc +26 -0
- data/lua-hooks/ext/lua/luac.c +200 -0
- data/lua-hooks/ext/lua/luac.rc +1 -0
- data/lua-hooks/ext/lua/luaconf.h +763 -0
- data/lua-hooks/ext/lua/luaconf.h.in +724 -0
- data/lua-hooks/ext/lua/luaconf.h.orig +763 -0
- data/lua-hooks/ext/lua/lualib.h +53 -0
- data/lua-hooks/ext/lua/lundump.c +227 -0
- data/lua-hooks/ext/lua/lundump.h +36 -0
- data/lua-hooks/ext/lua/lvm.c +767 -0
- data/lua-hooks/ext/lua/lvm.h +36 -0
- data/lua-hooks/ext/lua/lzio.c +82 -0
- data/lua-hooks/ext/lua/lzio.h +67 -0
- data/lua-hooks/ext/lua/print.c +227 -0
- data/lua-hooks/ext/luautf8/README.md +152 -0
- data/lua-hooks/ext/luautf8/lutf8lib.c +1274 -0
- data/lua-hooks/ext/luautf8/unidata.h +3064 -0
- data/lua-hooks/lib/boot.lua +254 -0
- data/lua-hooks/lib/encode.lua +4 -0
- data/lua-hooks/lib/lexers/LICENSE +21 -0
- data/lua-hooks/lib/lexers/bash.lua +134 -0
- data/lua-hooks/lib/lexers/bash_dqstr.lua +62 -0
- data/lua-hooks/lib/lexers/css.lua +216 -0
- data/lua-hooks/lib/lexers/html.lua +106 -0
- data/lua-hooks/lib/lexers/javascript.lua +68 -0
- data/lua-hooks/lib/lexers/lexer.lua +1575 -0
- data/lua-hooks/lib/lexers/markers.lua +33 -0
- metadata +308 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
require_relative "../context"
|
|
2
|
+
require_relative "active_record_relation.rb"
|
|
3
|
+
|
|
4
|
+
module Immunio
|
|
5
|
+
# Since every value that will be escaped is very likely to be param passed to a SQL query,
|
|
6
|
+
# we hook to the method escaping the values.
|
|
7
|
+
#
|
|
8
|
+
# Params are then sent to the QueryTracker which will take care of matching the params to the query.
|
|
9
|
+
module QuotingHooks
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
alias_method_chain :quote, :immunio if method_defined? :quote
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
IGNORED_TYPES = [TrueClass, FalseClass, NilClass, Fixnum, Bignum, Float].freeze
|
|
17
|
+
|
|
18
|
+
def quote_with_immunio(value, column = nil)
|
|
19
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
|
20
|
+
if column
|
|
21
|
+
column_name = column.name
|
|
22
|
+
else
|
|
23
|
+
column_name = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Ignored empty strings and values that can't contain injections.
|
|
27
|
+
unless value.blank? || IGNORED_TYPES.include?(value.class)
|
|
28
|
+
QueryTracker.instance.add_param column_name, value.to_s, object_id
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
|
32
|
+
quote_without_immunio(value, column)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# There is one place where a statement may be quoted without going through the
|
|
39
|
+
# quote method. This occurs when a where statement is given an array, like:
|
|
40
|
+
#
|
|
41
|
+
# Users.where(["email LIKE %s", "bob@example.com"])
|
|
42
|
+
#
|
|
43
|
+
# The first value is a sprintf format string and the rest are values
|
|
44
|
+
# interpolated into it. This triggers a call into sanitize_sql_array, which
|
|
45
|
+
# will pass the values through quote_string, but only if the first value is
|
|
46
|
+
# not a Hash and the statement does not include '?' placeholders. Otherwise,
|
|
47
|
+
# different interpolation and quoting mechanisms are used.
|
|
48
|
+
#
|
|
49
|
+
# The above has been verified to be the case from Rails 3.0 to 4.2.
|
|
50
|
+
module SanitizeHooks
|
|
51
|
+
extend ActiveSupport::Concern
|
|
52
|
+
|
|
53
|
+
included do |base|
|
|
54
|
+
base.class_eval do
|
|
55
|
+
def sanitize_sql_array_with_immunio(ary)
|
|
56
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
|
57
|
+
statement, *values = ary
|
|
58
|
+
|
|
59
|
+
# Check if rails will use some other mechanism for quoting
|
|
60
|
+
unless (values.first.is_a?(Hash) && statement =~ /:\w+/) ||
|
|
61
|
+
(statement.include?('?')) ||
|
|
62
|
+
(statement.blank?)
|
|
63
|
+
# Rails is going to use quote_string, so handle parameters
|
|
64
|
+
values.each { |value| QueryTracker.instance.add_param nil, value, connection.object_id }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
|
68
|
+
sanitize_sql_array_without_immunio ary
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
alias_method_chain :sanitize_sql_array, :immunio
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
module ArelToSqlHooks
|
|
78
|
+
extend ActiveSupport::Concern
|
|
79
|
+
|
|
80
|
+
included do
|
|
81
|
+
alias_method_chain :accept, :immunio if method_defined? :accept
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def accept_with_immunio(object, *args)
|
|
85
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
|
86
|
+
visitor = ArelNodeVisitor.new(@connection.object_id)
|
|
87
|
+
visitor.accept(object)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
accept_without_immunio(object, *args)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Arel AST visitor to collect params and modifiers. Based on Arel::Visitors::DepthFirst.
|
|
95
|
+
#
|
|
96
|
+
# Whenever a query is built in Rails, a tree of Ruby objects (AST) is built to represent that query.
|
|
97
|
+
# Arel (the engine building this tree) uses a Visitor to build the SQL statement represented by tree.
|
|
98
|
+
# We use the same base class as Arel, but instead of building some SQL, we track the params and modifiers.
|
|
99
|
+
#
|
|
100
|
+
# See http://en.wikipedia.org/wiki/Visitor_pattern
|
|
101
|
+
class ArelNodeVisitor < Arel::Visitors::Visitor
|
|
102
|
+
# Only accepts statements to avoid duplicates in params if branches are visited multiple times.
|
|
103
|
+
VISITABLES = [Arel::Nodes::SelectStatement, Arel::Nodes::InsertStatement,
|
|
104
|
+
Arel::Nodes::UpdateStatement, Arel::Nodes::DeleteStatement].freeze
|
|
105
|
+
|
|
106
|
+
IGNORED_EXPRESSIONS = ["", "*", "1 AS one"].freeze
|
|
107
|
+
|
|
108
|
+
# Copied from Arel::Visitors::Visitor to ensure the cached dispatch table isn't shared with
|
|
109
|
+
# other Arel visitors under Rails 3.2.
|
|
110
|
+
DISPATCH = Hash.new do |hash, klass|
|
|
111
|
+
hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def initialize(connection_id)
|
|
115
|
+
@connection_id = connection_id
|
|
116
|
+
super()
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Entry point into the visitor.
|
|
120
|
+
def accept(object)
|
|
121
|
+
if VISITABLES.include?(object.class)
|
|
122
|
+
visit object, {}
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def dispatch
|
|
127
|
+
DISPATCH
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
# Backported from Arel 6 Visitor::Reduce, as Arel 3 (Used in Rails 3.2) doesn't have the Reduce visitor.
|
|
132
|
+
def visit object, collector, opts={}
|
|
133
|
+
send dispatch[object.class], object, collector, opts
|
|
134
|
+
rescue NoMethodError => e
|
|
135
|
+
raise e if respond_to?(dispatch[object.class], true)
|
|
136
|
+
superklass = object.class.ancestors.find { |klass|
|
|
137
|
+
respond_to?(dispatch[klass], true)
|
|
138
|
+
}
|
|
139
|
+
raise(TypeError, "Cannot visit #{object.class}") unless superklass
|
|
140
|
+
dispatch[object.class] = dispatch[superklass]
|
|
141
|
+
retry
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# When an unsafe node is visited. Track the param or modifier.
|
|
145
|
+
def unsafe(o, context, _opts)
|
|
146
|
+
unless o.class == String
|
|
147
|
+
QueryTracker.instance.add_ast_data o.class.name, @connection_id
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if IGNORED_EXPRESSIONS.include?(o)
|
|
151
|
+
# Ignore
|
|
152
|
+
elsif context[:modifier]
|
|
153
|
+
QueryTracker.instance.add_modifier context[:modifier], o.to_s, @connection_id
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
alias :visit_Arel_Nodes_SqlLiteral :unsafe
|
|
157
|
+
alias :visit_String :unsafe
|
|
158
|
+
|
|
159
|
+
# We use a context (second argument of visit) to keep track of where we are in the AST.
|
|
160
|
+
# The following methods update the contexts based on the branch of the AST being visited
|
|
161
|
+
# and recursively visit the children nodes.
|
|
162
|
+
|
|
163
|
+
def visit_Arel_Nodes_SelectCore(o, context, _opts)
|
|
164
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
165
|
+
|
|
166
|
+
visit o.projections, modifier: :select
|
|
167
|
+
visit o.source, modifier: :from
|
|
168
|
+
visit o.wheres, modifier: :where
|
|
169
|
+
visit o.groups, modifier: :group
|
|
170
|
+
visit o.windows, context
|
|
171
|
+
visit o.having, modifier: :having
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def visit_Arel_Nodes_SelectStatement(o, context, _opts)
|
|
175
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
176
|
+
|
|
177
|
+
visit o.cores, context
|
|
178
|
+
visit o.orders, modifier: :order
|
|
179
|
+
visit o.limit, modifier: :limit
|
|
180
|
+
visit o.lock, modifier: :lock
|
|
181
|
+
visit o.offset, modifier: :offset
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def visit_Arel_Nodes_UpdateStatement(o, context, _opts)
|
|
185
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
186
|
+
|
|
187
|
+
visit o.relation, context
|
|
188
|
+
visit o.values, context
|
|
189
|
+
visit o.wheres, modifier: :where
|
|
190
|
+
visit o.orders, modifier: :order
|
|
191
|
+
visit o.limit, modifier: :limit
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def visit_Arel_Nodes_DeleteStatement(o, context, _opts)
|
|
195
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
196
|
+
|
|
197
|
+
visit o.relation, context
|
|
198
|
+
visit o.wheres, modifier: :where
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# All other methods bellow are for visiting each node and their children.
|
|
202
|
+
|
|
203
|
+
def unary(o, context, _opts)
|
|
204
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
205
|
+
|
|
206
|
+
visit o.expr, context
|
|
207
|
+
end
|
|
208
|
+
alias :visit_Arel_Nodes_Group :unary
|
|
209
|
+
alias :visit_Arel_Nodes_Grouping :unary
|
|
210
|
+
alias :visit_Arel_Nodes_Having :unary
|
|
211
|
+
alias :visit_Arel_Nodes_Limit :unary
|
|
212
|
+
alias :visit_Arel_Nodes_Not :unary
|
|
213
|
+
alias :visit_Arel_Nodes_Offset :unary
|
|
214
|
+
alias :visit_Arel_Nodes_On :unary
|
|
215
|
+
alias :visit_Arel_Nodes_Ordering :unary
|
|
216
|
+
alias :visit_Arel_Nodes_Ascending :unary
|
|
217
|
+
alias :visit_Arel_Nodes_Descending :unary
|
|
218
|
+
alias :visit_Arel_Nodes_Top :unary
|
|
219
|
+
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
|
|
220
|
+
alias :visit_Arel_Nodes_Lock :unary
|
|
221
|
+
alias :visit_Arel_Nodes_Quoted :unary
|
|
222
|
+
|
|
223
|
+
def function(o, context, _opts)
|
|
224
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
225
|
+
|
|
226
|
+
visit o.expressions, context
|
|
227
|
+
end
|
|
228
|
+
alias :visit_Arel_Nodes_Avg :function
|
|
229
|
+
alias :visit_Arel_Nodes_Exists :function
|
|
230
|
+
alias :visit_Arel_Nodes_Max :function
|
|
231
|
+
alias :visit_Arel_Nodes_Min :function
|
|
232
|
+
alias :visit_Arel_Nodes_Sum :function
|
|
233
|
+
|
|
234
|
+
def visit_Arel_Nodes_NamedFunction(o, context, _opts)
|
|
235
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
236
|
+
|
|
237
|
+
visit o.expressions, context
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def visit_Arel_Nodes_Count(o, context, _opts)
|
|
241
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
242
|
+
|
|
243
|
+
visit o.expressions, context
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def nary(o, context, _opts)
|
|
247
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
248
|
+
|
|
249
|
+
o.children.each { |child| visit child, context }
|
|
250
|
+
end
|
|
251
|
+
alias :visit_Arel_Nodes_And :nary
|
|
252
|
+
|
|
253
|
+
def binary(o, context, _opts)
|
|
254
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
255
|
+
|
|
256
|
+
visit o.left, context
|
|
257
|
+
visit o.right, context
|
|
258
|
+
end
|
|
259
|
+
alias :visit_Arel_Nodes_As :binary
|
|
260
|
+
alias :visit_Arel_Nodes_Assignment :binary
|
|
261
|
+
alias :visit_Arel_Nodes_Between :binary
|
|
262
|
+
alias :visit_Arel_Nodes_DoesNotMatch :binary
|
|
263
|
+
alias :visit_Arel_Nodes_Equality :binary
|
|
264
|
+
alias :visit_Arel_Nodes_FullOuterJoin :binary
|
|
265
|
+
alias :visit_Arel_Nodes_GreaterThan :binary
|
|
266
|
+
alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
|
|
267
|
+
alias :visit_Arel_Nodes_InfixOperation :binary
|
|
268
|
+
alias :visit_Arel_Nodes_JoinSource :binary
|
|
269
|
+
alias :visit_Arel_Nodes_InnerJoin :binary
|
|
270
|
+
alias :visit_Arel_Nodes_LessThan :binary
|
|
271
|
+
alias :visit_Arel_Nodes_LessThanOrEqual :binary
|
|
272
|
+
alias :visit_Arel_Nodes_Matches :binary
|
|
273
|
+
alias :visit_Arel_Nodes_NotEqual :binary
|
|
274
|
+
alias :visit_Arel_Nodes_NotRegexp :binary
|
|
275
|
+
alias :visit_Arel_Nodes_Or :binary
|
|
276
|
+
alias :visit_Arel_Nodes_OuterJoin :binary
|
|
277
|
+
alias :visit_Arel_Nodes_Regexp :binary
|
|
278
|
+
alias :visit_Arel_Nodes_RightOuterJoin :binary
|
|
279
|
+
alias :visit_Arel_Nodes_TableAlias :binary
|
|
280
|
+
alias :visit_Arel_Nodes_Values :binary
|
|
281
|
+
alias :visit_Arel_Nodes_Union :binary
|
|
282
|
+
|
|
283
|
+
# Special case the IN clause node. We don't want to add info about AST
|
|
284
|
+
# nodes in the right side of an IN clause if they are terminal or an
|
|
285
|
+
# array of terminal nodes.
|
|
286
|
+
def visit_Arel_Nodes_In(o, context, _opts)
|
|
287
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
288
|
+
|
|
289
|
+
visit o.left, context
|
|
290
|
+
visit o.right, context, IN_children: true
|
|
291
|
+
end
|
|
292
|
+
alias :visit_Arel_Nodes_NotIn :visit_Arel_Nodes_In
|
|
293
|
+
|
|
294
|
+
def visit_Arel_Nodes_StringJoin(o, context, _opts)
|
|
295
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
296
|
+
|
|
297
|
+
visit o.left, context
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def visit_Arel_Attribute(o, context, _opts)
|
|
301
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
302
|
+
|
|
303
|
+
visit o.relation, context
|
|
304
|
+
end
|
|
305
|
+
alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
|
|
306
|
+
alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
|
|
307
|
+
alias :visit_Arel_Attributes_String :visit_Arel_Attribute
|
|
308
|
+
alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
|
|
309
|
+
alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
|
|
310
|
+
alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
|
|
311
|
+
alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute
|
|
312
|
+
|
|
313
|
+
def visit_Arel_Table(o, _context, _opts)
|
|
314
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def terminal(_o, _context, _opts)
|
|
318
|
+
end
|
|
319
|
+
alias :visit_Arel_Nodes_Node :terminal
|
|
320
|
+
alias :visit_ActiveSupport_Multibyte_Chars :terminal
|
|
321
|
+
alias :visit_ActiveSupport_StringInquirer :terminal
|
|
322
|
+
alias :visit_Symbol :terminal
|
|
323
|
+
alias :visit_Arel_Nodes_Window :terminal
|
|
324
|
+
alias :visit_Arel_Nodes_True :terminal
|
|
325
|
+
alias :visit_Arel_Nodes_False :terminal
|
|
326
|
+
alias :visit_BigDecimal :terminal
|
|
327
|
+
alias :visit_Bignum :terminal
|
|
328
|
+
alias :visit_Class :terminal
|
|
329
|
+
alias :visit_Date :terminal
|
|
330
|
+
alias :visit_DateTime :terminal
|
|
331
|
+
alias :visit_FalseClass :terminal
|
|
332
|
+
alias :visit_Fixnum :terminal
|
|
333
|
+
alias :visit_Float :terminal
|
|
334
|
+
alias :visit_Arel_Nodes_BindParam :terminal
|
|
335
|
+
alias :visit_NilClass :terminal
|
|
336
|
+
alias :visit_Time :terminal
|
|
337
|
+
alias :visit_TrueClass :terminal
|
|
338
|
+
alias :visit_Object :terminal
|
|
339
|
+
|
|
340
|
+
def visit_Arel_Nodes_InsertStatement(o, context, _opts)
|
|
341
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
342
|
+
|
|
343
|
+
visit o.relation, context
|
|
344
|
+
visit o.columns, context
|
|
345
|
+
visit o.values, context
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def visit_Array(o, context, opts)
|
|
349
|
+
unless opts[:IN_children]
|
|
350
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
o.each { |i| visit i, context }
|
|
354
|
+
end
|
|
355
|
+
alias :visit_Set :visit_Array
|
|
356
|
+
|
|
357
|
+
def visit_Hash(o, context, _opts)
|
|
358
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
|
359
|
+
|
|
360
|
+
o.each { |k,v| visit(k, context); visit(v, context) }
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
class QueryTracker
|
|
365
|
+
include Singleton
|
|
366
|
+
|
|
367
|
+
def initialize
|
|
368
|
+
# The data in these hashes represent relations and connections whose
|
|
369
|
+
# lifecycle cannot be easily inferred. A relation could be kept around
|
|
370
|
+
# across multiple HTTP requests, for example. We defined finalizers to
|
|
371
|
+
# clean up data in the hashes when the objects they are linked to are
|
|
372
|
+
# released by the Ruby runtime.
|
|
373
|
+
#
|
|
374
|
+
# Note: Relations have an associated connection and are not accessed by
|
|
375
|
+
# other connections. Connections are associated with a thread and are not
|
|
376
|
+
# accessed by other threads at the same time. Thus, there is no need for
|
|
377
|
+
# thread safety in any of the logic in this class.
|
|
378
|
+
|
|
379
|
+
# Data about a relation. The data inside is stored at different times and
|
|
380
|
+
# must be reset properly:
|
|
381
|
+
#
|
|
382
|
+
# * params and relation_data: Added when ActiveRecord::Relation API calls
|
|
383
|
+
# are made, like #where. Should never be reset for a given relation.
|
|
384
|
+
# * ast_data and modifiers: Added when a relation is converted
|
|
385
|
+
# into a SQL query statement. Should be reset after every query
|
|
386
|
+
# execution.
|
|
387
|
+
@relation_data = Hash.new do |relation_data, relation_id|
|
|
388
|
+
# This should never happen, but if it does it's a sign of an impending
|
|
389
|
+
# memory leak. Log it, but just let it happen as it would be hard to
|
|
390
|
+
# handle elsewhere if we did not set up the data.
|
|
391
|
+
unless ObjectSpace._id2ref(relation_id).is_a? ActiveRecord::Relation
|
|
392
|
+
name = if ObjectSpace._id2ref(relation_id).is_a? Class
|
|
393
|
+
"#{ObjectSpace._id2ref(relation_id).name} Class"
|
|
394
|
+
else
|
|
395
|
+
"#{ObjectSpace._id2ref(relation_id).class.name} Instance"
|
|
396
|
+
end # rubocop:disable Lint/EndAlignment
|
|
397
|
+
Immunio.logger.warn {"Creating relation data for non-relation: #{name}"}
|
|
398
|
+
Immunio.logger.debug {"Call stack:\n#{caller.join "\n"}"}
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# NOTE: If you hold a reference to the relation here, like say:
|
|
402
|
+
#
|
|
403
|
+
# relation = ObjectSpace._id2ref(relation_id)
|
|
404
|
+
#
|
|
405
|
+
# the scope for the block will hold the relation and it will never be
|
|
406
|
+
# released.
|
|
407
|
+
ObjectSpace.define_finalizer ObjectSpace._id2ref(relation_id),
|
|
408
|
+
self.class.finalize_relation(relation_id)
|
|
409
|
+
|
|
410
|
+
relation_data[relation_id] = {
|
|
411
|
+
params: {},
|
|
412
|
+
relation_data: [],
|
|
413
|
+
ast_data: [],
|
|
414
|
+
modifiers: Hash.new do |modifiers, type|
|
|
415
|
+
modifiers[type] = []
|
|
416
|
+
end
|
|
417
|
+
}
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Stacks of relations for each connection. Used to find the appropriate
|
|
421
|
+
# relation for a connection when a query is executed. A stack is used
|
|
422
|
+
# because some relation methods create new relations and call other
|
|
423
|
+
# relation methods on the new relations.
|
|
424
|
+
@relations = Hash.new do |relations, connection_id|
|
|
425
|
+
connection = ObjectSpace._id2ref(connection_id)
|
|
426
|
+
ObjectSpace.define_finalizer(connection, self.class.finalize_connection(connection_id))
|
|
427
|
+
|
|
428
|
+
relations[connection_id] = []
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Last spawned relations for connections. Used for a hack to propagate
|
|
432
|
+
# params to the right relation in Rails 3.
|
|
433
|
+
@last_spawned_relations = {}
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Delete a relation record when the relation object is released.
|
|
437
|
+
def self.finalize_relation(relation_id)
|
|
438
|
+
proc do
|
|
439
|
+
relation_data = instance.instance_variable_get(:@relation_data)
|
|
440
|
+
|
|
441
|
+
# Check if key exists, delete will call the default value block if not
|
|
442
|
+
relation_data.delete(relation_id) if relation_data.has_key? relation_id
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Delete a connection record when the connection object is released.
|
|
447
|
+
def self.finalize_connection(connection_id)
|
|
448
|
+
proc do
|
|
449
|
+
relations = instance.instance_variable_get(:@relations)
|
|
450
|
+
|
|
451
|
+
# Check if key exists, delete will call the default value block if not
|
|
452
|
+
relations.delete(connection_id) if relations.has_key? connection_id
|
|
453
|
+
|
|
454
|
+
instance.instance_variable_get(:@last_spawned_relations).delete connection_id
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Push a relation onto the stack for its connection
|
|
459
|
+
def push_relation(relation)
|
|
460
|
+
@relations[relation.connection.object_id] << relation.object_id
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Pop a relation off the stack for its connection
|
|
464
|
+
def pop_relation(relation)
|
|
465
|
+
popped = @relations[relation.connection.object_id].pop
|
|
466
|
+
unless popped == relation.object_id
|
|
467
|
+
Immunio.logger.warn {"Popped wrong relation, expected: #{relation}, popped: #{popped}"}
|
|
468
|
+
Immunio.logger.debug {"Call stack:\n#{caller.join "\n"}"}
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Called when a relation is cloned. The data for the new relation must also
|
|
473
|
+
# be copied from the old relation.
|
|
474
|
+
def spawn_relation(orig, new)
|
|
475
|
+
orig_id = orig.object_id
|
|
476
|
+
new_id = new.object_id
|
|
477
|
+
|
|
478
|
+
# If we weren't tracking the original relation, don't bother setting up
|
|
479
|
+
# the new relation yet.
|
|
480
|
+
if @relation_data.has_key? orig_id
|
|
481
|
+
# ast_data and modifiers should be empty, but we must clone modifiers to
|
|
482
|
+
# get the initializer.
|
|
483
|
+
@relation_data[new_id] = {
|
|
484
|
+
params: @relation_data[orig_id][:params].clone,
|
|
485
|
+
relation_data: @relation_data[orig_id][:relation_data].clone,
|
|
486
|
+
ast_data: @relation_data[orig_id][:ast_data] = [],
|
|
487
|
+
modifiers: @relation_data[orig_id][:modifiers].clone
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
# The default block for the @relation_data hash isn't called when
|
|
491
|
+
# assigning a value to a new key. We must set up the finalizer manually.
|
|
492
|
+
ObjectSpace.define_finalizer(new, self.class.finalize_relation(new_id))
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Save the last spawned relation for a hack for storing params from #where
|
|
496
|
+
# and #having.
|
|
497
|
+
@last_spawned_relations[new.connection.object_id] = new_id
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Retrieve the last spawned relation for the connection. This is an ugly
|
|
501
|
+
# hack for a poor implementation of the #where and #having methods in Rails
|
|
502
|
+
# 3.
|
|
503
|
+
def last_spawned_relation(connection)
|
|
504
|
+
ObjectSpace._id2ref @last_spawned_relations[connection.object_id]
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Called when two relations are merged. The data for the other relation must
|
|
508
|
+
# be copied into the current relation. AST data and modifiers should be
|
|
509
|
+
# empty and don't need to be copied.
|
|
510
|
+
def merge_relations(relation, other)
|
|
511
|
+
params = @relation_data[relation.object_id][:params]
|
|
512
|
+
other_params = @relation_data[other.object_id][:params]
|
|
513
|
+
|
|
514
|
+
other_params.each_pair do |name, value|
|
|
515
|
+
# Update numeric ID for current relation if name is an integer.
|
|
516
|
+
name = params.size.to_s if name.to_i.to_s == name
|
|
517
|
+
|
|
518
|
+
params[name] = value
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
other_data = @relation_data[other.object_id][:relation_data]
|
|
522
|
+
@relation_data[relation.object_id][:relation_data] += other_data
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Add relation API context data to the relation.
|
|
526
|
+
def add_relation_data(relation, data)
|
|
527
|
+
@relation_data[relation.object_id][:relation_data] << data
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Add a parameter to the current relation for the connection. If the
|
|
531
|
+
# relation is copied or merged into another relation, the param will also
|
|
532
|
+
# be copied.
|
|
533
|
+
def add_param(name, value, connection_id)
|
|
534
|
+
relation_id = @relations[connection_id].last
|
|
535
|
+
|
|
536
|
+
# This can occur if the query statement isn't generated by the app but by
|
|
537
|
+
# ActiveRecord itself.
|
|
538
|
+
return unless relation_id
|
|
539
|
+
|
|
540
|
+
params = @relation_data[relation_id][:params]
|
|
541
|
+
|
|
542
|
+
# If no name given, use index.
|
|
543
|
+
if name.nil?
|
|
544
|
+
name = params.size.to_s
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
Immunio.logger.debug "Adding ActiveRecord SQL param to relation #{relation_id} (name: #{name}, value: #{value})"
|
|
548
|
+
|
|
549
|
+
params[name] = value
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Add a modifier to the current relation for the connection. This only
|
|
553
|
+
# occurs during conversion of a relation to SQL statement, so modifiers are
|
|
554
|
+
# never copied from one relation to another.
|
|
555
|
+
def add_modifier(type, value, connection_id)
|
|
556
|
+
relation_id = @relations[connection_id].last
|
|
557
|
+
|
|
558
|
+
# This can occur if the query statement isn't generated by the app but by
|
|
559
|
+
# ActiveRecord itself.
|
|
560
|
+
return unless relation_id
|
|
561
|
+
|
|
562
|
+
@relation_data[relation_id][:modifiers][type] << value
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Add data about an Arel AST node to the context data for the connection.
|
|
566
|
+
# This only occurs during conversion of a relation to SQL statement, so AST
|
|
567
|
+
# context data is never copied from one relation to another.
|
|
568
|
+
def add_ast_data(ast_node_name, connection_id)
|
|
569
|
+
relation_id = @relations[connection_id].last
|
|
570
|
+
|
|
571
|
+
# This can occur if the query statement was cached and there's no relation
|
|
572
|
+
# associated with the connection. That's ok here, though, because there's
|
|
573
|
+
# such a limited number of cacheable statement structures that we don't
|
|
574
|
+
# need AST info to differentiate between queries with the same stack
|
|
575
|
+
# trace.
|
|
576
|
+
return unless relation_id
|
|
577
|
+
|
|
578
|
+
@relation_data[relation_id][:ast_data] << "Arel AST visited node: #{ast_node_name}"
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# Evaluate a SQL call. This occurs after Arel AST conversion of a relation
|
|
582
|
+
# to a statement.
|
|
583
|
+
def call(payload)
|
|
584
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
|
585
|
+
Immunio.logger.debug "New ActiveRecord SQL query: #{payload}"
|
|
586
|
+
|
|
587
|
+
connection_id = payload[:connection_id]
|
|
588
|
+
|
|
589
|
+
relation_id = @relations[connection_id].last
|
|
590
|
+
|
|
591
|
+
if should_ignore? payload[:sql]
|
|
592
|
+
Immunio.logger.debug "Ignoring query as it was generated by ActiveRecord itself (#{payload[:sql]})"
|
|
593
|
+
return
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
if relation_id
|
|
597
|
+
# Note: If a relation is released between when it is converted to a
|
|
598
|
+
# SQL statement and now, we would lose the data and additionally leak
|
|
599
|
+
# an empty entry in the @relation_data hash. I don't believe this is
|
|
600
|
+
# possible due to how we wrap things, but there's no explicit
|
|
601
|
+
# guarantee.
|
|
602
|
+
relation_data = @relation_data[relation_id]
|
|
603
|
+
params = relation_data[:params].clone
|
|
604
|
+
context_data = (relation_data[:relation_data] + relation_data[:ast_data]).join "\n"
|
|
605
|
+
|
|
606
|
+
# modifiers must be cloned because it will be cleared when the
|
|
607
|
+
# relation is reset.
|
|
608
|
+
modifiers = relation_data[:modifiers].clone
|
|
609
|
+
else
|
|
610
|
+
params = {}
|
|
611
|
+
context_data = nil
|
|
612
|
+
modifiers = {}
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# Merge bound values
|
|
616
|
+
question_marks = 0
|
|
617
|
+
payload[:binds].each do |(column, value)|
|
|
618
|
+
if column.nil?
|
|
619
|
+
params["?:#{question_marks}"] = value.to_s
|
|
620
|
+
question_marks = question_marks + 1
|
|
621
|
+
else
|
|
622
|
+
# When using the activerecord-sqlserver-adapter gem, the "column" is
|
|
623
|
+
# the actual param name.
|
|
624
|
+
name = column.respond_to?(:name) ? column.name : column.to_s
|
|
625
|
+
params[name] = value.to_s
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
strict_context, loose_context, stack = Immunio::Context.context context_data
|
|
630
|
+
|
|
631
|
+
# Send in additional_context_data for debugging purposes
|
|
632
|
+
Immunio.run_hook! "active_record", "sql_execute", sql: payload[:sql],
|
|
633
|
+
connection_uuid: connection_id.to_s,
|
|
634
|
+
params: params,
|
|
635
|
+
modifiers: modifiers,
|
|
636
|
+
context_key: strict_context,
|
|
637
|
+
loose_context_key: loose_context,
|
|
638
|
+
stack: stack,
|
|
639
|
+
additional_context_data: context_data
|
|
640
|
+
|
|
641
|
+
reset relation_id
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# Reset per-execution data for a relation.
|
|
646
|
+
def reset(relation_id)
|
|
647
|
+
return unless relation_id
|
|
648
|
+
|
|
649
|
+
[:ast_data, :modifiers].each do |type|
|
|
650
|
+
@relation_data[relation_id][type].clear
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
private
|
|
655
|
+
IGNORE_START_WITH = ['PRAGMA', 'SHOW', 'SAVEPOINT', 'RELEASE', 'ROLLBACK']
|
|
656
|
+
IGNORE_SQL = /^(begin|commit|rollback)(?: transaction)$/i
|
|
657
|
+
def should_ignore?(sql)
|
|
658
|
+
# Ignore queries generated by ActiveRecord.
|
|
659
|
+
return sql.start_with?(*IGNORE_START_WITH) || IGNORE_SQL =~ sql
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Hook into the SQL query execution methods of Rails.
|
|
664
|
+
# Since all executed queries inside Rails are logged, we hook into the `log` method to catch them all.
|
|
665
|
+
module QueryExecutionHooks
|
|
666
|
+
extend ActiveSupport::Concern
|
|
667
|
+
|
|
668
|
+
included do
|
|
669
|
+
alias_method_chain :log, :immunio if method_defined? :log
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def log_with_immunio(sql, name = "SQL", binds = [], *args)
|
|
673
|
+
QueryTracker.instance.call sql: sql,
|
|
674
|
+
connection_id: object_id,
|
|
675
|
+
binds: binds
|
|
676
|
+
|
|
677
|
+
# Log and execute the query
|
|
678
|
+
log_without_immunio(sql, name, binds, *args) { yield }
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
# Hook into quoting methods at the highest level possible in the ancestors chain.
|
|
684
|
+
# In case the quote methods were overridden in a child class.
|
|
685
|
+
module ActiveRecord::ConnectionAdapters
|
|
686
|
+
if defined? Mysql2Adapter
|
|
687
|
+
Mysql2Adapter.send :include, Immunio::QuotingHooks
|
|
688
|
+
elsif defined? MysqlAdapter
|
|
689
|
+
MysqlAdapter.send :include, Immunio::QuotingHooks
|
|
690
|
+
end
|
|
691
|
+
if defined? PostgreSQLAdapter
|
|
692
|
+
PostgreSQLAdapter.send :include, Immunio::QuotingHooks
|
|
693
|
+
end
|
|
694
|
+
if defined? SQLite3Adapter
|
|
695
|
+
SQLite3Adapter.send :include, Immunio::QuotingHooks
|
|
696
|
+
elsif defined? SQLiteAdapter
|
|
697
|
+
SQLiteAdapter.send :include, Immunio::QuotingHooks
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
module ActiveRecord::Sanitization
|
|
702
|
+
ClassMethods.send :include, Immunio::SanitizeHooks
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
Arel::Visitors::ToSql.send :include, Immunio::ArelToSqlHooks
|
|
706
|
+
|
|
707
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.send :include, Immunio::QueryExecutionHooks
|