1#
2# This file is a part of DNSViz, a tool suite for DNS/DNSSEC monitoring,
3# analysis, and visualization.
4# Created by Casey Deccio (casey@deccio.net)
5#
6# Copyright 2012-2014 Sandia Corporation. Under the terms of Contract
7# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains
8# certain rights in this software.
9#
10# Copyright 2014-2016 VeriSign, Inc.
11#
12# Copyright 2016-2021 Casey Deccio
13#
14# DNSViz is free software; you can redistribute it and/or modify
15# it under the terms of the GNU General Public License as published by
16# the Free Software Foundation; either version 2 of the License, or
17# (at your option) any later version.
18#
19# DNSViz is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22# GNU General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License along
25# with DNSViz.  If not, see <http://www.gnu.org/licenses/>.
26#
27
28from __future__ import unicode_literals
29
30import codecs
31import errno
32import io
33import json
34import os
35import re
36import sys
37import xml.dom.minidom
38
39# minimal support for python2.6
40try:
41    from collections import OrderedDict
42except ImportError:
43    from ordereddict import OrderedDict
44
45# python3/python2 dual compatibility
46try:
47    from html import escape
48except ImportError:
49    from cgi import escape
50
51import dns.name, dns.rdtypes, dns.rdatatype, dns.dnssec
52
53from pygraphviz import AGraph
54
55from dnsviz.analysis import status as Status
56from dnsviz.analysis import errors as Errors
57from dnsviz.analysis.online import ANALYSIS_TYPE_RECURSIVE
58from dnsviz.config import DNSVIZ_SHARE_PATH
59from dnsviz import crypto
60from dnsviz import format as fmt
61from dnsviz import query as Q
62from dnsviz import response as Response
63from dnsviz.util import tuple_to_dict
64lb2s = fmt.latin1_binary_to_string
65
66COLORS = { 'secure': '#0a879a', 'secure_non_existent': '#9dcfd6',
67        'bogus': '#be1515', 'bogus_non_existent': '#e5a1a1',
68        'insecure': '#000000', 'insecure_non_existent': '#d0d0d0',
69        'misconfigured': '#f4b800',
70        'indeterminate': '#f4b800',
71        'expired': '#6131a3',
72        'invalid': '#be1515' }
73
74INVIS_STYLE_RE = re.compile(r'(^|,)invis(,|$)')
75DASHED_STYLE_RE = re.compile(r'(^|,)dashed(,|$)')
76OPTOUT_STYLE_RE = re.compile(r'BGCOLOR="lightgray"')
77
78ICON_PATH=os.path.join(DNSVIZ_SHARE_PATH, 'icons')
79WARNING_ICON=os.path.join(ICON_PATH, 'warning.png')
80ERROR_ICON=os.path.join(ICON_PATH, 'error.png')
81
82# python3/python2.6 dual compatibility
83vers0, vers1, vers2 = sys.version_info[:3]
84if (vers0, vers1) == (2, 6):
85    execv_encode = lambda x: codecs.encode(x, sys.getfilesystemencoding())
86else:
87    execv_encode = lambda x: x
88
89class DNSKEYNonExistent(object):
90    def __init__(self, name, algorithm, key_tag):
91        self.name = name
92        self.algorithm = algorithm
93        self.key_tag = key_tag
94
95    def serialize(self):
96        d = OrderedDict()
97        d['flags'] = None
98        d['protocol'] = None
99        d['algorithm'] = self.algorithm
100        d['key'] = None
101        d['ttl'] = None
102        d['key_length'] = None
103        d['key_tag'] = self.key_tag
104        return d
105
106class RRsetNonExistent(object):
107    def __init__(self, name, rdtype, nxdomain, servers_clients):
108        self.name = name
109        self.rdtype = rdtype
110        self.nxdomain = nxdomain
111        self.servers_clients = servers_clients
112
113    def serialize(self, consolidate_clients, html_format=False, map_ip_to_ns_name=None):
114        d = OrderedDict()
115
116        if html_format:
117            formatter = lambda x: escape(x, True)
118        else:
119            formatter = lambda x: x
120
121        if self.rdtype == dns.rdatatype.NSEC3:
122            d['name'] = fmt.format_nsec3_name(self.name)
123        else:
124            d['name'] = formatter(lb2s(self.name.canonicalize().to_text()))
125        d['ttl'] = None
126        d['type'] = dns.rdatatype.to_text(self.rdtype)
127        if self.nxdomain:
128            d['rdata'] = ['NXDOMAIN']
129        else:
130            d['rdata'] = ['NODATA']
131
132        servers = tuple_to_dict(self.servers_clients)
133        if consolidate_clients:
134            servers = list(servers)
135            servers.sort()
136        d['servers'] = servers
137
138        if map_ip_to_ns_name is not None:
139            ns_names = list(set([lb2s(map_ip_to_ns_name(s)[0][0].canonicalize().to_text()) for s in servers]))
140            ns_names.sort()
141            d['ns_names'] = ns_names
142
143        tags = set()
144        nsids = []
145        for server,client in self.servers_clients:
146            for response in self.servers_clients[(server,client)]:
147                tags.add(response.effective_query_tag())
148                nsid = response.nsid_val()
149                if nsid is not None:
150                    nsids.append(nsid)
151
152        if nsids:
153            d['nsid_values'] = nsids
154            d['nsid_values'].sort()
155
156        d['query_options'] = list(tags)
157        d['query_options'].sort()
158
159        return d
160
161class DNSAuthGraph:
162    def __init__(self, dlv_domain=None):
163        self.dlv_domain = dlv_domain
164
165        self.G = AGraph(directed=True, strict=False, compound='true', rankdir='BT', ranksep='0.3')
166
167        self.G.node_attr['penwidth'] = '1.5'
168        self.G.edge_attr['penwidth'] = '1.5'
169        self.node_info = {}
170        self.node_mapping = {}
171        self.node_reverse_mapping = {}
172        self.nsec_rr_status = {}
173        self.secure_dnskey_rrsets = set()
174        self.subgraph_not_stub = set()
175        self.node_subgraph_name = {}
176        self.processed_rrsets = {}
177
178        self.dnskey_ids = {}
179        self.ds_ids = {}
180        self.nsec_ids = {}
181        self.rrset_ids = {}
182        self.next_dnskey_id = 0
183        self.next_ds_id = 0
184        self.next_nsec_id = 0
185        self.next_rrset_id = 10
186
187        self._edge_keys = set()
188
189    def _raphael_unit_mapping_expression(self, val, unit):
190        #XXX doesn't work properly
191        #if unit:
192        #    return '%s*to_pixel_mapping[\'%s\']' % (val, unit)
193        return val
194
195    def _raphael_transform_str(self, trans_value):
196        transform_re = re.compile(r'(scale|rotate|translate)\((-?[0-9\.]+(px|pt|cm|in)?((,\s*|\s+)-?[0-9\.]+(px|pt|cm|in)?)?)\)')
197        number_units_re = re.compile(r'(-?[0-9\.]+)(px|pt|cm|in)?')
198
199        t = ''
200        for m in transform_re.findall(trans_value):
201            if m[0] == 'scale':
202                coords = number_units_re.findall(m[1])
203                if (len(coords) > 1):
204                    t += 's%s,%s,0,0' % (self._raphael_unit_mapping_expression(coords[0][0], coords[0][1]), self._raphael_unit_mapping_expression(coords[1][0], coords[1][1]))
205                else:
206                    t += 's%s,0,0,0' % (coords[0])
207            if m[0] == 'translate':
208                coords = number_units_re.findall(m[1])
209                if (len(coords) > 1):
210                    t += 't%s,%s' % (self._raphael_unit_mapping_expression(coords[0][0], coords[0][1]), self._raphael_unit_mapping_expression(coords[1][0], coords[1][1]))
211                else:
212                    t += 't%s,0,' % (self._raphael_unit_mapping_expression(coords[0][0], coords[0][1]))
213        return t
214
215    def _write_raphael_node(self, node, node_id, transform):
216        required_attrs = { 'path': set(['d']), 'ellipse': set(['cx','cy','rx','ry']),
217            'polygon': set(['points']), 'polyline': set(['points']),
218            'text': set(['x','y']), 'image': set(['src','x','y','width','height']) }
219
220        number_units_re = re.compile(r'(-?[0-9\.]+)(px|pt|cm|in)?')
221
222        s = ''
223        if node.nodeType != xml.dom.Node.ELEMENT_NODE:
224            return s
225        if node.hasAttribute('id'):
226            node_id = node.getAttribute('id')
227        if node.nodeName == 'svg':
228            width, width_unit = number_units_re.match(node.getAttribute('width')).group(1, 2)
229            height, height_unit = number_units_re.match(node.getAttribute('height')).group(1, 2)
230            s += '''
231	var imageWidth = %s*this.imageScale;
232	var imageHeight = %s*this.imageScale;
233	if (this.maxPaperWidth > 0 && imageWidth > this.maxPaperWidth) {
234		paperScale = this.maxPaperWidth/imageWidth;
235	} else {
236		paperScale = 1.0;
237	}
238''' % (width, height)
239            s += '\tpaper = Raphael(this.anchorElement, parseInt(paperScale*imageWidth), parseInt(paperScale*imageHeight));\n'
240        else:
241            if node.nodeName == 'path':
242                s += '\tel = paper.path(\'%s\')' % node.getAttribute('d')
243            elif node.nodeName == 'ellipse':
244                s += '\tel = paper.ellipse(%s, %s, %s, %s)' % (node.getAttribute('cx'), node.getAttribute('cy'),
245                        node.getAttribute('rx'), node.getAttribute('ry'))
246            elif node.nodeName == 'text':
247                if node.childNodes:
248                    text = node.childNodes[0].nodeValue
249                else:
250                    text = ''
251                s += '\tel = paper.text(%s, %s, \'%s\')' % (node.getAttribute('x'), node.getAttribute('y'), text)
252            elif node.nodeName == 'image':
253                width, width_unit = number_units_re.match(node.getAttribute('width')).group(1, 2)
254                height, height_unit = number_units_re.match(node.getAttribute('height')).group(1, 2)
255                s += '\tel = paper.image(\'%s\', %s, %s, %s, %s)' % (node.getAttribute('xlink:href'), node.getAttribute('x'), node.getAttribute('y'), self._raphael_unit_mapping_expression(width, width_unit),self._raphael_unit_mapping_expression(height, height_unit))
256            elif node.nodeName == 'polygon' or node.nodeName == 'polyline':
257                pathstring = 'M';
258                coords = number_units_re.findall(node.getAttribute('points'))
259                for i in range(len(coords)):
260                    if i > 0:
261                        if i % 2 == 0:
262                            pathstring += 'L'
263                        else:
264                            pathstring += ','
265                    pathstring += coords[i][0]
266                if node.nodeName == 'polygon':
267                    pathstring += 'Z'
268                s += '\tel = paper.path(\'%s\')' % pathstring
269            attrs = []
270            for i in range(node.attributes.length):
271                attr = node.attributes.item(i)
272                if attr.name not in required_attrs.get(node.nodeName, set()):
273                    if attr.name == 'stroke-dasharray':
274                        #XXX hack
275                        val = '\'\\-\''
276                    elif attr.name == 'stroke-width':
277                        val = attr.value+'*this.imageScale'
278                    elif attr.name == 'transform':
279                        transform += self._raphael_transform_str(attr.value)
280                        continue
281                    else:
282                        val = '\'%s\'' % attr.value
283                    attrs.append('\'%s\': %s' % (attr.name, val))
284            if transform:
285                attrs.append('\'%s\': \'%s\'' % ('transform', transform))
286            if s:
287                if attrs:
288                    s += '.attr({%s})' % (','.join(attrs))
289                s += ';\n'
290                if node_id is not None and node_id in self.node_info:
291                    s += '\tthis.addNodeEvent(el, node_info[\'%s\']);\n' % node_id.replace('\\', '\\\\').replace('--', '\\-\\-')
292
293        for i in range(node.childNodes.length):
294            s += self._write_raphael_node(node.childNodes[i], node_id, transform)
295        return s
296
297    def to_raphael(self):
298        svg = self.G.draw(format=execv_encode('svg'), prog=execv_encode('dot'))
299        dom = xml.dom.minidom.parseString(svg)
300
301        s = 'AuthGraph.prototype.draw = function () {\n'
302        s += '\tvar el, paperScale;\n'
303        s += '\tvar node_info = %s;\n' % json.dumps(self.node_info)
304        s += self._write_raphael_node(dom.documentElement, None, 's\'+this.imageScale+\',\'+this.imageScale+\',0,0')
305        s += '\tpaper.setViewBox(0, 0, imageWidth, imageHeight);\n'
306        s += '}\n'
307        return codecs.encode(s, 'utf-8')
308
309    def draw(self, format, path=None):
310        if format == 'js':
311            img = self.to_raphael()
312            if path is None:
313                return img
314            else:
315                io.open(path, 'w', encoding='utf-8').write(img)
316        else:
317            if path is None:
318                return self.G.draw(format=execv_encode(format), prog=execv_encode('dot'))
319            else:
320                return self.G.draw(path=execv_encode(path), format=execv_encode(format), prog=execv_encode('dot'))
321
322    def id_for_dnskey(self, name, dnskey):
323        try:
324            return self.dnskey_ids[(name,dnskey)]
325        except KeyError:
326            self.dnskey_ids[(name,dnskey)] = self.next_dnskey_id
327            self.next_dnskey_id += 1
328            return self.dnskey_ids[(name,dnskey)]
329
330    def id_for_ds(self, name, ds):
331        try:
332            return self.ds_ids[(name,ds)]
333        except KeyError:
334            self.ds_ids[(name,ds)] = self.next_ds_id
335            self.next_ds_id += 1
336            return self.ds_ids[(name,ds)]
337
338    def id_for_multiple_ds(self, name, ds):
339        id_list = []
340        for d in ds:
341            id_list.append(self.id_for_ds(name, d))
342        id_list.sort()
343        return '_'.join(map(str, id_list))
344
345    def id_for_nsec(self, name, rdtype, cls, nsec_set_info):
346        try:
347            nsec_set_info_list = self.nsec_ids[(name,rdtype,cls)]
348        except KeyError:
349            self.nsec_ids[(name,rdtype,cls)] = []
350            nsec_set_info_list = self.nsec_ids[(name,rdtype,cls)]
351
352        for nsec_set_info1, id in nsec_set_info_list:
353            if nsec_set_info == nsec_set_info1:
354                return id
355
356        id = self.next_nsec_id
357        self.nsec_ids[(name,rdtype,cls)].append((nsec_set_info, id))
358        self.next_nsec_id += 1
359        return id
360
361    def dnskey_node_str(self, id, name, algorithm, key_tag):
362        return 'DNSKEY-%s|%s|%d|%d' % (id, fmt.humanize_name(name), algorithm, key_tag)
363
364    def has_dnskey(self, id, name, algorithm, key_tag):
365        return self.G.has_node(self.dnskey_node_str(id, name, algorithm, key_tag))
366
367    def get_dnskey(self, id, name, algorithm, key_tag):
368        return self.G.get_node(self.dnskey_node_str(id, name, algorithm, key_tag))
369
370    def add_dnskey(self, name_obj, dnskey):
371        zone_obj = name_obj.zone
372        node_str = self.dnskey_node_str(self.id_for_dnskey(name_obj.name, dnskey.rdata), name_obj.name, dnskey.rdata.algorithm, dnskey.key_tag)
373
374        if not self.G.has_node(node_str):
375            rrset_info_with_errors = [x for x in dnskey.rrset_info if name_obj.rrset_errors[x]]
376            rrset_info_with_warnings = [x for x in dnskey.rrset_info if name_obj.rrset_warnings[x]]
377
378            img_str = ''
379            if dnskey.errors or rrset_info_with_errors:
380                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % ERROR_ICON
381            elif dnskey.warnings or rrset_info_with_warnings:
382                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % WARNING_ICON
383
384            if img_str:
385                label_str = '<<TABLE BORDER="0" CELLPADDING="0"><TR><TD></TD><TD VALIGN="bottom"><FONT POINT-SIZE="%d" FACE="%s">DNSKEY</FONT></TD><TD VALIGN="bottom">%s</TD></TR><TR><TD COLSPAN="3" VALIGN="top"><FONT POINT-SIZE="%d">alg=%d, id=%d<BR/>%d bits</FONT></TD></TR></TABLE>>' % \
386                        (12, 'Helvetica', img_str, 10, dnskey.rdata.algorithm, dnskey.key_tag, dnskey.key_len)
387            else:
388                label_str = '<<FONT POINT-SIZE="%d" FACE="%s">DNSKEY</FONT><BR/><FONT POINT-SIZE="%d">alg=%d, id=%d<BR/>%d bits</FONT>>' % \
389                        (12, 'Helvetica', 10, dnskey.rdata.algorithm, dnskey.key_tag, dnskey.key_len)
390
391            attr = {'style': 'filled', 'fillcolor': '#ffffff' }
392            if dnskey.rdata.flags & fmt.DNSKEY_FLAGS['SEP']:
393                attr['fillcolor'] = 'lightgray'
394            if dnskey.rdata.flags & fmt.DNSKEY_FLAGS['revoke']:
395                attr['penwidth'] = '4.0'
396
397            S, zone_node_str, zone_bottom_name, zone_top_name = self.get_zone(zone_obj.name)
398            S.add_node(node_str, id=node_str, shape='ellipse', label=label_str, **attr)
399            self.node_subgraph_name[node_str] = zone_top_name
400
401            consolidate_clients = name_obj.single_client()
402            dnskey_serialized = dnskey.serialize(consolidate_clients=consolidate_clients, html_format=True, map_ip_to_ns_name=name_obj.zone.get_ns_name_for_ip)
403
404            all_warnings = []
405            if rrset_info_with_warnings:
406                for rrset_info in rrset_info_with_warnings:
407                    for warning in name_obj.rrset_warnings[rrset_info]:
408                        servers_clients = warning.servers_clients
409                        warning = Errors.DomainNameAnalysisError.insert_into_list(warning.copy(), all_warnings, None, None, None)
410                        warning.servers_clients.update(servers_clients)
411                if 'warnings' not in dnskey_serialized:
412                    dnskey_serialized['warnings'] = []
413                dnskey_serialized['warnings'] += [w.serialize(consolidate_clients=consolidate_clients, html_format=True) for w in all_warnings]
414
415            all_errors = []
416            if rrset_info_with_errors:
417                for rrset_info in rrset_info_with_errors:
418                    for error in name_obj.rrset_errors[rrset_info]:
419                        servers_clients = error.servers_clients
420                        error = Errors.DomainNameAnalysisError.insert_into_list(error.copy(), all_errors, None, None, None)
421                        error.servers_clients.update(servers_clients)
422                if 'errors' not in dnskey_serialized:
423                    dnskey_serialized['errors'] = []
424                dnskey_serialized['errors'] += [e.serialize(consolidate_clients=consolidate_clients, html_format=True) for e in all_errors]
425
426            self.node_info[node_str] = [dnskey_serialized]
427
428        if node_str not in self.node_mapping:
429            self.node_mapping[node_str] = set()
430        self.node_mapping[node_str].add(dnskey)
431        self.node_reverse_mapping[dnskey] = node_str
432
433        return self.G.get_node(node_str)
434
435    def add_dnskey_non_existent(self, name, zone, algorithm, key_tag):
436        node_str = self.dnskey_node_str(0, name, algorithm, key_tag)
437
438        if not self.G.has_node(node_str):
439            label_str = '<<FONT POINT-SIZE="%d" FACE="%s">DNSKEY</FONT><BR/><FONT POINT-SIZE="%d">alg=%d, id=%d</FONT>>' % \
440                    (12, 'Helvetica', 10, algorithm, key_tag)
441
442            attr = {'style': 'filled,dashed', 'color': COLORS['insecure_non_existent'], 'fillcolor': '#ffffff' }
443
444            S, zone_node_str, zone_bottom_name, zone_top_name = self.get_zone(zone)
445            S.add_node(node_str, id=node_str, shape='ellipse', label=label_str, **attr)
446            self.node_subgraph_name[node_str] = zone_top_name
447
448            dnskey_meta = DNSKEYNonExistent(name, algorithm, key_tag)
449
450            self.node_info[node_str] = [dnskey_meta.serialize()]
451            self.node_mapping[node_str] = set()
452
453        return self.G.get_node(node_str)
454
455    def ds_node_str(self, id, name, ds, rdtype):
456        digest_types = [d.digest_type for d in ds]
457        digest_types.sort()
458        digest_str = '_'.join(map(str, digest_types))
459        return '%s-%s|%s|%d|%d|%s' % (dns.rdatatype.to_text(rdtype), id, fmt.humanize_name(name), ds[0].algorithm, ds[0].key_tag, digest_str)
460
461    def has_ds(self, id, name, ds, rdtype):
462        return self.G.has_node(self.ds_node_str(id, name, ds, rdtype))
463
464    def get_ds(self, id, name, ds, rdtype):
465        return self.G.get_node(self.ds_node_str(id, name, ds, rdtype))
466
467    def add_ds(self, name, ds_statuses, zone_obj, parent_obj):
468        ds_info = ds_statuses[0].ds_meta
469        ds = [d.ds for d in ds_statuses]
470        rdtype = ds_info.rrset.rdtype
471        node_str = self.ds_node_str(self.id_for_multiple_ds(name, ds), name, ds, rdtype)
472
473        if not self.G.has_node(node_str):
474            digest_types = [d.digest_type for d in ds]
475            digest_types.sort()
476            digest_str = ','.join(map(str, digest_types))
477            if len(digest_types) != 1:
478                plural = 's'
479            else:
480                plural = ''
481
482            img_str = ''
483            if [x for x in ds_statuses if [y for y in x.errors if isinstance(y, Errors.DSError)]] or zone_obj.rrset_errors[ds_info]:
484                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % ERROR_ICON
485            elif [x for x in ds_statuses if [y for y in x.warnings if isinstance(y, Errors.DSError)]] or zone_obj.rrset_warnings[ds_info]:
486                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % WARNING_ICON
487
488            attr = {'style': 'filled', 'fillcolor': '#ffffff' }
489            if img_str:
490                label_str = '<<TABLE BORDER="0" CELLPADDING="0"><TR><TD></TD><TD VALIGN="bottom"><FONT POINT-SIZE="%d" FACE="%s">%s</FONT></TD><TD VALIGN="bottom">%s</TD></TR><TR><TD COLSPAN="3" VALIGN="top"><FONT POINT-SIZE="%d">digest alg%s=%s</FONT></TD></TR></TABLE>>' % \
491                        (12, 'Helvetica', dns.rdatatype.to_text(rdtype), img_str, 10, plural, digest_str)
492            else:
493                label_str = '<<FONT POINT-SIZE="%d" FACE="%s">%s</FONT><BR/><FONT POINT-SIZE="%d">digest alg%s=%s</FONT>>' % \
494                        (12, 'Helvetica', dns.rdatatype.to_text(rdtype), 10, plural, digest_str)
495
496            S, parent_node_str, parent_bottom_name, parent_top_name = self.get_zone(parent_obj.name)
497            S.add_node(node_str, id=node_str, shape='ellipse', label=label_str, **attr)
498            self.node_subgraph_name[node_str] = parent_top_name
499
500            consolidate_clients = zone_obj.single_client()
501            ds_serialized = [d.serialize(consolidate_clients=consolidate_clients, html_format=True, map_ip_to_ns_name=zone_obj.get_ns_name_for_ip) for d in ds_statuses]
502
503            digest_algs = []
504            digests = []
505            for d in ds_serialized:
506                digest_algs.append(d['digest_type'])
507                digests.append(d['digest'])
508            digest_algs.sort()
509            digests.sort()
510            consolidated_ds_serialized = ds_serialized[0]
511            consolidated_ds_serialized['digest_type'] = digest_algs
512            consolidated_ds_serialized['digest'] = digests
513
514            if zone_obj.rrset_warnings[ds_info]:
515                if 'warnings' not in consolidated_ds_serialized:
516                    consolidated_ds_serialized['warnings'] = []
517                consolidated_ds_serialized['warnings'] += [w.serialize(consolidate_clients=consolidate_clients, html_format=True) for w in zone_obj.rrset_warnings[ds_info]]
518
519            if zone_obj.rrset_errors[ds_info]:
520                if 'errors' not in consolidated_ds_serialized:
521                    consolidated_ds_serialized['errors'] = []
522                consolidated_ds_serialized['errors'] += [e.serialize(consolidate_clients=consolidate_clients, html_format=True) for e in zone_obj.rrset_errors[ds_info]]
523
524            self.node_info[node_str] = [consolidated_ds_serialized]
525
526            T, zone_node_str, zone_bottom_name, zone_top_name = self.get_zone(zone_obj.name)
527
528            self.add_ds_map(name, node_str, ds_statuses, zone_obj, parent_obj)
529
530        if node_str not in self.node_mapping:
531            self.node_mapping[node_str] = set()
532        self.node_mapping[node_str].add(ds_info)
533        self.node_reverse_mapping[ds_info] = node_str
534
535        return self.G.get_node(node_str)
536
537    def add_ds_map(self, name, ds_node, ds_statuses, zone_obj, parent_obj):
538        rdtype = ds_statuses[0].ds_meta.rrset.rdtype
539        ds_status = ds_statuses[0]
540
541        if ds_status.validation_status == Status.DS_STATUS_VALID:
542            line_color = COLORS['secure']
543            line_style = 'solid'
544        elif ds_status.validation_status in (Status.DS_STATUS_INDETERMINATE_NO_DNSKEY, Status.DS_STATUS_INDETERMINATE_MATCH_PRE_REVOKE, Status.DS_STATUS_ALGORITHM_IGNORED):
545            line_color = COLORS['insecure_non_existent']
546            line_style = 'dashed'
547        elif ds_status.validation_status == Status.DS_STATUS_INDETERMINATE_UNKNOWN_ALGORITHM:
548            line_color = COLORS['indeterminate']
549            line_style = 'solid'
550        elif ds_status.validation_status == Status.DS_STATUS_INVALID_DIGEST:
551            line_color = COLORS['invalid']
552            line_style = 'solid'
553        elif ds_status.validation_status == Status.DS_STATUS_INVALID:
554            line_color = COLORS['invalid']
555            line_style = 'dashed'
556
557        if ds_status.dnskey is None:
558            dnskey_node = self.add_dnskey_non_existent(zone_obj.name, zone_obj.name, ds_status.ds.algorithm, ds_status.ds.key_tag)
559        else:
560            dnskey_node = self.get_dnskey(self.id_for_dnskey(zone_obj.name, ds_status.dnskey.rdata), zone_obj.name, ds_status.dnskey.rdata.algorithm, ds_status.dnskey.key_tag)
561
562        edge_id = 'digest-%s|%s|%s|%s' % (dnskey_node, ds_node, line_color.lstrip('#'), line_style)
563        self.G.add_edge(dnskey_node, ds_node, id=edge_id, color=line_color, style=line_style, dir='back')
564
565        self.node_info[edge_id] = [self.node_info[ds_node][0].copy()]
566        self.node_info[edge_id][0]['description'] = 'Digest for %s' % (self.node_info[edge_id][0]['description'])
567
568        self.node_mapping[edge_id] = set(ds_statuses)
569        for d in ds_statuses:
570            self.node_reverse_mapping[d] = edge_id
571
572    def zone_node_str(self, name):
573        return 'cluster_%s' % fmt.humanize_name(name)
574
575    def has_zone(self, name):
576        return self.G.get_subgraph(self.zone_node_str(name)) is not None
577
578    def get_zone(self, name):
579        node_str = self.zone_node_str(name)
580        top_name = node_str + '_top'
581        bottom_name = node_str + '_bottom'
582
583        S = self.G.get_subgraph(node_str)
584
585        return S, node_str, bottom_name, top_name
586
587    def add_zone(self, zone_obj):
588        node_str = self.zone_node_str(zone_obj.name)
589        top_name = node_str + '_top'
590        bottom_name = node_str + '_bottom'
591
592        S = self.G.get_subgraph(node_str)
593        if S is None:
594            img_str = ''
595            if zone_obj.zone_errors:
596                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % ERROR_ICON
597            elif zone_obj.zone_warnings:
598                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % WARNING_ICON
599
600            if zone_obj.analysis_end is not None:
601                label_str = '<<TABLE BORDER="0"><TR><TD ALIGN="LEFT"><FONT POINT-SIZE="%d">%s</FONT></TD><TD ALIGN="RIGHT">%s</TD></TR><TR><TD ALIGN="LEFT" COLSPAN="2"><FONT POINT-SIZE="%d">(%s)</FONT></TD></TR></TABLE>>' % \
602                        (12, zone_obj, img_str, 10, fmt.datetime_to_str(zone_obj.analysis_end))
603            else:
604                label_str = '<<TABLE BORDER="0"><TR><TD ALIGN="LEFT"><FONT POINT-SIZE="%d">%s</FONT></TD><TD ALIGN="RIGHT">%s</TD></TR></TABLE>>' % \
605                        (12, zone_obj, img_str)
606            S = self.G.add_subgraph(name=node_str, label=label_str, labeljust='l', penwidth='0.5', id=top_name)
607            S.add_node(top_name, shape='point', style='invis')
608            S.add_node(bottom_name, shape='point', style='invis')
609            self.node_subgraph_name[top_name] = top_name
610            self.node_subgraph_name[bottom_name] = top_name
611            self.node_reverse_mapping[zone_obj] = top_name
612
613            consolidate_clients = zone_obj.single_client()
614            zone_serialized = OrderedDict()
615            zone_serialized['description'] = '%s zone' % (zone_obj)
616            if zone_obj.zone_errors:
617                zone_serialized['errors'] = [e.serialize(consolidate_clients=consolidate_clients, html_format=True) for e in zone_obj.zone_errors]
618            if zone_obj.zone_warnings:
619                zone_serialized['warnings'] = [e.serialize(consolidate_clients=consolidate_clients, html_format=True) for e in zone_obj.zone_warnings]
620
621            self.node_info[top_name] = [zone_serialized]
622
623        return S, node_str, bottom_name, top_name
624
625    def add_rrsig(self, rrsig_status, name_obj, signer_obj, signed_node, port=None):
626        if signer_obj is not None:
627            zone_name = signer_obj.zone.name
628        else:
629            zone_name = name_obj.zone.name
630
631        if rrsig_status.dnskey is None:
632            dnskey_node = self.add_dnskey_non_existent(rrsig_status.rrsig.signer, zone_name, rrsig_status.rrsig.algorithm, rrsig_status.rrsig.key_tag)
633        else:
634            dnskey_node = self.get_dnskey(self.id_for_dnskey(rrsig_status.rrsig.signer, rrsig_status.dnskey.rdata), rrsig_status.rrsig.signer, rrsig_status.dnskey.rdata.algorithm, rrsig_status.dnskey.key_tag)
635
636        #XXX consider not adding icons if errors are apparent from color of line
637        edge_label = ''
638        if rrsig_status.errors:
639            edge_label = '<<TABLE BORDER="0"><TR><TD><IMG SCALE="TRUE" SRC="%s"/></TD></TR></TABLE>>' % ERROR_ICON
640        elif rrsig_status.warnings:
641            edge_label = '<<TABLE BORDER="0"><TR><TD><IMG SCALE="TRUE" SRC="%s"/></TD></TR></TABLE>>' % WARNING_ICON
642
643        if rrsig_status.validation_status == Status.RRSIG_STATUS_VALID:
644            line_color = COLORS['secure']
645            line_style = 'solid'
646        elif rrsig_status.validation_status in (Status.RRSIG_STATUS_INDETERMINATE_NO_DNSKEY, Status.RRSIG_STATUS_INDETERMINATE_MATCH_PRE_REVOKE, Status.RRSIG_STATUS_ALGORITHM_IGNORED):
647            line_color = COLORS['insecure_non_existent']
648            line_style = 'dashed'
649        elif rrsig_status.validation_status == Status.RRSIG_STATUS_INDETERMINATE_UNKNOWN_ALGORITHM:
650            line_color = COLORS['indeterminate']
651            line_style = 'solid'
652        elif rrsig_status.validation_status == Status.RRSIG_STATUS_EXPIRED:
653            line_color = COLORS['expired']
654            line_style = 'solid'
655        elif rrsig_status.validation_status == Status.RRSIG_STATUS_PREMATURE:
656            line_color = COLORS['expired']
657            line_style = 'solid'
658        elif rrsig_status.validation_status == Status.RRSIG_STATUS_INVALID_SIG:
659            line_color = COLORS['invalid']
660            line_style = 'solid'
661        elif rrsig_status.validation_status == Status.RRSIG_STATUS_INVALID:
662            line_color = COLORS['invalid']
663            line_style = 'dashed'
664
665        attrs = {}
666        edge_id = 'RRSIG-%s|%s|%s|%s' % (signed_node.replace('*', '_'), dnskey_node, line_color.lstrip('#'), line_style)
667        edge_key = '%s-%s' % (line_color, line_style)
668        if port is not None:
669            attrs['tailport'] = port
670            edge_id += '|%s' % port.replace('*', '_')
671            edge_key += '|%s' % port
672
673        # if this DNSKEY is signing data in a zone above itself (e.g., DS
674        # records), then remove constraint from the edge
675        signed_node_zone = self.node_subgraph_name[signed_node][8:-4]
676        dnskey_node_zone = self.node_subgraph_name[dnskey_node][8:-4]
677        if not signed_node_zone.endswith(dnskey_node_zone):
678            attrs['constraint'] = 'false'
679
680        if (signed_node, dnskey_node, edge_key) not in self._edge_keys:
681            self._edge_keys.add((signed_node, dnskey_node, edge_key))
682            self.G.add_edge(signed_node, dnskey_node, label=edge_label, id=edge_id, color=line_color, style=line_style, dir='back', **attrs)
683
684        consolidate_clients = name_obj.single_client()
685        rrsig_serialized = rrsig_status.serialize(consolidate_clients=consolidate_clients, html_format=True, map_ip_to_ns_name=name_obj.zone.get_ns_name_for_ip)
686
687        if edge_id not in self.node_info:
688            self.node_info[edge_id] = []
689            self.node_mapping[edge_id] = set()
690        self.node_info[edge_id].append(rrsig_serialized)
691        self.node_mapping[edge_id].add(rrsig_status)
692        self.node_reverse_mapping[rrsig_status] = edge_id
693
694    def id_for_rrset(self, rrset_info):
695        name, rdtype = rrset_info.rrset.name, rrset_info.rrset.rdtype
696        try:
697            rrset_info_list = self.rrset_ids[(name,rdtype)]
698        except KeyError:
699            self.rrset_ids[(name,rdtype)] = []
700            rrset_info_list = self.rrset_ids[(name,rdtype)]
701
702        for rrset_info1, id in rrset_info_list:
703            if rrset_info == rrset_info1:
704                return id
705
706        id = self.next_rrset_id
707        self.rrset_ids[(name,rdtype)].append((rrset_info, id))
708        self.next_rrset_id += 1
709        return id
710
711    def rrset_node_str(self, name, rdtype, id):
712        return 'RRset-%d|%s|%s' % (id, fmt.humanize_name(name), dns.rdatatype.to_text(rdtype))
713
714    def has_rrset(self, name, rdtype, id):
715        return self.G.has_node(self.rrset_node_str(name, rdtype, id))
716
717    def get_rrset(self, name, rdtype, id):
718        return self.G.get_node(self.rrset_node_str(name, rdtype, id))
719
720    def add_rrset(self, rrset_info, wildcard_name, name_obj, zone_obj):
721        name = wildcard_name or rrset_info.rrset.name
722        node_str = self.rrset_node_str(name, rrset_info.rrset.rdtype, self.id_for_rrset(rrset_info))
723        node_id = node_str.replace('*', '_')
724
725        if not self.G.has_node(node_str):
726            img_str = ''
727            if name_obj.rrset_errors[rrset_info]:
728                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % ERROR_ICON
729            elif name_obj.rrset_warnings[rrset_info]:
730                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % WARNING_ICON
731
732            if img_str:
733                node_label = '<<TABLE BORDER="0" CELLPADDING="0"><TR><TD><FONT POINT-SIZE="%d" FACE="%s">%s/%s</FONT></TD></TR><TR><TD>%s</TD></TR></TABLE>>' % \
734                        (12, 'Helvetica', fmt.humanize_name(name, True), dns.rdatatype.to_text(rrset_info.rrset.rdtype), img_str)
735            else:
736                node_label = '<<FONT POINT-SIZE="%d" FACE="%s">%s/%s</FONT>>' % \
737                        (12, 'Helvetica', fmt.humanize_name(name, True), dns.rdatatype.to_text(rrset_info.rrset.rdtype))
738
739            attr = {}
740            attr['shape'] = 'rectangle'
741            attr['style'] = 'rounded,filled'
742            attr['fillcolor'] = '#ffffff'
743
744            S, zone_node_str, zone_bottom_name, zone_top_name = self.get_zone(zone_obj.name)
745            S.add_node(node_str, id=node_id, label=node_label, fontsize='10', **attr)
746            self.node_subgraph_name[node_str] = zone_top_name
747
748            consolidate_clients = name_obj.single_client()
749            rrset_serialized = rrset_info.serialize(consolidate_clients=consolidate_clients, html_format=True, map_ip_to_ns_name=name_obj.zone.get_ns_name_for_ip)
750
751            if name_obj.rrset_warnings[rrset_info]:
752                if 'warnings' not in rrset_serialized:
753                    rrset_serialized['warnings'] = []
754                rrset_serialized['warnings'] += [w.serialize(consolidate_clients=consolidate_clients, html_format=True) for w in name_obj.rrset_warnings[rrset_info]]
755
756            if name_obj.rrset_errors[rrset_info]:
757                if 'errors' not in rrset_serialized:
758                    rrset_serialized['errors'] = []
759                rrset_serialized['errors'] += [w.serialize(consolidate_clients=consolidate_clients, html_format=True) for w in name_obj.rrset_errors[rrset_info]]
760
761            self.node_info[node_id] = [rrset_serialized]
762            self.G.add_edge(zone_bottom_name, node_str, style='invis')
763
764        if node_str not in self.node_mapping:
765            self.node_mapping[node_str] = set()
766        self.node_mapping[node_str].add(rrset_info)
767        self.node_reverse_mapping[rrset_info] = node_str
768
769        return self.G.get_node(node_str)
770
771    def add_rrset_non_existent(self, name_obj, zone_obj, neg_response_info, nxdomain, wildcard):
772        if nxdomain:
773            node_str = self.rrset_node_str(neg_response_info.qname, neg_response_info.rdtype, 0)
774        else:
775            node_str = self.rrset_node_str(neg_response_info.qname, neg_response_info.rdtype, 1)
776        node_id = node_str.replace('*', '_')
777
778        if not self.G.has_node(node_str):
779            if wildcard:
780                warnings_list = errors_list = []
781            else:
782                if nxdomain:
783                    warnings_list = name_obj.nxdomain_warnings[neg_response_info]
784                    errors_list = name_obj.nxdomain_errors[neg_response_info]
785                else:
786                    warnings_list = name_obj.nodata_warnings[neg_response_info]
787                    errors_list = name_obj.nodata_errors[neg_response_info]
788
789            if nxdomain:
790                rdtype_str = ''
791            else:
792                rdtype_str = '/%s' % dns.rdatatype.to_text(neg_response_info.rdtype)
793
794            img_str = ''
795            if errors_list:
796                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % ERROR_ICON
797            elif warnings_list:
798                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % WARNING_ICON
799
800            if img_str:
801                node_label = '<<TABLE BORDER="0" CELLPADDING="0"><TR><TD><FONT POINT-SIZE="%d" FACE="%s">%s%s</FONT></TD></TR><TR><TD>%s</TD></TR></TABLE>>' % \
802                        (12, 'Helvetica', fmt.humanize_name(neg_response_info.qname, True), rdtype_str, img_str)
803            else:
804                node_label = '<<FONT POINT-SIZE="%d" FACE="%s">%s%s</FONT>>' % \
805                        (12, 'Helvetica', fmt.humanize_name(neg_response_info.qname, True), rdtype_str)
806
807            attr = {}
808            attr['shape'] = 'rectangle'
809            attr['style'] = 'rounded,filled,dashed'
810            if nxdomain:
811                attr['style'] += ',diagonals'
812            attr['fillcolor'] = '#ffffff'
813
814            S, zone_node_str, zone_bottom_name, zone_top_name = self.get_zone(zone_obj.name)
815            S.add_node(node_str, id=node_id, label=node_label, fontsize='10', **attr)
816            self.node_subgraph_name[node_str] = zone_top_name
817
818            rrset_info = RRsetNonExistent(neg_response_info.qname, neg_response_info.rdtype, nxdomain, neg_response_info.servers_clients)
819
820            consolidate_clients = name_obj.single_client()
821            rrset_serialized = rrset_info.serialize(consolidate_clients=consolidate_clients, html_format=True, map_ip_to_ns_name=name_obj.zone.get_ns_name_for_ip)
822
823            if warnings_list:
824                if 'warnings' not in rrset_serialized:
825                    rrset_serialized['warnings'] = []
826                rrset_serialized['warnings'] += [w.serialize(consolidate_clients=consolidate_clients, html_format=True) for w in warnings_list]
827
828            if errors_list:
829                if 'errors' not in rrset_serialized:
830                    rrset_serialized['errors'] = []
831                rrset_serialized['errors'] += [w.serialize(consolidate_clients=consolidate_clients, html_format=True) for w in errors_list]
832
833            self.node_info[node_id] = [rrset_serialized]
834
835            self.G.add_edge(zone_bottom_name, node_str, style='invis')
836
837        if node_str not in self.node_mapping:
838            self.node_mapping[node_str] = set()
839        self.node_mapping[node_str].add(neg_response_info)
840        self.node_reverse_mapping[neg_response_info] = node_str
841
842        return self.G.get_node(node_str)
843
844    def _add_errors(self, name_obj, zone_obj, name, rdtype, errors_list, code, icon, category, status, description):
845        if not errors_list:
846            return None
847
848        node_str = self.rrset_node_str(name, rdtype, code)
849
850        img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % icon
851
852        node_label = '<<TABLE BORDER="0" CELLPADDING="0"><TR><TD>%s</TD></TR><TR><TD><FONT POINT-SIZE="%d" FACE="%s" COLOR="%s"><I>%s/%s</I></FONT></TD></TR></TABLE>>' % \
853                (img_str, 10, 'Helvetica', '#b0b0b0', fmt.humanize_name(name, True), dns.rdatatype.to_text(rdtype), )
854
855        attr = {}
856        attr['shape'] = 'none'
857        attr['margin'] = '0'
858
859        node_id = node_str.replace('*', '_')
860        S, zone_node_str, zone_bottom_name, zone_top_name = self.get_zone(zone_obj.name)
861        S.add_node(node_str, id=node_id, label=node_label, fontsize='10', **attr)
862        self.node_subgraph_name[node_str] = zone_top_name
863
864        consolidate_clients = name_obj.single_client()
865
866        errors_serialized = OrderedDict()
867
868        errors_serialized['description'] = '%s %s/%s' % (description, fmt.humanize_name(name), dns.rdatatype.to_text(rdtype))
869        errors_serialized[category] = [e.serialize(consolidate_clients=consolidate_clients, html_format=True) for e in errors_list]
870        errors_serialized['status'] = status
871
872        self.node_info[node_id] = [errors_serialized]
873        self.G.add_edge(zone_bottom_name, node_str, style='invis')
874
875        # no need to map errors
876        self.node_mapping[node_str] = set()
877
878        return self.G.get_node(node_str)
879
880    def add_errors(self, name_obj, zone_obj, name, rdtype, errors_list):
881        return self._add_errors(name_obj, zone_obj, name, rdtype, errors_list, 2, ERROR_ICON, 'errors', 'ERROR', 'Response errors for')
882
883    def add_warnings(self, name_obj, zone_obj, name, rdtype, warnings_list):
884        return self._add_errors(name_obj, zone_obj, name, rdtype, warnings_list, 3, WARNING_ICON, 'warnings', 'WARNING', 'Response warnings for')
885
886    def add_dname(self, dname_status, name_obj, zone_obj):
887        dname_rrset_info = dname_status.synthesized_cname.dname_info
888        dname_node = self.add_rrset(dname_rrset_info, None, name_obj, zone_obj)
889
890        if dname_status.validation_status == Status.DNAME_STATUS_VALID:
891            line_color = COLORS['secure']
892            line_style = 'solid'
893        elif dname_status.validation_status == Status.DNAME_STATUS_INDETERMINATE:
894            line_color = COLORS['indeterminate']
895            line_style = 'solid'
896        elif dname_status.validation_status == Status.DNAME_STATUS_INVALID:
897            line_color = COLORS['invalid']
898            line_style = 'solid'
899
900        if dname_status.included_cname is None:
901            cname_node = self.add_rrset_non_existent(name_obj, zone_obj, Response.NegativeResponseInfo(dname_status.synthesized_cname.rrset.name, dns.rdatatype.CNAME, False), False, False)
902        else:
903            cname_node = self.add_rrset(dname_status.included_cname, None, name_obj, zone_obj)
904
905        edge_id = 'dname-%s|%s|%s|%s' % (cname_node, dname_node, line_color.lstrip('#'), line_style)
906        edge_key = '%s-%s' % (line_color, line_style)
907        if (cname_node, dname_node, edge_key) not in self._edge_keys:
908            self._edge_keys.add((cname_node, dname_node, edge_key))
909
910            edge_label = ''
911            if dname_status.errors:
912                edge_label = '<<TABLE BORDER="0"><TR><TD><IMG SCALE="TRUE" SRC="%s"/></TD></TR></TABLE>>' % ERROR_ICON
913            elif dname_status.warnings:
914                edge_label = '<<TABLE BORDER="0"><TR><TD><IMG SCALE="TRUE" SRC="%s"/></TD></TR></TABLE>>' % WARNING_ICON
915
916            self.G.add_edge(cname_node, dname_node, label=edge_label, id=edge_id, color=line_color, style=line_style, dir='back')
917            self.node_info[edge_id] = [dname_status.serialize(html_format=True, map_ip_to_ns_name=name_obj.zone.get_ns_name_for_ip)]
918
919        if edge_id not in self.node_mapping:
920            self.node_mapping[edge_id] = set()
921        self.node_mapping[edge_id].add(dname_status)
922        self.node_reverse_mapping[dname_status] = edge_id
923
924        self.add_rrsigs(name_obj, zone_obj, dname_rrset_info, dname_node)
925
926        return cname_node
927
928    def nsec_node_str(self, nsec_rdtype, id, name, rdtype):
929        return '%s-%d|%s|%s' % (dns.rdatatype.to_text(nsec_rdtype), id, fmt.humanize_name(name), dns.rdatatype.to_text(rdtype))
930
931    def has_nsec(self, nsec_rdtype, id, name, rdtype):
932        return self.G.has_node(self.nsec_node_str(nsec_rdtype, id, name, rdtype))
933
934    def get_nsec(self, nsec_rdtype, id, name, rdtype):
935        return self.G.get_node(self.nsec_node_str(nsec_rdtype, id, name, rdtype))
936
937    def add_nsec(self, nsec_status, name, rdtype, name_obj, zone_obj, covered_node):
938        if nsec_status.nsec_set_info.use_nsec3:
939            nsec_rdtype = dns.rdatatype.NSEC3
940        else:
941            nsec_rdtype = dns.rdatatype.NSEC
942        node_str = self.nsec_node_str(nsec_rdtype, self.id_for_nsec(name, rdtype, nsec_status.__class__, nsec_status.nsec_set_info), name, rdtype)
943        node_id = node_str.replace('*', '_')
944        edge_id = '%sC-%s|%s' % (dns.rdatatype.to_text(nsec_rdtype), covered_node.replace('*', '_'), node_str)
945
946        if not self.G.has_node(node_str):
947            rrset_info_with_errors = [x for x in nsec_status.nsec_set_info.rrsets.values() if name_obj.rrset_errors[x]]
948            rrset_info_with_warnings = [x for x in nsec_status.nsec_set_info.rrsets.values() if name_obj.rrset_warnings[x]]
949
950            img_str = ''
951            if rrset_info_with_errors:
952                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % ERROR_ICON
953            elif rrset_info_with_warnings:
954                img_str = '<IMG SCALE="TRUE" SRC="%s"/>' % WARNING_ICON
955
956            # if it is NXDOMAIN, not type DS
957            if isinstance(nsec_status, (Status.NSEC3StatusNXDOMAIN, Status.NSEC3StatusNODATA)) and nsec_status.opt_out:
958                bgcolor = 'lightgray'
959            else:
960                bgcolor = '#ffffff'
961
962            #XXX it looks better when cellspacing is 0, but we can't do that
963            # when there is an icon in use because of the way the graphviz
964            # library draws it.
965            if img_str:
966                cellspacing = 0
967            else:
968                cellspacing = -2
969
970            self.nsec_rr_status[node_str] = {}
971            label_str = '<<TABLE BORDER="0" CELLSPACING="%d" CELLPADDING="0" BGCOLOR="%s"><TR>' % (cellspacing, bgcolor)
972            for nsec_name in nsec_status.nsec_set_info.rrsets:
973                nsec_name = lb2s(nsec_name.canonicalize().to_text()).replace(r'"', r'\"')
974                self.nsec_rr_status[node_str][nsec_name] = ''
975                label_str += '<TD PORT="%s" BORDER="2"><FONT POINT-SIZE="%d"> </FONT></TD>' % (nsec_name, 6)
976            label_str += '</TR><TR><TD COLSPAN="%d" BORDER="2" CELLPADDING="3">' % len(nsec_status.nsec_set_info.rrsets)
977            if img_str:
978                label_str += '<TABLE BORDER="0"><TR><TD><FONT POINT-SIZE="%d" FACE="%s">%s</FONT></TD><TD>%s</TD></TR></TABLE>' % \
979                        (12, 'Helvetica', dns.rdatatype.to_text(nsec_rdtype), img_str)
980            else:
981                label_str += '<FONT POINT-SIZE="%d" FACE="%s">%s</FONT>' % \
982                        (12, 'Helvetica', dns.rdatatype.to_text(nsec_rdtype))
983            label_str += '</TD></TR></TABLE>>'
984
985            S, zone_node_str, zone_bottom_name, zone_top_name = self.get_zone(zone_obj.name)
986            S.add_node(node_str, id=node_id, label=label_str, shape='none')
987            self.node_subgraph_name[node_str] = zone_top_name
988
989            consolidate_clients = name_obj.single_client()
990
991            nsec_serialized = nsec_status.serialize(consolidate_clients=consolidate_clients, html_format=True, map_ip_to_ns_name=name_obj.zone.get_ns_name_for_ip)
992
993            nsec_serialized_edge = nsec_serialized.copy()
994            nsec_serialized_edge['description'] = 'Non-existence proof provided by %s' % (nsec_serialized['description'])
995
996            all_warnings = []
997            if rrset_info_with_warnings:
998                for rrset_info in rrset_info_with_warnings:
999                    for warning in name_obj.rrset_warnings[rrset_info]:
1000                        servers_clients = warning.servers_clients
1001                        warning = Errors.DomainNameAnalysisError.insert_into_list(warning.copy(), all_warnings, None, None, None)
1002                        warning.servers_clients.update(servers_clients)
1003                if 'warnings' not in nsec_serialized:
1004                    nsec_serialized['warnings'] = []
1005                nsec_serialized['warnings'] += [w.serialize(consolidate_clients=consolidate_clients, html_format=True) for w in all_warnings]
1006
1007            all_errors = []
1008            if rrset_info_with_errors:
1009                for rrset_info in rrset_info_with_errors:
1010                    for error in name_obj.rrset_errors[rrset_info]:
1011                        servers_clients = error.servers_clients
1012                        error = Errors.DomainNameAnalysisError.insert_into_list(error.copy(), all_errors, None, None, None)
1013                        error.servers_clients.update(servers_clients)
1014                if 'errors' not in nsec_serialized:
1015                    nsec_serialized['errors'] = []
1016                nsec_serialized['errors'] += [e.serialize(consolidate_clients=consolidate_clients, html_format=True) for e in all_errors]
1017
1018            self.node_info[node_id] = [nsec_serialized]
1019
1020            nsec_node = self.G.get_node(node_str)
1021
1022            if nsec_status.validation_status == Status.NSEC_STATUS_VALID:
1023                line_color = COLORS['secure']
1024                line_style = 'solid'
1025            elif nsec_status.validation_status == Status.NSEC_STATUS_INDETERMINATE:
1026                line_color = COLORS['indeterminate']
1027                line_style = 'solid'
1028            elif nsec_status.validation_status == Status.NSEC_STATUS_INVALID:
1029                line_color = COLORS['bogus']
1030                line_style = 'solid'
1031
1032            edge_label = ''
1033            self.G.add_edge(covered_node, nsec_node, label=edge_label, id=edge_id, color=line_color, style=line_style, dir='back')
1034
1035            self.node_info[edge_id] = [nsec_serialized_edge]
1036
1037        else:
1038            nsec_node = self.G.get_node(node_str)
1039
1040        if node_str not in self.node_mapping:
1041            self.node_mapping[node_str] = set()
1042        self.node_mapping[node_str].add(nsec_status.nsec_set_info)
1043        self.node_reverse_mapping[nsec_status.nsec_set_info] = node_str
1044
1045        if edge_id not in self.node_mapping:
1046            self.node_mapping[edge_id] = set()
1047        self.node_mapping[edge_id].add(nsec_status)
1048        self.node_reverse_mapping[nsec_status] = edge_id
1049
1050        return nsec_node
1051
1052    def add_wildcard(self, name_obj, zone_obj, rrset_info, nsec_status, wildcard_name):
1053        wildcard_node = self.add_rrset(rrset_info, wildcard_name, name_obj, zone_obj)
1054        self.add_rrsigs(name_obj, zone_obj, rrset_info, wildcard_node)
1055        nxdomain_node = self.add_rrset_non_existent(name_obj, zone_obj, rrset_info.wildcard_info[wildcard_name], True, True)
1056
1057        if nsec_status is not None:
1058            nsec_node = self.add_nsec(nsec_status, rrset_info.rrset.name, rrset_info.rrset.rdtype, name_obj, zone_obj, nxdomain_node)
1059            for nsec_name, rrset_info in nsec_status.nsec_set_info.rrsets.items():
1060                nsec_cell = lb2s(nsec_name.canonicalize().to_text())
1061                self.add_rrsigs(name_obj, zone_obj, rrset_info, nsec_node, port=nsec_cell)
1062
1063        return wildcard_node
1064
1065        #XXX consider adding this node (using, e.g., clustering)
1066        #rrset_node = self.add_rrset(rrset_info, None, zone_obj, zone_obj)
1067        #self.G.add_edge(rrset_node, nxdomain_node, color=COLORS['secure'], style='invis', dir='back')
1068        #self.G.add_edge(rrset_node, wildcard_node, color=COLORS['secure'], style='invis', dir='back')
1069        #return rrset_node
1070
1071    def add_alias(self, alias, target):
1072        if not [x for x in self.G.out_edges(target) if x[1] == alias and x.attr['color'] == 'black']:
1073            alias_zone = self.node_subgraph_name[alias][8:-4]
1074            target_zone = self.node_subgraph_name[target][8:-4]
1075            if alias_zone.endswith(target_zone) and alias_zone != target_zone:
1076                self.G.add_edge(target, alias, color='black', dir='back', constraint='false')
1077            else:
1078                self.G.add_edge(target, alias, color='black', dir='back')
1079
1080    def add_rrsigs(self, name_obj, zone_obj, rrset_info, signed_node, port=None):
1081        for rrsig in name_obj.rrsig_status[rrset_info]:
1082            signer_obj = name_obj.get_name(rrsig.signer)
1083            if rrsig.signer != zone_obj.name and signer_obj is not None:
1084                self.graph_zone_auth(signer_obj, False)
1085            for dnskey in name_obj.rrsig_status[rrset_info][rrsig]:
1086                rrsig_status = name_obj.rrsig_status[rrset_info][rrsig][dnskey]
1087                self.add_rrsig(rrsig_status, name_obj, signer_obj, signed_node, port=port)
1088
1089    def graph_rrset_auth(self, name_obj, name, rdtype, trace=None):
1090        if (name, rdtype) not in self.processed_rrsets:
1091            self.processed_rrsets[(name, rdtype)] = []
1092
1093        #XXX there are reasons for this (e.g., NXDOMAIN, after which no further
1094        # queries are made), but it would be good to have a sanity check, so
1095        # we don't simply produce an incomplete graph.  (In the case above, perhaps
1096        # point to the NXDOMAIN produced by another query.)
1097        if (name, rdtype) not in name_obj.queries:
1098            return []
1099
1100        zone_obj = name_obj.zone
1101        if zone_obj is not None:
1102            self.graph_zone_auth(zone_obj, False)
1103        else:
1104            # in recursive analysis, if we don't contact any servers that are
1105            # valid and responsive, then we get a zone_obj that is None
1106            # (because we couldn't detect any NS records in the ancestry)
1107            zone_obj = name_obj
1108            self.add_zone(zone_obj)
1109
1110        if name_obj.nxdomain_ancestor is not None:
1111            self.graph_rrset_auth(name_obj.nxdomain_ancestor, name_obj.nxdomain_ancestor.name, name_obj.nxdomain_ancestor.referral_rdtype)
1112
1113        # if this is for DNSKEY or DS of a zone, then return, as we have
1114        # already take care of these types in graph_zone_auth()
1115        if name_obj.is_zone() and rdtype in (dns.rdatatype.DNSKEY, dns.rdatatype.DS):
1116            return []
1117
1118        # trace is used just for CNAME chains
1119        if trace is None:
1120            trace = [name]
1121
1122        cname_nodes = []
1123        # if this name is an alias, then graph its target, i.e., the canonical
1124        # name, unless this is a recursive analysis.
1125        if name_obj.analysis_type != ANALYSIS_TYPE_RECURSIVE:
1126            if name in name_obj.cname_targets:
1127                for target, cname_obj in name_obj.cname_targets[name].items():
1128                    if cname_obj is not None:
1129                        if target not in trace:
1130                            cname_nodes.extend(self.graph_rrset_auth(cname_obj, target, rdtype, trace + [target]))
1131
1132        query = name_obj.queries[(name, rdtype)]
1133        node_to_cname_mapping = set()
1134        for rrset_info in query.answer_info:
1135
1136            # only do qname, unless analysis type is recursive
1137            if not (rrset_info.rrset.name == name or name_obj.analysis_type == ANALYSIS_TYPE_RECURSIVE):
1138                continue
1139
1140            my_name = rrset_info.rrset.name
1141            my_nodes = []
1142            if (my_name, rdtype) not in self.processed_rrsets:
1143                self.processed_rrsets[(my_name, rdtype)] = []
1144
1145            my_name_obj = name_obj.get_name(my_name)
1146            my_zone_obj = my_name_obj.zone
1147            if my_zone_obj is not None:
1148                self.graph_zone_auth(my_zone_obj, False)
1149            else:
1150                my_zone_obj = my_name_obj
1151                self.add_zone(my_zone_obj)
1152
1153            #XXX can we combine multiple DNAMEs into one?
1154            #XXX can we combine multiple NSEC(3) into a cluster?
1155            #XXX can we combine wildcard components into a cluster?
1156            if rrset_info in name_obj.dname_status:
1157                for dname_status in name_obj.dname_status[rrset_info]:
1158                    my_nodes.append(self.add_dname(dname_status, name_obj, my_zone_obj))
1159            elif rrset_info.wildcard_info:
1160                for wildcard_name in rrset_info.wildcard_info:
1161                    if name_obj.wildcard_status[rrset_info.wildcard_info[wildcard_name]]:
1162                        for nsec_status in name_obj.wildcard_status[rrset_info.wildcard_info[wildcard_name]]:
1163                            my_nodes.append(self.add_wildcard(name_obj, my_zone_obj, rrset_info, nsec_status, wildcard_name))
1164                    else:
1165                        my_nodes.append(self.add_wildcard(name_obj, my_zone_obj, rrset_info, None, wildcard_name))
1166            else:
1167                rrset_node = self.add_rrset(rrset_info, None, name_obj, my_zone_obj)
1168                self.add_rrsigs(name_obj, my_zone_obj, rrset_info, rrset_node)
1169                my_nodes.append(rrset_node)
1170
1171            # if this is a CNAME record, create a node-to-target mapping
1172            if rrset_info.rrset.rdtype == dns.rdatatype.CNAME:
1173                for my_node in my_nodes:
1174                    node_to_cname_mapping.add((my_node, rrset_info.rrset[0].target))
1175
1176            self.processed_rrsets[(my_name, rdtype)] += my_nodes
1177
1178        for neg_response_info in query.nxdomain_info:
1179            # make sure this query was made to a server designated as
1180            # authoritative
1181            if not set([s for (s,c) in neg_response_info.servers_clients]).intersection(name_obj.zone.get_auth_or_designated_servers()):
1182                continue
1183
1184            # only do qname, unless analysis type is recursive
1185            if not (neg_response_info.qname == name or name_obj.analysis_type == ANALYSIS_TYPE_RECURSIVE):
1186                continue
1187
1188            if (neg_response_info.qname, neg_response_info.rdtype) not in self.processed_rrsets:
1189                self.processed_rrsets[(neg_response_info.qname, neg_response_info.rdtype)] = []
1190
1191            my_name_obj = name_obj.get_name(neg_response_info.qname)
1192            my_zone_obj = my_name_obj.zone
1193            if my_zone_obj is not None:
1194                self.graph_zone_auth(my_zone_obj, False)
1195            else:
1196                my_zone_obj = my_name_obj
1197                self.add_zone(my_zone_obj)
1198
1199            nxdomain_node = self.add_rrset_non_existent(name_obj, my_zone_obj, neg_response_info, True, False)
1200            self.processed_rrsets[(neg_response_info.qname, neg_response_info.rdtype)].append(nxdomain_node)
1201            for nsec_status in name_obj.nxdomain_status[neg_response_info]:
1202                nsec_node = self.add_nsec(nsec_status, name, rdtype, name_obj, my_zone_obj, nxdomain_node)
1203                for nsec_name, rrset_info in nsec_status.nsec_set_info.rrsets.items():
1204                    nsec_cell = lb2s(nsec_name.canonicalize().to_text())
1205                    self.add_rrsigs(name_obj, my_zone_obj, rrset_info, nsec_node, port=nsec_cell)
1206
1207            for soa_rrset_info in neg_response_info.soa_rrset_info:
1208                # If no servers match the authoritative servers, then put this in the parent zone
1209                if not set([s for (s,c) in soa_rrset_info.servers_clients]).intersection(my_zone_obj.get_auth_or_designated_servers()) and my_zone_obj.parent is not None:
1210                    z_obj = my_zone_obj.parent
1211                else:
1212                    z_obj = my_zone_obj
1213                soa_rrset_node = self.add_rrset(soa_rrset_info, None, name_obj, z_obj)
1214                self.add_rrsigs(name_obj, my_zone_obj, soa_rrset_info, soa_rrset_node)
1215
1216        for neg_response_info in query.nodata_info:
1217            # only do qname, unless analysis type is recursive
1218            if not (neg_response_info.qname == name or name_obj.analysis_type == ANALYSIS_TYPE_RECURSIVE):
1219                continue
1220
1221            if (neg_response_info.qname, neg_response_info.rdtype) not in self.processed_rrsets:
1222                self.processed_rrsets[(neg_response_info.qname, neg_response_info.rdtype)] = []
1223
1224            my_name_obj = name_obj.get_name(neg_response_info.qname)
1225            my_zone_obj = my_name_obj.zone
1226            if my_zone_obj is not None:
1227                self.graph_zone_auth(my_zone_obj, False)
1228            else:
1229                my_zone_obj = my_name_obj
1230                self.add_zone(my_zone_obj)
1231
1232            nodata_node = self.add_rrset_non_existent(name_obj, my_zone_obj, neg_response_info, False, False)
1233            self.processed_rrsets[(neg_response_info.qname, neg_response_info.rdtype)].append(nodata_node)
1234            for nsec_status in name_obj.nodata_status[neg_response_info]:
1235                nsec_node = self.add_nsec(nsec_status, name, rdtype, name_obj, my_zone_obj, nodata_node)
1236                for nsec_name, rrset_info in nsec_status.nsec_set_info.rrsets.items():
1237                    nsec_cell = lb2s(nsec_name.canonicalize().to_text())
1238                    self.add_rrsigs(name_obj, my_zone_obj, rrset_info, nsec_node, port=nsec_cell)
1239
1240            for soa_rrset_info in neg_response_info.soa_rrset_info:
1241                soa_rrset_node = self.add_rrset(soa_rrset_info, None, name_obj, my_zone_obj)
1242                self.add_rrsigs(name_obj, my_zone_obj, soa_rrset_info, soa_rrset_node)
1243
1244        error_node = self.add_errors(name_obj, zone_obj, name, rdtype, name_obj.response_errors[query])
1245        if error_node is not None:
1246            if (name, rdtype) not in self.processed_rrsets:
1247                self.processed_rrsets[(name, rdtype)] = []
1248            self.processed_rrsets[(name, rdtype)].append(error_node)
1249
1250        warning_node = self.add_warnings(name_obj, zone_obj, name, rdtype, name_obj.response_warnings[query])
1251        if warning_node is not None:
1252            if (name, rdtype) not in self.processed_rrsets:
1253                self.processed_rrsets[(name, rdtype)] = []
1254            self.processed_rrsets[(name, rdtype)].append(warning_node)
1255
1256        for alias_node, target in node_to_cname_mapping:
1257            # if this is a recursive analysis, then we've already graphed the
1258            # node, above, so we graph its hierarchy and then retrieve it from
1259            # self.processed_rrsets
1260            if name_obj.analysis_type == ANALYSIS_TYPE_RECURSIVE:
1261                # if we didn't get the cname RRset in same response, then
1262                # processed_rrsets won't be populated
1263                try:
1264                    cname_nodes = self.processed_rrsets[(target, rdtype)]
1265                except KeyError:
1266                    cname_nodes = []
1267
1268            for cname_node in cname_nodes:
1269                self.add_alias(alias_node, cname_node)
1270
1271        return self.processed_rrsets[(name, rdtype)]
1272
1273    def graph_zone_auth(self, name_obj, is_dlv):
1274        if (name_obj.name, -1) in self.processed_rrsets:
1275            return
1276        self.processed_rrsets[(name_obj.name, -1)] = True
1277
1278        zone_obj = name_obj.zone
1279        S, zone_graph_name, zone_bottom, zone_top = self.add_zone(zone_obj)
1280
1281        if zone_obj.stub:
1282            return
1283
1284        # indicate that this zone is not a stub
1285        self.subgraph_not_stub.add(zone_top)
1286
1287        #######################################
1288        # DNSKEY roles, based on what they sign
1289        #######################################
1290        all_dnskeys = name_obj.get_dnskeys()
1291
1292        # Add DNSKEY nodes to graph
1293        for dnskey in name_obj.get_dnskeys():
1294            self.add_dnskey(name_obj, dnskey)
1295
1296        for signed_keys, rrset_info in name_obj.get_dnskey_sets():
1297            for rrsig in name_obj.rrsig_status[rrset_info]:
1298                signer_obj = name_obj.get_name(rrsig.signer)
1299                if rrsig.signer != name_obj.name and not is_dlv:
1300                    self.graph_zone_auth(signer_obj, False)
1301                for dnskey in name_obj.rrsig_status[rrset_info][rrsig]:
1302                    rrsig_status = name_obj.rrsig_status[rrset_info][rrsig][dnskey]
1303                    if dnskey is None:
1304                        dnskey_node = None
1305                    else:
1306                        dnskey_node = self.get_dnskey(self.id_for_dnskey(signer_obj.name, dnskey.rdata), signer_obj.name, dnskey.rdata.algorithm, dnskey.key_tag)
1307
1308                    for signed_key in signed_keys:
1309                        signed_key_node = self.get_dnskey(self.id_for_dnskey(name_obj.name, signed_key.rdata), name_obj.name, signed_key.rdata.algorithm, signed_key.key_tag)
1310                        self.add_rrsig(rrsig_status, name_obj, signer_obj, signed_key_node)
1311
1312        # map negative responses for DNSKEY queries to top name of the zone
1313        try:
1314            dnskey_nodata_info = [x for x in name_obj.nodata_status if x.qname == name_obj.name and x.rdtype == dns.rdatatype.DNSKEY][0]
1315        except IndexError:
1316            pass
1317        else:
1318            self.node_reverse_mapping[dnskey_nodata_info] = zone_top
1319        try:
1320            dnskey_nxdomain_info = [x for x in name_obj.nxdomain_status if x.qname == name_obj.name and x.rdtype == dns.rdatatype.DNSKEY][0]
1321        except IndexError:
1322            pass
1323        else:
1324            self.node_reverse_mapping[dnskey_nxdomain_info] = zone_top
1325
1326        # handle other responses to DNSKEY/DS queries
1327        for rdtype in (dns.rdatatype.DS, dns.rdatatype.DNSKEY):
1328            if (name_obj.name, rdtype) in name_obj.queries:
1329
1330                # Handle errors and warnings for DNSKEY/DS queries
1331                if rdtype == dns.rdatatype.DS and zone_obj.parent is not None and not is_dlv:
1332                    z_obj = zone_obj.parent
1333                    self.graph_zone_auth(z_obj, False)
1334                else:
1335                    z_obj = zone_obj
1336                self.add_errors(name_obj, z_obj, name_obj.name, rdtype, name_obj.response_errors[name_obj.queries[(name_obj.name, rdtype)]])
1337                self.add_warnings(name_obj, z_obj, name_obj.name, rdtype, name_obj.response_warnings[name_obj.queries[(name_obj.name, rdtype)]])
1338
1339                # Map CNAME responses to DNSKEY/DS queries to appropriate node
1340                for rrset_info in name_obj.queries[(name_obj.name, rdtype)].answer_info:
1341                    if rrset_info.rrset.rdtype == dns.rdatatype.CNAME:
1342                        rrset_node = self.add_rrset(rrset_info, None, name_obj, name_obj.zone)
1343                        if rrset_node not in self.node_mapping:
1344                            self.node_mapping[rrset_node] = []
1345                        self.node_mapping[rrset_node].add(rrset_info)
1346                        self.node_reverse_mapping[rrset_info] = rrset_node
1347
1348        if not name_obj.is_zone():
1349            return
1350
1351        if name_obj.parent is None or is_dlv:
1352            return
1353
1354        for dlv in False, True:
1355            if dlv:
1356                parent_obj = name_obj.dlv_parent
1357                ds_name = name_obj.dlv_name
1358                rdtype = dns.rdatatype.DLV
1359            else:
1360                parent_obj = name_obj.parent
1361                ds_name = name_obj.name
1362                rdtype = dns.rdatatype.DS
1363
1364            if parent_obj is None or ds_name is None:
1365                continue
1366
1367            # if this is a DLV parent, and either we're not showing
1368            # DLV, or there is no DLV information for this zone, move along
1369            if dlv and (ds_name, rdtype) not in name_obj.queries:
1370                continue
1371
1372            self.graph_zone_auth(parent_obj, dlv)
1373
1374            P, parent_graph_name, parent_bottom, parent_top = self.add_zone(parent_obj)
1375
1376            for dnskey in name_obj.ds_status_by_dnskey[rdtype]:
1377                ds_statuses = list(name_obj.ds_status_by_dnskey[rdtype][dnskey].values())
1378
1379                # identify all validation_status/RRset/algorithm/key_tag
1380                # combinations, so we can cluster like DSs
1381                validation_statuses = set([(d.validation_status, d.ds_meta, d.ds.algorithm, d.ds.key_tag) for d in ds_statuses])
1382
1383                for validation_status, rrset_info, algorithm, key_tag in validation_statuses:
1384                    ds_status_subset = [x for x in ds_statuses if x.validation_status == validation_status and x.ds_meta is rrset_info and x.ds.algorithm == algorithm and x.ds.key_tag == key_tag]
1385
1386                    # create the DS node and edge
1387                    ds_node = self.add_ds(ds_name, ds_status_subset, name_obj, parent_obj)
1388
1389                    self.add_rrsigs(name_obj, parent_obj, rrset_info, ds_node)
1390
1391            edge_id = 0
1392
1393            nsec_statuses = []
1394            soa_rrsets = []
1395            try:
1396                ds_nodata_info = [x for x in name_obj.nodata_status if x.qname == ds_name and x.rdtype == rdtype][0]
1397                nsec_statuses.extend(name_obj.nodata_status[ds_nodata_info])
1398                soa_rrsets.extend(ds_nodata_info.soa_rrset_info)
1399            except IndexError:
1400                ds_nodata_info = None
1401            try:
1402                ds_nxdomain_info = [x for x in name_obj.nxdomain_status if x.qname == ds_name and x.rdtype == rdtype][0]
1403                nsec_statuses.extend(name_obj.nxdomain_status[ds_nxdomain_info])
1404                soa_rrsets.extend(ds_nxdomain_info.soa_rrset_info)
1405            except IndexError:
1406                ds_nxdomain_info = None
1407
1408            for nsec_status in nsec_statuses:
1409
1410                nsec_node = self.add_nsec(nsec_status, ds_name, rdtype, name_obj, parent_obj, zone_top)
1411                # add a tail to the cluster
1412                self.G.get_edge(zone_top, nsec_node).attr['ltail'] = zone_graph_name
1413                # anchor NSEC node to bottom
1414                self.G.add_edge(parent_bottom, nsec_node, style='invis')
1415
1416                for nsec_name, rrset_info in nsec_status.nsec_set_info.rrsets.items():
1417                    nsec_cell = lb2s(nsec_name.canonicalize().to_text())
1418                    self.add_rrsigs(name_obj, parent_obj, rrset_info, nsec_node, port=nsec_cell)
1419
1420                edge_id += 1
1421
1422            # add SOA
1423            for soa_rrset_info in soa_rrsets:
1424                soa_rrset_node = self.add_rrset(soa_rrset_info, None, name_obj, parent_obj)
1425                self.add_rrsigs(name_obj, parent_obj, soa_rrset_info, soa_rrset_node)
1426
1427            # add mappings for negative responses
1428            self.node_mapping[zone_top] = set()
1429            if ds_nodata_info is not None:
1430                self.node_mapping[zone_top].add(ds_nodata_info)
1431                self.node_reverse_mapping[ds_nodata_info] = zone_top
1432            if ds_nxdomain_info is not None:
1433                self.node_mapping[zone_top].add(ds_nxdomain_info)
1434                self.node_reverse_mapping[ds_nxdomain_info] = zone_top
1435
1436            has_warnings = name_obj.delegation_warnings[rdtype] or (ds_nxdomain_info is not None and name_obj.nxdomain_warnings[ds_nxdomain_info]) or (ds_nodata_info is not None and name_obj.nodata_warnings[ds_nodata_info])
1437            has_errors = name_obj.delegation_errors[rdtype] or (ds_nxdomain_info is not None and name_obj.nxdomain_errors[ds_nxdomain_info]) or (ds_nodata_info is not None and name_obj.nodata_errors[ds_nodata_info])
1438
1439            edge_label = ''
1440            if has_errors:
1441                edge_label = '<<TABLE BORDER="0"><TR><TD><IMG SCALE="TRUE" SRC="%s"/></TD></TR></TABLE>>' % ERROR_ICON
1442            elif has_warnings:
1443                edge_label = '<<TABLE BORDER="0"><TR><TD><IMG SCALE="TRUE" SRC="%s"/></TD></TR></TABLE>>' % WARNING_ICON
1444
1445            if name_obj.delegation_status[rdtype] == Status.DELEGATION_STATUS_SECURE:
1446                line_color = COLORS['secure']
1447                line_style = 'solid'
1448            elif name_obj.delegation_status[rdtype] == Status.DELEGATION_STATUS_INSECURE:
1449                line_color = COLORS['insecure']
1450                line_style = 'solid'
1451            elif name_obj.delegation_status[rdtype] in (Status.DELEGATION_STATUS_INCOMPLETE, Status.DELEGATION_STATUS_LAME):
1452                line_color = COLORS['misconfigured']
1453                line_style = 'dashed'
1454            elif name_obj.delegation_status[rdtype] == Status.DELEGATION_STATUS_BOGUS:
1455                line_color = COLORS['bogus']
1456                line_style = 'dashed'
1457
1458            consolidate_clients = name_obj.single_client()
1459            del_serialized = OrderedDict()
1460            del_serialized['description'] = 'Delegation from %s to %s' % (lb2s(name_obj.parent.name.to_text()), lb2s(name_obj.name.to_text()))
1461            del_serialized['status'] = Status.delegation_status_mapping[name_obj.delegation_status[rdtype]]
1462
1463            if has_warnings:
1464                del_serialized['warnings'] = []
1465                del_serialized['warnings'] += [w.serialize(consolidate_clients=consolidate_clients, html_format=True) for w in name_obj.delegation_warnings[rdtype]]
1466                del_serialized['warnings'] += [w.serialize(consolidate_clients=consolidate_clients, html_format=True) for w in name_obj.nxdomain_warnings.get(ds_nxdomain_info, [])]
1467                del_serialized['warnings'] += [w.serialize(consolidate_clients=consolidate_clients, html_format=True) for w in name_obj.nodata_warnings.get(ds_nodata_info, [])]
1468
1469            if has_errors:
1470                del_serialized['errors'] = []
1471                del_serialized['errors'] += [e.serialize(consolidate_clients=consolidate_clients, html_format=True) for e in name_obj.delegation_errors[rdtype]]
1472                del_serialized['errors'] += [e.serialize(consolidate_clients=consolidate_clients, html_format=True) for e in name_obj.nxdomain_errors.get(ds_nxdomain_info, [])]
1473                del_serialized['errors'] += [e.serialize(consolidate_clients=consolidate_clients, html_format=True) for e in name_obj.nodata_errors.get(ds_nodata_info, [])]
1474
1475            edge_id = 'del-%s|%s' % (fmt.humanize_name(zone_obj.name), fmt.humanize_name(parent_obj.name))
1476            self.node_info[edge_id] = [del_serialized]
1477            self.G.add_edge(zone_top, parent_bottom, label=edge_label, id=edge_id, color=line_color, penwidth='5.0', ltail=zone_graph_name, lhead=parent_graph_name, style=line_style, minlen='2', dir='back')
1478
1479    def _set_non_existent_color(self, n):
1480        if DASHED_STYLE_RE.search(n.attr['style']) is None:
1481            return
1482
1483        if n.attr['color'] == COLORS['secure']:
1484            n.attr['color'] = COLORS['secure_non_existent']
1485
1486            # if this is an authenticated negative response, and the NSEC3
1487            # RR used opt out, then the node is actually insecure, rather
1488            # than secure.
1489            for n1 in self.G.out_neighbors(n):
1490                if n1.startswith('NSEC3') and OPTOUT_STYLE_RE.search(n1.attr['label']):
1491                    n.attr['color'] = COLORS['insecure_non_existent']
1492
1493        elif n.attr['color'] == COLORS['bogus']:
1494            n.attr['color'] = COLORS['bogus_non_existent']
1495
1496        else:
1497            n.attr['color'] = COLORS['insecure_non_existent']
1498
1499    def _set_nsec_color(self, n):
1500        if not n.startswith('NSEC'):
1501            return
1502
1503        #XXX we have to assign l to n.attr['label'], perform any update
1504        # operations on l, then assign n.attr['label'] to l's new value,
1505        # wrapping it in "<...>".  This is because the "<" and ">" at the start
1506        # and end somehow get lost when the assignment is made directly.
1507        l = n.attr['label']
1508        l = re.sub(r'^(<<TABLE)', r'\1 COLOR="%s"' % n.attr['color'], l, 1)
1509        if n.attr['color'] == COLORS['bogus']:
1510            #XXX it looks better when cellspacing is 0, but we can't do that
1511            # when there are cells that are colored with different colors.  In
1512            # this case, we need to change the cell spacing back to 0
1513            l = re.sub(r'(<TABLE[^>]+CELLSPACING=")-\d+"', r'\g<1>0"', l, 1)
1514            for nsec_name in self.nsec_rr_status[n]:
1515                if not self.nsec_rr_status[n][nsec_name]:
1516                    self.nsec_rr_status[n][nsec_name] = COLORS['bogus']
1517                l = re.sub(r'(<TD[^>]+PORT="%s")' % nsec_name, r'\1 COLOR="%s"' % self.nsec_rr_status[n][nsec_name], l, 1)
1518        n.attr['label'] = '<%s>' % l
1519
1520    def _set_node_status(self, n):
1521        status = self.status_for_node(n)
1522
1523        node_id = n.replace('*', '_')
1524        for serialized in self.node_info[node_id]:
1525            serialized['status'] = Status.rrset_status_mapping[status]
1526
1527    def add_trust(self, trusted_keys, supported_algs=None):
1528        trusted_keys = tuple_to_dict(trusted_keys)
1529        if supported_algs is not None:
1530            supported_algs.intersection_update(crypto._supported_algs)
1531        else:
1532            supported_algs = crypto._supported_algs
1533
1534        dlv_nodes = []
1535        trusted_zone_top_names = set([self.get_zone(z)[3] for z in trusted_keys])
1536        for zone in trusted_keys:
1537            zone_top_name = self.get_zone(zone)[3]
1538            if not self.G.has_node(zone_top_name) or zone_top_name not in self.subgraph_not_stub:
1539                continue
1540
1541            # if at least one algorithm in trusted keys for the zone is
1542            # supported, then give zone no initial marking; otherwise mark it
1543            # as insecure
1544            algs = set([d.algorithm for d in trusted_keys[zone]])
1545            if algs.intersection(supported_algs):
1546                self.G.get_node(zone_top_name).attr['color'] = ''
1547            else:
1548                self.G.get_node(zone_top_name).attr['color'] = COLORS['insecure']
1549
1550            for dnskey in trusted_keys[zone]:
1551                try:
1552                    dnskey_node = self.get_dnskey(self.id_for_dnskey(zone, dnskey), zone, dnskey.algorithm, Response.DNSKEYMeta.calc_key_tag(dnskey))
1553                    dnskey_node.attr['peripheries'] = 2
1554                    if self.G.get_node(zone_top_name).attr['color'] == '':
1555                        self._add_trust_to_nodes_in_chain(dnskey_node, trusted_zone_top_names, dlv_nodes, False, [])
1556                except KeyError:
1557                    dnskey_node = self.add_dnskey_non_existent(zone, zone, dnskey.algorithm, Response.DNSKEYMeta.calc_key_tag(dnskey))
1558                    dnskey_node.attr['peripheries'] = 2
1559
1560        # determine DLV zones based on DLV nodes
1561        dlv_trusted_zone_top_names = []
1562        for dlv_node in dlv_nodes:
1563            dlv_trusted_zone_top_names.append(self.node_subgraph_name[dlv_node])
1564
1565        # now traverse clusters and mark insecure nodes in secure delegations as bad
1566        for zone in trusted_keys:
1567            S, zone_node_str, zone_bottom_name, zone_top_name = self.get_zone(zone)
1568            if not self.G.has_node(zone_top_name) or zone_top_name not in self.subgraph_not_stub:
1569                continue
1570
1571            # don't yet mark subdomains of DLV zones, as we have yet
1572            # to add trust to them
1573            if zone_top_name not in dlv_trusted_zone_top_names:
1574                self._add_trust_to_orphaned_nodes(zone_node_str, [])
1575
1576        # now that we can show which zones are provably insecure, we
1577        # can apply trust from the DLV zones
1578        for dlv_node in dlv_nodes:
1579            self._add_trust_to_nodes_in_chain(dlv_node, trusted_zone_top_names, [], True, [])
1580
1581        # now mark the orphaned nodes
1582        for dlv_node in dlv_nodes:
1583            zone_node_str = self.node_subgraph_name[dlv_node][:-4]
1584            self._add_trust_to_orphaned_nodes(zone_node_str, [])
1585
1586        for n in self.G.nodes():
1587            # set the status of (only) the cluster top node as well
1588            if n.attr['shape'] == 'point' and n.endswith('_top'):
1589                pass
1590            elif n.attr['shape'] not in ('ellipse', 'rectangle') and not n.startswith('NSEC'):
1591                continue
1592            self._set_non_existent_color(n)
1593            self._set_nsec_color(n)
1594            self._set_node_status(n)
1595
1596    def status_for_node(self, n, port=None):
1597        n = self.G.get_node(n)
1598
1599        if n.attr['color'] in (COLORS['secure'], COLORS['secure_non_existent']):
1600            status = Status.RRSET_STATUS_SECURE
1601        elif n.attr['color'] in (COLORS['bogus'], COLORS['bogus_non_existent']):
1602            if port is not None and self.nsec_rr_status[n][port] == COLORS['secure']:
1603                status = Status.RRSET_STATUS_SECURE
1604            else:
1605                status = Status.RRSET_STATUS_BOGUS
1606        else:
1607            if n.startswith('DNSKEY') and \
1608                    DASHED_STYLE_RE.search(n.attr['style']):
1609                status = Status.RRSET_STATUS_NON_EXISTENT
1610            else:
1611                status = Status.RRSET_STATUS_INSECURE
1612        return status
1613
1614    def secure_nsec3_optout_nodes_covering_node(self, n):
1615        return [x for x in self.G.out_neighbors(n) if x.startswith('NSEC') and \
1616                OPTOUT_STYLE_RE.search(x.attr['label']) is not None and \
1617                x.attr['color'] == COLORS['secure']]
1618
1619    def secure_nsec_nodes_covering_node(self, n):
1620        return [x for x in self.G.out_neighbors(n) if x.startswith('NSEC') and \
1621                x.attr['color'] == COLORS['secure']]
1622
1623    def is_invis(self, n):
1624        return INVIS_STYLE_RE.search(self.G.get_node(n).attr['style']) is not None
1625
1626    def _add_trust_to_nodes_in_chain(self, n, trusted_zones, dlv_nodes, force, trace):
1627        if n in trace:
1628            return
1629
1630        is_ds = n.startswith('DS-') or n.startswith('DLV-')
1631        is_dlv = n.startswith('DLV-')
1632        is_dnskey = n.startswith('DNSKEY-')
1633        is_nsec = n.startswith('NSEC')
1634        is_dname = n.endswith('|DNAME')
1635
1636        if is_dlv and not force:
1637            dlv_nodes.append(n)
1638            return
1639
1640        # if n isn't a DNSKEY, DS/DLV, or NSEC record,
1641        # then don't follow back edges
1642        if not (is_ds or is_dnskey or is_nsec or is_dname):
1643            return
1644
1645        is_revoked = n.attr['penwidth'] == '4.0'
1646        is_trust_anchor = n.attr['peripheries'] == '2'
1647
1648        top_name = self.G.get_node(self.node_subgraph_name[n])
1649
1650        # trust anchor and revoked DNSKEY must be self-signed
1651        if is_revoked or is_trust_anchor:
1652            valid_self_loop = False
1653            if self.G.has_edge(n,n):
1654                for e1 in self.G.out_edges(n) + self.G.in_edges(n):
1655                    if (n,n) == e1 and \
1656                            e1.attr['color'] == COLORS['secure']:
1657                        valid_self_loop = True
1658
1659                        # mark all the DNSKEY RRsets as valid
1660                        for rrsig in self.node_mapping[e1.attr['id']]:
1661                            self.secure_dnskey_rrsets.add(rrsig.rrset)
1662
1663                        break
1664
1665            #XXX revisit if we want to do this here
1666            if is_revoked and n.attr['color'] == COLORS['secure'] and not valid_self_loop:
1667                n.attr['color'] = COLORS['bogus']
1668
1669            # mark the zone as "secure" as there is a secure entry point;
1670            # descendants will be so marked by following the delegation edges
1671            if is_trust_anchor and valid_self_loop:
1672                n.attr['color'] = COLORS['secure']
1673                top_name.attr['color'] = COLORS['secure']
1674
1675        node_trusted = n.attr['color'] == COLORS['secure']
1676
1677        if is_dnskey and not node_trusted:
1678            # Here we are shortcutting the traversal because we are no longer
1679            # propagating trust.  But we still need to learn of any DLV nodes.
1680            if not force:
1681                S = self.G.get_subgraph(top_name[:-4])
1682                for n in S.nodes():
1683                    if n.startswith('DLV-'):
1684                        dlv_nodes.append(n)
1685            return
1686
1687        # iterate through each edge and propagate trust from this node
1688        for e in self.G.in_edges(n):
1689            p = e[0]
1690
1691            # if this is an edge used for formatting node (invis), then don't
1692            # follow it
1693            if INVIS_STYLE_RE.search(e.attr['style']) is not None:
1694                continue
1695
1696            prev_top_name = self.G.get_node(self.node_subgraph_name[p])
1697
1698            # don't derive trust from parent if there is a trust anchor at the
1699            # child
1700            if is_ds and prev_top_name in trusted_zones:
1701                continue
1702
1703            # if the previous node is already secure, then no need to follow it
1704            if p.attr['color'] == COLORS['secure']:
1705                continue
1706
1707            # if this is a DLV node, then the zone it covers must be marked
1708            # as insecure through previous trust traversal (not because of
1709            # a local trust anchor, which case is handled above)
1710            if is_dlv:
1711                if prev_top_name.attr['color'] not in ('', COLORS['insecure']):
1712                    continue
1713
1714                # reset the security of this top_name
1715                prev_top_name.attr['color'] = ''
1716
1717            # if this is a non-matching edge (dashed) then don't follow it
1718            if DASHED_STYLE_RE.search(e.attr['style']) is not None:
1719                continue
1720
1721            # derive trust for the previous node using the current node and the
1722            # color of the edge in between
1723            prev_node_trusted = node_trusted and e.attr['color'] == COLORS['secure']
1724
1725            if is_ds:
1726                # if this is an edge between DS and DNSKEY, then the DNSKEY is
1727                # not considered secure unless it has a valid self-loop (in
1728                # addition to the connecting edge being valid)
1729                valid_self_loop = False
1730                if self.G.has_edge(p,p):
1731                    for e1 in self.G.out_edges(p) + self.G.in_edges(p):
1732                        if (p,p) == e1 and \
1733                                e1.attr['color'] == COLORS['secure']:
1734                            valid_self_loop = True
1735
1736                            # mark all the DNSKEY RRsets as valid
1737                            for rrsig in self.node_mapping[e1.attr['id']]:
1738                                self.secure_dnskey_rrsets.add(rrsig.rrset)
1739
1740                            break
1741
1742                prev_node_trusted = prev_node_trusted and valid_self_loop
1743
1744            # if p is an NSEC (set) node, then we need to check that all the
1745            # NSEC RRs have been authenticated before we mark this one as
1746            # authenticated.
1747            elif p.startswith('NSEC'):
1748                rrsig_status = list(self.node_mapping[e.attr['id']])[0]
1749                nsec_name = lb2s(rrsig_status.rrset.rrset.name.canonicalize().to_text()).replace(r'"', r'\"')
1750                if prev_node_trusted:
1751                    self.nsec_rr_status[p][nsec_name] = COLORS['secure']
1752                    for nsec_name in self.nsec_rr_status[p]:
1753                        if self.nsec_rr_status[p][nsec_name] != COLORS['secure']:
1754                            prev_node_trusted = False
1755
1756            if is_nsec:
1757                # if this is an NSEC, then only propagate trust if the previous
1758                # node (i.e., the node it covers) is an RRset
1759                if prev_node_trusted and p.attr['shape'] == 'rectangle':
1760                    p.attr['color'] = COLORS['secure']
1761
1762            elif prev_node_trusted:
1763                p.attr['color'] = COLORS['secure']
1764
1765            self._add_trust_to_nodes_in_chain(p, trusted_zones, dlv_nodes, force, trace+[n])
1766
1767    def _add_trust_to_orphaned_nodes(self, subgraph_name, trace):
1768        if subgraph_name in trace:
1769            return
1770
1771        top_name = self.G.get_node(subgraph_name + '_top')
1772        bottom_name = self.G.get_node(subgraph_name + '_bottom')
1773
1774
1775        # if this subgraph (zone) is provably insecure, then don't process
1776        # further
1777        if top_name.attr['color'] == COLORS['insecure']:
1778            return
1779
1780        # iterate through each node in the subgraph (zone) and mark as bogus
1781        # all nodes that are not already marked as secure
1782        S = self.G.get_subgraph(subgraph_name)
1783        for n in S.nodes():
1784            # don't mark invisible nodes (zone marking as secure/insecure is handled in the
1785            # traversal at the delegation point below).
1786            if INVIS_STYLE_RE.search(n.attr['style']) is not None:
1787                continue
1788
1789            # if node is non-existent, then don't mark it, unless we are talking about an RRset
1790            # or a non-existent trust anchor; it doesn't make sense to mark other nodes
1791            # as bogus
1792            if DASHED_STYLE_RE.search(n.attr['style']) is not None and not (n.attr['shape'] == 'rectangle' or \
1793                    n.attr['peripheries'] == 2):
1794                continue
1795
1796            # if the name is already marked as secure
1797            if n.attr['color'] == COLORS['secure']:
1798                # don't mark it as bogus
1799                continue
1800
1801            n.attr['color'] = COLORS['bogus']
1802
1803        # propagate trust through each descendant node
1804        for p in self.G.predecessors(bottom_name):
1805            e = self.G.get_edge(p, bottom_name)
1806
1807            child_subgraph_name = p[:-4]
1808
1809            if top_name.attr['color'] == COLORS['secure']:
1810                # if this subgraph (zone) is secure, and the delegation is also
1811                # secure, then mark the delegated subgraph (zone) as secure.
1812                if e.attr['color'] == COLORS['secure']:
1813                    p.attr['color'] = COLORS['secure']
1814                # if this subgraph (zone) is secure, and the delegation is not
1815                # bogus (DNSSEC broken), then mark it as provably insecure.
1816                elif e.attr['color'] != COLORS['bogus']:
1817                    # in this case, it's possible that the proven insecurity is
1818                    # dependent on NSEC/NSEC3 records that need to be
1819                    # authenticated.  Before marking this as insecure, reach
1820                    # back up for NSEC records.  If any are found, make sure at
1821                    # least one has been authenticated (i.e., has secure
1822                    # color).
1823                    nsec_found = False
1824                    nsec_authenticated = False
1825                    for n in self.G.out_neighbors(p):
1826                        if not n.startswith('NSEC'):
1827                            continue
1828                        # check that this node is in the zone we're coming from
1829                        if self.node_subgraph_name[n] != top_name:
1830                            continue
1831                        nsec_found = True
1832                        if n.attr['color'] == COLORS['secure']:
1833                            nsec_authenticated = True
1834                            break
1835
1836                    # or if there are DS, then there are algorithms that are
1837                    # not understood (otherwise it would not be insecure).
1838                    # Check that at least one of the DS nodes was marked as
1839                    # secure.
1840                    ds_found = False
1841                    ds_authenticated = False
1842                    S = self.G.get_subgraph(child_subgraph_name)
1843                    for n in S.nodes():
1844                        # we're only concerned with DNSKEYs
1845                        if not n.startswith('DNSKEY-'):
1846                            continue
1847                        # we're looking for DS records
1848                        for d in self.G.out_neighbors(n):
1849                            if not (d.startswith('DS-') or d.startswith('DLV-')):
1850                                continue
1851                            # check that this node is in the zone we're coming from
1852                            if self.node_subgraph_name[d] != top_name:
1853                                continue
1854                            ds_found = True
1855                            if d.attr['color'] == COLORS['secure']:
1856                                ds_authenticated = True
1857                                break
1858
1859                    if nsec_found and not nsec_authenticated:
1860                        pass
1861                    elif ds_found and not ds_authenticated:
1862                        pass
1863                    else:
1864                        p.attr['color'] = COLORS['insecure']
1865
1866            # if the child was not otherwise marked, then mark it as bogus
1867            if p.attr['color'] == '':
1868                p.attr['color'] = COLORS['bogus']
1869
1870            self._add_trust_to_orphaned_nodes(child_subgraph_name, trace+[subgraph_name])
1871
1872    def remove_extra_edges(self, show_redundant=False):
1873        #XXX this assumes DNSKEYs with same name as apex
1874        for S in self.G.subgraphs():
1875            non_dnskey = set()
1876            all_dnskeys = set()
1877            ds_dnskeys = set()
1878            ta_dnskeys = set()
1879            ksks = set()
1880            zsks = set()
1881            sep_bit = set()
1882            revoked_dnskeys = set()
1883            non_existent_dnskeys = set()
1884            existing_dnskeys = set()
1885
1886            for n in S.nodes():
1887                if not n.startswith('DNSKEY-'):
1888                    if n.attr['shape'] != 'point':
1889                        non_dnskey.add(n)
1890                    continue
1891
1892                all_dnskeys.add(n)
1893
1894                in_edges = self.G.in_edges(n)
1895                out_edges = self.G.out_edges(n)
1896                ds_edges = [x for x in out_edges if x[1].startswith('DS-') or x[1].startswith('DLV-')]
1897
1898                is_ksk = bool([x for x in in_edges if x[0].startswith('DNSKEY-')])
1899                is_zsk = bool([x for x in in_edges if not x[0].startswith('DNSKEY-')])
1900                non_existent = DASHED_STYLE_RE.search(n.attr['style']) is not None
1901                has_sep_bit = n.attr['fillcolor'] == 'lightgray'
1902
1903                if is_ksk:
1904                    ksks.add(n)
1905                if is_zsk:
1906                    zsks.add(n)
1907                if has_sep_bit:
1908                    sep_bit.add(n)
1909                if n.attr['peripheries'] == '2':
1910                    ta_dnskeys.add(n)
1911                if ds_edges:
1912                    ds_dnskeys.add(n)
1913                if n.attr['penwidth'] == '4.0':
1914                    revoked_dnskeys.add(n)
1915                if non_existent:
1916                    non_existent_dnskeys.add(n)
1917                else:
1918                    existing_dnskeys.add(n)
1919
1920            seps = ds_dnskeys.union(ta_dnskeys).intersection(ksks).difference(revoked_dnskeys)
1921            ksk_only = ksks.difference(zsks).difference(revoked_dnskeys)
1922            zsk_only = zsks.difference(ksks).difference(revoked_dnskeys)
1923
1924            # if all keys have only KSK roles (i.e., none are signing the zone
1925            # data), then try to distinguish using SEP bit
1926            if ksk_only and not zsks and sep_bit:
1927                ksk_only.intersection_update(sep_bit)
1928
1929            if seps:
1930                top_level_keys = seps
1931            else:
1932                if ksk_only:
1933                    top_level_keys = ksk_only
1934                elif ksks:
1935                    top_level_keys = ksks
1936                elif sep_bit:
1937                    top_level_keys = sep_bit
1938                else:
1939                    top_level_keys = all_dnskeys
1940
1941            if top_level_keys:
1942
1943                # If there aren't any KSKs or ZSKs, then signing roles are
1944                # unknown, and the top-level keys are organized by SEP bit.
1945                # Because there are no roles, every key is an "island" (i.e.,
1946                # not signed by any top-level keys), so only look for "islands"
1947                # if there are ZSKs or KSKs.
1948                if zsks or ksks:
1949                    for n in all_dnskeys.difference(top_level_keys):
1950                        if set(self.G.out_neighbors(n)).intersection(top_level_keys):
1951                            # If this key is already signed by a top-level, then
1952                            # it's not in an island.
1953                            pass
1954                        else:
1955                            # Otherwise, find out what keys are connected to this one
1956                            neighbors = set(self.G.neighbors(n))
1957
1958                            # If this key is ksk only, then it is always a top-level key.
1959                            if n in ksk_only:
1960                                top_level_keys.add(n)
1961
1962                            # If this key is not a ksk, and there are ksks, then
1963                            # it's not a top-level key.
1964                            elif n not in ksks and neighbors.intersection(ksks):
1965                                pass
1966
1967                            # If this key does not have its sep bit set, and there
1968                            # are others that do, then it's not a top-level key.
1969                            elif n not in sep_bit and neighbors.intersection(sep_bit):
1970                                pass
1971
1972                            # Otherwise, it's on the same rank as all the others,
1973                            # so it is a top-level key.
1974                            else:
1975                                top_level_keys.add(n)
1976
1977                # In the case where a top-level key is signing zone data, and
1978                # there are other top-level keys that are not signing zone data,
1979                # remove it from the top-level keys list, and don't add an edge
1980                # to the top.  This will make the other top-level keys appear
1981                # "higher".
1982                for n in list(top_level_keys):
1983                    if n in zsks and set(self.G.neighbors(n)).intersection(top_level_keys).intersection(ksk_only):
1984                        top_level_keys.remove(n)
1985                    else:
1986                        self.G.add_edge(n, self.node_subgraph_name[n], style='invis')
1987
1988                # Now handle all the keys not at the top level
1989                non_top_level_keys = all_dnskeys.difference(top_level_keys)
1990                if non_top_level_keys:
1991                    # If there are any keys that are not at the top level, then
1992                    # determine whether they should be connected to the
1993                    # top-level keys, to the top, or left alone.
1994                    for n in non_top_level_keys:
1995
1996                        # Non-existent DNSKEYs corresponding to DS and trust
1997                        # anchors should be connected to the top.
1998                        if n in non_existent_dnskeys:
1999                            if n in ds_dnskeys or n in ta_dnskeys:
2000                                self.G.add_edge(n, self.node_subgraph_name[n], style='invis')
2001
2002                        # If not linked to any other DNSKEYs, then link to
2003                        # top-level keys.
2004                        elif not [x for x in self.G.out_neighbors(n) if x.startswith('DNSKEY')]:
2005                            for m in top_level_keys:
2006                                if not self.G.has_edge(n, m):
2007                                    self.G.add_edge(n, m, style='invis')
2008
2009                    intermediate_keys = non_top_level_keys
2010                else:
2011                    intermediate_keys = top_level_keys
2012
2013                # If there are ZSKs (and possible ZSKs only signing zone data),
2014                # then make those the intermediate keys, instead of using all
2015                # the top-level (or non-top-level) keys.
2016                if zsk_only:
2017                    intermediate_keys = zsk_only
2018                elif zsks:
2019                    intermediate_keys = zsks
2020
2021                # Link non-keys to intermediate DNSKEYs
2022                for n in non_dnskey:
2023                    if [x for x in self.G.out_neighbors(n) if x.startswith('DNSKEY') or x.startswith('NSEC')]:
2024                        continue
2025                    for m in intermediate_keys:
2026                        # we only link to non-existent DNSKEYs corresponding to
2027                        # DS records if there aren't any existing DNSKEYs.
2028                        if m in ds_dnskeys and m in non_existent_dnskeys:
2029                            if existing_dnskeys:
2030                                continue
2031                        self.G.add_edge(n, m, style='invis')
2032
2033            else:
2034                # For all non-existent non-DNSKEYs, add an edge to the top
2035                for n in non_dnskey:
2036                    if [x for x in self.G.out_neighbors(n) if x.startswith('DNSKEY') or x.startswith('NSEC')]:
2037                        continue
2038                    self.G.add_edge(n, self.node_subgraph_name[n], style='invis')
2039
2040            for n in ksks:
2041                retain_edge_default = n in top_level_keys
2042                for e in self.G.in_edges(n):
2043                    m = e[0]
2044                    if not m.startswith('DNSKEY-'):
2045                        continue
2046                    if n == m:
2047                        continue
2048
2049                    if retain_edge_default and m in top_level_keys:
2050                        retain_edge = False
2051                    else:
2052                        retain_edge = retain_edge_default
2053
2054                    if not retain_edge:
2055                        if show_redundant:
2056                            self.G.get_edge(m, n).attr['constraint'] = 'false'
2057                        else:
2058                            try:
2059                                del self.node_info[e.attr.get('id', None)]
2060                            except KeyError:
2061                                pass
2062                            self.G.remove_edge(m, n)
2063