dnsbl-client 1.0.7 → 1.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 +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
|
- - ">="
|