Build a Network Reconnaissance Scanner

Scan your own machine for open ports, services, and security risks.

Introduction

30 Second Summary

Every computer connected to a network has thousands of virtual doors called ports. When those doors are left open and unguarded, attackers can walk right in.

In this project, you will build a network reconnaissance scanner in Python that discovers open ports on a target machine, identifies what services are running behind them, and generates a security report flagging risky exposures.

What You'll Build

A multithreaded TCP port scanner with banner grabbing and automated security assessment, built entirely with Python's standard library.

By the end of this project, you'll have:

  • A working multithreaded port scanner that discovers open ports on any host you own in seconds.
  • A banner grabbing module that fingerprints software versions running on open services.
  • A security report generator that flags risky ports with severity levels and explains why they are dangerous.
  • Secret Mission: Add a network sweep feature that discovers live hosts on your local network before scanning them.

Are there any prerequisites?

You need Python installed on your machine and a code editor like VS Code. This project uses only Python's built-in standard library modules, so there is nothing extra to install.

Before We Start

Every security assessment begins with reconnaissance. Before you can defend a network, you need to understand what is exposed on it.

In this step, you will set up your project environment and confirm your machine has open ports for your scanner to discover. This gives you a real target to work with throughout the project.

In this step, get ready to:
  • Verify Python 3.8 or higher is installed on your machine.
  • Create the project folder with your scanner and report files.
  • Confirm your machine has open ports that your scanner will discover.
Verify your Python version

Your scanner uses Python's built-in socket and concurrent.futures modules. These are included with any Python installation 3.8 or higher.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Check your Python version by running this command:
python --version

You should see a version number like Python 3.11.4 or similar. Any version 3.8 or higher works.

Seeing a version below 3.8 or an error?

If you see a version lower than 3.8, download the latest Python from python.org and install it. Make sure to check the box that says "Add Python to PATH" during installation.

If you see "command not found" or "not recognized", Python may not be in your system PATH. Reinstall from python.org and check the PATH option.

help me fix my Python version or installation

Create the project folder and files

Your project needs two files. scanner.py will contain the port scanner logic. report.py will generate a formatted security assessment.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir recon-scanner
cd recon-scanner
  • Create the two project files by running these commands:
New-Item -Path .\scanner.py -ItemType File
New-Item -Path .\report.py -ItemType File
  • Confirm the files exist by running:
ls

You should see scanner.py and report.py listed in the output.

Don't see both files?

If mkdir threw an error saying the folder already exists, delete it first with Remove-Item -Recurse recon-scanner and try again.

Make sure you ran cd recon-scanner before creating the files. The New-Item commands create files in your current directory.

help me create my project files in PowerShell

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the recon-scanner folder on your Desktop, then click Open.

You should see both scanner.py and report.py listed in the Explorer sidebar on the left.

Check for open ports on your machine

Before building the scanner, you need to confirm your machine actually has open ports to discover. This gives you a real target to test against.

  • Switch back to your PowerShell window.
  • View the ports currently open on your machine by running this command:
netstat -an | findstr LISTENING

You should see a list of lines showing TCP addresses with the state LISTENING. These are the open ports your scanner will find later.

What does this command do?

  • netstat -an shows all active network connections and listening ports on your machine in numeric form.
  • findstr LISTENING filters the output to only show ports that are actively waiting for incoming connections.

Common ports you might see include 135 (MSRPC) and 445 (SMB) on Windows machines. Your scanner will identify these automatically.

Not seeing any output?

Make sure you are running this in PowerShell or Command Prompt, not in a code editor terminal that might not have network access.

If you see an error about findstr not being recognized, you may be on macOS or Linux. Use netstat -an | grep LISTEN instead.

help me check for open ports on my machine

Your environment is ready. You have Python, your project files, and confirmed open ports to scan. Next up, you will write the core scanning function that connects to each port and checks if it is open.

Set Up Your Project

Before you write any scanning code, you need a working Python environment and a clean project structure. This step confirms Python is ready, creates your project files, and verifies that your machine has open ports to discover.

Once this setup is complete, you will have two dedicated files ready for the scanner logic and the report generator. You will also confirm that your Windows machine is actively listening on ports, giving your scanner something real to find.

In this step, get ready to:
  • Verify Python 3.8+ is installed on your machine.
  • Create the recon-scanner project folder with scanner.py and report.py.
  • Confirm your machine has listening ports ready for scanning.
Verify Python is installed and check your version

Your scanner uses only Python's built-in standard library modules, so no extra packages are needed. You just need Python 3.8 or higher installed on your machine.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Check your Python version by running this command:
python --version

You should see output like Python 3.11.4 or any version at 3.8 or above.

Not seeing a version number?

If you see an error instead of a version number, Python may not be installed or may not be added to your system PATH.

  • Try running python3 --version instead. Some systems use python3 as the command name.
  • If neither command works, download Python from python.org/downloads and make sure to check Add Python to PATH during installation.
  • After installing, close and reopen PowerShell, then try the command again.

help me fix my Python installation so I can run python --version

Create the project folder and files

Your project needs two files: scanner.py for the port scanner logic and report.py for the security report generator.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir recon-scanner
cd recon-scanner
  • Create the two project files by running this command:
New-Item scanner.py -ItemType File
New-Item report.py -ItemType File
  • Confirm the files exist by running:
ls

You should see scanner.py and report.py listed in the output.

  • Open the folder in your code editor by running:
code .

What if I use a different code editor?

The code . command opens the current folder in VS Code. If you use a different editor, open the recon-scanner folder on your Desktop manually through your editor's File menu.

Don't see both files in your editor?

  • Make sure you ran cd recon-scanner before creating the files. If you created them in the wrong folder, delete them and start from the cd ~/Desktop step again.
  • In VS Code, check that the Explorer sidebar shows recon-scanner as the workspace root with both files listed.

help me find my recon-scanner project files in my code editor

Verify your machine has scannable ports

Before building the scanner, you need to confirm that your machine actually has open ports for it to discover. Windows machines typically have several services listening on ports like 135, 445, and others.

  • Switch back to PowerShell and check which ports are currently open by running this command:
netstat -an | findstr LISTENING

You should see a list of lines showing TCP connections in a LISTENING state, each with a port number.

What does this output mean?

Each line represents a service running on your machine that is actively waiting for incoming connections. The numbers after the colon (like :135 or :445) are port numbers.

These are the exact ports your scanner will discover in the next steps. Professional security tools perform the same kind of discovery to find exposed services on a network.

Not seeing any LISTENING ports?

  • Make sure you typed the command exactly, including the pipe character | between netstat -an and findstr LISTENING.
  • Try running just netstat -an without the filter to see all connections.

help me see which ports are listening on my Windows machine using PowerShell

Your environment is ready and your machine has ports waiting to be discovered. Next up, you will write the core scanning function that probes these ports one by one.

Build the Basic Port Scanner

Your project is set up and ready for code. Now it's time to build the core of your reconnaissance tool: the port scanner itself.

Port scanning works by attempting a TCP connection to each port on a target machine. If the connection succeeds, the port is open and something is listening. Python's built-in socket module gives you everything you need to make these connections without installing anything extra.

In this step, get ready to:
  • Write a function that tests whether a single port is open or closed.
  • Build dictionaries that map port numbers to service names and security risks.
  • Run a sequential scan against your own machine and see real results.
Write the core scanning function

The foundation of any port scanner is a function that attempts to connect to a single port and reports whether it succeeded. You'll use socket.connect_ex() which returns 0 on a successful connection and a non-zero error code if the port is closed.

  • Add the following code to scanner.py to import the modules and create your scanning function:
import socket
import time


def scan_port(host, port, timeout=0.5):
    """Scan a single port using TCP connect. Returns True if open."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        sock.close()
        return result == 0
    except socket.error:
        return False

What does this code do?

  • socket.AF_INET tells Python to use IPv4 addressing.
  • socket.SOCK_STREAM creates a TCP socket (the type used for reliable connections).
  • sock.settimeout(timeout) ensures closed ports fail quickly instead of hanging for seconds.
  • connect_ex() returns 0 if the connection succeeds (port is open) and a non-zero error code if it fails (port is closed or filtered).
  • Save scanner.py.

You won't see any output yet. The next chunks build on this function to produce visible results.

Seeing a red underline on the import?

Some editors show warnings for unused imports. The time import will be used in a later step. You can safely ignore this warning for now.

help me understand why my editor is showing import warnings

Add service identification dictionaries

A port number alone doesn't tell you much. Real scanners map port numbers to service names so you can immediately see what's running.

  • Add the following COMMON_SERVICES dictionary below your imports (above the scan_port function) in scanner.py:
COMMON_SERVICES = {
    21: "FTP",
    22: "SSH",
    23: "Telnet",
    25: "SMTP",
    53: "DNS",
    80: "HTTP",
    110: "POP3",
    135: "MSRPC",
    139: "NetBIOS",
    143: "IMAP",
    443: "HTTPS",
    445: "SMB",
    993: "IMAPS",
    995: "POP3S",
    1433: "MSSQL",
    3306: "MySQL",
    3389: "RDP",
    5432: "PostgreSQL",
    8080: "HTTP-Proxy",
    8443: "HTTPS-Alt",
}

What does this dictionary do?

Each key is a well-known port number and each value is the standard service name. When your scanner finds an open port, it looks up the number here to tell you what service is likely running.

For example, port 443 maps to HTTPS because that's where web servers handle encrypted traffic. Port 3389 maps to RDP because that's the default port for Windows remote desktop.

  • Now add the RISKY_PORTS dictionary directly below COMMON_SERVICES:
RISKY_PORTS = {
    21: {"severity": "HIGH", "reason": "FTP sends credentials in plain text. Use SFTP instead."},
    23: {"severity": "HIGH", "reason": "Telnet is unencrypted. All data including passwords is visible on the network."},
    135: {"severity": "MEDIUM", "reason": "MSRPC is frequently targeted by worms and exploits."},
    139: {"severity": "MEDIUM", "reason": "NetBIOS can leak system information and is often exploited."},
    445: {"severity": "HIGH", "reason": "SMB should not be exposed publicly. Target of EternalBlue and ransomware."},
    1433: {"severity": "MEDIUM", "reason": "MSSQL should be restricted to trusted hosts only."},
    3306: {"severity": "MEDIUM", "reason": "MySQL should not be accessible from untrusted networks."},
    3389: {"severity": "HIGH", "reason": "RDP is a top target for brute-force attacks. Use VPN or restrict access."},
}

What does RISKY_PORTS do?

This dictionary flags ports that are especially dangerous when exposed. Each entry includes a severity level (HIGH or MEDIUM) and a reason explaining the risk.

Your scanner will use this later to generate security warnings. For now, it's data your report generator will rely on.

  • Save scanner.py.

You won't see output from these dictionaries yet. The scan loop in the next substep will use them to label open ports.

Getting a syntax error?

Make sure every dictionary entry ends with a comma, including the last one. Also check that the opening { is on the same line as the variable name, and the closing } is on its own line.

help me fix a syntax error in my Python dictionary

Write the sequential scan loop and test it

Now you'll connect everything together. The scan_host function loops through a range of ports, calls scan_port() on each one, and prints what it finds.

  • Add the following code below your scan_port function in scanner.py:
def scan_host(host, start_port=1, end_port=1024):
    """Scan a range of ports on a host sequentially."""
    print(f"\n[*] Scanning {host} (ports {start_port}-{end_port})...\n")
    open_ports = []
    for port in range(start_port, end_port + 1):
        if scan_port(host, port):
            service = COMMON_SERVICES.get(port, "Unknown")
            print(f"  [+] Port {port}/tcp OPEN ({service})")
            open_ports.append({"port": port, "service": service})
    return open_ports


if __name__ == "__main__":
    results = scan_host("127.0.0.1")

What does this code do?

  • scan_host() iterates through every port in the range and calls scan_port() on each one.
  • COMMON_SERVICES.get(port, "Unknown") looks up the service name for each open port, defaulting to "Unknown" if the port isn't in the dictionary.
  • if __name__ == "__main__" ensures the scan only runs when you execute the file directly, not when it's imported by another script.
  • "127.0.0.1" is the localhost address. You're scanning your own machine, which is always legal and safe.
  • Save scanner.py.
  • Run your scanner by entering this command in PowerShell:
python scanner.py

The scan will take a minute or two because it checks each port one at a time (1024 ports total). You should see output like this as open ports are discovered:

[*] Scanning 127.0.0.1 (ports 1-1024)...

  [+] Port 135/tcp OPEN (MSRPC)
  [+] Port 445/tcp OPEN (SMB)

What should I see?

You should see at least one or two open ports appear in the output. On Windows, ports 135 (MSRPC) and 445 (SMB) are almost always open because Windows uses them for system services.

The exact ports you see depend on what's running on your machine. If you have a web server or database installed, you might see those ports too.

Not seeing any open ports?

Make sure you're scanning 127.0.0.1 (localhost), not an external address. If no ports show up, your firewall might be blocking loopback connections. Try temporarily disabling Windows Firewall and running the scan again.

help me figure out why my port scanner isn't finding any open ports on localhost

✔️ Awesome, I've got everything!

Great. Make sure you've saved scanner.py and you can see open ports in your terminal output.

ⓧ I'd like to double check the full code

Here's the complete scanner.py at this point:

import socket
import time

COMMON_SERVICES = {
    21: "FTP",
    22: "SSH",
    23: "Telnet",
    25: "SMTP",
    53: "DNS",
    80: "HTTP",
    110: "POP3",
    135: "MSRPC",
    139: "NetBIOS",
    143: "IMAP",
    443: "HTTPS",
    445: "SMB",
    993: "IMAPS",
    995: "POP3S",
    1433: "MSSQL",
    3306: "MySQL",
    3389: "RDP",
    5432: "PostgreSQL",
    8080: "HTTP-Proxy",
    8443: "HTTPS-Alt",
}

RISKY_PORTS = {
    21: {"severity": "HIGH", "reason": "FTP sends credentials in plain text. Use SFTP instead."},
    23: {"severity": "HIGH", "reason": "Telnet is unencrypted. All data including passwords is visible on the network."},
    135: {"severity": "MEDIUM", "reason": "MSRPC is frequently targeted by worms and exploits."},
    139: {"severity": "MEDIUM", "reason": "NetBIOS can leak system information and is often exploited."},
    445: {"severity": "HIGH", "reason": "SMB should not be exposed publicly. Target of EternalBlue and ransomware."},
    1433: {"severity": "MEDIUM", "reason": "MSSQL should be restricted to trusted hosts only."},
    3306: {"severity": "MEDIUM", "reason": "MySQL should not be accessible from untrusted networks."},
    3389: {"severity": "HIGH", "reason": "RDP is a top target for brute-force attacks. Use VPN or restrict access."},
}


def scan_port(host, port, timeout=0.5):
    """Scan a single port using TCP connect. Returns True if open."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        sock.close()
        return result == 0
    except socket.error:
        return False


def scan_host(host, start_port=1, end_port=1024):
    """Scan a range of ports on a host sequentially."""
    print(f"\n[*] Scanning {host} (ports {start_port}-{end_port})...\n")
    open_ports = []
    for port in range(start_port, end_port + 1):
        if scan_port(host, port):
            service = COMMON_SERVICES.get(port, "Unknown")
            print(f"  [+] Port {port}/tcp OPEN ({service})")
            open_ports.append({"port": port, "service": service})
    return open_ports


if __name__ == "__main__":
    results = scan_host("127.0.0.1")

Your basic scanner is working and discovering real services on your machine. Next up, you'll add multithreading to scan all 1024 ports in seconds instead of minutes.

Add Multithreaded Scanning

Your scanner successfully found open ports on your machine. The problem is speed. Checking each port one at a time means scanning all 1024 ports can take several minutes because each closed port waits for the connection timeout before moving on.

In this step, you'll add multithreading to scan 100 ports simultaneously. This is the same technique professional scanners use to deliver results in seconds instead of minutes.

In this step, get ready to:
  • Replace your sequential scan loop with parallel multithreaded scanning.
  • Add timing and summary output to measure the speed improvement.
Import concurrent.futures and refactor for speed

Python's concurrent.futures module lets you run many tasks at the same time using a thread pool. Instead of scanning ports one by one, you'll submit all 1024 port-check tasks to a pool of 100 worker threads. The pool runs up to 100 checks simultaneously, and you collect results as they finish.

Why concurrent.futures instead of raw threading?

ThreadPoolExecutor gives you a clean, high-level API for parallelism. The older threading module requires you to manually create threads, manage queues, and handle coordination yourself.

concurrent.futures handles all of that in a few lines of code. It's the standard pattern used across production Python applications for I/O-bound work like network scanning.

  • In scanner.py, find your import lines at the top of the file.
  • Add this new import below import time:
from concurrent.futures import ThreadPoolExecutor, as_completed
  • Now find your existing scan_host function (the one with the sequential for loop) and replace the entire function with this multithreaded version:
def scan_host(host, start_port=1, end_port=1024, max_workers=100, timeout=0.5):
    """Scan a range of ports on a host using multiple threads."""
    print(f"\n[*] Scanning {host} (ports {start_port}-{end_port})...")
    print(f"[*] Using {max_workers} threads\n")

    open_ports = []
    start_time = time.time()

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        # Submit a scan_port task for every port in the range
        futures = {
            executor.submit(scan_port, host, port, timeout): port
            for port in range(start_port, end_port + 1)
        }

        # Collect results as each thread finishes
        for future in as_completed(futures):
            port = futures[future]
            try:
                if future.result():
                    service = COMMON_SERVICES.get(port, "Unknown")
                    print(f"  [+] Port {port}/tcp OPEN ({service})")
                    open_ports.append({"port": port, "service": service})
            except Exception:
                pass

    # Calculate elapsed time and sort results by port number
    elapsed = time.time() - start_time
    open_ports.sort(key=lambda x: x["port"])

    # Print scan summary
    print(f"\n[*] Scan complete: {end_port - start_port + 1} ports scanned in {elapsed:.2f} seconds")
    print(f"[*] {len(open_ports)} open port(s) found")

    return open_ports, elapsed

What does this code do?

  • ThreadPoolExecutor(max_workers=100) creates a pool of 100 threads ready to run tasks in parallel.
  • The dictionary comprehension uses executor.submit() to queue a scan_port call for every port in the range. Each key is a future (a pending result) and each value is the port number it belongs to.
  • as_completed(futures) yields futures as they finish, so you see results in real time instead of waiting for all 1024 ports to complete.
  • time.time() bookends the scan to measure how long it took. The elapsed time proves the speed improvement over the sequential version.
  • open_ports.sort() reorders results by port number since threads complete in unpredictable order.
  • Save scanner.py.

You can't run the scanner yet because the __main__ block still expects the old return value. The next substep fixes that.

Add progress feedback and result collection

The refactored scan_host now returns a tuple containing both the results list and the elapsed time. The __main__ block needs to unpack both values.

  • In scanner.py, scroll to the bottom and find this line in the if __name__ == "__main__": block:
    results = scan_host("127.0.0.1")
  • Replace it with this line, which unpacks both the results list and the elapsed time:
    results, elapsed = scan_host("127.0.0.1")
  • Save scanner.py.
  • Run the scanner by executing this command in PowerShell:
python scanner.py
  • Watch the output. You should see open ports printed as they're discovered, followed by a summary showing total ports scanned, open ports found, and elapsed time.

What should I see?

Your output will look something like this (exact ports and time vary by machine):

[*] Scanning 127.0.0.1 (ports 1-1024)...
[*] Using 100 threads

  [+] Port 135/tcp OPEN (MSRPC)
  [+] Port 445/tcp OPEN (SMB)

[*] Scan complete: 1024 ports scanned in 2.34 seconds
[*] 2 open port(s) found

The key improvement is that the scan now finishes in under 5 seconds instead of the several minutes the sequential version took.

Scanner still taking a long time?

  • Make sure you replaced the entire scan_host function, not just part of it. The old sequential for loop must be completely gone.
  • Check that your import line reads from concurrent.futures import ThreadPoolExecutor, as_completed exactly. A typo in the import causes a NameError: name 'ThreadPoolExecutor' is not defined.
  • If you see no output at all, make sure the __main__ block uses results, elapsed = scan_host("127.0.0.1") instead of the old single-variable assignment.

Why is my multithreaded scanner still running slowly?

✔️ Awesome, I've got everything!

Great. Double check you've saved scanner.py.

ⓧ I'd like to double check the full code

import socket
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

COMMON_SERVICES = {
    21: "FTP",
    22: "SSH",
    23: "Telnet",
    25: "SMTP",
    53: "DNS",
    80: "HTTP",
    110: "POP3",
    135: "MSRPC",
    139: "NetBIOS",
    143: "IMAP",
    443: "HTTPS",
    445: "SMB",
    993: "IMAPS",
    995: "POP3S",
    1433: "MSSQL",
    3306: "MySQL",
    3389: "RDP",
    5432: "PostgreSQL",
    8080: "HTTP-Proxy",
    8443: "HTTPS-Alt",
}

RISKY_PORTS = {
    21: {"severity": "HIGH", "reason": "FTP sends credentials in plain text. Use SFTP instead."},
    23: {"severity": "HIGH", "reason": "Telnet is unencrypted. All data including passwords is visible on the network."},
    135: {"severity": "MEDIUM", "reason": "MSRPC is frequently targeted by worms and exploits."},
    139: {"severity": "MEDIUM", "reason": "NetBIOS can leak system information and is often exploited."},
    445: {"severity": "HIGH", "reason": "SMB should not be exposed publicly. Target of EternalBlue and ransomware."},
    1433: {"severity": "MEDIUM", "reason": "MSSQL should be restricted to trusted hosts only."},
    3306: {"severity": "MEDIUM", "reason": "MySQL should not be accessible from untrusted networks."},
    3389: {"severity": "HIGH", "reason": "RDP is a top target for brute-force attacks. Use VPN or restrict access."},
}


def scan_port(host, port, timeout=0.5):
    """Scan a single port using TCP connect. Returns True if open."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        sock.close()
        return result == 0
    except socket.error:
        return False


def scan_host(host, start_port=1, end_port=1024, max_workers=100, timeout=0.5):
    """Scan a range of ports on a host using multiple threads."""
    print(f"\n[*] Scanning {host} (ports {start_port}-{end_port})...")
    print(f"[*] Using {max_workers} threads\n")

    open_ports = []
    start_time = time.time()

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(scan_port, host, port, timeout): port
            for port in range(start_port, end_port + 1)
        }

        for future in as_completed(futures):
            port = futures[future]
            try:
                if future.result():
                    service = COMMON_SERVICES.get(port, "Unknown")
                    print(f"  [+] Port {port}/tcp OPEN ({service})")
                    open_ports.append({"port": port, "service": service})
            except Exception:
                pass

    elapsed = time.time() - start_time
    open_ports.sort(key=lambda x: x["port"])

    print(f"\n[*] Scan complete: {end_port - start_port + 1} ports scanned in {elapsed:.2f} seconds")
    print(f"[*] {len(open_ports)} open port(s) found")

    return open_ports, elapsed


if __name__ == "__main__":
    results, elapsed = scan_host("127.0.0.1")

Your scanner now rips through 1024 ports in seconds. Next up, you'll add banner grabbing to identify what software is actually running behind those open ports.

Add Banner Grabbing

Your scanner finds open ports in seconds. But knowing a port is open only tells you there is a door. It does not tell you what is behind it.

In this step, you will add banner grabbing to your scanner. Banner grabbing connects to each open port and reads whatever the service sends back. This often reveals the software name and version running on that port.

In this step, get ready to:
  • Write a banner grabbing function that probes open ports and captures service responses.
  • Integrate banner grabbing into your scan results so each open port displays its software version.
Write the banner grabbing function

Some services announce themselves the moment you connect. Others require you to send a request first. Your banner grabber needs to handle both cases gracefully.

  • In scanner.py, find your scan_port() function. Add the following grab_banner() function directly below it (before scan_host()):
def grab_banner(host, port, timeout=2):
    """Attempt to grab a banner from an open port."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        sock.connect((host, port))

        # Send an HTTP probe for web ports
        if port in (80, 8080, 8443):
            sock.sendall(b"HEAD / HTTP/1.0\r\nHost: " + host.encode() + b"\r\n\r\n")

        # Read whatever the service sends back
        banner = sock.recv(1024).decode(errors="ignore").strip()
        sock.close()
        return banner[:200] if banner else None
    except (socket.timeout, socket.error, OSError):
        return None

What does this code do?

This function opens a fresh TCP connection to a specific port on the target host, with a 2-second timeout to avoid hanging on unresponsive services.

  • For web ports (80, 8080, 8443), it sends an HTTP HEAD request as a probe. Without this probe, web servers wait silently for a request and never send a banner.
  • sock.recv(1024) reads up to 1024 bytes of whatever the service responds with. The decode(errors="ignore") handles binary data gracefully without crashing.
  • The function returns the first 200 characters of the response, or None if the service does not send anything before the timeout.
  • The except block catches socket.timeout, socket.error, and OSError so that ports that refuse the banner grab do not crash the scanner.
  • Save scanner.py.

Seeing a syntax error when you save?

  • Make sure grab_banner() is at the same indentation level as scan_port() (no extra indent). Both are top-level functions.
  • Check that the except line uses parentheses around the three exception types: (socket.timeout, socket.error, OSError).

Still stuck? Why is my grab_banner function causing an error?

Integrate banner grabbing into the scan results

Now that you can grab banners from individual ports, you need to wire this into your existing scan_host() function so it runs automatically after every port scan completes.

  • In scanner.py, scroll to the scan_host() function and find this line:
    print(f"[*] {len(open_ports)} open port(s) found")
  • Add the following banner grabbing phase between that line and the return open_ports, elapsed statement:
    # Banner grabbing phase
    if open_ports:
        print("\n[*] Grabbing banners from open ports...")
        for entry in open_ports:
            banner = grab_banner(host, entry["port"])
            entry["banner"] = banner
            if banner:
                preview = banner.split("\n")[0][:80]
                print(f"  [+] Port {entry['port']}: {preview}")
            else:
                print(f"  [-] Port {entry['port']}: No banner received")

What does this code do?

  • After the port scan finishes, it checks whether any open ports were found. If so, it loops through each one and calls grab_banner().
  • The banner string gets stored in the result dictionary under a new "banner" key. This enriches each result with version information.
  • The preview is trimmed to the first line (max 80 characters) so the terminal output stays readable.
  • Ports that do not respond with a banner still get logged with a [-] marker. This is normal for services like MSRPC that do not send initial data.
  • Save scanner.py.
  • Run your scanner by executing this command in PowerShell:
python scanner.py

You should see open ports listed first, followed by a banner grabbing phase showing version strings next to each port. Some ports will show responses like HTTP headers. Others will show "No banner received" which is expected.

Not seeing any banner output?

  • Make sure the banner grabbing code is inside scan_host() and sits between the summary print lines and the return statement. If it is outside the function, it will not run.
  • Verify the indentation matches scan_host()'s body (one level inside the function, not inside the with block).
  • If all ports say "No banner received", that is still valid output. It means those services do not send banners without protocol-specific requests.

Need help? Why is my banner grabbing phase not printing anything?

✔️ Awesome, I've got everything!

Great. Double check you have saved scanner.py with both the new grab_banner() function and the banner grabbing phase inside scan_host().

ⓧ I'd like to double check the full code

Here is the complete scanner.py at this point:

import socket
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

COMMON_SERVICES = {
    21: "FTP",
    22: "SSH",
    23: "Telnet",
    25: "SMTP",
    53: "DNS",
    80: "HTTP",
    110: "POP3",
    135: "MSRPC",
    139: "NetBIOS",
    143: "IMAP",
    443: "HTTPS",
    445: "SMB",
    993: "IMAPS",
    995: "POP3S",
    1433: "MSSQL",
    3306: "MySQL",
    3389: "RDP",
    5432: "PostgreSQL",
    8080: "HTTP-Proxy",
    8443: "HTTPS-Alt",
}

RISKY_PORTS = {
    21: {"severity": "HIGH", "reason": "FTP sends credentials in plain text. Use SFTP instead."},
    23: {"severity": "HIGH", "reason": "Telnet is unencrypted. All data including passwords is visible on the network."},
    135: {"severity": "MEDIUM", "reason": "MSRPC is frequently targeted by worms and exploits."},
    139: {"severity": "MEDIUM", "reason": "NetBIOS can leak system information and is often exploited."},
    445: {"severity": "HIGH", "reason": "SMB should not be exposed publicly. Target of EternalBlue and ransomware."},
    1433: {"severity": "MEDIUM", "reason": "MSSQL should be restricted to trusted hosts only."},
    3306: {"severity": "MEDIUM", "reason": "MySQL should not be accessible from untrusted networks."},
    3389: {"severity": "HIGH", "reason": "RDP is a top target for brute-force attacks. Use VPN or restrict access."},
}


def scan_port(host, port, timeout=0.5):
    """Scan a single port using TCP connect. Returns True if open."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        sock.close()
        return result == 0
    except socket.error:
        return False


def grab_banner(host, port, timeout=2):
    """Attempt to grab a banner from an open port."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        sock.connect((host, port))

        if port in (80, 8080, 8443):
            sock.sendall(b"HEAD / HTTP/1.0\r\nHost: " + host.encode() + b"\r\n\r\n")

        banner = sock.recv(1024).decode(errors="ignore").strip()
        sock.close()
        return banner[:200] if banner else None
    except (socket.timeout, socket.error, OSError):
        return None


def scan_host(host, start_port=1, end_port=1024, max_workers=100, timeout=0.5):
    """Scan a range of ports on a host using multiple threads."""
    print(f"\n[*] Scanning {host} (ports {start_port}-{end_port})...")
    print(f"[*] Using {max_workers} threads\n")

    open_ports = []
    start_time = time.time()

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(scan_port, host, port, timeout): port
            for port in range(start_port, end_port + 1)
        }

        for future in as_completed(futures):
            port = futures[future]
            try:
                if future.result():
                    service = COMMON_SERVICES.get(port, "Unknown")
                    print(f"  [+] Port {port}/tcp OPEN ({service})")
                    open_ports.append({"port": port, "service": service})
            except Exception:
                pass

    elapsed = time.time() - start_time
    open_ports.sort(key=lambda x: x["port"])

    print(f"\n[*] Scan complete: {end_port - start_port + 1} ports scanned in {elapsed:.2f} seconds")
    print(f"[*] {len(open_ports)} open port(s) found")

    # Banner grabbing phase
    if open_ports:
        print("\n[*] Grabbing banners from open ports...")
        for entry in open_ports:
            banner = grab_banner(host, entry["port"])
            entry["banner"] = banner
            if banner:
                preview = banner.split("\n")[0][:80]
                print(f"  [+] Port {entry['port']}: {preview}")
            else:
                print(f"  [-] Port {entry['port']}: No banner received")

    return open_ports, elapsed


if __name__ == "__main__":
    results, elapsed = scan_host("127.0.0.1")

Your scanner now fingerprints the software behind every open port. Next up, you will build a report generator that turns these raw findings into a professional security assessment.

Generate the Security Report

Your scanner now discovers open ports, identifies services, and grabs banners to fingerprint software versions. That is a lot of raw data, but raw data is not actionable.

In this step, you will build a report generator that transforms scan results into a formatted security assessment. The report will flag risky ports by severity, display findings in a clean table, and provide recommendations.

In this step, get ready to:
  • Build a report generator that formats scan findings into a professional security assessment.
  • Add a main entry point that runs the full scan-to-report workflow.
  • Save the completed report to a text file.
Build the report generator in report.py

The report generator takes your scan results and presents them the way a professional security tool would. It includes a summary header, a ports table, a risk assessment, and actionable recommendations.

  • Switch to report.py in your code editor. Add the import and risk dictionary by pasting this code:
import datetime

# Ports that represent security risks and their severity levels
RISKY_PORTS = {
    21: {"severity": "HIGH", "reason": "FTP sends credentials in plain text. Use SFTP instead."},
    23: {"severity": "HIGH", "reason": "Telnet is unencrypted. All data including passwords is visible on the network."},
    135: {"severity": "MEDIUM", "reason": "MSRPC is frequently targeted by worms and exploits."},
    139: {"severity": "MEDIUM", "reason": "NetBIOS can leak system information and is often exploited."},
    445: {"severity": "HIGH", "reason": "SMB should not be exposed publicly. Target of EternalBlue and ransomware."},
    1433: {"severity": "MEDIUM", "reason": "MSSQL should be restricted to trusted hosts only."},
    3306: {"severity": "MEDIUM", "reason": "MySQL should not be accessible from untrusted networks."},
    3389: {"severity": "HIGH", "reason": "RDP is a top target for brute-force attacks. Use VPN or restrict access."},
}

What does this code do?

The datetime import lets you timestamp the report so you know when the scan was performed.

The RISKY_PORTS dictionary maps dangerous port numbers to their severity level and an explanation. The report generator uses this to flag findings that need attention.

  • Save report.py.

You will not see any visible output yet. The next chunk adds the function that uses this dictionary.

  • Below the RISKY_PORTS dictionary in report.py, add the report generator function by pasting this code:

def generate_report(results, target, elapsed):
    """Generate a formatted security assessment report."""
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    lines = []
    lines.append("\n" + "=" * 60)
    lines.append("  SECURITY ASSESSMENT REPORT")
    lines.append("=" * 60)
    lines.append(f"\n  Target:     {target}")
    lines.append(f"  Date:       {timestamp}")
    lines.append(f"  Scan Time:  {elapsed:.2f} seconds")
    lines.append(f"  Ports Found: {len(results)} open")

    # Open Ports Table
    lines.append("\n" + "-" * 60)
    lines.append("  OPEN PORTS")
    lines.append("-" * 60)
    lines.append(f"  {'PORT':<10}{'SERVICE':<15}{'BANNER'}")
    lines.append(f"  {'-'*8:<10}{'-'*12:<15}{'-'*30}")

    for entry in results:
        port_str = f"{entry['port']}/tcp"
        service = entry["service"]
        banner = entry.get("banner", "")
        banner_preview = banner.split("\n")[0][:35] if banner else "N/A"
        lines.append(f"  {port_str:<10}{service:<15}{banner_preview}")

    # Risk Assessment
    risks_found = []
    for entry in results:
        if entry["port"] in RISKY_PORTS:
            risk = RISKY_PORTS[entry["port"]]
            risks_found.append({
                "port": entry["port"],
                "service": entry["service"],
                "severity": risk["severity"],
                "reason": risk["reason"],
            })

    lines.append("\n" + "-" * 60)
    lines.append("  RISK ASSESSMENT")
    lines.append("-" * 60)

    if risks_found:
        for risk in sorted(risks_found, key=lambda x: x["severity"]):
            lines.append(f"\n  [{risk['severity']}] Port {risk['port']} ({risk['service']})")
            lines.append(f"         {risk['reason']}")
    else:
        lines.append("\n  No high-risk ports detected.")

    # Recommendations
    lines.append("\n" + "-" * 60)
    lines.append("  RECOMMENDATIONS")
    lines.append("-" * 60)

    if risks_found:
        lines.append("\n  1. Review all HIGH severity findings and close or restrict access")
        lines.append("  2. Ensure firewalls block unnecessary inbound connections")
        lines.append("  3. Replace unencrypted protocols (FTP, Telnet) with secure alternatives")
        lines.append("  4. Keep all services updated to patch known vulnerabilities")
        lines.append("  5. Monitor open ports regularly for unauthorized changes")
    else:
        lines.append("\n  No critical issues found. Continue monitoring regularly.")

    lines.append("\n" + "=" * 60)
    lines.append("  END OF REPORT")
    lines.append("=" * 60 + "\n")

    return "\n".join(lines)

What does this function do?

The generate_report() function takes the scan results, target IP, and elapsed time, then builds a multi-section text report.

  • The Open Ports Table uses f-string alignment (:<10, :<15) to create evenly spaced columns.
  • The Risk Assessment section cross-references each open port against RISKY_PORTS and displays a severity tag like [HIGH] or [MEDIUM] for each flagged port.
  • The Recommendations section provides five prioritized steps if risky ports are found, or an all-clear message if none are detected.
  • Save report.py.

You will not see output yet. The next substep wires this function into your scanner so you can run the full workflow.

Seeing a syntax error when you save?

Check that you have a blank line between the end of the RISKY_PORTS dictionary and the def generate_report line. Python requires a newline between top-level definitions.

Also confirm that all f-string braces use matching opening and closing {} pairs.

Help me debug a syntax error in my report.py file

✔️ Awesome, I've got everything!

Great. Double check you have saved report.py.

ⓧ I'd like to double check the full code

import datetime

RISKY_PORTS = {
    21: {"severity": "HIGH", "reason": "FTP sends credentials in plain text. Use SFTP instead."},
    23: {"severity": "HIGH", "reason": "Telnet is unencrypted. All data including passwords is visible on the network."},
    135: {"severity": "MEDIUM", "reason": "MSRPC is frequently targeted by worms and exploits."},
    139: {"severity": "MEDIUM", "reason": "NetBIOS can leak system information and is often exploited."},
    445: {"severity": "HIGH", "reason": "SMB should not be exposed publicly. Target of EternalBlue and ransomware."},
    1433: {"severity": "MEDIUM", "reason": "MSSQL should be restricted to trusted hosts only."},
    3306: {"severity": "MEDIUM", "reason": "MySQL should not be accessible from untrusted networks."},
    3389: {"severity": "HIGH", "reason": "RDP is a top target for brute-force attacks. Use VPN or restrict access."},
}


def generate_report(results, target, elapsed):
    """Generate a formatted security assessment report."""
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    lines = []
    lines.append("\n" + "=" * 60)
    lines.append("  SECURITY ASSESSMENT REPORT")
    lines.append("=" * 60)
    lines.append(f"\n  Target:     {target}")
    lines.append(f"  Date:       {timestamp}")
    lines.append(f"  Scan Time:  {elapsed:.2f} seconds")
    lines.append(f"  Ports Found: {len(results)} open")

    # Open Ports Table
    lines.append("\n" + "-" * 60)
    lines.append("  OPEN PORTS")
    lines.append("-" * 60)
    lines.append(f"  {'PORT':<10}{'SERVICE':<15}{'BANNER'}")
    lines.append(f"  {'-'*8:<10}{'-'*12:<15}{'-'*30}")

    for entry in results:
        port_str = f"{entry['port']}/tcp"
        service = entry["service"]
        banner = entry.get("banner", "")
        banner_preview = banner.split("\n")[0][:35] if banner else "N/A"
        lines.append(f"  {port_str:<10}{service:<15}{banner_preview}")

    # Risk Assessment
    risks_found = []
    for entry in results:
        if entry["port"] in RISKY_PORTS:
            risk = RISKY_PORTS[entry["port"]]
            risks_found.append({
                "port": entry["port"],
                "service": entry["service"],
                "severity": risk["severity"],
                "reason": risk["reason"],
            })

    lines.append("\n" + "-" * 60)
    lines.append("  RISK ASSESSMENT")
    lines.append("-" * 60)

    if risks_found:
        for risk in sorted(risks_found, key=lambda x: x["severity"]):
            lines.append(f"\n  [{risk['severity']}] Port {risk['port']} ({risk['service']})")
            lines.append(f"         {risk['reason']}")
    else:
        lines.append("\n  No high-risk ports detected.")

    # Recommendations
    lines.append("\n" + "-" * 60)
    lines.append("  RECOMMENDATIONS")
    lines.append("-" * 60)

    if risks_found:
        lines.append("\n  1. Review all HIGH severity findings and close or restrict access")
        lines.append("  2. Ensure firewalls block unnecessary inbound connections")
        lines.append("  3. Replace unencrypted protocols (FTP, Telnet) with secure alternatives")
        lines.append("  4. Keep all services updated to patch known vulnerabilities")
        lines.append("  5. Monitor open ports regularly for unauthorized changes")
    else:
        lines.append("\n  No critical issues found. Continue monitoring regularly.")

    lines.append("\n" + "=" * 60)
    lines.append("  END OF REPORT")
    lines.append("=" * 60 + "\n")

    return "\n".join(lines)
Add the main entry point that ties everything together

Right now, the __main__ block in scanner.py only runs a scan. You need to update it so it prompts for a target, runs the scan, and then feeds the results into your new report generator.

  • Switch to scanner.py in your editor.
  • Scroll to the bottom and find the existing if __name__ == "__main__": block:
if __name__ == "__main__":
    results, elapsed = scan_host("127.0.0.1")
  • Delete those lines and replace the entire block with this code:
if __name__ == "__main__":
    from report import generate_report

    # Print a header banner
    print("=" * 60)
    print("  NETWORK RECONNAISSANCE SCANNER")
    print("=" * 60)

    # Prompt user for target, defaulting to localhost
    target = input("\nEnter target IP [127.0.0.1]: ").strip()
    if not target:
        target = "127.0.0.1"

    # Run the scan and generate the report
    results, elapsed = scan_host(target)

    report_text = generate_report(results, target, elapsed)
    print(report_text)

What does this code do?

  • from report import generate_report imports your report module. Placing it inside the __main__ block means it only runs when you execute the script directly.
  • input() prompts the user for a target IP. If they press Enter without typing anything, the default 127.0.0.1 is used.
  • The results from scan_host() flow directly into generate_report(), which returns the formatted text and prints it to your terminal.
  • Save scanner.py.
  • Run the scanner by executing this in PowerShell:
python scanner.py
  • When prompted for a target IP, press Enter to accept the default (127.0.0.1).

You should see a full security assessment report printed in your terminal, including the scan summary, an open ports table, a risk assessment with severity tags, and recommendations.

Seeing a ModuleNotFoundError?

Make sure you are running the command from inside the recon-scanner folder where both scanner.py and report.py live. Python needs both files in the same directory to import one from the other.

Help me fix a ModuleNotFoundError when running my scanner

Save the report to a file

A printed report disappears when you close the terminal. Adding a file-save option gives you a permanent artifact you can reference later or share in a professional context.

  • In scanner.py, find the last line of your __main__ block (print(report_text)).
  • Add the following code directly below it, at the same indentation level:
    save = input("\nSave report to scan_report.txt? [y/N]: ").strip().lower()
    if save == "y":
        with open("scan_report.txt", "w") as f:
            f.write(report_text)
        print("[*] Report saved to scan_report.txt")

What does this code do?

After printing the report, the script asks whether to save it. If the user types y, it writes the full report text to scan_report.txt in the project folder. The [y/N] notation signals that the default answer is No (just pressing Enter skips the save).

  • Save scanner.py.
  • Run the scanner again:
python scanner.py
  • Press Enter to accept the default target.
  • When the report finishes, type y and press Enter to save.
  • Confirm the file was created by running:
ls

You should see scan_report.txt listed alongside scanner.py and report.py.

Don't see scan_report.txt?

Make sure you typed y (lowercase) when prompted. Any other input, including just pressing Enter, skips the save.

Also confirm your terminal is in the recon-scanner folder. The file is written to whatever directory you ran the script from.

Help me figure out why scan_report.txt isn't appearing

✔️ Awesome, I've got everything!

Great. Double check you have saved both scanner.py and report.py.

ⓧ I'd like to double check the full code

import socket
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

COMMON_SERVICES = {
    21: "FTP",
    22: "SSH",
    23: "Telnet",
    25: "SMTP",
    53: "DNS",
    80: "HTTP",
    110: "POP3",
    135: "MSRPC",
    139: "NetBIOS",
    143: "IMAP",
    443: "HTTPS",
    445: "SMB",
    993: "IMAPS",
    995: "POP3S",
    1433: "MSSQL",
    3306: "MySQL",
    3389: "RDP",
    5432: "PostgreSQL",
    8080: "HTTP-Proxy",
    8443: "HTTPS-Alt",
}

RISKY_PORTS = {
    21: {"severity": "HIGH", "reason": "FTP sends credentials in plain text. Use SFTP instead."},
    23: {"severity": "HIGH", "reason": "Telnet is unencrypted. All data including passwords is visible on the network."},
    135: {"severity": "MEDIUM", "reason": "MSRPC is frequently targeted by worms and exploits."},
    139: {"severity": "MEDIUM", "reason": "NetBIOS can leak system information and is often exploited."},
    445: {"severity": "HIGH", "reason": "SMB should not be exposed publicly. Target of EternalBlue and ransomware."},
    1433: {"severity": "MEDIUM", "reason": "MSSQL should be restricted to trusted hosts only."},
    3306: {"severity": "MEDIUM", "reason": "MySQL should not be accessible from untrusted networks."},
    3389: {"severity": "HIGH", "reason": "RDP is a top target for brute-force attacks. Use VPN or restrict access."},
}


def scan_port(host, port, timeout=0.5):
    """Scan a single port using TCP connect. Returns True if open."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        sock.close()
        return result == 0
    except socket.error:
        return False


def grab_banner(host, port, timeout=2):
    """Attempt to grab a banner from an open port."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        sock.connect((host, port))

        if port in (80, 8080, 8443):
            sock.sendall(b"HEAD / HTTP/1.0\r\nHost: " + host.encode() + b"\r\n\r\n")

        banner = sock.recv(1024).decode(errors="ignore").strip()
        sock.close()
        return banner[:200] if banner else None
    except (socket.timeout, socket.error, OSError):
        return None


def scan_host(host, start_port=1, end_port=1024, max_workers=100, timeout=0.5):
    """Scan a range of ports on a host using multiple threads."""
    print(f"\n[*] Scanning {host} (ports {start_port}-{end_port})...")
    print(f"[*] Using {max_workers} threads\n")

    open_ports = []
    start_time = time.time()

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(scan_port, host, port, timeout): port
            for port in range(start_port, end_port + 1)
        }

        for future in as_completed(futures):
            port = futures[future]
            try:
                if future.result():
                    service = COMMON_SERVICES.get(port, "Unknown")
                    print(f"  [+] Port {port}/tcp OPEN ({service})")
                    open_ports.append({"port": port, "service": service})
            except Exception:
                pass

    elapsed = time.time() - start_time
    open_ports.sort(key=lambda x: x["port"])

    print(f"\n[*] Scan complete: {end_port - start_port + 1} ports scanned in {elapsed:.2f} seconds")
    print(f"[*] {len(open_ports)} open port(s) found")

    # Banner grabbing phase
    if open_ports:
        print("\n[*] Grabbing banners from open ports...")
        for entry in open_ports:
            banner = grab_banner(host, entry["port"])
            entry["banner"] = banner
            if banner:
                preview = banner.split("\n")[0][:80]
                print(f"  [+] Port {entry['port']}: {preview}")
            else:
                print(f"  [-] Port {entry['port']}: No banner received")

    return open_ports, elapsed


if __name__ == "__main__":
    from report import generate_report

    print("=" * 60)
    print("  NETWORK RECONNAISSANCE SCANNER")
    print("=" * 60)

    target = input("\nEnter target IP [127.0.0.1]: ").strip()
    if not target:
        target = "127.0.0.1"

    results, elapsed = scan_host(target)

    report_text = generate_report(results, target, elapsed)
    print(report_text)

    save = input("\nSave report to scan_report.txt? [y/N]: ").strip().lower()
    if save == "y":
        with open("scan_report.txt", "w") as f:
            f.write(report_text)
        print("[*] Report saved to scan_report.txt")

Secret mission

Add Network Host Discovery

Your scanner can probe a single host, but real penetration testers need to find all live machines on a network first. Add a host discovery feature that sweeps an entire subnet using ping, then automatically scans every host it finds.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your files for future experimentation or delete them entirely.

Resources you used:

  • scanner.py - your multithreaded port scanner with banner grabbing and host discovery (local file, no cost).
  • report.py - your security report generator (local file, no cost).
  • scan_report.txt - the generated security assessment report (local file, no cost).

Keep everything running

No action needed. Choose this if you want to keep building on your scanner or use it to scan other hosts you own.

  • Your recon-scanner folder stays on your Desktop with all three files intact.
  • You can run python scanner.py anytime to scan a new target.
  • Python remains installed on your machine (it was already there before this project).

Pause - I'll come back to this later

Since this project is entirely local files, there is nothing to pause. Your files sit on disk using negligible space until you return.

  • Close the recon-scanner folder in your code editor to free up a tab.
  • Your files will be exactly as you left them when you come back.

Delete - I don't want to use this again

Remove all project files from your machine. This deletes everything you built in this project.

  • Close the recon-scanner folder in your code editor.
  • Delete the project folder by running the command for your operating system below.

macOS

  • Delete the project folder by running:
rm -rf ~/Desktop/recon-scanner

Windows

  • Delete the project folder by running:
rmdir /s /q $HOME\Desktop\recon-scanner

That removes scanner.py, report.py, and scan_report.txt along with the recon-scanner folder.

Nice Work!

Nice Work!

You just built a network reconnaissance scanner from scratch using nothing but Python's standard library. This is the same core workflow that professional penetration testers use in the first phase of every security assessment.

You've learned how to:

  • Built a multithreaded TCP port scanner using Python's socket module and ThreadPoolExecutor to scan 1024 ports in seconds.
  • Implemented banner grabbing to fingerprint software versions running on open services and identify what is exposed.
  • Created a security report generator that flags risky ports with severity levels and produces actionable recommendations.
  • Secret Mission: Added network host discovery to sweep an entire subnet and map live hosts before scanning them.

Ready to quiz yourself?