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