3
\$\begingroup\$

I'm pretty new to python but after watching Mr Robot I felt inspired to make a CLI portscanner. With all the fancy console outputs you see in movies like a progress bar.

It's finished and works nicely however I am not very happy with having to write twice global host_IP in main() and scan_tcp(). I have to do this the only way I was able to get a progress bar with tqdm was by using executor.map() which as to my understanding can only take the iterator argument unlucky the executor.submit(). Is there a cleaner way to doing this?

I am also interested in any general feedback on the code or performance improvements :)

import socket
import sys
import argparse
import concurrent.futures
import pyfiglet
from tqdm import tqdm

host_IP = None


def resolve_args():
    argv = sys.argv[1:]
    parser = argparse.ArgumentParser()
    parser.add_argument("host", type=_host, help="Hostname or IP of host system to be scanned")
    parser.add_argument("-s", "--startPort", type=_port, default=0, help="Port number to start scan (0-65535)")
    parser.add_argument("-e", "--endPort", type=_port, default=65535, help="Port number to end scan (0-65535)")
    return vars(parser.parse_args(argv))


def _host(s):
    try:
        value = socket.gethostbyname(s)
    except socket.gaierror:
        raise argparse.ArgumentTypeError(f"Host '{s}' could not be resolved.")
    return value


def _port(s):
    try:
        value = int(s)
    except ValueError:
        raise argparse.ArgumentTypeError(f"Expected integer got '{s}'")
    if 0 > value > 65535:
        raise argparse.ArgumentTypeError(f"Port number must be 0-65535, got {s}")
    return value


def scan_tcp(port):
    global host_IP
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socket.setdefaulttimeout(1)
    result = 0
    try:
        answer = sock.connect_ex((host_IP, port))
        if answer == 0:
            result = port
        sock.close()
    except socket.error:
        print(f"Could not connect to host '{host_IP}'")
        sys.exit()
    except KeyboardInterrupt:
        print("Exiting...")
        sys.exit()

    return result


def print_open_ports(results):
    results = list(filter(None, results))
    for open_port in results:
        print(f"Port {open_port} is OPEN")


def main():
    args = resolve_args()
    pyfiglet.print_figlet("PORTSCANNER", font="slant")
    global host_IP
    host_IP = args.get("host")
    ports = range(args.get("startPort"), args.get("endPort") + 1)
    print(f"Starting scan of {host_IP}...")
    with concurrent.futures.ThreadPoolExecutor() as executor:
        results = list(tqdm(executor.map(scan_tcp, ports), total=len(ports)))
    print(f"Finished scan of {host_IP}!")
    print_open_ports(results)


if __name__ == "__main__":
    main()
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

It's finished and works nicely however I am not very happy with having to write twice global host_IP in main() and scan_tcp().

I don't really understand the issue. The variable already has global scope since you assign it a None value on top of your code. Then all you have to do is pass it as a function argument, for example (preferably in lowercase notation):

def scan_tcp(host_ip, port):

But this is just variable duplication, you could downright pass args.get("host") or args.host if you add this: dest="host" in the relevant add_argument statement.

scan_tcp should probably return a boolean value and be named something else eg get_port_state. If the connection is successful you return the port number, otherwise 0 - this is not very consistent and even misleading since 0 is a port too. All you want is a yes/no answer.

Regarding the parsing of arguments you don't need this remnant of the old ways:

argv = sys.argv[1:]

This is sufficient:

parser = argparse.ArgumentParser()
parser.add_argument(...)
parser.add_argument(...)
args = parser.parse_args()

Your script will refuse to run if the host cannot be resolved. Why ? Not every IP address can be resolved to a host name. I would ditch that check.

The approach you've chosen (using socket) is quite rudimentary and is not a reliable test. Because determining what makes a port open or not is not that easy to tell reliably. For your next version you might consider nmap since it can be instrumented in Python and provides many more options. Maybe this topic could interest you: A port scanner made in python. Be aware that even nmap makes educated guesses, and I am not even touching on intrusion detection systems (IDS) or any other measures that may interfere with your scanning.

And of course scapy should definitely interest you.

I am not familiar enough with tqdm.

PS: an interesting article on using socket in Python: Socket Programming in Python (Guide)

\$\endgroup\$
3
  • \$\begingroup\$ Thank you for the feedback. I will definitely look into nmap and scapy. I am slightly confused on the host_ip tho. My issue is that to save the "arg_value" with the host_ip after resolving it and pass it to the scan_tcp function the host_ip variable needs to be global. If possible I would not want host_ip to be a global variable and just pass it as a an argument to scan_tcp however this isn't possible as scan_tcp is being called with the executor.map() function and I therefore cant pass any additional arguments. \$\endgroup\$
    – George R
    Commented Sep 18, 2021 at 9:18
  • \$\begingroup\$ Can't you do something like: executor.map(scan_tcp, ports, host_ip) to provide more than one function argument or am I missing something ? \$\endgroup\$
    – Kate
    Commented Sep 18, 2021 at 16:27
  • \$\begingroup\$ As i understand from the documentation u cant, docs.python.org/3/library/concurrent.futures.html submit u can tho... but it has to be with the map function otherwise the loading bar wont work \$\endgroup\$
    – George R
    Commented Sep 19, 2021 at 11:55
2
\$\begingroup\$

Your

try:
    value = socket.gethostbyname(s)
except socket.gaierror:
    raise argparse.ArgumentTypeError(f"Host '{s}' could not be resolved.")
return value

should probably be

try:
    return socket.gethostbyname(s)
except socket.gaierror as e:
    raise argparse.ArgumentTypeError(f"Host '{s}' could not be resolved.") from e

in other words, it's usually a good idea to maintain rather than break the exception cause chain.

I would move your except KeyboardInterrupt up to a try in main, and rather than exiting, just pass. That way testers or external callers of scan_tcp can make their own decisions about how to handle keyboard interrupts.

Why build up a list of results and defer printing until the end? Why not print results as you find them?

\$\endgroup\$
1
  • \$\begingroup\$ thx for the advice. I build up a list and print at the end as otherwise The progress bar always gets interrupted so u have progressbar port x port y progressbar etc. and I didn't find that very pretty. \$\endgroup\$
    – George R
    Commented Sep 18, 2021 at 9:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.