diff --git a/ndiff/README b/ndiff/README index ac7861422..26715fba9 100644 --- a/ndiff/README +++ b/ndiff/README @@ -22,15 +22,19 @@ Here is a sample of the text output: Host is up, was unknown. Add ipv4 address 10.214.143.33. Add hostname cuvtdnray-504.example.com. - 3389/tcp is open. + +3389/tcp open microsoft-rdp Microsoft Terminal Service 999 tcp ports are filtered. scnqxez-842.example.com (10.189.71.117): Remove hostname scnqxez-842.example.com. 10.226.19.80: - 21/tcp is open, was filtered. - 23/tcp is open, was filtered. - 80/tcp is open, was filtered. - 8701/tcp is filtered, was open. + -21/tcp filtered + +21/tcp open ftp Netgear broadband router ftpd 1.0 + -23/tcp filtered + +23/tcp open telnet Netgear broadband router admin telnetd + -80/tcp filtered + +80/tcp open http Embedded Allegro RomPager webserver 4.07 UPnP/1.0 (ZyXEL ZyWALL 2) + -8701/tcp open unknown + +8701/tcp filtered ywnleu-108.example.com (10.242.160.155): Host is up, was unknown. Add ipv4 address 10.242.160.155. @@ -40,7 +44,7 @@ Here is a sample of the text output: Host is unknown, was up. Remove ipv4 address 10.65.53.252. Remove hostname fiyrownc-307.example.com. - 8089/tcp is unknown, was open. + -8089/tcp open upnp Microsoft Windows UPnP 999 tcp ports changed state from filtered to unknown. Here is an abbreviated sample of the XML output: diff --git a/ndiff/docs/ndiff.dtd b/ndiff/docs/ndiff.dtd index ddbba1230..45915fb4f 100644 --- a/ndiff/docs/ndiff.dtd +++ b/ndiff/docs/ndiff.dtd @@ -135,8 +135,25 @@ The port identified by the portid and protocol attributes changed state from that given by the a-state attribute to that given by the b-state attribute. --> - + + + + + + + + diff --git a/ndiff/ndiff b/ndiff/ndiff index 8e979fb35..d929c8923 100755 --- a/ndiff/ndiff +++ b/ndiff/ndiff @@ -28,18 +28,19 @@ PORT_STATE_CHANGE_CONSOLIDATION_THRESHOLD = 10 PORT_STATE_CHANGE_DOUBLE_CONSOLIDATION_CHAR_THRESHOLD = 80 class Port(object): - """A single port, consisting of a port specification and a state. A - specification, or "spec," is the 2-tuple (number, protocol). So (10, "tcp") - corresponds to the port 10/tcp. Port states are strings.""" + """A single port, consisting of a port specification, a state, and a service + version. A specification, or "spec," is the 2-tuple (number, protocol). So + (10, "tcp") corresponds to the port 10/tcp. Port states are strings.""" # This represents an "unknown" port state, the state a port is in when it # has not been scanned. It must not compare equal with any real Nmap port # state like "open", "closed", etc. For future compatibility's sake, always # compare against Port.UNKNOWN, not the literal string "unknown". UNKNOWN = "unknown" - def __init__(self, spec): + def __init__(self, spec, state = None): self.spec = spec - self.state = Port.UNKNOWN + self.state = state or Port.UNKNOWN + self.service = Service() def get_state_string(self): return Port.state_to_string(self.state) @@ -66,6 +67,43 @@ class PortDict(dict): def __len__(self): raise ValueError(u"__len__ is not defined for objects of type PortDict.") +class Service(object): + """A service version as determined by -sV scan. Also contains the looked-up + port name if -sV wasn't used.""" + def __init__(self): + self.name = None + self.product = None + self.version = None + self.extrainfo = None + # self.hostname = None + # self.ostype = None + # self.devicetype = None + # self.tunnel = None + + def __eq__(self, other): + return self.name == other.name \ + and self.product == other.product \ + and self.version == other.version \ + and self.extrainfo == other.extrainfo + + def to_string(self): + """Get a string like in the SERVICE column of Nmap output.""" + if self.name is None: + return u"" + else: + return self.name + + def version_to_string(self): + """Get a string like in the VERSION column of Nmap output.""" + parts = [] + if self.product is not None: + parts.append(self.product) + if self.version is not None: + parts.append(self.version) + if self.extrainfo is not None: + parts.append(u"(%s)" % self.extrainfo) + return u" ".join(parts) + class Host(object): """A single host, with a state (unknown, up, or down), addresses, and a dict mapping port specs to Ports.""" @@ -112,10 +150,8 @@ class Host(object): return unicode(id(self)) - def add_port(self, spec, state): - """Add a port in the given state.""" - port = self.ports[spec] - port.state = state + def add_port(self, port): + self.ports[port.spec] = port def swap_ports(self, spec_a, spec_b): """Swap the ports given by the two specs. This is used when a service is @@ -295,28 +331,48 @@ class PortIdChangeHunk(DiffHunk): return frag class PortStateChangeHunk(DiffHunk): - def __init__(self, spec, a_state, b_state): + def __init__(self, spec, a_port, b_port): self.spec = spec - self.a_state = a_state - self.b_state = b_state + self.a_port = a_port + self.b_port = b_port def to_string(self): - if self.a_state == Port.UNKNOWN: - return u"%s is %s." % (Port.spec_to_string(self.spec), self.b_state) - else: - return u"%s is %s, was %s." % (Port.spec_to_string(self.spec), self.b_state, self.a_state) + lines = [] + a_str = u"%s %s" % (self.a_port.service.to_string(), self.a_port.service.version_to_string()) + b_str = u"%s %s" % (self.b_port.service.to_string(), self.b_port.service.version_to_string()) + if self.a_port.state != Port.UNKNOWN: + lines.append("-%s %s %s" % (Port.spec_to_string(self.a_port.spec), self.a_port.state, a_str)) + if self.b_port.state != Port.UNKNOWN: + lines.append("+%s %s %s" % (Port.spec_to_string(self.b_port.spec), self.b_port.state, b_str)) + return u"\n".join(lines) + + def service_elem(service, document, name): + """Create a service element.""" + elem = document.createElement(name) + if service.name is not None: + elem.setAttribute(u"name", service.name) + if service.product is not None: + elem.setAttribute(u"product", service.product) + if service.version is not None: + elem.setAttribute(u"version", service.version) + if service.extrainfo is not None: + elem.setAttribute(u"extrainfo", service.extrainfo) + return elem + service_elem = staticmethod(service_elem) def to_dom_fragment(self, document): frag = document.createDocumentFragment() elem = document.createElement(u"port-state-change") elem.setAttribute(u"portid", unicode(self.spec[0])) elem.setAttribute(u"protocol", self.spec[1]) - elem.setAttribute(u"a-state", self.a_state) - elem.setAttribute(u"b-state", self.b_state) + elem.setAttribute(u"a-state", self.a_port.state) + elem.setAttribute(u"b-state", self.b_port.state) frag.appendChild(elem) + if not self.a_port.service == self.b_port.service: + elem.appendChild(self.service_elem(self.a_port.service, document, u"a-service")) + elem.appendChild(self.service_elem(self.b_port.service, document, u"b-service")) return frag - def partition_port_state_changes(diff): """Partition a list of PortStateChangeHunks into equivalence classes based on the tuple (protocol, a_state, b_state). The partition is returned @@ -325,8 +381,8 @@ def partition_port_state_changes(diff): for hunk in diff: if not isinstance(hunk, PortStateChangeHunk): continue - a_state = hunk.a_state - b_state = hunk.b_state + a_state = hunk.a_port.state + b_state = hunk.b_port.state protocol = hunk.spec[1] transitions.setdefault((protocol, a_state, b_state), []).append(hunk) return transitions.values() @@ -373,10 +429,10 @@ class ScanDiff(object): h_diff_copy = h_diff[:] cons_port_state_changes = consolidate_port_state_changes(h_diff_copy, PORT_STATE_CHANGE_CONSOLIDATION_THRESHOLD) for hunk in h_diff_copy: - print >> f, u"\t" + hunk.to_string(); + print >> f, u"\n".join(u"\t" + s for s in hunk.to_string().split(u"\n")); for group in cons_port_state_changes: - a_state = group[0].a_state - b_state = group[0].b_state + a_state = group[0].a_port.state + b_state = group[0].b_port.state protocol = group[0].spec[1] port_list = [hunk.spec[0] for hunk in group] port_list_string = render_port_list(port_list) @@ -428,8 +484,8 @@ def port_diff(a, b): if a.spec != b.spec: hunk = PortIdChangeHunk(a.spec, b.spec) diff.append(hunk) - if a.state != b.state: - hunk = PortStateChangeHunk(b.spec, a.state, b.state) + if not (a.state == b.state and a.service == b.service): + hunk = PortStateChangeHunk(b.spec, a, b) diff.append(hunk) return diff @@ -558,7 +614,7 @@ class NmapContentHandler(xml.sax.handler.ContentHandler): self.scanned_ports = {} self.current_host = None self.current_extraports = [] - self.current_spec = None + self.current_port = None def parent_element(self): """Return the name of the element containing the current one, or None if @@ -654,17 +710,26 @@ class NmapContentHandler(xml.sax.handler.ContentHandler): except KeyError: warn(u"port element of host %s missing the \"protocol\" attribute; skipping." % self.current_host.format_name()) return - self.current_spec = portid, protocol + self.current_port = Port((portid, protocol)) elif name == u"state": assert self.parent_element() == u"port" assert self.current_host is not None - if self.current_spec is None: + if self.current_port is None: return if not attrs.has_key(u"state"): - warn("state element of port %s is missing the \"state\" attribute; assuming \"unknown\"." % Port.spec_to_string(self.current_spec)) + warn("state element of port %s is missing the \"state\" attribute; assuming \"unknown\"." % Port.spec_to_string(self.current_port.spec)) return - state = attrs[u"state"] - self.current_host.add_port(self.current_spec, state) + self.current_port.state = attrs[u"state"] + self.current_host.add_port(self.current_port) + elif name == u"service": + assert self.parent_element() == u"port" + assert self.current_host is not None + if self.current_port is None: + return + self.current_port.service.name = attrs.get(u"name") + self.current_port.service.product = attrs.get(u"product") + self.current_port.service.version = attrs.get(u"version") + self.current_port.service.extrainfo = attrs.get(u"extrainfo") elif name == u"finished": assert self.parent_element() == u"runstats" if attrs.has_key(u"time"): @@ -689,12 +754,12 @@ class NmapContentHandler(xml.sax.handler.ContentHandler): if spec in known_specs: continue assert self.current_host.ports[spec].state == Port.UNKNOWN - self.current_host.add_port(spec, extraports_state) + self.current_host.add_port(Port(spec, state = extraports_state)) self.current_host = None self.current_extraports = [] elif name == u"port": - self.current_spec = None + self.current_port = None def usage(): print u"""\ diff --git a/ndiff/ndifftest.py b/ndiff/ndifftest.py index 5266d5db5..202bdb357 100755 --- a/ndiff/ndifftest.py +++ b/ndiff/ndifftest.py @@ -77,9 +77,9 @@ class partition_port_state_changes_test(unittest.TestCase): for host, h_diff in self.diff: partition = partition_port_state_changes(h_diff) for group in partition: - key = (group[0].spec[1], group[0].a_state, group[0].b_state) + key = (group[0].spec[1], group[0].a_port.state, group[0].b_port.state) for hunk in group: - self.assertTrue(key == (hunk.spec[1], hunk.a_state, hunk.b_state)) + self.assertTrue(key == (hunk.spec[1], hunk.a_port.state, hunk.b_port.state)) class consolidate_port_state_changes_test(unittest.TestCase): """Test the consolidate_port_state_changes function.""" @@ -151,6 +151,34 @@ class port_diff_test(unittest.TestCase): diff = port_diff(a, b) self.assertTrue(len(diff) > 1) +class service_test(unittest.TestCase): + """Test the Service class.""" + def test_to_string(self): + serv = Service() + self.assertTrue(serv.to_string() == u"") + serv.name = u"ftp" + self.assertTrue(serv.to_string() == serv.name) + + def test_version_to_string(self): + serv = Service() + self.assertTrue(serv.version_to_string() == u"") + serv = Service() + serv.product = u"FooBar" + self.assertTrue(len(serv.version_to_string()) > 0) + serv = Service() + serv.version = u"1.2.3" + self.assertTrue(len(serv.version_to_string()) > 0) + serv = Service() + serv.extrainfo = u"misconfigured" + self.assertTrue(len(serv.version_to_string()) > 0) + serv = Service() + serv.product = u"FooBar" + serv.version = u"1.2.3" + # Must match Nmap output. + self.assertTrue(serv.version_to_string() == u"%s %s" % (serv.product, serv.version)) + serv.extrainfo = u"misconfigured" + self.assertTrue(serv.version_to_string() == u"%s %s (%s)" % (serv.product, serv.version, serv.extrainfo)) + class host_test(unittest.TestCase): """Test the Host class.""" def test_empty(self): @@ -178,15 +206,27 @@ class host_test(unittest.TestCase): spec = (10, "tcp") port = h.ports[spec] self.assertTrue(port.state == Port.UNKNOWN, "Port state is %s, expected %s." % (port.get_state_string(), "unknown")) - h.add_port(spec, "open") + h.add_port(Port(spec, "open")) self.assertTrue(len(h.get_known_ports()) == 1) port = h.ports[spec] self.assertTrue(port.state == "open", "Port state is %s, expected %s." % (port.get_state_string(), "open")) - h.add_port(spec, "closed") + h.add_port(Port(spec, "closed")) self.assertTrue(len(h.get_known_ports()) == 1) port = h.ports[spec] self.assertTrue(port.state == "closed", "Port state is %s, expected %s." % (port.get_state_string(), "closed")) + spec = (22, "tcp") + port = h.ports[spec] + self.assertTrue(port.state == Port.UNKNOWN, "Port state is %s, expected %s." % (port.get_state_string(), "unknown")) + port = Port(spec) + port.state = "open" + port.service.name = "ssh" + h.add_port(port) + self.assertTrue(len(h.get_known_ports()) == 2) + port = h.ports[spec] + self.assertTrue(port.state == "open", "Port state is %s, expected %s." % (port.get_state_string(), "open")) + self.assertTrue(port.service.name == "ssh", "Port service.name is %s, expected %s." % (port.service.name, "ssh")) + def test_swap_ports(self): h = Host() spec_a = (10, "tcp") @@ -196,13 +236,13 @@ class host_test(unittest.TestCase): self.assertTrue(h.ports[spec_b].state == Port.UNKNOWN) self.assertTrue(h.ports[spec_a].spec == spec_a) self.assertTrue(h.ports[spec_b].spec == spec_b) - h.add_port(spec_a, "open") + h.add_port(Port(spec_a, "open")) h.swap_ports(spec_a, spec_b) self.assertTrue(h.ports[spec_a].state == Port.UNKNOWN) self.assertTrue(h.ports[spec_b].state == "open") self.assertTrue(h.ports[spec_a].spec == spec_a) self.assertTrue(h.ports[spec_b].spec == spec_b) - h.add_port(spec_a, "closed") + h.add_port(Port(spec_a, "closed")) h.swap_ports(spec_a, spec_b) self.assertTrue(h.ports[spec_a].state == "open") self.assertTrue(h.ports[spec_b].state == "closed") @@ -227,8 +267,9 @@ def host_apply_diff(host, diff): host.swap_ports(hunk.a_spec, hunk.b_spec) elif isinstance(hunk, PortStateChangeHunk): port = host.ports[hunk.spec] - assert port.state == hunk.a_state - host.add_port(hunk.spec, hunk.b_state) + assert port.state == hunk.a_port.state + host.add_port(Port(hunk.spec, hunk.b_port.state)) + host.ports[hunk.spec].service = hunk.b_port.service else: assert False @@ -245,8 +286,8 @@ class host_diff_test(unittest.TestCase): def test_self(self): h = Host() - h.add_port((10, "tcp"), "open") - h.add_port((22, "tcp"), "closed") + h.add_port(Port((10, "tcp"), "open")) + h.add_port(Port((22, "tcp"), "closed")) diff = host_diff(h, h) self.assertTrue(len(diff) == 0) @@ -277,8 +318,8 @@ class host_diff_test(unittest.TestCase): a = Host() b = Host() spec = (10, "tcp") - a.add_port(spec, "open") - b.add_port(spec, "closed") + a.add_port(Port(spec, "open")) + b.add_port(Port(spec, "closed")) diff = host_diff(a, b) self.assertTrue(len(diff) > 0) for hunk in diff: @@ -287,7 +328,7 @@ class host_diff_test(unittest.TestCase): def test_port_state_change_unknown(self): a = Host() b = Host() - b.add_port((10, "tcp"), "open") + b.add_port(Port((10, "tcp"), "open")) diff = host_diff(a, b) self.assertTrue(len(diff) > 0) for hunk in diff: @@ -300,12 +341,12 @@ class host_diff_test(unittest.TestCase): def test_port_state_change_multi(self): a = Host() b = Host() - a.add_port((10, "tcp"), "open") - a.add_port((20, "tcp"), "closed") - a.add_port((30, "tcp"), "open") - b.add_port((10, "tcp"), "open") - b.add_port((20, "tcp"), "open") - b.add_port((30, "tcp"), "open") + a.add_port(Port((10, "tcp"), "open")) + a.add_port(Port((20, "tcp"), "closed")) + a.add_port(Port((30, "tcp"), "open")) + b.add_port(Port((10, "tcp"), "open")) + b.add_port(Port((20, "tcp"), "open")) + b.add_port(Port((30, "tcp"), "open")) diff = host_diff(a, b) self.assertTrue(len(diff) > 0) for hunk in diff: @@ -377,12 +418,12 @@ class host_diff_test(unittest.TestCase): the hosts become the same.""" a = Host() b = Host() - a.add_port((10, "tcp"), "open") - a.add_port((20, "tcp"), "closed") - a.add_port((40, "udp"), "open|filtered") - b.add_port((10, "tcp"), "open") - b.add_port((30, "tcp"), "open") - a.add_port((40, "udp"), "open") + a.add_port(Port((10, "tcp"), "open")) + a.add_port(Port((20, "tcp"), "closed")) + a.add_port(Port((40, "udp"), "open|filtered")) + b.add_port(Port((10, "tcp"), "open")) + b.add_port(Port((30, "tcp"), "open")) + a.add_port(Port((40, "udp"), "open")) a.hostnames = ["a", "localhost"] a.hostnames = ["b", "localhost", "b.example.com"] diff = host_diff(a, b)