opendns-dnsdb 0.1.0

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +4 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE +20 -0
  6. data/README.md +64 -0
  7. data/Rakefile +6 -0
  8. data/THANKS +1 -0
  9. data/docs/Makefile +177 -0
  10. data/docs/_themes/LICENSE +45 -0
  11. data/docs/_themes/README.rst +25 -0
  12. data/docs/_themes/flask_theme_support.py +86 -0
  13. data/docs/_themes/kr/layout.html +32 -0
  14. data/docs/_themes/kr/relations.html +19 -0
  15. data/docs/_themes/kr/static/flasky.css_t +469 -0
  16. data/docs/_themes/kr/static/small_flask.css +70 -0
  17. data/docs/_themes/kr/theme.conf +7 -0
  18. data/docs/_themes/kr_small/layout.html +22 -0
  19. data/docs/_themes/kr_small/static/flasky.css_t +287 -0
  20. data/docs/_themes/kr_small/theme.conf +10 -0
  21. data/docs/conf.py +261 -0
  22. data/docs/index.rst +101 -0
  23. data/docs/make.bat +242 -0
  24. data/docs/operations/by_ip.rst +229 -0
  25. data/docs/operations/by_name.rst +256 -0
  26. data/docs/operations/label.rst +217 -0
  27. data/docs/operations/related.rst +127 -0
  28. data/docs/operations/traffic.rst +126 -0
  29. data/lib/opendns-dnsdb.rb +5 -0
  30. data/lib/opendns-dnsdb/dnsdb.rb +58 -0
  31. data/lib/opendns-dnsdb/dnsdb/by_ip.rb +69 -0
  32. data/lib/opendns-dnsdb/dnsdb/by_name.rb +93 -0
  33. data/lib/opendns-dnsdb/dnsdb/label.rb +105 -0
  34. data/lib/opendns-dnsdb/dnsdb/related.rb +92 -0
  35. data/lib/opendns-dnsdb/dnsdb/response.rb +41 -0
  36. data/lib/opendns-dnsdb/dnsdb/rrutils.rb +11 -0
  37. data/lib/opendns-dnsdb/dnsdb/siphash.rb +94 -0
  38. data/lib/opendns-dnsdb/dnsdb/traffic.rb +80 -0
  39. data/lib/opendns-dnsdb/version.rb +5 -0
  40. data/opendns-dnsdb.gemspec +20 -0
  41. data/spec/by_ip_spec.rb +54 -0
  42. data/spec/by_name_spec.rb +88 -0
  43. data/spec/label_spec.rb +88 -0
  44. data/spec/related_spec.rb +92 -0
  45. data/spec/spec_helper.rb +5 -0
  46. data/spec/traffic_spec.rb +36 -0
  47. metadata +123 -0
@@ -0,0 +1,105 @@
1
+
2
+ require_relative 'siphash'
3
+
4
+ module OpenDNS
5
+ class DNSDB
6
+ module Label
7
+ CACHE_KEY = 'Umbrella/OpenDNS'
8
+
9
+ def distinct_labels(labels)
10
+ return [ labels ] unless labels.kind_of?(Hash)
11
+ Response::Distinct.new(labels.values.flatten.uniq)
12
+ end
13
+
14
+ def labels_by_name(names)
15
+ names_is_array = names.kind_of?(Enumerable)
16
+ names = [ names ] unless names_is_array
17
+ multi = Ethon::Multi.new
18
+ names_json = MultiJson.dump(names)
19
+ cacheid = SipHash::digest(CACHE_KEY, names_json).to_s(16)
20
+ url = "/infected/names/#{cacheid}.json"
21
+ query = query_handler(url, :get, { body: names_json })
22
+ multi.add(query)
23
+ multi.perform
24
+ responses = MultiJson.load(query.response_body)
25
+ responses = responses['scores']
26
+ responses.each_pair do |name, label|
27
+ responses[name] = [:suspicious, :unknown, :benign][label + 1]
28
+ end
29
+ responses = Response::HashByName[responses]
30
+ responses = responses.values.first unless names_is_array
31
+ responses
32
+ end
33
+
34
+ def distinct_labels_by_name(names)
35
+ distinct_labels(labels_by_name(names))
36
+ end
37
+
38
+ def include_suspicious?(names)
39
+ distinct_labels_by_name(names).include?(:suspicious)
40
+ end
41
+
42
+ def is_suspicious?(name)
43
+ include_suspicious?(name)
44
+ end
45
+
46
+ def include_benign?(names)
47
+ distinct_labels_by_name(names).include?(:benign)
48
+ end
49
+
50
+ def is_benign?(name)
51
+ include_benign?(name)
52
+ end
53
+
54
+ def include_unknown?(names)
55
+ distinct_labels_by_name(names).include?(:unknown)
56
+ end
57
+
58
+ def is_unknown?(name)
59
+ include_unknown?(name)
60
+ end
61
+
62
+ def suspicious_names(names)
63
+ labels = labels_by_name(names)
64
+ labels = [ labels ] unless labels.kind_of?(Enumerable)
65
+ names = labels.select { |name, label| label == :suspicious }.keys
66
+ Response::Distinct.new(names)
67
+ end
68
+
69
+ def not_suspicious_names(names)
70
+ labels = labels_by_name(names)
71
+ labels = [ labels ] unless labels.kind_of?(Enumerable)
72
+ names = labels.select { |name, label| label != :suspicious }.keys
73
+ Response::Distinct.new(names)
74
+ end
75
+
76
+ def unknown_names(names)
77
+ labels = labels_by_name(names)
78
+ labels = [ labels ] unless labels.kind_of?(Enumerable)
79
+ names = labels.select { |name, label| label == :unknown }.keys
80
+ Response::Distinct.new(names)
81
+ end
82
+
83
+ def not_unknown_names(names)
84
+ labels = labels_by_name(names)
85
+ labels = [ labels ] unless labels.kind_of?(Enumerable)
86
+ names = labels.select { |name, label| label != :unknown }.keys
87
+ Response::Distinct.new(names)
88
+ end
89
+
90
+ def benign_names(names)
91
+ labels = labels_by_name(names)
92
+ labels = [ labels ] unless labels.kind_of?(Enumerable)
93
+ names = labels.select { |name, label| label == :benign }.keys
94
+ Response::Distinct.new(names)
95
+ end
96
+
97
+ def not_benign_names(names)
98
+ labels = labels_by_name(names)
99
+ labels = [ labels ] unless labels.kind_of?(Enumerable)
100
+ names = labels.select { |name, label| label != :benign }.keys
101
+ Response::Distinct.new(names)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,92 @@
1
+
2
+ require_relative 'rrutils'
3
+
4
+ module OpenDNS
5
+ class DNSDB
6
+ module Related
7
+ include OpenDNS::DNSDB::RRUtils
8
+
9
+ def related_names_with_score(names, &filter)
10
+ names_is_array = names.kind_of?(Enumerable)
11
+ names = [ names ] unless names_is_array
12
+ multi = Ethon::Multi.new
13
+ queries_links = { }
14
+ queries_coocs = { }
15
+ names.each do |name|
16
+ url_links = "/links/name/#{name}.json"
17
+ query_links = query_handler(url_links)
18
+ multi.add(query_links)
19
+ queries_links[name] = query_links
20
+
21
+ url_coocs = "/recommendations/name/#{name}.json"
22
+ query_coocs = query_handler(url_coocs)
23
+ multi.add(query_coocs)
24
+ queries_coocs[name] = query_coocs
25
+ end
26
+ multi.perform
27
+ responses = { }
28
+ queries_coocs.each_pair do |name, query|
29
+ if query.response_body.empty?
30
+ responses[name] ||= { }
31
+ next
32
+ end
33
+ obj = MultiJson.load(query.response_body)
34
+ if pfs2 = Response::Raw.new(obj).pfs2
35
+ responses[name] = Hash[*pfs2.flatten]
36
+ else
37
+ responses[name] = { }
38
+ end
39
+ end
40
+ queries_links.each_pair do |name, query|
41
+ responses[name] ||= { }
42
+ if query.response_body.empty?
43
+ next
44
+ end
45
+ obj = MultiJson.load(query.response_body)
46
+ if tb1 = Response::Raw.new(obj).tb1
47
+ upper = [1.0, tb1.map { |x| x[1] }.max].max
48
+ responses[name] = Hash[*tb1.map { |x| [x[0], x[1] / upper] }.flatten]
49
+ end
50
+ end
51
+ responses = Response::HashByName[responses]
52
+ if block_given?
53
+ responses.each_key do |name|
54
+ responses[name].select! do |related_name, score|
55
+ filter.call(related_name, score)
56
+ end
57
+ end
58
+ end
59
+ responses = responses.values.first unless names_is_array
60
+ responses
61
+ end
62
+
63
+ def related_names(names, options = { }, &filter)
64
+ res = related_names_with_score(names, &filter) || { }
65
+ if names.kind_of?(Enumerable)
66
+ res.merge(res) do |name, v|
67
+ res0 = v.keys
68
+ res0 = res0[0...options[:max_names]] if options[:max_names]
69
+ res0
70
+ end
71
+ else
72
+ res0 = res.keys
73
+ res0 = res[0...options[:max_names]] if options[:max_names]
74
+ res0
75
+ end
76
+ end
77
+
78
+ def distinct_related_names(names, options = { }, &filter)
79
+ res = Response::Distinct.new(distinct_rrs(related_names(names, &filter)))
80
+ res = res[0...options[:max_names]] if options[:max_names]
81
+ if (options[:max_depth] || 1) > 1
82
+ options0 = options.clone
83
+ options0[:max_depth] -= 1
84
+ related = distinct_related_names(res, options0, &filter)
85
+ res = (res + related).uniq
86
+ res = res[0...options[:max_names]] if options[:max_names]
87
+ end
88
+ res
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,41 @@
1
+
2
+ module OpenDNS
3
+ class DNSDB
4
+ module Response
5
+ module Common
6
+ end
7
+
8
+ class Raw < Hashie::Mash
9
+ include Common
10
+
11
+ def initialize(source_hash = nil, default = nil, &blk)
12
+ if source_hash
13
+ if source_hash[:first_seen]
14
+ source_hash[:first_seen] = Date.parse(source_hash[:first_seen])
15
+ end
16
+ if source_hash[:last_seen]
17
+ source_hash[:last_seen] = Date.parse(source_hash[:last_seen])
18
+ end
19
+ end
20
+ super(source_hash, default, &blk)
21
+ end
22
+ end
23
+
24
+ class HashByName < Hash
25
+ include Common
26
+ end
27
+
28
+ class HashByIP < Hash
29
+ include Common
30
+ end
31
+
32
+ class Distinct < Array
33
+ include Common
34
+ end
35
+
36
+ class TimeSeries < Array
37
+ include Common
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+
2
+ module OpenDNS
3
+ class DNSDB
4
+ module RRUtils
5
+ def distinct_rrs(rrs)
6
+ return rrs unless rrs.kind_of?(Hash)
7
+ Response::Distinct.new(rrs.values.flatten.uniq)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,94 @@
1
+ module SipHash
2
+
3
+ def self.digest(key, msg)
4
+ s = State.new(key)
5
+ len = msg.size
6
+ iter = len / 8
7
+
8
+ iter.times do |i|
9
+ m = msg.slice(i * 8, 8).unpack("Q<")[0]
10
+ s.apply_block(m)
11
+ end
12
+
13
+ m = last_block(msg, len, iter)
14
+
15
+ s.apply_block(m)
16
+ s.finalize
17
+ s.digest
18
+ end
19
+
20
+ private
21
+
22
+ def self.last_block(msg, len, iter)
23
+ last = (len << 56) & State::MASK_64;
24
+
25
+ r = len % 8
26
+ off = iter * 8
27
+
28
+ last |= msg[off + 6].ord << 48 if r >= 7
29
+ last |= msg[off + 5].ord << 40 if r >= 6
30
+ last |= msg[off + 4].ord << 32 if r >= 5
31
+ last |= msg[off + 3].ord << 24 if r >= 4
32
+ last |= msg[off + 2].ord << 16 if r >= 3
33
+ last |= msg[off + 1].ord << 8 if r >= 2
34
+ last |= msg[off].ord if r >= 1
35
+ last
36
+ end
37
+
38
+ class State
39
+
40
+ MASK_64 = 0xffffffffffffffff
41
+
42
+ def initialize(key)
43
+ @v0 = 0x736f6d6570736575
44
+ @v1 = 0x646f72616e646f6d
45
+ @v2 = 0x6c7967656e657261
46
+ @v3 = 0x7465646279746573
47
+
48
+ k0 = key.slice(0, 8).unpack("Q<")[0]
49
+ k1 = key.slice(8, 8).unpack("Q<")[0]
50
+
51
+ @v0 ^= k0
52
+ @v1 ^= k1
53
+ @v2 ^= k0
54
+ @v3 ^= k1
55
+ end
56
+
57
+ def apply_block(m)
58
+ @v3 ^= m
59
+ 2.times { compress }
60
+ @v0 ^= m
61
+ end
62
+
63
+ def rotl64(num, shift)
64
+ ((num << shift) & MASK_64) | (num >> (64 - shift))
65
+ end
66
+
67
+ def compress
68
+ @v0 = (@v0 + @v1) & MASK_64
69
+ @v2 = (@v2 + @v3) & MASK_64
70
+ @v1 = rotl64(@v1, 13)
71
+ @v3 = rotl64(@v3, 16)
72
+ @v1 ^= @v0
73
+ @v3 ^= @v2
74
+ @v0 = rotl64(@v0, 32)
75
+ @v2 = (@v2 + @v1) & MASK_64
76
+ @v0 = (@v0 + @v3) & MASK_64
77
+ @v1 = rotl64(@v1, 17)
78
+ @v3 = rotl64(@v3, 21)
79
+ @v1 ^= @v2
80
+ @v3 ^= @v0
81
+ @v2 = rotl64(@v2, 32)
82
+ end
83
+
84
+ def finalize
85
+ @v2 ^= 0xff
86
+ 4.times { compress }
87
+ end
88
+
89
+ def digest
90
+ @v0 ^ @v1 ^ @v2 ^ @v3
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,80 @@
1
+
2
+ module OpenDNS
3
+ class DNSDB
4
+ module Traffic
5
+ require 'cgi'
6
+ require 'date'
7
+ require 'descriptive_statistics'
8
+
9
+ DEFAULT_DAYS_BACK = 7
10
+
11
+ def daily_traffic_by_name(names, options = { })
12
+ names_is_array = names.kind_of?(Enumerable)
13
+ names = [ names ] unless names_is_array
14
+ multi = Ethon::Multi.new
15
+ date_end = options[:start] || Date.today
16
+ date_end_s = CGI::escape("#{date_end.year}/#{date_end.month}/#{date_end.day}/23")
17
+ days_back = options[:days_back] || DEFAULT_DAYS_BACK
18
+ date_start = date_end - days_back
19
+ date_start += 1 unless date_start == date_end
20
+ date_start_s = CGI::escape("#{date_start.year}/#{date_start.month}/#{date_start.day}/00")
21
+ date_end, date_start = date_start, date_end if date_start > date_end
22
+ queries_traffic = { }
23
+ names.each do |name|
24
+ name0 = name.gsub(/^www[.]/, '')
25
+ url_traffic = "/appserver/?v=1&function=domain2-system&domains=#{name0}" +
26
+ "&locations=&start=#{date_start_s}&stop=#{date_end_s}"
27
+ query_traffic = query_handler(url_traffic)
28
+ multi.add(query_traffic)
29
+ queries_traffic[name] = query_traffic
30
+ end
31
+ multi.perform
32
+ responses = { }
33
+ queries_traffic.each_pair do |name, query|
34
+ obj = MultiJson.load(query.response_body)
35
+ tc = obj['response']
36
+ tc = tc.group_by { |x| x[0].split('/')[0...3].join('/') }
37
+ tc.each_key do |date_s|
38
+ tc[date_s] = tc[date_s].inject(0) { |a, x| a + x[1] }
39
+ end
40
+ responses[name] = tc.values
41
+ end
42
+ responses = Response::HashByName[responses]
43
+ responses = responses.values.first unless names_is_array
44
+ responses
45
+ end
46
+
47
+ def relative_standard_deviation(names_ts)
48
+ names_ts_is_hash = names_ts.kind_of?(Hash)
49
+ names_ts = { x: names_ts } unless names_ts_is_hash
50
+ responses = { }
51
+ names_ts.each_pair do |name, ts|
52
+ mean = ts.mean
53
+ if mean <= 0.0
54
+ responses[name] = 0.0
55
+ else
56
+ responses[name] = ts.standard_deviation / mean * 100.0
57
+ end
58
+ end
59
+ responses = Response::HashByName[responses]
60
+ responses = responses.values.first unless names_ts_is_hash
61
+ responses
62
+ end
63
+
64
+ def high_pass_filter(names_ts, options)
65
+ names_ts_is_hash = names_ts.kind_of?(Hash)
66
+ names_ts = { x: names_ts } unless names_ts_is_hash
67
+ responses = { }
68
+ cutoff = options[:cutoff]
69
+ names_ts.each_pair do |name, ts|
70
+ responses[name] = ts.map { |x| x < cutoff ? 0.0 : x }
71
+ end
72
+ responses = Response::HashByName[responses]
73
+ responses = responses.values.first unless names_ts_is_hash
74
+ responses
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+
@@ -0,0 +1,5 @@
1
+ module OpenDNS
2
+ class DNSDB
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/opendns-dnsdb/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Frank Denis"]
6
+ gem.email = ["frank@opendns.com"]
7
+ gem.description = "Client library for the OpenDNS Security Graph"
8
+ gem.summary = gem.description
9
+ gem.homepage = "https://github.com/jedisct1/opendns-dnsdb-ruby"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "opendns-dnsdb"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = OpenDNS::DNSDB::VERSION
17
+
18
+ gem.add_development_dependency 'rake'
19
+ gem.add_development_dependency 'rspec', '>= 2.14'
20
+ end