passivedns-client 2.0.6 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|