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