sqreen 0.1.0.pre → 0.7.01461158029

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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