2017-12-05 16:41:41 +01:00
|
|
|
#!/usr/bin/env python
|
|
|
|
#
|
|
|
|
# Note: This file is NOT the official one from dnsdb, as it has a python3 cherry-picked pull-request applied for python3 compatibility
|
|
|
|
# See https://github.com/dnsdb/dnsdb-query/pull/30
|
|
|
|
#
|
|
|
|
# Copyright (c) 2013 by Farsight Security, Inc.
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
from __future__ import print_function
|
|
|
|
|
|
|
|
import calendar
|
|
|
|
import errno
|
|
|
|
import locale
|
|
|
|
import optparse
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
import json
|
|
|
|
from io import StringIO
|
|
|
|
|
|
|
|
try:
|
|
|
|
from urllib2 import build_opener, Request, ProxyHandler, HTTPError, URLError
|
|
|
|
from urllib import quote as urllib_quote, urlencode
|
|
|
|
except ImportError:
|
|
|
|
from urllib.request import build_opener, Request, ProxyHandler, HTTPError, URLError
|
|
|
|
from urllib.parse import quote as urllib_quote, urlencode
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_CONFIG_FILES = filter(os.path.isfile, ('/etc/dnsdb-query.conf', os.path.expanduser('~/.dnsdb-query.conf')))
|
|
|
|
DEFAULT_DNSDB_SERVER = 'https://api.dnsdb.info'
|
|
|
|
DEFAULT_HTTP_PROXY = ''
|
|
|
|
DEFAULT_HTTPS_PROXY = ''
|
|
|
|
|
|
|
|
cfg = None
|
|
|
|
options = None
|
|
|
|
|
|
|
|
locale.setlocale(locale.LC_ALL, '')
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
class QueryError(Exception):
|
|
|
|
pass
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
class DnsdbClient(object):
|
|
|
|
def __init__(self, server, apikey, limit=None, http_proxy=None, https_proxy=None):
|
|
|
|
self.server = server
|
|
|
|
self.apikey = apikey
|
|
|
|
self.limit = limit
|
|
|
|
self.http_proxy = http_proxy
|
|
|
|
self.https_proxy = https_proxy
|
|
|
|
|
|
|
|
def query_rrset(self, oname, rrtype=None, bailiwick=None, before=None, after=None):
|
|
|
|
if bailiwick:
|
|
|
|
if not rrtype:
|
|
|
|
rrtype = 'ANY'
|
|
|
|
path = 'rrset/name/%s/%s/%s' % (quote(oname), rrtype, quote(bailiwick))
|
|
|
|
elif rrtype:
|
|
|
|
path = 'rrset/name/%s/%s' % (quote(oname), rrtype)
|
|
|
|
else:
|
|
|
|
path = 'rrset/name/%s' % quote(oname)
|
|
|
|
return self._query(path, before, after)
|
|
|
|
|
|
|
|
def query_rdata_name(self, rdata_name, rrtype=None, before=None, after=None):
|
|
|
|
if rrtype:
|
|
|
|
path = 'rdata/name/%s/%s' % (quote(rdata_name), rrtype)
|
|
|
|
else:
|
|
|
|
path = 'rdata/name/%s' % quote(rdata_name)
|
|
|
|
return self._query(path, before, after)
|
|
|
|
|
|
|
|
def query_rdata_ip(self, rdata_ip, before=None, after=None):
|
|
|
|
path = 'rdata/ip/%s' % rdata_ip.replace('/', ',')
|
|
|
|
return self._query(path, before, after)
|
|
|
|
|
|
|
|
def _query(self, path, before=None, after=None):
|
|
|
|
url = '%s/lookup/%s' % (self.server, path)
|
|
|
|
|
|
|
|
params = {}
|
|
|
|
if self.limit:
|
|
|
|
params['limit'] = self.limit
|
|
|
|
if before and after:
|
|
|
|
params['time_first_after'] = after
|
|
|
|
params['time_last_before'] = before
|
|
|
|
else:
|
|
|
|
if before:
|
|
|
|
params['time_first_before'] = before
|
|
|
|
if after:
|
|
|
|
params['time_last_after'] = after
|
|
|
|
if params:
|
|
|
|
url += '?{0}'.format(urlencode(params))
|
|
|
|
|
|
|
|
req = Request(url)
|
|
|
|
req.add_header('Accept', 'application/json')
|
|
|
|
req.add_header('X-Api-Key', self.apikey)
|
|
|
|
|
|
|
|
proxy_args = {}
|
|
|
|
if self.http_proxy:
|
|
|
|
proxy_args['http'] = self.http_proxy
|
|
|
|
if self.https_proxy:
|
|
|
|
proxy_args['https'] = self.https_proxy
|
|
|
|
proxy_handler = ProxyHandler(proxy_args)
|
|
|
|
opener = build_opener(proxy_handler)
|
|
|
|
|
|
|
|
try:
|
|
|
|
http = opener.open(req)
|
|
|
|
while True:
|
|
|
|
line = http.readline()
|
|
|
|
if not line:
|
|
|
|
break
|
|
|
|
yield json.loads(line.decode('ascii'))
|
|
|
|
except (HTTPError, URLError) as e:
|
2020-11-04 18:36:06 +01:00
|
|
|
try:
|
|
|
|
raise QueryError(str(e), sys.exc_traceback)
|
|
|
|
except AttributeError:
|
|
|
|
raise QueryError(str(e), sys.exc_info)
|
2017-12-05 16:41:41 +01:00
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
def quote(path):
|
|
|
|
return urllib_quote(path, safe='')
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
def sec_to_text(ts):
|
|
|
|
return time.strftime('%Y-%m-%d %H:%M:%S -0000', time.gmtime(ts))
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
def rrset_to_text(m):
|
|
|
|
s = StringIO()
|
|
|
|
|
|
|
|
try:
|
|
|
|
if 'bailiwick' in m:
|
|
|
|
s.write(';; bailiwick: %s\n' % m['bailiwick'])
|
|
|
|
|
|
|
|
if 'count' in m:
|
|
|
|
s.write(';; count: %s\n' % locale.format('%d', m['count'], True))
|
|
|
|
|
|
|
|
if 'time_first' in m:
|
|
|
|
s.write(';; first seen: %s\n' % sec_to_text(m['time_first']))
|
|
|
|
if 'time_last' in m:
|
|
|
|
s.write(';; last seen: %s\n' % sec_to_text(m['time_last']))
|
|
|
|
|
|
|
|
if 'zone_time_first' in m:
|
|
|
|
s.write(';; first seen in zone file: %s\n' % sec_to_text(m['zone_time_first']))
|
|
|
|
if 'zone_time_last' in m:
|
|
|
|
s.write(';; last seen in zone file: %s\n' % sec_to_text(m['zone_time_last']))
|
|
|
|
|
|
|
|
if 'rdata' in m:
|
|
|
|
for rdata in m['rdata']:
|
|
|
|
s.write('%s IN %s %s\n' % (m['rrname'], m['rrtype'], rdata))
|
|
|
|
|
|
|
|
s.seek(0)
|
|
|
|
return s.read()
|
|
|
|
finally:
|
|
|
|
s.close()
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
def rdata_to_text(m):
|
|
|
|
return '%s IN %s %s' % (m['rrname'], m['rrtype'], m['rdata'])
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
def parse_config(cfg_files):
|
|
|
|
config = {}
|
|
|
|
|
|
|
|
if not cfg_files:
|
|
|
|
raise IOError(errno.ENOENT, 'dnsdb_query: No config files found')
|
|
|
|
|
|
|
|
for fname in cfg_files:
|
|
|
|
for line in open(fname):
|
|
|
|
key, eq, val = line.strip().partition('=')
|
|
|
|
val = val.strip('"')
|
|
|
|
config[key] = val
|
|
|
|
|
|
|
|
return config
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
def time_parse(s):
|
|
|
|
try:
|
|
|
|
epoch = int(s)
|
|
|
|
return epoch
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
try:
|
|
|
|
epoch = int(calendar.timegm(time.strptime(s, '%Y-%m-%d')))
|
|
|
|
return epoch
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
try:
|
|
|
|
epoch = int(calendar.timegm(time.strptime(s, '%Y-%m-%d %H:%M:%S')))
|
|
|
|
return epoch
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
m = re.match(r'^(?=\d)(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$', s, re.I)
|
|
|
|
if m:
|
2018-12-11 15:29:09 +01:00
|
|
|
return -1 * (int(m.group(1) or 0) * 604800
|
|
|
|
+ int(m.group(2) or 0) * 86400
|
|
|
|
+ int(m.group(3) or 0) * 3600
|
|
|
|
+ int(m.group(4) or 0) * 60
|
|
|
|
+ int(m.group(5) or 0))
|
2017-12-05 16:41:41 +01:00
|
|
|
|
|
|
|
raise ValueError('Invalid time: "%s"' % s)
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
def epipe_wrapper(func):
|
|
|
|
def f(*args, **kwargs):
|
|
|
|
try:
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
except IOError as e:
|
|
|
|
if e.errno == errno.EPIPE:
|
|
|
|
sys.exit(e.errno)
|
|
|
|
raise
|
|
|
|
return f
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
@epipe_wrapper
|
|
|
|
def main():
|
|
|
|
global cfg
|
|
|
|
global options
|
|
|
|
|
|
|
|
parser = optparse.OptionParser(epilog='Time formats are: "%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%d" (UNIX timestamp), "-%d" (Relative time in seconds), BIND format (e.g. 1w1h, (w)eek, (d)ay, (h)our, (m)inute, (s)econd)')
|
2018-12-11 15:29:09 +01:00
|
|
|
parser.add_option('-c', '--config', dest='config', help='config file', action='append')
|
|
|
|
parser.add_option('-r', '--rrset', dest='rrset', type='string', help='rrset <ONAME>[/<RRTYPE>[/BAILIWICK]]')
|
|
|
|
parser.add_option('-n', '--rdataname', dest='rdata_name', type='string', help='rdata name <NAME>[/<RRTYPE>]')
|
|
|
|
parser.add_option('-i', '--rdataip', dest='rdata_ip', type='string', help='rdata ip <IPADDRESS|IPRANGE|IPNETWORK>')
|
|
|
|
parser.add_option('-t', '--rrtype', dest='rrtype', type='string', help='rrset or rdata rrtype')
|
|
|
|
parser.add_option('-b', '--bailiwick', dest='bailiwick', type='string', help='rrset bailiwick')
|
2017-12-05 16:41:41 +01:00
|
|
|
parser.add_option('-s', '--sort', dest='sort', type='string', help='sort key')
|
2018-12-11 15:29:09 +01:00
|
|
|
parser.add_option('-R', '--reverse', dest='reverse', action='store_true', default=False, help='reverse sort')
|
|
|
|
parser.add_option('-j', '--json', dest='json', action='store_true', default=False, help='output in JSON format')
|
|
|
|
parser.add_option('-l', '--limit', dest='limit', type='int', default=0, help='limit number of results')
|
2017-12-05 16:41:41 +01:00
|
|
|
|
|
|
|
parser.add_option('', '--before', dest='before', type='string', help='only output results seen before this time')
|
|
|
|
parser.add_option('', '--after', dest='after', type='string', help='only output results seen after this time')
|
|
|
|
|
|
|
|
options, args = parser.parse_args()
|
|
|
|
if args:
|
|
|
|
parser.print_help()
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
try:
|
|
|
|
if options.before:
|
|
|
|
options.before = time_parse(options.before)
|
|
|
|
except ValueError:
|
|
|
|
print('Could not parse before: {}'.format(options.before))
|
|
|
|
|
|
|
|
try:
|
|
|
|
if options.after:
|
|
|
|
options.after = time_parse(options.after)
|
|
|
|
except ValueError:
|
|
|
|
print('Could not parse after: {}'.format(options.after))
|
|
|
|
|
|
|
|
try:
|
|
|
|
cfg = parse_config(options.config or DEFAULT_CONFIG_FILES)
|
|
|
|
except IOError as e:
|
|
|
|
print(str(e), file=sys.stderr)
|
|
|
|
sys.exit(1)
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
if 'DNSDB_SERVER' not in cfg:
|
2017-12-05 16:41:41 +01:00
|
|
|
cfg['DNSDB_SERVER'] = DEFAULT_DNSDB_SERVER
|
2018-12-11 15:29:09 +01:00
|
|
|
if 'HTTP_PROXY' not in cfg:
|
2017-12-05 16:41:41 +01:00
|
|
|
cfg['HTTP_PROXY'] = DEFAULT_HTTP_PROXY
|
2018-12-11 15:29:09 +01:00
|
|
|
if 'HTTPS_PROXY' not in cfg:
|
2017-12-05 16:41:41 +01:00
|
|
|
cfg['HTTPS_PROXY'] = DEFAULT_HTTPS_PROXY
|
2018-12-11 15:29:09 +01:00
|
|
|
if 'APIKEY' not in cfg:
|
2017-12-05 16:41:41 +01:00
|
|
|
sys.stderr.write('dnsdb_query: APIKEY not defined in config file\n')
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
client = DnsdbClient(cfg['DNSDB_SERVER'], cfg['APIKEY'],
|
2018-12-11 15:29:09 +01:00
|
|
|
limit=options.limit,
|
|
|
|
http_proxy=cfg['HTTP_PROXY'],
|
|
|
|
https_proxy=cfg['HTTPS_PROXY'])
|
2017-12-05 16:41:41 +01:00
|
|
|
if options.rrset:
|
|
|
|
if options.rrtype or options.bailiwick:
|
|
|
|
qargs = (options.rrset, options.rrtype, options.bailiwick)
|
|
|
|
else:
|
|
|
|
qargs = (options.rrset.split('/', 2))
|
|
|
|
|
|
|
|
results = client.query_rrset(*qargs, before=options.before, after=options.after)
|
|
|
|
fmt_func = rrset_to_text
|
|
|
|
elif options.rdata_name:
|
|
|
|
if options.rrtype:
|
|
|
|
qargs = (options.rdata_name, options.rrtype)
|
|
|
|
else:
|
|
|
|
qargs = (options.rdata_name.split('/', 1))
|
|
|
|
|
|
|
|
results = client.query_rdata_name(*qargs, before=options.before, after=options.after)
|
|
|
|
fmt_func = rdata_to_text
|
|
|
|
elif options.rdata_ip:
|
|
|
|
results = client.query_rdata_ip(options.rdata_ip, before=options.before, after=options.after)
|
|
|
|
fmt_func = rdata_to_text
|
|
|
|
else:
|
|
|
|
parser.print_help()
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
if options.json:
|
|
|
|
fmt_func = json.dumps
|
|
|
|
|
|
|
|
try:
|
|
|
|
if options.sort:
|
|
|
|
results = list(results)
|
|
|
|
if len(results) > 0:
|
2018-12-11 15:29:09 +01:00
|
|
|
if options.sort not in results[0]:
|
2017-12-05 16:41:41 +01:00
|
|
|
sort_keys = results[0].keys()
|
|
|
|
sort_keys.sort()
|
|
|
|
sys.stderr.write('dnsdb_query: invalid sort key "%s". valid sort keys are %s\n' % (options.sort, ', '.join(sort_keys)))
|
|
|
|
sys.exit(1)
|
|
|
|
results.sort(key=lambda r: r[options.sort], reverse=options.reverse)
|
|
|
|
for res in results:
|
|
|
|
sys.stdout.write('%s\n' % fmt_func(res))
|
|
|
|
except QueryError as e:
|
|
|
|
print(e.message, file=sys.stderr)
|
|
|
|
sys.exit(1)
|
|
|
|
|
2018-12-11 15:29:09 +01:00
|
|
|
|
2017-12-05 16:41:41 +01:00
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|