1#!@PYTHON@ 2############################################################################ 3# Copyright (C) 2013, 2014 Internet Systems Consortium, Inc. ("ISC") 4# 5# Permission to use, copy, modify, and/or distribute this software for any 6# purpose with or without fee is hereby granted, provided that the above 7# copyright notice and this permission notice appear in all copies. 8# 9# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH 10# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, 12# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15# PERFORMANCE OF THIS SOFTWARE. 16############################################################################ 17 18import argparse 19import os 20import glob 21import sys 22import re 23import time 24import calendar 25from collections import defaultdict 26import pprint 27 28prog='dnssec-coverage' 29 30# These routines permit platform-independent location of BIND 9 tools 31if os.name == 'nt': 32 import win32con 33 import win32api 34 35def prefix(bindir = ''): 36 if os.name != 'nt': 37 return os.path.join('@prefix@', bindir) 38 39 bind_subkey = "Software\\ISC\\BIND" 40 hKey = None 41 keyFound = True 42 try: 43 hKey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey) 44 except: 45 keyFound = False 46 if keyFound: 47 try: 48 (namedBase, _) = win32api.RegQueryValueEx(hKey, "InstallDir") 49 except: 50 keyFound = False 51 win32api.RegCloseKey(hKey) 52 if keyFound: 53 return os.path.join(namedBase, bindir) 54 return os.path.join(win32api.GetSystemDirectory(), bindir) 55 56######################################################################## 57# Class Event 58######################################################################## 59class Event: 60 """ A discrete key metadata event, e.g., Publish, Activate, Inactive, 61 Delete. Stores the date of the event, and identifying information about 62 the key to which the event will occur.""" 63 64 def __init__(self, _what, _key): 65 now = time.time() 66 self.what = _what 67 self.when = _key.metadata[_what] 68 self.key = _key 69 self.keyid = _key.keyid 70 self.sep = _key.sep 71 self.zone = _key.zone 72 self.alg = _key.alg 73 74 def __repr__(self): 75 return repr((self.when, self.what, self.keyid, self.sep, 76 self.zone, self.alg)) 77 78 def showtime(self): 79 return time.strftime("%a %b %d %H:%M:%S UTC %Y", self.when) 80 81 def showkey(self): 82 return self.key.showkey() 83 84 def showkeytype(self): 85 return self.key.showkeytype() 86 87######################################################################## 88# Class Key 89######################################################################## 90class Key: 91 """An individual DNSSEC key. Identified by path, zone, algorithm, keyid. 92 Contains a dictionary of metadata events.""" 93 94 def __init__(self, keyname): 95 directory = os.path.dirname(keyname) 96 key = os.path.basename(keyname) 97 (zone, alg, keyid) = key.split('+') 98 keyid = keyid.split('.')[0] 99 key = [zone, alg, keyid] 100 key_file = directory + os.sep + '+'.join(key) + ".key" 101 private_file = directory + os.sep + '+'.join(key) + ".private" 102 103 self.zone = zone[1:-1] 104 self.alg = int(alg) 105 self.keyid = int(keyid) 106 107 kfp = open(key_file, "r") 108 for line in kfp: 109 if line[0] == ';': 110 continue 111 tokens = line.split() 112 if not tokens: 113 continue 114 115 if tokens[1].lower() in ('in', 'ch', 'hs'): 116 septoken = 3 117 self.ttl = args.keyttl 118 if not self.ttl: 119 vspace() 120 print("WARNING: Unable to determine TTL for DNSKEY %s." % 121 self.showkey()) 122 print("\t Using 1 day (86400 seconds); re-run with the -d " 123 "option for more\n\t accurate results.") 124 self.ttl = 86400 125 else: 126 septoken = 4 127 self.ttl = int(tokens[1]) if not args.keyttl else args.keyttl 128 129 if (int(tokens[septoken]) & 0x1) == 1: 130 self.sep = True 131 else: 132 self.sep = False 133 kfp.close() 134 135 pfp = open(private_file, "rU") 136 propDict = dict() 137 for propLine in pfp: 138 propDef = propLine.strip() 139 if len(propDef) == 0: 140 continue 141 if propDef[0] in ('!', '#'): 142 continue 143 punctuation = [propDef.find(c) for c in ':= '] + [len(propDef)] 144 found = min([ pos for pos in punctuation if pos != -1 ]) 145 name = propDef[:found].rstrip() 146 value = propDef[found:].lstrip(":= ").rstrip() 147 propDict[name] = value 148 149 if("Publish" in propDict): 150 propDict["Publish"] = time.strptime(propDict["Publish"], 151 "%Y%m%d%H%M%S") 152 153 if("Activate" in propDict): 154 propDict["Activate"] = time.strptime(propDict["Activate"], 155 "%Y%m%d%H%M%S") 156 157 if("Inactive" in propDict): 158 propDict["Inactive"] = time.strptime(propDict["Inactive"], 159 "%Y%m%d%H%M%S") 160 161 if("Delete" in propDict): 162 propDict["Delete"] = time.strptime(propDict["Delete"], 163 "%Y%m%d%H%M%S") 164 165 if("Revoke" in propDict): 166 propDict["Revoke"] = time.strptime(propDict["Revoke"], 167 "%Y%m%d%H%M%S") 168 pfp.close() 169 self.metadata = propDict 170 171 def showkey(self): 172 return "%s/%03d/%05d" % (self.zone, self.alg, self.keyid); 173 174 def showkeytype(self): 175 return ("KSK" if self.sep else "ZSK") 176 177 # ensure that the gap between Publish and Activate is big enough 178 def check_prepub(self): 179 now = time.time() 180 181 if (not "Activate" in self.metadata): 182 debug_print("No Activate information in key: %s" % self.showkey()) 183 return False 184 a = calendar.timegm(self.metadata["Activate"]) 185 186 if (not "Publish" in self.metadata): 187 debug_print("No Publish information in key: %s" % self.showkey()) 188 if a > now: 189 vspace() 190 print("WARNING: Key %s (%s) is scheduled for activation but \n" 191 "\t not for publication." % 192 (self.showkey(), self.showkeytype())) 193 return False 194 p = calendar.timegm(self.metadata["Publish"]) 195 196 now = time.time() 197 if p < now and a < now: 198 return True 199 200 if p == a: 201 vspace() 202 print ("WARNING: %s (%s) is scheduled to be published and\n" 203 "\t activated at the same time. This could result in a\n" 204 "\t coverage gap if the zone was previously signed." % 205 (self.showkey(), self.showkeytype())) 206 print("\t Activation should be at least %s after publication." 207 % duration(self.ttl)) 208 return True 209 210 if a < p: 211 vspace() 212 print("WARNING: Key %s (%s) is active before it is published" % 213 (self.showkey(), self.showkeytype())) 214 return False 215 216 if (a - p < self.ttl): 217 vspace() 218 print("WARNING: Key %s (%s) is activated too soon after\n" 219 "\t publication; this could result in coverage gaps due to\n" 220 "\t resolver caches containing old data." 221 % (self.showkey(), self.showkeytype())) 222 print("\t Activation should be at least %s after publication." % 223 duration(self.ttl)) 224 return False 225 226 return True 227 228 # ensure that the gap between Inactive and Delete is big enough 229 def check_postpub(self, timespan = None): 230 if not timespan: 231 timespan = self.ttl 232 233 now = time.time() 234 235 if (not "Delete" in self.metadata): 236 debug_print("No Delete information in key: %s" % self.showkey()) 237 return False 238 d = calendar.timegm(self.metadata["Delete"]) 239 240 if (not "Inactive" in self.metadata): 241 debug_print("No Inactive information in key: %s" % self.showkey()) 242 if d > now: 243 vspace() 244 print("WARNING: Key %s (%s) is scheduled for deletion but\n" 245 "\t not for inactivation." % 246 (self.showkey(), self.showkeytype())) 247 return False 248 i = calendar.timegm(self.metadata["Inactive"]) 249 250 if d < now and i < now: 251 return True 252 253 if (d < i): 254 vspace() 255 print("WARNING: Key %s (%s) is scheduled for deletion before\n" 256 "\t inactivation." % (self.showkey(), self.showkeytype())) 257 return False 258 259 if (d - i < timespan): 260 vspace() 261 print("WARNING: Key %s (%s) scheduled for deletion too soon after\n" 262 "\t deactivation; this may result in coverage gaps due to\n" 263 "\t resolver caches containing old data." 264 % (self.showkey(), self.showkeytype())) 265 print("\t Deletion should be at least %s after inactivation." % 266 duration(timespan)) 267 return False 268 269 return True 270 271######################################################################## 272# class Zone 273######################################################################## 274class Zone: 275 """Stores data about a specific zone""" 276 277 def __init__(self, _name, _keyttl = None, _maxttl = None): 278 self.name = _name 279 self.keyttl = _keyttl 280 self.maxttl = _maxttl 281 282 def load(self, filename): 283 if not args.compilezone: 284 sys.stderr.write(prog + ': FATAL: "named-compilezone" not found\n') 285 exit(1) 286 287 if not self.name: 288 return 289 290 maxttl = keyttl = None 291 292 fp = os.popen("%s -o - %s %s 2> /dev/null" % 293 (args.compilezone, self.name, filename)) 294 for line in fp: 295 fields = line.split() 296 if not maxttl or int(fields[1]) > maxttl: 297 maxttl = int(fields[1]) 298 if fields[3] == "DNSKEY": 299 keyttl = int(fields[1]) 300 fp.close() 301 302 self.keyttl = keyttl 303 self.maxttl = maxttl 304 305############################################################################ 306# debug_print: 307############################################################################ 308def debug_print(debugVar): 309 """pretty print a variable iff debug mode is enabled""" 310 if not args.debug_mode: 311 return 312 if type(debugVar) == str: 313 print("DEBUG: " + debugVar) 314 else: 315 print("DEBUG: " + pprint.pformat(debugVar)) 316 return 317 318############################################################################ 319# vspace: 320############################################################################ 321_firstline = True 322def vspace(): 323 """adds vertical space between two sections of output text if and only 324 if this is *not* the first section being printed""" 325 global _firstline 326 if _firstline: 327 _firstline = False 328 else: 329 print 330 331############################################################################ 332# vreset: 333############################################################################ 334def vreset(): 335 """reset vertical spacing""" 336 global _firstline 337 _firstline = True 338 339############################################################################ 340# getunit 341############################################################################ 342def getunit(secs, size): 343 """given a number of seconds, and a number of seconds in a larger unit of 344 time, calculate how many of the larger unit there are and return both 345 that and a remainder value""" 346 bigunit = secs // size 347 if bigunit: 348 secs %= size 349 return (bigunit, secs) 350 351############################################################################ 352# addtime 353############################################################################ 354def addtime(output, unit, t): 355 """add a formatted unit of time to an accumulating string""" 356 if t: 357 output += ("%s%d %s%s" % 358 ((", " if output else ""), 359 t, unit, ("s" if t > 1 else ""))) 360 361 return output 362 363############################################################################ 364# duration: 365############################################################################ 366def duration(secs): 367 """given a length of time in seconds, print a formatted human duration 368 in larger units of time 369 """ 370 # define units: 371 minute = 60 372 hour = minute * 60 373 day = hour * 24 374 month = day * 30 375 year = day * 365 376 377 # calculate time in units: 378 (years, secs) = getunit(secs, year) 379 (months, secs) = getunit(secs, month) 380 (days, secs) = getunit(secs, day) 381 (hours, secs) = getunit(secs, hour) 382 (minutes, secs) = getunit(secs, minute) 383 384 output = '' 385 output = addtime(output, "year", years) 386 output = addtime(output, "month", months) 387 output = addtime(output, "day", days) 388 output = addtime(output, "hour", hours) 389 output = addtime(output, "minute", minutes) 390 output = addtime(output, "second", secs) 391 return output 392 393############################################################################ 394# parse_time 395############################################################################ 396def parse_time(s): 397 """convert a formatted time (e.g., 1y, 6mo, 15mi, etc) into seconds""" 398 s = s.strip() 399 400 # if s is an integer, we're done already 401 try: 402 n = int(s) 403 return n 404 except: 405 pass 406 407 # try to parse as a number with a suffix indicating unit of time 408 r = re.compile('([0-9][0-9]*)\s*([A-Za-z]*)') 409 m = r.match(s) 410 if not m: 411 raise Exception("Cannot parse %s" % s) 412 (n, unit) = m.groups() 413 n = int(n) 414 unit = unit.lower() 415 if unit[0] == 'y': 416 return n * 31536000 417 elif unit[0] == 'm' and unit[1] == 'o': 418 return n * 2592000 419 elif unit[0] == 'w': 420 return n * 604800 421 elif unit[0] == 'd': 422 return n * 86400 423 elif unit[0] == 'h': 424 return n * 3600 425 elif unit[0] == 'm' and unit[1] == 'i': 426 return n * 60 427 elif unit[0] == 's': 428 return n 429 else: 430 raise Exception("Invalid suffix %s" % unit) 431 432############################################################################ 433# algname: 434############################################################################ 435def algname(alg): 436 """return the mnemonic for a DNSSEC algorithm""" 437 names = (None, 'RSAMD5', 'DH', 'DSA', 'ECC', 'RSASHA1', 438 'NSEC3DSA', 'NSEC3RSASHA1', 'RSASHA256', None, 439 'RSASHA512', None, 'ECCGOST', 'ECDSAP256SHA256', 440 'ECDSAP384SHA384') 441 name = None 442 if alg in range(len(names)): 443 name = names[alg] 444 return (name if name else str(alg)) 445 446############################################################################ 447# list_events: 448############################################################################ 449def list_events(eventgroup): 450 """print a list of the events in an eventgroup""" 451 if not eventgroup: 452 return 453 print (" " + eventgroup[0].showtime() + ":") 454 for event in eventgroup: 455 print (" %s: %s (%s)" % 456 (event.what, event.showkey(), event.showkeytype())) 457 458############################################################################ 459# process_events: 460############################################################################ 461def process_events(eventgroup, active, published): 462 """go through the events in an event group in time-order, add to active 463 list upon Activate event, add to published list upon Publish event, 464 remove from active list upon Inactive event, and remove from published 465 upon Delete event. Emit warnings when inconsistant states are reached""" 466 for event in eventgroup: 467 if event.what == "Activate": 468 active.add(event.keyid) 469 elif event.what == "Publish": 470 published.add(event.keyid) 471 elif event.what == "Inactive": 472 if event.keyid not in active: 473 vspace() 474 print ("\tWARNING: %s (%s) scheduled to become inactive " 475 "before it is active" % 476 (event.showkey(), event.showkeytype())) 477 else: 478 active.remove(event.keyid) 479 elif event.what == "Delete": 480 if event.keyid in published: 481 published.remove(event.keyid) 482 else: 483 vspace() 484 print ("WARNING: key %s (%s) is scheduled for deletion before " 485 "it is published, at %s" % 486 (event.showkey(), event.showkeytype())) 487 elif event.what == "Revoke": 488 # We don't need to worry about the logic of this one; 489 # just stop counting this key as either active or published 490 if event.keyid in published: 491 published.remove(event.keyid) 492 if event.keyid in active: 493 active.remove(event.keyid) 494 495 return (active, published) 496 497############################################################################ 498# check_events: 499############################################################################ 500def check_events(eventsList, ksk): 501 """create lists of events happening at the same time, check for 502 inconsistancies""" 503 active = set() 504 published = set() 505 eventgroups = list() 506 eventgroup = list() 507 keytype = ("KSK" if ksk else "ZSK") 508 509 # collect up all events that have the same time 510 eventsfound = False 511 for event in eventsList: 512 # if checking ZSKs, skip KSKs, and vice versa 513 if (ksk and not event.sep) or (event.sep and not ksk): 514 continue 515 516 # we found an appropriate (ZSK or KSK event) 517 eventsfound = True 518 519 # add event to current eventgroup 520 if (not eventgroup or eventgroup[0].when == event.when): 521 eventgroup.append(event) 522 523 # if we're at the end of the list, we're done. if 524 # we've found an event with a later time, start a new 525 # eventgroup 526 if (eventgroup[0].when != event.when): 527 eventgroups.append(eventgroup) 528 eventgroup = list() 529 eventgroup.append(event) 530 531 if eventgroup: 532 eventgroups.append(eventgroup) 533 534 for eventgroup in eventgroups: 535 if (args.checklimit and 536 calendar.timegm(eventgroup[0].when) > args.checklimit): 537 print("Ignoring events after %s" % 538 time.strftime("%a %b %d %H:%M:%S UTC %Y", 539 time.gmtime(args.checklimit))) 540 return True 541 542 (active, published) = \ 543 process_events(eventgroup, active, published) 544 545 list_events(eventgroup) 546 547 # and then check for inconsistencies: 548 if len(active) == 0: 549 print ("ERROR: No %s's are active after this event" % keytype) 550 return False 551 elif len(published) == 0: 552 sys.stdout.write("ERROR: ") 553 print ("ERROR: No %s's are published after this event" % keytype) 554 return False 555 elif len(published.intersection(active)) == 0: 556 sys.stdout.write("ERROR: ") 557 print (("ERROR: No %s's are both active and published " + 558 "after this event") % keytype) 559 return False 560 561 if not eventsfound: 562 print ("ERROR: No %s events found in '%s'" % 563 (keytype, args.path)) 564 return False 565 566 return True 567 568############################################################################ 569# check_zones: 570# ############################################################################ 571def check_zones(eventsList): 572 """scan events per zone, algorithm, and key type, in order of occurrance, 573 noting inconsistent states when found""" 574 global foundprob 575 576 foundprob = False 577 zonesfound = False 578 for zone in eventsList: 579 if args.zone and zone != args.zone: 580 continue 581 582 zonesfound = True 583 for alg in eventsList[zone]: 584 if not args.no_ksk: 585 vspace() 586 print("Checking scheduled KSK events for zone %s, algorithm %s..." % 587 (zone, algname(alg))) 588 if not check_events(eventsList[zone][alg], True): 589 foundprob = True 590 else: 591 print ("No errors found") 592 593 if not args.no_zsk: 594 vspace() 595 print("Checking scheduled ZSK events for zone %s, algorithm %s..." % 596 (zone, algname(alg))) 597 if not check_events(eventsList[zone][alg], False): 598 foundprob = True 599 else: 600 print ("No errors found") 601 602 if not zonesfound: 603 print("ERROR: No key events found for %s in '%s'" % 604 (args.zone, args.path)) 605 exit(1) 606 607############################################################################ 608# fill_eventsList: 609############################################################################ 610def fill_eventsList(eventsList): 611 """populate the list of events""" 612 for zone, algorithms in keyDict.items(): 613 for alg, keys in algorithms.items(): 614 for keyid, keydata in keys.items(): 615 if("Publish" in keydata.metadata): 616 eventsList[zone][alg].append(Event("Publish", keydata)) 617 if("Activate" in keydata.metadata): 618 eventsList[zone][alg].append(Event("Activate", keydata)) 619 if("Inactive" in keydata.metadata): 620 eventsList[zone][alg].append(Event("Inactive", keydata)) 621 if("Delete" in keydata.metadata): 622 eventsList[zone][alg].append(Event("Delete", keydata)) 623 624 eventsList[zone][alg] = sorted(eventsList[zone][alg], 625 key=lambda event: event.when) 626 627 foundprob = False 628 if not keyDict: 629 print("ERROR: No key events found in '%s'" % args.path) 630 exit(1) 631 632############################################################################ 633# set_path: 634############################################################################ 635def set_path(command, default=None): 636 """find the location of a specified command. if a default is supplied 637 and it works, we use it; otherwise we search PATH for a match. If 638 not found, error and exit""" 639 fpath = default 640 if not fpath or not os.path.isfile(fpath) or not os.access(fpath, os.X_OK): 641 path = os.environ["PATH"] 642 if not path: 643 path = os.path.defpath 644 for directory in path.split(os.pathsep): 645 fpath = directory + os.sep + command 646 if os.path.isfile(fpath) or os.access(fpath, os.X_OK): 647 break 648 fpath = None 649 650 return fpath 651 652############################################################################ 653# parse_args: 654############################################################################ 655def parse_args(): 656 """Read command line arguments, set global 'args' structure""" 657 global args 658 compilezone = set_path('named-compilezone', 659 os.path.join(prefix('bin'), 'named-compilezone')) 660 661 parser = argparse.ArgumentParser(description=prog + ': checks future ' + 662 'DNSKEY coverage for a zone') 663 664 parser.add_argument('zone', type=str, help='zone to check') 665 parser.add_argument('-K', dest='path', default='.', type=str, 666 help='a directory containing keys to process', 667 metavar='dir') 668 parser.add_argument('-f', dest='filename', type=str, 669 help='zone master file', metavar='file') 670 parser.add_argument('-m', dest='maxttl', type=str, 671 help='the longest TTL in the zone(s)', 672 metavar='time') 673 parser.add_argument('-d', dest='keyttl', type=str, 674 help='the DNSKEY TTL', metavar='time') 675 parser.add_argument('-r', dest='resign', default='1944000', 676 type=int, help='the RRSIG refresh interval ' 677 'in seconds [default: 22.5 days]', 678 metavar='time') 679 parser.add_argument('-c', dest='compilezone', 680 default=compilezone, type=str, 681 help='path to \'named-compilezone\'', 682 metavar='path') 683 parser.add_argument('-l', dest='checklimit', 684 type=str, default='0', 685 help='Length of time to check for ' 686 'DNSSEC coverage [default: 0 (unlimited)]', 687 metavar='time') 688 parser.add_argument('-z', dest='no_ksk', 689 action='store_true', default=False, 690 help='Only check zone-signing keys (ZSKs)') 691 parser.add_argument('-k', dest='no_zsk', 692 action='store_true', default=False, 693 help='Only check key-signing keys (KSKs)') 694 parser.add_argument('-D', '--debug', dest='debug_mode', 695 action='store_true', default=False, 696 help='Turn on debugging output') 697 parser.add_argument('-v', '--version', action='version', version='9.9.1') 698 699 args = parser.parse_args() 700 701 if args.no_zsk and args.no_ksk: 702 print("ERROR: -z and -k cannot be used together."); 703 exit(1) 704 705 # convert from time arguments to seconds 706 try: 707 if args.maxttl: 708 m = parse_time(args.maxttl) 709 args.maxttl = m 710 except: 711 pass 712 713 try: 714 if args.keyttl: 715 k = parse_time(args.keyttl) 716 args.keyttl = k 717 except: 718 pass 719 720 try: 721 if args.resign: 722 r = parse_time(args.resign) 723 args.resign = r 724 except: 725 pass 726 727 try: 728 if args.checklimit: 729 lim = args.checklimit 730 r = parse_time(args.checklimit) 731 if r == 0: 732 args.checklimit = None 733 else: 734 args.checklimit = time.time() + r 735 except: 736 pass 737 738 # if we've got the values we need from the command line, stop now 739 if args.maxttl and args.keyttl: 740 return 741 742 # load keyttl and maxttl data from zonefile 743 if args.zone and args.filename: 744 try: 745 zone = Zone(args.zone) 746 zone.load(args.filename) 747 if not args.maxttl: 748 args.maxttl = zone.maxttl 749 if not args.keyttl: 750 args.keyttl = zone.maxttl 751 except Exception as e: 752 print("Unable to load zone data from %s: " % args.filename, e) 753 754 if not args.maxttl: 755 vspace() 756 print ("WARNING: Maximum TTL value was not specified. Using 1 week\n" 757 "\t (604800 seconds); re-run with the -m option to get more\n" 758 "\t accurate results.") 759 args.maxttl = 604800 760 761############################################################################ 762# Main 763############################################################################ 764def main(): 765 global keyDict 766 767 parse_args() 768 path=args.path 769 770 print ("PHASE 1--Loading keys to check for internal timing problems") 771 keyDict = defaultdict(lambda : defaultdict(dict)) 772 files = glob.glob(os.path.join(path, '*.private')) 773 for infile in files: 774 key = Key(infile) 775 if args.zone and key.zone != args.zone: 776 continue 777 keyDict[key.zone][key.alg][key.keyid] = key 778 key.check_prepub() 779 if key.sep: 780 key.check_postpub() 781 else: 782 key.check_postpub(args.maxttl + args.resign) 783 784 vspace() 785 print ("PHASE 2--Scanning future key events for coverage failures") 786 vreset() 787 788 eventsList = defaultdict(lambda : defaultdict(list)) 789 fill_eventsList(eventsList) 790 check_zones(eventsList) 791 792 if foundprob: 793 exit(1) 794 else: 795 exit(0) 796 797if __name__ == "__main__": 798 main() 799