diff options
Diffstat (limited to 'zenmap/zenmapCore/NmapOptions.py')
-rw-r--r-- | zenmap/zenmapCore/NmapOptions.py | 1406 |
1 files changed, 1406 insertions, 0 deletions
diff --git a/zenmap/zenmapCore/NmapOptions.py b/zenmap/zenmapCore/NmapOptions.py new file mode 100644 index 0000000..296c7b0 --- /dev/null +++ b/zenmap/zenmapCore/NmapOptions.py @@ -0,0 +1,1406 @@ +#!/usr/bin/env python3 + +# This is an Nmap command line parser. It has two main parts: +# +# getopt_long_only_extras, which is like getopt_long_only with robust +# handling of unknown options. +# +# NmapOptions, a class representing a set of Nmap options. +# +# NmapOptions is the class for external use. NmapOptions.parse parses a list of +# a command followed by command-line arguments. NmapOptions.render returns a +# list of of a command followed by arguments. NmapOptions.parse_string and +# NmapOptions.render_string first split strings into lists, following certain +# quoting rules. +# +# >>> ops = NmapOptions() +# >>> ops.parse(["nmap", "-v", "--script", "safe", "localhost"]) +# >>> ops.executable +# 'nmap' +# >>> ops.target_specs +# ['localhost'] +# >>> ops["-v"] +# 1 +# >>> ops["--script"] +# 'safe' +# +# The command line may be modified by accessing member variables: +# +# >>> ops.executable = "C:\Program Files\Nmap\nmap.exe" +# >>> ops["-v"] = 2 +# >>> ops["-oX"] = "output.xml" +# >>> ops.render() +# ['C:\\Program Files\\Nmap\\nmap.exe', '-v', '-v', '-oX', 'output.xml', +# '--script', 'safe', 'localhost'] +# >>> ops.render_string() +# '"C:\\Program Files\\Nmap\\nmap.exe" -v -v -oX output.xml\ +# --script safe localhost' +# +# A primary design consideration was robust handling of unknown options. That +# gives this code a degree of independence from Nmap's own list of options. If +# an option is added to Nmap but not added here, that option is treated as an +# "extra," an uninterpreted string that is inserted verbatim into the option +# list. Because the unknown option may or may not take an argument, pains are +# taken to avoid interpreting any option ambiguously. +# +# Consider the following case, where -x is an unknown option: +# nmap -x -e eth0 scanme.nmap.org +# If -x, whatever it is, does not take an argument, it is equivalent to +# nmap -e eth0 scanme.nmap.org -x +# that is, a scan of scanme.nmap.org over interface eth0. But if it does take +# an argument, its argument is "-e", and the command line is the same as +# nmap eth0 scanme.nmap.org -x -e +# which is a scan of the two hosts eth0 and scanme.nmap.org, over the default +# interface. In either case scanme.nmap.org is a target but the other arguments +# are ambiguous. To resolve this, once an unknown option is found, all +# following arguments that can be interpreted ambiguously are removed with it +# and placed in the extras, with normal option processing resumed only when +# there is no more ambiguity. This ensures that such options maintain their +# relative order when rendered again to output. In this example "-x -e eth0" +# will always appear in that order, and the -e option will be uninterpreted. +# +# To add a new option, one should do the following: +# 1) Add a test case to the NmapOptionsTest::test_options() method for the new +# option and make sure it initially fails. +# 2) Add the new option to NmapOptions.SHORT_OPTIONS and/or +# NmapOptions.LONG_OPTIONS. +# 3) Add an appropriate case to NmapOptions::handle_result(). This should +# include a line something like +# self[opt] = True +# or, if the option has an argument 'arg': +# self[opt] = arg +# 4) Add an appropriate case to NmapOptions::render() +# This should include a check to make sure the option was set in +# handle_result: +# if self[opt]: +# or, if self[opt] contains arguments +# if self[opt] is not None: +# If the check passed, then opt should be added to opt_list. +# 5) Edit profile_editor.xml to display the new option in the GUI. +# 6) Depending on the option, one may need to edit +# get_option_check_auxiliary_widget in OptionBuilder.py. +# 7) Make sure the test case works now. + +from functools import reduce + + +class option: + """A single option, part of a pool of potential options. It's just a name + and a flag saying if the option takes no argument, if an argument is + optional, or if an argument is required.""" + NO_ARGUMENT = 0 + REQUIRED_ARGUMENT = 1 + OPTIONAL_ARGUMENT = 2 + + def __init__(self, name, has_arg): + self.name = name + self.has_arg = has_arg + + +def split_quoted(s): + """Like str.split, except that no splits occur inside quoted strings, and + quoted strings are unquoted.""" + r = [] + i = 0 + while i < len(s) and s[i].isspace(): + i += 1 + while i < len(s): + part = [] + while i < len(s) and not s[i].isspace(): + c = s[i] + if c == "\"" or c == "'": + begin = c + i += 1 + while i < len(s): + c = s[i] + if c == begin: + i += 1 + break + elif c == "\\": + i += 1 + if i < len(s): + c = s[i] + # Otherwise, ignore the error and leave the backslash + # at the end of the string. + part.append(c) + i += 1 + else: + part.append(c) + i += 1 + r.append("".join(part)) + while i < len(s) and s[i].isspace(): + i += 1 + + return r + + +def maybe_quote(s): + """Return s quoted if it needs to be, otherwise unchanged.""" + for c in s: + if c == "\"" or c == "\\" or c == "'" or c.isspace(): + break + else: + return s + + r = [] + for c in s: + if c == "\"": + r.append("\\\"") + elif c == "\\": + r.append("\\\\") + else: + r.append(c) + + return "\"" + "".join(r) + "\"" + + +def join_quoted(l): + return " ".join([maybe_quote(x) for x in l]) + + +def make_options(short_opts, long_opts): + """Parse a short option specification string and long option tuples into a + list of option objects.""" + options = [] + for name, has_arg in long_opts: + options.append(option(name, has_arg)) + + while len(short_opts) > 0: + name = short_opts[0] + short_opts = short_opts[1:] + assert name != ":" + num_colons = 0 + while len(short_opts) > 0 and short_opts[0] == ":": + short_opts = short_opts[1:] + num_colons += 1 + if num_colons == 0: + has_arg = option.NO_ARGUMENT + elif num_colons == 1: + has_arg = option.REQUIRED_ARGUMENT + else: + has_arg = option.OPTIONAL_ARGUMENT + options.append(option(name, has_arg)) + + return options + +lookup_option_cache = {} + + +def lookup_option(name, options): + """Find an option with the given (possibly abbreviated) name. None is + returned if no options match or if the name is ambiguous (more than one + option matches with no exact match).""" + + # This function turns out to be a huge bottleneck. Therefore we memoize it. + # We hash on the option name and the id of the options list, because lists + # aren't hashable. This means that the options list can't change after the + # first time you call this function, or you will get stale results. Turning + # the list into a tuple and hashing that is too slow. + cache_code = (name, id(options)) + try: + return lookup_option_cache[cache_code] + except KeyError: + pass + + # Nmap treats '_' the same as '-' in long option names. + def canonicalize_name(name): + return name.replace("_", "-") + + name = canonicalize_name(name) + matches = [o for o in options + if canonicalize_name(o.name).startswith(name)] + if len(matches) == 0: + # No match. + lookup_option_cache[cache_code] = None + elif len(matches) == 1: + # Only one match--not an ambiguous abbreviation. + lookup_option_cache[cache_code] = matches[0] + else: + # More than one match--return only an exact match. + for match in matches: + if canonicalize_name(match.name) == name: + lookup_option_cache[cache_code] = match + break + else: + # No exact matches + lookup_option_cache[cache_code] = None + return lookup_option_cache[cache_code] + + +def split_option(cmd_arg, options): + """Split an option into a name, argument (if any), and possible remainder. + It is not an error if the option does not include an argument even though + it is required; the caller must take the argument from the next + command-line argument. The remainder is what's left over after stripping a + single short option that doesn't take an argument. At most one of argument + and remainder will be non-None. + Examples: + >>> split_option("-v", [option("v", option.NO_ARGUMENT)]) + ('v', None, None) + >>> split_option("--min-rate", + ... [option("min-rate", option.REQUIRED_ARGUMENT)]) + ('min-rate', None, None) + >>> split_option("--min-rate=100", + ... [option("min-rate", option.REQUIRED_ARGUMENT)]) + ('min-rate', '100', None) + >>> split_option("-d9", [option("d", option.OPTIONAL_ARGUMENT)]) + ('d', '9', None) + >>> split_option("-AFn", [option("A", option.NO_ARGUMENT)]) + ('A', None, '-Fn') + >>> split_option("-Amin-rate", [option("A", option.NO_ARGUMENT)]) + ('A', None, '-min-rate') + """ + if cmd_arg.startswith("--"): + name = cmd_arg[2:] + index = name.find('=') + if index < 0: + arg = None + else: + name, arg = name[:index], name[index + 1:] + return name, arg, None + elif cmd_arg.startswith("-"): + name = cmd_arg[1:] + # Check for a lone -. + if name == "": + return name, None, None + # First see if it's really a long option (or a single short option). + index = name.find('=') + if index < 0: + arg = None + else: + name, arg = name[:index], name[index + 1:] + if lookup_option(name, options) is not None: + return name, arg, None + # No luck. Must be a short option. + name = cmd_arg[1] + option = lookup_option(name, options) + if option is None: + # An unknown short option. Return the whole thing. + return cmd_arg[1:], None, None + rest = cmd_arg[2:] + if rest == "": + return name, None, None + if option.has_arg == option.NO_ARGUMENT: + return name, None, "-" + rest + else: + return name, rest, None + else: + assert False, cmd_arg + + +def get_option(cmd_args, options): + """Find and return the first option (plus a possible option argument) or + positional argument from the command-line option list in cmd_args. The + return value will have one of the following forms: + * a string, representing a positional argument; + * an (option, argument) pair (argument may be None); + * a (None, extra, ...) tuple, where extra, ... is a chain of an unknown + option and its following arguments that cannot be interpreted + unambiguously; or + * None, at the end of the option list.""" + if len(cmd_args) == 0: + return None + cmd_arg = cmd_args.pop(0) + if cmd_arg == "--": + if len(cmd_args) == 0: + return None + # Grab the positional argument and replace the --. + name = cmd_args[0] + cmd_args[0] = "--" + return name + # A normal positional argument. + if not cmd_arg.startswith("-"): + return cmd_arg + name, arg, remainder = split_option(cmd_arg, options) + if remainder is not None: + cmd_args.insert(0, remainder) + option = lookup_option(name, options) + if option is None: + # Unrecognized option. + if arg is not None: + return (None, cmd_arg) + else: + extras = [None, cmd_arg] + # We found an unknown option but we have a problem--we don't know + # if it takes an argument or not. So what we do is, we simulate + # what would happen both if the option took and argument and if it + # didn't. The sync function does that by calling this function in a + # loop. + rest = sync(cmd_args[1:], cmd_args[:], options) + # rest is the part of the argument list that is the same whether or + # not the unknown option takes an argument. Put everything up until + # rest begins in the extras, then set cmd_args to rest. + extras += cmd_args[0:len(cmd_args) - len(rest)] + del cmd_args[0:len(cmd_args) - len(rest)] + return tuple(extras) + elif option.has_arg == option.NO_ARGUMENT and arg is not None: + # It has an arg but it shouldn't (like --send-ip=5). Treat it as + # an extra. + return (None, cmd_arg) + elif option.has_arg == option.REQUIRED_ARGUMENT and arg is None: + # An argument is required but not yet read. + if len(cmd_args) == 0: + # No more args. Treat it as an extra. + return (None, cmd_arg) + else: + arg = cmd_args.pop(0) + return (option.name, arg) + else: + return (option.name, arg) + + +def sync(a, b, options): + """Given two command-line argument lists, incrementally get an option from + whichever is longer until both lists are equal. Return the resulting + list.""" + while a != b: + if len(a) > len(b): + get_option(a, options) + else: + get_option(b, options) + return a + + +def getopt_long_only_extras(cmd_args, short_opts, long_opts): + """This is a generator version of getopt_long_only that additionally has + robust handling of unknown options. Each of the items in the sequence it + yields will be one of the following: + * a string, representing a positional argument; + * an (option, argument) pair (argument may be None); + * a (None, extra, ...) tuple, where extra, ... is a chain of an unknown + option and its following arguments that cannot be interpreted + unambiguously; or + * None, at the end of the option list.""" + options = make_options(short_opts, long_opts) + # get_option modifies its list of arguments in place. Don't modify the + # original list. + cmd_args_copy = cmd_args[:] + while True: + result = get_option(cmd_args_copy, options) + if result is None: + break + yield result + + +class NmapOptions(object): + SHORT_OPTIONS = "6Ab:D:d::e:Ffg:hi:M:m:nO::o:P:p:RrS:s:T:v::V" + LONG_OPTIONS = ( + ("allports", option.NO_ARGUMENT), + ("append-output", option.NO_ARGUMENT), + ("badsum", option.NO_ARGUMENT), + ("data-length", option.REQUIRED_ARGUMENT), + ("datadir", option.REQUIRED_ARGUMENT), + ("debug", option.OPTIONAL_ARGUMENT), + ("defeat-rst-ratelimit", option.NO_ARGUMENT), + ("dns-servers", option.REQUIRED_ARGUMENT), + ("exclude", option.REQUIRED_ARGUMENT), + ("excludefile", option.REQUIRED_ARGUMENT), + ("fuzzy", option.NO_ARGUMENT), + ("help", option.NO_ARGUMENT), + ("host-timeout", option.REQUIRED_ARGUMENT), + ("iL", option.REQUIRED_ARGUMENT), + ("iR", option.REQUIRED_ARGUMENT), + ("iflist", option.NO_ARGUMENT), + ("initial-rtt-timeout", option.REQUIRED_ARGUMENT), + ("ip-options", option.REQUIRED_ARGUMENT), + ("log-errors", option.NO_ARGUMENT), + ("max-hostgroup", option.REQUIRED_ARGUMENT), + ("max-os-tries", option.REQUIRED_ARGUMENT), + ("max-parallelism", option.REQUIRED_ARGUMENT), + ("max-rate", option.REQUIRED_ARGUMENT), + ("max-retries", option.REQUIRED_ARGUMENT), + ("max-rtt-timeout", option.REQUIRED_ARGUMENT), + ("max-scan-delay", option.REQUIRED_ARGUMENT), + ("min-hostgroup", option.REQUIRED_ARGUMENT), + ("min-parallelism", option.REQUIRED_ARGUMENT), + ("min-rate", option.REQUIRED_ARGUMENT), + ("min-retries", option.REQUIRED_ARGUMENT), + ("min-rtt-timeout", option.REQUIRED_ARGUMENT), + ("mtu", option.REQUIRED_ARGUMENT), + ("no-stylesheet", option.NO_ARGUMENT), + ("oA", option.REQUIRED_ARGUMENT), + ("oG", option.REQUIRED_ARGUMENT), + ("oM", option.REQUIRED_ARGUMENT), + ("oN", option.REQUIRED_ARGUMENT), + ("oS", option.REQUIRED_ARGUMENT), + ("oX", option.REQUIRED_ARGUMENT), + ("open", option.NO_ARGUMENT), + ("osscan-guess", option.NO_ARGUMENT), + ("osscan-limit", option.NO_ARGUMENT), + ("packet-trace", option.NO_ARGUMENT), + ("port-ratio", option.REQUIRED_ARGUMENT), + ("privileged", option.NO_ARGUMENT), + ("randomize-hosts", option.NO_ARGUMENT), + ("reason", option.NO_ARGUMENT), + ("release-memory", option.NO_ARGUMENT), + ("scan-delay", option.REQUIRED_ARGUMENT), + ("scanflags", option.REQUIRED_ARGUMENT), + ("sI", option.REQUIRED_ARGUMENT), + ("script", option.REQUIRED_ARGUMENT), + ("script-args", option.REQUIRED_ARGUMENT), + ("script-trace", option.NO_ARGUMENT), + ("script-updatedb", option.NO_ARGUMENT), + ("script-help", option.REQUIRED_ARGUMENT), + ("send-eth", option.NO_ARGUMENT), + ("send-ip", option.NO_ARGUMENT), + ("servicedb", option.REQUIRED_ARGUMENT), + ("source-port", option.REQUIRED_ARGUMENT), + ("spoof-mac", option.REQUIRED_ARGUMENT), + ("stylesheet", option.REQUIRED_ARGUMENT), + ("system-dns", option.NO_ARGUMENT), + ("timing", option.REQUIRED_ARGUMENT), + ("top-ports", option.REQUIRED_ARGUMENT), + ("traceroute", option.NO_ARGUMENT), + ("ttl", option.REQUIRED_ARGUMENT), + ("unprivileged", option.NO_ARGUMENT), + ("verbose", option.OPTIONAL_ARGUMENT), + ("version", option.NO_ARGUMENT), + ("version-all", option.NO_ARGUMENT), + ("version-intensity", option.REQUIRED_ARGUMENT), + ("version-light", option.NO_ARGUMENT), + ("version-trace", option.NO_ARGUMENT), + ("versiondb", option.REQUIRED_ARGUMENT), + ("webxml", option.NO_ARGUMENT), + ) + + # Sets of options that should be treated as equivalent from the point of + # view of the external interface. For example, ops["--timing"] means the + # same thing as ops["-T"]. + EQUIVALENT_OPTIONS = ( + ("debug", "d"), + ("help", "h"), + ("iL", "i"), + ("max-parallelism", "M"), + ("osscan-guess", "fuzzy"), + ("oG", "oM", "m"), + ("oN", "o"), + ("sP", "sn"), + ("P", "PE", "PI"), + ("PA", "PT"), + ("P0", "PD", "PN", "Pn"), + ("rH", "randomize-hosts"), + ("source-port", "g"), + ("timing", "T"), + ("verbose", "v"), + ("version", "V"), + ) + EQUIVALENCE_MAP = {} + for set in EQUIVALENT_OPTIONS: + base = set[0] + aliases = set[1:] + for alias in aliases: + EQUIVALENCE_MAP[alias] = base + + TIMING_PROFILE_NAMES = { + "paranoid": 0, "sneaky": 1, "polite": 2, + "normal": 3, "aggressive": 4, "insane": 5 + } + + def __init__(self): + self.options = make_options(self.SHORT_OPTIONS, self.LONG_OPTIONS) + + self.clear() + + def clear(self): + self._executable = None + self.target_specs = [] + self.extras = [] + + # This is the internal mapping of option names to values. + self.d = {} + + def _set_executable(self, executable): + self._executable = executable + + executable = property(lambda self: self._executable or "nmap", + _set_executable) + + def canonicalize_name(self, name): + opt, arg, remainder = split_option(name, self.options) + assert remainder is None + if arg is None: + option = lookup_option(opt, self.options) + if option: + option = option.name + else: + option = opt + else: + option = name.lstrip("-") + option = NmapOptions.EQUIVALENCE_MAP.get(option, option) + return option + + def __getitem__(self, key): + return self.d.get(self.canonicalize_name(key)) + + def __setitem__(self, key, value): + self.d[self.canonicalize_name(key)] = value + + def setdefault(self, key, default): + return self.d.setdefault(self.canonicalize_name(key), default) + + def handle_result(self, result): + if isinstance(result, str): + # A positional argument. + self.target_specs.append(result) + return + elif result[0] is None: + # An unknown option. + self.extras.extend(result[1:]) + return + + # A normal option. + opt, arg = result + if opt in ("6", "A", "F", "h", "n", "R", "r", "V"): + self["-" + opt] = True + elif opt in ( + "allports", + "append-output", + "badsum", + "defeat-rst-ratelimit", + "fuzzy", + "help", + "iflist", + "log-errors", + "no-stylesheet", + "open", + "osscan-guess", + "osscan-limit", + "packet-trace", + "privileged", + "randomize-hosts", + "reason", + "release-memory", + "script-trace", + "script-updatedb", + "send-eth", + "send-ip", + "system-dns", + "traceroute", + "unprivileged", + "version", + "version-all", + "version-light", + "version-trace", + "webxml", + ): + self["--" + opt] = True + elif opt in ("b", "D", "e", "g", "i", "iL", "m", "M", "o", "oA", "oG", + "oM", "oN", "oS", "oX", "p", "S", "sI"): + assert arg is not None + if self["-" + opt] is None: + self["-" + opt] = arg + else: + self.extras.extend(("-" + opt, arg)) + elif opt in ( + "datadir", + "data-length", + "dns-servers", + "exclude", + "excludefile", + "host-timeout", + "initial-rtt-timeout", + "ip-options", + "max-hostgroup", + "max-os-tries", + "max-parallelism", + "max-rate", + "max-retries", + "max-rtt-timeout", + "max-scan-delay", + "min-hostgroup", + "min-parallelism", + "min-rate", + "min-retries", + "min-rtt-timeout", + "mtu", + "port-ratio", + "scan-delay", + "scanflags", + "script", + "script-args", + "script-help", + "servicedb", + "source-port", + "spoof-mac", + "stylesheet", + "top-ports", + "ttl", + "versiondb", + "version-intensity", + ): + assert arg is not None + if self["--" + opt] is None: + self["--" + opt] = arg + else: + self.extras.extend(("--" + opt, arg)) + elif opt == "d" or opt == "debug": + if arg is None: + arg = "" + try: + self["-d"] = int(arg) + except ValueError: + if reduce(lambda x, y: x and y, + [z == "d" for z in arg], True): + self.setdefault("-d", 0) + self["-d"] += len(arg) + 1 + else: + self.extras.append("-d%s" % arg) + elif opt == "f": + self.setdefault("-f", 0) + self["-f"] += 1 + elif opt == "iR": + if self["-iR"] is None: + try: + self["-iR"] = int(arg) + except ValueError: + self.extras.extend(("-iR", arg)) + else: + self.extras.extend(("-iR", arg)) + elif opt == "O": + if arg is None: + if self["-O"] is None: + self["-O"] = True + else: + self.extras.append("-O") + else: + if self["-O"] is None: + self["-O"] = arg + else: + self.extras.append("-O%s" % arg) + elif opt == "P": + type, ports = arg[:1], arg[1:] + if (type == "0" or type == "D" or type == "N" or + type == "n" and ports == ""): + self["-Pn"] = True + elif (type == "" or type == "I" or type == "E") and ports == "": + self["-PE"] = True + elif type == "M" and ports == "": + self["-PM"] = True + elif type == "P" and ports == "": + self["-PP"] = True + elif type == "R" and ports == "": + self["-PR"] = True + elif type == "S": + self["-PS"] = ports + elif type == "T" or type == "A": + self["-PA"] = ports + elif type == "U": + self["-PU"] = ports + elif type == "O": + self["-PO"] = ports + elif type == "B": + self["-PB"] = ports + elif type == "Y": + self["-PY"] = ports + else: + self.extras.append("-P%s" % arg) + elif opt == "s": + for type in arg: + if type in "ACFLMNOPRSTUVWXYZn": + self["-s%s" % type] = True + else: + self.extras.append("-s%s" % type) + elif opt == "T" or opt == "timing": + if self["-T"] is None: + try: + self["-T"] = int(arg) + except ValueError: + try: + self["-T"] = self.TIMING_PROFILE_NAMES[arg.lower()] + except KeyError: + self.extras.extend(("-T", arg)) + else: + self.extras.extend(("-T", arg)) + elif opt == "v" or opt == "verbose": + if arg is None: + arg = "" + try: + self["-v"] = int(arg) + if self["-v"] == 0: + self["-v"] = -1 + except ValueError: + if reduce(lambda x, y: x and y, + [z == "v" for z in arg], True): + self.setdefault("-v", 0) + self["-v"] += len(arg) + 1 + else: + self.extras.append("-v%s" % arg) + else: + assert False, (opt, arg) + + def parse(self, opt_list): + self.clear() + + if len(opt_list) > 0: + self.executable = opt_list[0] + + for result in getopt_long_only_extras( + opt_list[1:], self.SHORT_OPTIONS, self.LONG_OPTIONS): + self.handle_result(result) + + def parse_string(self, opt_string): + self.parse(split_quoted(opt_string)) + + def render(self): + opt_list = [] + + for opt in ("-sA", "-sC", "-sF", "-sL", "-sM", "-sN", "-sO", "-sn", + "-sR", "-sS", "-sT", "-sU", "-sV", "-sW", "-sX", "-sY", "-sZ"): + if self[opt]: + opt_list.append(opt) + + if self["-sI"] is not None: + opt_list.extend(("-sI", self["-sI"])) + + for opt in ("-6",): + if self[opt]: + opt_list.append(opt) + + if self["-p"] is not None: + opt_list.extend(("-p", self["-p"])) + + if self["-T"] is not None: + opt_list.append("-T%s" % str(self["-T"])) + + if self["-O"] is not None: + if isinstance(self["-O"], str): + opt_list.append("-O%s" % self["-O"]) + elif self["-O"]: + opt_list.append("-O") + + if self["-A"]: + opt_list.append("-A") + + if self["-d"]: + if self["-d"] == 1: + opt_list.append("-d") + elif self["-d"] > 1: + opt_list.append("-d%s" % self["-d"]) + + if self["-f"]: + opt_list.extend(["-f"] * self["-f"]) + if self["-v"]: + if self["-v"] == -1: + opt_list.append("-v0") + opt_list.extend(["-v"] * self["-v"]) + + if self["-F"]: + opt_list.append("-F") + if self["-n"]: + opt_list.append("-n") + + if self["-iL"] is not None: + opt_list.extend(("-iL", self["-iL"])) + if self["-iR"] is not None: + opt_list.extend(("-iR", str(self["-iR"]))) + + for opt in ("-oA", "-oG", "-oN", "-oS", "-oX"): + if self[opt] is not None: + opt_list.extend((opt, self[opt])) + + for opt in ("--min-hostgroup", "--max-hostgroup", + "--min-parallelism", "--max-parallelism", + "--min-rtt-timeout", "--max-rtt-timeout", + "--initial-rtt-timeout", + "--scan-delay", "--max-scan-delay", + "--min-rate", "--max-rate", + "--max-retries", "--max-os-tries", "--host-timeout"): + if self[opt] is not None: + opt_list.extend((opt, self[opt])) + + for ping_option in ("-Pn", "-PE", "-PM", "-PP", "-PR"): + if self[ping_option]: + opt_list.append(ping_option) + for ping_option in ("-PS", "-PA", "-PU", "-PO", "-PY"): + if self[ping_option] is not None: + opt_list.append(ping_option + self[ping_option]) + if self["-PB"] is not None: + if isinstance(self["-PB"], str): + opt_list.append("-PB" + self["-PB"]) + elif self["-PB"]: + opt_list.append("-PB") + + for opt in ( + "--allports", + "--append-output", + "--badsum", + "--defeat-rst-ratelimit", + "--fuzzy", + "--help", + "--iflist", + "--log-errors", + "--no-stylesheet", + "--open", + "--osscan-guess", + "--osscan-limit", + "--packet-trace", + "--privileged", + "-r", + "-R", + "--randomize-hosts", + "--reason", + "--release-memory", + "--script-trace", + "--script-updatedb", + "--send-eth", + "--send-ip", + "--system-dns", + "--traceroute", + "--unprivileged", + "--version", + "--version-all", + "--version-light", + "--version-trace", + "--webxml", + ): + if self[opt]: + opt_list.append(opt) + + for opt in ( + "-b", + "-D", + "--datadir", + "--data-length", + "--dns-servers", + "-e", + "--exclude", + "--excludefile", + "-g", + "--ip-options", + "--mtu", + "--port-ratio", + "-S", + "--scanflags", + "--script", + "--script-args", + "--script-help", + "--servicedb", + "--spoof-mac", + "--stylesheet", + "--top-ports", + "--ttl", + "--versiondb", + "--version-intensity", + ): + if self[opt] is not None: + opt_list.extend((opt, self[opt])) + + opt_list.extend(self.target_specs) + + opt_list.extend(self.extras) + + return [self.executable] + opt_list + + def render_string(self): + return join_quoted(self.render()) + +import doctest +import unittest + + +class NmapOptionsTest(unittest.TestCase): + def test_clear(self): + """Test that a new object starts without defining any options, that the + clear method removes all options, and that parsing the empty string or + an empty list removes all options.""" + TEST = "nmap -T4 -A -v localhost --webxml" + ops = NmapOptions() + self.assertTrue(len(ops.render()) == 1) + ops.parse_string(TEST) + self.assertFalse(len(ops.render()) == 1) + ops.clear() + self.assertTrue(len(ops.render()) == 1) + ops.parse_string(TEST) + ops.parse_string("") + self.assertEqual(ops.render_string(), "nmap") + ops.parse_string(TEST) + ops.parse([]) + self.assertEqual(ops.render_string(), "nmap") + + def test_default_executable(self): + """Test that there is a default executable member set.""" + ops = NmapOptions() + self.assertNotNull(ops.executable) + + def test_default_executable(self): + """Test that you can set the executable.""" + ops = NmapOptions() + ops.executable = "foo" + self.assertEqual(ops.executable, "foo") + self.assertEqual(ops.render(), ["foo"]) + + def test_render(self): + """Test that the render method returns a list.""" + TEST = "nmap -T4 -A -v localhost --webxml" + ops = NmapOptions() + ops.parse_string(TEST) + self.assertTrue(type(ops.render()) == list, + "type == %s" % type(ops.render)) + + def test_quoted(self): + """Test that strings can be quoted.""" + ops = NmapOptions() + + ops.parse_string('nmap --script ""') + self.assertEqual(ops["--script"], "") + ops.parse_string("nmap --script ''") + self.assertEqual(ops["--script"], "") + + ops.parse_string('nmap --script test one two three') + self.assertEqual(ops["--script"], "test") + self.assertEqual(ops.target_specs, ["one", "two", "three"]) + ops.parse_string('nmap --script "test" one two three') + self.assertEqual(ops["--script"], "test") + self.assertEqual(ops.target_specs, ["one", "two", "three"]) + ops.parse_string('nmap --script "test one" two three') + self.assertEqual(ops["--script"], "test one") + self.assertEqual(ops.target_specs, ["two", "three"]) + ops.parse_string('nmap --script test" one" two three') + self.assertEqual(ops["--script"], "test one") + self.assertEqual(ops.target_specs, ["two", "three"]) + ops.parse_string('nmap --script test" one"""" two" three') + self.assertEqual(ops["--script"], "test one two") + self.assertEqual(ops.target_specs, ["three"]) + + ops.parse_string("nmap --script test one two three") + self.assertEqual(ops["--script"], "test") + self.assertEqual(ops.target_specs, ["one", "two", "three"]) + ops.parse_string("nmap --script 'test' one two three") + self.assertEqual(ops["--script"], "test") + self.assertEqual(ops.target_specs, ["one", "two", "three"]) + ops.parse_string("nmap --script 'test one' two three") + self.assertEqual(ops["--script"], "test one") + self.assertEqual(ops.target_specs, ["two", "three"]) + ops.parse_string("nmap --script test' one' two three") + self.assertEqual(ops["--script"], "test one") + self.assertEqual(ops.target_specs, ["two", "three"]) + ops.parse_string("nmap --script test' one'''' two' three") + self.assertEqual(ops["--script"], "test one two") + self.assertEqual(ops.target_specs, ["three"]) + + ops.parse_string('nmap --script "ab\\\"cd"') + self.assertEqual(ops["--script"], "ab\"cd") + ops.parse_string('nmap --script "ab\\\\cd"') + self.assertEqual(ops["--script"], "ab\\cd") + ops.parse_string('nmap --script "ab\\\'cd"') + self.assertEqual(ops["--script"], "ab'cd") + ops.parse_string("nmap --script 'ab\\\"cd'") + self.assertEqual(ops["--script"], 'ab"cd') + + ops.parse_string('nmap "--script" test') + self.assertEqual(ops["--script"], "test") + ops.parse_string("nmap '--script' test") + self.assertEqual(ops["--script"], "test") + + ops.parse_string('"nmap foo" --script test') + self.assertEqual(ops.executable, "nmap foo") + ops.parse_string("'nmap foo' --script test") + self.assertEqual(ops.executable, "nmap foo") + + def test_render_quoted(self): + """Test that strings that need to be quoted are quoted.""" + ops = NmapOptions() + ops.parse_string('"/path/ /nmap" --script "test one two three"') + self.assertEqual(ops.executable, "/path/ /nmap") + self.assertEqual(ops["--script"], "test one two three") + self.assertEqual(ops.target_specs, []) + s = ops.render_string() + ops.parse_string(s) + self.assertEqual(ops.executable, "/path/ /nmap") + self.assertEqual(ops["--script"], "test one two three") + self.assertEqual(ops.target_specs, []) + + def test_end(self): + """Test that -- ends argument processing.""" + ops = NmapOptions() + ops.parse_string("nmap -v -- -v") + self.assertTrue(ops["-v"] == 1) + self.assertTrue(ops.target_specs == ["-v"]) + + def test_roundtrip(self): + """Test that parsing and re-rendering a previous rendering gives the + same thing as the previous rendering.""" + TESTS = ( + "nmap", + "nmap -v", + "nmap -vv", + "nmap -d -v", + "nmap -d -d", + "nmap -d -v -d", + "nmap localhost", + "nmap -oX - 192.168.0.1 -PS10", + ) + ops = NmapOptions() + for test in TESTS: + ops.parse_string(test) + opt_string_1 = ops.render_string() + ops.parse_string(opt_string_1) + opt_string_2 = ops.render_string() + self.assertEqual(opt_string_1, opt_string_2) + + def test_underscores(self): + """Test that underscores in option names are treated the same as + dashes (and are canonicalized to dashes).""" + ops = NmapOptions() + ops.parse_string("nmap --osscan_guess") + self.assertTrue("--osscan-guess" in ops.render_string()) + + def test_args(self): + """Test potentially tricky argument scenarios.""" + ops = NmapOptions() + ops.parse_string("nmap -d9") + self.assertTrue(len(ops.target_specs) == 0) + self.assertTrue(ops["-d"] == 9, ops["-d"]) + ops.parse_string("nmap -d 9") + self.assertTrue(ops.target_specs == ["9"]) + self.assertTrue(ops["-d"] == 1) + + def test_repetition(self): + """Test options that can be repeated to increase their effect.""" + ops = NmapOptions() + ops.parse_string("nmap -vv") + self.assertTrue(ops["-v"] == 2) + ops.parse_string("nmap -v -v") + self.assertTrue(ops["-v"] == 2) + ops.parse_string("nmap -ff") + self.assertTrue(ops["-f"] == 2) + ops.parse_string("nmap -f -f") + self.assertTrue(ops["-f"] == 2) + # Note: unlike -d, -v doesn't take an optional numeric argument. + ops.parse_string("nmap -d2 -d") + self.assertTrue(ops["-d"] == 3) + + def test_scan_types(self): + """Test that multiple scan types given to the -s option are all + interpreted correctly.""" + ops = NmapOptions() + ops.parse_string("nmap -s") + self.assertTrue(ops.extras == ["-s"]) + ops.parse_string("nmap -sS") + self.assertTrue(ops.extras == []) + self.assertTrue(ops["-sS"]) + self.assertTrue(not ops["-sU"]) + ops.parse_string("nmap -sSU") + self.assertTrue(ops["-sS"]) + self.assertTrue(ops["-sU"]) + + def test_extras(self): + """Test that unknown arguments are correctly recorded. A few subtleties + are tested: + 1. Unknown options are not simply discarded. + 2. When an unknown option is found, any following arguments that could + have a different meaning depending on whether the unknown option + takes an argument are moved with the argument to the extras. + 3. Any arguments moved to the extras are not otherwise interpreted. + 4. Extra options so copied are copied in blocks, keeping their original + ordering with each block.""" + ops = NmapOptions() + + ops.parse_string("nmap --fee") + self.assertTrue(ops.extras == ["--fee"]) + self.assertTrue(ops.render_string() == "nmap --fee") + + # Note: -x is not a real Nmap option. + + ops.parse_string("nmap -x") + self.assertTrue(ops.extras == ["-x"]) + self.assertTrue(ops.render_string() == "nmap -x") + + ops.parse_string("nmap -v --fie scanme.nmap.org -d") + self.assertTrue(ops.extras == ["--fie", "scanme.nmap.org"]) + self.assertTrue(ops["-v"] == 1) + self.assertTrue(ops["-d"] == 1) + self.assertTrue(len(ops.target_specs) == 0) + + ops.parse_string("nmap -v --foe=5 scanme.nmap.org -d") + self.assertTrue(ops.extras == ["--foe=5"]) + self.assertTrue(ops.target_specs == ["scanme.nmap.org"]) + + ops.parse_string("nmap --fum -oX out.xml -v") + self.assertTrue(ops.extras == ["--fum", "-oX", "out.xml"]) + self.assertTrue(ops["-v"] == 1) + + ops.parse_string("nmap -x -A localhost") + self.assertTrue(ops.extras == ["-x", "-A"]) + + ops.parse_string("nmap -x --fee -A localhost") + self.assertTrue(ops.extras == ["-x", "--fee", "-A"]) + + ops.parse_string("nmap -x -x --timing 3 localhost") + self.assertTrue(ops.extras == ["-x", "-x", "--timing", "3"]) + self.assertTrue(ops.target_specs == ["localhost"]) + + ops.parse_string("nmap -x -x --timing=3 localhost") + self.assertTrue(ops.extras == ["-x", "-x", "--timing=3"]) + self.assertTrue(ops.target_specs == ["localhost"]) + + ops.parse_string("nmap -x -Ad9") + self.assertTrue(ops.extras == ["-x", "-Ad9"]) + + ops.parse_string("nmap -xrest") + self.assertTrue(ops.extras == ["-xrest"]) + + # Options that can't be given more than once should end up in extras. + ops.parse_string("nmap -p 53 -p 80 -O --mtu 50 --mtu 100 -O2") + self.assertTrue(ops["-p"] == "53") + self.assertTrue(ops["--mtu"] == "50") + self.assertTrue(ops["-O"]) + self.assertTrue(ops.extras == ["-p", "80", "--mtu", "100", "-O2"]) + + def test_quirks(self): + """Test the handling of constructions whose interpretation isn't + specified in documentation, but should match that of GNU getopt.""" + ops = NmapOptions() + # Long options can be written with one dash. + ops.parse_string("nmap -min-rate 100") + self.assertTrue(ops["--min-rate"] == "100") + ops.parse_string("nmap -min-rate=100") + self.assertTrue(ops["--min-rate"] == "100") + + # Short options not taking an argument can be followed by a long + # option. + ops.parse_string("nmap -nFmin-rate 100") + self.assertTrue(ops["-n"]) + self.assertTrue(ops["-F"]) + self.assertTrue(ops["--min-rate"] == "100") + + # Short options taking an argument consume the rest of the argument. + ops.parse_string("nmap -nFp1-100") + self.assertTrue(ops["-n"]) + self.assertTrue(ops["-F"]) + self.assertTrue(ops["-p"] == "1-100") + + def test_conversion(self): + """Test that failed integer conversions cause the option to wind up in + the extras.""" + ops = NmapOptions() + ops.parse_string("nmap -d#") + self.assertTrue(ops.extras == ["-d#"]) + ops.parse_string("nmap -T monkeys") + self.assertTrue(ops["-T"] is None) + self.assertTrue(ops.extras == ["-T", "monkeys"]) + ops.parse_string("nmap -iR monkeys") + self.assertTrue(ops["-iR"] is None) + self.assertTrue(ops.extras == ["-iR", "monkeys"]) + + def test_read_unknown(self): + """Test that getting the value of non-options returns None.""" + ops = NmapOptions() + self.assertEqual(ops["-x"], None) + self.assertEqual(ops["--nonoption"], None) + + def test_canonical_option_names(self): + """Test that equivalent option names are properly canonicalized, so + that ops["--timing"] and ops["-T"] mean the same thing, for example.""" + EQUIVS = ( + ("--debug", "-d"), + ("--help", "-h"), + ("-iL", "-i"), + ("--max-parallelism", "-M"), + ("--osscan-guess", "--fuzzy"), + ("-oG", "-oM", "-m"), + ("-oN", "-o"), + ("-sP", "-sn"), + ("-P", "-PE", "-PI"), + ("-PA", "-PT"), + ("-P0", "-PD", "-PN", "-Pn"), + ("--source-port", "-g"), + ("--timing", "-T"), + ("--verbose", "-v"), + ("--version", "-V"), + ("--min-rate", "-min-rate", "--min_rate", "-min_rate") + ) + ops = NmapOptions() + for set in EQUIVS: + for opt in set: + ops.clear() + ops[opt] = "test" + for other in set: + self.assertTrue(ops[other] == "test", + "%s and %s not the same" % (opt, other)) + + def test_options(self): + """Test that all options that are supposed to be supported are really + supported. They must be parsed and not as extras, and must produce + output on rendering that can be parsed again.""" + TESTS = ["-" + opt for opt in "6AFfhnRrVv"] + TESTS += ["-b host", "-D 192.168.0.1,ME,RND", "-d", "-d -d", "-d2", + "-e eth0", "-f -f", "-g 53", "-i input.txt", "-M 100", + "-m output.gnmap", "-O", "-O2", "-o output.nmap", "-p 1-100", + "-S 192.168.0.1", "-T0", "-v -v"] + TESTS += ["-s" + opt for opt in "ACFLMNnOPRSTUVWXYZ"] + TESTS += ["-P" + opt for opt in "IEMP0NnDRBSTAUOY"] + TESTS += ["-P" + opt + "100" for opt in "STAUOY"] + TESTS += [ + "--version", + "--verbose", + "--datadir=dir", + "--datadir dir", + "--servicedb=db", + "--servicedb db", + "--versiondb=db", + "--versiondb db", + "--debug", + "--debug=3", + "--debug 3", + "--help", + "--iflist", + "--release-memory", + "--max-os-tries=10", + "--max-os-tries 10", + "--max-parallelism=10", + "--min-parallelism 10", + "--timing=0", + "--timing 0", + "--max-rtt-timeout=10", + "--max-rtt-timeout 10", + "--min-rtt-timeout=10", + "--min-rtt-timeout 10", + "--initial-rtt-timeout=10", + "--initial-rtt-timeout 10", + "--excludefile=file", + "--excludefile file", + "--exclude=192.168.0.0", + "--exclude 192.168.0.0", + "--max-hostgroup=10", + "--max-hostgroup 10", + "--min-hostgroup=10", + "--min-hostgroup 10", + "--open", + "--scanflags=RST,ACK", + "--scanflags RST,ACK", + "--defeat-rst-ratelimit", + "--host-timeout=10", + "--host-timeout 10", + "--scan-delay=10", + "--scan-delay 10", + "--max-scan-delay=10", + "--max-scan-delay 10", + "--max-retries=10", + "--max-retries 10", + "--source-port=53", + "--source-port 53", + "--randomize-hosts", + "--osscan-limit", + "--osscan-guess", + "--fuzzy", + "--packet-trace", + "--version-trace", + "--data-length=10", + "--data-length 10", + "--send-eth", + "--send-ip", + "--stylesheet=style.xml", + "--stylesheet style.xml", + "--no-stylesheet", + "--webxml", + "--privileged", + "--unprivileged", + "--mtu=1500", + "--mtu 1500", + "--append-output", + "--spoof-mac=00:00:00:00:00:00", + "--spoof-mac 00:00:00:00:00:00", + "--badsum", + "--ttl=64", + "--ttl 64", + "--traceroute", + "--reason", + "--allports", + "--version-intensity=5", + "--version-intensity 5", + "--version-light", + "--version-all", + "--system-dns", + "--log-errors", + "--dns-servers=localhost", + "--dns-servers localhost", + "--port-ratio=0.5", + "--port-ratio 0.5", + "--top-ports=1000", + "--top-ports 1000", + "--script=script.nse", + "--script script.nse", + "--script-trace", + "--script-updatedb", + "--script-args=none", + "--script-args none", + "--script-help=script.nse", + "--script-help script.nse", + "--ip-options=S", + "--ip-options S", + "--min-rate=10", + "--min-rate 10", + "--max-rate=10", + "--max-rate 10", + "-iL=input.txt", + "-iL input.txt", + "-iR=1000", + "-iR 1000", + "-oA=out", + "-oA out", + "-oG=out.gnmap", + "-oG out.gnmap", + "-oM=out.gnmap", + "-oM out.gnmap", + "-oN=out.nmap", + "-oN out.nmap", + "-oS=out.skid", + "-oS out.skid", + "-oX=out.xml", + "-oX out.xml", + "-sI=zombie.example.com", + "-sI zombie.example.com", + ] + + # The following options are present in the Nmap source but are not + # tested for because they are deprecated or not documented or whatever. + # "-I", + # "--noninteractive", + # "--thc", + # "--nogcc", + # "-rH", + # "-ff", + # "-vv", + # "-oH", + + ops = NmapOptions() + for test in TESTS: + ops.parse_string("nmap " + test) + opt_list_1 = ops.render() + self.assertTrue(len(opt_list_1) > 1, "%s missing on render" % test) + self.assertTrue(len(ops.extras) == 0, + "%s caused extras: %s" % (test, repr(ops.extras))) + ops.parse(opt_list_1) + opt_list_2 = ops.render() + self.assertTrue(opt_list_1 == opt_list_2, + "Result of parsing and rendering %s not parsable again" % ( + test)) + self.assertTrue(len(ops.extras) == 0, + "Result of parsing and rendering %s left extras: %s" % ( + test, ops.extras)) + + +class SplitQuotedTest(unittest.TestCase): + """A unittest class that tests the split_quoted function.""" + + def test_split(self): + self.assertEqual(split_quoted(''), []) + self.assertEqual(split_quoted('a'), ['a']) + self.assertEqual(split_quoted('a b c'), 'a b c'.split()) + + def test_quotes(self): + self.assertEqual(split_quoted('a "b" c'), ['a', 'b', 'c']) + self.assertEqual(split_quoted('a "b c"'), ['a', 'b c']) + self.assertEqual(split_quoted('a "b c""d e"'), ['a', 'b cd e']) + self.assertEqual(split_quoted('a "b c"z"d e"'), ['a', 'b czd e']) + + def test_backslash(self): + self.assertEqual(split_quoted('"\\""'), ['"']) + self.assertEqual(split_quoted('\\"\\""'), ['\\"']) + self.assertEqual(split_quoted('"\\"\\""'), ['""']) + + +if __name__ == "__main__": + doctest.testmod() + unittest.main() |