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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +4 -0
- data/Gemfile +8 -0
- data/LICENSE +20 -0
- data/README.md +64 -0
- data/Rakefile +6 -0
- data/THANKS +1 -0
- data/docs/Makefile +177 -0
- data/docs/_themes/LICENSE +45 -0
- data/docs/_themes/README.rst +25 -0
- data/docs/_themes/flask_theme_support.py +86 -0
- data/docs/_themes/kr/layout.html +32 -0
- data/docs/_themes/kr/relations.html +19 -0
- data/docs/_themes/kr/static/flasky.css_t +469 -0
- data/docs/_themes/kr/static/small_flask.css +70 -0
- data/docs/_themes/kr/theme.conf +7 -0
- data/docs/_themes/kr_small/layout.html +22 -0
- data/docs/_themes/kr_small/static/flasky.css_t +287 -0
- data/docs/_themes/kr_small/theme.conf +10 -0
- data/docs/conf.py +261 -0
- data/docs/index.rst +101 -0
- data/docs/make.bat +242 -0
- data/docs/operations/by_ip.rst +229 -0
- data/docs/operations/by_name.rst +256 -0
- data/docs/operations/label.rst +217 -0
- data/docs/operations/related.rst +127 -0
- data/docs/operations/traffic.rst +126 -0
- data/lib/opendns-dnsdb.rb +5 -0
- data/lib/opendns-dnsdb/dnsdb.rb +58 -0
- data/lib/opendns-dnsdb/dnsdb/by_ip.rb +69 -0
- data/lib/opendns-dnsdb/dnsdb/by_name.rb +93 -0
- data/lib/opendns-dnsdb/dnsdb/label.rb +105 -0
- data/lib/opendns-dnsdb/dnsdb/related.rb +92 -0
- data/lib/opendns-dnsdb/dnsdb/response.rb +41 -0
- data/lib/opendns-dnsdb/dnsdb/rrutils.rb +11 -0
- data/lib/opendns-dnsdb/dnsdb/siphash.rb +94 -0
- data/lib/opendns-dnsdb/dnsdb/traffic.rb +80 -0
- data/lib/opendns-dnsdb/version.rb +5 -0
- data/opendns-dnsdb.gemspec +20 -0
- data/spec/by_ip_spec.rb +54 -0
- data/spec/by_name_spec.rb +88 -0
- data/spec/label_spec.rb +88 -0
- data/spec/related_spec.rb +92 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/traffic_spec.rb +36 -0
- 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,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,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
|