1# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
2#
3# SPDX-License-Identifier: MPL-2.0
4#
5# This Source Code Form is subject to the terms of the Mozilla Public
6# License, v. 2.0.  If a copy of the MPL was not distributed with this
7# file, you can obtain one at https://mozilla.org/MPL/2.0/.
8#
9# See the COPYRIGHT file distributed with this work for additional
10# information regarding copyright ownership.
11
12from collections import defaultdict
13from .dnskey import *
14from .keydict import *
15from .keyevent import *
16
17
18class eventlist:
19    _K = defaultdict(lambda: defaultdict(list))
20    _Z = defaultdict(lambda: defaultdict(list))
21    _zones = set()
22    _kdict = None
23
24    def __init__(self, kdict):
25        properties = [
26            "SyncPublish",
27            "Publish",
28            "SyncDelete",
29            "Activate",
30            "Inactive",
31            "Delete",
32        ]
33        self._kdict = kdict
34        for zone in kdict.zones():
35            self._zones.add(zone)
36            for alg, keys in kdict[zone].items():
37                for k in keys.values():
38                    for prop in properties:
39                        t = k.gettime(prop)
40                        if not t:
41                            continue
42                        e = keyevent(prop, k, t)
43                        if k.sep:
44                            self._K[zone][alg].append(e)
45                        else:
46                            self._Z[zone][alg].append(e)
47
48                self._K[zone][alg] = sorted(
49                    self._K[zone][alg], key=lambda event: event.when
50                )
51                self._Z[zone][alg] = sorted(
52                    self._Z[zone][alg], key=lambda event: event.when
53                )
54
55    # scan events per zone, algorithm, and key type, in order of
56    # occurrence, noting inconsistent states when found
57    def coverage(self, zone, keytype, until, output=None):
58        def noop(*args, **kwargs):
59            pass
60
61        if not output:
62            output = noop
63
64        no_zsk = True if (keytype and keytype == "KSK") else False
65        no_ksk = True if (keytype and keytype == "ZSK") else False
66        kok = zok = True
67        found = False
68
69        if zone and not zone in self._zones:
70            output("ERROR: No key events found for %s" % zone)
71            return False
72
73        if zone:
74            found = True
75            if not no_ksk:
76                kok = self.checkzone(zone, "KSK", until, output)
77            if not no_zsk:
78                zok = self.checkzone(zone, "ZSK", until, output)
79        else:
80            for z in self._zones:
81                if not no_ksk and z in self._K.keys():
82                    found = True
83                    kok = self.checkzone(z, "KSK", until, output)
84                if not no_zsk and z in self._Z.keys():
85                    found = True
86                    zok = self.checkzone(z, "ZSK", until, output)
87
88        if not found:
89            output("ERROR: No key events found")
90            return False
91
92        return kok and zok
93
94    def checkzone(self, zone, keytype, until, output):
95        allok = True
96        if keytype == "KSK":
97            kz = self._K[zone]
98        else:
99            kz = self._Z[zone]
100
101        for alg in kz.keys():
102            output(
103                "Checking scheduled %s events for zone %s, "
104                "algorithm %s..." % (keytype, zone, dnskey.algstr(alg))
105            )
106            ok = eventlist.checkset(kz[alg], keytype, until, output)
107            if ok:
108                output("No errors found")
109            allok = allok and ok
110
111        return allok
112
113    @staticmethod
114    def showset(eventset, output):
115        if not eventset:
116            return
117        output("  " + eventset[0].showtime() + ":", skip=False)
118        for event in eventset:
119            output("    %s: %s" % (event.what, repr(event.key)), skip=False)
120
121    @staticmethod
122    def checkset(eventset, keytype, until, output):
123        groups = list()
124        group = list()
125
126        # collect up all events that have the same time
127        eventsfound = False
128        for event in eventset:
129            # we found an event
130            eventsfound = True
131
132            # add event to current group
133            if not group or group[0].when == event.when:
134                group.append(event)
135
136            # if we're at the end of the list, we're done.  if
137            # we've found an event with a later time, start a new group
138            if group[0].when != event.when:
139                groups.append(group)
140                group = list()
141                group.append(event)
142
143        if group:
144            groups.append(group)
145
146        if not eventsfound:
147            output("ERROR: No %s events found" % keytype)
148            return False
149
150        active = published = None
151        for group in groups:
152            if until and calendar.timegm(group[0].when) > until:
153                output(
154                    "Ignoring events after %s"
155                    % time.strftime("%a %b %d %H:%M:%S UTC %Y", time.gmtime(until))
156                )
157                return True
158
159            for event in group:
160                (active, published) = event.status(active, published)
161
162            eventlist.showset(group, output)
163
164            # and then check for inconsistencies:
165            if not active:
166                output("ERROR: No %s's are active after this event" % keytype)
167                return False
168            elif not published:
169                output("ERROR: No %s's are published after this event" % keytype)
170                return False
171            elif not published.intersection(active):
172                output(
173                    "ERROR: No %s's are both active and published "
174                    "after this event" % keytype
175                )
176                return False
177
178        return True
179