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.
@@ -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")
@@ -1,5 +1,7 @@
1
- module PassiveDNS
1
+ module PassiveDNS # :nodoc:
2
+ # coodinates the lookups accross all configured PassiveDNS providers
2
3
  class Client
3
- VERSION = "2.0.6"
4
+ # version of PassiveDNS::Client
5
+ VERSION = "2.1.0"
4
6
  end
5
7
  end
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("3bcdmptv", letter_map.keys.sort.join(""))
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 [3bcdmptv]] [-g|-v|-m|-c|-x|-y|-j|-t] [-os <sep>] [-f <file>] [-r#|-w#|-v] [-l <count>] <ip|domain|cidr>
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
- -d3bcdmptv uses all of the available passive dns database
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(["-dcdm"])
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