1#!/usr/bin/env python 2 3# Ndiff 4# 5# This programs reads two Nmap XML files and displays a list of their 6# differences. 7# 8# Copyright 2008 Insecure.Com LLC 9# Ndiff is distributed under the same license as Nmap. See the file LICENSE or 10# https://nmap.org/data/LICENSE. See https://nmap.org/book/man-legal.html for 11# more details. 12# 13# David Fifield 14# based on a design by Michael Pattrick 15 16import datetime 17import difflib 18import getopt 19import sys 20import time 21 22# Prevent loading PyXML 23import xml 24xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x] 25 26import xml.sax 27import xml.sax.saxutils 28import xml.dom.minidom 29from StringIO import StringIO 30 31verbose = False 32 33NDIFF_XML_VERSION = u"1" 34 35 36class OverrideEntityResolver(xml.sax.handler.EntityResolver): 37 """This class overrides the default behavior of xml.sax to download 38 remote DTDs, instead returning blank strings""" 39 empty = StringIO() 40 41 def resolveEntity(self, publicId, systemId): 42 return OverrideEntityResolver.empty 43 44 45class Scan(object): 46 """A single Nmap scan, corresponding to a single invocation of Nmap. It is 47 a container for a list of hosts. It also has utility methods to load itself 48 from an Nmap XML file.""" 49 def __init__(self): 50 self.scanner = None 51 self.version = None 52 self.args = None 53 self.start_date = None 54 self.end_date = None 55 self.hosts = [] 56 self.pre_script_results = [] 57 self.post_script_results = [] 58 59 def sort_hosts(self): 60 self.hosts.sort(key=lambda h: h.get_id()) 61 62 def load(self, f): 63 """Load a scan from the Nmap XML in the file-like object f.""" 64 parser = xml.sax.make_parser() 65 handler = NmapContentHandler(self) 66 parser.setEntityResolver(OverrideEntityResolver()) 67 parser.setContentHandler(handler) 68 parser.parse(f) 69 70 def load_from_file(self, filename): 71 """Load a scan from the Nmap XML file with the given filename.""" 72 with open(filename, "r") as f: 73 self.load(f) 74 75 def write_nmaprun_open(self, writer): 76 attrs = {} 77 if self.scanner is not None: 78 attrs[u"scanner"] = self.scanner 79 if self.args is not None: 80 attrs[u"args"] = self.args 81 if self.start_date is not None: 82 attrs[u"start"] = "%d" % time.mktime(self.start_date.timetuple()) 83 attrs[u"startstr"] = self.start_date.strftime( 84 "%a %b %d %H:%M:%S %Y") 85 if self.version is not None: 86 attrs[u"version"] = self.version 87 writer.startElement(u"nmaprun", attrs) 88 89 def write_nmaprun_close(self, writer): 90 writer.endElement(u"nmaprun") 91 92 def nmaprun_to_dom_fragment(self, document): 93 frag = document.createDocumentFragment() 94 elem = document.createElement(u"nmaprun") 95 if self.scanner is not None: 96 elem.setAttribute(u"scanner", self.scanner) 97 if self.args is not None: 98 elem.setAttribute(u"args", self.args) 99 if self.start_date is not None: 100 elem.setAttribute( 101 u"start", "%d" % time.mktime(self.start_date.timetuple())) 102 elem.setAttribute( 103 u"startstr", 104 self.start_date.strftime("%a %b %d %H:%M:%S %Y")) 105 if self.version is not None: 106 elem.setAttribute(u"version", self.version) 107 frag.appendChild(elem) 108 return frag 109 110 111class Host(object): 112 """A single host, with a state, addresses, host names, a dict mapping port 113 specs to Ports, and a list of OS matches. Host states are strings, or None 114 for "unknown".""" 115 def __init__(self): 116 self.state = None 117 self.addresses = [] 118 self.hostnames = [] 119 self.ports = {} 120 self.extraports = {} 121 self.os = [] 122 self.script_results = [] 123 124 def get_id(self): 125 """Return an id that is used to determine if hosts are "the same" 126 across scans.""" 127 hid = None 128 if len(self.addresses) > 0: 129 hid = "%-40s" % (str(sorted(self.addresses)[0])) 130 if len(self.hostnames) > 0: 131 return (hid or " " * 40) + str(sorted(self.hostnames)[0]) 132 return hid or id(self) 133 134 def format_name(self): 135 """Return a human-readable identifier for this host.""" 136 address_s = u", ".join(a.s for a in sorted(self.addresses)) 137 hostname_s = u", ".join(sorted(self.hostnames)) 138 if len(hostname_s) > 0: 139 if len(address_s) > 0: 140 return u"%s (%s)" % (hostname_s, address_s) 141 else: 142 return hostname_s 143 elif len(address_s) > 0: 144 return address_s 145 else: 146 return u"<no name>" 147 148 def add_port(self, port): 149 self.ports[port.spec] = port 150 151 def add_address(self, address): 152 if address not in self.addresses: 153 self.addresses.append(address) 154 155 def add_hostname(self, hostname): 156 if hostname not in self.hostnames: 157 self.hostnames.append(hostname) 158 159 def is_extraports(self, state): 160 return state is None or state in self.extraports 161 162 def extraports_string(self): 163 list = [(count, state) for (state, count) in self.extraports.items()] 164 # Reverse-sort by count. 165 list.sort(reverse=True) 166 return u", ".join( 167 [u"%d %s ports" % (count, state) for (count, state) in list]) 168 169 def state_to_dom_fragment(self, document): 170 frag = document.createDocumentFragment() 171 if self.state is not None: 172 elem = document.createElement(u"status") 173 elem.setAttribute(u"state", self.state) 174 frag.appendChild(elem) 175 return frag 176 177 def hostname_to_dom_fragment(self, document, hostname): 178 frag = document.createDocumentFragment() 179 elem = document.createElement(u"hostname") 180 elem.setAttribute(u"name", hostname) 181 frag.appendChild(elem) 182 return frag 183 184 def extraports_to_dom_fragment(self, document): 185 frag = document.createDocumentFragment() 186 for state, count in self.extraports.items(): 187 elem = document.createElement(u"extraports") 188 elem.setAttribute(u"state", state) 189 elem.setAttribute(u"count", unicode(count)) 190 frag.appendChild(elem) 191 return frag 192 193 def os_to_dom_fragment(self, document, os): 194 frag = document.createDocumentFragment() 195 elem = document.createElement(u"osmatch") 196 elem.setAttribute(u"name", os) 197 frag.appendChild(elem) 198 return frag 199 200 def to_dom_fragment(self, document): 201 frag = document.createDocumentFragment() 202 elem = document.createElement(u"host") 203 204 if self.state is not None: 205 elem.appendChild(self.state_to_dom_fragment(document)) 206 207 for addr in self.addresses: 208 elem.appendChild(addr.to_dom_fragment(document)) 209 210 if len(self.hostnames) > 0: 211 hostnames_elem = document.createElement(u"hostnames") 212 for hostname in self.hostnames: 213 hostnames_elem.appendChild( 214 self.hostname_to_dom_fragment(document, hostname)) 215 elem.appendChild(hostnames_elem) 216 217 ports_elem = document.createElement(u"ports") 218 ports_elem.appendChild(self.extraports_to_dom_fragment(document)) 219 for port in sorted(self.ports.values()): 220 if not self.is_extraports(port.state): 221 ports_elem.appendChild(port.to_dom_fragment(document)) 222 if ports_elem.hasChildNodes(): 223 elem.appendChild(ports_elem) 224 225 if len(self.os) > 0: 226 os_elem = document.createElement(u"os") 227 for os in self.os: 228 os_elem.appendChild(self.os_to_dom_fragment(document, os)) 229 elem.appendChild(os_elem) 230 231 if len(self.script_results) > 0: 232 hostscript_elem = document.createElement(u"hostscript") 233 for sr in self.script_results: 234 hostscript_elem.appendChild(sr.to_dom_fragment(document)) 235 elem.appendChild(hostscript_elem) 236 237 frag.appendChild(elem) 238 return frag 239 240 241class Address(object): 242 def __init__(self, s): 243 self.s = s 244 245 def __eq__(self, other): 246 return self.__cmp__(other) == 0 247 248 def __ne__(self, other): 249 return not self.__eq__(other) 250 251 def __hash__(self): 252 return hash(self.sort_key()) 253 254 def __cmp__(self, other): 255 return cmp(self.sort_key(), other.sort_key()) 256 257 def __str__(self): 258 return str(self.s) 259 260 def __unicode__(self): 261 return self.s 262 263 def new(type, s): 264 if type == u"ipv4": 265 return IPv4Address(s) 266 elif type == u"ipv6": 267 return IPv6Address(s) 268 elif type == u"mac": 269 return MACAddress(s) 270 else: 271 raise ValueError(u"Unknown address type %s." % type) 272 new = staticmethod(new) 273 274 def to_dom_fragment(self, document): 275 frag = document.createDocumentFragment() 276 elem = document.createElement(u"address") 277 elem.setAttribute(u"addr", self.s) 278 elem.setAttribute(u"addrtype", self.type) 279 frag.appendChild(elem) 280 return frag 281 282# The sort_key method in the Address subclasses determines the order in which 283# addresses are displayed. We do IPv4, then IPv6, then MAC. 284 285 286class IPv4Address(Address): 287 type = property(lambda self: u"ipv4") 288 289 def sort_key(self): 290 return (0, self.s) 291 292 293class IPv6Address(Address): 294 type = property(lambda self: u"ipv6") 295 296 def sort_key(self): 297 return (1, self.s) 298 299 300class MACAddress(Address): 301 type = property(lambda self: u"mac") 302 303 def sort_key(self): 304 return (2, self.s) 305 306 307class Port(object): 308 """A single port, consisting of a port specification, a state, and a 309 service version. A specification, or "spec," is the 2-tuple (number, 310 protocol). So (10, "tcp") corresponds to the port 10/tcp. Port states are 311 strings, or None for "unknown".""" 312 def __init__(self, spec, state=None): 313 self.spec = spec 314 self.state = state 315 self.service = Service() 316 self.script_results = [] 317 318 def state_string(self): 319 if self.state is None: 320 return u"unknown" 321 else: 322 return unicode(self.state) 323 324 def spec_string(self): 325 return u"%d/%s" % self.spec 326 327 def __hash__(self): 328 return hash(self.spec) 329 330 def __cmp__(self, other): 331 d = cmp(self.spec, other.spec) 332 if d != 0: 333 return d 334 return cmp((self.spec, self.service, self.script_results), 335 (other.spec, other.service, other.script_results)) 336 337 def to_dom_fragment(self, document): 338 frag = document.createDocumentFragment() 339 elem = document.createElement(u"port") 340 elem.setAttribute(u"portid", unicode(self.spec[0])) 341 elem.setAttribute(u"protocol", self.spec[1]) 342 if self.state is not None: 343 state_elem = document.createElement(u"state") 344 state_elem.setAttribute(u"state", self.state) 345 elem.appendChild(state_elem) 346 elem.appendChild(self.service.to_dom_fragment(document)) 347 for sr in self.script_results: 348 elem.appendChild(sr.to_dom_fragment(document)) 349 frag.appendChild(elem) 350 return frag 351 352 353class Service(object): 354 """A service version as determined by -sV scan. Also contains the looked-up 355 port name if -sV wasn't used.""" 356 def __init__(self): 357 self.name = None 358 self.product = None 359 self.version = None 360 self.extrainfo = None 361 self.tunnel = None 362 363 # self.hostname = None 364 # self.ostype = None 365 # self.devicetype = None 366 367 __hash__ = None 368 369 def __eq__(self, other): 370 return self.name == other.name \ 371 and self.product == other.product \ 372 and self.version == other.version \ 373 and self.extrainfo == other.extrainfo 374 375 def __ne__(self, other): 376 return not self.__eq__(other) 377 378 def name_string(self): 379 parts = [] 380 if self.tunnel is not None: 381 parts.append(self.tunnel) 382 if self.name is not None: 383 parts.append(self.name) 384 385 if len(parts) == 0: 386 return None 387 else: 388 return u"/".join(parts) 389 390 def version_string(self): 391 """Get a string like in the VERSION column of Nmap output.""" 392 parts = [] 393 if self.product is not None: 394 parts.append(self.product) 395 if self.version is not None: 396 parts.append(self.version) 397 if self.extrainfo is not None: 398 parts.append(u"(%s)" % self.extrainfo) 399 400 if len(parts) == 0: 401 return None 402 else: 403 return u" ".join(parts) 404 405 def to_dom_fragment(self, document): 406 frag = document.createDocumentFragment() 407 elem = document.createElement(u"service") 408 for attr in (u"name", u"product", u"version", u"extrainfo", u"tunnel"): 409 v = getattr(self, attr) 410 if v is None: 411 continue 412 elem.setAttribute(attr, v) 413 if len(elem.attributes) > 0: 414 frag.appendChild(elem) 415 return frag 416 417 418class ScriptResult(object): 419 def __init__(self): 420 self.id = None 421 self.output = None 422 423 __hash__ = None 424 425 def __eq__(self, other): 426 return self.id == other.id and self.output == other.output 427 428 def __ne__(self, other): 429 return not self.__eq__(other) 430 431 def __cmp__(self, other): 432 return cmp((self.id, self.output), (other.id, other.output)) 433 434 def get_lines(self): 435 result = [] 436 lines = self.output.splitlines() 437 if len(lines) > 0: 438 lines[0] = self.id + u": " + lines[0] 439 for line in lines[:-1]: 440 result.append(u"| " + line) 441 if len(lines) > 0: 442 result.append(u"|_ " + lines[-1]) 443 return result 444 445 def to_dom_fragment(self, document): 446 frag = document.createDocumentFragment() 447 elem = document.createElement(u"script") 448 elem.setAttribute(u"id", self.id) 449 elem.setAttribute(u"output", self.output) 450 frag.appendChild(elem) 451 return frag 452 453 454def format_banner(scan): 455 """Format a startup banner more or less like Nmap does.""" 456 scanner = u"Nmap" 457 if scan.scanner is not None and scan.scanner != u"nmap": 458 scanner = scan.scanner 459 parts = [scanner] 460 if scan.version is not None: 461 parts.append(scan.version) 462 parts.append(u"scan") 463 if scan.start_date is not None: 464 parts.append(u"initiated %s" % scan.start_date.strftime( 465 "%a %b %d %H:%M:%S %Y")) 466 if scan.args is not None: 467 parts.append(u"as: %s" % scan.args) 468 return u" ".join(parts) 469 470 471def print_script_result_diffs_text(title, script_results_a, script_results_b, 472 script_result_diffs, f=sys.stdout): 473 table = Table(u"*") 474 for sr_diff in script_result_diffs: 475 sr_diff.append_to_port_table(table) 476 if len(table) > 0: 477 print >> f 478 if len(script_results_b) == 0: 479 print >> f, u"-%s:" % title 480 elif len(script_results_a) == 0: 481 print >> f, u"+%s:" % title 482 else: 483 print >> f, u" %s:" % title 484 print >> f, table 485 486 487def script_result_diffs_to_dom_fragment(elem, script_results_a, 488 script_results_b, script_result_diffs, document): 489 if len(script_results_a) == 0 and len(script_results_b) == 0: 490 return document.createDocumentFragment() 491 elif len(script_results_b) == 0: 492 a_elem = document.createElement(u"a") 493 for sr in script_results_a: 494 elem.appendChild(sr.to_dom_fragment(document)) 495 a_elem.appendChild(elem) 496 return a_elem 497 elif len(script_results_a) == 0: 498 b_elem = document.createElement(u"b") 499 for sr in script_results_b: 500 elem.appendChild(sr.to_dom_fragment(document)) 501 b_elem.appendChild(elem) 502 return b_elem 503 else: 504 for sr_diff in script_result_diffs: 505 elem.appendChild(sr_diff.to_dom_fragment(document)) 506 return elem 507 508 509def host_pairs(a, b): 510 """Take hosts lists a and b, which must be sorted by id, and return pairs. 511 When the heads of both lists have the same ids, they are returned together. 512 Otherwise the one with the smaller id is returned, with an empty host as 513 its counterpart, and the one with the higher id will remain in its list for 514 a later iteration.""" 515 i = 0 516 j = 0 517 while i < len(a) and j < len(b): 518 if a[i].get_id() < b[j].get_id(): 519 yield a[i], Host() 520 i += 1 521 elif a[i].get_id() > b[j].get_id(): 522 yield Host(), b[j] 523 j += 1 524 else: 525 yield a[i], b[j] 526 i += 1 527 j += 1 528 while i < len(a): 529 yield a[i], Host() 530 i += 1 531 while j < len(b): 532 yield Host(), b[j] 533 j += 1 534 535 536class ScanDiff(object): 537 """An abstract class for different diff output types. Subclasses must 538 define various output methods.""" 539 def __init__(self, scan_a, scan_b, f=sys.stdout): 540 """Create a ScanDiff from the "before" scan_a and the "after" 541 scan_b.""" 542 self.scan_a = scan_a 543 self.scan_b = scan_b 544 self.f = f 545 546 def output(self): 547 self.scan_a.sort_hosts() 548 self.scan_b.sort_hosts() 549 550 self.output_beginning() 551 552 pre_script_result_diffs = ScriptResultDiff.diff_lists( 553 self.scan_a.pre_script_results, self.scan_b.pre_script_results) 554 self.output_pre_scripts(pre_script_result_diffs) 555 556 cost = 0 557 # Currently we never consider diffing hosts with a different id 558 # (address or host name), which could lead to better diffs. 559 for host_a, host_b in host_pairs(self.scan_a.hosts, self.scan_b.hosts): 560 h_diff = HostDiff(host_a, host_b) 561 cost += h_diff.cost 562 if h_diff.cost > 0 or verbose: 563 self.output_host_diff(h_diff) 564 565 post_script_result_diffs = ScriptResultDiff.diff_lists( 566 self.scan_a.post_script_results, 567 self.scan_b.post_script_results) 568 self.output_post_scripts(post_script_result_diffs) 569 570 self.output_ending() 571 572 return cost 573 574 575class ScanDiffText(ScanDiff): 576 def __init__(self, scan_a, scan_b, f=sys.stdout): 577 ScanDiff.__init__(self, scan_a, scan_b, f) 578 579 def output_beginning(self): 580 banner_a = format_banner(self.scan_a) 581 banner_b = format_banner(self.scan_b) 582 if banner_a != banner_b: 583 print >> self.f, u"-%s" % banner_a 584 print >> self.f, u"+%s" % banner_b 585 elif verbose: 586 print >> self.f, u" %s" % banner_a 587 588 def output_pre_scripts(self, pre_script_result_diffs): 589 print_script_result_diffs_text("Pre-scan script results", 590 self.scan_a.pre_script_results, self.scan_b.pre_script_results, 591 pre_script_result_diffs, self.f) 592 593 def output_post_scripts(self, post_script_result_diffs): 594 print_script_result_diffs_text("Post-scan script results", 595 self.scan_a.post_script_results, self.scan_b.post_script_results, 596 post_script_result_diffs, self.f) 597 598 def output_host_diff(self, h_diff): 599 print >> self.f 600 h_diff.print_text(self.f) 601 602 def output_ending(self): 603 pass 604 605 606class ScanDiffXML(ScanDiff): 607 def __init__(self, scan_a, scan_b, f=sys.stdout): 608 ScanDiff.__init__(self, scan_a, scan_b, f) 609 610 impl = xml.dom.minidom.getDOMImplementation() 611 self.document = impl.createDocument(None, None, None) 612 613 self.writer = XMLWriter(f) 614 615 def nmaprun_differs(self): 616 for attr in ("scanner", "version", "args", "start_date", "end_date"): 617 if getattr(self.scan_a, attr, None) !=\ 618 getattr(self.scan_b, attr, None): 619 return True 620 return False 621 622 def output_beginning(self): 623 self.writer.startDocument() 624 self.writer.startElement(u"nmapdiff", {u"version": NDIFF_XML_VERSION}) 625 self.writer.startElement(u"scandiff", {}) 626 627 if self.nmaprun_differs(): 628 self.writer.frag_a( 629 self.scan_a.nmaprun_to_dom_fragment(self.document)) 630 self.writer.frag_b( 631 self.scan_b.nmaprun_to_dom_fragment(self.document)) 632 elif verbose: 633 self.writer.frag( 634 self.scan_a.nmaprun_to_dom_fragment(self.document)) 635 636 def output_pre_scripts(self, pre_script_result_diffs): 637 if len(pre_script_result_diffs) > 0 or verbose: 638 prescript_elem = self.document.createElement(u"prescript") 639 frag = script_result_diffs_to_dom_fragment( 640 prescript_elem, self.scan_a.pre_script_results, 641 self.scan_b.pre_script_results, pre_script_result_diffs, 642 self.document) 643 self.writer.frag(frag) 644 frag.unlink() 645 646 def output_post_scripts(self, post_script_result_diffs): 647 if len(post_script_result_diffs) > 0 or verbose: 648 postscript_elem = self.document.createElement(u"postscript") 649 frag = script_result_diffs_to_dom_fragment( 650 postscript_elem, self.scan_a.post_script_results, 651 self.scan_b.post_script_results, post_script_result_diffs, 652 self.document) 653 self.writer.frag(frag) 654 frag.unlink() 655 656 def output_host_diff(self, h_diff): 657 frag = h_diff.to_dom_fragment(self.document) 658 self.writer.frag(frag) 659 frag.unlink() 660 661 def output_ending(self): 662 self.writer.endElement(u"scandiff") 663 self.writer.endElement(u"nmapdiff") 664 self.writer.endDocument() 665 666 667class HostDiff(object): 668 """A diff of two Hosts. It contains the two hosts, variables describing 669 what changed, and a list of PortDiffs and OS differences.""" 670 def __init__(self, host_a, host_b): 671 self.host_a = host_a 672 self.host_b = host_b 673 self.state_changed = False 674 self.id_changed = False 675 self.os_changed = False 676 self.extraports_changed = False 677 self.ports = [] 678 self.port_diffs = {} 679 self.os_diffs = [] 680 self.script_result_diffs = [] 681 self.cost = 0 682 683 self.diff() 684 685 def diff(self): 686 if self.host_a.state != self.host_b.state: 687 self.state_changed = True 688 self.cost += 1 689 690 if set(self.host_a.addresses) != set(self.host_b.addresses) \ 691 or set(self.host_a.hostnames) != set(self.host_b.hostnames): 692 self.id_changed = True 693 self.cost += 1 694 695 all_specs = list( 696 set(self.host_a.ports.keys()).union( 697 set(self.host_b.ports.keys()))) 698 all_specs.sort() 699 for spec in all_specs: 700 # Currently we only compare ports with the same spec. This ignores 701 # the possibility that a service is moved lock, stock, and barrel 702 # to another port. 703 port_a = self.host_a.ports.get(spec) 704 port_b = self.host_b.ports.get(spec) 705 diff = PortDiff(port_a or Port(spec), port_b or Port(spec)) 706 if self.include_diff(diff): 707 port = port_a or port_b 708 self.ports.append(port) 709 self.port_diffs[port] = diff 710 self.cost += diff.cost 711 712 os_diffs = difflib.SequenceMatcher( 713 None, self.host_a.os, self.host_b.os) 714 self.os_diffs = os_diffs.get_opcodes() 715 os_cost = len([x for x in self.os_diffs if x[0] != "equal"]) 716 if os_cost > 0: 717 self.os_changed = True 718 self.cost += os_cost 719 720 extraports_a = tuple((count, state) 721 for (state, count) in self.host_a.extraports.items()) 722 extraports_b = tuple((count, state) 723 for (state, count) in self.host_b.extraports.items()) 724 if extraports_a != extraports_b: 725 self.extraports_changed = True 726 self.cost += 1 727 728 self.script_result_diffs = ScriptResultDiff.diff_lists( 729 self.host_a.script_results, self.host_b.script_results) 730 self.cost += len(self.script_result_diffs) 731 732 def include_diff(self, diff): 733 # Don't include the diff if the states are only extraports. Include all 734 # diffs, even those with cost == 0, in verbose mode. 735 if self.host_a.is_extraports(diff.port_a.state) and \ 736 self.host_b.is_extraports(diff.port_b.state): 737 return False 738 elif verbose: 739 return True 740 return diff.cost > 0 741 742 def print_text(self, f=sys.stdout): 743 host_a = self.host_a 744 host_b = self.host_b 745 746 # Names and addresses. 747 if self.id_changed: 748 if host_a.state is not None: 749 print >> f, u"-%s:" % host_a.format_name() 750 if self.host_b.state is not None: 751 print >> f, u"+%s:" % host_b.format_name() 752 else: 753 print >> f, u" %s:" % host_a.format_name() 754 755 # State. 756 if self.state_changed: 757 if host_a.state is not None: 758 print >> f, u"-Host is %s." % host_a.state 759 if host_b.state is not None: 760 print >> f, u"+Host is %s." % host_b.state 761 elif verbose: 762 print >> f, u" Host is %s." % host_b.state 763 764 # Extraports. 765 if self.extraports_changed: 766 if len(host_a.extraports) > 0: 767 print >> f, u"-Not shown: %s" % host_a.extraports_string() 768 if len(host_b.extraports) > 0: 769 print >> f, u"+Not shown: %s" % host_b.extraports_string() 770 elif verbose: 771 if len(host_a.extraports) > 0: 772 print >> f, u" Not shown: %s" % host_a.extraports_string() 773 774 # Port table. 775 port_table = Table(u"** * * *") 776 if host_a.state is None: 777 mark = u"+" 778 elif host_b.state is None: 779 mark = u"-" 780 else: 781 mark = u" " 782 port_table.append((mark, u"PORT", u"STATE", u"SERVICE", u"VERSION")) 783 784 for port in self.ports: 785 port_diff = self.port_diffs[port] 786 port_diff.append_to_port_table(port_table, host_a, host_b) 787 788 if len(port_table) > 1: 789 print >> f, port_table 790 791 # OS changes. 792 if self.os_changed or verbose: 793 if len(host_a.os) > 0: 794 if len(host_b.os) > 0: 795 print >> f, u" OS details:" 796 else: 797 print >> f, u"-OS details:" 798 elif len(host_b.os) > 0: 799 print >> f, u"+OS details:" 800 # os_diffs is a list of 5-tuples returned by 801 # difflib.SequenceMatcher. 802 for op, i1, i2, j1, j2 in self.os_diffs: 803 if op == "replace" or op == "delete": 804 for i in range(i1, i2): 805 print >> f, "- %s" % host_a.os[i] 806 if op == "replace" or op == "insert": 807 for i in range(j1, j2): 808 print >> f, "+ %s" % host_b.os[i] 809 if op == "equal": 810 for i in range(i1, i2): 811 print >> f, " %s" % host_a.os[i] 812 813 print_script_result_diffs_text("Host script results", 814 host_a.script_results, host_b.script_results, 815 self.script_result_diffs) 816 817 def to_dom_fragment(self, document): 818 host_a = self.host_a 819 host_b = self.host_b 820 821 frag = document.createDocumentFragment() 822 hostdiff_elem = document.createElement(u"hostdiff") 823 frag.appendChild(hostdiff_elem) 824 825 if host_a.state is None or host_b.state is None: 826 # The host is missing in one scan. Output the whole thing. 827 if host_a.state is not None: 828 a_elem = document.createElement(u"a") 829 a_elem.appendChild(host_a.to_dom_fragment(document)) 830 hostdiff_elem.appendChild(a_elem) 831 elif host_b.state is not None: 832 b_elem = document.createElement(u"b") 833 b_elem.appendChild(host_b.to_dom_fragment(document)) 834 hostdiff_elem.appendChild(b_elem) 835 return frag 836 837 host_elem = document.createElement(u"host") 838 839 # State. 840 if host_a.state == host_b.state: 841 if verbose: 842 host_elem.appendChild(host_a.state_to_dom_fragment(document)) 843 else: 844 a_elem = document.createElement(u"a") 845 a_elem.appendChild(host_a.state_to_dom_fragment(document)) 846 host_elem.appendChild(a_elem) 847 b_elem = document.createElement(u"b") 848 b_elem.appendChild(host_b.state_to_dom_fragment(document)) 849 host_elem.appendChild(b_elem) 850 851 # Addresses. 852 addrset_a = set(host_a.addresses) 853 addrset_b = set(host_b.addresses) 854 for addr in sorted(addrset_a.intersection(addrset_b)): 855 host_elem.appendChild(addr.to_dom_fragment(document)) 856 a_elem = document.createElement(u"a") 857 for addr in sorted(addrset_a - addrset_b): 858 a_elem.appendChild(addr.to_dom_fragment(document)) 859 if a_elem.hasChildNodes(): 860 host_elem.appendChild(a_elem) 861 b_elem = document.createElement(u"b") 862 for addr in sorted(addrset_b - addrset_a): 863 b_elem.appendChild(addr.to_dom_fragment(document)) 864 if b_elem.hasChildNodes(): 865 host_elem.appendChild(b_elem) 866 867 # Host names. 868 hostnames_elem = document.createElement(u"hostnames") 869 hostnameset_a = set(host_a.hostnames) 870 hostnameset_b = set(host_b.hostnames) 871 for hostname in sorted(hostnameset_a.intersection(hostnameset_b)): 872 hostnames_elem.appendChild( 873 host_a.hostname_to_dom_fragment(document, hostname)) 874 a_elem = document.createElement(u"a") 875 for hostname in sorted(hostnameset_a - hostnameset_b): 876 a_elem.appendChild( 877 host_a.hostname_to_dom_fragment(document, hostname)) 878 if a_elem.hasChildNodes(): 879 hostnames_elem.appendChild(a_elem) 880 b_elem = document.createElement(u"b") 881 for hostname in sorted(hostnameset_b - hostnameset_a): 882 b_elem.appendChild( 883 host_b.hostname_to_dom_fragment(document, hostname)) 884 if b_elem.hasChildNodes(): 885 hostnames_elem.appendChild(b_elem) 886 if hostnames_elem.hasChildNodes(): 887 host_elem.appendChild(hostnames_elem) 888 889 ports_elem = document.createElement(u"ports") 890 # Extraports. 891 if host_a.extraports == host_b.extraports: 892 ports_elem.appendChild(host_a.extraports_to_dom_fragment(document)) 893 else: 894 a_elem = document.createElement(u"a") 895 a_elem.appendChild(host_a.extraports_to_dom_fragment(document)) 896 ports_elem.appendChild(a_elem) 897 b_elem = document.createElement(u"b") 898 b_elem.appendChild(host_b.extraports_to_dom_fragment(document)) 899 ports_elem.appendChild(b_elem) 900 # Port list. 901 for port in self.ports: 902 p_diff = self.port_diffs[port] 903 if p_diff.cost == 0: 904 if verbose: 905 ports_elem.appendChild(port.to_dom_fragment(document)) 906 else: 907 ports_elem.appendChild(p_diff.to_dom_fragment(document)) 908 if ports_elem.hasChildNodes(): 909 host_elem.appendChild(ports_elem) 910 911 # OS changes. 912 if self.os_changed or verbose: 913 os_elem = document.createElement(u"os") 914 # os_diffs is a list of 5-tuples returned by 915 # difflib.SequenceMatcher. 916 for op, i1, i2, j1, j2 in self.os_diffs: 917 if op == "replace" or op == "delete": 918 a_elem = document.createElement(u"a") 919 for i in range(i1, i2): 920 a_elem.appendChild(host_a.os_to_dom_fragment( 921 document, host_a.os[i])) 922 os_elem.appendChild(a_elem) 923 if op == "replace" or op == "insert": 924 b_elem = document.createElement(u"b") 925 for i in range(j1, j2): 926 b_elem.appendChild(host_b.os_to_dom_fragment( 927 document, host_b.os[i])) 928 os_elem.appendChild(b_elem) 929 if op == "equal": 930 for i in range(i1, i2): 931 os_elem.appendChild(host_a.os_to_dom_fragment( 932 document, host_a.os[i])) 933 if os_elem.hasChildNodes(): 934 host_elem.appendChild(os_elem) 935 936 # Host script changes. 937 if len(self.script_result_diffs) > 0 or verbose: 938 hostscript_elem = document.createElement(u"hostscript") 939 host_elem.appendChild(script_result_diffs_to_dom_fragment( 940 hostscript_elem, host_a.script_results, 941 host_b.script_results, self.script_result_diffs, 942 document)) 943 944 hostdiff_elem.appendChild(host_elem) 945 946 return frag 947 948 949class PortDiff(object): 950 """A diff of two Ports. It contains the two ports and the cost of changing 951 one into the other. If the cost is 0 then the two ports are the same.""" 952 def __init__(self, port_a, port_b): 953 self.port_a = port_a 954 self.port_b = port_b 955 self.script_result_diffs = [] 956 self.cost = 0 957 958 self.diff() 959 960 def diff(self): 961 if self.port_a.spec != self.port_b.spec: 962 self.cost += 1 963 964 if self.port_a.state != self.port_b.state: 965 self.cost += 1 966 967 if self.port_a.service != self.port_b.service: 968 self.cost += 1 969 970 self.script_result_diffs = ScriptResultDiff.diff_lists( 971 self.port_a.script_results, self.port_b.script_results) 972 self.cost += len(self.script_result_diffs) 973 974 # PortDiffs are inserted into a Table and then printed, not printed out 975 # directly. That's why this class has append_to_port_table instead of 976 # print_text. 977 def append_to_port_table(self, table, host_a, host_b): 978 """Append this port diff to a Table containing five columns: 979 +- PORT STATE SERVICE VERSION 980 The "+-" stands for the diff indicator column.""" 981 a_columns = [self.port_a.spec_string(), 982 self.port_a.state_string(), 983 self.port_a.service.name_string(), 984 self.port_a.service.version_string()] 985 b_columns = [self.port_b.spec_string(), 986 self.port_b.state_string(), 987 self.port_b.service.name_string(), 988 self.port_b.service.version_string()] 989 if a_columns == b_columns: 990 if verbose or self.script_result_diffs > 0: 991 table.append([u" "] + a_columns) 992 else: 993 if not host_a.is_extraports(self.port_a.state): 994 table.append([u"-"] + a_columns) 995 if not host_b.is_extraports(self.port_b.state): 996 table.append([u"+"] + b_columns) 997 998 for sr_diff in self.script_result_diffs: 999 sr_diff.append_to_port_table(table) 1000 1001 def to_dom_fragment(self, document): 1002 frag = document.createDocumentFragment() 1003 portdiff_elem = document.createElement(u"portdiff") 1004 frag.appendChild(portdiff_elem) 1005 if (self.port_a.spec == self.port_b.spec and 1006 self.port_a.state == self.port_b.state): 1007 port_elem = document.createElement(u"port") 1008 port_elem.setAttribute(u"portid", unicode(self.port_a.spec[0])) 1009 port_elem.setAttribute(u"protocol", self.port_a.spec[1]) 1010 if self.port_a.state is not None: 1011 state_elem = document.createElement(u"state") 1012 state_elem.setAttribute(u"state", self.port_a.state) 1013 port_elem.appendChild(state_elem) 1014 if self.port_a.service == self.port_b.service: 1015 port_elem.appendChild( 1016 self.port_a.service.to_dom_fragment(document)) 1017 else: 1018 a_elem = document.createElement(u"a") 1019 a_elem.appendChild( 1020 self.port_a.service.to_dom_fragment(document)) 1021 port_elem.appendChild(a_elem) 1022 b_elem = document.createElement(u"b") 1023 b_elem.appendChild( 1024 self.port_b.service.to_dom_fragment(document)) 1025 port_elem.appendChild(b_elem) 1026 for sr_diff in self.script_result_diffs: 1027 port_elem.appendChild(sr_diff.to_dom_fragment(document)) 1028 portdiff_elem.appendChild(port_elem) 1029 else: 1030 a_elem = document.createElement(u"a") 1031 a_elem.appendChild(self.port_a.to_dom_fragment(document)) 1032 portdiff_elem.appendChild(a_elem) 1033 b_elem = document.createElement(u"b") 1034 b_elem.appendChild(self.port_b.to_dom_fragment(document)) 1035 portdiff_elem.appendChild(b_elem) 1036 1037 return frag 1038 1039 1040class ScriptResultDiff(object): 1041 def __init__(self, sr_a, sr_b): 1042 """One of sr_a and sr_b may be None.""" 1043 self.sr_a = sr_a 1044 self.sr_b = sr_b 1045 1046 def diff_lists(a, b): 1047 """Return a list of ScriptResultDiffs from two sorted lists of 1048 ScriptResults.""" 1049 diffs = [] 1050 i = 0 1051 j = 0 1052 # This algorithm is like a merge of sorted lists. 1053 while i < len(a) and j < len(b): 1054 if a[i].id < b[j].id: 1055 diffs.append(ScriptResultDiff(a[i], None)) 1056 i += 1 1057 elif a[i].id > b[j].id: 1058 diffs.append(ScriptResultDiff(None, b[j])) 1059 j += 1 1060 else: 1061 if a[i].output != b[j].output or verbose: 1062 diffs.append(ScriptResultDiff(a[i], b[j])) 1063 i += 1 1064 j += 1 1065 while i < len(a): 1066 diffs.append(ScriptResultDiff(a[i], None)) 1067 i += 1 1068 while j < len(b): 1069 diffs.append(ScriptResultDiff(None, b[j])) 1070 j += 1 1071 return diffs 1072 diff_lists = staticmethod(diff_lists) 1073 1074 # Script result diffs are appended to a port table rather than being 1075 # printed directly, so append_to_port_table exists instead of print_text. 1076 def append_to_port_table(self, table): 1077 a_lines = [] 1078 b_lines = [] 1079 if self.sr_a is not None: 1080 a_lines = self.sr_a.get_lines() 1081 if self.sr_b is not None: 1082 b_lines = self.sr_b.get_lines() 1083 if a_lines != b_lines or verbose: 1084 diffs = difflib.SequenceMatcher(None, a_lines, b_lines) 1085 for op, i1, i2, j1, j2 in diffs.get_opcodes(): 1086 if op == "replace" or op == "delete": 1087 for k in range(i1, i2): 1088 table.append_raw(u"-" + a_lines[k]) 1089 if op == "replace" or op == "insert": 1090 for k in range(j1, j2): 1091 table.append_raw(u"+" + b_lines[k]) 1092 if op == "equal": 1093 for k in range(i1, i2): 1094 table.append_raw(u" " + a_lines[k]) 1095 1096 def to_dom_fragment(self, document): 1097 frag = document.createDocumentFragment() 1098 if (self.sr_a is not None and 1099 self.sr_b is not None and 1100 self.sr_a == self.sr_b): 1101 frag.appendChild(self.sr_a.to_dom_fragment(document)) 1102 else: 1103 if self.sr_a is not None: 1104 a_elem = document.createElement(u"a") 1105 a_elem.appendChild(self.sr_a.to_dom_fragment(document)) 1106 frag.appendChild(a_elem) 1107 if self.sr_b is not None: 1108 b_elem = document.createElement(u"b") 1109 b_elem.appendChild(self.sr_b.to_dom_fragment(document)) 1110 frag.appendChild(b_elem) 1111 return frag 1112 1113 1114class Table(object): 1115 """A table of character data, like NmapOutputTable.""" 1116 def __init__(self, template): 1117 """template is a string consisting of "*" and other characters. Each 1118 "*" is a left-justified space-padded field. All other characters are 1119 copied to the output.""" 1120 self.widths = [] 1121 self.rows = [] 1122 self.prefix = u"" 1123 self.padding = [] 1124 j = 0 1125 while j < len(template) and template[j] != "*": 1126 j += 1 1127 self.prefix = template[:j] 1128 j += 1 1129 i = j 1130 while j < len(template): 1131 while j < len(template) and template[j] != "*": 1132 j += 1 1133 self.padding.append(template[i:j]) 1134 j += 1 1135 i = j 1136 1137 def append(self, row): 1138 strings = [] 1139 1140 row = list(row) 1141 # Remove trailing Nones. 1142 while len(row) > 0 and row[-1] is None: 1143 row.pop() 1144 1145 for i in range(len(row)): 1146 if row[i] is None: 1147 s = u"" 1148 else: 1149 s = str(row[i]) 1150 if i == len(self.widths): 1151 self.widths.append(len(s)) 1152 elif len(s) > self.widths[i]: 1153 self.widths[i] = len(s) 1154 strings.append(s) 1155 self.rows.append(strings) 1156 1157 def append_raw(self, s): 1158 """Append a raw string for a row that is not formatted into columns.""" 1159 self.rows.append(s) 1160 1161 def __len__(self): 1162 return len(self.rows) 1163 1164 def __str__(self): 1165 lines = [] 1166 for row in self.rows: 1167 parts = [self.prefix] 1168 i = 0 1169 if isinstance(row, basestring): 1170 # A raw string. 1171 lines.append(row) 1172 else: 1173 while i < len(row): 1174 parts.append(row[i].ljust(self.widths[i])) 1175 if i < len(self.padding): 1176 parts.append(self.padding[i]) 1177 i += 1 1178 lines.append(u"".join(parts).rstrip()) 1179 return u"\n".join(lines) 1180 1181 1182def warn(str): 1183 """Print a warning to stderr.""" 1184 print >> sys.stderr, str 1185 1186 1187class NmapContentHandler(xml.sax.handler.ContentHandler): 1188 """The xml.sax ContentHandler for the XML parser. It contains a Scan object 1189 that is filled in and can be read back again once the parse method is 1190 finished.""" 1191 def __init__(self, scan): 1192 xml.sax.handler.ContentHandler.__init__(self) 1193 self.scan = scan 1194 1195 # We keep a stack of the elements we've seen, pushing on start and 1196 # popping on end. 1197 self.element_stack = [] 1198 1199 self.current_host = None 1200 self.current_port = None 1201 self.skip_over = False 1202 1203 self._start_elem_handlers = { 1204 u"nmaprun": self._start_nmaprun, 1205 u"host": self._start_host, 1206 u"hosthint": self._start_hosthint, 1207 u"status": self._start_status, 1208 u"address": self._start_address, 1209 u"hostname": self._start_hostname, 1210 u"extraports": self._start_extraports, 1211 u"port": self._start_port, 1212 u"state": self._start_state, 1213 u"service": self._start_service, 1214 u"script": self._start_script, 1215 u"osmatch": self._start_osmatch, 1216 u"finished": self._start_finished, 1217 } 1218 self._end_elem_handlers = { 1219 u'host': self._end_host, 1220 u"hosthint": self._end_hosthint, 1221 u'port': self._end_port, 1222 } 1223 1224 def parent_element(self): 1225 """Return the name of the element containing the current one, or None 1226 if this is the root element.""" 1227 if len(self.element_stack) == 0: 1228 return None 1229 return self.element_stack[-1] 1230 1231 def startElement(self, name, attrs): 1232 """This method keeps track of element_stack. The real parsing work is 1233 done in the _start_*() handlers. This is to make it easy for them to 1234 bail out on error.""" 1235 handler = self._start_elem_handlers.get(name) 1236 if handler is not None and not self.skip_over: 1237 handler(name, attrs) 1238 self.element_stack.append(name) 1239 1240 def endElement(self, name): 1241 """This method keeps track of element_stack. The real parsing work is 1242 done in _end_*() handlers.""" 1243 self.element_stack.pop() 1244 handler = self._end_elem_handlers.get(name) 1245 if handler is not None: 1246 handler(name) 1247 1248 def _start_nmaprun(self, name, attrs): 1249 assert self.parent_element() is None 1250 if "start" in attrs: 1251 start_timestamp = int(attrs.get(u"start")) 1252 self.scan.start_date = datetime.datetime.fromtimestamp( 1253 start_timestamp) 1254 self.scan.scanner = attrs.get(u"scanner") 1255 self.scan.args = attrs.get(u"args") 1256 self.scan.version = attrs.get(u"version") 1257 1258 def _start_host(self, name, attrs): 1259 assert self.parent_element() == u"nmaprun" 1260 self.current_host = Host() 1261 self.scan.hosts.append(self.current_host) 1262 1263 def _start_hosthint(self, name, attrs): 1264 assert self.parent_element() == u"nmaprun" 1265 self.skip_over = True 1266 1267 def _start_status(self, name, attrs): 1268 assert self.parent_element() == u"host" 1269 assert self.current_host is not None 1270 state = attrs.get(u"state") 1271 if state is None: 1272 warn(u'%s element of host %s is missing the "state" attribute; ' 1273 'assuming \unknown\.' % ( 1274 name, self.current_host.format_name())) 1275 return 1276 self.current_host.state = state 1277 1278 def _start_address(self, name, attrs): 1279 assert self.parent_element() == u"host" 1280 assert self.current_host is not None 1281 addr = attrs.get(u"addr") 1282 if addr is None: 1283 warn(u'%s element of host %s is missing the "addr" ' 1284 'attribute; skipping.' % ( 1285 name, self.current_host.format_name())) 1286 return 1287 addrtype = attrs.get(u"addrtype", u"ipv4") 1288 self.current_host.add_address(Address.new(addrtype, addr)) 1289 1290 def _start_hostname(self, name, attrs): 1291 assert self.parent_element() == u"hostnames" 1292 assert self.current_host is not None 1293 hostname = attrs.get(u"name") 1294 if hostname is None: 1295 warn(u'%s element of host %s is missing the "name" ' 1296 'attribute; skipping.' % ( 1297 name, self.current_host.format_name())) 1298 return 1299 self.current_host.add_hostname(hostname) 1300 1301 def _start_extraports(self, name, attrs): 1302 assert self.parent_element() == u"ports" 1303 assert self.current_host is not None 1304 state = attrs.get(u"state") 1305 if state is None: 1306 warn(u'%s element of host %s is missing the "state" ' 1307 'attribute; assuming "unknown".' % ( 1308 name, self.current_host.format_name())) 1309 state = None 1310 if state in self.current_host.extraports: 1311 warn(u'Duplicate extraports state "%s" in host %s.' % ( 1312 state, self.current_host.format_name())) 1313 1314 count = attrs.get(u"count") 1315 if count is None: 1316 warn(u'%s element of host %s is missing the "count" ' 1317 'attribute; assuming 0.' % ( 1318 name, self.current_host.format_name())) 1319 count = 0 1320 else: 1321 try: 1322 count = int(count) 1323 except ValueError: 1324 warn(u"Can't convert extraports count \"%s\" " 1325 "to an integer in host %s; assuming 0." % ( 1326 attrs[u"count"], self.current_host.format_name())) 1327 count = 0 1328 self.current_host.extraports[state] = count 1329 1330 def _start_port(self, name, attrs): 1331 assert self.parent_element() == u"ports" 1332 assert self.current_host is not None 1333 portid_str = attrs.get(u"portid") 1334 if portid_str is None: 1335 warn(u'%s element of host %s missing the "portid" ' 1336 'attribute; skipping.' % ( 1337 name, self.current_host.format_name())) 1338 return 1339 try: 1340 portid = int(portid_str) 1341 except ValueError: 1342 warn(u"Can't convert portid \"%s\" to an integer " 1343 "in host %s; skipping port." % ( 1344 portid_str, self.current_host.format_name())) 1345 return 1346 protocol = attrs.get(u"protocol") 1347 if protocol is None: 1348 warn(u'%s element of host %s missing the "protocol" ' 1349 'attribute; skipping.' % ( 1350 name, self.current_host.format_name())) 1351 return 1352 self.current_port = Port((portid, protocol)) 1353 1354 def _start_state(self, name, attrs): 1355 assert self.parent_element() == u"port" 1356 assert self.current_host is not None 1357 if self.current_port is None: 1358 return 1359 if "state" not in attrs: 1360 warn(u'%s element of port %s is missing the "state" ' 1361 'attribute; assuming "unknown".' % ( 1362 name, self.current_port.spec_string())) 1363 return 1364 self.current_port.state = attrs[u"state"] 1365 self.current_host.add_port(self.current_port) 1366 1367 def _start_service(self, name, attrs): 1368 assert self.parent_element() == u"port" 1369 assert self.current_host is not None 1370 if self.current_port is None: 1371 return 1372 self.current_port.service.name = attrs.get(u"name") 1373 self.current_port.service.product = attrs.get(u"product") 1374 self.current_port.service.version = attrs.get(u"version") 1375 self.current_port.service.extrainfo = attrs.get(u"extrainfo") 1376 self.current_port.service.tunnel = attrs.get(u"tunnel") 1377 1378 def _start_script(self, name, attrs): 1379 result = ScriptResult() 1380 result.id = attrs.get(u"id") 1381 if result.id is None: 1382 warn(u'%s element missing the "id" attribute; skipping.' % name) 1383 return 1384 1385 result.output = attrs.get(u"output") 1386 if result.output is None: 1387 warn(u'%s element missing the "output" attribute; skipping.' 1388 % name) 1389 return 1390 if self.parent_element() == u"prescript": 1391 self.scan.pre_script_results.append(result) 1392 elif self.parent_element() == u"postscript": 1393 self.scan.post_script_results.append(result) 1394 elif self.parent_element() == u"hostscript": 1395 self.current_host.script_results.append(result) 1396 elif self.parent_element() == u"port": 1397 self.current_port.script_results.append(result) 1398 else: 1399 warn(u"%s element not inside prescript, postscript, hostscript, " 1400 "or port element; ignoring." % name) 1401 return 1402 1403 def _start_osmatch(self, name, attrs): 1404 assert self.parent_element() == u"os" 1405 assert self.current_host is not None 1406 if "name" not in attrs: 1407 warn(u'%s element of host %s is missing the "name" ' 1408 'attribute; skipping.' % ( 1409 name, self.current_host.format_name())) 1410 return 1411 self.current_host.os.append(attrs[u"name"]) 1412 1413 def _start_finished(self, name, attrs): 1414 assert self.parent_element() == u"runstats" 1415 if "time" in attrs: 1416 end_timestamp = int(attrs.get(u"time")) 1417 self.scan.end_date = datetime.datetime.fromtimestamp(end_timestamp) 1418 1419 def _end_host(self, name): 1420 self.current_host.script_results.sort() 1421 self.current_host = None 1422 1423 def _end_hosthint(self, name): 1424 self.skip_over = False 1425 1426 def _end_port(self, name): 1427 self.current_port.script_results.sort() 1428 self.current_port = None 1429 1430 1431class XMLWriter (xml.sax.saxutils.XMLGenerator): 1432 def __init__(self, f): 1433 xml.sax.saxutils.XMLGenerator.__init__(self, f, "utf-8") 1434 self.f = f 1435 1436 def frag(self, frag): 1437 for node in frag.childNodes: 1438 node.writexml(self.f, newl=u"\n") 1439 1440 def frag_a(self, frag): 1441 self.startElement(u"a", {}) 1442 for node in frag.childNodes: 1443 node.writexml(self.f, newl=u"\n") 1444 self.endElement(u"a") 1445 1446 def frag_b(self, frag): 1447 self.startElement(u"b", {}) 1448 for node in frag.childNodes: 1449 node.writexml(self.f, newl=u"\n") 1450 self.endElement(u"b") 1451 1452 1453def usage(): 1454 print u"""\ 1455Usage: %s [option] FILE1 FILE2 1456Compare two Nmap XML files and display a list of their differences. 1457Differences include host state changes, port state changes, and changes to 1458service and OS detection. 1459 1460 -h, --help display this help 1461 -v, --verbose also show hosts and ports that haven't changed. 1462 --text display output in text format (default) 1463 --xml display output in XML format\ 1464""" % sys.argv[0] 1465 1466EXIT_EQUAL = 0 1467EXIT_DIFFERENT = 1 1468EXIT_ERROR = 2 1469 1470 1471def usage_error(msg): 1472 print >> sys.stderr, u"%s: %s" % (sys.argv[0], msg) 1473 print >> sys.stderr, u"Try '%s -h' for help." % sys.argv[0] 1474 sys.exit(EXIT_ERROR) 1475 1476 1477def main(): 1478 global verbose 1479 output_format = None 1480 1481 try: 1482 opts, input_filenames = getopt.gnu_getopt( 1483 sys.argv[1:], "hv", ["help", "text", "verbose", "xml"]) 1484 except getopt.GetoptError, e: 1485 usage_error(e.msg) 1486 for o, a in opts: 1487 if o == "-h" or o == "--help": 1488 usage() 1489 sys.exit(0) 1490 elif o == "-v" or o == "--verbose": 1491 verbose = True 1492 elif o == "--text": 1493 if output_format is not None and output_format != "text": 1494 usage_error(u"contradictory output format options.") 1495 output_format = "text" 1496 elif o == "--xml": 1497 if output_format is not None and output_format != "xml": 1498 usage_error(u"contradictory output format options.") 1499 output_format = "xml" 1500 1501 if len(input_filenames) != 2: 1502 usage_error(u"need exactly two input filenames.") 1503 1504 if output_format is None: 1505 output_format = "text" 1506 1507 filename_a = input_filenames[0] 1508 filename_b = input_filenames[1] 1509 1510 try: 1511 scan_a = Scan() 1512 scan_a.load_from_file(filename_a) 1513 scan_b = Scan() 1514 scan_b.load_from_file(filename_b) 1515 except IOError, e: 1516 print >> sys.stderr, u"Can't open file: %s" % str(e) 1517 sys.exit(EXIT_ERROR) 1518 1519 if output_format == "text": 1520 diff = ScanDiffText(scan_a, scan_b) 1521 elif output_format == "xml": 1522 diff = ScanDiffXML(scan_a, scan_b) 1523 cost = diff.output() 1524 1525 if cost == 0: 1526 return EXIT_EQUAL 1527 else: 1528 return EXIT_DIFFERENT 1529 1530 1531# Catch uncaught exceptions so they can produce an exit code of 2 (EXIT_ERROR), 1532# not 1 like they would by default. 1533def excepthook(type, value, tb): 1534 sys.__excepthook__(type, value, tb) 1535 sys.exit(EXIT_ERROR) 1536 1537if __name__ == "__main__": 1538 sys.excepthook = excepthook 1539 sys.exit(main()) 1540