Source code for wrdrd.tools.domain

#!/usr/bin/env python
# encoding: utf-8
from __future__ import print_function
"""
wrdrd.tools.domain

* nslookup, whois, and dig command wrappers
* check_google_domain (_mx, _spf, _dkim, _dmarc)

| https://en.wikipedia.org/wiki/DNS
| https://en.wikipedia.org/wiki/List_of_DNS_record_types
"""

import sarge
import structlog

log = structlog.get_logger()


[docs] def nslookup(domain, nameserver=''): """ Get nslookup information with nslookup (resolve a domainname to an IP) Args: domain (str): DNS domain nameserver (str): DNS domain name server to query (default: ``''``) Returns: str: nslookup output """ if not domain.endswith('.'): domain = domain + '.' cmd = sarge.shell_format('nslookup {0} {1}', domain, nameserver) log.info('cmd', cmd=cmd) output = sarge.capture_both(cmd) return output
[docs] def whois(domain): """ Get whois information with whois Args: domain (str): DNS domain Returns: str: whois output """ cmd = sarge.shell_format('whois {0}', domain) log.info('cmd', cmd=cmd) output = sarge.capture_both(cmd) return output
[docs] def dig_all(domain): """ Get all DNS records with dig Args: domain (str): DNS domain Returns: str: dig output """ cmd = sarge.shell_format( "dig {0} +cmd +nocomments +question +noidentify +nostats +dnssec", domain) log.info('cmd', cmd=cmd) output = sarge.capture_both(cmd) return output
[docs] def dig_ns(domain): """ Get DNS NS records with dig Args: domain (str): DNS domain Returns: str: dig output """ cmd = sarge.shell_format( "dig {0} ns +cmd +nocomments +question +noidentify +nostats", domain) log.info('cmd', cmd=cmd) output = sarge.capture_both(cmd) return output
[docs] def dig_txt(domain): """ Get DNS TXT records with dig Args: domain (str): DNS domain Returns: str: dig output """ cmd = sarge.shell_format( "dig {0} txt +cmd +nocomments +question +noidentify +nostats", domain) log.info('cmd', cmd=cmd) output = sarge.capture_both(cmd) return output
[docs] def dig_spf(domain): """ Get SPF DNS TXT records with dig Args: domain (str): DNS domain Returns: str: dig output | https://en.wikipedia.org/wiki/Sender_Policy_Framework """ output = dig_txt(domain) # TODO: look for "v=spf1" return output
[docs] def dig_mx(domain): """ Get MX DNS records with dig Args: domain (str): DNS domain Returns: str: dig output | https://en.wikipedia.org/wiki/MX_record """ cmd = sarge.shell_format( "dig {0} mx +cmd +nocomments +question +noidentify +nostats", domain) log.info('cmd', cmd=cmd) output = sarge.capture_both(cmd) return output
[docs] def dig_dnskey(zone): """ Get DNSSEC DNS records with dig Args: zone (str): DNS zone Returns: str: dig output """ cmd = sarge.shell_format( "dig {0} +dnssec dnskey +cmd +nocomments +question +noidentify +nostats", zone) log.info('cmd', cmd=cmd) output = sarge.capture_both(cmd) return output
[docs] def check_google_mx(domain): """ Check Google MX DNS records Args: domain (str): DNS domain name Returns: int: 0 if OK, 1 on error | https://support.google.com/a/topic/2716885?hl=en&ref_topic=2426592 """ cmd = sarge.shell_format("dig {0} mx +short", domain) log.info('cmd', cmd=cmd) output = sarge.capture_both(cmd).stdout.text.rstrip() log.debug('MX', record=output) result = 0 check_domain1 = "aspmx.l.google.com." check_domain2 = "googlemail.com." lines = output.split('\n') if not lines: log.error('err', msg="No MX records found for %r" % domain) result += 1 for l in lines: l = l.lower() if not (l.endswith(check_domain1) or l.endswith(check_domain2)): result += 1 log.error('err', msg="%r does not end with %r or %r" % (l, check_domain1, check_domain2)) if result is None: result += 1 return result
[docs] def check_google_spf(domain): """ Check a Google SPF DNS TXT record Args: domain (str): DNS domain name Returns: int: 0 if OK, 1 on error | https://support.google.com/a/answer/178723?hl=en """ cmd = sarge.shell_format("dig {0} txt +short", domain) log.info('cmd', op='check_google_spf', cmd=cmd) proc = sarge.capture_both(cmd) output = proc.stdout.text.rstrip().split('\n') for line in output: log.debug('TXT', record=line) expected = u"\"v=spf1 include:_spf.google.com ~all\"" if line == expected: return 0 errmsg = "%r != %r" % (output, expected) log.error('err', msg=errmsg) return 1
[docs] def check_google_dmarc(domain): """ Check a Google DMARC DNS TXT record Args: domain (str): DNS domain name Returns: int: 0 if OK, 1 on error | https://support.google.com/a/answer/2466580 | https://support.google.com/a/answer/2466563 """ dmarc_domain = "_dmarc." + domain cmd = sarge.shell_format("dig {0} txt +short", dmarc_domain) log.info('cmd', op='check_google_dmarc', cmd=cmd) proc = sarge.capture_both(cmd) output = proc.stdout.text.rstrip().split('\n') for line in output: log.debug('TXT', record=line) expected = u"\"v=DMARC1" # ... "\; p=none\; rua=mailto:" if line.startswith(expected): if 'p=' in line: return 0 errmsg = "%r != %r" % (output, expected) log.error('err', msg=errmsg) return 1
DEFAULT_GOOGLE_DKIM_PREFIX = 'google'
[docs] def check_google_dkim(domain, prefix=DEFAULT_GOOGLE_DKIM_PREFIX): """ Check a Google DKIM DNS TXT record Args: domain (str): DNS domain name prefix (str): DKIM ``s=`` selector ('DKIM prefix') Returns: int: 0 if OK, 1 on error | https://support.google.com/a/answer/174126 | https://admin.google.com/AdminHome?fral=1#AppDetails:service=email&flyout=dkim .. note:: This check function only finds "v=DKIM1" TXT records; it defaults to the default ``google`` prefix and **does not validate DKIM signatures**. | http://dkim.org/specs/rfc4871-dkimbase.html#rfc.section.3.6.2.1 | http://dkim.org/specs/rfc4871-dkimbase.html#rfc.section.A.3 """ dkim_record_name = "%s._domainkey.%s" % (prefix, domain) cmd = sarge.shell_format("dig {0} txt +short", dkim_record_name) log.info('cmd', op='check_google_dkim', cmd=cmd) proc = sarge.capture_both(cmd) output = proc.stdout.text.rstrip().split('\n') for line in output: log.debug('TXT', record=line) expected = u"\"v=DKIM1" # ... "\; p=none\; rua=mailto:" if line.startswith(expected): if 'k=' in line and 'p=' in line: return 0 errmsg = "%s is not a valid DKIM record" % (output) log.error('err', msg=errmsg) return 1
[docs] def domain_tools(domain): """ Get whois and DNS information for a domain. Args: domain (str): DNS domain name Returns: int: nonzero returncode on failure (sum of returncodes) """ returncode = 0 proc = whois(domain) returncode += proc.returncode print(proc.stdout.text) print('-') stderr = proc.stderr.text stderr and print(stderr) print('--') proc = dig_all(domain) returncode += proc.returncode print(proc.stdout.text) print('-') stderr = proc.stderr.text stderr and print(stderr) print('--') proc = dig_ns(domain) returncode += proc.returncode print(proc.stdout.text) print('-') stderr = proc.stderr.text stderr and print(stderr) print('--') proc = dig_mx(domain) returncode += proc.returncode print(proc.stdout.text) print('-') stderr = proc.stderr.text stderr and print(stderr) print('--') proc = dig_txt(domain) returncode += proc.returncode print(proc.stdout.text) print('-') stderr = proc.stderr.text stderr and print(stderr) print('--') # See: dig_txt #proc = dig_spf(domain) #returncode += proc.returncode # print(proc.stdout.text) # print('-') #stderr = proc.stderr.text; stderr and print(stderr) # print('--') dmarc_domain = "_dmarc." + domain proc = dig_txt(dmarc_domain) returncode += proc.returncode print(proc.stdout.text) print('-') stderr = proc.stderr.text stderr and print(stderr) print('--') # Would need to specify a DKIM prefix (DKIM s= selector) #dkim_record_name = "%s._domainkey.%s" % (dkim_prefix, domain) #proc = dig_txt(dkim_record_name) #returncode += proc.returncode # print(proc.stdout.text) # print('-') #stderr = proc.stderr.text; stderr and print(stderr) # print('--') proc = dig_dnskey(domain.split(".")[-1]) # TODO: actual zone returncode += proc.returncode print(proc.stdout.text) print('-') stderr = proc.stderr.text stderr and print(stderr) print('--') return returncode
[docs] def check_google_domain(domain, dkim_prefix=DEFAULT_GOOGLE_DKIM_PREFIX): """ Check DNS MX, SPF, DMARC, and DKIM records for a Google Apps domain Args: domain (str): DNS domain dkim_prefix (str): DKIM prefix (``<prefix>._domainkey``) Returns: int: nonzero returncode on failure (sum of returncodes) | https://support.google.com/a/answer/2716802 """ mx = check_google_mx(domain) spf = check_google_spf(domain) dmarc = check_google_dmarc(domain) dkim = check_google_dkim(domain, prefix=dkim_prefix) returncode = mx + spf + dmarc + dkim if returncode: log.error('err', mx=mx, spf=spf, dmarc=dmarc, dkim=dkim) else: log.info('OK', mx=mx, spf=spf, dmarc=dmarc, dkim=dkim) return returncode
#import unittest # class Test_domain_tools(unittest.TestCase): # def test_domain_tools(self): # pass
[docs] def main(*args): """ :py:mod:`wrdrd.tools.domain` main method Args: args (list): commandline arguments Returns: int: nonzero returncode on failure (sum of returncodes) """ import logging import optparse import sys prs = optparse.OptionParser( usage="%prog <domain>", description="Collect DNS information with whois and dig" ) prs.add_option('-g', '--check-google-domain', dest='check_google_domain', action='store_true', help="Check Google MX, SPF, DMARC records") prs.add_option('-v', '--verbose', dest='verbose', action='store_true',) prs.add_option('-q', '--quiet', dest='quiet', action='store_true',) prs.add_option('-t', '--test', dest='run_tests', action='store_true',) args = args and list(args) or sys.argv[1:] (opts, args) = prs.parse_args() if len(args) != 1: raise Exception("Must specify a domain name") domain = args[0] if not opts.quiet: logging.basicConfig() if opts.verbose: logging.getLogger().setLevel(logging.DEBUG) if opts.run_tests: sys.argv = [sys.argv[0]] + args import unittest sys.exit(unittest.main()) returncode = 0 returncode += domain_tools(domain) print("domain_tools: %d" % returncode) if opts.check_google_domain: print("## check_google_domain: %r" % domain) returncode += check_google_domain(domain) print("check_google_domain: %d" % returncode) return returncode
if __name__ == "__main__": import sys sys.exit(main())