1#!/usr/bin/env python 2# 3# This file is a part of DNSViz, a tool suite for DNS/DNSSEC monitoring, 4# analysis, and visualization. 5# Created by Casey Deccio (casey@deccio.net) 6# 7# Copyright 2014-2016 VeriSign, Inc. 8# 9# Copyright 2016-2021 Casey Deccio 10# 11# DNSViz is free software; you can redistribute it and/or modify 12# it under the terms of the GNU General Public License as published by 13# the Free Software Foundation; either version 2 of the License, or 14# (at your option) any later version. 15# 16# DNSViz is distributed in the hope that it will be useful, 17# but WITHOUT ANY WARRANTY; without even the implied warranty of 18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19# GNU General Public License for more details. 20# 21# You should have received a copy of the GNU General Public License along 22# with DNSViz. If not, see <http://www.gnu.org/licenses/>. 23# 24 25from __future__ import unicode_literals 26 27import argparse 28import codecs 29import io 30import json 31import logging 32import os 33import re 34import sys 35 36# minimal support for python2.6 37try: 38 from collections import OrderedDict 39except ImportError: 40 from ordereddict import OrderedDict 41 42import dns.exception, dns.name 43 44from dnsviz.analysis import TTLAgnosticOfflineDomainNameAnalysis, DNS_RAW_VERSION 45from dnsviz.format import latin1_binary_to_string as lb2s 46from dnsviz.util import get_trusted_keys, get_default_trusted_keys 47 48# If the import of DNSAuthGraph fails because of the lack of pygraphviz, it 49# will be reported later 50try: 51 from dnsviz.viz.dnssec import DNSAuthGraph 52except ImportError: 53 try: 54 import pygraphviz 55 except ImportError: 56 pass 57 else: 58 raise 59 60logging.basicConfig(level=logging.WARNING, format='%(message)s') 61logger = logging.getLogger() 62 63class AnalysisInputError(Exception): 64 pass 65 66def finish_graph(G, name_objs, rdtypes, trusted_keys, supported_algs, filename): 67 G.add_trust(trusted_keys, supported_algs=supported_algs) 68 69 try: 70 fh = io.open(filename, 'w', encoding='utf-8') 71 except IOError as e: 72 logger.error('%s: "%s"' % (e.strerror, filename)) 73 sys.exit(3) 74 75 show_colors = fh.isatty() and os.environ.get('TERM', 'dumb') != 'dumb' 76 77 tuples = [] 78 processed = set() 79 for name_obj in name_objs: 80 name_obj.populate_response_component_status(G) 81 tuples.extend(name_obj.serialize_status_simple(rdtypes, processed)) 82 83 fh.write(textualize_status_output(tuples, show_colors)) 84 85TERM_COLOR_MAP = { 86 'BOLD': '\033[1m', 87 'RESET': '\033[0m', 88 'SECURE': '\033[36m', 89 'BOGUS': '\033[31m', 90 'INSECURE': '\033[37m', 91 'NOERROR': '\033[37m', 92 'NXDOMAIN': '\033[37m', 93 'INDETERMINATE': '\033[31m', 94 'NON_EXISTENT': '\033[37m', 95 'VALID': '\033[36m', 96 'INDETERMINATE': '\033[37m', 97 'INDETERMINATE_NO_DNSKEY': '\033[37m', 98 'INDETERMINATE_MATCH_PRE_REVOKE': '\033[37m', 99 'INDETERMINATE_UNKNOWN_ALGORITHM': '\033[33m', 100 'ALGORITHM_IGNORED': '\033[37m', 101 'EXPIRED': '\033[35m', 102 'PREMATURE': '\033[35m', 103 'INVALID_SIG': '\033[31m', 104 'INVALID': '\033[31m', 105 'INVALID_DIGEST': '\033[31m', 106 'INCOMPLETE': '\033[33m', 107 'LAME': '\033[33m', 108 'INVALID_TARGET': '\033[31m', 109 'ERROR': '\033[31m', 110 'WARNING': '\033[33m', 111} 112 113STATUS_MAP = { 114 'SECURE': '.', 115 'BOGUS': '!', 116 'INSECURE': '-', 117 'NON_EXISTENT': '-', 118 'VALID': '.', 119 'INDETERMINATE': '-', 120 'INDETERMINATE_NO_DNSKEY': '-', 121 'INDETERMINATE_MATCH_PRE_REVOKE': '-', 122 'INDETERMINATE_UNKNOWN_ALGORITHM': '?', 123 'ALGORITHM_IGNORED': '-', 124 'EXPIRED': '!', 125 'PREMATURE': '!', 126 'INVALID_SIG': '!', 127 'INVALID': '!', 128 'INVALID_DIGEST': '!', 129 'INCOMPLETE': '?', 130 'LAME': '?', 131 'INVALID_TARGET': '!', 132 'ERROR': '!', 133 'WARNING': '?', 134} 135 136def _errors_warnings_full(warnings, errors, indent, show_color): 137 # display status, errors, and warnings 138 s = '' 139 for error in errors: 140 if show_color: 141 s += '%s%sE:%s%s\n' % (indent, TERM_COLOR_MAP['ERROR'], error, TERM_COLOR_MAP['RESET']) 142 else: 143 s += '%sE:%s\n' % (indent, error) 144 145 for warning in warnings: 146 if show_color: 147 s += '%s%sW:%s%s\n' % (indent, TERM_COLOR_MAP['WARNING'], warning, TERM_COLOR_MAP['RESET']) 148 else: 149 s += '%sW:%s\n' % (indent, warning) 150 151 return s 152 153def _errors_warnings_str(status, warnings, errors, show_color): 154 # display status, errors, and warnings 155 error_str = '' 156 if errors: 157 if show_color: 158 error_str = '%s%s%s' % (TERM_COLOR_MAP['ERROR'], STATUS_MAP['ERROR'], TERM_COLOR_MAP[status]) 159 else: 160 error_str = STATUS_MAP['ERROR'] 161 elif warnings: 162 if show_color: 163 error_str = '%s%s%s' % (TERM_COLOR_MAP['WARNING'], STATUS_MAP['WARNING'], TERM_COLOR_MAP[status]) 164 else: 165 error_str = STATUS_MAP['WARNING'] 166 return '[%s%s]' % (STATUS_MAP[status], error_str) 167 168def _textualize_status_output_response(rdtype_str, status, warnings, errors, rdata, children, depth, show_color): 169 s = '' 170 171 response_prefix = ' %(status_color)s%(status)s%(preindent)s %(indent)s%(rdtype)s: ' 172 response_rdata = '%(rdata)s%(color_reset)s%(status_color_rdata)s%(status_rdata)s%(color_reset)s' 173 join_str_template = '%(status_color)s, ' 174 175 params = {} 176 params['status_color'] = '' 177 params['status_color_rdata'] = '' 178 179 if show_color: 180 params['color_reset'] = TERM_COLOR_MAP['RESET'] 181 else: 182 params['color_reset'] = '' 183 184 # display status, errors, and warnings 185 params['status'] = _errors_warnings_str(status, warnings, errors, show_color) 186 187 # indent based on the presence of errors and warnings 188 if errors or warnings: 189 params['preindent'] = '' 190 else: 191 params['preindent'] = ' ' 192 193 params['rdtype'] = rdtype_str 194 params['indent'] = ' '*depth 195 if show_color: 196 params['status_color'] = TERM_COLOR_MAP[status] 197 s += response_prefix % params 198 199 rdata_set = [] 200 subwarnings_all = warnings[:] 201 suberrors_all = errors[:] 202 for i, (substatus, subwarnings, suberrors, rdata_item) in enumerate(rdata): 203 params['rdata'] = rdata_item 204 # display status, errors, and warnings 205 if substatus is not None: 206 if show_color: 207 params['status_color_rdata'] = TERM_COLOR_MAP[substatus] 208 params['status_rdata'] = ' ' + _errors_warnings_str(substatus, subwarnings, suberrors, show_color) 209 else: 210 params['status_color_rdata'] = '' 211 params['status_rdata'] = '' 212 rdata_set.append(response_rdata % params) 213 214 subwarnings_all.extend(subwarnings) 215 suberrors_all.extend(suberrors) 216 217 join_str = join_str_template % params 218 s += join_str.join(rdata_set) + '\n' 219 220 s += _errors_warnings_full(subwarnings_all, suberrors_all, ' ' + params['preindent'] + params['indent'], show_color) 221 222 for rdtype_str_child, status_child, warnings_child, errors_child, rdata_child, children_child in children: 223 s += _textualize_status_output_response(rdtype_str_child, status_child, warnings_child, errors_child, rdata_child, children_child, depth + 1, show_color) 224 225 return s 226 227def _textualize_status_output_name(name, zone_status, zone_warnings, zone_errors, delegation_status, delegation_warnings, delegation_errors, responses, show_color): 228 s = '' 229 230 name_template = '%(status_color)s%(name)s%(color_reset)s%(status_color_rdata)s%(status_rdata)s%(color_reset)s\n' 231 232 params = {} 233 params['status_color'] = '' 234 params['status_color_rdata'] = '' 235 236 if show_color: 237 params['color_reset'] = TERM_COLOR_MAP['RESET'] 238 else: 239 params['color_reset'] = '' 240 241 warnings_all = zone_warnings + delegation_warnings 242 errors_all = zone_errors + delegation_errors 243 244 params['name'] = name 245 params['status_rdata'] = '' 246 if show_color: 247 params['status_color'] = TERM_COLOR_MAP['BOLD'] 248 params['color_reset'] = TERM_COLOR_MAP['RESET'] 249 if zone_status is not None: 250 params['status_rdata'] += ' ' + _errors_warnings_str(zone_status, zone_warnings, zone_errors, show_color) 251 if show_color: 252 params['status_color_rdata'] = TERM_COLOR_MAP[zone_status] 253 if delegation_status is not None: 254 params['status_rdata'] += ' ' + _errors_warnings_str(delegation_status, delegation_warnings, delegation_errors, show_color) 255 if show_color: 256 params['status_color_rdata'] = TERM_COLOR_MAP[delegation_status] 257 s += name_template % params 258 259 s += _errors_warnings_full(warnings_all, errors_all, ' ', show_color) 260 261 for rdtype_str, status, warnings, errors, rdata, children in responses: 262 s += _textualize_status_output_response(rdtype_str, status, warnings, errors, rdata, children, 0, show_color) 263 264 return s 265 266def textualize_status_output(names, show_color): 267 s = '' 268 for name, zone_status, zone_warnings, zone_errors, delegation_status, delegation_warnings, delegation_errors, responses in names: 269 s += _textualize_status_output_name(name, zone_status, zone_warnings, zone_errors, delegation_status, delegation_warnings, delegation_errors, responses, show_color) 270 271 return s 272 273def test_pygraphviz(): 274 try: 275 try: 276 # pygraphviz < 1.7 used pygraphviz.release.version 277 from pygraphviz import release 278 version = release.version 279 except ImportError: 280 # pygraphviz 1.7 changed to pygraphviz.__version__ 281 from pygraphviz import __version__ 282 version = __version__ 283 try: 284 major, minor = version.split('.')[:2] 285 major = int(major) 286 minor = int(re.sub(r'(\d+)[^\d].*', r'\1', minor)) 287 if (major, minor) < (1,3): 288 logger.error('''pygraphviz version >= 1.3 is required, but version %s is installed.''' % version) 289 sys.exit(2) 290 except ValueError: 291 logger.error('''pygraphviz version >= 1.3 is required, but version %s is installed.''' % version) 292 sys.exit(2) 293 except ImportError: 294 logger.error('''pygraphviz is required, but not installed.''') 295 sys.exit(2) 296 297class PrintArgHelper: 298 299 def __init__(self, logger): 300 self.parser = None 301 302 self.trusted_keys = None 303 self.names = None 304 self.analysis_structured = None 305 306 self.args = None 307 self._arg_mapping = None 308 309 self._logger = logger 310 311 def build_parser(self, prog): 312 self.parser = argparse.ArgumentParser(description='Print the assessment of diagnostic DNS queries', prog=prog) 313 314 # python3/python2 dual compatibility 315 stdin_buffer = io.open(sys.stdin.fileno(), 'rb', closefd=False) 316 stdout_buffer = io.open(sys.stdout.fileno(), 'wb', closefd=False) 317 318 try: 319 self.parser.add_argument('-f', '--names-file', 320 type=argparse.FileType('r', encoding='UTF-8'), 321 action='store', metavar='<filename>', 322 help='Read names from a file') 323 except TypeError: 324 # this try/except is for 325 # python3/python2 dual compatibility 326 self.parser.add_argument('-f', '--names-file', 327 type=argparse.FileType('r'), 328 action='store', metavar='<filename>', 329 help='Read names from a file') 330 #self.parser.add_argument('-s', '--silent', 331 # const=True, default=False, 332 # action='store_const', 333 # help='Suppress error messages') 334 try: 335 self.parser.add_argument('-r', '--input-file', 336 type=argparse.FileType('r', encoding='UTF-8'), default=stdin_buffer, 337 action='store', metavar='<filename>', 338 help='Read diagnostic queries from a file') 339 except TypeError: 340 # this try/except is for 341 # python3/python2 dual compatibility 342 self.parser.add_argument('-r', '--input-file', 343 type=argparse.FileType('r'), default=stdin_buffer, 344 action='store', metavar='<filename>', 345 help='Read diagnostic queries from a file') 346 try: 347 self.parser.add_argument('-t', '--trusted-keys-file', 348 type=argparse.FileType('r', encoding='UTF-8'), 349 action='append', metavar='<filename>', 350 help='Use trusted keys from the designated file') 351 except TypeError: 352 # this try/except is for 353 # python3/python2 dual compatibility 354 self.parser.add_argument('-t', '--trusted-keys-file', 355 type=argparse.FileType('r'), 356 action='append', metavar='<filename>', 357 help='Use trusted keys from the designated file') 358 self.parser.add_argument('-a', '--algorithms', 359 type=self.comma_separated_ints_set, 360 action='store', metavar='<alg>,[<alg>...]', 361 help='Support only the specified DNSSEC algorithm(s)') 362 self.parser.add_argument('-d', '--digest-algorithms', 363 type=self.comma_separated_ints_set, 364 action='store', metavar='<digest_alg>,[<digest_alg>...]', 365 help='Support only the specified DNSSEC digest algorithm(s)') 366 self.parser.add_argument('-b', '--validate-prohibited-algs', 367 const=True, default=False, 368 action='store_const', 369 help='Validate algorithms for which validation is otherwise prohibited') 370 self.parser.add_argument('-C', '--enforce-cookies', 371 const=True, default=False, 372 action='store_const', 373 help='Enforce DNS cookies strictly') 374 self.parser.add_argument('-P', '--allow-private', 375 const=True, default=False, 376 action='store_const', 377 help='Allow private IP addresses for authoritative DNS servers') 378 self.parser.add_argument('-R', '--rr-types', 379 type=self.comma_separated_dns_types, 380 action='store', metavar='<type>,[<type>...]', 381 help='Process queries of only the specified type(s)') 382 self.parser.add_argument('-O', '--derive-filename', 383 const=True, default=False, 384 action='store_const', 385 help='Derive the filename(s) from domain name(s)') 386 self.parser.add_argument('-o', '--output-file', 387 type=argparse.FileType('wb'), default=stdout_buffer, 388 action='store', metavar='<filename>', 389 help='Save the output to the specified file') 390 self.parser.add_argument('domain_name', 391 type=self.valid_domain_name, 392 action='store', nargs='*', metavar='<domain_name>', 393 help='Domain names') 394 395 self._arg_mapping = dict([(a.dest, '/'.join(a.option_strings)) for a in self.parser._actions]) 396 397 def parse_args(self, args): 398 self.args = self.parser.parse_args(args) 399 400 @classmethod 401 def comma_separated_dns_types(cls, arg): 402 rdtypes = [] 403 arg = arg.strip() 404 if not arg: 405 return rdtypes 406 for r in arg.split(','): 407 try: 408 rdtypes.append(dns.rdatatype.from_text(r.strip())) 409 except dns.rdatatype.UnknownRdatatype: 410 raise argparse.ArgumentTypeError('Invalid resource record type: %s' % (r)) 411 return rdtypes 412 413 @classmethod 414 def comma_separated_ints_set(cls, arg): 415 return set(cls.comma_separated_ints(arg)) 416 417 @classmethod 418 def comma_separated_ints(cls, arg): 419 ints = [] 420 arg = arg.strip() 421 if not arg: 422 return ints 423 for i in arg.split(','): 424 try: 425 ints.append(int(i.strip())) 426 except ValueError: 427 raise argparse.ArgumentTypeError('Invalid integer: %s' % (i)) 428 return ints 429 430 @classmethod 431 def valid_domain_name(cls, arg): 432 try: 433 return dns.name.from_text(arg) 434 except dns.exception.DNSException: 435 raise argparse.ArgumentTypeError('Invalid domain name: "%s"' % arg) 436 437 def check_args(self): 438 if self.args.names_file and self.args.domain_name: 439 raise argparse.ArgumentTypeError('If %(names_file)s is used, then domain names may not supplied as command line arguments.' % \ 440 self._arg_mapping) 441 if self.args.derive_filename and self.args.output_file.fileno() != sys.stdout.fileno(): 442 raise argparse.ArgumentTypeError('The %(derive_filename)s and %(output_file)s options may not be used together.' % \ 443 self._arg_mapping) 444 445 def set_buffers(self): 446 # This entire method is for 447 # python3/python2 dual compatibility 448 if self.args.input_file is not None: 449 if self.args.input_file.fileno() == sys.stdin.fileno(): 450 filename = self.args.input_file.fileno() 451 else: 452 filename = self.args.input_file.name 453 self.args.input_file.close() 454 self.args.input_file = io.open(filename, 'r', encoding='utf-8') 455 if self.args.names_file is not None: 456 if self.args.names_file.fileno() == sys.stdin.fileno(): 457 filename = self.args.names_file.fileno() 458 else: 459 filename = self.args.names_file.name 460 self.args.names_file.close() 461 self.args.names_file = io.open(filename, 'r', encoding='utf-8') 462 if self.args.trusted_keys_file is not None: 463 trusted_keys_files = [] 464 for tk_file in self.args.trusted_keys_file: 465 if tk_file.fileno() == sys.stdin.fileno(): 466 filename = tk_file.fileno() 467 else: 468 filename = tk_file.name 469 tk_file.close() 470 trusted_keys_files.append(io.open(filename, 'r', encoding='utf-8')) 471 self.args.trusted_keys_file = trusted_keys_files 472 if self.args.output_file is not None: 473 if self.args.output_file.fileno() == sys.stdout.fileno(): 474 filename = self.args.output_file.fileno() 475 else: 476 filename = self.args.output_file.name 477 self.args.output_file.close() 478 self.args.output_file = io.open(filename, 'wb') 479 480 def aggregate_trusted_key_info(self): 481 if not self.args.trusted_keys_file: 482 return 483 484 self.trusted_keys = [] 485 for fh in self.args.trusted_keys_file: 486 tk_str = fh.read() 487 try: 488 self.trusted_keys.extend(get_trusted_keys(tk_str)) 489 except dns.exception.DNSException: 490 raise argparse.ArgumentTypeError('There was an error parsing the trusted keys file: "%s"' % \ 491 self._arg_mapping) 492 493 def update_trusted_key_info(self, latest_analysis_date): 494 if self.args.trusted_keys_file is None: 495 self.trusted_keys = get_default_trusted_keys(latest_analysis_date) 496 497 def ingest_input(self): 498 analysis_str = self.args.input_file.read() 499 if not analysis_str: 500 if self.args.input_file.fileno() != sys.stdin.fileno(): 501 raise AnalysisInputError('No input') 502 else: 503 raise AnalysisInputError() 504 try: 505 self.analysis_structured = json.loads(analysis_str) 506 except ValueError: 507 raise AnalysisInputError('There was an error parsing the JSON input: "%s"' % self.args.input_file.name) 508 509 # check version 510 if '_meta._dnsviz.' not in self.analysis_structured or 'version' not in self.analysis_structured['_meta._dnsviz.']: 511 raise AnalysisInputError('No version information in JSON input: "%s"' % self.args.input_file.name) 512 try: 513 major_vers, minor_vers = [int(x) for x in str(self.analysis_structured['_meta._dnsviz.']['version']).split('.', 1)] 514 except ValueError: 515 raise AnalysisInputError('Version of JSON input is invalid: %s' % self.analysis_structured['_meta._dnsviz.']['version']) 516 # ensure major version is a match and minor version is no greater 517 # than the current minor version 518 curr_major_vers, curr_minor_vers = [int(x) for x in str(DNS_RAW_VERSION).split('.', 1)] 519 if major_vers != curr_major_vers or minor_vers > curr_minor_vers: 520 raise AnalysisInputError('Version %d.%d of JSON input is incompatible with this software.' % (major_vers, minor_vers)) 521 522 def ingest_names(self): 523 self.names = OrderedDict() 524 525 if self.args.domain_name: 526 for name in self.args.domain_name: 527 if name not in self.names: 528 self.names[name] = None 529 return 530 531 if self.args.names_file: 532 args = self.args.names_file 533 else: 534 try: 535 args = self.analysis_structured['_meta._dnsviz.']['names'] 536 except KeyError: 537 raise AnalysisInputError('No names found in JSON input!') 538 539 for arg in args: 540 name = arg.strip() 541 542 # python3/python2 dual compatibility 543 if hasattr(name, 'decode'): 544 name = name.decode('utf-8') 545 546 try: 547 name = dns.name.from_text(name) 548 except UnicodeDecodeError as e: 549 self._logger.error('%s: "%s"' % (e, name)) 550 except dns.exception.DNSException: 551 self._logger.error('The domain name was invalid: "%s"' % name) 552 else: 553 if name not in self.names: 554 self.names[name] = None 555 556def build_helper(logger, cmd, subcmd): 557 arghelper = PrintArgHelper(logger) 558 arghelper.build_parser('%s %s' % (cmd, subcmd)) 559 return arghelper 560 561def main(argv): 562 try: 563 test_pygraphviz() 564 565 arghelper = build_helper(logger, sys.argv[0], argv[0]) 566 arghelper.parse_args(argv[1:]) 567 logger.setLevel(logging.WARNING) 568 569 try: 570 arghelper.check_args() 571 arghelper.set_buffers() 572 arghelper.aggregate_trusted_key_info() 573 arghelper.ingest_input() 574 arghelper.ingest_names() 575 except argparse.ArgumentTypeError as e: 576 arghelper.parser.error(str(e)) 577 except AnalysisInputError as e: 578 s = str(e) 579 if s: 580 logger.error(s) 581 sys.exit(3) 582 583 latest_analysis_date = None 584 name_objs = [] 585 cache = {} 586 for name in arghelper.names: 587 name_str = lb2s(name.canonicalize().to_text()) 588 if name_str not in arghelper.analysis_structured or arghelper.analysis_structured[name_str].get('stub', True): 589 logger.error('The analysis of "%s" was not found in the input.' % lb2s(name.to_text())) 590 continue 591 name_obj = TTLAgnosticOfflineDomainNameAnalysis.deserialize(name, arghelper.analysis_structured, cache, strict_cookies=arghelper.args.enforce_cookies, allow_private=arghelper.args.allow_private) 592 name_objs.append(name_obj) 593 594 if latest_analysis_date is None or latest_analysis_date > name_obj.analysis_end: 595 latest_analysis_date = name_obj.analysis_end 596 597 if not name_objs: 598 sys.exit(4) 599 600 arghelper.update_trusted_key_info(latest_analysis_date) 601 602 G = DNSAuthGraph() 603 for name_obj in name_objs: 604 name_obj.populate_status(arghelper.trusted_keys, supported_algs=arghelper.args.algorithms, supported_digest_algs=arghelper.args.digest_algorithms, validate_prohibited_algs=arghelper.args.validate_prohibited_algs) 605 for qname, rdtype in name_obj.queries: 606 if arghelper.args.rr_types is None: 607 # if rdtypes was not specified, then graph all, with some 608 # exceptions 609 if name_obj.is_zone() and rdtype in (dns.rdatatype.DNSKEY, dns.rdatatype.DS, dns.rdatatype.DLV): 610 continue 611 else: 612 # if rdtypes was specified, then only graph rdtypes that 613 # were specified 614 if qname != name_obj.name or rdtype not in arghelper.args.rr_types: 615 continue 616 G.graph_rrset_auth(name_obj, qname, rdtype) 617 618 if arghelper.args.rr_types is not None: 619 for rdtype in arghelper.args.rr_types: 620 if (name_obj.name, rdtype) not in name_obj.queries: 621 logger.error('No query for "%s/%s" was included in the analysis.' % (lb2s(name_obj.name.to_text()), dns.rdatatype.to_text(rdtype))) 622 623 if arghelper.args.derive_filename: 624 if name_obj.name == dns.name.root: 625 name = 'root' 626 else: 627 name = lb2s(name_obj.name.canonicalize().to_text()).rstrip('.') 628 name = name.replace(os.sep, '--') 629 finish_graph(G, [name_obj], arghelper.args.rr_types, arghelper.trusted_keys, arghelper.args.algorithms, '%s.txt' % name) 630 G = DNSAuthGraph() 631 632 if not arghelper.args.derive_filename: 633 finish_graph(G, name_objs, arghelper.args.rr_types, arghelper.trusted_keys, arghelper.args.algorithms, arghelper.args.output_file.fileno()) 634 635 except KeyboardInterrupt: 636 logger.error('Interrupted.') 637 sys.exit(4) 638 639if __name__ == "__main__": 640 main(sys.argv) 641