passivedns-client 1.0.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,36 @@
1
+ require 'socket'
2
+
3
+ module PassiveDNS
4
+ class CERTEE
5
+ @@host = "sim.cert.ee"
6
+ attr_accessor :debug
7
+ def initialize
8
+ end
9
+ def lookup(label)
10
+ $stderr.puts "DEBUG: CERTEE.lookup(#{label})" if @debug
11
+ recs = []
12
+ begin
13
+ t1 = Time.now
14
+ s = TCPSocket.new(@@host,43)
15
+ s.puts(label)
16
+ s.each_line do |l|
17
+ (lbl,ans,fs,ls) = l.chomp.split(/\t/)
18
+ rrtype = 'A'
19
+ if ans =~ /^\d+\.\d+\.\d+\.\d+$/
20
+ rrtype = 'A'
21
+ elsif ans =~ /^ns/
22
+ rrtype = 'NS'
23
+ else
24
+ rrtype = 'CNAME'
25
+ end
26
+ t2 = Time.now
27
+ recs << PDNSResult.new('CERTEE',t2-t1,lbl,ans,rrtype,0,Time.parse(fs).utc.strftime("%Y-%m-%dT%H:%M:%SZ"),Time.parse(ls).utc.strftime("%Y-%m-%dT%H:%M:%SZ"))
28
+ end
29
+ rescue SocketError => e
30
+ $stderr.puts e
31
+ end
32
+ return nil unless recs.length > 0
33
+ recs
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,89 @@
1
+ # DESCRIPTION: this is a module for pdns.rb, primarily used by pdnstool.rb, to query Bojan Zdrnja's passive DNS database, DNSParse
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'openssl'
5
+
6
+ module PassiveDNS
7
+ class DNSParse
8
+ attr_accessor :debug
9
+ @@dns_rtypes = {1 => 'A', 2 => 'NS', 3 => 'MD', 4 => 'MF', 5 => 'CNAME', 6 => 'SOA', 7 => 'MB', 8 => 'MG', 9 => 'MR', 10 => 'NULL', 11 => 'WKS', 12 => 'PTR', 13 => 'HINFO', 14 => 'MINFO', 15 => 'MX', 16 => 'TXT', 17 => 'RP', 18 => 'AFSDB', 19 => 'X25', 20 => 'ISDN', 21 => 'RT', 22 => 'NSAP', 23 => 'NSAP-PTR', 24 => 'SIG', 25 => 'KEY', 26 => 'PX', 27 => 'GPOS', 28 => 'AAAA', 29 => 'LOC', 30 => 'NXT', 31 => 'EID', 32 => 'NIMLOC', 33 => 'SRV', 34 => 'ATMA', 35 => 'NAPTR', 36 => 'KX', 37 => 'CERT', 38 => 'A6', 39 => 'DNAME', 40 => 'SINK', 41 => 'OPT', 42 => 'APL', 43 => 'DS', 44 => 'SSHFP', 45 => 'IPSECKEY', 46 => 'RRSIG', 47 => 'NSEC', 48 => 'DNSKEY', 49 => 'DHCID', 55 => 'HIP', 99 => 'SPF', 100 => 'UINFO', 101 => 'UID', 102 => 'GID', 103 => 'UNSPEC', 249 => 'TKEY', 250 => 'TSIG', 251 => 'IXFR', 252 => 'AXFR', 253 => 'MAILB', 254 => 'MAILA', 255 => 'ALL'}
10
+ def initialize(config="#{ENV['HOME']}/.dnsparse")
11
+ if File.exist?(config)
12
+ @base,@user,@pass = File.open(config).read.split(/\n/)
13
+ $stderr.puts "DEBUG: DNSParse#initialize(#{@base}, #{@user}, #{@pass})" if @debug
14
+ else
15
+ raise "Configuration file for DNSParse is required for intialization\nFormat of configuration file (default: #{ENV['HOME']}/.dnsparse) is:\n<url>\n<username>\n<password>\n"
16
+ end
17
+ end
18
+
19
+ def parse_json(page,response_time=0)
20
+ res = []
21
+ # need to remove the json_class tag or the parser will crap itself trying to find a class to align it to
22
+ page = page.gsub(/\"json_class\"\:\"PDNSResult\"\,/,'')
23
+ recs = JSON.parse(page)
24
+ recs.each do |row|
25
+ res << PDNSResult.new('DNSParse',response_time,row["query"],row["answer"],@@dns_rtypes[row["rrtype"].to_i],row["ttl"],row["firstseen"],row["lastseen"])
26
+ end
27
+ res
28
+ rescue Exception => e
29
+ $stderr.puts "DNSParse Exception: #{e}"
30
+ raise e
31
+ end
32
+
33
+ def parse_html(page,response_time=0)
34
+ rows = []
35
+ line = page.split(/<table/).grep(/ id=\"dnsparse\"/)
36
+ return [] unless line.length > 0
37
+ line = line[0].gsub(/[\t\n]/,'').gsub(/<\/table.*/,'')
38
+ rows = line.split(/<tr.*?>/)
39
+ res = []
40
+ rows.collect do |row|
41
+ r = row.split(/<td>/).map{|x| x.gsub(/<.*?>/,'').gsub(/\&.*?;/,'').gsub(/[\t\n]/,'')}[1,1000]
42
+ if r and r[0] =~ /\w/
43
+ #TXT records screw up other functions and don't provide much without a lot of subparshing. Dropping for now.
44
+ if r[2]!="TXT" then
45
+ res << PDNSResult.new('DNSParse',response_time,r[0],r[1],r[2],r[3],r[4],r[5])
46
+ end
47
+ end
48
+ end
49
+ res
50
+ rescue Exception => e
51
+ $stderr.puts "DNSParse Exception: #{e}"
52
+ raise e
53
+ end
54
+
55
+ def lookup(label)
56
+ $stderr.puts "DEBUG: DNSParse.lookup(#{label})" if @debug
57
+ Timeout::timeout(240) {
58
+ year = Time.now.strftime("%Y").to_i
59
+ month = Time.now.strftime("%m").to_i
60
+ url = "#{@base}#{label}&year=#{year - 1}"
61
+ url = "#{@base}#{label}&year=#{year}" if month > 3
62
+ if label =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/
63
+ url.gsub!(/query\.php/,'cidr.php')
64
+ elsif label =~ /\*$/
65
+ url.gsub!(/query\.php/,'wildcard.php')
66
+ end
67
+ $stderr.puts "DEBUG: DNSParse url = #{url}" if @debug
68
+ url = URI.parse url
69
+ http = Net::HTTP.new(url.host, url.port)
70
+ http.use_ssl = (url.scheme == 'https')
71
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
72
+ http.verify_depth = 5
73
+ request = Net::HTTP::Get.new(url.path+"?"+url.query)
74
+ request.add_field("User-Agent", "Ruby/#{RUBY_VERSION} passivedns-client rubygem v#{PassiveDNS::Client::VERSION}")
75
+ request.basic_auth @user, @pass
76
+ t1 = Time.now
77
+ response = http.request(request)
78
+ t2 = Time.now
79
+ if @base =~ /format=json/
80
+ parse_json(response.body,t2-t1)
81
+ else
82
+ parse_html(response.body,t2-t1)
83
+ end
84
+ }
85
+ rescue Timeout::Error => e
86
+ $stderr.puts "DNSParse lookup timed out: #{label}"
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,80 @@
1
+ # DESCRIPTION: this is a module for pdns.rb, primarily used by pdnstool.rb, to query the ISC passive DNS database
2
+ # details on the API are at https://dnsdb.isc.org/doc/isc-dnsdb-api.html
3
+ # to request an API key, please email dnsdb@isc.org
4
+ require 'net/http'
5
+ require 'net/https'
6
+
7
+ module PassiveDNS
8
+ class ISC
9
+ attr_accessor :debug
10
+ @@base="https://dnsdb-api.isc.org/lookup"
11
+
12
+ def initialize(config="#{ENV['HOME']}/.isc-dnsdb-query.conf")
13
+ @debug = false
14
+ if File.exist?(config)
15
+ @key = File.open(config).readline.chomp
16
+ if @key =~ /^[0-9a-f]{64}$/
17
+ # pass
18
+ elsif @key =~ /^APIKEY=\"([0-9a-f]{64})\"/
19
+ @key = $1
20
+ else
21
+ raise "Format of configuration file (default: #{ENV['HOME']}/.isc-dnsdb-query.conf) is:\nAPIKEY=\"<key>\"\nE.g.,\nAPIKEY=\"d41d8cd98f00b204e9800998ecf8427ed41d8cd98f00b204e9800998ecf8427e\"\n"
22
+ end
23
+ else
24
+ raise "Configuration file for ISC is required for intialization\nFormat of configuration file (default: #{ENV['HOME']}/.isc-dnsdb-query.conf) is:\nAPIKEY=\"<key>\"\nE.g.,\nAPIKEY=\"d41d8cd98f00b204e9800998ecf8427ed41d8cd98f00b204e9800998ecf8427e\"\n"
25
+ end
26
+ end
27
+
28
+ def parse_json(page,response_time)
29
+ res = []
30
+ raise "Error: unable to parse request" if page =~ /Error: unable to parse request/
31
+ # need to remove the json_class tag or the parser will crap itself trying to find a class to align it to
32
+ rows = page.split(/\n/)
33
+ rows.each do |row|
34
+ record = JSON.parse(row)
35
+ record['rdata'] = [record['rdata']] if record['rdata'].class == String
36
+ record['rdata'].each do |rdata|
37
+ if record['time_first']
38
+ res << PDNSResult.new('ISC',response_time,record['rrname'],rdata,record['rrtype'],0,Time.at(record['time_first'].to_i).utc.strftime("%Y-%m-%dT%H:%M:%SZ"),Time.at(record['time_last'].to_i).utc.strftime("%Y-%m-%dT%H:%M:%SZ"),record['count'])
39
+ else
40
+ res << PDNSResult.new('ISC',response_time,record['rrname'],rdata,record['rrtype'])
41
+ end
42
+ end
43
+ end
44
+ res
45
+ rescue Exception => e
46
+ $stderr.puts "ISC Exception: #{e}"
47
+ $stderr.puts page
48
+ raise e
49
+ end
50
+
51
+ def lookup(label)
52
+ $stderr.puts "DEBUG: ISC.lookup(#{label})" if @debug
53
+ Timeout::timeout(240) {
54
+ url = nil
55
+ if label =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$/
56
+ label = label.gsub(/\//,',')
57
+ url = "#{@@base}/rdata/ip/#{label}"
58
+ else
59
+ url = "#{@@base}/rrset/name/#{label}"
60
+ end
61
+ url = URI.parse url
62
+ http = Net::HTTP.new(url.host, url.port)
63
+ http.use_ssl = (url.scheme == 'https')
64
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
65
+ http.verify_depth = 5
66
+ request = Net::HTTP::Get.new(url.path)
67
+ request.add_field("User-Agent", "Ruby/#{RUBY_VERSION} passivedns-client rubygem v#{PassiveDNS::Client::VERSION}")
68
+ request.add_field("X-API-Key", @key)
69
+ request.add_field("Accept", "application/json")
70
+ t1 = Time.now
71
+ response = http.request(request)
72
+ t2 = Time.now
73
+ $stderr.puts response if @debug
74
+ parse_json(response.body,t2-t1)
75
+ }
76
+ rescue Timeout::Error => e
77
+ $stderr.puts "ISC lookup timed out: #{label}"
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,272 @@
1
+ require 'sqlite3'
2
+ require 'yaml'
3
+ require 'structformatter'
4
+
5
+ module PassiveDNS
6
+ class PDNSQueueEntry < Struct.new(:query, :state, :level); end
7
+
8
+ class PDNSToolState
9
+ attr_accessor :debug
10
+ attr_reader :level
11
+
12
+ def initialize
13
+ @queue = []
14
+ @recs = []
15
+ @level = 0
16
+ end
17
+
18
+ def next_result
19
+ @recs.each do |rec|
20
+ yield rec
21
+ end
22
+ end
23
+
24
+ def add_result(res)
25
+ @recs << res
26
+ add_query(res.answer,'pending')
27
+ add_query(res.query,'pending')
28
+ end
29
+
30
+ def update_query(query,state)
31
+ @queue.each do |q|
32
+ if q.query == query
33
+ puts "update_query: #{query} (#{q.state}) -> (#{state})" if @debug
34
+ q.state = state
35
+ break
36
+ end
37
+ end
38
+ end
39
+
40
+ def get_state(query)
41
+ @queue.each do |q|
42
+ if q.query == query
43
+ return q.state
44
+ end
45
+ end
46
+ false
47
+ end
48
+
49
+ def add_query(query,state,level=@level+1)
50
+ if query =~ /^\d+ \w+\./
51
+ query = query.split(/ /,2)[1]
52
+ end
53
+ return if get_state(query)
54
+ puts "Adding query: #{query}, #{state}, #{level}" if @debug
55
+ @queue << PDNSQueueEntry.new(query,state,level)
56
+ end
57
+
58
+ def each_query(max_level=20)
59
+ @queue.each do |q|
60
+ if q.state == 'pending' or q.state == 'failed'
61
+ @level = q.level
62
+ q.state = 'queried'
63
+ if q.level < max_level
64
+ yield q.query
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def to_gdf
71
+ output = "nodedef> name,description VARCHAR(12),color,style\n"
72
+ # IP "$node2,,white,1"
73
+ # domain "$node2,,gray,2"
74
+ # Struct.new(:query, :answer, :rrtype, :ttl, :firstseen, :lastseen)
75
+ colors = {"MX" => "green", "A" => "blue", "CNAME" => "pink", "NS" => "red", "SOA" => "white", "PTR" => "purple", "TXT" => "brown"}
76
+ nodes = {}
77
+ edges = {}
78
+ next_result do |i|
79
+ if i
80
+ nodes[i.query + ",,gray,2"] = true
81
+ if i.answer =~ /[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ then
82
+ nodes[i.answer + ",,white,1"] = true
83
+ else
84
+ nodes[i.answer + ",,gray,2"] = true
85
+ end
86
+ color = colors[i.rrtype]
87
+ color ||= "blue"
88
+ edges[i.query + "," + i.answer + "," + color] = true
89
+ end
90
+ end
91
+ nodes.each do |i,j|
92
+ output += i+"\n"
93
+ end
94
+ output += "edgedef> node1,node2,color\n"
95
+ edges.each do |i,j|
96
+ output += i+"\n"
97
+ end
98
+ output
99
+ end
100
+
101
+ def to_graphviz
102
+ colors = {"MX" => "green", "A" => "blue", "CNAME" => "pink", "NS" => "red", "SOA" => "white", "PTR" => "purple", "TXT" => "brown"}
103
+ output = "graph pdns {\n"
104
+ nodes = {}
105
+ next_result do |l|
106
+ if l
107
+ unless nodes[l.query]
108
+ output += " \"#{l.query}\" [shape=ellipse, style=filled, color=gray];\n"
109
+ if l.answer =~ /^\d{3}\.\d{3}\.\d{3}\.\d{3}$/
110
+ output += " \"#{l.answer}\" [shape=box, style=filled, color=white];\n"
111
+ else
112
+ output += " \"#{l.answer}\" [shape=ellipse, style=filled, color=gray];\n"
113
+ end
114
+ nodes[l.query] = true
115
+ end
116
+ output += " \"#{l.query}\" -- \"#{l.answer}\" [color=#{colors[l.rrtype]}];\n"
117
+ end
118
+ end
119
+ output += "}\n"
120
+ end
121
+
122
+ def to_graphml
123
+ output = '<?xml version="1.0" encoding="UTF-8"?>
124
+ <graphml xmlns="http://graphml.graphdrawing.org/xmlns"
125
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
126
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
127
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
128
+ <graph id="G" edgedefault="directed">
129
+ '
130
+ nodes = {}
131
+ edges = {}
132
+ next_result do |r|
133
+ if r
134
+ output += " <node id='#{r.query}'/>\n" unless nodes["#{r.query}"]
135
+ nodes[r.query] = true
136
+ output += " <node id='#{r.answer}'/>\n" unless nodes["#{r.answer}"]
137
+ nodes[r.answer] = true
138
+ output += " <edge source='#{r.query}' target='#{r.answer}'/>\n" unless edges["#{r.query}|#{r.answer}"]
139
+ end
140
+ end
141
+ output += '</graph></graphml>'+"\n"
142
+ end
143
+
144
+ def to_xml
145
+ output = '<?xml version="1.0" encoding="UTF-8" ?>'+"\n"
146
+ output += "<report>\n"
147
+ output += " <results>\n"
148
+ next_result do |rec|
149
+ output += " "+rec.to_xml+"\n"
150
+ end
151
+ output += " </results>\n"
152
+ output += "</report>\n"
153
+ end
154
+
155
+ def to_yaml
156
+ output = ""
157
+ next_result do |rec|
158
+ output += rec.to_yaml+"\n"
159
+ end
160
+ output
161
+ end
162
+
163
+ def to_json
164
+ output = "[\n"
165
+ sep = ""
166
+ next_result do |rec|
167
+ output += sep
168
+ output += rec.to_json
169
+ sep = ",\n"
170
+ end
171
+ output += "\n]\n"
172
+ end
173
+
174
+ def to_s(sep="\t")
175
+ output = ""
176
+ next_result do |rec|
177
+ output += rec.to_s(sep)+"\n"
178
+ end
179
+ output
180
+ end
181
+ end # class PDNSToolState
182
+
183
+
184
+ class PDNSToolStateDB < PDNSToolState
185
+ attr_reader :level
186
+ def initialize(sqlitedb=nil)
187
+ puts "PDNSToolState initialize #{sqlitedb}" if @debug
188
+ @level = 0
189
+ @sqlitedb = sqlitedb
190
+ raise "Cannot use this class without a database file" unless @sqlitedb
191
+ unless File.exists?(@sqlitedb)
192
+ newdb = true
193
+ end
194
+ @sqlitedbh = SQLite3::Database.new(@sqlitedb)
195
+ if newdb
196
+ create_tables
197
+ end
198
+ res = @sqlitedbh.execute("select min(level) from queue where state = 'pending'")
199
+ if res
200
+ res.each do |row|
201
+ @level = row[0].to_i
202
+ puts "changed @level = #{@level}" if @debug
203
+ end
204
+ end
205
+ end
206
+
207
+ def create_tables
208
+ puts "creating tables" if @debug
209
+ @sqlitedbh.execute("create table results (query, answer, rrtype, ttl, firstseen, lastseen, ts REAL)")
210
+ @sqlitedbh.execute("create table queue (query, state, level INTEGER, ts REAL)")
211
+ @sqlitedbh.execute("create index residx on results (ts)")
212
+ @sqlitedbh.execute("create unique index queue_unique on queue (query)")
213
+ @sqlitedbh.execute("create index queue_level_idx on queue (level)")
214
+ @sqlitedbh.execute("create index queue_state_idx on queue (state)")
215
+ end
216
+
217
+ def next_result
218
+ rows = @sqlitedbh.execute("select query, answer, rrtype, ttl, firstseen, lastseen from results order by ts")
219
+ rows.each do |row|
220
+ yield PDNSResult.new(*row)
221
+ end
222
+ end
223
+
224
+ def add_result(res)
225
+ puts "adding result: #{res.to_s}" if @debug
226
+ curtime = Time.now().to_f
227
+ @sqlitedbh.execute("insert into results values ('#{res.query}','#{res.answer}','#{res.rrtype}','#{res.ttl}','#{res.firstseen}','#{res.lastseen}',#{curtime})")
228
+
229
+ add_query(res.answer,'pending')
230
+ add_query(res.query,'pending')
231
+ end
232
+
233
+ def add_query(query,state,level=@level+1)
234
+ return if get_state(query)
235
+ curtime = Time.now().to_f
236
+ begin
237
+ puts "add_query(#{query},#{state},level=#{level})" if @debug
238
+ @sqlitedbh.execute("insert into queue values ('#{query}','#{state}',#{level},#{curtime})")
239
+ rescue
240
+ end
241
+ end
242
+
243
+ def update_query(query,state)
244
+ @sqlitedbh.execute("update queue set state = '#{state}' where query = '#{query}'")
245
+ end
246
+
247
+ def get_state(query)
248
+ rows = @sqlitedbh.execute("select state from queue where query = '#{query}'")
249
+ if rows
250
+ rows.each do |row|
251
+ return row[0]
252
+ end
253
+ end
254
+ false
255
+ end
256
+
257
+ def each_query(max_level=20)
258
+ puts "each_query max_level=#{max_level} curlevel=#{@level}" if @debug
259
+ rows = @sqlitedbh.execute("select query, state, level from queue where state = 'failed' or state = 'pending' order by level limit 1")
260
+ if rows
261
+ rows.each do |row|
262
+ query,state,level = row
263
+ puts " #{query},#{state},#{level}" if @debug
264
+ if level < max_level
265
+ update_query(query,'queried')
266
+ yield query
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end # class PDNSToolStateDB
272
+ end