passivedns-client 2.0.6 → 2.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/.gitignore +1 -0
- data/README.md +6 -0
- data/Rakefile +8 -0
- data/lib/passivedns/client.rb +14 -7
- data/lib/passivedns/client/cli.rb +34 -6
- data/lib/passivedns/client/passivedb.rb +6 -1
- data/lib/passivedns/client/provider/bfk.rb +102 -0
- data/lib/passivedns/client/provider/circl.rb +111 -0
- data/lib/passivedns/client/provider/cn360.rb +108 -0
- data/lib/passivedns/client/provider/dnsdb.rb +110 -0
- data/lib/passivedns/client/provider/mnemonic.rb +98 -0
- data/lib/passivedns/client/provider/passivetotal.rb +103 -0
- data/lib/passivedns/client/provider/riskiq.rb +130 -0
- data/lib/passivedns/client/provider/tcpiputils.rb +118 -0
- data/lib/passivedns/client/provider/virustotal.rb +105 -0
- data/lib/passivedns/client/state.rb +30 -2
- data/lib/passivedns/client/version.rb +4 -2
- data/test/test_cli.rb +6 -5
- data/test/test_passivedns-client.rb +33 -8
- metadata +11 -10
- data/lib/passivedns/client/providers/bfk.rb +0 -77
- data/lib/passivedns/client/providers/circl.rb +0 -79
- data/lib/passivedns/client/providers/cn360.rb +0 -80
- data/lib/passivedns/client/providers/dnsdb.rb +0 -85
- data/lib/passivedns/client/providers/mnemonic.rb +0 -72
- data/lib/passivedns/client/providers/passivetotal.rb +0 -77
- data/lib/passivedns/client/providers/tcpiputils.rb +0 -92
- data/lib/passivedns/client/providers/virustotal.rb +0 -78
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'openssl'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# Please read http://www.tcpiputils.com/terms-of-service under automated requests
|
7
|
+
|
8
|
+
module PassiveDNS #:nodoc: don't document this
|
9
|
+
# The Provider module contains all the Passive DNS provider client code
|
10
|
+
module Provider
|
11
|
+
# Queries TCPIPUtils's passive DNS database
|
12
|
+
class TCPIPUtils < PassiveDB
|
13
|
+
# Sets the modules self-reported name to "TCPIPUtils"
|
14
|
+
def self.name
|
15
|
+
"TCPIPUtils"
|
16
|
+
end
|
17
|
+
# Sets the configuration section name to "tcpiputils"
|
18
|
+
def self.config_section_name
|
19
|
+
"tcpiputils"
|
20
|
+
end
|
21
|
+
# Sets the command line database argument to "t"
|
22
|
+
def self.option_letter
|
23
|
+
"t"
|
24
|
+
end
|
25
|
+
|
26
|
+
# :debug enables verbose logging to standard output
|
27
|
+
attr_accessor :debug
|
28
|
+
# === Options
|
29
|
+
# * :debug Sets the debug flag for the module
|
30
|
+
# * "APIKEY" REQUIRED: The API key associated with TCPIPUtils
|
31
|
+
# * "URL" Alternate url for testing. Defaults to "https://www.utlsapi.com/api.php?version=1.0&apikey="
|
32
|
+
#
|
33
|
+
# === Example Instantiation
|
34
|
+
#
|
35
|
+
# options = {
|
36
|
+
# :debug => true,
|
37
|
+
# "APIKEY" => "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
38
|
+
# "URL" => "https://www.utlsapi.com/api.php?version=1.0&apikey="
|
39
|
+
# }
|
40
|
+
#
|
41
|
+
# PassiveDNS::Provider::TCPIPUtils.new(options)
|
42
|
+
#
|
43
|
+
def initialize(options={})
|
44
|
+
@debug = options[:debug] || false
|
45
|
+
@apikey = options["APIKEY"] || raise("#{self.class.name} requires an APIKEY. See README.md")
|
46
|
+
@url = options["URL"] || "https://www.utlsapi.com/api.php?version=1.0&apikey="
|
47
|
+
end
|
48
|
+
|
49
|
+
# Takes a label (either a domain or an IP address) and returns
|
50
|
+
# an array of PassiveDNS::PDNSResult instances with the answers to the query
|
51
|
+
def lookup(label, limit=nil)
|
52
|
+
$stderr.puts "DEBUG: #{self.class.name}.lookup(#{label})" if @debug
|
53
|
+
type = (label.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) ? "domainneighbors" : "domainipdnshistory"
|
54
|
+
url = "#{@url}#{@apikey}&type=#{type}&q=#{label}"
|
55
|
+
recs = []
|
56
|
+
Timeout::timeout(240) {
|
57
|
+
url = URI.parse url
|
58
|
+
http = Net::HTTP.new(url.host, url.port)
|
59
|
+
http.use_ssl = (url.scheme == 'https')
|
60
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
61
|
+
http.verify_depth = 5
|
62
|
+
request = Net::HTTP::Get.new(url.path+"?"+url.query)
|
63
|
+
request.add_field("User-Agent", "Ruby/#{RUBY_VERSION} passivedns-client rubygem v#{PassiveDNS::Client::VERSION}")
|
64
|
+
t1 = Time.now
|
65
|
+
response = http.request(request)
|
66
|
+
delta = (Time.now - t1).to_f
|
67
|
+
reply = JSON.parse(response.body)
|
68
|
+
if reply["status"] and reply["status"] == "succeed"
|
69
|
+
question = reply["data"]["question"]
|
70
|
+
recs = format_recs(reply["data"], question, delta)
|
71
|
+
elsif reply["status"] and reply["status"] == "error"
|
72
|
+
raise "#{self.class.name}: error from web API: #{reply["data"]}"
|
73
|
+
end
|
74
|
+
if limit
|
75
|
+
recs[0,limit]
|
76
|
+
else
|
77
|
+
recs
|
78
|
+
end
|
79
|
+
}
|
80
|
+
rescue Timeout::Error => e
|
81
|
+
$stderr.puts "#{self.class.name} lookup timed out: #{label}"
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# translates the data structure derived from of tcpiputils's JSON reply
|
87
|
+
def format_recs(reply_data, question, delta)
|
88
|
+
recs = []
|
89
|
+
reply_data.each do |key, data|
|
90
|
+
case key
|
91
|
+
when "ipv4"
|
92
|
+
data.each do |rec|
|
93
|
+
recs << PDNSResult.new(self.class.name, delta, question, rec["ip"], "A", nil, nil, rec["updatedate"], nil)
|
94
|
+
end
|
95
|
+
when "ipv6"
|
96
|
+
data.each do |rec|
|
97
|
+
recs << PDNSResult.new(self.class.name, delta, question, rec["ip"], "AAAA", nil, nil, rec["updatedate"], nil)
|
98
|
+
end
|
99
|
+
when "dns"
|
100
|
+
data.each do |rec|
|
101
|
+
recs << PDNSResult.new(self.class.name, delta, question, rec["dns"], "NS", nil, nil, rec["updatedate"], nil)
|
102
|
+
end
|
103
|
+
when "mx"
|
104
|
+
data.each do |rec|
|
105
|
+
recs << PDNSResult.new(self.class.name, delta, question, rec["dns"], "MX", nil, nil, rec["updatedate"], nil)
|
106
|
+
end
|
107
|
+
when "domains"
|
108
|
+
data.each do |rec|
|
109
|
+
recs << PDNSResult.new(self.class.name, delta, rec, question, "A", nil, nil, nil, nil)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
recs
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# DESCRIPTION: this is a module for pdns.rb, primarily used by pdnstool.rb, to query VirusTotal's passive DNS database
|
2
|
+
require 'net/http'
|
3
|
+
require 'net/https'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
module PassiveDNS #:nodoc: don't document this
|
7
|
+
# The Provider module contains all the Passive DNS provider client code
|
8
|
+
module Provider
|
9
|
+
# Queries VirusTotal's passive DNS database
|
10
|
+
class VirusTotal < PassiveDB
|
11
|
+
# Sets the modules self-reported name to "VirusTotal"
|
12
|
+
def self.name
|
13
|
+
"VirusTotal"
|
14
|
+
end
|
15
|
+
# Sets the configuration section name to "virustotal"
|
16
|
+
def self.config_section_name
|
17
|
+
"virustotal"
|
18
|
+
end
|
19
|
+
# Sets the command line database argument to "v"
|
20
|
+
def self.option_letter
|
21
|
+
"v"
|
22
|
+
end
|
23
|
+
|
24
|
+
# :debug enables verbose logging to standard output
|
25
|
+
attr_accessor :debug
|
26
|
+
|
27
|
+
# === Options
|
28
|
+
# * :debug Sets the debug flag for the module
|
29
|
+
# * "APIKEY" Mandatory. API Key associated with your VirusTotal account
|
30
|
+
# * "URL" Alternate url for testing. Defaults to https://www.virustotal.com/vtapi/v2/
|
31
|
+
#
|
32
|
+
# === Example Instantiation
|
33
|
+
#
|
34
|
+
# options = {
|
35
|
+
# :debug => true,
|
36
|
+
# "APIKEY" => "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
37
|
+
# "URL" => "https://www.virustotal.com/vtapi/v2/"
|
38
|
+
# }
|
39
|
+
#
|
40
|
+
# PassiveDNS::Provider::VirusTotal.new(options)
|
41
|
+
#
|
42
|
+
def initialize(options={})
|
43
|
+
@debug = options[:debug] || false
|
44
|
+
@apikey = options["APIKEY"] || raise("#{self.class.name} requires an APIKEY. See README.md")
|
45
|
+
@url = options["URL"] || "https://www.virustotal.com/vtapi/v2/"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Takes a label (either a domain or an IP address) and returns
|
49
|
+
# an array of PassiveDNS::PDNSResult instances with the answers to the query
|
50
|
+
def lookup(label, limit=nil)
|
51
|
+
$stderr.puts "DEBUG: #{self.class.name}.lookup(#{label})" if @debug
|
52
|
+
Timeout::timeout(240) {
|
53
|
+
url = nil
|
54
|
+
if label =~ /^[\d\.]+$/
|
55
|
+
url = "#{@url}ip-address/report?ip=#{label}&apikey=#{@apikey}"
|
56
|
+
else
|
57
|
+
url = "#{@url}domain/report?domain=#{label}&apikey=#{@apikey}"
|
58
|
+
end
|
59
|
+
$stderr.puts "DEBUG: #{self.class.name} url = #{url}" if @debug
|
60
|
+
url = URI.parse url
|
61
|
+
http = Net::HTTP.new(url.host, url.port)
|
62
|
+
http.use_ssl = (url.scheme == 'https')
|
63
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
64
|
+
http.verify_depth = 5
|
65
|
+
request = Net::HTTP::Get.new(url.path+"?"+url.query)
|
66
|
+
request.add_field("User-Agent", "Ruby/#{RUBY_VERSION} passivedns-client rubygem v#{PassiveDNS::Client::VERSION}")
|
67
|
+
t1 = Time.now
|
68
|
+
response = http.request(request)
|
69
|
+
t2 = Time.now
|
70
|
+
recs = parse_json(response.body, label, t2-t1)
|
71
|
+
if limit
|
72
|
+
recs[0,limit]
|
73
|
+
else
|
74
|
+
recs
|
75
|
+
end
|
76
|
+
}
|
77
|
+
rescue Timeout::Error => e
|
78
|
+
$stderr.puts "#{self.class.name} lookup timed out: #{label}"
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# parses the response of virustotal's JSON reply to generate an array of PDNSResult
|
84
|
+
def parse_json(page,query,response_time=0)
|
85
|
+
res = []
|
86
|
+
# need to remove the json_class tag or the parser will crap itself trying to find a class to align it to
|
87
|
+
data = JSON.parse(page)
|
88
|
+
if data['resolutions']
|
89
|
+
data['resolutions'].each do |row|
|
90
|
+
if row['ip_address']
|
91
|
+
res << PDNSResult.new(self.class.name,response_time,query,row['ip_address'],'A',nil,nil,row['last_resolved'])
|
92
|
+
elsif row['hostname']
|
93
|
+
res << PDNSResult.new(self.class.name,response_time,row['hostname'],query,'A',nil,nil,row['last_resolved'])
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
res
|
98
|
+
rescue Exception => e
|
99
|
+
$stderr.puts "VirusTotal Exception: #{e}"
|
100
|
+
raise e
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -2,31 +2,39 @@ require 'sqlite3'
|
|
2
2
|
require 'yaml'
|
3
3
|
require 'structformatter'
|
4
4
|
|
5
|
-
module PassiveDNS
|
5
|
+
module PassiveDNS # :nodoc:
|
6
|
+
# struct to hold pending entries for query
|
6
7
|
class PDNSQueueEntry < Struct.new(:query, :state, :level); end
|
7
8
|
|
9
|
+
# holds state in memory of the queue to be queried, records returned, and the level of recursion
|
8
10
|
class PDNSToolState
|
11
|
+
# :debug enables verbose logging to standard output
|
9
12
|
attr_accessor :debug
|
13
|
+
# :level is the recursion depth
|
10
14
|
attr_reader :level
|
11
15
|
|
16
|
+
# creates a new, blank PDNSToolState instance
|
12
17
|
def initialize
|
13
18
|
@queue = []
|
14
19
|
@recs = []
|
15
20
|
@level = 0
|
16
21
|
end
|
17
|
-
|
22
|
+
|
23
|
+
# returns the next record
|
18
24
|
def next_result
|
19
25
|
@recs.each do |rec|
|
20
26
|
yield rec
|
21
27
|
end
|
22
28
|
end
|
23
29
|
|
30
|
+
# adds the record to the list of records received and tries to add the answer and query back to the queue for future query
|
24
31
|
def add_result(res)
|
25
32
|
@recs << res
|
26
33
|
add_query(res.answer,'pending')
|
27
34
|
add_query(res.query,'pending')
|
28
35
|
end
|
29
36
|
|
37
|
+
# sets the state of a given query
|
30
38
|
def update_query(query,state)
|
31
39
|
@queue.each do |q|
|
32
40
|
if q.query == query
|
@@ -37,6 +45,7 @@ module PassiveDNS
|
|
37
45
|
end
|
38
46
|
end
|
39
47
|
|
48
|
+
# returns the state of a provided query
|
40
49
|
def get_state(query)
|
41
50
|
@queue.each do |q|
|
42
51
|
if q.query == query
|
@@ -46,6 +55,7 @@ module PassiveDNS
|
|
46
55
|
false
|
47
56
|
end
|
48
57
|
|
58
|
+
# adding a query to the queue of things to be queried, but only if the query isn't already queued or answered
|
49
59
|
def add_query(query,state,level=@level+1)
|
50
60
|
if query =~ /^\d+ \w+\./
|
51
61
|
query = query.split(/ /,2)[1]
|
@@ -55,6 +65,7 @@ module PassiveDNS
|
|
55
65
|
@queue << PDNSQueueEntry.new(query,state,level)
|
56
66
|
end
|
57
67
|
|
68
|
+
# returns each query waiting on the queue
|
58
69
|
def each_query(max_level=20)
|
59
70
|
@queue.each do |q|
|
60
71
|
if q.state == 'pending' or q.state == 'failed'
|
@@ -67,6 +78,7 @@ module PassiveDNS
|
|
67
78
|
end
|
68
79
|
end
|
69
80
|
|
81
|
+
# transforms a set of results into GDF syntax
|
70
82
|
def to_gdf
|
71
83
|
output = "nodedef> name,description VARCHAR(12),color,style\n"
|
72
84
|
# IP "$node2,,white,1"
|
@@ -98,6 +110,7 @@ module PassiveDNS
|
|
98
110
|
output
|
99
111
|
end
|
100
112
|
|
113
|
+
# transforms a set of results into graphviz syntax
|
101
114
|
def to_graphviz
|
102
115
|
colors = {"MX" => "green", "A" => "blue", "CNAME" => "pink", "NS" => "red", "SOA" => "white", "PTR" => "purple", "TXT" => "brown"}
|
103
116
|
output = "graph pdns {\n"
|
@@ -119,6 +132,7 @@ module PassiveDNS
|
|
119
132
|
output += "}\n"
|
120
133
|
end
|
121
134
|
|
135
|
+
# transforms a set of results into graphml syntax
|
122
136
|
def to_graphml
|
123
137
|
output = '<?xml version="1.0" encoding="UTF-8"?>
|
124
138
|
<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
|
@@ -141,6 +155,7 @@ module PassiveDNS
|
|
141
155
|
output += '</graph></graphml>'+"\n"
|
142
156
|
end
|
143
157
|
|
158
|
+
# transforms a set of results into XML
|
144
159
|
def to_xml
|
145
160
|
output = '<?xml version="1.0" encoding="UTF-8" ?>'+"\n"
|
146
161
|
output += "<report>\n"
|
@@ -152,6 +167,7 @@ module PassiveDNS
|
|
152
167
|
output += "</report>\n"
|
153
168
|
end
|
154
169
|
|
170
|
+
# transforms a set of results into YAML
|
155
171
|
def to_yaml
|
156
172
|
output = ""
|
157
173
|
next_result do |rec|
|
@@ -160,6 +176,7 @@ module PassiveDNS
|
|
160
176
|
output
|
161
177
|
end
|
162
178
|
|
179
|
+
# transforms a set of results into JSON
|
163
180
|
def to_json
|
164
181
|
output = "[\n"
|
165
182
|
sep = ""
|
@@ -171,6 +188,7 @@ module PassiveDNS
|
|
171
188
|
output += "\n]\n"
|
172
189
|
end
|
173
190
|
|
191
|
+
# transforms a set of results into a text string
|
174
192
|
def to_s(sep="\t")
|
175
193
|
output = ""
|
176
194
|
next_result do |rec|
|
@@ -181,8 +199,11 @@ module PassiveDNS
|
|
181
199
|
end # class PDNSToolState
|
182
200
|
|
183
201
|
|
202
|
+
# creates persistence to the tool state by leveraging SQLite3
|
184
203
|
class PDNSToolStateDB < PDNSToolState
|
185
204
|
attr_reader :level
|
205
|
+
# creates an SQLite3-based Passive DNS Client state
|
206
|
+
# only argument is the filename of the sqlite3 database
|
186
207
|
def initialize(sqlitedb=nil)
|
187
208
|
puts "PDNSToolState initialize #{sqlitedb}" if @debug
|
188
209
|
@level = 0
|
@@ -204,6 +225,7 @@ module PassiveDNS
|
|
204
225
|
end
|
205
226
|
end
|
206
227
|
|
228
|
+
# creates the sqlite3 tables needed to track the state of this tool as itqueries and recurses
|
207
229
|
def create_tables
|
208
230
|
puts "creating tables" if @debug
|
209
231
|
@sqlitedbh.execute("create table results (query, answer, rrtype, ttl, firstseen, lastseen, ts REAL)")
|
@@ -214,6 +236,7 @@ module PassiveDNS
|
|
214
236
|
@sqlitedbh.execute("create index queue_state_idx on queue (state)")
|
215
237
|
end
|
216
238
|
|
239
|
+
# returns the next record
|
217
240
|
def next_result
|
218
241
|
rows = @sqlitedbh.execute("select query, answer, rrtype, ttl, firstseen, lastseen from results order by ts")
|
219
242
|
rows.each do |row|
|
@@ -221,6 +244,7 @@ module PassiveDNS
|
|
221
244
|
end
|
222
245
|
end
|
223
246
|
|
247
|
+
# adds the record to the list of records received and tries to add the answer and query back to the queue for future query
|
224
248
|
def add_result(res)
|
225
249
|
puts "adding result: #{res.to_s}" if @debug
|
226
250
|
curtime = Time.now().to_f
|
@@ -230,6 +254,7 @@ module PassiveDNS
|
|
230
254
|
add_query(res.query,'pending')
|
231
255
|
end
|
232
256
|
|
257
|
+
# adding a query to the queue of things to be queried, but only if the query isn't already queued or answered
|
233
258
|
def add_query(query,state,level=@level+1)
|
234
259
|
return if get_state(query)
|
235
260
|
curtime = Time.now().to_f
|
@@ -240,10 +265,12 @@ module PassiveDNS
|
|
240
265
|
end
|
241
266
|
end
|
242
267
|
|
268
|
+
# sets the state of a given query
|
243
269
|
def update_query(query,state)
|
244
270
|
@sqlitedbh.execute("update queue set state = '#{state}' where query = '#{query}'")
|
245
271
|
end
|
246
272
|
|
273
|
+
# returns each query waiting on the queue
|
247
274
|
def get_state(query)
|
248
275
|
rows = @sqlitedbh.execute("select state from queue where query = '#{query}'")
|
249
276
|
if rows
|
@@ -254,6 +281,7 @@ module PassiveDNS
|
|
254
281
|
false
|
255
282
|
end
|
256
283
|
|
284
|
+
# returns each query waiting on the queue
|
257
285
|
def each_query(max_level=20)
|
258
286
|
puts "each_query max_level=#{max_level} curlevel=#{@level}" if @debug
|
259
287
|
rows = @sqlitedbh.execute("select query, state, level from queue where state = 'failed' or state = 'pending' order by level limit 1")
|
data/test/test_cli.rb
CHANGED
@@ -13,22 +13,23 @@ require_relative '../lib/passivedns/client/cli.rb'
|
|
13
13
|
class TestCLI < Minitest::Test
|
14
14
|
def test_letter_map
|
15
15
|
letter_map = PassiveDNS::CLI.get_letter_map
|
16
|
-
assert_equal("
|
16
|
+
assert_equal("3bcdmprtv", letter_map.keys.sort.join(""))
|
17
17
|
end
|
18
18
|
|
19
19
|
def test_help_text
|
20
20
|
helptext = PassiveDNS::CLI.run(["--help"])
|
21
21
|
helptext.gsub!(/Usage: .*?\[/, "Usage: [")
|
22
22
|
assert_equal(
|
23
|
-
"Usage: [-d [
|
23
|
+
"Usage: [-d [3bcdmprtv]] [-g|-v|-m|-c|-x|-y|-j|-t] [-os <sep>] [-f <file>] [-r#|-w#|-v] [-l <count>] <ip|domain|cidr>
|
24
24
|
Passive DNS Providers
|
25
|
-
-
|
25
|
+
-d3bcdmprtv uses all of the available passive dns database
|
26
26
|
-d3 use 360.cn
|
27
27
|
-db use BFK.de
|
28
28
|
-dc use CIRCL
|
29
29
|
-dd use DNSDB
|
30
30
|
-dm use Mnemonic
|
31
31
|
-dp use PassiveTotal
|
32
|
+
-dr use RiskIQ
|
32
33
|
-dt use TCPIPUtils
|
33
34
|
-dv use VirusTotal
|
34
35
|
-dvt uses VirusTotal and TCPIPUtils (for example)
|
@@ -84,8 +85,8 @@ Getting Help
|
|
84
85
|
assert_equal(options_target, options)
|
85
86
|
assert_equal([], items)
|
86
87
|
|
87
|
-
options_target[:pdnsdbs] = ["circl", "dnsdb", "mnemonic"]
|
88
|
-
options, items = PassiveDNS::CLI.parse_command_line(["-
|
88
|
+
options_target[:pdnsdbs] = ["circl", "dnsdb", "mnemonic", "riskiq"]
|
89
|
+
options, items = PassiveDNS::CLI.parse_command_line(["-dcdmr"])
|
89
90
|
assert_equal(options_target, options)
|
90
91
|
assert_equal([], items)
|
91
92
|
|