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