sqreen 0.1.0.pre → 0.7.01461158029

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CODE_OF_CONDUCT.md +22 -0
  3. data/README.md +77 -0
  4. data/Rakefile +40 -0
  5. data/lib/sqreen.rb +67 -0
  6. data/lib/sqreen/binding_accessor.rb +184 -0
  7. data/lib/sqreen/ca.crt +72 -0
  8. data/lib/sqreen/callback_tree.rb +78 -0
  9. data/lib/sqreen/callbacks.rb +120 -0
  10. data/lib/sqreen/capped_queue.rb +23 -0
  11. data/lib/sqreen/condition_evaluator.rb +169 -0
  12. data/lib/sqreen/conditionable.rb +50 -0
  13. data/lib/sqreen/configuration.rb +151 -0
  14. data/lib/sqreen/context.rb +22 -0
  15. data/lib/sqreen/deliveries/batch.rb +80 -0
  16. data/lib/sqreen/deliveries/simple.rb +36 -0
  17. data/lib/sqreen/detect.rb +14 -0
  18. data/lib/sqreen/detect/shell_injection.rb +61 -0
  19. data/lib/sqreen/detect/sql_injection.rb +115 -0
  20. data/lib/sqreen/event.rb +16 -0
  21. data/lib/sqreen/events/attack.rb +60 -0
  22. data/lib/sqreen/events/remote_exception.rb +53 -0
  23. data/lib/sqreen/exception.rb +31 -0
  24. data/lib/sqreen/frameworks.rb +40 -0
  25. data/lib/sqreen/frameworks/generic.rb +243 -0
  26. data/lib/sqreen/frameworks/rails.rb +155 -0
  27. data/lib/sqreen/frameworks/rails3.rb +36 -0
  28. data/lib/sqreen/frameworks/sinatra.rb +34 -0
  29. data/lib/sqreen/frameworks/sqreen_test.rb +26 -0
  30. data/lib/sqreen/instrumentation.rb +504 -0
  31. data/lib/sqreen/log.rb +116 -0
  32. data/lib/sqreen/metrics.rb +6 -0
  33. data/lib/sqreen/metrics/average.rb +39 -0
  34. data/lib/sqreen/metrics/base.rb +41 -0
  35. data/lib/sqreen/metrics/collect.rb +22 -0
  36. data/lib/sqreen/metrics/sum.rb +20 -0
  37. data/lib/sqreen/metrics_store.rb +94 -0
  38. data/lib/sqreen/parsers/sql.rb +98 -0
  39. data/lib/sqreen/parsers/sql_tokenizer.rb +266 -0
  40. data/lib/sqreen/parsers/unix.rb +110 -0
  41. data/lib/sqreen/payload_creator.rb +132 -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 +82 -0
  47. data/lib/sqreen/rule_attributes.rb +25 -0
  48. data/lib/sqreen/rule_callback.rb +97 -0
  49. data/lib/sqreen/rules.rb +116 -0
  50. data/lib/sqreen/rules_callbacks.rb +29 -0
  51. data/lib/sqreen/rules_callbacks/binding_accessor_metrics.rb +79 -0
  52. data/lib/sqreen/rules_callbacks/count_http_codes.rb +18 -0
  53. data/lib/sqreen/rules_callbacks/crawler_user_agent_matches.rb +24 -0
  54. data/lib/sqreen/rules_callbacks/crawler_user_agent_matches_metrics.rb +25 -0
  55. data/lib/sqreen/rules_callbacks/execjs.rb +136 -0
  56. data/lib/sqreen/rules_callbacks/headers_insert.rb +20 -0
  57. data/lib/sqreen/rules_callbacks/inspect_rule.rb +20 -0
  58. data/lib/sqreen/rules_callbacks/matcher_rule.rb +103 -0
  59. data/lib/sqreen/rules_callbacks/rails_parameters.rb +14 -0
  60. data/lib/sqreen/rules_callbacks/record_request_context.rb +23 -0
  61. data/lib/sqreen/rules_callbacks/reflected_xss.rb +40 -0
  62. data/lib/sqreen/rules_callbacks/regexp_rule.rb +36 -0
  63. data/lib/sqreen/rules_callbacks/shell.rb +33 -0
  64. data/lib/sqreen/rules_callbacks/shell_env.rb +32 -0
  65. data/lib/sqreen/rules_callbacks/sql.rb +41 -0
  66. data/lib/sqreen/rules_callbacks/system_shell.rb +25 -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 +142 -0
  70. data/lib/sqreen/runner.rb +312 -0
  71. data/lib/sqreen/runtime_infos.rb +127 -0
  72. data/lib/sqreen/session.rb +340 -0
  73. data/lib/sqreen/stats.rb +18 -0
  74. data/lib/sqreen/version.rb +6 -0
  75. metadata +95 -34
@@ -0,0 +1,266 @@
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
+ # Inspired by
5
+ # https://practicingruby.com/articles/parsing-json-the-hard-way
6
+
7
+ require 'strscan'
8
+ require 'sqreen/exception'
9
+
10
+ module Sqreen
11
+ module Parsers
12
+ class ParsingException < ::Sqreen::Exception
13
+ end
14
+
15
+ # Forward declaration
16
+ class SQLTokenizer; end
17
+
18
+ # https://www.sqlite.org/lang_select.html
19
+ # https://www.sqlite.org/lang_expr.html
20
+ class SQLiteTokenizer < SQLTokenizer
21
+ def initialize(_db_infos,
22
+ _backslash_escape = false,
23
+ _has_hex = false)
24
+ @name = 'SQLite'
25
+ end
26
+ end
27
+
28
+ # How string litterals work in MySQL
29
+ # https://dev.mysql.com/doc/refman/5.0/en/string-literals.html
30
+ # https://dev.mysql.com/doc/refman/5.0/en/number-literals.html
31
+ #
32
+ # Configuration modifiers:
33
+ # sqlmode: The backslash charcter does not perform escapes
34
+ # https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_ansi_quotes
35
+ # sqlmode: No backslash escapes
36
+ # https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_backslash_escapes
37
+
38
+ # The double quote cannot be a string delimiter
39
+ #
40
+ # Double quote strangeness:
41
+ #
42
+ # mysql> SELECT 'hello', '"hello"', '""hello""', 'hel''lo', '\'hello';
43
+ # +-------+---------+-----------+--------+--------+
44
+ # | hello | "hello" | ""hello"" | hel'lo | 'hello |
45
+ # +-------+---------+-----------+--------+--------+
46
+ #
47
+ # mysql> SELECT "hello", "'hello'", "''hello''", "hel""lo", "\"hello";
48
+ # +-------+---------+-----------+--------+--------+
49
+ # | hello | 'hello' | ''hello'' | hel"lo | "hello |
50
+ # +-------+---------+-----------+--------+--------+
51
+ class MySQLTokenizer < SQLTokenizer
52
+ def initialize(db_infos,
53
+ backslash_escape = true,
54
+ has_hex = true)
55
+ super
56
+ @name = 'MySQL'
57
+ end
58
+ end
59
+
60
+ class SQLTokenizer
61
+ NUMBER = /[-+]*(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/
62
+ COMMAND = /[a-zA-Z_][a-zA-Z_0-9]*/
63
+ HEX = /0x[0-9a-fA-F]+/
64
+
65
+ binary_operators_list = %w(\+ - ~ \! <=> <> != <= = >= < > := % NOT OR \|\| AND IS IN BETWEEN LIKE REGEXP)
66
+ binary_operators_list << 'SOUNDS LIKE' << 'IS NULL' << 'IS NOT NULL'
67
+ BINARY_OPERATORS_LIST = binary_operators_list.freeze
68
+
69
+ BITS_OPERATORS_LIST = %w(\| & << >> \+ - \* / DIV MOD % ^).freeze
70
+
71
+ BINARY_OPERATORS = Regexp.new(BINARY_OPERATORS_LIST.join('|'))
72
+ BITS_OPERATORS = Regexp.new(BITS_OPERATORS_LIST.join('|'))
73
+
74
+ SQ = "'".freeze
75
+ DQ = '"'.freeze
76
+ BQ = '`'.freeze
77
+
78
+ attr_reader :ss
79
+
80
+ def initialize(db_infos,
81
+ backslash_escape = true,
82
+ has_hex = true)
83
+
84
+ @db_infos = db_infos
85
+ @ss = nil
86
+ @backslash_escape = backslash_escape
87
+ @has_hex = has_hex
88
+ end
89
+
90
+ def tokenize(str)
91
+ @ss = StringScanner.new(str)
92
+ end
93
+
94
+ def each_token
95
+ while token = next_token
96
+ type = get_type(token[0])
97
+ puts token.inspect if $DEBUG
98
+ puts 'After: ' + ss.rest if $DEBUG
99
+ yield(token, type)
100
+ end
101
+ end
102
+
103
+ # Can raise a ParsingException.
104
+ def next_token
105
+ @ss.skip(/^[ \t\n]+/)
106
+
107
+ return if @ss.eos?
108
+
109
+ # FIXME: multiple commands (; separated)
110
+ # FIXME endline comments (-- )
111
+
112
+ res = if text = @ss.scan(COMMAND)
113
+ [:COMMAND, text]
114
+ elsif @has_hex && text = @ss.scan(HEX)
115
+ [:HEX_STRING, text]
116
+ elsif text = @ss.scan(NUMBER)
117
+ [:NUMBER, text]
118
+ elsif text = extract_string(SQ)
119
+ [:SINGLE_QUOTED_STRING, SQ + text + SQ]
120
+ elsif text = extract_string(DQ)
121
+ [:DOUBLE_QUOTED_STRING, DQ + text + DQ]
122
+ elsif text = extract_string(BQ)
123
+ [:BACK_QUOTED_STRING, BQ + text + BQ]
124
+ elsif @ss.peek(1) == '*' && @ss.pos += 1
125
+ [:ASTERISK, '*']
126
+ elsif @ss.peek(3) == '/*!' && @ss.pos += 3 and text = @ss.search_full(/\*\//, true, true)
127
+ [:INLINE_COMMENT, '/*!' + text]
128
+ elsif @ss.peek(2) == '/*' && @ss.pos += 2 and text = @ss.search_full(/\*\//, true, true)
129
+ [:INLINE_COMMENT, '/*' + text]
130
+ elsif text = @ss.scan(/\)/)
131
+ [:PAR_CLOSE, text]
132
+ elsif text = @ss.scan(/\(/)
133
+ [:PAR_OPEN, text]
134
+ elsif text = @ss.scan(/,/)
135
+ [:COMA, text]
136
+ elsif @ss.peek(1) == '?' && @ss.pos += 1
137
+ [:QUESTION_MARK, '?']
138
+ elsif text = @ss.scan(/;/)
139
+ [:QUERY_END, text]
140
+ elsif @ss.peek(1) == ':' && @ss.pos += 1
141
+ [:LABEL_MARK, ':']
142
+ elsif @ss.peek(1) == '.' && @ss.pos += 1
143
+ [:DOT, '.']
144
+ elsif text = @ss.scan(BINARY_OPERATORS)
145
+ [:BINARY_OPERATOR, text]
146
+ # elsif text = @ss.scan(BITS_OPERATORS) then
147
+ # [:BITS_OPERATOR, text]
148
+ else
149
+ pos = @ss.pos
150
+ @ss.reset
151
+ request = @ss.rest
152
+ @ss.pos = pos
153
+ raise ParsingException, format('could not find anything %s', request.inspect)
154
+ end
155
+
156
+ res
157
+ end
158
+
159
+ TYPES_MAPPING = {
160
+ :literal_string => [
161
+ :SINGLE_QUOTED_STRING,
162
+ :DOUBLE_QUOTED_STRING,
163
+ :HEX_STRING,
164
+ ],
165
+ :literal_number => [
166
+ :NUMBER,
167
+ ],
168
+ :object => [
169
+ :BACK_QUOTED_STRING,
170
+ ],
171
+ :command => [
172
+ :COMMAND,
173
+ ],
174
+ }.freeze
175
+
176
+ INVERT_TYPE_MAPPING = TYPES_MAPPING.inject({}) do |new, types_h|
177
+ meta_type, types = types_h
178
+ for type in types
179
+ new[type] = meta_type
180
+ end
181
+ new
182
+ end
183
+
184
+ def get_type(token_type)
185
+ INVERT_TYPE_MAPPING[token_type]
186
+ end
187
+
188
+ def extract_string(delim)
189
+ return unless @ss.peek(1) == delim
190
+ @ss.pos += 1
191
+ find_string(delim)
192
+ end
193
+
194
+ def find_dq_str
195
+ find_string('"')
196
+ end
197
+
198
+ def find_sq_str(_string_scanner)
199
+ find_string("'")
200
+ end
201
+
202
+ def find_string(delim)
203
+ remain = @ss.rest
204
+ end_idx = find_impair_delim_pos(remain, delim)
205
+ raise ParsingException, format('no end for string %s', remain.inspect) if end_idx.nil?
206
+
207
+ # So we point on the last internal char of string
208
+ res = remain[0...end_idx]
209
+
210
+ # Ruby 1.9.3: String.bytes = Enumerator has no size method
211
+ bytes_size = 0
212
+ res.bytes.each { bytes_size += 1 }
213
+
214
+ # Remain is a unicode string. @ss the StringScanner works with
215
+ # bytes, so we need to move it forward by numbre of bytes in remain.
216
+ @ss.pos += bytes_size + 1
217
+ res
218
+ end
219
+
220
+ # MySQL string exctraction is a pain since:
221
+ # - a string may hold antislashes, which escapes the next char
222
+ # - a string may hold their delimiter (' or ") twice: '' or "", which
223
+ # translates \' or \"
224
+ # Hence we browse the string. We skip \*, and if we find delimiters, we
225
+ # count how many there are.
226
+ # - If its an odd number, (1, 3...) this is the end of the string.
227
+ # - Else, they have all been escaped, we can ignore them.
228
+ def find_impair_delim_pos(str, delim)
229
+ skip = false
230
+ first = nil
231
+ str.chars.each_with_index do |char, i|
232
+ if skip
233
+ skip = false
234
+ next
235
+ end
236
+ if @backslash_escape && char == '\\'
237
+ # if we have an escape, let's skip it
238
+ skip = true
239
+ next
240
+ end
241
+ if char == delim
242
+ first = i unless first
243
+ else
244
+ if first
245
+ # first time we reach the end of a `delim` sequence
246
+ nb = i - first
247
+ if nb.odd?
248
+ # we had an odd number of delim
249
+ return i - 1
250
+ else
251
+ # we had an even number of delim, keep searching
252
+ first = nil
253
+ end
254
+ end
255
+ end
256
+ end
257
+ i = str.size - 1
258
+ if first
259
+ nb = i - first + 1
260
+ return i if nb.odd?
261
+ end
262
+ nil
263
+ end
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,110 @@
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 'strscan'
5
+ require 'sqreen/exception'
6
+
7
+ module Sqreen
8
+ module Parsers
9
+ # A command fragment
10
+ class UnixAtom
11
+ attr_accessor :kind, :val, :start, :end
12
+
13
+ def initialize(kind, value, start)
14
+ self.kind = kind
15
+ self.val = value
16
+ self.start = start
17
+ self.end = start + value.size
18
+ end
19
+
20
+ def executable?
21
+ kind == :exec
22
+ end
23
+ end
24
+
25
+ # A basic Shell command parser
26
+ class Unix
27
+ attr_reader :atoms
28
+
29
+ def initialize
30
+ @ifs = ENV.fetch('IFS', " \t\n")
31
+ @white = Regexp.new("[#{@ifs}]+")
32
+ @nonwhite = Regexp.new("[^#{@ifs}]+")
33
+ @control = ';&|'
34
+ @nonwhite_and_control = Regexp.new("[^#{@ifs}#{@control}]+")
35
+ end
36
+
37
+ # Parse a shell command.
38
+ #
39
+ # fills atoms with parsing results
40
+ # @param cmd [String] Command to be parsed
41
+ # @return [Boolean] true if successful
42
+ def parse(cmd)
43
+ @atoms = []
44
+ each_token(cmd) do |atom|
45
+ @atoms << atom
46
+ end
47
+
48
+ true
49
+ end
50
+
51
+ protected
52
+
53
+ def each_token(cmd)
54
+ scan = StringScanner.new(cmd)
55
+
56
+ @first = true
57
+ loop do
58
+ token = next_token(scan)
59
+ break if token.nil?
60
+ yield token
61
+ end
62
+ end
63
+
64
+ def next_token(scan)
65
+ scan.skip(@white)
66
+
67
+ return nil if scan.eos?
68
+
69
+ pos = scan.pos
70
+ text = scan.scan(Regexp.new("#{@nonwhite}="))
71
+ return UnixAtom.new(:param, text + find_word(scan), pos) if text
72
+ next_ch = scan.peek(1)
73
+ if @control.include?(next_ch)
74
+ @first = true
75
+ return UnixAtom.new(:control, scan.scan(Regexp.new("[#{@control}]+")), pos)
76
+ end
77
+ word = find_word(scan)
78
+ return nil unless word
79
+ if @first
80
+ @first = false
81
+ return UnixAtom.new(:exec, word, pos)
82
+ else
83
+ return UnixAtom.new(:field, word, pos)
84
+ end
85
+ end
86
+
87
+ def find_word(scan)
88
+ first = scan.peek(1)
89
+ return '' if first.match(@white)
90
+
91
+ if first == "'"
92
+ scan.getch
93
+ "'" + scan.scan_until(/'|\z/)
94
+ elsif first == '"'
95
+ txt = ['"']
96
+ scan.getch
97
+ until scan.eos?
98
+ txt << scan.scan_until(/"|\z/)
99
+ escaped = txt[-1].match(/\\+"\z/)
100
+ break unless escaped
101
+ break if escaped.size.even?
102
+ end
103
+ txt.join
104
+ else
105
+ scan.scan(@nonwhite_and_control)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,132 @@
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/context'
5
+ require 'sqreen/runtime_infos'
6
+ require 'sqreen/events/remote_exception'
7
+
8
+ module Sqreen
9
+ # Create a payload from a given query
10
+ #
11
+ # Template elements are made of sections and subsections.
12
+ # This class is able to send the full content of section or
13
+ # only the required subsections as needed.
14
+ #
15
+ # The payload will always be outputed as a
16
+ # Hash of section => subsection.
17
+ class PayloadCreator
18
+ def initialize(query)
19
+ self.query = query
20
+ end
21
+
22
+ def query=(keys)
23
+ @sections = {}
24
+ keys.each do |key|
25
+ section, subsection = key.split('.', 2)
26
+ @sections[section] = true if subsection.nil?
27
+ next if @sections[section] == true
28
+ @sections[section] ||= []
29
+ @sections[section].push(subsection)
30
+ end
31
+ end
32
+
33
+ def payload(framework, rule = {})
34
+ ret = {}
35
+ METHODS.each_key do |section|
36
+ ret = fill(section, ret, framework, rule)
37
+ end
38
+ ret
39
+ end
40
+
41
+ protected
42
+
43
+ def fill(key, base, framework, rule)
44
+ subsection = @sections[key]
45
+ return base if subsection.nil?
46
+ if subsection == true
47
+ return base.merge!(key => full_section(key, framework, rule))
48
+ end
49
+ return base if subsection.size == 0
50
+ base[key] = fields(key, framework, rule)
51
+ base
52
+ end
53
+
54
+ FULL_SECTIONS = {
55
+ 'request' => 'request_infos',
56
+ 'params' => 'filtered_request_params',
57
+ 'local' => 'local_infos',
58
+ }.freeze
59
+
60
+ METHODS = {
61
+ 'request' => {
62
+ 'addr' => 'client_ip',
63
+ 'rid' => 'request_id',
64
+ },
65
+ 'local' => {
66
+ 'name' => 'hostname',
67
+ },
68
+ 'params' => {
69
+ 'form' => 'form_params',
70
+ 'query' => 'query_params',
71
+ 'cookies' => 'cookies_params',
72
+ 'rails' => 'rails_params',
73
+ },
74
+ 'rule' => {},
75
+ 'context' => {
76
+ 'backtrace' => 'get_current_backtrace',
77
+ },
78
+ }.freeze
79
+
80
+ def section_object(section, framework, rule)
81
+ return RuntimeInfos if section == 'local'
82
+ return rule if section == 'rule'
83
+ return Context.new if section == 'context'
84
+ framework
85
+ end
86
+
87
+ def full_section(section, framework, rule)
88
+ return section_rule(framework, rule) if section == 'rule'
89
+ return section_context(framework, rule) if section == 'context'
90
+ so = section_object(section, framework, rule)
91
+ so.send(FULL_SECTIONS[section])
92
+ end
93
+
94
+ def fields(section, framework, rule)
95
+ out = {}
96
+ object = section_object(section, framework, rule)
97
+ remove = []
98
+ @sections[section].each do |key|
99
+ meth = METHODS[section][key]
100
+ invoke(out, key, object, meth || key, remove)
101
+ end
102
+ remove.each { |k| @sections[section].delete(k) }
103
+ Hash[out]
104
+ end
105
+
106
+ def invoke(out, key, object, method, remove)
107
+ out[key] = if object.respond_to?(:[])
108
+ object[method]
109
+ else
110
+ object.send(method)
111
+ end
112
+ rescue NoMethodError => e
113
+ remove.push(key)
114
+ Sqreen::RemoteException.record(e)
115
+ end
116
+
117
+ def section_context(framework, rule)
118
+ obj = section_object('context', framework, rule)
119
+ {
120
+ 'backtrace' => obj.get_current_backtrace,
121
+ }
122
+ end
123
+
124
+ def section_rule(_framework, rule)
125
+ {
126
+ 'name' => rule['name'],
127
+ 'rulespack_id' => rule['rulespack_id'],
128
+ 'test' => rule['test'],
129
+ }
130
+ end
131
+ end
132
+ end