2930 - Sanitize sorting and comparisons

Change 1: added missing less-than method for sorting
The ScriptResult class in nmap/ndiff/ndiff.py is not specifying a __lt__ (less-than) method by which the precedence of its instances could be compared when sorting.
This can lead to an error on the sort method call in line 1420.
self.current_port.script_results.sort()
The error occurs when multiple script results are exported to xml and parsed by the underlying xml.sax library.

Change 2: added missing len function call when comparing list to int in conditional statement
Conditional statement in line 983 throws a TypeError when script results have differences for the same port.

Change 3: implemented host object sorting and comparative logic  in OO style
The Host class does now define the equals and less-than methods. This allows for the get_id() method of the Host class to be treated as private and comparisons to be made directly against between instances.
The `Scan` objects list of addresses is not being mutated in-place, but instead wrapped in a `sorted` function call and passed to `host_pairs()` in line 552.


Change 4: Removed OO boilerplate
Python3 classes inherit from the object base class implicitly, therefore `class A (object)` can be reduced to `class A`.
See accepted answer in https://stackoverflow.com/questions/4015417/why-do-python-classes-inherit-object and 

In Python3 the not-equals method implicitly delegates to the equals method. Therefore it is unnecessary to explicitly define `__ne__`  as the negation of the corresponding `__eq__` method. See https://docs.python.org/3/reference/datamodel.html#object.__eq__
This commit is contained in:
Julijan Nedic 2024-09-08 14:10:29 +02:00 committed by GitHub
parent 66bbf3dc96
commit a32d26fad7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -41,7 +41,7 @@ class OverrideEntityResolver(xml.sax.handler.EntityResolver):
return OverrideEntityResolver.empty
class Scan(object):
class Scan:
"""A single Nmap scan, corresponding to a single invocation of Nmap. It is
a container for a list of hosts. It also has utility methods to load itself
from an Nmap XML file."""
@ -55,9 +55,6 @@ class Scan(object):
self.pre_script_results = []
self.post_script_results = []
def sort_hosts(self):
self.hosts.sort(key=lambda h: h.get_id())
def load(self, f):
"""Load a scan from the Nmap XML in the file-like object f."""
parser = xml.sax.make_parser()
@ -107,7 +104,7 @@ class Scan(object):
return frag
class Host(object):
class Host:
"""A single host, with a state, addresses, host names, a dict mapping port
specs to Ports, and a list of OS matches. Host states are strings, or None
for "unknown"."""
@ -120,7 +117,7 @@ class Host(object):
self.os = []
self.script_results = []
def get_id(self):
def _get_id(self):
"""Return an id that is used to determine if hosts are "the same"
across scans."""
hid = None
@ -130,6 +127,12 @@ class Host(object):
return (hid or " " * 40) + str(sorted(self.hostnames)[0])
return hid or id(self)
def __eq__(self, other):
return self._get_id() == other._get_id()
def __lt__(self, other):
return self._get_id() < other._get_id()
def format_name(self):
"""Return a human-readable identifier for this host."""
address_s = ", ".join(a.s for a in sorted(self.addresses))
@ -237,16 +240,13 @@ class Host(object):
return frag
class Address(object):
class Address:
def __init__(self, s):
self.s = s
def __eq__(self, other):
return self.sort_key() == other.sort_key()
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self.sort_key())
@ -303,7 +303,7 @@ class MACAddress(Address):
return (2, self.s)
class Port(object):
class Port:
"""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
@ -346,7 +346,7 @@ class Port(object):
return frag
class Service(object):
class Service:
"""A service version as determined by -sV scan. Also contains the looked-up
port name if -sV wasn't used."""
def __init__(self):
@ -368,9 +368,6 @@ class Service(object):
and self.version == other.version \
and self.extrainfo == other.extrainfo
def __ne__(self, other):
return not self.__eq__(other)
def name_string(self):
parts = []
if self.tunnel is not None:
@ -421,8 +418,8 @@ class ScriptResult(object):
def __eq__(self, other):
return self.id == other.id and self.output == other.output
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
return self.id < other.id
def get_lines(self):
result = []
@ -505,13 +502,12 @@ def host_pairs(a, b):
Otherwise the one with the smaller id is returned, with an empty host as
its counterpart, and the one with the higher id will remain in its list for
a later iteration."""
i = 0
j = 0
i,j = 0,0
while i < len(a) and j < len(b):
if a[i].get_id() < b[j].get_id():
if a[i] < b[j]:
yield a[i], Host()
i += 1
elif a[i].get_id() > b[j].get_id():
elif a[i] > b[j]:
yield Host(), b[j]
j += 1
else:
@ -526,7 +522,7 @@ def host_pairs(a, b):
j += 1
class ScanDiff(object):
class ScanDiff:
"""An abstract class for different diff output types. Subclasses must
define various output methods."""
def __init__(self, scan_a, scan_b, f=sys.stdout):
@ -537,11 +533,7 @@ class ScanDiff(object):
self.f = f
def output(self):
self.scan_a.sort_hosts()
self.scan_b.sort_hosts()
self.output_beginning()
pre_script_result_diffs = ScriptResultDiff.diff_lists(
self.scan_a.pre_script_results, self.scan_b.pre_script_results)
self.output_pre_scripts(pre_script_result_diffs)
@ -549,7 +541,8 @@ class ScanDiff(object):
cost = 0
# Currently we never consider diffing hosts with a different id
# (address or host name), which could lead to better diffs.
for host_a, host_b in host_pairs(self.scan_a.hosts, self.scan_b.hosts):
for host_a, host_b in host_pairs(sorted(self.scan_a.hosts),
sorted(self.scan_b.hosts)):
h_diff = HostDiff(host_a, host_b)
cost += h_diff.cost
if h_diff.cost > 0 or verbose:
@ -657,7 +650,7 @@ class ScanDiffXML(ScanDiff):
self.writer.endDocument()
class HostDiff(object):
class HostDiff:
"""A diff of two Hosts. It contains the two hosts, variables describing
what changed, and a list of PortDiffs and OS differences."""
def __init__(self, host_a, host_b):
@ -939,7 +932,7 @@ class HostDiff(object):
return frag
class PortDiff(object):
class PortDiff:
"""A diff of two Ports. It contains the two ports and the cost of changing
one into the other. If the cost is 0 then the two ports are the same."""
def __init__(self, port_a, port_b):
@ -980,7 +973,7 @@ class PortDiff(object):
self.port_b.service.name_string(),
self.port_b.service.version_string()]
if a_columns == b_columns:
if verbose or self.script_result_diffs > 0:
if verbose or len(self.script_result_diffs) > 0:
table.append([" "] + a_columns)
else:
if not host_a.is_extraports(self.port_a.state):
@ -1030,7 +1023,7 @@ class PortDiff(object):
return frag
class ScriptResultDiff(object):
class ScriptResultDiff:
def __init__(self, sr_a, sr_b):
"""One of sr_a and sr_b may be None."""
self.sr_a = sr_a
@ -1104,7 +1097,7 @@ class ScriptResultDiff(object):
return frag
class Table(object):
class Table:
"""A table of character data, like NmapOutputTable."""
def __init__(self, template):
"""template is a string consisting of "*" and other characters. Each