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,14 @@
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/rule_callback'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ class RailsParametersCB < RuleCB
9
+ def pre(_inst, *_args, &_block)
10
+ nil
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
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/rule_callback'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # Save request context for handling further down
9
+ class RecordRequestContext < RuleCB
10
+ def pre(_inst, *args, &_block)
11
+ framework.store_request(args[0])
12
+ end
13
+
14
+ def post(_rv, _inst, *_args, &_block)
15
+ framework.clean_request
16
+ end
17
+
18
+ def failing(_exception, _inst, *_args, &_block)
19
+ framework.clean_request
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
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 'cgi'
5
+
6
+ require 'sqreen/rules_callbacks/regexp_rule'
7
+
8
+ module Sqreen
9
+ module Rules
10
+ # look for reflected XSS
11
+ class ReflectedXSSCB < RegexpRuleCB
12
+ def pre(_inst, *args, &_block)
13
+ value = args[0]
14
+ return if value.nil?
15
+ # If the value is not marked as html_safe, it will be escaped later
16
+ return unless value.html_safe?
17
+
18
+ # Sqreen::log.debug value
19
+ # Sqreen::log.debug params
20
+
21
+ return unless framework.params_include?(value)
22
+
23
+ Sqreen.log.debug { format('Found unescaped user param: %s', value) }
24
+
25
+ saved_value = value.dup
26
+ # potential XSS! let's escape
27
+ args[0].replace(CGI.escape_html(value)) if block
28
+ # The remaining code is only to find out if user entry was an attack,
29
+ # and record it. Since we don't rely on it to respond to user, it would
30
+ # be better to do it in background.
31
+ found = match_regexp(saved_value)
32
+
33
+ return unless found
34
+ infos = { :found => found }
35
+ record_event(infos)
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ 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/rule_callback'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # Generic regexp based matching
9
+ class RegexpRuleCB < RuleCB
10
+ def initialize(*args)
11
+ super(*args)
12
+ prepare
13
+ end
14
+
15
+ def prepare
16
+ @patterns = []
17
+ raw_patterns = @data['values']
18
+ if raw_patterns.nil?
19
+ msg = "no key 'values' in data (had #{@data.keys})"
20
+ raise Sqreen::Exception, msg
21
+ end
22
+
23
+ @patterns = raw_patterns.map do |pattern|
24
+ Regexp.compile(pattern, Regexp::IGNORECASE)
25
+ end
26
+ end
27
+
28
+ def match_regexp(str)
29
+ @patterns.each do |pattern|
30
+ return pattern if pattern.match(str)
31
+ end
32
+ nil
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
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/rule_callback'
5
+ require 'sqreen/detect'
6
+
7
+ module Sqreen
8
+ module Rules
9
+ # Look for Shell injections
10
+ class ShellCB < RuleCB
11
+ def pre(_inst, *args, &_block)
12
+ Sqreen.log.debug { "<< #{@klass} #{@method} #{Thread.current}" }
13
+ Sqreen.log.debug { args.inspect }
14
+
15
+ cmd = args[0]
16
+ params = framework.request_params
17
+ return if params.nil? || params == {}
18
+ Sqreen.log.debug { 'Searching injection in:' }
19
+ Sqreen.log.debug { 'command: ' + cmd }
20
+ Sqreen.log.debug { 'params: ' + params.inspect }
21
+
22
+ # FIXME: Handle IFS coming from spawn/exec/system ENV argument
23
+ inj = Sqreen::Detect::ShellInjection.new
24
+ shi = inj.user_escape?(cmd, params)
25
+ Sqreen.log.warn { "presence of a shell injection: #{shi}" }
26
+ return unless shi
27
+ infos = { :sh_cmd => cmd }
28
+ record_event(infos)
29
+ { :status => :raise }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,32 @@
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/rules_callbacks/regexp_rule'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # Callback that detect nifty env in system calls
9
+ class ShellEnvCB < RegexpRuleCB
10
+ def pre(_inst, *args, &_block)
11
+ return if args.size == 0
12
+ env = args.first
13
+ return unless env.is_a?(Hash)
14
+ return if env.size == 0
15
+ found = nil
16
+ var, value = env.find do |_, val|
17
+ next unless val.is_a?(String)
18
+ found = match_regexp(val)
19
+ end
20
+ return unless var
21
+ infos = {
22
+ :variable_name => var,
23
+ :variable_value => value,
24
+ :found => found,
25
+ }
26
+ Sqreen.log.warn "presence of a shell env tampering: #{infos.inspect}"
27
+ record_event(infos)
28
+ { :status => :raise }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,41 @@
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/rule_callback'
5
+ require 'sqreen/detect'
6
+
7
+ module Sqreen
8
+ module Rules
9
+ # Look for SQL injections
10
+ class SQLCB < RuleCB
11
+ def pre(inst, *args, &_block)
12
+ Sqreen.log.debug { "<< #{@klass} #{@method} #{Thread.current}" }
13
+ Sqreen.log.debug { args.inspect }
14
+
15
+ request = args[0]
16
+ params = framework.request_params
17
+ return if params.nil? || params == {}
18
+ Sqreen.log.debug { 'Searching injection in:' }
19
+ Sqreen.log.debug { 'request: ' + request }
20
+ Sqreen.log.debug { 'params: ' + params.inspect }
21
+
22
+ db_type, db_infos = framework.db_settings(:connection_adapter => inst)
23
+ if db_type.nil?
24
+ Sqreen.log.debug { "Database '#{db_infos[:name]}' not supported yet" }
25
+ return
26
+ end
27
+ inj = Sqreen::Detect::SQLInjection.new(db_type, db_infos)
28
+ sqli = inj.user_escape?(request, params)
29
+ Sqreen.log.info { "presence of an SQLi: #{sqli}" }
30
+ return unless sqli
31
+ infos = {
32
+ :db_request => request,
33
+ :db_type => db_type,
34
+ :db_infos => db_infos,
35
+ }
36
+ record_event(infos)
37
+ { :status => :raise }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
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/rules_callbacks/shell'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # Look for Shell injections in system like calls
9
+ class SystemShellCB < ShellCB
10
+ alias initial_pre pre
11
+ def pre(inst, *args, &block)
12
+ return if args.size == 0
13
+ cmd = args[0]
14
+ if cmd.is_a?(Hash)
15
+ # skip optional env arguments
16
+ return unless args.size > 1
17
+ cmd = args[1]
18
+ end
19
+ # skip [cmd, argv0] arguments
20
+ return if cmd.is_a?(Array)
21
+ initial_pre(inst, cmd, &block)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
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/rules_callbacks/regexp_rule'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # FIXME: Tune this as Rack capable callback?
9
+ # If:
10
+ # - we have a 404
11
+ # - the path is a typical bot scanning request
12
+ # Then we deny the ressource and record the attack.
13
+ class URLMatchesCB < RegexpRuleCB
14
+ def post(rv, _inst, *args, &_block)
15
+ return unless rv.is_a?(Array) && rv.size > 0 && rv[0] == 404
16
+ env = args[0]
17
+ path = env['SCRIPT_NAME'].to_s + env['PATH_INFO'].to_s
18
+ found = match_regexp(path)
19
+ infos = { :path => path, :found => found }
20
+ record_event(infos) if found
21
+ nil
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
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/rules_callbacks/regexp_rule'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # Look for badly behaved clients
9
+ class UserAgentMatchesCB < RegexpRuleCB
10
+ def pre(_inst, *_args, &_block)
11
+ ua = framework.client_user_agent
12
+ return unless ua
13
+ found = match_regexp(ua)
14
+ return unless found
15
+ Sqreen.log.debug { "Found UA #{ua} - found: #{found}" }
16
+ infos = { :found => found }
17
+ record_event(infos)
18
+ { :status => :raise, :data => found }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,142 @@
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/rules_callbacks'
5
+ require 'sqreen/exception'
6
+
7
+ require 'set'
8
+ require 'openssl'
9
+ require 'base64'
10
+ require 'json'
11
+
12
+ ## Rules signature
13
+ module Sqreen
14
+ # Perform an EC + digest verification of a message.
15
+ class SignatureVerifier
16
+ def initialize(key, digest)
17
+ @pub_key = OpenSSL::PKey.read(key)
18
+ @digest = digest
19
+ end
20
+
21
+ def verify(sig, val)
22
+ hashed_val = @digest.digest(val)
23
+ @pub_key.dsa_verify_asn1(hashed_val, sig)
24
+ end
25
+ end
26
+
27
+ # Normalize and verify a rule
28
+ class SqreenSignedVerifier
29
+ REQUIRED_SIGNED_KEYS = %w(hookpoint name callbacks conditions).freeze
30
+ SIGNATURE_KEY = 'signature'.freeze
31
+ SIGNATURE_VALUE_KEY = 'value'.freeze
32
+ SIGNED_KEYS_KEY = 'signed_keys'.freeze
33
+ SIGNATURE_VERSION = 'v0_9'.freeze
34
+ PUBLIC_KEY = <<-END.gsub(/^ */, '').freeze
35
+ -----BEGIN PUBLIC KEY-----
36
+ MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA39oWMHR8sxb9LRaM5evZ7mw03iwJ
37
+ WNHuDeGqgPo1HmvuMfLnAyVLwaMXpGPuvbqhC1U65PG90bTJLpvNokQf0VMA5Tpi
38
+ m+NXwl7bjqa03vO/HErLbq3zBRysrZnC4OhJOF1jazkAg0psQOea2r5HcMcPHgMK
39
+ fnWXiKWnZX+uOWPuerE=
40
+ -----END PUBLIC KEY-----
41
+ END
42
+
43
+ attr_accessor :pub_key
44
+ attr_accessor :required_signed_keys
45
+ attr_accessor :digest
46
+
47
+ def initialize(required_keys = REQUIRED_SIGNED_KEYS,
48
+ public_key = PUBLIC_KEY,
49
+ digest = OpenSSL::Digest::SHA512.new)
50
+ @required_signed_keys = required_keys
51
+ @signature_verifier = SignatureVerifier.new(public_key, digest)
52
+ end
53
+
54
+ def normalize_val(val, level)
55
+ raise Sqreen::Exception, 'recursion level too deep' if level == 0
56
+
57
+ case val
58
+ when Hash
59
+ normalize(val, nil, level - 1)
60
+ when Array
61
+ ary = val.map do |i|
62
+ normalize_val(i, level - 1)
63
+ end
64
+ "[#{ary.join(',')}]"
65
+ when String, Integer
66
+ JSON.dump(val)
67
+ else
68
+ msg = "JSON hash parsing error (wrong value type: #{val.class})"
69
+ raise Sqreen::Exception.new, msg
70
+ end
71
+ end
72
+
73
+ def normalize_key(key)
74
+ case key
75
+ when String, Integer
76
+ JSON.dump(key)
77
+ else
78
+ msg = "JSON hash parsing error (wrong key type: #{key.class})"
79
+ raise Sqreen::Exception, msg
80
+ end
81
+ end
82
+
83
+ def normalize(hash_rule, signed_keys = nil, level = 20)
84
+ # Normalize the provided hash to a string:
85
+ # - sort keys lexicographically, recursively
86
+ # - convert each scalar to its JSON representation
87
+ # - convert hash to '{key:value}'
88
+ # - convert array [v1,v2] to '[v1,v2]' and [] to '[]'
89
+ # Two hash with different key ordering should have the same normalized
90
+ # value.
91
+
92
+ raise Sqreen::Exception, 'recursion level too deep' if level == 0
93
+ unless hash_rule.is_a?(Hash)
94
+ raise Sqreen::Exception, "wrong hash type #{hash_rule.class}"
95
+ end
96
+
97
+ res = []
98
+ hash_rule.sort.each do |k, v|
99
+ # Only keep signed keys
100
+ next if signed_keys && !signed_keys.include?(k)
101
+
102
+ k = normalize_key(k)
103
+ v = normalize_val(v, level - 1)
104
+
105
+ res << "#{k}:#{v}"
106
+ end
107
+ "{#{res.join(',')}}"
108
+ end
109
+
110
+ def get_sig_infos_or_fail(hash_rule)
111
+ raise Sqreen::Exception, 'non hash argument' unless hash_rule.is_a?(Hash)
112
+
113
+ sigs = hash_rule[SIGNATURE_KEY]
114
+ raise Sqreen::Exception, 'no signature found' unless sigs
115
+
116
+ sig = sigs[SIGNATURE_VERSION]
117
+ msg = "signature #{SIGNATURE_VERSION} not found"
118
+ raise Sqreen::Exception, msg unless sig
119
+
120
+ sig_value = sig[SIGNATURE_VALUE_KEY]
121
+ raise Sqreen::Exception, 'no signature value found' unless sig_value
122
+
123
+ signed_keys = sig[SIGNED_KEYS_KEY]
124
+ raise Sqreen::Exception, 'no signed keys found' unless signed_keys
125
+
126
+ inc = Set.new(signed_keys).superset?(Set.new(@required_signed_keys))
127
+ raise Sqreen::Exception, 'signed keys miss equired keys' unless inc
128
+
129
+ [signed_keys, sig_value]
130
+ end
131
+
132
+ def verify(hash_rule)
133
+ signed_keys, sig_value = get_sig_infos_or_fail(hash_rule)
134
+
135
+ norm_str = normalize(hash_rule, signed_keys)
136
+ bin_sig = Base64.decode64(sig_value)
137
+ @signature_verifier.verify(bin_sig, norm_str)
138
+ rescue OpenSSL::PKey::ECError
139
+ false
140
+ end
141
+ end
142
+ end