Source code for dns_sprockets_lib.validators

'''
__init__.py - Validators library for dns_sprockets zone validator.
------------------------------------------------------------------

.. Copyright (c) 2015 Neustar, Inc. All rights reserved.
.. See COPYRIGHT.txt for full notice.  See LICENSE.txt for terms and conditions.
'''


import dns.rdatatype
import dns.name
import dns.rdata
import dns.rdtypes
import dns.rdtypes.ANY
import dns.node

import dns_sprockets_lib.utils as utils
import dns_sprockets_lib.dns_utils as dns_utils


# Test types:
(ZONE_TEST,   # A test type that validates entire zone.
 NODE_TEST,   # A test type that validates nodes in the zone.
 RRSET_TEST,  # A test type that validates RRSets in the zone.
 REC_TEST     # A test type that validates individual records in the zone.
) = range(4)


[docs]def test_type_to_str(test_type, test_rrtype=None): ''' Convert a test_type and test_rrtype to a string for output purposes. :param int test_type: The TEST_TYPE attribute from the test. :param str test_rrtype: The string describing record type(s) covered by the test. :return: Description string for test. ''' specific = test_rrtype and '[%s]' % (test_rrtype) or '' if test_type == ZONE_TEST: return 'ZONE_TEST' + specific elif test_type == NODE_TEST: return 'NODE_TEST' + specific elif test_type == RRSET_TEST: return 'RRSET_TEST' + specific elif test_type == REC_TEST: return 'REC_TEST' + specific
[docs]def rec_to_abbrev_text(name, ttl, klass, rdata): ''' Translates a record to abbreviated text. For most records, this is the same as the to_text(); for others (such as RRSIG), it is truncated to attempt to fit on a single terminal line. :param str name: The owner name of the record. :param int ttl: The TTL for the record. :param int/str klass: The class of the record. :param obj rdata: The rdata of the record. :return: Text description of the record. ''' if isinstance(rdata, dns.rdtypes.ANY.RRSIG.RRSIG): # pylint: disable=protected-access sig = dns.rdata._base64ify(rdata.signature) rdata_txt = '%s %d %d %d %s %s %d %s %s' % ( dns.rdatatype.to_text(rdata.type_covered), rdata.algorithm, rdata.labels, rdata.original_ttl, dns.rdtypes.ANY.RRSIG.posixtime_to_sigtime(rdata.expiration), dns.rdtypes.ANY.RRSIG.posixtime_to_sigtime(rdata.inception), rdata.key_tag, rdata.signer, '%s...%s' % (sig[:6], sig[len(sig) - 6:])) else: rdata_txt = rdata.to_text(relativize=False) klass_txt = (isinstance(klass, (str, unicode)) and klass or dns.rdataclass.to_text(klass)) return '%s %d %s %s %s' % ( name, ttl, klass_txt, rdata.__class__.__name__, rdata_txt)
[docs]def dnssec_filter_tests_by_context(tests, context): ''' Removes any tests from the tests list that do not apply to the context. :param list tests: List of tests to filter. :param obj context: The context being used. ''' remove_tests = [] for test in tests: rem_test = None if context.dnssec_type == 'unsigned': if test.TEST_DNSSECTYPE is not None: rem_test = test elif context.dnssec_type == 'NSEC': if test.TEST_DNSSECTYPE == 'NSEC3': rem_test = test elif context.dnssec_type == 'NSEC3': if test.TEST_DNSSECTYPE == 'NSEC': rem_test = test if rem_test: remove_tests.append(rem_test) print '# Skipping test: %s (DNSSEC type for zone: %s, for test: %s)' % ( rem_test.TEST_NAME, context.dnssec_type, test.TEST_DNSSECTYPE) for test in remove_tests: tests.remove(test)
[docs]def filter_node(node, test_rrtype): ''' Returns a node that has rdatasets that match the test RR types. If the test_rrtype is specified, a new, temporary node for use by the validator will be generated, which only has those rdatasets mentioned. :param obj node: The node to inspect. :param str test_rrtype: The string description of RR type(s) that the test covers. :return: The node for the validator to examine. :rtype: obj ''' if not test_rrtype: return node rrtypes_txt = test_rrtype.split(',') rrtypes = [dns.rdatatype.from_text(t) for t in rrtypes_txt] new_node = dns.node.Node() new_node.rdatasets = [rds for rds in node.rdatasets if rds.rdtype in rrtypes] return new_node
[docs]def test_covers_type(test, rdtype): ''' Checks to see if a test covers a RR type. :param obj test: The test to examine. :param int rdtype: The dns.rdatatype for the rdataset/record under consideration. :return: True if the test covers the type; False if not. ''' if not test.TEST_RRTYPE: return True rrtypes_txt = test.TEST_RRTYPE.split(',') rrtypes = [dns.rdatatype.from_text(t) for t in rrtypes_txt] return rdtype in rrtypes
[docs]def make_suggested_tested(test, context, **kwargs): ''' Generates a description for the test being run. A test description is printed for each test instance being run against zone, node, rrset, or record, and this is the suggested description. Usually, specific test instances will use this value for 'tested' return variable, but are free to ignore this description in favor of their own if desired. :param obj test: The test being run. :param obj context: The testing context. :param dict kwargs: Optional, test-type-specific parameters. :return: A string describing the test instance being run. ''' if test.TEST_TYPE == ZONE_TEST: suggested_tested = 'ZONE(%s %s)' % ( context.zone_name, dns.rdataclass.to_text(context.zone_obj.rdclass)) elif test.TEST_TYPE == NODE_TEST: suggested_tested = 'NODE(%s %s)' % ( kwargs['name'], dns.rdataclass.to_text(context.zone_obj.rdclass)) elif test.TEST_TYPE == RRSET_TEST: suggested_tested = 'RRSET(%s %s %s)' % ( kwargs['name'], dns.rdataclass.to_text(context.zone_obj.rdclass), dns.rdatatype.to_text(kwargs['rdataset'].rdtype)) elif test.TEST_TYPE == REC_TEST: suggested_tested = 'REC(%s)' % ( rec_to_abbrev_text( kwargs['name'], kwargs['ttl'], context.zone_obj.rdclass, kwargs['rdata'])) return suggested_tested
[docs]class Context(object): # pylint: disable=too-few-public-methods # pylint: disable=too-many-instance-attributes ''' A testing context containing the zone name, zone_obj, etc. '''
[docs] def __init__(self, args, zone_obj): ''' Ctor. :param obj args: The application arguments. :param obj zone_obj: The dns.zone.Zone instance. ''' self.zone_name = dns.name.from_text(args.zone) self.zone_obj = zone_obj # Get DNSSEC-ordered list of names in zone, including any Empty Non- # Terminals implied by wildcard names: self.node_names = dns_utils.calc_node_names(zone_obj.nodes.keys()) # Get SOA if available: self.soa_rdataset = self.zone_obj.get_rdataset( self.zone_name, dns.rdatatype.SOA) # Get DNSKEY(s) if available: self.dnskey_rdataset = self.zone_obj.get_rdataset( self.zone_name, dns.rdatatype.DNSKEY) # Get NSEC3PARAM(s) if available: self.nsec3param_rdataset = self.zone_obj.get_rdataset( self.zone_name, dns.rdatatype.NSEC3PARAM) # Get delegated zones if any: self.delegated_names = [ name for (name, _) in self.zone_obj.iterate_rdatasets('NS') if name != self.zone_name] # Force or detect zone's DNSSEC type: if args.force_dnssec_type != 'detect': self.dnssec_type = args.force_dnssec_type else: # See if there are any NSEC or NSEC3's: has_nsec = next(self.zone_obj.iterate_rdatasets(dns.rdatatype.NSEC), None) has_nsec3 = (self.nsec3param_rdataset or next(self.zone_obj.iterate_rdatasets(dns.rdatatype.NSEC3), None)) # See if this appears to be a signed zone (note: can't seem to practically # check all RRSIG's since they "cover" other records, which would require # us to iterate all possible "covers" values, so just try a few obvious ones): seems_signed = ( self.dnskey_rdataset or has_nsec or has_nsec3 or next(self.zone_obj.iterate_rdatasets( dns.rdatatype.DS), None) or next(self.zone_obj.iterate_rdatasets( dns.rdatatype.RRSIG, dns.rdatatype.SOA), None) or next(self.zone_obj.iterate_rdatasets( dns.rdatatype.RRSIG, dns.rdatatype.NS), None) or next(self.zone_obj.iterate_rdatasets( dns.rdatatype.RRSIG, dns.rdatatype.A), None) or next(self.zone_obj.iterate_rdatasets( dns.rdatatype.RRSIG, dns.rdatatype.AAAA), None)) self.dnssec_type = ( has_nsec3 and 'NSEC3' or has_nsec and 'NSEC' or seems_signed and 'NSEC3' or # assume NSEC3-type 'unsigned')
[docs] def is_delegated(self, name): ''' :return: True if name is delegated w.r.t. the context. ''' return dns_utils.is_delegated(self.delegated_names, name) # # For validator classes, use short docstrings, which will be used for the actual # test descriptions! #
class _Validator(object): # pylint: disable=too-few-public-methods ''' [Base class for validator classes] ''' TEST_NAME = None # Automatically set in __init__. TEST_TYPE = None # Override expected! e.g.: ZONE_TEST TEST_DNSSECTYPE = None # Override possible! one of: None, True, 'NSEC' or 'NSEC3' TEST_RRTYPE = None # Override possible! e.g.: 'A', or 'RRSIG,NSEC3PARAM' TEST_OPTARGS = {} # Override possible! e.g.: {'now': (None, 'Time to use for now')} def __init__(self, args): ''' Ctor, caches the arguments used to run the application, and grabs any optional test arguments. ''' # pylint: disable=invalid-name self.TEST_NAME = utils.camelcase_to_underscores(self.__class__.__name__) self.args = args utils.process_optargs(self.TEST_OPTARGS, self.TEST_NAME, self)
[docs]class ZoneTest(_Validator): # pylint: disable=too-few-public-methods ''' [Base class for zone-type validators] ''' TEST_TYPE = ZONE_TEST
[docs] def run(self, suggested_tested, context): # pylint: disable=unused-argument ''' Runs the zone-type validator. :param str suggested_tested: A suggested tested value. :param obj context: The testing context. :return: A tuple (tested, result) ''' return ('OOPS!', 'ERROR: run() not overridden for %s' % (self.TEST_NAME))
[docs]class NodeTest(_Validator): # pylint: disable=too-few-public-methods ''' [Base class for node-type validators. Derived classes *may* be restricted to specific RRType's by specifying a TEST_RRTYPE] ''' TEST_TYPE = NODE_TEST
[docs] def run(self, context, suggested_tested, name, node): # pylint: disable=unused-argument ''' Runs the node-type validator. If a TEST_RRTYPE specified, the node presented to the validator will be filtered accordingly. :param obj context: The testing context. :param str suggested_tested: A suggested tested value. :param str name: The name being tested. :param obj node: The dns.Node corresponding to the name. :return: A tuple (tested, result) ''' return ('OOPS!', 'ERROR: run() not overridden for %s' % (self.TEST_NAME))
[docs]class RRSetTest(_Validator): # pylint: disable=too-few-public-methods ''' [Base class for rrset-type validators. Derived classes *may* be restricted to specific RRType's by specifying a TEST_RRTYPE] ''' TEST_TYPE = RRSET_TEST
[docs] def run(self, context, suggested_tested, name, rdataset): # pylint: disable=unused-argument ''' Runs the name-type validator. If a TEST_RRTYPE is specified, the RRSet presented to the validator will be filtered accordingly. :param obj context: The testing context. :param str suggested_tested: A suggested tested value. :param str name: The name being tested. :param obj rdataset: The dns.rdataset corresponding to the name. :return: A tuple (tested, result) ''' return ('OOPS!', 'ERROR: run() not overridden for %s' % (self.TEST_NAME))
[docs]class RecTest(_Validator): # pylint: disable=too-few-public-methods ''' [Base class for record-type validators. Derived classes *may* be restricted to specific RRType's by specifying a TEST_RRTYPE] ''' TEST_TYPE = REC_TEST
[docs] def run(self, context, suggested_tested, name, ttl, rdata): # pylint: disable=too-many-arguments # pylint: disable=unused-argument ''' Runs the record-type validator. If a TEST_RRTYPE is specified, the validator will only see those types of records. :param obj context: The testing context. :param str suggested_tested: A suggested tested value. :param str name: The name of the record being tested. :param int ttl: The TTL of the record being tested. :param obj rdata: The dns.rdata.Rdata object being tested. :return: A tuple (tested, result) ''' return ('OOPS!', 'ERROR: run() not overridden for %s' % (self.TEST_NAME)) # end of file