dnsbl-client 1.0.7 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/dnsbl-client +25 -23
- data/data/dnsbl.yaml +2 -2
- data/lib/dnsbl/client/version.rb +3 -1
- data/lib/dnsbl/client.rb +251 -252
- data/test/helper.rb +3 -1
- data/test/test_dnsbl-client.rb +168 -162
- metadata +45 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2627a94892578f667a8a2b0e55bb74352eccc688caa036037f3252a695079434
|
4
|
+
data.tar.gz: 8908b619c28fce048d42090af6fcb775df6e7617bad764c33fbba304f3aab418
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 832f66d7c6a57a509d15a4655f9fcae795dd6713a01c4a1bd4093dc121870eb637ea36eb5789c8c17bd75ccac71060d0dec048cd1dfd9e9200eac8ca3ce7f0cb
|
7
|
+
data.tar.gz: 78e2c95ee4185e45a2c61d1c5ace689cc6fcab3a3bbd0d6a522490c2369c17e6247f0cd46d64f19aca9a9b51d7091b72922d9542f5afdbc01318433d8104e36c
|
data/bin/dnsbl-client
CHANGED
@@ -1,30 +1,32 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
2
4
|
require 'dnsbl/client'
|
3
5
|
|
4
6
|
c = DNSBL::Client.new
|
5
7
|
c.first_only = true
|
6
8
|
|
7
|
-
if ARGV.length
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
9
|
+
if ARGV.length.positive?
|
10
|
+
c.lookup(ARGV).each do |res|
|
11
|
+
sep = ''
|
12
|
+
res.members.each do |member|
|
13
|
+
print sep
|
14
|
+
print res[member]
|
15
|
+
sep = "\t"
|
16
|
+
end
|
17
|
+
puts
|
18
|
+
end
|
17
19
|
else
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
20
|
+
$stdin.each_line do |ip|
|
21
|
+
ip.chomp!
|
22
|
+
c.lookup(ip).each do |res|
|
23
|
+
sep = ''
|
24
|
+
res.members.each do |member|
|
25
|
+
print sep
|
26
|
+
print res[member]
|
27
|
+
sep = "\t"
|
28
|
+
end
|
29
|
+
puts
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/data/dnsbl.yaml
CHANGED
@@ -178,7 +178,7 @@ PROJECTHONEYPOT:
|
|
178
178
|
type: ip
|
179
179
|
apikey: abcdefghijkl
|
180
180
|
disabled: true
|
181
|
-
decoder:
|
181
|
+
decoder: phpot
|
182
182
|
TOREXITNODE:
|
183
183
|
domain: torexit.dan.me.uk
|
184
184
|
type: ip
|
@@ -186,5 +186,5 @@ TOREXITNODE:
|
|
186
186
|
SPFBL:
|
187
187
|
domain: dnsbl.spfbl.net
|
188
188
|
type: ip
|
189
|
-
127.0.0.2: Ignoring complaints of spamming by customers.
|
189
|
+
127.0.0.2: Ignoring complaints of spamming by customers.
|
190
190
|
127.0.0.3: Dynamic IP, generic rDNS, no rDNS or invalid FCrDNS.
|
data/lib/dnsbl/client/version.rb
CHANGED
data/lib/dnsbl/client.rb
CHANGED
@@ -1,277 +1,276 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dnsbl/client/version'
|
2
4
|
|
3
5
|
# DESCRIPTION: is a module that queries dnsbls. The configuration is in a YAML file.
|
4
6
|
require 'resolv'
|
5
7
|
require 'socket'
|
6
|
-
require 'thread'
|
7
8
|
require 'yaml'
|
8
9
|
require 'ipaddr'
|
9
10
|
|
10
|
-
# This is a monkeypatch for the built-in Ruby DNS resolver to specify nameservers
|
11
|
-
class Resolv
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
11
|
+
# This is a monkeypatch for the built-in Ruby DNS resolver to specify nameservers
|
12
|
+
class Resolv
|
13
|
+
class DNS
|
14
|
+
class Config
|
15
|
+
# Monkeypatch the nameservers to set a default if there are no defined nameservers
|
16
|
+
def nameservers
|
17
|
+
return @nameservers if defined?(@nameservers)
|
18
|
+
|
19
|
+
lazy_initialize
|
20
|
+
if respond_to? :nameserver_port
|
21
|
+
@nameservers = nameserver_port
|
22
|
+
else
|
23
|
+
@nameserver ||= ['4.2.2.2',
|
24
|
+
'4.2.2.5',
|
25
|
+
'8.8.4.4',
|
26
|
+
'8.8.8.8',
|
27
|
+
'208.67.222.222',
|
28
|
+
'208.67.220.220'].shuffle
|
29
|
+
@nameservers ||= @nameserver.map { |i| [i, 53] }
|
30
|
+
end
|
31
|
+
@nameservers
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
25
35
|
end
|
26
36
|
|
27
37
|
module DNSBL # :nodoc:
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
38
|
+
# DNSBLResult holds the result of a DNSBL lookup
|
39
|
+
# dnsbl: name of the DNSBL that returned the answer
|
40
|
+
# item: the item queried, an IP or a domain
|
41
|
+
# result: the result code, e.g., 127.0.0.2
|
42
|
+
# meaning: the mapping of the result code to the meaning from the configuration file
|
43
|
+
# timing: the time between starting to send queries to the DNSBLs and when the result from this DNSBL returned
|
44
|
+
DNSBLResult = Struct.new :dnsbl, :item, :query, :result, :meaning, :timing
|
45
|
+
|
46
|
+
# Client actually handles the sending of queries to a recursive DNS server and places any replies into DNSBLResults
|
47
|
+
class Client
|
48
|
+
attr_writer :first_only,
|
49
|
+
:timeout
|
50
|
+
|
51
|
+
# initialize a new DNSBL::Client object
|
52
|
+
# the config file automatically points to a YAML file containing the list of DNSBLs and their return codes
|
53
|
+
# the two-level-tlds file lists most of the two level tlds, needed for hostname to domain normalization
|
54
|
+
def initialize(config = YAML.safe_load(File.read("#{File.expand_path '../../data', __dir__}/dnsbl.yaml")),
|
55
|
+
two_level_tldfile = "#{File.expand_path '../../data', __dir__}/two-level-tlds",
|
56
|
+
three_level_tldfile = "#{File.expand_path '../../data', __dir__}/three-level-tlds")
|
57
|
+
@dnsbls = config
|
45
58
|
@timeout = 1.5
|
46
59
|
@first_only = false
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
60
|
+
@two_level_tld = []
|
61
|
+
@three_level_tld = []
|
62
|
+
File.open(two_level_tldfile).readlines.each do |l|
|
63
|
+
@two_level_tld << l.strip
|
64
|
+
end
|
65
|
+
File.open(three_level_tldfile).readlines.each do |l|
|
66
|
+
@three_level_tld << l.strip
|
67
|
+
end
|
68
|
+
@sockets = []
|
69
|
+
config = Resolv::DNS::Config.new
|
70
|
+
|
71
|
+
# let's just the first nameserver in this version of the library
|
72
|
+
ip, port = config.nameservers.first
|
73
|
+
|
74
|
+
sock = UDPSocket.new
|
75
|
+
sock.connect ip, port
|
76
|
+
@sockets << sock
|
77
|
+
@socket_index = 0
|
78
|
+
end
|
79
|
+
|
80
|
+
# sets the nameservers used for performing DNS lookups in round-robin fashion
|
81
|
+
def nameservers=(nss = Resolv::DNS::Config.new.nameservers)
|
82
|
+
@sockets.each(&:close)
|
83
|
+
@sockets = []
|
84
|
+
|
85
|
+
# let's just the first nameserver in this version of the library
|
86
|
+
ip, port = nss.first
|
87
|
+
|
88
|
+
sock = UDPSocket.new
|
89
|
+
sock.connect ip, port
|
90
|
+
@sockets << sock
|
91
|
+
@socket_index = 0
|
92
|
+
end
|
93
|
+
|
94
|
+
# Converts a hostname to the domain: e.g., www.google.com => google.com, science.somewhere.co.uk => somewhere.co.uk
|
95
|
+
def normalize(domain)
|
96
|
+
# strip off the protocol (\w{1,20}://), the URI (/), parameters (?), port number (:), and username (.*@)
|
97
|
+
# then split into parts via the .
|
98
|
+
parts = domain.gsub(%r{^\w{1,20}://}, '').gsub(%r{[/?:].*}, '').gsub(/.*?@/, '').split('.')
|
99
|
+
# grab the last two parts of the domain
|
100
|
+
dom = parts[-2, 2].join '.'
|
101
|
+
# if the dom is in the two_level_tld list, then use three parts
|
102
|
+
dom = parts[-3, 3].join '.' if @two_level_tld.index dom
|
103
|
+
dom = parts[-4, 4].join '.' if @three_level_tld.index dom
|
104
|
+
dom
|
105
|
+
end
|
106
|
+
|
107
|
+
# allows the adding of a new DNSBL to the set of configured DNSBLs
|
108
|
+
def add_dnsbl(name, domain, type = 'ip', codes = { '0' => 'OK', '127.0.0.2' => 'Blacklisted' })
|
109
|
+
@dnsbls[name] = codes
|
110
|
+
@dnsbls[name]['domain'] = domain
|
111
|
+
@dnsbls[name]['type'] = type
|
68
112
|
end
|
69
|
-
|
70
|
-
|
71
|
-
|
113
|
+
|
114
|
+
# returns a list of DNSBL names currently configured
|
115
|
+
def dnsbls
|
116
|
+
@dnsbls.keys
|
72
117
|
end
|
73
|
-
|
74
|
-
|
75
|
-
def nameservers=(ns=Resolv::DNS::Config.new.nameservers)
|
76
|
-
@sockets.each do |s|
|
77
|
-
s.close
|
78
|
-
end
|
79
|
-
@sockets = []
|
80
|
-
ns.each do |ip,port|
|
81
|
-
sock = UDPSocket.new
|
82
|
-
sock.connect(ip,port)
|
83
|
-
@sockets << sock
|
84
|
-
break # let's just the first nameserver in this version of the library
|
85
|
-
end
|
86
|
-
@socket_index = 0
|
87
|
-
end
|
88
|
-
|
89
|
-
# Converts a hostname to the domain: e.g., www.google.com => google.com, science.somewhere.co.uk => somewhere.co.uk
|
90
|
-
def normalize(domain)
|
91
|
-
# strip off the protocol (\w{1,20}://), the URI (/), parameters (?), port number (:), and username (.*@)
|
92
|
-
# then split into parts via the .
|
93
|
-
parts = domain.gsub(/^\w{1,20}:\/\//,'').gsub(/[\/\?\:].*/,'').gsub(/.*?\@/,'').split(/\./)
|
94
|
-
# grab the last two parts of the domain
|
95
|
-
dom = parts[-2,2].join(".")
|
96
|
-
# if the dom is in the two_level_tld list, then use three parts
|
97
|
-
if @two_level_tld.index(dom)
|
98
|
-
dom = parts[-3,3].join(".")
|
99
|
-
end
|
100
|
-
if @three_level_tld.index(dom)
|
101
|
-
dom = parts[-4,4].join(".")
|
102
|
-
end
|
103
|
-
dom
|
104
|
-
end
|
105
|
-
|
106
|
-
# allows the adding of a new DNSBL to the set of configured DNSBLs
|
107
|
-
def add_dnsbl(name,domain,type='ip',codes={"0"=>"OK","127.0.0.2"=>"Blacklisted"})
|
108
|
-
@dnsbls[name] = codes
|
109
|
-
@dnsbls[name]['domain'] = domain
|
110
|
-
@dnsbls[name]['type'] = type
|
111
|
-
end
|
112
|
-
|
113
|
-
# returns a list of DNSBL names currently configured
|
114
|
-
def dnsbls
|
115
|
-
@dnsbls.keys
|
116
|
-
end
|
117
|
-
|
118
|
-
# converts an ip or a hostname into the DNS query packet requires to lookup the result
|
119
|
-
def _encode_query(item,itemtype,domain,apikey=nil)
|
120
|
-
label = nil
|
121
|
-
if itemtype == 'ip'
|
122
|
-
ip = IPAddr.new(item)
|
123
|
-
label = ip.reverse.gsub('.ip6.arpa', '').gsub('.in-addr.arpa', '')
|
124
|
-
elsif itemtype == 'domain'
|
125
|
-
label = normalize(item)
|
126
|
-
end
|
127
|
-
lookup = "#{label}.#{domain}"
|
128
|
-
if apikey
|
129
|
-
lookup = "#{apikey}.#{lookup}"
|
130
|
-
end
|
131
|
-
txid = lookup.sum
|
132
|
-
message = Resolv::DNS::Message.new(txid)
|
133
|
-
message.rd = 1
|
134
|
-
message.add_question(lookup,Resolv::DNS::Resource::IN::A)
|
135
|
-
message.encode
|
136
|
-
end
|
137
|
-
|
138
|
-
|
139
|
-
# lookup performs the sending of DNS queries for the given items
|
118
|
+
|
119
|
+
# lookup performs the sending of DNS queries for the given items
|
140
120
|
# returns an array of DNSBLResult
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
121
|
+
def lookup(loopup_item)
|
122
|
+
# if item is an array, use it, otherwise make it one
|
123
|
+
items = Array loopup_item
|
124
|
+
|
125
|
+
# place the results in the results array
|
126
|
+
results = []
|
127
|
+
# for each ip or hostname
|
128
|
+
items.each do |item|
|
129
|
+
# sent is used to determine when we have all the answers
|
130
|
+
sent = 0
|
131
|
+
# record the start time
|
132
|
+
@starttime = Time.now.to_f
|
133
|
+
# determine the type of query
|
134
|
+
itemtype = item.match?(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) ? 'ip' : 'domain'
|
135
|
+
if item.match?(/^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/) # rubocop: disable Layout/LineLength
|
136
|
+
itemtype = 'ip'
|
137
|
+
end
|
138
|
+
|
139
|
+
# for each dnsbl that supports our type, create the DNS query packet and send it
|
140
|
+
# rotate across our configured name servers and increment sent
|
141
|
+
@dnsbls.each do |name, config|
|
142
|
+
next if config['disabled']
|
143
|
+
next unless config['type'] == itemtype
|
144
|
+
|
145
|
+
begin
|
146
|
+
msg = encode_query item, itemtype, config['domain'], config['apikey']
|
147
|
+
@sockets[@socket_index].send msg, 0
|
148
|
+
@socket_index += 1
|
149
|
+
@socket_index %= @sockets.length
|
150
|
+
sent += 1
|
151
|
+
rescue StandardError => e
|
152
|
+
puts "error for #{name}: e"
|
153
|
+
puts e.backtrace.join("\n")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# while we still expect answers
|
158
|
+
while sent.positive?
|
159
|
+
# wait on the socket for maximally @timeout seconds
|
160
|
+
r, = IO.select @sockets, nil, nil, @timeout
|
161
|
+
# if we time out, break out of the loop
|
162
|
+
break unless r
|
158
163
|
|
159
|
-
|
160
|
-
# rotate across our configured name servers and increment sent
|
161
|
-
@dnsbls.each do |name,config|
|
162
|
-
next if config['disabled']
|
163
|
-
next unless config['type'] == itemtype
|
164
|
-
begin
|
165
|
-
msg = _encode_query(item,itemtype,config['domain'],config['apikey'])
|
166
|
-
@sockets[@socket_index].send(msg,0)
|
167
|
-
@socket_index += 1
|
168
|
-
@socket_index %= @sockets.length
|
169
|
-
sent += 1
|
170
|
-
rescue Exception => e
|
171
|
-
puts e
|
172
|
-
puts e.backtrace.join("\n")
|
173
|
-
end
|
174
|
-
end
|
175
|
-
# while we still expect answers
|
176
|
-
while sent > 0
|
177
|
-
# wait on the socket for maximally @timeout seconds
|
178
|
-
r,_,_ = IO.select(@sockets,nil,nil,@timeout)
|
179
|
-
# if we time out, break out of the loop
|
180
|
-
break unless r
|
181
|
-
# for each reply, decode it and receive results, decrement the pending answers
|
164
|
+
# for each reply, decode it and receive results, decrement the pending answers
|
182
165
|
first_only = false
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
166
|
+
r.each do |s|
|
167
|
+
begin
|
168
|
+
response = decode_response(s.recv(4096))
|
169
|
+
results += response
|
170
|
+
rescue StandardError => e
|
171
|
+
puts e
|
172
|
+
puts e.backtrace.join("\n")
|
173
|
+
end
|
174
|
+
sent -= 1
|
192
175
|
if @first_only
|
193
176
|
first_only = true
|
194
177
|
break
|
195
178
|
end
|
196
|
-
end
|
197
|
-
if first_only
|
198
|
-
break
|
199
179
|
end
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
180
|
+
break if first_only
|
181
|
+
end
|
182
|
+
end
|
183
|
+
results
|
184
|
+
end
|
185
|
+
|
205
186
|
private
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
187
|
+
|
188
|
+
# converts an ip or a hostname into the DNS query packet requires to lookup the result
|
189
|
+
def encode_query(item, itemtype, domain, apikey = nil)
|
190
|
+
label = case itemtype
|
191
|
+
when 'ip'
|
192
|
+
ip = IPAddr.new item
|
193
|
+
ip.reverse.gsub('.ip6.arpa', '').gsub('.in-addr.arpa', '')
|
194
|
+
when 'domain'
|
195
|
+
normalize item
|
196
|
+
end
|
197
|
+
|
198
|
+
lookup = "#{label}.#{domain}"
|
199
|
+
lookup = "#{apikey}.#{lookup}" if apikey
|
200
|
+
txid = lookup.sum
|
201
|
+
message = Resolv::DNS::Message.new txid
|
202
|
+
message.rd = 1
|
203
|
+
message.add_question lookup, Resolv::DNS::Resource::IN::A
|
204
|
+
message.encode
|
205
|
+
end
|
206
|
+
|
207
|
+
# takes a DNS response and converts it into a DNSBLResult
|
208
|
+
def decode_response(buf)
|
209
|
+
reply = Resolv::DNS::Message.decode buf
|
210
|
+
results = []
|
211
|
+
reply.each_answer do |name, _ttl, data|
|
212
|
+
if name.to_s =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(.+)$/
|
213
|
+
ip = [Regexp.last_match(4),
|
214
|
+
Regexp.last_match(3),
|
215
|
+
Regexp.last_match(2),
|
216
|
+
Regexp.last_match(1)].join '.'
|
217
|
+
domain = Regexp.last_match 5
|
218
|
+
@dnsbls.each do |dnsblname, config|
|
219
|
+
next unless data.is_a? Resolv::DNS::Resource::IN::A
|
220
|
+
next unless domain == config['domain']
|
221
|
+
|
222
|
+
meaning = config[data.address.to_s] || data.address.to_s
|
223
|
+
results << DNSBLResult.new(dnsblname, ip, name.to_s, data.address.to_s, meaning, Time.now.to_f - @starttime)
|
224
|
+
break
|
225
|
+
end
|
226
|
+
else
|
227
|
+
@dnsbls.each do |dnsblname, config|
|
228
|
+
next unless name.to_s.end_with? config['domain']
|
229
|
+
|
230
|
+
meaning = if config['decoder']
|
231
|
+
send "#{config['decoder']}_decoder".to_sym, data.address.to_s
|
232
|
+
elsif config[data.address.to_s]
|
233
|
+
config[data.address.to_s]
|
234
|
+
else
|
235
|
+
data.address.to_s
|
236
|
+
end
|
237
|
+
|
238
|
+
results << DNSBLResult.new(dnsblname, name.to_s.gsub(".#{config['domain']}", ''),
|
239
|
+
name.to_s,
|
240
|
+
data.address.to_s,
|
241
|
+
meaning,
|
242
|
+
Time.now.to_f - @starttime)
|
243
|
+
break
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
results
|
248
|
+
end
|
249
|
+
|
243
250
|
# decodes the response from Project Honey Pot's service
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
if (flags & 0xf8) > 0
|
270
|
-
types << "reserved"
|
271
|
-
end
|
272
|
-
type = types.join(",")
|
273
|
-
return "days=#{days},score=#{threatscore},type=#{type}"
|
274
|
-
end
|
275
|
-
end
|
276
|
-
end
|
251
|
+
def phpot_decoder(ip)
|
252
|
+
octets = ip.split '.'
|
253
|
+
if octets.length != 4 || octets[0] != '127'
|
254
|
+
'invalid response'
|
255
|
+
elsif octets[3] == '0'
|
256
|
+
search_engines = %w[undocumented AltaVista Ask Baidu Excite Google Looksmart Lycos MSN Yahoo Cuil InfoSeek Miscellaneous]
|
257
|
+
sindex = octets[2].to_i
|
258
|
+
if sindex >= search_engines.length
|
259
|
+
'type=search engine,engine=unknown'
|
260
|
+
else
|
261
|
+
"type=search engine,engine=#{search_engines[sindex]}"
|
262
|
+
end
|
263
|
+
else
|
264
|
+
days, threatscore, flags = octets[1, 3]
|
265
|
+
flags = flags.to_i
|
266
|
+
types = []
|
267
|
+
types << 'suspicious' if (flags & 0x1) == 0x1
|
268
|
+
types << 'harvester' if (flags & 0x2) == 0x2
|
269
|
+
types << 'comment spammer' if (flags & 0x4) == 0x4
|
270
|
+
types << 'reserved' if (flags & 0xf8).positive?
|
271
|
+
type = types.join ','
|
272
|
+
"days=#{days},score=#{threatscore},type=#{type}"
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
277
276
|
end
|
data/test/helper.rb
CHANGED
data/test/test_dnsbl-client.rb
CHANGED
@@ -1,203 +1,209 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
unless Kernel.respond_to? :require_relative
|
2
4
|
module Kernel
|
3
5
|
def require_relative(path)
|
4
|
-
require File.join(File.dirname(caller
|
6
|
+
require File.join(File.dirname(caller(1..1).first), path.to_str)
|
5
7
|
end
|
6
8
|
end
|
7
9
|
end
|
8
10
|
|
9
11
|
require_relative 'helper'
|
10
12
|
|
11
|
-
$nameservers = [['4.2.2.2',53]]
|
12
|
-
|
13
13
|
class TestDNSBLClient < Minitest::Test
|
14
|
+
NAME_SERVERS = [['4.2.2.2', 53]].freeze
|
15
|
+
|
14
16
|
def test_return_no_hits_for_0_0_0_254
|
15
17
|
c = DNSBL::Client.new
|
16
|
-
c.nameservers =
|
18
|
+
c.nameservers = NAME_SERVERS
|
17
19
|
# for some reason DRONEBL returns 127.0.0.255 when queried for 127.0.0.255, so I'll use 127.0.0.254
|
18
20
|
# spfbl started returning 127.0.0.254 for 127.0.0.254, so I'll try 0.0.0.254
|
19
|
-
res = c.lookup
|
20
|
-
if res.length
|
21
|
-
|
22
|
-
end
|
23
|
-
assert_equal(0,res.length)
|
21
|
+
res = c.lookup '0.0.0.254'
|
22
|
+
puts res if res.length.positive?
|
23
|
+
assert res.length.zero?
|
24
24
|
end
|
25
|
+
|
25
26
|
def test_return_all_lists_for_127_0_0_2
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
skip 'not work anymore'
|
28
|
+
|
29
|
+
c = DNSBL::Client.new
|
30
|
+
c.nameservers = NAME_SERVERS
|
31
|
+
res = c.lookup '127.0.0.2'
|
32
|
+
c.dnsbls.each do |bls|
|
33
|
+
assert (res.detect { |ci| ci.dnsbl == bls }), "#{bls} missing"
|
34
|
+
end
|
35
|
+
assert res.count >= c.dnsbls.count
|
33
36
|
end
|
37
|
+
|
34
38
|
def test_return_results_for_bad_domains
|
35
39
|
c = DNSBL::Client.new
|
36
|
-
c.nameservers =
|
37
|
-
res = c.lookup
|
38
|
-
assert
|
40
|
+
c.nameservers = NAME_SERVERS
|
41
|
+
res = c.lookup 'pfizer.viagra.aqybasej.gurdoctor.com'
|
42
|
+
assert res.length >= 0
|
39
43
|
end
|
44
|
+
|
40
45
|
def test_interpret_project_honeypot_results
|
41
|
-
|
42
|
-
|
43
|
-
|
46
|
+
apikey = ENV.fetch 'PHPAPIKEY', nil
|
47
|
+
skip 'Project Honeypot API Key Required for this test. Please set PHPAPIKEY.' unless apikey
|
48
|
+
|
49
|
+
config = YAML.safe_load("---
|
44
50
|
PROJECTHONEYPOT:
|
45
51
|
domain: dnsbl.httpbl.org
|
46
52
|
type: ip
|
47
53
|
apikey: #{apikey}
|
48
|
-
decoder:
|
49
|
-
c = DNSBL::Client.new
|
50
|
-
c.nameservers =
|
51
|
-
res = c.lookup
|
52
|
-
assert_equal
|
53
|
-
res = c.lookup
|
54
|
-
assert_equal
|
55
|
-
assert_equal
|
56
|
-
assert_equal
|
57
|
-
assert_equal
|
58
|
-
res = c.lookup
|
59
|
-
assert_equal
|
60
|
-
assert_equal
|
61
|
-
assert_equal
|
62
|
-
assert_equal
|
63
|
-
assert_equal
|
64
|
-
res = c.lookup
|
65
|
-
assert_equal
|
66
|
-
assert_equal
|
67
|
-
assert_equal
|
68
|
-
assert_equal
|
69
|
-
assert_equal
|
70
|
-
res = c.lookup
|
71
|
-
assert_equal
|
72
|
-
assert_equal
|
73
|
-
assert_equal
|
74
|
-
assert_equal
|
75
|
-
assert_equal
|
76
|
-
res = c.lookup
|
77
|
-
assert_equal
|
78
|
-
assert_equal
|
79
|
-
assert_equal
|
80
|
-
assert_equal
|
81
|
-
assert_equal
|
82
|
-
res = c.lookup
|
83
|
-
assert_equal
|
84
|
-
assert_equal
|
85
|
-
assert_equal
|
86
|
-
assert_equal
|
87
|
-
assert_equal
|
88
|
-
res = c.lookup
|
89
|
-
assert_equal
|
90
|
-
assert_equal
|
91
|
-
assert_equal
|
92
|
-
assert_equal
|
93
|
-
assert_equal
|
94
|
-
res = c.lookup
|
95
|
-
assert_equal
|
96
|
-
assert_equal
|
97
|
-
assert_equal
|
98
|
-
assert_equal
|
99
|
-
assert_equal
|
100
|
-
res = c.lookup
|
101
|
-
assert_equal
|
102
|
-
assert_equal
|
103
|
-
assert_equal
|
104
|
-
assert_equal
|
105
|
-
assert_equal
|
106
|
-
res = c.lookup
|
107
|
-
assert_equal
|
108
|
-
assert_equal
|
109
|
-
assert_equal
|
110
|
-
assert_equal
|
111
|
-
assert_equal
|
112
|
-
res = c.lookup
|
113
|
-
assert_equal
|
114
|
-
assert_equal
|
115
|
-
assert_equal
|
116
|
-
assert_equal
|
117
|
-
assert_equal
|
118
|
-
res = c.lookup
|
119
|
-
assert_equal
|
120
|
-
assert_equal
|
121
|
-
assert_equal
|
122
|
-
assert_equal
|
123
|
-
assert_equal
|
124
|
-
res = c.lookup
|
125
|
-
assert_equal
|
126
|
-
assert_equal
|
127
|
-
assert_equal
|
128
|
-
assert_equal
|
129
|
-
assert_equal
|
130
|
-
res = c.lookup
|
131
|
-
assert_equal
|
132
|
-
assert_equal
|
133
|
-
assert_equal
|
134
|
-
assert_equal
|
135
|
-
assert_equal
|
136
|
-
res = c.lookup
|
137
|
-
assert_equal
|
138
|
-
assert_equal
|
139
|
-
assert_equal
|
140
|
-
assert_equal
|
141
|
-
assert_equal
|
142
|
-
res = c.lookup
|
143
|
-
assert_equal
|
144
|
-
assert_equal
|
145
|
-
assert_equal
|
146
|
-
assert_equal
|
147
|
-
assert_equal
|
148
|
-
res = c.
|
149
|
-
assert_equal
|
150
|
-
res = c.
|
151
|
-
assert_equal
|
152
|
-
res = c.
|
153
|
-
assert_equal
|
154
|
-
res = c.
|
155
|
-
assert_equal
|
156
|
-
res = c.
|
157
|
-
assert_equal
|
158
|
-
res = c.
|
159
|
-
assert_equal
|
160
|
-
res = c.
|
161
|
-
assert_equal
|
162
|
-
res = c.
|
163
|
-
assert_equal
|
164
|
-
res = c.
|
165
|
-
assert_equal
|
166
|
-
res = c.
|
167
|
-
assert_equal
|
168
|
-
res = c.
|
169
|
-
assert_equal
|
170
|
-
res = c.
|
171
|
-
assert_equal
|
172
|
-
res = c.
|
173
|
-
assert_equal
|
54
|
+
decoder: phpot")
|
55
|
+
c = DNSBL::Client.new config
|
56
|
+
c.nameservers = NAME_SERVERS
|
57
|
+
res = c.lookup '127.0.0.1'
|
58
|
+
assert_equal 0, res.length
|
59
|
+
res = c.lookup '127.1.1.0'
|
60
|
+
assert_equal 1, res.length
|
61
|
+
assert_equal "#{apikey}.0.1.1.127.dnsbl.httpbl.org", res[0].query
|
62
|
+
assert_equal '127.1.1.0', res[0].result
|
63
|
+
assert_equal 'type=search engine,engine=AltaVista', res[0].meaning
|
64
|
+
res = c.lookup '127.1.1.1'
|
65
|
+
assert_equal 1, res.length
|
66
|
+
assert_equal "#{apikey}.1.1.1.127", res[0].item
|
67
|
+
assert_equal "#{apikey}.1.1.1.127.dnsbl.httpbl.org", res[0].query
|
68
|
+
assert_equal '127.1.1.1', res[0].result
|
69
|
+
assert_equal 'days=1,score=1,type=suspicious', res[0].meaning
|
70
|
+
res = c.lookup '127.1.1.2'
|
71
|
+
assert_equal 1, res.length
|
72
|
+
assert_equal "#{apikey}.2.1.1.127", res[0].item
|
73
|
+
assert_equal "#{apikey}.2.1.1.127.dnsbl.httpbl.org", res[0].query
|
74
|
+
assert_equal '127.1.1.2', res[0].result
|
75
|
+
assert_equal 'days=1,score=1,type=harvester', res[0].meaning
|
76
|
+
res = c.lookup '127.1.1.3'
|
77
|
+
assert_equal 1, res.length
|
78
|
+
assert_equal "#{apikey}.3.1.1.127", res[0].item
|
79
|
+
assert_equal "#{apikey}.3.1.1.127.dnsbl.httpbl.org", res[0].query
|
80
|
+
assert_equal '127.1.1.3', res[0].result
|
81
|
+
assert_equal 'days=1,score=1,type=suspicious,harvester', res[0].meaning
|
82
|
+
res = c.lookup '127.1.1.4'
|
83
|
+
assert_equal 1, res.length
|
84
|
+
assert_equal "#{apikey}.4.1.1.127", res[0].item
|
85
|
+
assert_equal "#{apikey}.4.1.1.127.dnsbl.httpbl.org", res[0].query
|
86
|
+
assert_equal '127.1.1.4', res[0].result
|
87
|
+
assert_equal 'days=1,score=1,type=comment spammer', res[0].meaning
|
88
|
+
res = c.lookup '127.1.1.5'
|
89
|
+
assert_equal 1, res.length
|
90
|
+
assert_equal "#{apikey}.5.1.1.127", res[0].item
|
91
|
+
assert_equal "#{apikey}.5.1.1.127.dnsbl.httpbl.org", res[0].query
|
92
|
+
assert_equal '127.1.1.5', res[0].result
|
93
|
+
assert_equal 'days=1,score=1,type=suspicious,comment spammer', res[0].meaning
|
94
|
+
res = c.lookup '127.1.1.6'
|
95
|
+
assert_equal 1, res.length
|
96
|
+
assert_equal "#{apikey}.6.1.1.127", res[0].item
|
97
|
+
assert_equal "#{apikey}.6.1.1.127.dnsbl.httpbl.org", res[0].query
|
98
|
+
assert_equal '127.1.1.6', res[0].result
|
99
|
+
assert_equal 'days=1,score=1,type=harvester,comment spammer', res[0].meaning
|
100
|
+
res = c.lookup '127.1.1.7'
|
101
|
+
assert_equal 1, res.length
|
102
|
+
assert_equal "#{apikey}.7.1.1.127", res[0].item
|
103
|
+
assert_equal "#{apikey}.7.1.1.127.dnsbl.httpbl.org", res[0].query
|
104
|
+
assert_equal '127.1.1.7', res[0].result
|
105
|
+
assert_equal 'days=1,score=1,type=suspicious,harvester,comment spammer', res[0].meaning
|
106
|
+
res = c.lookup '127.1.10.1'
|
107
|
+
assert_equal 1, res.length
|
108
|
+
assert_equal "#{apikey}.1.10.1.127", res[0].item
|
109
|
+
assert_equal "#{apikey}.1.10.1.127.dnsbl.httpbl.org", res[0].query
|
110
|
+
assert_equal '127.1.10.1', res[0].result
|
111
|
+
assert_equal 'days=1,score=10,type=suspicious', res[0].meaning
|
112
|
+
res = c.lookup '127.1.20.1'
|
113
|
+
assert_equal 1, res.length
|
114
|
+
assert_equal "#{apikey}.1.20.1.127", res[0].item
|
115
|
+
assert_equal "#{apikey}.1.20.1.127.dnsbl.httpbl.org", res[0].query
|
116
|
+
assert_equal '127.1.20.1', res[0].result
|
117
|
+
assert_equal 'days=1,score=20,type=suspicious', res[0].meaning
|
118
|
+
res = c.lookup '127.1.40.1'
|
119
|
+
assert_equal 1, res.length
|
120
|
+
assert_equal "#{apikey}.1.40.1.127", res[0].item
|
121
|
+
assert_equal "#{apikey}.1.40.1.127.dnsbl.httpbl.org", res[0].query
|
122
|
+
assert_equal '127.1.40.1', res[0].result
|
123
|
+
assert_equal 'days=1,score=40,type=suspicious', res[0].meaning
|
124
|
+
res = c.lookup '127.1.80.1'
|
125
|
+
assert_equal 1, res.length
|
126
|
+
assert_equal "#{apikey}.1.80.1.127", res[0].item
|
127
|
+
assert_equal "#{apikey}.1.80.1.127.dnsbl.httpbl.org", res[0].query
|
128
|
+
assert_equal '127.1.80.1', res[0].result
|
129
|
+
assert_equal 'days=1,score=80,type=suspicious', res[0].meaning
|
130
|
+
res = c.lookup '127.10.1.1'
|
131
|
+
assert_equal 1, res.length
|
132
|
+
assert_equal "#{apikey}.1.1.10.127", res[0].item
|
133
|
+
assert_equal "#{apikey}.1.1.10.127.dnsbl.httpbl.org", res[0].query
|
134
|
+
assert_equal '127.10.1.1', res[0].result
|
135
|
+
assert_equal 'days=10,score=1,type=suspicious', res[0].meaning
|
136
|
+
res = c.lookup '127.20.1.1'
|
137
|
+
assert_equal 1, res.length
|
138
|
+
assert_equal "#{apikey}.1.1.20.127", res[0].item
|
139
|
+
assert_equal "#{apikey}.1.1.20.127.dnsbl.httpbl.org", res[0].query
|
140
|
+
assert_equal '127.20.1.1', res[0].result
|
141
|
+
assert_equal 'days=20,score=1,type=suspicious', res[0].meaning
|
142
|
+
res = c.lookup '127.40.1.1'
|
143
|
+
assert_equal 1, res.length
|
144
|
+
assert_equal "#{apikey}.1.1.40.127", res[0].item
|
145
|
+
assert_equal "#{apikey}.1.1.40.127.dnsbl.httpbl.org", res[0].query
|
146
|
+
assert_equal '127.40.1.1', res[0].result
|
147
|
+
assert_equal 'days=40,score=1,type=suspicious', res[0].meaning
|
148
|
+
res = c.lookup '127.80.1.1'
|
149
|
+
assert_equal 1, res.length
|
150
|
+
assert_equal "#{apikey}.1.1.80.127", res[0].item
|
151
|
+
assert_equal "#{apikey}.1.1.80.127.dnsbl.httpbl.org", res[0].query
|
152
|
+
assert_equal '127.80.1.1', res[0].result
|
153
|
+
assert_equal 'days=80,score=1,type=suspicious', res[0].meaning
|
154
|
+
res = c.send :phpot_decoder, '127.0.0.0'
|
155
|
+
assert_equal 'type=search engine,engine=undocumented', res
|
156
|
+
res = c.send :phpot_decoder, '127.0.1.0'
|
157
|
+
assert_equal 'type=search engine,engine=AltaVista', res
|
158
|
+
res = c.send :phpot_decoder, '127.0.2.0'
|
159
|
+
assert_equal 'type=search engine,engine=Ask', res
|
160
|
+
res = c.send :phpot_decoder, '127.0.3.0'
|
161
|
+
assert_equal 'type=search engine,engine=Baidu', res
|
162
|
+
res = c.send :phpot_decoder, '127.0.4.0'
|
163
|
+
assert_equal 'type=search engine,engine=Excite', res
|
164
|
+
res = c.send :phpot_decoder, '127.0.5.0'
|
165
|
+
assert_equal 'type=search engine,engine=Google', res
|
166
|
+
res = c.send :phpot_decoder, '127.0.6.0'
|
167
|
+
assert_equal 'type=search engine,engine=Looksmart', res
|
168
|
+
res = c.send :phpot_decoder, '127.0.7.0'
|
169
|
+
assert_equal 'type=search engine,engine=Lycos', res
|
170
|
+
res = c.send :phpot_decoder, '127.0.8.0'
|
171
|
+
assert_equal 'type=search engine,engine=MSN', res
|
172
|
+
res = c.send :phpot_decoder, '127.0.9.0'
|
173
|
+
assert_equal 'type=search engine,engine=Yahoo', res
|
174
|
+
res = c.send :phpot_decoder, '127.0.10.0'
|
175
|
+
assert_equal 'type=search engine,engine=Cuil', res
|
176
|
+
res = c.send :phpot_decoder, '127.0.11.0'
|
177
|
+
assert_equal 'type=search engine,engine=InfoSeek', res
|
178
|
+
res = c.send :phpot_decoder, '127.0.12.0'
|
179
|
+
assert_equal 'type=search engine,engine=Miscellaneous', res
|
174
180
|
end
|
175
181
|
|
176
182
|
def test_normalize_domains_to_two_levels_if_it_s_neither_in_two_level_nor_three_level_list
|
177
183
|
c = DNSBL::Client.new
|
178
|
-
c.nameservers =
|
184
|
+
c.nameservers = NAME_SERVERS
|
179
185
|
|
180
|
-
assert_equal
|
181
|
-
assert_equal
|
182
|
-
assert_equal
|
186
|
+
assert_equal 'example.org', c.normalize('example.org')
|
187
|
+
assert_equal 'example.org', c.normalize('www.example.org')
|
188
|
+
assert_equal 'example.org', c.normalize('foo.bar.baz.example.org')
|
183
189
|
end
|
184
190
|
|
185
191
|
def test_normaize_domains_to_three_levels_if_it_s_in_two_level_list
|
186
192
|
c = DNSBL::Client.new
|
187
|
-
c.nameservers =
|
193
|
+
c.nameservers = NAME_SERVERS
|
188
194
|
|
189
|
-
assert_equal
|
190
|
-
assert_equal
|
191
|
-
assert_equal
|
192
|
-
assert_equal
|
195
|
+
assert_equal 'example.co.uk', c.normalize('example.co.uk')
|
196
|
+
assert_equal 'example.co.uk', c.normalize('www.example.co.uk')
|
197
|
+
assert_equal 'example.co.uk', c.normalize('foo.bar.baz.example.co.uk')
|
198
|
+
assert_equal 'example.blogspot.com', c.normalize('example.blogspot.com')
|
193
199
|
end
|
194
200
|
|
195
201
|
def test_normalize_domains_to_four_levels_if_it_s_in_three_level_list
|
196
202
|
c = DNSBL::Client.new
|
197
|
-
c.nameservers =
|
203
|
+
c.nameservers = NAME_SERVERS
|
198
204
|
|
199
|
-
assert_equal
|
200
|
-
assert_equal
|
201
|
-
assert_equal
|
205
|
+
assert_equal 'example.act.edu.au', c.normalize('example.act.edu.au')
|
206
|
+
assert_equal 'example.act.edu.au', c.normalize('www.example.act.edu.au')
|
207
|
+
assert_equal 'example.act.edu.au', c.normalize('foo.bar.example.act.edu.au')
|
202
208
|
end
|
203
209
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dnsbl-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- chrislee35
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-07-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -38,6 +38,48 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '13'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-performance
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
41
83
|
description: simple interface to lookup blacklists results
|
42
84
|
email:
|
43
85
|
- rubygems@chrislee.dhs.org
|
@@ -66,7 +108,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
108
|
requirements:
|
67
109
|
- - ">="
|
68
110
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
111
|
+
version: '2.7'
|
70
112
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
113
|
requirements:
|
72
114
|
- - ">="
|