passivedns-client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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